fundispatch

This shows you the differences between two versions of the page.

— |
fundispatch [2018/12/27 14:38] (current) |
||
---|---|---|---|

Line 1: | Line 1: | ||

+ | |||

+ | ~~CLOSETOC~~ | ||

+ | |||

+ | ~~TOC 1-3 wide~~ | ||

+ | |||

+ | |||

+ | ```juliarepl | ||

+ | julia> pkgchk.( [ "julia" => v"1.0.3" ] ); | ||

+ | ``` | ||

+ | |||

+ | |||

+ | |||

+ | # Function Argument Dispatch | ||

+ | |||

+ | * Recall that chapter [[inquiring#Functions]] described how to interrogate functions. | ||

+ | |||

+ | |||

+ | Julia is built around the idea that one may want to define many functions with the same name but differently typed arguments. Ergo, a function like `sqr` (square) could be defined one way for integers, another way for Float64, another for Float32, another for real/imaginary numbers, and yet another for a user structure named `BuildingVector` (or whatever). The compiler will search for the most appropriately matched function, and invoke it. | ||

+ | |||

+ | This is called **generic programming**, and nowadays prominently employed by the C++ STL. Originally, C++ was an object-oriented programming language. With the arrival of its STL, everything changed. One could now easily design functions that would work, e.g., with stacks of objects, regardless of what type the objects were. One did not have to write one search function for strings, another for integers, and so on (or worse, pass around a void pointer to arbitrary memory with the hope that the program would remember what type the pointer was about). While generic programming was a "bolt-on" to C++, it was a first-order design feature for Julia. | ||

+ | |||

+ | ## Writing Generic or Specific Functions? | ||

+ | |||

+ | When should user functions be generic (applicable to any type that the user throws at them), and when should they be specific (applicable only to specific types)? My answer here is that they should be as specific as required, but no more. This is because it helps catch errors earlier and it helps the compiler create faster code. | ||

+ | |||

+ | For example, on one end of the spectrum, your program could define a generic function and it would work: | ||

+ | |||

+ | ```juliarepl | ||

+ | julia> f(x,y)= sqrt( x^2 + y^2 ) ## (Any, Any) -> Any | ||

+ | f (generic function with 1 method) | ||

+ | |||

+ | ``` | ||

+ | |||

+ | Alas, because you may only want this function to work with `Float64` in your own application (to make sure the program does not accidentally lose precision!). In this case, your user program can define | ||

+ | |||

+ | ```juliarepl | ||

+ | julia> f(x::Float64,y::Float64)::Float64= sqrt( x^2 + y^2 ) ## (Float64,Float64) -> Float64 | ||

+ | f (generic function with 1 method) | ||

+ | |||

+ | ``` | ||

+ | |||

+ | Now you will get compile-time**(!)** errors if you try to invoke `f` with any Float32, an integer, character, complex number, etc.---or if you accidentally assigned the result to an integer variable (both inside the function and in the function call). Specificity also eliminates the need for the compiler to determine whether it needs to execute a run-time dispatch or whether a faster compile-time dispatch will do. | ||

+ | |||

+ | On the other end of the spectrum, generics can be very useful, too. For example, you may want to define a function | ||

+ | |||

+ | ```juliarepl | ||

+ | julia> p(x)= println("Your progam used $x"); ## (Any) -> Nothing | ||

+ | |||

+ | ``` | ||

+ | |||

+ | This `p` is a function that you may want to use with all sorts of different input types. In this case, the generic functionality is great. | ||

+ | |||

+ | Genericism tends to be more useful when writing packages that will be used and reused as inputs elsewhere for all sorts of applications that the package author cannot foresee. Specificity tends to be more useful when writing one's own (final) application. | ||

+ | |||

+ | |||

+ | The middle of the spectrum is often the right location. You may want | ||

+ | |||

+ | ```juliarepl | ||

+ | julia> f(x::AbstractFloat,y::AbstractFloat)::AbstractFloat= sqrt( x^2 + y^2 ); ## (Float,Float) -> Float | ||

+ | |||

+ | julia> g(x::Vector)::Vector= sort( g ); | ||

+ | |||

+ | ``` | ||

+ | |||

+ | (The latter function could be even more specific by declaring that the element types of `x` will also be the element types of the function output. For example, | ||

+ | |||

+ | ```juliarepl | ||

+ | julia> function g(x::Vector{T})::Vector{T} where T <: Number; sort( g ); end#function## | ||

+ | g (generic function with 1 method) | ||

+ | |||

+ | ``` | ||

+ | |||

+ | |||

+ | IMPORTANT In sum, although type specificity is optional for function arguments and return values, it is almost always better for end users to be specific. Whenever you can, type-define your function arguments. | ||

+ | |||

+ | |||

+ | |||

+ | ## The Return Type is *Not* a Dispatch Selector | ||

+ | |||

+ | WARNING A caller cannot request a specific dispatch based on the return type. | ||

+ | |||

+ | ```juliarepl | ||

+ | julia> function g( a::Int )::Int 3 end;##function ## g() barfs when not fed an Int | ||

+ | |||

+ | julia> f()::Int=1 ## f() returns an Int, ... briefly until | ||

+ | f (generic function with 1 method) | ||

+ | |||

+ | julia> f()::AbstractFloat= 1.0 ## no more. this f() s not a second method, but overwrites f() | ||

+ | f (generic function with 1 method) | ||

+ | |||

+ | julia> g( f() ) ## ergo, g() now receives a float function and value | ||

+ | ERROR: MethodError: no method matching g(::Float64) | ||

+ | Closest candidates are: | ||

+ | g(!Matched::Int64) at none:1 | ||

+ | Stacktrace: | ||

+ | |||

+ | ``` | ||

+ | |||

+ | |||

+ | |||

+ | ## Declaring Return Types | ||

+ | |||

+ | The return type can be forced not only onto the function declaration, but also on the return statement. Again, it can help catch bugs early, sometimes even at compile time: | ||

+ | |||

+ | ```juliarepl | ||

+ | julia> f()::Int= 42 ## return value is Int | ||

+ | f (generic function with 1 method) | ||

+ | |||

+ | julia> meaningoflife= f() ## ok | ||

+ | 42 | ||

+ | |||

+ | julia> y= Vector{Int}[ 1.0, f() ] ## user bug, because f() did not give an Int. hail the compiler. | ||

+ | ERROR: MethodError: Cannot `convert` an object of type Float64 to an object of type Array{Int64,1} | ||

+ | Closest candidates are: | ||

+ | convert(::Type{T<:Array}, !Matched::AbstractArray) where T<:Array at array.jl:489 | ||

+ | convert(::Type{T<:AbstractArray}, !Matched::T<:AbstractArray) where T<:AbstractArray at abstractarray.jl:14 | ||

+ | convert(::Type{T<:AbstractArray}, !Matched::LinearAlgebra.Factorization) where T<:AbstractArray at /Users/osx/buildbot/slave/package_osx64/build/usr/share/julia/stdlib/v1.0/LinearAlgebra/src/factorization.jl:46 | ||

+ | ... | ||

+ | Stacktrace: | ||

+ | |||

+ | ``` | ||

+ | |||

+ | Here is another bug catcher: | ||

+ | |||

+ | ```juliarepl | ||

+ | julia> f()::Int= sqrt(12.0) | ||

+ | f (generic function with 1 method) | ||

+ | |||

+ | julia> f() | ||

+ | ERROR: InexactError: Int64(Int64, 3.4641016151377544) | ||

+ | Stacktrace: | ||

+ | |||

+ | ``` | ||

+ | |||

+ | Now here is a reallly bad function. | ||

+ | |||

+ | ```juliarepl | ||

+ | julia> x=12; g()= x::Float64 ## compiler infers that return value is Float64 | ||

+ | g (generic function with 1 method) | ||

+ | |||

+ | julia> g() | ||

+ | ERROR: TypeError: in g, in typeassert, expected Float64, got Int64 | ||

+ | Stacktrace: | ||

+ | |||

+ | ``` | ||

+ | |||

+ | The inside of function `g` cannot know what x is. This will terribly slow down the Julia program. In this context, it is also useful to mention that one should never to have one function (accidentally) return different return types based on its inputs or calculations. | ||

+ | |||

+ | ```juliarepl | ||

+ | julia> y=3.0 | ||

+ | 3.0 | ||

+ | |||

+ | julia> f()= (y>0) ? 1 : 2.0 ## Don't do this. Bad logic and speed consequences. | ||

+ | f (generic function with 1 method) | ||

+ | |||

+ | ``` | ||

+ | |||

+ | * It is good practice to have only one return statement at the end of the function (especially if the function declaration has not typed its output), assuming this is reasonable from the perspective of the algorithm. | ||

+ | |||

+ | |||

+ | |||

+ | ## Declaring Functions With Arguments | ||

+ | |||

+ | ### Ordinary Functions with Arguments (and Names and Types and Default Values) | ||

+ | |||

+ | ```juliarepl | ||

+ | julia> function f( a::Int, b::Int=2; c::Int=3 )::Int; a+b+c ; end#function f | ||

+ | f (generic function with 2 methods) | ||

+ | |||

+ | julia> methods(f) ## one definition created multiple methods | ||

+ | # 2 methods for generic function "f": | ||

+ | [1] f(a::Int64, b::Int64; c) in Main at none:1 | ||

+ | [2] f(a::Int64) in Main at none:1 | ||

+ | |||

+ | julia> f( a::Int , b::Int=2 ; c::Int=3 )::Int= a+b+c ## same definition | ||

+ | f (generic function with 2 methods) | ||

+ | |||

+ | ``` | ||

+ | |||

+ | * `a` is an ordinary argument, that must be an integer. | ||

+ | * `b` must be an integer and has a default value. The caller can omit it if it wants '2' | ||

+ | * `c` is a "named argument" (which must have a default value). To change its value, the caller must name 'c'. | ||

+ | |||

+ | |||

+ | |||

+ | ### Typing Abstract Containers (like Arrays or Dicts) | ||

+ | |||

+ | ```juliarepl | ||

+ | julia> sumprod( x::Vector )= sum( prod(x) ); ## catch any Vector. but why allow String Vector? | ||

+ | |||

+ | julia> function sumprod( x::Vector{T} )::T where T<:Number; sum( prod(x) ); end;##function## better | ||

+ | |||

+ | ``` | ||

+ | |||

+ | |||

+ | |||

+ | ### Null Defaults | ||

+ | |||

+ | Julia does not have null pointers, which are natural defaults in languages like C. But this functionality is easy to accomplish with two functions: | ||

+ | |||

+ | ```juliarepl | ||

+ | julia> f( x::Int)= "argument $x"; | ||

+ | |||

+ | julia> f( )= "no argument call"; | ||

+ | |||

+ | julia> ( f(20), f() ) | ||

+ | ( "argument 20", "no argument call" ) | ||

+ | |||

+ | ``` | ||

+ | |||

+ | Another alternative is to make up your own default value. Typical choices may be | ||

+ | |||

+ | ```juliarepl | ||

+ | julia> f( x::Int= typemin(Int) )= (x == typemin(Int)) ? "no argument" : "argument $x"; | ||

+ | |||

+ | julia> f( x::AbstractFloat= NaN )= (isnan(x)) ? 0.0 : x; | ||

+ | |||

+ | julia> f( gradient::Function=error )= (gradient == error) || "do not use finite differences"; | ||

+ | ``` | ||

+ | |||

+ | |||

+ | ### Keyword-Required Named Arguments | ||

+ | |||

+ | Arguments after the semi-colon must be have a default value (if they are not named) by the function call. Callers must never name the argument when the argument was declared before the semicolon; and must always name the argument when declared after. | ||

+ | |||

+ | |||

+ | ```juliarepl | ||

+ | julia> function f(x::Int; y::Int=0); println("f: $x $y"); end#function ## semicolon starts named args | ||

+ | f (generic function with 1 method) | ||

+ | |||

+ | julia> f(1, y=2) ## on function calls, second argument must be named | ||

+ | f: 1 2 | ||

+ | |||

+ | julia> f(1; y=2) ## for clarity, you can also separate this with semicolon | ||

+ | f: 1 2 | ||

+ | |||

+ | julia> f(3) ## or, because we have a default, you can omit it | ||

+ | f: 3 0 | ||

+ | |||

+ | julia> f(1,2) ## but you cannot give y like an ordinary argument | ||

+ | ERROR: MethodError: no method matching f(::Int64, ::Int64) | ||

+ | Closest candidates are: | ||

+ | f(::Int64; y) at none:1 | ||

+ | |||

+ | Stacktrace: | ||

+ | |||

+ | ``` | ||

+ | |||

+ | ### Functions Passing Along Arbitrary Named Keyword Arguments | ||

+ | |||

+ | ```juliarepl | ||

+ | julia> entry(x::AbstractFloat, y::AbstractFloat, z::AbstractFloat; arg1::Int=1, args...)= deeper(x, y+z; args...) | ||

+ | entry (generic function with 1 method) | ||

+ | |||

+ | julia> deeper(x::AbstractFloat, y::AbstractFloat; arg1::Int=1, arg2::Int=2, arg3::String="hi")= | ||

+ | "Your function has $x and $y; with optargs $arg1 and $arg2 and $arg3" ## | ||

+ | deeper (generic function with 1 method) | ||

+ | |||

+ | julia> entry(1.0, 2.0, 3.0; arg1=10) | ||

+ | "Your function has 1.0 and 5.0; with optargs 1 and 2 and hi" | ||

+ | |||

+ | julia> entry(1.0, 2.0, 3.0; arg1=10, arg2=12) | ||

+ | "Your function has 1.0 and 5.0; with optargs 1 and 12 and hi" | ||

+ | |||

+ | julia> entry(1.0, 2.0, 3.0; arg1=10, badarg=122) ## cannot work, because the first f has nowhere to pass this onto | ||

+ | ERROR: MethodError: no method matching deeper(::Float64, ::Float64; badarg=122) | ||

+ | Closest candidates are: | ||

+ | deeper(::AbstractFloat, ::AbstractFloat; arg1, arg2, arg3) at none:1 got unsupported keyword argument "badarg" | ||

+ | Stacktrace: | ||

+ | [1] (::#kw##deeper)(::Array{Any,1}, ::#deeper, ::Float64, ::Float64) at ./<missing>:0 | ||

+ | [2] #entry#1(::Int64, ::Array{Any,1}, ::Function, ::Float64, ::Float64, ::Float64) at ./none:1 | ||

+ | [3] (::#kw##entry)(::Array{Any,1}, ::#entry, ::Float64, ::Float64, ::Float64) at ./<missing>:0 | ||

+ | |||

+ | ``` | ||

+ | |||

+ | |||

+ | |||

+ | |||

+ | |||

+ | |||

+ | |||

+ | |||

+ | ## The "Where" Approach to Constraining Multiple Argument and/or Return Types | ||

+ | |||

+ | Julia also offers an even more flexible `where`. The principal use of `where` is in generic function, in which you want to require multiple arguments (or the return value) to be of the same type as one of the input argument. | ||

+ | |||

+ | ```juliarepl | ||

+ | julia> function normed_distance(p::Pair{T,T})::T where T<:Real; sqrt(first(p)^2+last(p)^2); end;#function## | ||

+ | |||

+ | julia> normed_distance( Pair( 2.0, 3.0 ) ) | ||

+ | 3.605551275463989 | ||

+ | |||

+ | julia> normed_distance( Pair( 2, 3.0 ) ) ## does not match two same inputs in the pair | ||

+ | ERROR: MethodError: no method matching normed_distance(::Pair{Int64,Float64}) | ||

+ | Closest candidates are: | ||

+ | normed_distance(!Matched::Pair{T<:Real,T<:Real}) where T<:Real at none:1 | ||

+ | Stacktrace: | ||

+ | |||

+ | ``` | ||

+ | |||

+ | |||

+ | |||

+ | ### Basic Example | ||

+ | |||

+ | The following could be done more easily with `Vector{<:Real}`, but illustrates the method: | ||

+ | |||

+ | ```juliarepl | ||

+ | julia> f( x::Vector{T} ) where T <: Real= "where vector is $(typeof(x))" ## Basic Use | ||

+ | f (generic function with 1 method) | ||

+ | |||

+ | julia> f( [1,2] ) | ||

+ | "where vector is Array{Int64,1}" | ||

+ | |||

+ | julia> f( [1.0,2.0] ) | ||

+ | "where vector is Array{Float64,1}" | ||

+ | |||

+ | julia> f( Vector{Float32}([1.0,2.0]) ) | ||

+ | "where vector is Array{Float32,1}" | ||

+ | |||

+ | julia> f( ["a","b"] ) | ||

+ | ERROR: MethodError: no method matching f(::Array{String,1}) | ||

+ | Closest candidates are: | ||

+ | f(!Matched::Array{T<:Real,1}) where T<:Real at none:1 | ||

+ | Stacktrace: | ||

+ | ... | ||

+ | |||

+ | ``` | ||

+ | |||

+ | |||

+ | ### Constraints Among Arguments and Returns | ||

+ | |||

+ | A definition where `where` is more useful is | ||

+ | |||

+ | ```juliarepl | ||

+ | |||

+ | julia> function f( x::Vector{T}, y::T )::Vector{T} where T<:AbstractFloat; vcat(x,y) .+ 1; end##function | ||

+ | f (generic function with 1 method) | ||

+ | |||

+ | julia> f( [1.0, 2.0], 3.0 ) ## x, y, and the return are all of type Float64 | ||

+ | 3-element Array{Float64,1}: | ||

+ | 2.0 | ||

+ | 3.0 | ||

+ | 4.0 | ||

+ | |||

+ | julia> f( Vector{Float32}([1.0, 2.0]), Float32(3.0) ) ## x, y, and the return are all of type Float32 | ||

+ | 3-element Array{Float32,1}: | ||

+ | 2.0 | ||

+ | 3.0 | ||

+ | 4.0 | ||

+ | |||

+ | julia> f( [1, 2], 3 ) ## fails: Integers are not AbstractFloats | ||

+ | ERROR: MethodError: no method matching f(::Array{Int64,1}, ::Int64) | ||

+ | Stacktrace: | ||

+ | [1] top-level scope at none:0 | ||

+ | |||

+ | julia> f( [1.0, 2.0], Float32(3.0) ) ## Float64 and Float32 = mixed call. fails. | ||

+ | ERROR: MethodError: no method matching f(::Array{Float64,1}, ::Float32) | ||

+ | Closest candidates are: | ||

+ | f(::Array{T<:AbstractFloat,1}, !Matched::T<:AbstractFloat) where T<:AbstractFloat at none:1 | ||

+ | Stacktrace: | ||

+ | [1] top-level scope at none:0 | ||

+ | |||

+ | julia> f( [1, 2], Float32(3.0) ) ## mixed call. fails. | ||

+ | ERROR: MethodError: no method matching f(::Array{Int64,1}, ::Float32) | ||

+ | Closest candidates are: | ||

+ | f(!Matched::Array{T<:AbstractFloat,1}, ::T<:AbstractFloat) where T<:AbstractFloat at none:1 | ||

+ | Stacktrace: | ||

+ | [1] top-level scope at none:0 | ||

+ | |||

+ | ``` | ||

+ | |||

+ | * Multiple comma-separated `where`'s make it possible to catch multiple types for multiple arguments. | ||

+ | |||

+ | * `myfun(x::Vector{T}, x::T) where {T}= [v..., x]` can dispatch `myfun( [1.0, 2.0], 3.0 )`. [https://docs.julialang.org/en/stable/manual/methods/#Methods-1] | ||

+ | |||

+ | * It can be useful to have a `where T <: Any`, as in the [[numbers#all_permutations|Numbers Permutation]] example. | ||

+ | |||

+ | |||

+ | |||

+ | |||

+ | |||

+ | ## Dispatch for both Scalar and Array | ||

+ | |||

+ | Remember that if you define a function, you will automatically also have a dot form. In many cases, this is what you want a similar vector function to do, releasing you from the need to define it. | ||

+ | |||

+ | Just define two functions with the same name: | ||

+ | |||

+ | |||

+ | ```juliarepl | ||

+ | julia> function dispatch( x::AbstractFloat ); "Float Scalar"; end#function | ||

+ | dispatch (generic function with 1 method) | ||

+ | |||

+ | julia> function dispatch( x::Vector{Float64} ); "Float Vector"; end#function | ||

+ | dispatch (generic function with 2 methods) | ||

+ | |||

+ | julia> dispatch( [ 1.0, 2.0 ] ) | ||

+ | "Float Vector" | ||

+ | |||

+ | julia> dispatch( [ 1.0 2.0; 3.0 4.0 ] ) ## but Vector does not work on Matrices | ||

+ | ERROR: MethodError: no method matching dispatch(::Array{Float64,2}) | ||

+ | Closest candidates are: | ||

+ | dispatch(!Matched::Array{Float64,1}) at none:1 | ||

+ | dispatch(!Matched::AbstractFloat) at none:1 | ||

+ | Stacktrace: | ||

+ | ... | ||

+ | |||

+ | julia> dispatch.( [ 1.0 2.0; 3.0 4.0 ] ) ## dot-postfix invokation feeds array elements as scalars | ||

+ | 2ร2 Array{String,2}: | ||

+ | "Float Scalar" "Float Scalar" | ||

+ | "Float Scalar" "Float Scalar" | ||

+ | |||

+ | julia> function dispatch( x::Array{Float64,2} ); "Float Matrix"; end#function | ||

+ | dispatch (generic function with 3 methods) | ||

+ | |||

+ | julia> dispatch( [ 1.0 2.0; 3.0 4.0 ] ) ## but now we have a matching native matrix method | ||

+ | "Float Matrix" | ||

+ | |||

+ | ``` | ||

+ | |||

+ | |||

+ | |||

+ | |||

+ | ## Conversion and Promotion | ||

+ | |||

+ | Julia is fairly strictly typed. When a function requests a `Float32` argument, it fails when it receives a `Float64`. However, magic seems to happen when the user types | ||

+ | |||

+ | ```juliarepl | ||

+ | julia> x= [ 1.0, 2 ] ## ever wonder how a [ Float64, Int ] can become a [ Float64, Float64 ] ?? | ||

+ | 2-element Array{Float64,1}: | ||

+ | 1.0 | ||

+ | 2.0 | ||

+ | ``` | ||

+ | |||

+ | This works due to behind-the-scene magic. First, Julia has a system to declare that an `Int` can be converted into a Float64. This is accomplished via the julia `convert`. Second, Julia has a system to declare how it should react when it encounters multiple values of different kinds. In this case, there was a rule that said that "when you see a list of Float64,Int within the [array], promote them both to Float64. Voi-la. This is explained in detail in the [official Julia docs](https://docs.julialang.org/en/stable/manual/conversion-and-promotion/). | ||

+ | |||

+ | The promotion aspect can sometimes be useful even in userland. For example, you can take advantage of the built-in Julia conversions in either of these two ways | ||

+ | |||

+ | ```juliarepl | ||

+ | julia> f( xx::AbstractFloat,yy::AbstractFloat )= sqrt( xx^2+yy^2 ); | ||

+ | |||

+ | julia> f( xx, yy )= sqrt( Float64(xx), Float64(yy) ); ## choice 1: you provide some intelligence | ||

+ | |||

+ | julia> f( xx,yy )= f( promote(xx,yy) ); ## choice 2: promote() provides the intelligence | ||

+ | |||

+ | ``` | ||

+ | |||

+ | |||

+ | |||

+ | ## Passing a Function as an Argument to Another Function | ||

+ | |||

+ | ```juliarepl | ||

+ | julia> workonfun( f::Function )= "Your function is ", f, " and operating on 10 it yields ", f(10) | ||

+ | workonfun (generic function with 1 method) | ||

+ | |||

+ | julia> workonfun(sqrt) | ||

+ | ("Your function is ", sqrt, " and operating on 10 it yields ", 3.1622776601683795) | ||

+ | |||

+ | ``` | ||

+ | |||

+ | * For dispatch purposes, Julia treats all functions the same. That is, you cannot write one dispatch that applies to "functions that take an integer argument" and another dispatch that applies to "functions that take a float vector argument". Instead, you should check the arguments upon entry into the function, and then dispatch yourself to the appropriate destination function. | ||

+ | |||

+ | * It seems difficult to discover whether a method of function `f()` exists for a particular type argument. You are probably better off simply [[controlflow#exceptions|trapping the function]] for a `MethodError`. | ||

+ | |||

+ | ```juliarepl | ||

+ | julia> function workon( f::Function, argument ) | ||

+ | (isa(argument,Real)) && return f(argument) | ||

+ | try | ||

+ | f(argument) | ||

+ | catch err | ||

+ | isa(err, MethodError) ? "f does not work on an argument of type $(typeof(argument))" : "something else went wrong: $err" | ||

+ | end#try | ||

+ | end;#function## | ||

+ | |||

+ | julia> workon( sqrt, 2 ) | ||

+ | 1.4142135623730951 | ||

+ | |||

+ | julia> workon( sqrt, "a" ) | ||

+ | "f does not work on an argument of type String" | ||

+ | |||

+ | ``` | ||

+ | |||

+ | |||

+ | |||

+ | |||

+ | |||

+ | |||

+ | ## Allowing Missing in Function Arguments | ||

+ | |||

+ | Missing is now a core feature of Julia. Ergo, it is important to understand how to dispatch when missing values can be involved. See also [[missings|Missings and NaN]]. Reminder: **M**issing is a type, **m**issing is a value. | ||

+ | |||

+ | ### Adding Missing Handling To Scalar Arguments | ||

+ | |||

+ | Define one function for the scalar type, and another for the missing type. | ||

+ | |||

+ | ```juliarepl | ||

+ | |||

+ | julia> function mysqrt(in::AbstractFloat)::AbstractFloat; sqrt(in) end#function | ||

+ | mysqrt (generic function with 1 method) | ||

+ | |||

+ | julia> function mysqrt(in::Missing)::Missing; in; end#function ## any call with missing returns missing | ||

+ | mysqrt (generic function with 2 methods) | ||

+ | |||

+ | julia> mysqrt.( [1.0, missing, NaN, 4.0] ) | ||

+ | 4-element Array{Union{Missing, Float64},1}: | ||

+ | 1.0 | ||

+ | missing | ||

+ | NaN | ||

+ | 2.0 | ||

+ | ``` | ||

+ | |||

+ | |||

+ | |||

+ | ### Adding Missing (Unions) To Array Arguments | ||

+ | |||

+ | The `<:` operator is smart enough to understand "union with missing" versions, too: | ||

+ | |||

+ | ```juliarepl | ||

+ | julia> MReal= Union{Real,Missing} | ||

+ | Union{Missing, Real} | ||

+ | |||

+ | julia> filtermissing( x::Vector{<:MReal} )= filter( !ismissing, x ) | ||

+ | filtermissing (generic function with 1 method) | ||

+ | |||

+ | julia> filtermissing( [ 1.0, 2.0, missing, 3.0 ] ) | ||

+ | 3-element Array{Union{Missing, Float64},1}: | ||

+ | 1.0 | ||

+ | 2.0 | ||

+ | 3.0 | ||

+ | |||

+ | julia> filtermissing( [ Float32(1.0), missing] ) ## Note: preserves return type | ||

+ | 1-element Array{Union{Missing, Float32},1}: | ||

+ | 1.0f0 | ||

+ | |||

+ | julia> filtermissing( [ "a", missing] ) ## Strings are not Reals | ||

+ | ERROR: MethodError: no method matching filtermissing(::Array{Union{Missing, String},1}) | ||

+ | Closest candidates are: | ||

+ | filtermissing(!Matched::Array{#s24,1} where #s24<:Union{Missing, Real}) at none:1 | ||

+ | Stacktrace: | ||

+ | [1] top-level scope at none:0 | ||

+ | |||

+ | ``` | ||

+ | |||

+ | * You could define one function for Real and another for MReal types. | ||

+ | |||

+ | |||

+ | |||

+ | |||

+ | |||

+ | # Common Dispatch Needs | ||

+ | |||

+ | ## Dispatching on Common Numeric Scalars | ||

+ | |||

+ | The heart of Julia is its ability to overload functions based on the types of call arguments (but not return type). | ||

+ | |||

+ | [[numbers|Reminder]]: A `Real` is any Float, Integer, Irrational, or Rational, but not a `Complex`. An Int is not an `AbstractFloat`. A `Number` is the supertype of all numerics. | ||

+ | |||

+ | |||

+ | ### Only Floats (All Precisions) | ||

+ | |||

+ | ```juliarepl | ||

+ | julia> myfloat( x::AbstractFloat )= typeof(x); ## integers not welcome | ||

+ | |||

+ | julia> myfloat(1.0), Float16(2.0) | ||

+ | (Float64, Float16(2.0)) | ||

+ | |||

+ | julia> myfloat(1) | ||

+ | ERROR: MethodError: no method matching myfloat(::Int64) | ||

+ | Closest candidates are: | ||

+ | myfloat(!Matched::AbstractFloat) at none:1 | ||

+ | Stacktrace: | ||

+ | ... | ||

+ | |||

+ | ``` | ||

+ | |||

+ | * You could also dispatch on specific Floats or Integers, such as `Float64` | ||

+ | |||

+ | |||

+ | ### Only Ints (All Precisions) | ||

+ | |||

+ | |||

+ | ```juliarepl | ||

+ | julia> myint( x::Integer )= typeof(x); ## floats not welcome | ||

+ | |||

+ | julia> myint( 1 ), myint( Int8(2) ) | ||

+ | (Int64, Int8) | ||

+ | |||

+ | julia> myint( 32.0 ) | ||

+ | ERROR: MethodError: no method matching myint(::Float64) | ||

+ | Closest candidates are: | ||

+ | myint(!Matched::Integer) at none:1 | ||

+ | Stacktrace: | ||

+ | |||

+ | ``` | ||

+ | |||

+ | |||

+ | ### Only Ints or Floats (but not Complex) | ||

+ | |||

+ | ```juliarepl | ||

+ | julia> myreal( x::Real )= typeof(x); | ||

+ | |||

+ | julia> myreal(10.0), myreal( Int8(6) ) | ||

+ | (Float64, Int8) | ||

+ | |||

+ | ``` | ||

+ | |||

+ | |||

+ | ### Only High (64-bit) Precision Ints or Floats | ||

+ | |||

+ | |||

+ | ```juliarepl | ||

+ | julia> Prec64= Union{Int64,Float64}; ## could add Missing | ||

+ | |||

+ | julia> f( x::Prec64 )= "x is $(typeof(x))" | ||

+ | f (generic function with 1 method) | ||

+ | |||

+ | julia> f( 1.0 ) | ||

+ | "x is Float64" | ||

+ | |||

+ | julia> f( 1 ) | ||

+ | "x is Int64" | ||

+ | |||

+ | julia> f( Float32(1.0) ) | ||

+ | ERROR: MethodError: no method matching f(::Float32) | ||

+ | Closest candidates are: | ||

+ | f(!Matched::Union{Float64, Int64}) at none:1 | ||

+ | Stacktrace: | ||

+ | ... | ||

+ | |||

+ | ``` | ||

+ | |||

+ | |||

+ | |||

+ | ### Different Numeric Type-Specific Behavior | ||

+ | |||

+ | Overloading the function. | ||

+ | |||

+ | ```juliarepl | ||

+ | julia> function dispatch( x ); "to Any f"; end#function | ||

+ | dispatch (generic function with 1 method) | ||

+ | |||

+ | julia> function dispatch( x::Float64 ); "to Float64 f"; end#function | ||

+ | dispatch (generic function with 2 methods) | ||

+ | |||

+ | julia> function dispatch( x::Real ); "to Real f"; end#function | ||

+ | dispatch (generic function with 3 methods) | ||

+ | |||

+ | julia> dispatch("a"), dispatch(0.2), dispatch(1), dispatch( Float32(1.0) ) | ||

+ | ("to Any f", "to Float64 f", "to Real f", "to Real f") | ||

+ | |||

+ | julia> dispatch( [ 1.0, 2.0, NaN ] ) ## no dot-postfix. argument type is Vector (of Floats). no such dispatch() yet defined. | ||

+ | "to Any f" | ||

+ | |||

+ | julia> dispatch.( [ 1.0, 2.0, NaN ] ) ## with dot-postfix, array arguments are multi-fed as calls with scalars | ||

+ | 3-element Array{String,1}: | ||

+ | "to Float64 f" | ||

+ | "to Float64 f" | ||

+ | "to Float64 f" | ||

+ | ``` | ||

+ | |||

+ | |||

+ | |||

+ | |||

+ | |||

+ | ## Dispatching on *Arrays* of Common (Numeric) Types | ||

+ | |||

+ | Remember that if you define a function, you will automatically also have a dot form. In many cases, this is what you want a similar vector function to do, releasing you from the need to define it. | ||

+ | |||

+ | |||

+ | |||

+ | ### Any Type of Array | ||

+ | |||

+ | ```juliarepl | ||

+ | julia> f(x::Vector)::String= "vector of $(typeof(x))"; ## could also dispatch on broader Matrix or Array | ||

+ | |||

+ | julia> f( [ "a", 1] ) | ||

+ | "vector of Array{Any,1}" | ||

+ | ``` | ||

+ | |||

+ | |||

+ | ### Writing a Function that Works on Lists Or Arrays | ||

+ | |||

+ | ```juliarepl | ||

+ | |||

+ | julia> work( x::Int, y::Int, z::Int )::Int= x+2*y+3*z ; | ||

+ | |||

+ | julia> work( 1, 2, 3 ) | ||

+ | 14 | ||

+ | |||

+ | julia> x= [1,2,3] | ||

+ | 3-element Array{Int64,1}: | ||

+ | 1 | ||

+ | 2 | ||

+ | 3 | ||

+ | |||

+ | julia> work( x... ) ## splatter operator on vector x | ||

+ | 14 | ||

+ | |||

+ | julia> work( v::Vector{Int} )::Int= work(v...); ## or define directly as a function of splatterer | ||

+ | |||

+ | julia> work( x ) | ||

+ | 14 | ||

+ | |||

+ | ``` | ||

+ | |||

+ | |||

+ | |||

+ | ### Primitive Numeric Types | ||

+ | |||

+ | ```juliarepl | ||

+ | julia> f( x::Vector{Float64} )= "vector of 64-bit floats" ## most straightforward | ||

+ | f (generic function with 1 method) | ||

+ | |||

+ | julia> f( [1.0,2.0] ) | ||

+ | "vector of 64-bit floats" | ||

+ | ``` | ||

+ | |||

+ | |||

+ | ### Broader Types: Floats, Ints, and/or Both | ||

+ | |||

+ | The following is probably not what you want: | ||

+ | |||

+ | ```juliarepl | ||

+ | julia> f( x::Vector{Real} )= "vector of reals" | ||

+ | f (generic function with 1 method) | ||

+ | |||

+ | julia> f( [1.0,2.0] ) ## it does not catch a Vector of Float64's !! | ||

+ | ERROR: MethodError: no method matching f(::Array{Float64,1}) | ||

+ | Closest candidates are: | ||

+ | f(!Matched::Array{Real,1}) at none:1 | ||

+ | Stacktrace: | ||

+ | ... | ||

+ | ``` | ||

+ | |||

+ | This is because you can define a Vector of Reals, and this is what f catches: | ||

+ | |||

+ | ```juliarepl | ||

+ | julia> f( x::Vector{Real} )= "vector of reals" | ||

+ | f (generic function with 1 method) | ||

+ | |||

+ | julia> x= Vector{Real}( [ 1.0, 3.0 ] ) ## this is because you can define a Vector of Reals | ||

+ | 2-element Array{Real,1}: | ||

+ | 1.0 | ||

+ | 3.0 | ||

+ | |||

+ | julia> x[2]=4; x ## see? a Real Vector can contain different types! | ||

+ | 2-element Array{Real,1}: | ||

+ | 1.0 | ||

+ | 4 | ||

+ | |||

+ | julia> f(x) ## so it dispatches what you asked for. | ||

+ | "vector of reals" | ||

+ | ``` | ||

+ | |||

+ | |||

+ | Instead, what you probably meant was to take a unitype vector of Real "or less": | ||

+ | |||

+ | ```juliarepl | ||

+ | julia> f( x::Vector{<:Real} )= "vector of $(typeof(x))" | ||

+ | f (generic function with 1 method) | ||

+ | |||

+ | julia> f( [1.0, 2.0] ) ## it can catch a vector of Float64s | ||

+ | "vector of Array{Float64,1}" | ||

+ | |||

+ | julia> f( [1, 2] ) ## or a vector of Int64s | ||

+ | "vector of Array{Int64,1}" | ||

+ | ``` | ||

+ | |||

+ | * This approach also works with Missing types, [[#allowing_missing_in_function_arguments|see below.]] | ||

+ | |||

+ | |||

+ | ## Example: A Lag Function | ||

+ | |||

+ | ```juliarepl | ||

+ | julia> function lag( v::Vector{T}, n::Int=1 )::Vector{T} where T <: Union{Missing,Float64} | ||

+ | @assert( n < length(v), "n must be less than the length of the vector." ) | ||

+ | vcat( fill( NaN, n), v[1:(end-n)] ) | ||

+ | end#function## | ||

+ | lag (generic function with 2 methods) | ||

+ | |||

+ | julia> function lag( v::Vector{T}, n::Int=1 )::Vector{Union{Missing,T}} where T | ||

+ | @assert( n < length(v), "n must be less than the length of the vector." ) | ||

+ | vcat( fill( missing, n), v[1:(end-n)] ) | ||

+ | end#function## | ||

+ | lag (generic function with 4 methods) | ||

+ | |||

+ | julia> lag( [ 1.0, 2.0, 3.0 ] ) ## caught by first lag fun. result is still Float only | ||

+ | 3-element Array{Float64,1}: | ||

+ | NaN | ||

+ | 1.0 | ||

+ | 2.0 | ||

+ | |||

+ | julia> lag( [ 1.0, missing, 2.0, 3.0 ] ) ## caught by first lag fun. result still has missing | ||

+ | 4-element Array{Union{Missing, Float64},1}: | ||

+ | NaN | ||

+ | 1.0 | ||

+ | missing | ||

+ | 2.0 | ||

+ | |||

+ | julia> lag( [ "a", "b", "c" ] ) ## caught by second fun. extends type. | ||

+ | 3-element Array{Union{Missing, String},1}: | ||

+ | missing | ||

+ | "a" | ||

+ | "b" | ||

+ | |||

+ | julia> lag( [ 1, missing, 2, 3 ] ) ## caught by second lag. keeps type | ||

+ | 4-element Array{Union{Missing, Int64},1}: | ||

+ | missing | ||

+ | 1 | ||

+ | missing | ||

+ | 2 | ||

+ | |||

+ | |||

+ | ``` | ||

+ | |||

+ | ### Again? NaN for Floats, Missing for Others | ||

+ | |||

+ | Define a `nada()` function that returns NaN for floats and missing for non-floats, which are the respective kinds of "bad obs" value that we want. | ||

+ | |||

+ | ```juliarepl | ||

+ | |||

+ | julia> nada(::Type{T}) where {T <: AbstractFloat}= T(NaN) | ||

+ | nada (generic function with 1 method) | ||

+ | |||

+ | julia> nada(::Type)= missing | ||

+ | nada (generic function with 2 methods) | ||

+ | |||

+ | julia> nada(Float32) , nada(Int) | ||

+ | (NaN32, missing) | ||

+ | |||

+ | julia> function lag(v::AbstractVector{T}, n::Int=1) where T | ||

+ | @assert n < length(v) "n must be less than the length of the vector." | ||

+ | vcat( fill( nada(T), n ), v[1:(end-n)] ) | ||

+ | end#function## | ||

+ | lag (generic function with 2 methods) | ||

+ | |||

+ | julia> lag( [ 1.0, 2.0, 3.0 ] ) ## Floats use NaN | ||

+ | 3-element Array{Float64,1}: | ||

+ | NaN | ||

+ | 1.0 | ||

+ | 2.0 | ||

+ | |||

+ | julia> lag( [ 'a', 'b', 'c' ] ) ## Other types use missing | ||

+ | 3-element Array{Union{Missing, Char},1}: | ||

+ | missing | ||

+ | 'a' | ||

+ | 'b' | ||

+ | |||

+ | ``` | ||

+ | |||

+ | |||

+ | |||

+ | |||

+ | |||

+ | |||

+ | # Backmatter | ||

+ | |||

+ | ## Useful Packages on Julia Repository | ||

+ | |||

+ | ## Notes | ||

+ | |||

+ | * In Computer Science, sometimes the arguments (parameters) in the function definition are named "formal parameters," and the arguments (parameters) in the caller are named "actual parameters." I wish the jargon was clearer. | ||

+ | |||

+ | * Unfortunately, it is not possible to turn on a compiler warning whenever meaningful default type variability arises, e.g., due to global variables or untyped arguments. Use [Traceur](https://github.com/MikeInnes/Traceur.jl). | ||

+ | |||

+ | * There are all sorts of ambiguities that can arise with abstract containers. Be specific (and/or read 15.9 of the Julia manual). | ||

+ | |||

+ | |||

+ | ## Question | ||

+ | |||

+ | ```juliarepl | ||

+ | julia> f0( x::Union{Nothing,VersionNumber} )= println("ok") | ||

+ | f0 (generic function with 1 method) | ||

+ | |||

+ | julia> f0( v"1.0" ) | ||

+ | ok | ||

+ | |||

+ | |||

+ | julia> f1( x::Pair{String,VersionNumber} )= println("ok") | ||

+ | f1 (generic function with 1 method) | ||

+ | |||

+ | julia> f1( "me" => v"1.0" ) | ||

+ | ok | ||

+ | |||

+ | |||

+ | julia> f2( x::Pair{String,Union{Nothing,VersionNumber}} )= println("ok") | ||

+ | f2 (generic function with 1 method) | ||

+ | |||

+ | julia> f2( "me" => v"1.0" ) | ||

+ | ERROR: MethodError: no method matching f2(::Pair{String,VersionNumber}) | ||

+ | Closest candidates are: | ||

+ | f2(::Pair{String,Union{Nothing, VersionNumber}}) at REPL[5]:1 | ||

+ | Stacktrace: | ||

+ | [1] top-level scope at none:0 | ||

+ | |||

+ | ``` | ||

fundispatch.txt ยท Last modified: 2018/12/27 14:38 (external edit)