Why can I `deepcopy(nothing)` but not `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). Numbers 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).

1 Like

See previous discussion of copy for immutable objects:

5 Likes

Why do you need that? Cannot you call ismutable before and decide if you will copy or not based on that?

1 Like

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
1 Like

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.