class: center, middle # Julia Tutorial @ Home Office ## Part 2: Advanced Julia & Design Patterns Michael Kraus NMPP Seminar 14.05.2020, 28.05.2020, 04.06.2020 --- layout: false .toc[ .three-column-one[ #### Part 1: Language Overview * Introduction * Variables * Numbers * Data Structures * Functions * Control Flow * Types * Methods * Constructors * Modules * Scope of Variables * Package Management * Performance Tips * Style Guide .highlighted[ #### Part 2: Advanced Julia * Parametric Types * Parametric Methods * Conversion and Promotion * Interfaces * Meta Programming ] ] .three-column-two[ #### Part 3: Introspection * Benchmarking * Code Introspection * More Performance Tips * Profiling * Debugging * Stack Traces #### Part 4: Design Patterns * Composition * Multiple Dispatch * Traits * Method Design * Maintainability Patterns * Robustness Patterns * Anti-Patterns ] .three-column-three[ #### Part 5: Package Development * Create a Package * Tests * Documentation * GitHub Actions * Travis CI * Code Coverage #### Part 6: Parallel Programming * Tasks * Threads * Distributed * SharedArrays * DistributedArrays * MPI #### Part 7: Useful Packages * Plotting * Numerics * Literate Programming * Automatic Differentiation * Language Interoperability ] ] --- class: center, middle # Parametric Types --- .left-column[ ## Parametric Types ] .right-column[ * an important and powerful feature of Julia's type system is that it is parametric: types can take parameters, so that type declarations actually introduce a whole family of new types – one for each possible combination of parameter values * because Julia is a dynamically typed language and does not need to make all type decisions at compile time, many traditional difficulties encountered in static parametric type systems can be relatively easily handled * all declared types (the `DataType` variety) can be parameterized by any type or a value of any bits type, with the same syntax in each case ] --- .left-column[ ## Parametric Types ### Composite Types ] .right-column[ * type parameters are introduced immediately after the type name, surrounded by curly braces ```julia struct Point{T} x::T y::T end ``` * this declaration defines a new parametric type, `Point{T}`, holding two "coordinates" of type `T`, where the parameter `T` is clearly used as a type (and not a value of a bits type) * `Point{Float64}` is a concrete type equivalent to the type defined by replacing `T` in the definition of Point with `Float64` * thus, this single declaration actually declares an unlimited number of types: `Point{Float64}`, `Point{AbstractString}`, `Point{Int64}`, etc., each of which is now a usable concrete type ```julia julia> Point{Float64} Main.##WeaveSandBox#269.Point{Float64} julia> Point{AbstractString} Main.##WeaveSandBox#269.Point{AbstractString} ``` * the type `Point{Float64}` is a point whose coordinates are 64-bit floating-point values, while the type `Point{AbstractString}` is a "point" whose "coordinates" are string objects ] --- .left-column[ ## Parametric Types ### Composite Types ] .right-column[ * `Point` itself is also a valid type object, containing all instances `Point{Float64}`, `Point{AbstractString}`, etc. as subtypes ```julia julia> Point{Float64} <: Point true julia> Point{AbstractString} <: Point true ``` * other types, of course, are not subtypes of `Point` ```julia julia> Float64 <: Point false julia> AbstractString <: Point false ``` * concrete Point types with different values of `T` are never subtypes of each other ```julia julia> Point{Float64} <: Point{Int64} false julia> Point{Float64} <: Point{Real} false ``` ] --- .left-column[ ## Parametric Types ### Composite Types ] .right-column[ * the last obervation is very important: even though `Float64 <: Real` we **DO NOT** have `Point{Float64} <: Point{Real}` * in the parlance of type theory: Julia's type parameters are *invariant*, rather than being *covariant* (or even *contravariant*) * while any instance of `Point{Float64}` may conceptually be like an instance of `Point{Real}` as well, the two types have different representations in memory * an instance of `Point{Float64}` can be represented compactly and efficiently as an immediate pair of 64-bit values * an instance of `Point{Real}` must be able to hold any pair of instances of `Real`; since objects that are instances of `Real` can be of arbitrary size and structure, in practice an instance of `Point{Real}` must be represented as a pair of pointers to individually allocated `Real` objects * the efficiency gained by being able to store `Point{Float64}` objects with immediate values is magnified enormously in the case of arrays: an `Array{Float64}` can be stored as a contiguous memory block of 64-bit floating-point values, whereas an `Array{Real}` must be an array of pointers to individually allocated `Real` objects * although these may well be boxed 64-bit floating-point values, they also might be arbitrarily large, complex objects, which are declared to be implementations of the `Real` abstract type ] --- .left-column[ ## Parametric Types ### Composite Types ] .right-column[ * since `Point{Float64}` is not a subtype of `Point{Real}`, the following method cannot be applied to arguments of type `Point{Float64}` ```julia function norm(p::Point{Real}) sqrt(p.x^2 + p.y^2) end ``` * a correct way to define a method that accepts all arguments of type `Point{T}` where `T` is a subtype of `Real` is ```julia function norm(p::Point{<:Real}) sqrt(p.x^2 + p.y^2) end ``` or ```julia function norm(p::Point{T} where T<:Real) sqrt(p.x^2 + p.y^2) end ``` or ```julia function norm(p::Point{T}) where T<:Real sqrt(p.x^2 + p.y^2) end ``` ] --- .left-column[ ## Parametric Types ### Composite Types ### Parametric Constructors ] .right-column[ * in the absence of any special constructor declarations, there are two default ways of creating new composite objects, * one in which the type parameters are explicitly given, * and the other in which they are implied by the arguments to the object constructor * since the type `Point{Float64}` is a concrete type equivalent to `Point` declared with `Float64` in place of `T`, it can be applied as a constructor accordingly ```julia julia> Point{Float64}(1.0, 2.0) Main.##WeaveSandBox#269.Point{Float64}(1.0, 2.0) ``` * for the default constructor, exactly one argument must be supplied for each field ```julia julia> Point{Float64}(1.0) Error: MethodError: no method matching Main.##WeaveSandBox#269.Point{Float64}(::Float64) Closest candidates are: Main.##WeaveSandBox#269.Point{Float64}(::Any, !Matched::Any) where T at none:2 julia> Point{Float64}(1.0, 2.0, 3.0) Error: MethodError: no method matching Main.##WeaveSandBox#269.Point{Float64}(::Float64, ::Float64, ::Float64) Closest candidates are: Main.##WeaveSandBox#269.Point{Float64}(::Any, ::Any) where T at none:2 ``` * only one default constructor is generated for parametric types, since overriding it is not possible * this constructor accepts any arguments and converts them to the field types ] --- .left-column[ ## Parametric Types ### Composite Types ### Parametric Constructors ] .right-column[ * in many cases, it is redundant to provide the type of Point object one wants to construct, since the types of arguments to the constructor call already implicitly provide type information * for that reason, you can also apply `Point` itself as a constructor, provided that the implied value of the parameter type `T` is unambiguous ```julia julia> Point(1.0,2.0) Main.##WeaveSandBox#269.Point{Float64}(1.0, 2.0) julia> Point(1,2) Main.##WeaveSandBox#269.Point{Int64}(1, 2) ``` * in the case of `Point`, the type of `T` is unambiguously implied if and only if the two arguments to `Point` have the same type ```julia julia> Point(1.0,2) Error: MethodError: no method matching Main.##WeaveSandBox#269.Point(::Float64, ::Int64) Closest candidates are: Main.##WeaveSandBox#269.Point(::T, !Matched::T) where T at none:2 ``` * constructor methods to appropriately handle such mixed cases can be defined * the definition of custom constructors for composite types will be discussed in detail in the next section ] --- .left-column[ ## Parametric Types ### Composite Types ### Parametric Constructors ### Abstract Types ] .right-column[ * parametric abstract type declarations declare a collection of abstract types ```julia julia> abstract type Pointy{T} end ``` * with this declaration, `Pointy{T}` is a distinct abstract type for each type or integer value of `T` * as with parametric composite types, each such instance is a subtype of `Pointy` ```julia julia> Pointy{Int64} <: Pointy true julia> Pointy{1} <: Pointy true ``` * parametric abstract types are invariant, much as parametric composite types are ```julia julia> Pointy{Float64} <: Pointy{Real} false julia> Pointy{Real} <: Pointy{Float64} false ``` * the notation `Pointy{<:Real}` can be used to express the Julia analogue of a covariant type, while `Pointy{>:Int}` the analogue of a contravariant type, but technically these represent sets of types ```julia julia> Pointy{Float64} <: Pointy{<:Real} true julia> Pointy{Real} <: Pointy{>:Int} true ``` ] --- .left-column[ ## Parametric Types ### Composite Types ### Parametric Constructors ### Abstract Types ] .right-column[ * much as abstract types serve to create useful hierarchies of types over concrete types, parametric abstract types serve the same purpose with respect to parametric composite types * example: declare `XYPoint{T}` to be a subtype of `Pointy{T}` ```julia struct XYPoint{T} <: Pointy{T} x::T y::T end ``` * given such a declaration, for each choice of `T`, we have `XYPoint{T}` as a subtype of `Pointy{T}` ```julia julia> XYPoint{Float64} <: Pointy{Float64} true julia> XYPoint{Real} <: Pointy{Real} true julia> XYPoint{AbstractString} <: Pointy{AbstractString} true ``` * this relationship is also invariant ```julia julia> XYPoint{Float64} <: Pointy{Real} false julia> XYPoint{Float64} <: Pointy{<:Real} true ``` ] --- .left-column[ ## Parametric Types ### Composite Types ### Parametric Constructors ### Abstract Types ] .right-column[ #### What purpose do parametric abstract types like Pointy serve? * create a point-like implementation that only requires a single coordinate because the point is on the diagonal line `x = y` ```julia julia> struct DiagPoint{T} <: Pointy{T} x::T end ``` * both `XYPoint{Float64}` and `DiagPoint{Float64}` are implementations of the `Pointy{Float64}` abstraction, and similarly for every other possible choice of type `T` * this allows programming to a common interface shared by all `Pointy` objects, implemented for both `XYPoint` and `DiagPoint` ] --- .left-column[ ## Parametric Types ### Composite Types ### Parametric Constructors ### Abstract Types ] .right-column[ * there are situations where it may not make sense for type parameters to range freely over all possible types * in such situations, one can constrain the range of `T` ```julia julia> abstract type RealPointy{T<:Real} end ``` * with such a declaration, it is acceptable to use any type that is a subtype of `Real` in place of `T`, but not types that are not subtypes of `Real` ```julia julia> RealPointy{Float64} Main.##WeaveSandBox#269.RealPointy{Float64} julia> RealPointy{Real} Main.##WeaveSandBox#269.RealPointy{Real} julia> RealPointy{AbstractString} Error: TypeError: in RealPointy, in T, expected T<:Real, got Type{AbstractString} julia> RealPointy{1} Error: TypeError: in RealPointy, in T, expected T<:Real, got Int64 ``` * type parameters for parametric composite types can be restricted in the same manner ```julia struct RealXYPoint{T<:Real} <: Pointy{T} x::T y::T end ``` ] --- .left-column[ ## Parametric Types ### Composite Types ### Parametric Constructors ### Abstract Types ### Tuple Types ] .right-column[ * tuples are an abstraction of the arguments of a function – without the function itself * the salient aspects of a function's arguments are their order and their types * therefore a tuple type is similar to a parameterized immutable type where each parameter is the type of one field * example: a 2-element tuple type resembles the following immutable type ```julia struct Tuple2{A,B} a::A b::B end ``` * however, there are three key differences: * tuple types may have any number of parameters * tuple types are covariant in their parameters: `Tuple{Int}` is a subtype of `Tuple{Any}`; therefore `Tuple{Any}` is considered an abstract type, and tuple types are only concrete if their parameters are * tuples do not have field names; fields are only accessed by index * when a tuple is constructed, an appropriate tuple type is generated on demand ```julia julia> typeof((1,"foo",2.5)) Tuple{Int64,String,Float64} ``` * note the implications of covariance ```julia julia> Tuple{Int,AbstractString} <: Tuple{Real,Any} true ``` ] --- .left-column[ ## Parametric Types ### Composite Types ### Parametric Constructors ### Abstract Types ### Tuple Types ### Vararg Tuple Types ] .right-column[ * the last parameter of a tuple type can be the special type `Vararg`, which denotes any number of trailing elements ```julia julia> mytupletype = Tuple{AbstractString,Vararg{Int}} Tuple{AbstractString,Vararg{Int64,N} where N} julia> isa(("1",), mytupletype) true julia> isa(("1",1), mytupletype) true julia> isa(("1",1,2), mytupletype) true julia> isa(("1",1,2,3.0), mytupletype) false ``` * `Vararg` tuple types are used to represent the arguments accepted by varargs methods * the type `Vararg{T}` corresponds to zero or more elements of type `T` * the type `Vararg{T,N}` corresponds to exactly `N` elements of type `T` * `NTuple{N,T}` is a convenient alias for `Tuple{Vararg{T,N}}`, i.e. a tuple type containing exactly `N` elements of type `T` ] --- .left-column[ ## Parametric Types ### Composite Types ### Parametric Constructors ### Abstract Types ### Tuple Types ### Vararg Tuple Types ### Named Tuple Types ] .right-column[ * named tuples are instances of the `NamedTuple` type, which has two parameters: * a tuple of symbols giving the field names * and a tuple type giving the field types ```julia julia> typeof((a=1,b="hello")) NamedTuple{(:a, :b),Tuple{Int64,String}} ``` * a `NamedTuple` type can be used as a constructor, accepting a single tuple argument * the constructed `NamedTuple` type can be either a concrete type, with both parameters specified, or a type that specifies only field names ```julia julia> NamedTuple{(:a, :b),Tuple{Float32, String}}((1,"")) (a = 1.0f0, b = "") julia> NamedTuple{(:a, :b)}((1,"")) (a = 1, b = "") ``` * if field types are specified, the arguments are converted; otherwise the types of the arguments are used directly ] --- .left-column[ ## Parametric Types ### Composite Types ### Parametric Constructors ### Abstract Types ### Tuple Types ### Vararg Tuple Types ### Named Tuple Types ### Singleton Types ] .right-column[ * singleton types are a special kind of abstract parametric types: for each type `T`, the "singleton type" `Type{T}` is an abstract type whose only instance is the object `T` * since the definition is a little difficult to parse, let's look at some examples ```julia julia> isa(Float64, Type{Float64}) true julia> isa(Real, Type{Float64}) false julia> isa(Real, Type{Real}) true julia> isa(Float64, Type{Real}) false ``` * `isa(A,Type{B})` is `true` iff `A` and `B` are the same object and that object is a type * without the parameter, `Type` is an abstract type which has all type objects as its instances, including singleton types, but excluding objects that are not a type ```julia julia> isa(Type{Float64}, Type) true julia> isa(Float64, Type) true julia> isa(Real, Type) true julia> isa(1, Type) false ``` ] --- .left-column[ ## Parametric Types ### Composite Types ### Parametric Constructors ### Abstract Types ### Tuple Types ### Vararg Tuple Types ### Named Tuple Types ### Singleton Types ] .right-column[ * singleton types allow to specialize function behaviour on specific type values * this is useful for writing methods (especially parametric ones) whose behaviour depends on a type that is given as an explicit argument rather than implied by the type of one of its arguments * their full utility will become clearer in the context of parametric methods and conversions * in general usage, the term "singleton type" refers to a type whose only instance is a single value; this meaning applies to Julia's singleton types, but with that caveat that only type objects have singleton types ] --- .left-column[ ## Parametric Types ### Composite Types ### Parametric Constructors ### Abstract Types ### Tuple Types ### Vararg Tuple Types ### Named Tuple Types ### Singleton Types ### Parametric Primitive Types ] .right-column[ * primitive types can also be declared parametrically * example: pointers are represented as primitive types which would be declared like this ```julia # 32-bit system: primitive type Ptr{T} 32 end # 64-bit system: primitive type Ptr{T} 64 end ``` * in contrast to typical parametric composite types, the type parameter `T` is not used in the definition of the type itself * instead it is just an abstract tag, essentially defining an entire family of types with identical structure, differentiated only by their type parameter * thus, `Ptr{Float64}` and `Ptr{Int64}` are distinct types, even though they have identical representations * all specific pointer types are subtypes of the umbrella `Ptr` type ```julia julia> Ptr{Float64} <: Ptr true julia> Ptr{Int64} <: Ptr true ``` ] --- .left-column[ ## Parametric Types ### Composite Types ### Parametric Constructors ### Abstract Types ### Tuple Types ### Vararg Tuple Types ### Named Tuple Types ### Singleton Types ### Parametric Primitive Types ### UnionAll Types ] .right-column[ * a parametric type like `Ptr` acts as a supertype of all its instances (`Ptr{Int64}` etc.); How does this work? * `Ptr` itself cannot be a normal data type, since without knowing the type of the referenced data the type clearly cannot be used for memory operations * the answer is that `Ptr` (or other parametric types like `Array`) is a different kind of type called a `UnionAll` type * such a type expresses the iterated union of types for all values of some parameter * `UnionAll` types are usually written using the keyword `where`, e.g., `Ptr` could be more accurately written as `Ptr{T} where T`, meaning all values whose type is `Ptr{T}` for some value of `T` * in this context, the parameter `T` is often called a "type variable" since it is like a variable that ranges over types * each where introduces a single type variable, so these expressions are nested for types with multiple parameters, for example `Array{T,N} where N where T` ] --- .left-column[ ## Parametric Types ### Composite Types ### Parametric Constructors ### Abstract Types ### Tuple Types ### Vararg Tuple Types ### Named Tuple Types ### Singleton Types ### Parametric Primitive Types ### UnionAll Types ] .right-column[ * the type application syntax `A{B,C}` requires `A` to be a `UnionAll` type * it first substitutes `B` for the outermost type variable in `A` * the result is expected to be another `UnionAll` type, into which `C` is substituted * `A{B,C}` is equivalent to `A{B}{C}`, which is why it is possible to partially instantiate a type, as in `Array{Float64}`: the first parameter value has been fixed, but the second still ranges over all possible values * using explicit `where` syntax, any subset of parameters can be fixed, e.g., the type of all one-dimensional arrays can be written as `Array{T,1} where T` * type variables can be restricted with subtype relations, e.g., `Array{T} where T<:Integer` refers to all arrays whose element type is some kind of `Integer` * the syntax `Array{<:Integer}` is a convenient shorthand for `Array{T} where T<:Integer` * type variables can have both lower and upper bounds, e.g., `Array{T} where Int<:T<:Number` refers to all arrays of `Number`s that are able to contain `Int`s * the syntax `where T>:Int` also works to specify only the lower bound of a type variable, and `Array{>:Int}` is equivalent to `Array{T} where T>:Int` * since where expressions nest, type variable bounds can refer to outer type variables, e.g., `Tuple{T,Array{S}} where S<:AbstractArray{T} where T<:Real` refers to two-tuples whose first element is some `Real`, and whose second element is an `Array` of any kind of array whose element type contains the type of the first tuple element ] --- .left-column[ ## Parametric Types ### Composite Types ### Parametric Constructors ### Abstract Types ### Tuple Types ### Vararg Tuple Types ### Named Tuple Types ### Singleton Types ### Parametric Primitive Types ### UnionAll Types ] .right-column[ * the `where` keyword itself can be nested inside a more complex declaration ```julia julia> const T1 = Array{Array{T,1} where T, 1} Array{Array{T,1} where T,1} julia> const T2 = Array{Array{T,1}, 1} where T Array{Array{T,1},1} where T ``` * type `T1` defines a one-dimensional array of one-dimensional arrays; each of the inner arrays consists of objects of the same type, but this type may vary from one inner array to the next * type `T2` defines a one-dimensional array of one-dimensional arrays all of whose inner arrays must have the same type * note that `T2` is an abstract type, e.g., `Array{Array{Int,1},1} <: T2`, whereas `T1` is a concrete type; consequently, `T1` can be constructed with a zero-argument constructor `a=T1()` but `T2` cannot * there is a convenient syntax for naming such types, similar to the short form of function definition syntax ```julia Vector{T} = Array{T,1} ``` which is equivalent to ```julia const Vector = Array{T,1} where T ``` * writing `Vector{Float64}` is equivalent to writing `Array{Float64,1}`, and the umbrella type `Vector` has as instances all `Array` objects where the second parameter – the number of array dimensions – is 1, regardless of what the element type is ] --- class: center, middle # Parametric Methods --- .left-column[ ## Parametric Methods ] .right-column[ * similar to types, method definitions can optionally have type parameters qualifying the signature using the same `where` syntax that is used to write types * example: define a boolean function that checks whether its two arguments are of the same type ```julia julia> same_type(x::T, y::T) where {T} = true same_type (generic function with 1 method) julia> same_type(x,y) = false same_type (generic function with 2 methods) ``` * the first method applies whenever both arguments are of the same concrete type, regardless of what type that is, while the second method acts as a catch-all, covering all other cases ```julia julia> same_type(1, 2) true julia> same_type(1, 2.0) false julia> same_type("1", 2.0) false julia> same_type(Int32(1), Int64(2)) false ``` * such definitions correspond to methods whose type signatures are `UnionAll` types ] --- .left-column[ ## Parametric Methods ] .right-column[ * in Julia this kind of definition of function behaviour by dispatch is quite common, even idiomatic * method type parameters are not restricted to being used as the types of arguments: they can be used anywhere a value would be in the signature or body of the function * example: the method type parameter `T` is used as the type parameter to the parametric type `Vector{T}` in the method signature ```julia julia> myappend(v::Vector{T}, x::T) where {T} = [v..., x] myappend (generic function with 1 method) julia> myappend([1,2,3], 4) 4-element Array{Int64,1}: 1 2 3 4 julia> myappend([1,2,3], 4.0) Error: MethodError: no method matching myappend(::Array{Int64,1}, ::Float64) Closest candidates are: myappend(::Array{T,1}, !Matched::T) where T at none:1 julia> myappend([1.0,2.0,3.0], 4.0) 4-element Array{Float64,1}: 1.0 2.0 3.0 4.0 ``` * the type of the appended element must match the element type of the vector it is appended to, or else a `MethodError` is raised ] --- .left-column[ ## Parametric Methods ] .right-column[ * example: use the method type parameter `T` as the return value ```julia julia> mytypeof(x::T) where {T} = T; mytypeof (generic function with 1 method) julia> mytypeof(1) Int64 julia> mytypeof(1.0) Float64 ``` * type parameters of methods can be constrained in a similar fashion to subtype constraints on type parameters in type declarations ```julia julia> same_type_numeric(x::T, y::T) where {T<:Number} = true same_type_numeric (generic function with 1 method) julia> same_type_numeric(x::Number, y::Number) = false same_type_numeric (generic function with 2 methods) julia> same_type_numeric(1, 2) true julia> same_type_numeric(1, 2.0) false julia> same_type_numeric("1", 2) Error: MethodError: no method matching same_type_numeric(::String, ::Int64) Closest candidates are: same_type_numeric(!Matched::T, ::T) where T<:Number at none:1 same_type_numeric(!Matched::Number, ::Number) at none:1 ``` * the `same_type_numeric` function behaves much like the `same_type` function defined before, but is only defined for pairs of numbers ] --- .left-column[ ## Parametric Methods ### Parametric Constructors ] .right-column[ * parametric types add a few wrinkles to the constructor story * recall that by default instances of parametric composite types can be constructed either with explicitly given type parameters or with type parameters implied by the types of the arguments given to the constructor ```julia julia> struct RealPoint{T<:Real} x::T y::T end julia> RealPoint(1,2) ## implicit T ## Main.##WeaveSandBox#269.RealPoint{Int64}(1, 2) julia> RealPoint(1.0,2.5) ## implicit T ## Main.##WeaveSandBox#269.RealPoint{Float64}(1.0, 2.5) julia> RealPoint(1,2.5) ## implicit T ## Error: MethodError: no method matching Main.##WeaveSandBox#269.RealPoint(::Int64, ::Float64) Closest candidates are: Main.##WeaveSandBox#269.RealPoint(::T, !Matched::T) where T<:Real at none:2 julia> RealPoint{Int64}(1, 2) ## explicit T ## Main.##WeaveSandBox#269.RealPoint{Int64}(1, 2) julia> RealPoint{Int64}(1.0,2.5) ## explicit T ## Error: InexactError: Int64(2.5) julia> RealPoint{Float64}(1.0, 2.5) ## explicit T ## Main.##WeaveSandBox#269.RealPoint{Float64}(1.0, 2.5) julia> RealPoint{Float64}(1,2) ## explicit T ## Main.##WeaveSandBox#269.RealPoint{Float64}(1.0, 2.0) ``` ] --- .left-column[ ## Parametric Methods ### Parametric Constructors ] .right-column[ * for constructor calls with explicit type parameters, the arguments are converted to the implied field types: * `RealPoint{Int64}(1,2)` works * `RealPoint{Float64}(1,2)`also works * `RealPoint{Int64}(1.0,2.5)` raises an `InexactError` when converting `2.5` to `Int64` * when the type is implied by the arguments to the constructor call, the types of the arguments must agree, otherwise `T` cannot be determined; only pairs of real arguments with matching type may be given to the generic `RealPoint` constructor * `RealPoint{T}` is a distinct constructor function for each type `T`, so that `RealPoint`, `RealPoint{Float64}` and `RealPoint{Int64}` are all different constructor *functions* * without any explicitly provided inner constructors, the declaration of the composite type `RealPoint{T<:Real}` automatically provides an inner constructor, `RealPoint{T}`, for each possible type `T<:Real`, that behaves just like non-parametric default inner constructors do * it also provides a single general outer `RealPoint` constructor that takes pairs of real arguments, which must be of the same type ] --- .left-column[ ## Parametric Methods ### Parametric Constructors ] .right-column[ * the automatic provision of constructors is equivalent to the explicit declaration ```julia struct RealPoint{T<:Real} x::T y::T RealPoint{T}(x,y) where {T<:Real} = new(x,y) end RealPoint(x::T, y::T) where {T<:Real} = RealPoint{T}(x,y); ``` * each definition looks like the form of constructor call that it handles: * the call `RealPoint{Int64}(1,2)` will invoke the definition `RealPoint{T}(x,y)` inside the struct block * the outer constructor declaration defines a method for the general `RealPoint` constructor which only applies to pairs of values of the same real type and makes constructor calls without explicit type parameters work * since the other constructor method declaration restricts the arguments to being of the same type, calls with arguments of different types, result in "no method" errors * the constructor call `RealPoint(1,2.5)` can be made to work by promoting the integer value `1` to the floating-point value `1.0` with an additional outer constructor method ```julia julia> RealPoint(x::Int64, y::Float64) = RealPoint(convert(Float64,x),y); Main.##WeaveSandBox#269.RealPoint julia> RealPoint(1,2.5) Main.##WeaveSandBox#269.RealPoint{Float64}(1.0, 2.5) ``` * this method uses the `convert` function to explicitly convert `x` to `Float64` and then delegates construction to the general constructor for the case where both arguments are `Float64` ] --- .left-column[ ## Parametric Methods ### Parametric Constructors ] .right-column[ * however, similar calls still do not work ```julia julia> RealPoint(1.5, 2) Error: MethodError: no method matching Main.##WeaveSandBox#269.RealPoint(::Float64, ::Int64) Closest candidates are: Main.##WeaveSandBox#269.RealPoint(::T, !Matched::T) where T<:Real at none:2 ``` * all it takes to make all calls to the general `RealPoint` constructor work as one would expect is the following outer method definition ```julia julia> RealPoint(x::Real, y::Real) = RealPoint(promote(x,y)...); Main.##WeaveSandBox#269.RealPoint ``` * the `promote` function converts all its arguments to a common type, here `Float64` * with this method definition, the `RealPoint` constructor promotes its arguments the same way that numeric operators like `+` do, and works for all kinds of real numbers ```julia julia> RealPoint(1.5, 2) Main.##WeaveSandBox#269.RealPoint{Float64}(1.5, 2.0) julia> RealPoint(1, 1//2) Main.##WeaveSandBox#269.RealPoint{Rational{Int64}}(1//1, 1//2) julia> RealPoint(1.0, 1//2) Main.##WeaveSandBox#269.RealPoint{Float64}(1.0, 0.5) ``` * while the implicit type parameter constructors provided by default in Julia are fairly strict, it is possible to make them behave in a more relaxed but sensible manner quite easily * since constructors can leverage all of the power of the type system, methods, and multiple dispatch, defining sophisticated behaviour is typically quite simple ] --- class: center, middle # Conversion and Promotion --- .left-column[ ## Conversion and Promotion ] .right-column[ * in Julia, no automatic casting or conversion of function arguments is ever performed: all conversion in Julia is non-magical and completely explicit * still Julia's system for conversion and promotion of arguments of mathematical operators to a common type can sometimes be indistinguishable from magic * in the following we explain how this promotion system works, how to extend it to new types and how to apply it to functions besides built-in mathematical operators * in Julia, mathematical operators are just functions with special syntax, and the arguments of functions are never automatically converted; nevertheless one may observe that applying mathematical operations to a wide variety of mixed argument types is possible * this is just an extreme case of polymorphic multiple dispatch: Julia comes with pre-defined catch-all dispatch rules for mathematical operators, invoked when no specific implementation exists for some combination of operand types * these catch-all rules first promote all operands to a common type using user-definable promotion rules, and then invoke a specialized implementation of the operator in question for the resulting values, now of the same type * user-defined types can easily participate in this promotion system by defining methods for conversion to and from other types, and providing a handful of promotion rules defining what types they should promote to when mixed with other types ] --- .left-column[ ## Conversion and Promotion ### Conversion ] .right-column[ * the standard way to obtain a value of a type `T` is to call the type's constructor `T(x)` ```julia julia> Float64(12) 12.0 ``` * there are many cases where it is convenient to convert a value from one type to another without the programmer asking for it explicitly * example: assigning a value into an array: if `A` is a `Vector{Float64}`, the expression `A[1] = 2` should work by automatically converting the `2` from `Int` to `Float64`, and storing the result in the array * this is done via the `convert` function, which generally takes two arguments: the first is a type object and the second is a value to convert to that type ```julia julia> x = 12 12 julia> typeof(x) Int64 julia> convert(UInt8, x) 0x0c julia> typeof(convert(UInt8, x)) UInt8 julia> convert(AbstractFloat, x) 12.0 julia> typeof(convert(AbstractFloat, x)) Float64 ``` ] --- .left-column[ ## Conversion and Promotion ### Conversion ] .right-column[ * the `convert` function is also applicable to more complicated types ```julia julia> a = Any[1 2 3; 4 5 6] 2×3 Array{Any,2}: 1 2 3 4 5 6 julia> convert(Array{Float64}, a) 2×3 Array{Float64,2}: 1.0 2.0 3.0 4.0 5.0 6.0 ``` * conversion is not always possible, in which case a `MethodError` is thrown indicating that `convert` doesn't know how to perform the requested conversion ```julia julia> convert(AbstractFloat, "foo") Error: MethodError: Cannot `convert` an object of type String to an object of type AbstractFloat Closest candidates are: convert(::Type{T}, !Matched::T) where T<:Number at number.jl:6 convert(::Type{T}, !Matched::Number) where T<:Number at number.jl:7 convert(::Type{T}, !Matched::Base.TwicePrecision) where T<:Number at twiceprecision.jl:250 ... ``` * some languages consider parsing strings as numbers or formatting numbers as strings to be conversions and some will even perform conversion for you automatically * as most strings are not valid representations of numbers, and only a very limited subset of them are, in Julia the dedicated `parse` function must be used to perform this operation, making it more explicit ] --- .left-column[ ## Conversion and Promotion ### Conversion ] .right-column[ * the following Julia constructs call `convert`: * assigning to an array converts to the array's element type * assigning to a field of an object converts to the declared type of the field * constructing an object with `new` converts to the object's declared field types * assigning to a variable with a declared type (e.g. `local x::T`) converts to that type * a function with a declared return type converts its return value to that type * passing a value to `ccall` converts it to the corresponding argument type * `convert` will only convert between types that represent the same basic kind of thing, e.g. different representations of numbers, or different string encodings * `convert` is usually lossless: converting a value to a different type and back again should result in the exact same value; lossy conversions are usually not supported ```julia julia> convert(Int, 12.0) 12 julia> convert(Int, 12.5) Error: InexactError: Int64(12.5) ``` * the behaviour of `convert(T, x)` appears to be nearly identical to the constructor `T(x)`, and indeed, it usually is * however, there is a key semantic difference: since `convert` can be called implicitly, its methods are restricted to cases that are considered "safe" or "unsurprising" ] --- .left-column[ ## Conversion and Promotion ### Conversion ] .right-column[ * there are certain cases where constructors differ from `convert`: * constructors for types unrelated to their arguments: some constructors do not implement the concept of "conversion", e.g., `Timer(2)` creates a 2-second timer, which is not really a "conversion" from an integer to a timer * mutable collections: `convert(T, x)` is expected to return the original `x` if `x` is already of type `T`; in contrast, if `T` is a mutable collection type then `T(x)` should always make a new collection (copying elements from `x`) * constructors that do not return instances of their own type: in *very rare* cases it might make sense for the constructor `T(x)` to return an object not of type `T`, e.g. if a wrapper type is its own inverse, such as `Flip(Flip(x)) === x` * wrapper types: for some types which "wrap" other values, the constructor may wrap its argument inside a new object even if it is already of the requested type, `convert`, on the other hand, would just return its arguments since it is already of the requested type ```julia julia> struct MyWrapper x end julia> v = MyWrapper(1) Main.##WeaveSandBox#269.MyWrapper(1) julia> w = MyWrapper(v) Main.##WeaveSandBox#269.MyWrapper(Main.##WeaveSandBox#269.MyWrapper(1)) julia> convert(MyWrapper, v) Main.##WeaveSandBox#269.MyWrapper(1) ``` ] --- .left-column[ ## Conversion and Promotion ### Conversion ### Defining New Conversions ] .right-column[ * when defining a new type, initially all ways of creating it should be defined as constructors * if it becomes clear that implicit conversion would be useful, and that some constructors meet the above "safety" criteria, then `convert` methods can be added * these methods are typically quite simple, as they only need to call the appropriate constructor, for example ```julia convert(::Type{MyType}, x) = MyType(x) ``` * the type of the first argument of this method is a singleton type, `Type{MyType}`, the only instance of which is `MyType`; thus, this method is only invoked when the first argument is the type value `MyType` * notice the syntax used for the first argument: the argument name is omitted prior to the `::` symbol, and only the type is given * this is the syntax in Julia for a function argument whose type is specified but whose value does not need to be referenced by name; since the type is a singleton, we already know its value without referring to an argument name ] --- .left-column[ ## Conversion and Promotion ### Conversion ### Defining New Conversions ] .right-column[ * for some abstract types all instances are by default considered "sufficiently similar" that a universal `convert` definition is provided in Julia `Base` * example: the following definition states that it is valid to convert any `Number` type to any other by calling a 1-argument constructor ```julia convert(::Type{T}, x::Number) where {T<:Number} = T(x) ``` * this means that new `Number` types only need to define constructors, since this definition will handle `convert` for them * an identity conversion is also provided to handle the case where the argument is already of the requested type ```julia convert(::Type{T}, x::T) where {T<:Number} = x ``` * similar definitions exist for `AbstractString`, `AbstractArray`, and `AbstractDict` ] --- .left-column[ ## Conversion and Promotion ### Conversion ### Defining New Conversions ### Promotion ] .right-column[ * promotion refers to converting values of mixed types to a single common type * it is generally implied that the common type to which the values are converted can faithfully represent all of the original values * in this sense, the term "promotion" is appropriate: the values are converted to a "greater" type, which can represent all of the input values in a single common type * it is important not to confuse this with object-oriented (structural) super-typing, or Julia's notion of abstract super-types: promotion has nothing to do with the type hierarchy, and everything to do with converting between alternate representations * example: although every `Int32` value can also be represented as a `Float64` value, `Int32` is not a subtype of `Float64` * in Julia promotion to a common "greater" type is performed by the `promote` function * it takes any number of arguments, and returns a tuple of the same number of values, converted to a common type, and throws an exception if promotion is not possible * the most typical application of promotions is the definition of catch-all methods ```julia +(x::Number, y::Number) = +(promote(x,y)...) ``` * this method definition says that in the absence of more specific rules for adding pairs of numeric values, promote the values to a common type and then try again * in outer constructors methods `promote` allows constructor calls with mixed types to delegate to an inner type with fields promoted to an appropriate common type ```julia Rational(n::Integer, d::Integer) = Rational(promote(n,d)...) ``` ] --- .left-column[ ## Conversion and Promotion ### Conversion ### Defining New Conversions ### Promotion ### Promotion of Numbers ] .right-column[ * common use case for promotion: convert numeric arguments to a common type: ```julia julia> promote(1, 2.5) (1.0, 2.5) julia> promote(1, 2.5, 3) (1.0, 2.5, 3.0) julia> promote(2, 3//4) (2//1, 3//4) julia> promote(1, 2.5, 3, 3//4) (1.0, 2.5, 3.0, 0.75) julia> promote(1.5, im) (1.5 + 0.0im, 0.0 + 1.0im) julia> promote(1 + 2im, 3//4) (1//1 + 2//1*im, 3//4 + 0//1*im) ``` * floating-point values are promoted to the largest of the argument types * integer values are promoted to the larger of either the native machine word size or the largest integer argument type * mixtures of integers and floating-point values are promoted to a floating-point type big enough to hold all the values * integers mixed with rationals are promoted to rationals * rationals mixed with floats are promoted to floats * complex values mixed with real values are promoted to the appropriate kind of complex value ] --- .left-column[ ## Conversion and Promotion ### Conversion ### Defining New Conversions ### Promotion ### Promotion of Numbers ### Defining Promotion Rules ] .right-column[ * in principle, one could define methods for the `promote` function directly; this would require many redundant definitions for all possible permutations of argument types * instead, the behaviour of promote is defined in terms of an auxiliary function called `promote_rule`, which one can provide methods for * the `promote_rule` function takes a pair of type objects and returns another type object, to which instances of the argument types will be promoted * example: a pair of 64-bit and 32-bit floating-point values should be promoted to 64-bit floating-point ```julia promote_rule(::Type{Float64}, ::Type{Float32}) = Float64 ``` * the promotion type does not need to be one of the argument types ```julia promote_rule(::Type{BigInt}, ::Type{Float64}) = BigFloat ``` * one does not need to define both `promote_rule(::Type{A}, ::Type{B})` and `promote_rule(::Type{B}, ::Type{A})`; the symmetry is implied by the way `promote_rule` is used in the promotion process * `promote_rule` is used by a second function, `promote_type`, which, given any number of type objects, returns the common type to which those values should be promoted ```julia julia> promote_type(Int8, Int64) Int64 ``` * `promote_type` is used inside of `promote` to determine what type argument values should be converted to for promotion ]