@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.
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:
convert
for safety, but they tend to be a bit hit-and-miss.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...
Most helpful comment
馃挴 seems much clearer and safer to me.