Julia: Remove implicit conversion constructors from Number types

Created on 24 Sep 2018  路  7Comments  路  Source: JuliaLang/julia

@jrevels brings up an interesting point that implicit conversions via Number constructors can be ambiguous, for example:

julia> struct A <: Number
           value
       end

julia> a = A(1)
A(1)

# is this asking us to convert or asking us to construct nested `A`s?
julia> A(a)

Because of this he removed these constructors from ForwardDiff and ReverseDiff. However, this broke a lot of generic codes, which spawned

https://github.com/JuliaDiff/ForwardDiff.jl/pull/342
https://github.com/JuliaDiff/ForwardDiff.jl/issues/336
https://github.com/JuliaDiff/ForwardDiff.jl/pull/347
https://github.com/JuliaDiff/ReverseDiff.jl/pull/112

and Slack discussions to fix generic codes. That is fine, for now, but the better solution is probably to just stop relying on this implicit conversion. It's not just user-codes like DiffEq that have been doing it, but in Base there's things like

sign(x::Real) = ifelse(x < 0, oftype(one(x),-1), ifelse(x > 0, one(x), typeof(one(x))(x)))

and thus if someone uses a lot of Base, they are accidentally making use of this assumption, which can then give issues with things like nesting Dual numbers for example. So in order to fully fix these issues, this pun would need to be eliminated from Base.

Most helpful comment

For fixed point numbers we've really insisted on reinterpret when your goal is to use the same bit pattern but interpret it differently. Somehow that seems clearer than differentiating between construction and conversion.

馃挴 seems much clearer and safer to me.

All 7 comments

I think it's a good thing to change instances of T(x) to convert(T, x) when conversion semantics are what's needed. Is that sufficient, or are you also suggesting removing the constructors themselves, so that e.g. Int(x) is no longer allowed? That doesn't seem practical.

I think that is fine. The constructors for those specific types make sense, but for example in generic functions for Real we shouldn't make the assumption that the constructor always means convert.

Somewhat related: The one that always gets me is fixed point numbers. When dealing with colors, I know that 0xff is bright, 0x77 is medium, etc, and that a N0f8 simply wraps 8 bits - yet the single argument constructor doesn't accept it's single field, but instead does the conversion.

I would prefer following a strong semantic seperation between construction and conversion. I'm not sure how to best do that... for primitive types like Float64 I wonder if it's fair to say that they can't really be "constructed" by anything (other than another Float64, I suppose, or by reinterpretting 64 bits) but that you should make liberal use of convert instead? There's plenty of code that looks like T(Inf) and T(0) out there that would need to change, however...

yet the single argument constructor doesn't accept it's single field, but instead does the conversion.

This is indeed a bit of legacy from when constructors fell back on convert. Nevertheless the following is a bit scary:

julia> struct Times2 <: Number
           val::Float64
       end

julia> Base.convert(::Type{Times2}, x::Number) = Times2(2*x)

julia> dump(Times2(3.3))
Times2
  val: Float64 3.3

julia> dump(convert(Times2, 3.3))
Times2
  val: Float64 6.6

which is essentially what N0f8(0xff) would be doing in a brave new world where these are distinguished (since 0xff == 255 and 255 != 1.0).

For fixed point numbers we've really insisted on reinterpret when your goal is to use the same bit pattern but interpret it differently. Somehow that seems clearer than differentiating between construction and conversion.

For fixed point numbers we've really insisted on reinterpret when your goal is to use the same bit pattern but interpret it differently. Somehow that seems clearer than differentiating between construction and conversion.

馃挴 seems much clearer and safer to me.

Looking at this abstractly, there are two options: (1) each type's constructor is totally idiosyncratic and specific to that type (e.g. N0f8(0xff) == 1), or (2) constructors within a family of types obey some sort of interface (e.g. T(x::Number) == x for all T <: Number). Both options are reasonable, and in fact very early in the project I tended towards option (1) and tried to avoid "generic construction" (writing T(x) where T is not exactly known). But the pull of (2) has been very strong, and I would say that ship has sailed. It has proven unrealistic to try to stop people from writing T(x) generically.

Sure, the ship has sailed... thus I'm vaguely wondering if the only practical things we can do is either:

  1. Leave it be. I suppose linting tools could recommend usage of convert for safety, but they tend to be a bit hit-and-miss.
  2. Recall the ship. Remove constructors for primitive Number types in Base in Julia 2.0, which will force the user to use convert in generic Number code if that's what they mean (or occassionally reinterpret where that is useful). This is a version of Jeff's (1) but leaves users in no doubt at all that Number constructors have type-specific behavior. Obviously, a major breaking change...
Was this page helpful?
0 / 5 - 0 ratings

Related issues

TotalVerb picture TotalVerb  路  3Comments

ararslan picture ararslan  路  3Comments

iamed2 picture iamed2  路  3Comments

Keno picture Keno  路  3Comments

i-apellaniz picture i-apellaniz  路  3Comments