Same constructors, less code written

Now that Julia constructors are so well supported, we could avoid writing convert functions where there is a promote_rule and the proper conversion is to apply the promotion selected type constructor.

Please see my next comment (post 3) for a use case.

1 Like

Is it though? I always thought that convert can be a no-op (convert(::Type{T}, a::T) === a), whereas constructor always create a new object? Replacing converts by calls to constructor (what I understand you suggest) leads to data copy e.g. when assigning to structs fields (which calls convert), hence major inefficiency.

julia> v = [1,2,3];

julia> convert(Vector{Int}, v) === v
true

julia> Vector{Int}(v) === v
false

I mean for this use to occur when appropriate, where there is a promotion and to realize the promotion, a conversion must occur. Often, the conversion is accomplished by applying a constructor of the promoted type to the targeted type. When there is need for special conversion logic, we have convert. For the other, common, occurrences, Julia could just use the appropriate constructor when it is available.

struct MyInt
  val::Int
end

Base.promote_rule(::Type{MyInt}, ::Type{Int}) = MyInt
# MyInt(x::Int) already constructs a MyInt ... yet

julia> add(x::Int, y::MyInt) = add(promote(x,y)...)
julia> add(x::MyInt, y::MyInt) = MyInt(x.val + y.val)

julia> anint = 8; myint = MyInt(5);
julia> add(anint, myint)
ERROR: MethodError: Cannot `convert` an object
   of type Int64 to an object of type MyInt
Closest candidates are:
  convert(::Type{T}, ::T) where T at essentials.jl:171
  MyInt(::Int64) at REPL[1]:2
  MyInt(::Any) at REPL[1]:2

julia> # this next line fixes it, imo it should not be needed
julia> Base.convert(::Type{MyInt}, x::Int) = MyInt(x)
julia> add(int, myint)
MyInt(13)

this the convert you suggest is already defined for “arithmetic types”, i.e. if You make MyInt <: Number:

julia> struct MyInt <: Number
         val::Int
       end

julia> Base.promote_rule(::Type{MyInt}, ::Type{Int}) = MyInt

julia> add(x::Int, y::MyInt) = add(promote(x,y)...)
add (generic function with 2 methods)

julia> add(x::MyInt, y::MyInt) = MyInt(x.val + y.val)
add (generic function with 2 methods)

julia> anint = 8; myint = MyInt(5);

julia> add(anint, myint)
MyInt(13)

It’s a valid question though if we’d want to have converts to fallback to constructors, unconditionally. I’d say no, as constructors for more complex types usually require much more information so You’d need to implement those converts nonetheless. I’ve seen also packages opting out of using promote rules even for arithmetic types , but your use-case may be different.

My experience has been that constructors for nonsimple types are stratified, keeping the specifics of working away from the constructive api. Using AbstractFloat as an abstract supertype (often a closest fitting supertype for me) caused problems in the past, because of the very special attention given Floating Point types from the early dev days. I do not know if this has been revisited. Falling back to Real pulls in Bool (Bool <: Real), and that brings the need for unobvious accommodations to preclude ambiguities. Number is useful for broader than Complex numeric (and for Complex too, as there is no abstract type between Complex and Number). Yet Number and Real feel too broad a blanket for e.g. Float8.

It’s not that I disagree with the view you write, I just see some low hanging fruit. If my construct benefits from some specialized conversion logic, Julia already facilitates that. Where my construct comports with recent Julia best practice (favoring the use of Constructors over Conversions where there is no special reason to have Conversions doing that work) and the way it is promulgating in Base and through the ecosystem, why not apply the constructor if one has defined a constructor that accepts the promotion target type.

The absence of such a constructor would raise a similar error to that we see at present. The presence of such a constructor in the absence of a defined converter for the promotion target type very strongly indicates the intent of the software designer. There is no additional benefit to explicitly typing (essentially rewriting) that intent, as all the methods and relevant contexts are fully at hand. So we allow that “Julia just works” correctly with a dollop more serendipity.

1 Like

I am confused by this. At what point were they weren’t well-supported? What changed?

AFAIK they are one of the oldest feature of the language.

…so well supported in the designs of community packages
(years ago, explicit calls to convert were more common,
and constructors were more focused, like funnels)

AFAIK there was a change in the language, see

https://github.com/JuliaLang/julia/issues/15120

and related issues/PRs.