When you write typeof(foo) you are asking about the type of function foo. This function can have multiple methods (type stable or not --- but this is another issue), and these methods have different signatures (types of arguments) and for some of them the compiler might be able to infer the return type while for others it might not (and you should not rely on it AFAICT --- just assume that most of the time the compiler does the right job).
To give an example consider this code (1 function, 2 methods):
julia> f1(::Int) = 1
f1 (generic function with 1 method)
julia> f1(::Bool) = 2
f1 (generic function with 2 methods)
julia> typeof(f1)
typeof(f1)
julia> methods(f1)
# 2 methods for generic function "f1":
[1] f1(::Bool) in Main at REPL[21]:1
[2] f1(::Int64) in Main at REPL[20]:1
Now referring to return value specifications like you have written:
f(x)::Float64 = x
they are not mere assertions. Julia actually does two things:
- it converts the return value to the requested type
- does assertion that the conversion succeeded
This is relevant as e.g. in my function f above you can write:
julia> f(1)
1.0
julia> f(true)
1.0
and you can see that the conversion (not only assertion) takes place.
This style is quite relevant in type unstable code (e.g. when using DataFrame that is a type unstable data structure) as such assertions can help "break the type instability chain" in your code (if you have some part of it that is not type stable).
EDIT
Could this not be resolved by using Union{Int, Bool} -> Int? for f1, for example? And similarly for type instability? I guess that would require the code to be compiled for all input types and therefore lose the advantages of JIT?
But what if for Int an Int were returned and for Bool a Bool were returned? Consider e.g. an identity function.
Also take the following example:
julia> f(x) = ("a",)[x]
f (generic function with 1 method)
julia> @code_warntype f(2)
Variables
#self#::Core.Compiler.Const(f, false)
x::Int64
Body::String
1 ─ %1 = Core.tuple("a")::Core.Compiler.Const(("a",), false)
│ %2 = Base.getindex(%1, x)::String
└── return %2
julia> Base.return_types(f)
1-element Array{Any,1}:
Any
@code_warntype correctly identifies that if this function returns something it is guaranteed to be String, however, return_types as Przemysław suggested tells you that it is Any. So you can see that this is a hard thing and you should not rely on it blindly - just assume that it is up to the compiler to decide what it can infer. In particular - for performance reasons - the compiler might give up doing the inference even if in theory it would be possible.
In your question you are probably referring to what e.g. Haskell provides, but in Haskell there is a restriction that the return value of the function may not depend on runtime values of the arguments passed, but rather only on the types of arguments. There is no such restriction in Julia.