Convert() vs constructors

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

That doc is out of date. Don’t define multiple argument convert. It doesn’t make any sense.

3 Likes

I find this thread more confusing than helpful, may I summarize, please correct me if I am wrong:

  • Constructors and convert can be defined independently of each other.
  • Constructor calls with a single argument will fall back to convert if they are not defined.
  • In contrast to constructors (see https://docs.julialang.org/en/stable/manual/constructors/), methods for convert are never defined automatically.
  • 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

7 Likes

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.

1 Like

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

yes, with “conversion happens automatically” I meant a call to convert.

1 Like

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.

2 Likes

Some type constructors are automatically defined.

Thanks, for clarifying that.

I updated my post above.

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.

if you come up with a better way to express it, I will edit my post.

1 Like

Following up on this:

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

gives me the following timings in 0.5.2:

julia> conv_float(randn(1000000));
  0.000000 seconds
  0.000000 seconds
  0.001493 seconds (3 allocations: 7.629 MB)
  0.000854 seconds (1 allocation: 16 bytes)

and in 0.6:

julia> conv_float(randn(1000000));
  0.000000 seconds
  0.000000 seconds
  0.001451 seconds (2 allocations: 7.629 MiB)
  0.000000 seconds

So in 0.6 broadcast can optimize the last line to a no-op.

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.

6 Likes

If I want to explicitly convert an object foo of type Foo to Bar, what should I do Bar(foo) or convert(Bar, foo)?

3 Likes