Generic functions and multiple dispatch

We have already seen that functions are inherently defined as generic, that is, they can be used for different types of their arguments. The compiler will generate a separate version of the function each time it is called with arguments of a new type. In Julia, a concrete version of a function for a specific combination of argument types is called a method. To define a new method for a function (also called overloading), just use the same function name but a different signature, that is, with different argument types. A list of all the methods is stored in a virtual method table (vtable) on the function itself; methods do not belong to a particular type. When a function is called, Julia will lookup in vtable at runtime to find which concrete method it should call, based on the types of all its arguments; this is Julia's multiple dispatch mechanism, which Python, C++, or Fortran do not implement this. It allows open extensions where normal object-oriented code would have forced you to change a class or subclass to an existing class and thus change your library. Note that only positional arguments are taken into account for multiple dispatch, and not keyword arguments.

For each of these different methods, specialized low-level code is generated, targeted to the processor's instruction set. In contrast to object-oriented (OO) languages, vtable is stored in the function, and not in the type (or class). In OO languages, a method is called on a single object, object.method(), which is generally called single dispatch. In Julia, one can say that a function belongs to multiple types, or that a function is specialized or overloaded for different types. Julia's ability to compile code that reads like a high-level dynamic language into machine code that performs almost entirely like C is derived from its ability to do multiple dispatch.

To make this idea more concrete, a function such as square(x) = x * x actually defines a potentially infinite family of methods, one for each of the possible types of the argument x. For example, square(2) will call a specialized method that uses the CPU's native integer multiplication instruction, whereas square(2.0) will use the CPU's native floating point multiplication instruction.

Let's see multiple dispatch in action. We will define a function f that takes two arguments n and m returning a string, but, in some methods, the type of n or m, or both, is annotated. (Number is a supertype of Integer, refer to the The type hierarchy – subtypes and supertypes section in Chapter 6, More on Types, Methods, and Modules.) This can be seen in the following example:

f(n, m) = "base case" 
f(n::Number, m::Number) = "n and m are both numbers"
f(n::Number, m) = "n is a number"
f(n, m::Number) = "m is a number"
f(n::Integer, m::Integer) = "n and m are both integers"

This returns f (generic function with 5 methods).

When n and m have no type, as in "base case", then their type is Any, the supertype of all types. Let's take a look at how the most appropriate method is chosen in each of the following function calls:

  • f(1.5, 2) returns n and m are both numbers
  • f(1, "bar") returns n is a number
  • f(1, 2) returns n and m are both integers
  • f("foo", [1,2]) returns base case

Calling f(n, m) will never result in an error, because if no other method matches, the base case will be invoked when we add a new method:

f(n::Float64, m::Integer) = "n is a float and m is an integer"

So, the call to f(1.5,2) now returns n is a float and m is an integer.

To get a quick overview of all the versions of a function, type methods(fname) into the REPL. For example, methods(+) shows a listing of 174 methods for a generic function +:

+(x::Bool) at bool.jl:36 
+(x::Bool,y::Bool) at bool.jl:39
...
+(a,b,c) at operators.jl:82
+(a,b,c,xs...) at operators.jl:83

You can even take a look in the source code at how they are defined, as in base/bool.jl in the local Julia installation or at https://github.com/JuliaLang/julia/blob/master/base/bool.jl, where we can see the addition of Bool variables equal to the addition of integers: +(x::Bool, y::Bool) = int(x) + int(y), where int(false) is 0 and int(true) is 1.

As a second example, methods(sort) shows # 6 methods for the generic function "sort".

The macro @which gives you the exact method that is used and where in the source code that method is defined, for example, @which 2 * 2 returns *(x::Int64, y::Int64) at int.jl:47. This also works the other way around. If you want to know which methods are defined for a certain type, or use that type, ask methodswith(Type) from the InteractiveUtils module. For example, here is a part of the output of InteractiveUtils .methodswith(String):

[18] getindex(s::String, r::UnitRange{Int64}) in Base at strings/string.jl:240 
[19] getindex(s::String, i::Int64) in Base at strings/string.jl:205
[20] getindex(s::String, r::UnitRange{#s56} where #s56<:Integer) in Base at strings/string.jl:237 ...

As already noted, type stability is crucial for optimal performance. A function is type-stable if the return type(s) of all the output variables can be deduced from the types of the inputs. So try to design your functions with type stability in mind.

Some crude performance measurements (execution time and memory used) on the execution of functions can be obtained from the macro @time, for example:

@time fib(35) 
elapsed time: 0.115188593 seconds (6756 bytes allocated) 9227465

@elapsed only returns the execution time. @elapsed fib(35) returns 0.115188593.

In Julia, the first call of a method invokes the LLVM JIT compiler backend (refer to the How Julia works section in Chapter 1, Installing the Julia Platform), to emit machine code for it, so this warm-up call will take a bit longer. Start timing or benchmarking from the second call onward, after doing a dry run.

When writing a program with Julia, first write an easy version that works. Then, if necessary, improve the performance of that version by profiling it and then fixing performance bottlenecks. We'll come back to performance measurements in the Performance tips section of Chapter 9, Running External Programs.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.217.206.112