convert never call constructors by default, types with one field is no exception. The special case about such type is that their constructor won’t call convert by default.
I checked the documentation in the link and it said the following:
However, in some cases you could consider adding methods to Base.convert instead of defining a constructor, because Julia falls back to calling convert() if no matching constructor is found. For example, if no constructor T(args…) = … exists Base.convert(::Type{T}, args…) = … is called.
Then I tested the following but it seems inconsistent.
julia> type T
a
b
end
julia> convert(::Type{T}, args::Int...) = T(args[1], args[2])
convert (generic function with 599 methods)
julia> T(1,2,3,4)
ERROR: MethodError: no method matching T(::Int64, ::Int64, ::Int64, ::Int64)
Closest candidates are:
T(::Any, ::Any) at REPL[52]:2
T{T}(::Any) at sysimg.jl:53
For most basic types like e.g. Float64 constructors fall back to convert and therefore T(x) are identical convert(T, x).
If I assign a T2 into a field/slot of type T1, e.g. object fields, array elements, typed local variables, return type annotation, conversion happens automatically by calling convert and I do not have to specify it, e.g
x = zeros(Float64, 5)
y = one(Float32)
x[1] = y
@assert eltype(x) == Float64
The last point was what confused me, because @yuyichao said that constructors are always explicit, therefore I wondered if T(x) and convert(T, x) were no-ops for x::T.
update: corrections
update: added assignment bullet
update: clarified the assignment conversion bullet
update: expanded assignment into typed fields etc.
update: clarification: no automatic definition of constructors
Correction: “Constructor calls with a single argument will fall back to convert if they are not defined either implicitly or explicitly. A constructor with a single argument is defined implicitly for types with a single field. If the type T has multiple fields, then calling T(x), when no constructor T(x) has been explicitly defined, is like asking Julia to convert x to type T, which falls back to convert(T, x). In cases like this, you must define convert(::Type{T}, x).”
I am not sure how basic types are defined exactly, or how many fields they actually have. But they should follow the same line of thought explained above, if all is to be consistent, which I suppose it is.
I thought this was worth clarifying further. I can see this happening for basic types “intuitively”, but for general user defined types (including basic types as well), I believe convert(T1, y) is called behind the scenes, here is my evidence code.
julia> type T1
a
end
julia> type T2
a
end
julia> b = T1[T1(1), T1(2), T1(3)]
3-element Array{T1,1}:
T1(1)
T1(2)
T1(3)
julia> b[1] = T2(1)
ERROR: MethodError: Cannot `convert` an object of type T2 to an object of type T1
This may have arisen from a call to the constructor T1(...), since type constructors fall back to convert methods.
in setindex!(::Array{T1,1}, ::T2, ::Int64) at .\array.jl:415
julia> b = Int[1,2,3]
3-element Array{Int64,1}:
1
2
3
julia> b[1] = "S"
ERROR: MethodError: Cannot `convert` an object of type String to an object of type Int64
This may have arisen from a call to the constructor Int64(...), since type constructors fall back to convert methods.
in setindex!(::Array{Int64,1}, ::String, ::Int64) at .\array.jl:415
Well, true but unrelated… Anything will fail with an error if it’s not defined…
There’s nothing special about basic types. There’s just a catch-all single argument constructor as fallback unless overwritten.
This is not specific to Array but any typed field/slots. This includes object fields, Array element, typed local variable, return type annotation.
I mean constructor calls are always explicit. Julia won’t automatically insert it anywhere so you can use it to do “convertion”(“construction”) that’s less intuitive.
I don’t see how this is related to explicit vs implicit but both are true unless overwritten.
so it is still “defined”… It’s just not explicitly defined by the user.
It’s also pretty confusing to say it this way. Maybe you mean convert are never automatically defined.
function conv_float(x)
@time Array{Float64}(x) #no-op
@time convert(Array{Float64}, x) #no-op
@time Float64.(x) #converts and makes copy
@time x .= Float64.(x) #converts without copy, no-op in Julia 0.6
end
I seem to understand the theory, but what is the practical advise that follows from it? As a designer of a new type T, what method(s) should I implement to ensure interoperability with say integers - T(x::Integer) or Base.convert(::Type{T}, x::Integer)? As a user of such type, when should I call T(x) and when convert(Integer, x)?
If you want T(x) and convert(T, x) to both work, and you have no other single-argument constructor, then just defining convert(::Type{T}, x::Integer) is all you need to do. But there’s also a semantic choice that you can make when you decide which method to implement. When you define convert() for your type, you are implying that that conversion makes logical sense.
For example, you might have a new numeric type Foo that holds an integer:
struct Foo <: Number
x::Int
end
and you might want to define convert(::Type{Foo}, y::Integer) = Foo(y) since converting an integer to a Foo makes some logical sense.
But you might also have a type Bar that holds some data, whose constructor tells it how much data to hold:
struct Bar
x::Vector{Float64}
end
Bar(y::Integer) = Bar(zeros(y))
I would argue that for Foo having the conversion makes sense if Foo is going to behave somehow like the original number (for example, tracking derivatives or something). But even though you can call Bar(5), that’s not really a conversion from 5 to Bar, since the Bar type doesn’t behave anything like the number 5. So I would argue that it does not make sense to define convert(::Type{Bar}, y::Integer).
Another way to think about it is: if I have a vector v of Ts, does it make sense to say: push!(v, 5)? By default, push!() will automatically call convert(T, 5). I would argue that for Foo it makes sense for push!() to work (and convert to Foo), but for Bar it does not make sense. That’s a further argument in favor of defining convert() for Foo but not for Bar.
tl;dr convert() and constructors do similar things, but they have different meanings, and you can decide which meaning is appropriate for a given type.