Many non-concrete types have constructors, for example we have AbstractFloat(0.3) == 0.3
and Union{Int,Float64}(0.3) == 0.3
. Should we also have a method like Any(x) = x
? Then we would have map(Any, [3,4,5]) == map(identity, [3,4,5])
.
Is there any real application to that? E.g. are some codes producing error messages?
Usually, one should anyway prefer convert(Any, x)
to convert to another datatype.
Adding such a method would make Julia a bit more internally consistent, so a bit friendlier to those who are still learning the language.
I just saw the GitHub PR. However, it is unclear to me if the right answer would be in that case that one should use convert
instead of assuming a constructor exists.
E.g. referring to your PR, the following works already
convert(Union{String, Int}, 4)
I could imagine that there are many more corner cases where a constructor is missing for some weird type or intentionally not allowed, but it is reasonable to have a convert
.
Yes, convert
exists, but TBH that’s not really relevant to this topic, as we already do have many non-concrete constructors.
We need to clearly distinguish conversion behavior and identity behavior, which is a little weird because convert
does both.
For convert
s where the input type does not match the target type, convert
has conversion methods. For Number
s, convert(::Type{T}, x::Number) where {T<:Number} = T(x)::T
forwards to the specified type’s constructor that implements the conversions. Note this includes defined Number
subtypes like AbstractFloat
, not type unions that aren’t specifically defined, so pushing 1
to a Vector{Union{Float32, Float64}}
errors even though it would work for a Vector{Float32}
or Vector{Float64}
.
For convert
s where identity behavior occurs, convert
has identity-like methods. I don’t know why there’s so many, but there’s one for Type{T}
, Any
, and Number
in Base
.
The constructors with identity behavior like Union{Int,Float64}(0.3)
, AbstractFloat(0.3)
, and even Float64(0.3)
actually use (::Type{T})(x::T) where {T<:Number} = x
from boot.jl in Core
. These are not used by convert
with identity behavior. It makes sense for this fallback to exist because it’d be strange if the specifically defined types’ constructors only worked on different types, but again this additionally covers type unions.
Taking all that together, it seems like the identity behavior is only as flexible and justified as it is for Number
s where constructors also implement type conversion. There’s no reason to replicate this identity behavior for other types like Function
and Any
where you don’t want conversion e.g. what on earth would Function(0.3)
do? Any
being the universal supertype shouldn’t ever convert, sure, but there’s no reason to do this when convert
exists to handle both conversion and identity behavior for all types. It’s informative to let Function
and Any
lack any constructors like other defined abstract types by default, even printing so in the MethodError
instead of listing close method matches.
I’m confused. Why are you bringing up convert
and Function
? This seems completely off-topic.
BTW I’m definitely not advocating for Function(0.3)
to do anything but throw (0.3 isa Function
doesn’t hold), as it already does.
Because SteffenPL brought up convert(Any, x)
for type conversion, and Function
is a constructor-less abstract type like Any
is. Function
and Any
also happen to share the limitation of being disallowed type annotations of the callable in a method definition. If you’re saying that type conversion is unrelated to identity, that’s my point. convert(Any, x)
does not perform type conversion as SteffenPL said, it uses an identity-like method.
Despite that, I agree that identity
should be used for identity and convert
should be used for both conversion and identity (which is something you want for generic functions), instead of adding identity constructors beyond Number
. Feature bloat is already bad, this would be non-feature bloat because we already have clearer and better ways. It wouldn’t actually be consistent for the language because 1) it’s normal for behaviors to be limited to an abstract type like Number
and all its subtypes, 2) Any
shouldn’t ever convert inputs like Number
subtypes do, 3) identity constructors would still not exist for most subtypes of Any
, which would lack the consistency of Number
described in (1).
This would be breaking to add in general, because constructors should create new, independent instances, e.g.:
julia> v = [1,2,3];
julia> v2 = Vector{Int}(v);
julia> v === v2
false
So having (::Type{T1})(x::T2) where {T1, T2 <: T1} = x
would at the very least violate this rule, and perhaps even be breaking. It’s fine for Number
because Number
s are immutable (this is undocumented, but it’s assumed to be true in Julia), and so new instances are indistinguishable from old ones.
Technically mutable Number
s are an important minority, though it beats me how you mutate them:
julia> x = BigInt(3)
3
julia> y = BigInt(x) # identity constructor
3
julia> x === y
true
julia> ismutable(x)
true
BigInt
and BigFloat
are semantically immutable, meaning they can technically be mutated by reaching into internal functions, but Base Julia assumes one never does that. This is a dumb, undocumented gotcha in Julia, and based on the idea that Number
s are always immutable (despite their implementation).
For example
julia> f() = big"1" # always returns 1
f (generic function with 1 method)
julia> x = Base.GMP.MPZ.add!(f(), big"2")
3
julia> f() # oops
3