Skip to content

It's too easy to accidentally sidestep our convert(::Type{T}, ::T) no-op fallback #17559

@jrevels

Description

@jrevels

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    types and dispatchTypes, subtyping and method dispatch

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions