Clarification on function signature and dispatching behaviour in julia - julia

I was trying out some things in the Julia (1.2) REPL and I stuck my mind on something I don't understand about dispatching.
I first tried this thing which is working the way I expected:
f(a::T) where {T <: Int} = "Test"
Calling f(3) works since Int <: Int == true
Calling f("Hello") results in an "MethodError: no method matching" error since String <: Int == false
Then, I tried this method, and I don't understand why calling it works in some case:
f(a::T, b::U) where {T, U <: T} = "Another Test"
Calling f(3, 3) works (as I expected)
BUT f(3, "Hello") also works and does not throw a "MethodError: no method matching" ???
I thought that (since T becomes an Int and U a String) String <: Int == false ???
I guess I am missing something pretty straightforward here but I can't find it...
So this is my question, why f(3, "Hello") is working???
Moreover, I tried this snippet of code (i tried to recreate the second method signature) and it correctly fails as I expected:
Test = Tuple{T, U} where {T, U <: T}
Test{Int, String} (this fails as i expected with "TypeError: in Type, in U, expected U<:Int64, got Type{String}")

What's happening here is that Tcan be a datatype and U can be any supertype of string. This the conditions are fulfilled. The thing that was tripping you up with string not being a subtype of int is a red herring since no concrete type is the subtype of any other.

Ok, thanks to laborg it seems I now understand what was going on. If we take this method:
f(a::T, b::U) where {T, U <: T} = "Another Test"
'T' is the "UnionAll" aka "Iterated Union" of all possible types of 'a'.
'U' is the "UnionAll" aka "Iterated Union" of all possible types of 'b' that are subtypes of 'T'
What I was misunderstanding was the fact that if (example) a::Int, then T can take a parent abstract type of typeof(a). Since Int <: Any, then T = Any is a valid solution for dispatch.
The same for U = Any since String <: Any.
So we now have U <: T that resolves to Any <: Any == true, and the method is called!
I hope I get it :)

Related

Purpose of "where" keyword in constructor of a parametric type

For the Julia manual parametric composite type example
struct Point{T}
x::T
y::T
end
it is possible to write an outer constructor such as
Point(x::T, y::T) where {T} = Point{T}(x, y)
Why is the where {T} part needed, i.e. why isn't
Point(x::T, y::T) = Point{T}(x, y)
possible? Julia complains that T is not defined, but could it not figure out that T is a type from the :: syntax? I am a newcomer to Julia so I might be missing something pretty basic.
T is a variable that has to be defined. If you do not define it in where Julia starts looking for it in an outer scope. Here is an example:
julia> struct Point{T}
x::T
y::T
end
julia> Point(x::T, y::T) = Point{T}(x, y)
ERROR: UndefVarError: T not defined
Stacktrace:
[1] top-level scope at REPL[2]:1
julia> T = Integer
Integer
julia> Point(x::T, y::T) = Point{T}(x, y)
Point
julia> Point(1,1)
Point{Integer}(1, 1)
Note that in this example you do not get what you expected to get, as probably you have hoped for Point{Int64}(1,1).
Now the most tricky part comes:
julia> Point(1.5,1.5)
Point{Float64}(1.5, 1.5)
What is going on you might ask. Well:
julia> methods(Point)
# 2 methods for type constructor:
[1] (::Type{Point})(x::Integer, y::Integer) in Main at REPL[4]:1
[2] (::Type{Point})(x::T, y::T) where T in Main at REPL[1]:2
The point is that The (::Type{Point})(x::T, y::T) where T already got defined by default when the struct as defined so with the second definition you have just added a new method for T = Integer.
In short - always use where to avoid getting surprising results. For example if you would write:
julia> T = String
String
julia> Point(1,1)
ERROR: MethodError: Cannot `convert` an object of type Int64 to an object of type String
you get a problem. As the signature of Point is fixed when it is defined but in the body T is taken from a global scope, and you have changed it from Integer to String, so you get an error.
Expanding on what #Oscar Smith noted the location of where is also sensitive to scope, and tells Julia on what level the "wildcard" is introduced. For example:
ulia> T1 = Vector{Vector{T} where T<:Real}
Array{Array{T,1} where T<:Real,1}
julia> T2 = Vector{Vector{T}} where T<:Real
Array{Array{T,1},1} where T<:Real
julia> isconcretetype(T1)
true
julia> isconcretetype(T2)
false
julia> T1 isa UnionAll
false
julia> T2 isa UnionAll
true
julia> x = T1()
0-element Array{Array{T,1} where T<:Real,1}
julia> push!(x, [1,2])
1-element Array{Array{T,1} where T<:Real,1}:
[1, 2]
julia> push!(x, [1.0, 2.0])
2-element Array{Array{T,1} where T<:Real,1}:
[1, 2]
[1.0, 2.0]
and you can see that T1 is a concrete type that can have an instance, and we created one calling it x. This concrete type can hold vectors whose element type is <:Real, but their element types do not have to be the same.
On the other hand T2 is a UnionAll, i.e. some of its "widlcards" are free (not known yet). However, the restriction is that in all concrete types that match T2 all vectors must have the same element type, so:
julia> Vector{Vector{Int}} <: T2
true
julia> Vector{Vector{Real}} <: T2
true
but
julia> T1 <: T2
false
In other words in T2 the wildcard must have one concrete value that can be matched by a concrete type.
I'm going to take a different tack from BogumiƂ's excellent answer (which is 100% correct, but might be missing the crux of the confusion).
There's nothing special about the name T. It's just a common convention for a short name for an arbitrary type. Kinda like how we often use the name A for matrices or i in for loops. The fact that you happened to use the same name in the struct Point{T} and in the outer constructor is irrelevant (but handy for the readers of your code). You could have just as well done:
struct Point{SpecializedType}
x::SpecializedType
y::SpecializedType
end
Point(x::Wildcard, y::Wildcard) where {Wildcard <: Any} = Point{Wildcard}(x, y)
That behaves exactly the same as what you wrote. Both of the above syntaxes (the struct and the method) introduce a new name that will behave like a "wildcard" that specializes and matches appropriately. When you don't have a where clause, you're no longer introducing a wildcard. Instead, you're just referencing types that have already been defined. For example:
Point(x::Int, y::Int) = Point{Int}(x, y)
That is, this will reference the Int that was already defined.
I suppose you could say, but if T wasn't defined, why can't Julia just figure out that it should be used as a wildcard. That may be true, but it introduces a little bit of non-locality to the syntax, where the behavior is drastically different depending upon what happens to be defined (or even exported from a used package).

Underspecification when checking complex types in Julia

Given some complex object, e.g.:
> d = Dict(("a", "b")=>3, ("c", "d")=>2)
Dict{Tuple{String,String},Int64} with 2 entries:
("c","d") => 2
("a","b") => 3
I could check the type with:
> isa(d, Dict{Tuple{String, String},Int64})
true
But when I underspecify the Tuple type, it failed the check:
> isa(d, Dict{Tuple,Int64})
false
Is it possible to check for underspecified types in Julia? If so, how? If not, why?
In Julia, dictionaries, like arrays, are invariant. For example, see this question on the concept as it applies to arrays.
This means that:
julia> Int <: Number
true
but,
julia> Vector{Int} <: Vector{Number}
false
and similarly,
julia> Dict{Int, String} <: Dict{Number, String}
false
However, note that Dict on its own is abstract, so
julia> Dict{Int, String} <: Dict
true
and in the code you provide,
julia> isa(d, Dict)
true
As far as I know, if you want to reason specifically about the pair of types in your dictionary, you will need to reference them explicitly. You can do this using keytype(d) and valtype(d). For example, from your question, note that
julia> keytype(d) <: Tuple
true
Of course, if your dictionary is passed into a function, then you can use Dict{T1, T2} in the function signature, and then just reason about T1 and T2...
EDIT: See #FengyangWang's answer for a neat little syntax shortcut being introduced in v0.6
In Julia 0.6 you can simply do
isa(d, Dict{<:Tuple, Int64})
which means a Dict with key type a subtype of Tuple, and value type Int64.
Note that Dict{Tuple, Int64} is not an "underspecified" type: it is a concrete type and can have instances. However, Dict{Tuple{String, String}, Int64} is not the same thing. The former is a type that accepts all tuples as keys, whereas the latter only accepts Tuple{String, String}. This is an example of parametric invariance.

Type unstable & factory constructor

Say, I have a type hierarchy
abstract A
immutable B <: A end
immutable C <: A end
The constructor of A follows factory pattern:
function A(x::Int)
if x > 0
B()
else
C()
end
end
It returns different subtypes based on the input as expected. However, it is also type unstable as I cannot find a way to force the return type to be A.
So, is it bad to have factory pattern here? Does the type instability only affects immutable types rather than mutable types, since the latter is reference type anyway.
Do I have to opt to the parametric type for this?
immutable D{T <: A}
type::T
end
function D(x::Int)
if x > 0
D(B())
else
D(C())
end
end
It feels a bit bad.
Actually, how bad it is to have type unstable functions? Is is worthwhile to trade for better code readability?
Alternatively, should I define typealias A Union{B,C} instead?
Well, you could do this:
function A(x::Int)
if x > 0
B()::A
else
C()::A
end
end
but it doesn't help:
julia> #code_warntype A(5)
Variables:
x::Int64
Body:
begin # none, line 2:
unless (Base.slt_int)(0,x::Int64)::Bool goto 0 # none, line 3:
return $(Expr(:new, :((top(getfield))(Main,:B)::Type{B})))
goto 1
0: # none, line 5:
return $(Expr(:new, :((top(getfield))(Main,:C)::Type{C})))
1:
end::Union{B,C}
You can't create instances of an abstract type. Moreover, in current julia, any abstract type is automatically "type-unstable," meaning that the compiler can't generate optimized code for it. So there is no such thing as "forcing the return type to be A" and then having that somehow make the function type-stable (in the sense of obtaining great performance).
You can implement a type-stable factory pattern, but the output type should be determined by the input types, not the input values. For example:
A(x::Vector) = B()
A(x::Matrix) = C()
is a type-stable constructor for objects of the A hierarchy.
If there aren't obvious types to use to signal your intent, you can always use Val:
A(x, ::Type{Val{1}}) = B()
A(x, ::Type{Val{2}}) = C()
A(1, Val{1}) # returns B()
A(1, Val{2}) # returns C()

Strange behaviour of a type assert

For simplicity's sake, suppose I have the following function defined:
function returnVectorOrMatrix()
vals = Array(Array{Float32,1}, 10) # vector in this definition
return vals::Array{Array{Float32},1}
end
arr = returnVectorOrMatrix()
Which to my big surprise generates the following error:
ERROR: type: typeassert: expected Array{Array{Float32,N},1},
got Array{Array{Float32,1},1}
Does anyone have a good logical reasoning as to why this is happening / was designed in such way?
Because Array{Array{Float32,1},1} is just a special case of Array{Array{Float32,N},1} with N = 1 so given that Julia is multiple-dispatch I would expect such function to work fine (and seems logical/intuitive as well)
Hint:
julia> Array{Float32,1} <: Array{Float32}
true
julia> Array{Array{Float32,1},1} <: Array{Array{Float32},1}
false
julia> Array{Array{Float32,1},1} <: Array{Array{Float32,1}}
true
could you find any clue now?
in fact, the Array{Array{Float32,1},1} is a parametric type,
julia> Array{Array{Float32,1},1} <: Array{Array{Float32},1}
false
I think the mechanism here is the same case as:
julia> Array{Int32,1} <: Array{Int,1}
false
even if Int32 is a special case of Int, julia will return a false here.
Bacause Array{Array{Float32,1},1} is just a special case of Array{Array{Float32,N},1} with N = 1
so this statement is not true because Array{Float32,1} and Array{Float32,N} are type parameters which are invariant in julia.
I think the problem is simply that Array{Float32} is really Array{Float32,N}, and not Array{Float32,1} (which is the same as Vector{Float32}).

julia interpolation of own type with string()

I'm quite new to Julia and I'm looking into porting some Python code to Julia. This code uses __repr__() overloading to display cutsom types. I understand that Julia provides the string() method for this functionality. But I can't figure it out.
julia> type Thomas
t::Integer
end
julia> function Base.string(t::Thomas)
"---> $(t.t) <---"
end
julia> r = Thomas(8);
With these definitions I expected my string(::Thomas) function to be called whenever a value of type Thomas needed to be converted to a string. In one case, it works as expected:
julia> println("$r")
---> 8 <---
But, for the most cases it does not:
julia> println(r)
Thomas(8)
julia> println(" $r")
Thomas(8)
julia> println("r = $r")
r = Thomas(8)
julia> repr(r)
"Thomas(8)"
What did I get wrong ? Is there some other function I should define for my new custom type ?
I am running Julia 0.4.0-dev. (the code above was pasted from the REPL of Version 0.4.0-dev+3607 (2015-02-26 07:41 UTC), Commit bef6bf3*, x86_64-linux-gnu)
At the moment just overriding Base.show should be enough, as follows.
type Thomas
t::Int # note Int not Integer
end
Base.show(io::IO, x::Thomas) = print(io, "Thomas with $(x.t)")
Note that in the definition of the type, you should use the concrete type Int (equivalent to Int64 or Int32, depending on your machine's word size), not the abstract type Integer, which will lead to bad performance.
The situation with Base.show, Base.print etc. is indeed confusing at the moment, but with some recent work (look up IOContext) should get simplified and clarified soon.
You have to override two versions of Base.print actually to get a consistent behavior of string interpolation:
Base.print(io::IOBuffer, t::Thomas) = Base.print(io, "---> $(t.t) <---")
Base.print(t::Thomas) = Base.print("---> $(t.t) <---")
Then you'll have:
print(t)
string(t)
string(t, t, ...)
"$t"
"t = $t"
"$t $t $t"
and co.
You may want to override the show method as well.
Base.show(io::IO, x::Thomas) = show(io, string(x))

Resources