I know it sounds silly but it is currently blocking an AD package of mine ^^
julia> deepcopy(nothing)
julia> copy(nothing)
ERROR: MethodError: no method matching copy(::Nothing)
I know it sounds silly but it is currently blocking an AD package of mine ^^
julia> deepcopy(nothing)
julia> copy(nothing)
ERROR: MethodError: no method matching copy(::Nothing)
While we’re at it, one can’t copy(missing)
either. While there isn’t a meaningful use of copy
for these, it seems “harmless” to extend copy
to them (like we already do for Number
).
More broadly, there’s a philosophical question of whether an object that cannot alias should have copy
defined. Pragmatically, however, it’s darn convenient to be able to “copy
” any object even when this is a no-op so that one can write generic code (I imagine this is your situation). Number
s fit under this pragmatic case, just like we allow one to getindex
them.
Going even more broadly, there’s a question of exactly what sense you want a copy. For example, what does it mean to copy
an IOStream
? Does deepcopy
even do what you want?
Now back to non-esoteric cases. I guess one part of the reason many methods don’t exist is that this is pretty whack-a-mole. A generic fallback copy
can’t exist because there’s no way to ensure it properly de-aliases instances in a semantically-correct way. It’s hard to anticipate every Base
type one might try to copy
so that a valid method can be added, and hard to ensure every package out there also defines a valid copy
on its types. So when they fail, one can either replace copy
(see below) or turn to piracy (never a durable solution).
I can’t find where I actually used it right now, but I’ve definitely used patterns like
maybecopy(x) = copy(x) # fallback
maybecopy(x::Tuple) = maybecopy.(x) # copy each entry, like an Array
maybecopy(x::NamedTuple{N}) where N = NamedTuple{N}(maybecopy(Tuple(x))) # copy underlying tuple
for T in [Nothing, Missing, Symbol, String, #= etc =#]
@eval maybecopy(x::$T) = x
end
# add methods as MethodErrors emerge
to make a usable copy
-like function for generic code. For a tiny bit more smarts, you can replace the fallback with either of
# we know the final option will throw a MethodError that we can try to deal with
maybecopy(x::T) where T = hasmethod(copy, Tuple{T}) ? copy(x) : isbitstype(T) ? x : copy(x)
# deepcopy is reliable but has devastatingly poor performance
maybecopy(x::T) where T = hasmethod(copy, Tuple{T}) ? copy(x) : isbitstype(T) ? x : deepcopy(x)
so that at least a good number of basic types are covered. There’s a chance that no-op copy
of even an isbitstype
is somehow not correct for some esoteric types, but I can’t think of any so I include it here. deepcopy
should be avoided (for performance) so I’d recommend adding what methods you can find before enabling it.
But if your code needs to work with Base.copy
directly (i.e., for user-passed functions) then you obviously can’t use a pattern like this (maybe you could with Cassette.jl
? but that feels excessive and possibly still fragile).
See previous discussion of copy
for immutable objects:
Why do you need that? Cannot you call ismutable
before and decide if you will copy or not based on that?
No because ismutable
just checks if the object’s type is a mutable type, which might not be the intended meaning of “is it mutable”. For example
julia> struct A
x
end
julia> ismutable(A([]))
false
In my case, because I wanna differentiate through a mutable function f!(y, x)
which returns nothing
since it does the modifications in-place.
The experimental Tapir.jl package that I use copies the output of f!
regardless of what it is, hence my surprise when I discovered this loophole.