copy(::CartesianIndices)

copy(x::CartesianIndices) and copy(x::LinearIndices) do not return the same type as the input argument.

I guess you could argue that it doesn’t make sense to copy an immutable. But perhaps you could also argue that trying to copy an immutable should either return the correct type or result in a “no matching method” error.

There is a precedent for defining copy for immutables. Here’s a definition for copying ranges from Base:

# Ranges are immutable
copy(r::AbstractRange) = r

So let’s try copying CartesianIndices and LinearIndices

dims = ntuple(d -> -1:1, 2)
CIs = CartesianIndices(dims)
LIs = LinearIndices(CIs)
julia> copy(CIs)
3×3 Array{CartesianIndex{2},2}:
 CartesianIndex(-1, -1)  CartesianIndex(-1, 0)  CartesianIndex(-1, 1)
 CartesianIndex(0, -1)   CartesianIndex(0, 0)   CartesianIndex(0, 1)
 CartesianIndex(1, -1)   CartesianIndex(1, 0)   CartesianIndex(1, 1)

julia> copy(LIs)
3×3 Array{Int64,2}:
 1  4  7
 2  5  8
 3  6  9

The fallback for copy seems to go through similar which returns an Array for these types.

For non-standard indexing, an OffsetArray is returned (if OffsetArrays are in scope, otherwise there’s a no method matching error).

Summary
dims = ntuple(d -> -1:1, 2)
CIs = CartesianIndices(Base.IdentityUnitRange.(dims))
LIs = LinearIndices(CIs)
julia> copy(CIs)
ERROR: MethodError: no method matching similar(::CartesianIndices{2,Tuple{Base.IdentityUnitRange{UnitRange{Int64}},Base.IdentityUnitRange{UnitRange{Int64}}}}, ::Type{CartesianIndex{2}}, ::Tuple{Base.IdentityUnitRange{UnitRange{Int64}},Base.IdentityUnitRange{UnitRange{Int64}}})
Closest candidates are:
  similar(::AbstractArray, ::Type{T}) where T at abstractarray.jl:629
  similar(::AbstractArray, ::Type{T}, ::Union{Integer, AbstractUnitRange}...) where T at abstractarray.jl:632
  similar(::AbstractArray, ::Type{T}, ::Tuple{Vararg{Int64,N}}) where {T, N} at abstractarray.jl:640
  ...
Stacktrace:
 [1] similar(::CartesianIndices{2,Tuple{Base.IdentityUnitRange{UnitRange{Int64}},Base.IdentityUnitRange{UnitRange{Int64}}}}, ::Type{CartesianIndex{2}}) at .\abstractarray.jl:629
 [2] similar(::CartesianIndices{2,Tuple{Base.IdentityUnitRange{UnitRange{Int64}},Base.IdentityUnitRange{UnitRange{Int64}}}}) at .\abstractarray.jl:628
 [3] copymutable at .\abstractarray.jl:971 [inlined]
 [4] copy(::CartesianIndices{2,Tuple{Base.IdentityUnitRange{UnitRange{Int64}},Base.IdentityUnitRange{UnitRange{Int64}}}}) at .\abstractarray.jl:915
 [5] top-level scope at REPL[4]:1

julia> copy(LIs)
ERROR: MethodError: no method matching similar(::LinearIndices{2,Tuple{Base.IdentityUnitRange{UnitRange{Int64}},Base.IdentityUnitRange{UnitRange{Int64}}}}, ::Type{Int64}, ::Tuple{Base.IdentityUnitRange{UnitRange{Int64}},Base.IdentityUnitRange{UnitRange{Int64}}})
Closest candidates are:
  similar(::AbstractArray, ::Type{T}) where T at abstractarray.jl:629
  similar(::AbstractArray, ::Type{T}, ::Union{Integer, AbstractUnitRange}...) where T at abstractarray.jl:632
  similar(::AbstractArray, ::Type{T}, ::Tuple{Vararg{Int64,N}}) where {T, N} at abstractarray.jl:640
  ...
Stacktrace:
 [1] similar(::LinearIndices{2,Tuple{Base.IdentityUnitRange{UnitRange{Int64}},Base.IdentityUnitRange{UnitRange{Int64}}}}, ::Type{Int64}) at .\abstractarray.jl:629
 [2] similar(::LinearIndices{2,Tuple{Base.IdentityUnitRange{UnitRange{Int64}},Base.IdentityUnitRange{UnitRange{Int64}}}}) at .\abstractarray.jl:628
 [3] copymutable at .\abstractarray.jl:971 [inlined]
 [4] copy(::LinearIndices{2,Tuple{Base.IdentityUnitRange{UnitRange{Int64}},Base.IdentityUnitRange{UnitRange{Int64}}}}) at .\abstractarray.jl:915
 [5] top-level scope at REPL[5]:1

Would it make sense to define the following copy methods?

Base.copy(x::CartesianIndices) = x
Base.copy(x::LinearIndices) = x

My opinion is that copy(x) = x is the right thing for immutables. While it is a useless operation if you know that x is immutable, it can be useful in generic code. I think it would be nice to just open a PR to julialang/julia with the proposed change.

Why is it useful for generic code? The reason you copy is so that you will mutate it later, which will fail for immutables anyway?

 Why is it useful for generic code? The reason you copy is so that you will mutate it later, which will fail for immutables anyway?

Or that you want a snapshot of the current version that does not change if the original is mutated.

Almost feels like deepcopy is better then.

1 Like

I tend to agree. However I still think relying on copy to return something mutable is a bad idea. E.g. it fails for ranges right now. Also typeof(copy(x)) != typeof(x) is a surprise. I think if you want a mutable copy, then you should be explicit about it. Anyway if people are using copy like this, then it is not worth breaking their code. But maybe copy docs could be clarified about semantics, e.g. typeof(copy(x)) != typeof(x) and mutability.

Yes, this is incorrect. The function that Base uses when it needs a mutable copy is Base.copymutable (added here: https://github.com/JuliaLang/julia/pull/16620). Maybe it’s time to finally export that function?

Alternatively, similar is guaranteed to return a mutable type, so you could use copy!(similar(a),a) (at least for arrays — this doesn’t work for generic iterables)… encapsulating that common pattern was precisely why copymutable was added.

No:

julia> copy(1:10)
1:10

I agree with @greg_plowman that copying CartesianIndices or LinearIndices should act like copying ranges.

3 Likes

I think you need to elaborate a bit. That looked pointless to me.

Pointless or not, it’s behaved that way for years and it probably can’t be changed before Julia 2.0. copy does not guarantee a mutable result, so assuming that property is wrong.

I know it doesn’t guarantee that and I never said that it did. I said that the reason why you want to copy something is because you want to mutate it later. The fact that there are some pointless copy methods defined for various types doesn’t change that.

Are you referring to copy and deepcopy or just copy? Because some generic code may need to keep an user-provided object in a way that it is sure it will not be changed externally. In this case, the code does not change the object, nor assume it is mutable, but keeps a deepcopy of it because it may be mutable.