Having some trouble with Julia's conversion rules - julia

In the Julia documentation manual, it says the following [1]:
When is convert called?
The following language constructs call convert:
Assigning to an array converts to the array's element type.
[1] https://docs.julialang.org/en/v1/manual/conversion-and-promotion/#When-is-convert-called?-1
I've defined the following code:
julia> abstract type Element end
julia> abstract type Inline <: Element end
julia> struct Str <: Inline
content::String
end
julia> convert(::Type{Str}, e::String) = Str(e)
convert (generic function with 1 method)
julia> convert(::Type{Element}, e::String) = convert(Str, e)
convert (generic function with 2 methods)
I have convert defined for Julia type String. And converting to Element and converting to Str from an instance of type String works as expected. However, the following fails:
julia> convert(Str, "hi")
Str("hi")
julia> convert(Element, "hi")
Str("hi")
julia> arr = Element[]
0-element Array{Element,1}
julia> push!(arr, "hi")
ERROR: MethodError: Cannot `convert` an object of type String to an object of type Element
Closest candidates are:
convert(::Type{T}, ::T) where T at essentials.jl:168
Stacktrace:
[1] push!(::Array{Element,1}, ::String) at ./array.jl:866
[2] top-level scope at REPL[25]:1
julia> arr = Str[]
0-element Array{Str,1}
julia> push!(arr, "hi")
ERROR: MethodError: Cannot `convert` an object of type String to an object of type Str
Closest candidates are:
convert(::Type{T}, ::T) where T at essentials.jl:168
Str(::String) at REPL[19]:2
Str(::Any) at REPL[19]:2
Stacktrace:
[1] push!(::Array{Str,1}, ::String) at ./array.jl:866
[2] top-level scope at REPL[27]:1
julia>
Can someone explain why the above fails? And how, if it is possible to do so, prevent it from failing?

the main hint of your code was this line:
convert (generic function with 1 method)
that means that the convert function has only one definition (your definition), so something was wrong. when Julia uses convert, it actually calls Base.convert. so, overloading the proper method:
Base.convert(::Type{Str}, e::String) = Str(e)
Base.convert(::Type{Element}, e::String) = convert(Str, e)
worked as expected with your code.
In a nutshell, your code was right, you just were overloading the wrong convert (add always Base.method when working with Julia core functions)

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).

Is it possible to have "public/global" fields in Julia structs?

In Julia it is possible to have public fields in functions for instance
function foo(arg)
global a = arg
a
end
Is it possible to achieve something similar using Julia structures.
For instance what I would like to do is:
julia> struct foobarfoo
global a
end
julia>
julia> test = foobarfoo(1)
ERROR: MethodError: no method matching foobarfoo(::Int64)
Stacktrace:
[1] top-level scope at none:0
julia> a
ERROR: UndefVarError: a not defined
Instead of:
julia> struct foobarfoo
a
end
julia> test = foobarfoo(1)
foobarfoo(1)
julia> test.a
1
julia>
I think that the short answer is no but you may be able to achieve what you want using the #unpack macro of Parameters.jl.

Parametric composite type inner constructors in Julia: Why is `where {T}` necessary?

Why does this raise LoadError: UndefVarError: T not defined:
struct Point{T}
x::T
y::T
Point{T}(x,y) = new(x,y)
end
while this works fine:
struct Point{T}
x::T
y::T
Point{T}(x,y) where {T} = new(x,y)
end
?
It seems that once upon a time they were both fine: Inner constructor of parametric type
EDIT: To clarify, I would have expected the fact that we are within the struct Point{T} block to have made it clear what T refers to even in the first case.
Without where clause T is inherited from the global scope (which is kind of surprising, but this is how it works):
julia> T = String
String
julia> struct Point{T}
x::T
y::T
Point{T}(x,y) = new(x,y)
end
julia> Point{String}("b","a")
Point{String}("b", "a")
julia> Point{String}(SubString("b",1,1),SubString("a",1,1))
Point{String}("b", "a")
julia> Point{Int}(1, 2)
ERROR: MethodError: no method matching Point{Int64}(::Int64, ::Int64)
julia> Point{String}(1, 2)
ERROR: MethodError: Cannot `convert` an object of type Int64 to an object of type String
EDIT
The short answer, given the comments on Discourse, is that the reason for this is that T of struct is not known at the moment an inner constructor is called.

How to define a parametric type over parametric type?

Suppose I have types
immutable X{T}
a::T
end
immutable Y{T}
a::T
end
I would like to do something like
type A{T, U}
x::U{T}
y::T
end
So that the instances could be A(X(a), a) or A(Y(a), a)
It doesn't work as
LoadError: TypeError: Type{...} expression: expected Type{T}, got TypeVar
What's the correct way for it?
As the error says, U is a TypeVar, not a Type. The answer is to make U a real type:
julia> abstract U{T}
julia> immutable X{T} <: U{T}
a::T
end
julia> immutable Y{T} <: U{T}
a::T
end
julia> type A{T}
x::U{T}
y::T
end
julia> A(X(1),1)
A{Int64}(X{Int64}(1),1)
julia> A(X(1),1.)
ERROR: MethodError: no method matching A{T}(::X{Int64}, ::Float64)
Closest candidates are:
A{T}{T}(::U{T}, ::T) at REPL[4]:2
A{T}{T}(::Any) at sysimg.jl:53
julia> A(Y(1),1)
A{Int64}(Y{Int64}(1),1)

how to make Julia detect incompatible types on assignment?

Sorry for the basic question, googling was not too useful so far.
I am a newbie. I'd like to ask if it is possible to have Julia automatically detect incompatible types on assignments.
For example, when I write
julia> x=10;
julia> typeof(x)
Int32
julia> y=9.0;
julia> typeof(y)
Float64
julia> x=y // I'd like this to generate an error or a warning at least
9.0
julia> typeof(x) // do not want automatic type conversion
Float64
I found that if change the assignment to
julia> (x=y)::Int32
ERROR: type: typeassert: expected Int32, got Float64
But I do not want to write this all the time, and want Julia to automatically detect this.
I tried to do declarations like this below, but I seem to be doing something wrong.
julia> x::Int32=10;
julia> y::Float64=9.0
ERROR: type: typeassert: expected Float64, got Int32
For example, in Java, this generates a compile error:
public class App{
public static void main (String[] args) {
int x=10;
double y=9.0;
x=y;
}
}
error: incompatible types: possible lossy conversion from double to int x=y;
What do I need to change to make this happen? do I need to declare x,y with the correct type before? how? I am using Julia 0.3 on Linux.
Values represented by variables can be qualified to always have a particular type, and incompatible values will be checked, but the check for this violation occurs at run time. From the documentation:
When appended to a variable in a statement context, the :: operator means something a bit different: it declares the variable to always have the specified type, like a type declaration in a statically-typed language such as C. Every value assigned to the variable will be converted to the declared type using the convert function.
here is a important caveat, though
Currently, type declarations cannot be used in global scope, e.g. in the REPL, since Julia does not yet have constant-type globals.
So your experiment does not work because you're using globals. However, type assertions using variables scoped in a function do.
So taking your example
Case I
julia> main()=(x::Int64=10;y::Float64=9.0;x=y)
main (generic function with 1 method)
julia> main()
9.0
That looks like the wrong thing happened, but the result of a function is last expression evaluated. So in Case I, the assignment expression returns a Float64 value, though x is still implicitly assigned 9.0 converted to an Int64 or 9. But in Case II, the last expression is simply x which is Int64.
Case II
julia> main()=(x::Int64=10;y::Float64=9.0;x=y;x)
main (generic function with 1 method)
julia> main()
9
julia> typeof(main())
Int64
When y takes on a value that can't be converted without loss to Int64 an error is thrown
Case III
julia> main()=(x::Int64=10;y::Float64=9.9;x=y;x)
main (generic function with 1 method)
julia> main()
ERROR: InexactError()
in main at none:1
The Guts
You can use the function code_typed to see what is going to more precisely. Consider the following:
julia> f(y::Float64)=(x::Int64=y)
f (generic function with 1 method)
julia> code_typed(f,(Float64,))
1-element Array{Any,1}:
:($(Expr(:lambda, {:y}, {{:x},{{:y,Float64,0},{:x,Int64,18}},{}}, :(begin # none, line 1:
x = top(typeassert)(top(box)(Int64,top(checked_fptosi)(Int64,y::Float64))::Int64,Int64)::Int64
return y::Float64
end::Float64))))
julia> f(y::Float64)=(x::Int64=y;x)
f (generic function with 1 method)
julia> code_typed(f,(Float64,))
1-element Array{Any,1}:
:($(Expr(:lambda, {:y}, {{:x},{{:y,Float64,0},{:x,Int64,18}},{}}, :(begin # none, line 1:
x = top(typeassert)(top(box)(Int64,top(checked_fptosi)(Int64,y::Float64))::Int64,Int64)::Int64
return x::Int64
end::Int64))))

Resources