When you call any function, a specialized version of that function is compiled given the argument types. This is true regardless of the declared argument types, and in fact is true even if you declared no types at all. For example, if you declared
h(x) = f(x) it would do the same thing.
In this case, when you call
g(0) you are passing an
0 on a 64-bit machine). The compiler says, "okay,
x is an
Int64, now specialize everything in
g for that type of
x. It looks inside
g and finds a call to
f(x) and thinks "aha, this is calling the
f(x::Int64) method, which returns an
Int so the return type of
g(::Int64) is also
Int64. Also, it notices that
f(x::Int64) is really short, so it inlines it. The result is the one-line compiled code you saw.
If you subsequently call
g(Int32(0)), then it will compile a second version of
g, this one specialized for
Int32 (so it will end up being hard-coded to return
32). These two compiled versions of
g will stay in memory, and will be called whenever
g is called with arguments of those types.
Dynamic (runtime) dispatch only occurs when the compiler can’t figure out the types. For example, if you do
map(g, Any[1,2,Int32(3),4]), where it is applying
g to an array of
Any and therefore doesn’t know the type of
x until runtime, it will compile a generic version of
g that dispatches at runtime to the correct version of
This kind of aggressive type specialization and type inference is the key strength of Julia. It allows you to write really generic code that works for any type, and doesn’t declare any types, and yet which is at the same time fast (because it is specialized when the code calling it is compiled).