-
-
Notifications
You must be signed in to change notification settings - Fork 5.7k
Description
The following ambiguity error seems wonky to me:
julia> type Foo{T<:Real}
x::T
end
# should result in the same behavior as the default inner constructor
julia> Base.convert{T}(::Type{Foo{T}}, f::Foo) = Foo{T}(T(f.x))
# have to define this, since the above method is more specific than Base's default convert no-op
julia> Base.convert{T}(::Type{Foo{T}}, f::Foo{T}) = f
julia> f = Foo{Int}(1)
Foo{Int64}(1)
julia> convert(Foo{Int}, f)
ERROR: MethodError: convert(::Type{Foo{Int64}}, ::Foo{Int64}) is ambiguous. Candidates:
convert{T}(::Type{Foo{T}}, f::Foo{T}) at REPL[2]:1
convert{T}(::Type{Foo{T}}, f::Foo) at REPL[1]:1
in eval(::Module, ::Any) at ./boot.jl:234
in macro expansion at ./REPL.jl:92 [inlined]
in (::Base.REPL.##1#2{Base.REPL.REPLBackend})() at ./event.jl:46
Additionally, this kind of behavior can result in silent copies (or worse) whenever implicit conversions occur:
julia> type Baz{T<:Real}
x::Vector{T}
end
# Imagine I had some convert method in this case which did something
# relatively expensive (e.g. make a copy). Obviously, this specific implementation
# is dumb, but it's just an example.
julia> Base.convert{T}(::Type{Baz{T}}, b::Baz) = Baz{T}(convert(Vector{T}, copy(b.x)))
julia> Base.convert{T}(::Type{Baz{T}}, b::Baz{T}) = b
julia> type Wrap{B}
b::B
end
julia> b = Baz([1,2,3])
Baz{Int64}([1,2,3])
# implicit conversion caused a copy, such that b !== w.b
julia> w = Wrap(b)
Wrap{Baz{Int64}}(Baz{Int64}([1,2,3]))
julia> w.b.x[1] = 100
100
julia> w
Wrap{Baz{Int64}}(Baz{Int64}([100,2,3]))
julia> b
Baz{Int64}([1,2,3])
Note that the above examples don't have this behavior if the T
parameter isn't restricted to T<:Real
in the type definition.
Now, these examples were silly, but they show that it's currently very easy to screw this up and get weird behavior and performance problems as a result. convert
unexpectedly copying data is a problem that I've seen in real code - it showed up in our own Tuple
code a while ago, and I've just spent a couple of hours chasing a bug this behavior caused in a prototype package I'm working on.
I'd argue that the root of this problem is that our current no-op convert fallback (convert{T}(::Type{T}, x::T) = x
) is too easy to accidentally sidestep. To fix this, I propose that all new type definitions also add the following method to Base.convert
:
Base.convert{T<:MyType}(::Type{T}, x::T) = x
It still wouldn't help in messier cases like #11767, but AFAIK, it'd at least be more specific than our current fallback.