- 
          
- 
                Notifications
    You must be signed in to change notification settings 
- Fork 5.7k
RFC: setindex for mutable collections #33495
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
| I think this PR is quite useful for writing generic code that works on both immutable and mutable collections. To me this looks is a basic interface, that really should live in  | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is great! I'm in full support of adding this functionality. It still doesn't fully complete the copy-and-widen story, but it's a huge helping block in getting there.
        
          
                base/array.jl
              
                Outdated
          
        
      | ``` | ||
| with a suitable definition of `isequal′` (e.g., elements are approximately but possibly | ||
| not exactly equal), _if_ `setindex!` supports given arguments. | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The not exactly equal part here is just because we have, e.g., convert(Float64, Int(maxintfloat()) + 1) != Int(maxintfloat()) + 1, right?  Maybe it'd be a bit clearer to directly say that it's equal after conversion?  I think I'd rather focus on the copy side of things than the equality side of things:
That is, something more like:
y1 = setindex(x, value, key...)
y2 = copy′(x)
y2[key...] = value
@assert convert.(eltype(y2), y1) == y2with a suitable definition of copy′ that ensures value can be assigned into the collection.  This is where the focus should be.  Getting the @assert exactly right (which it's not right now because broadcast) is slightly less important than getting the widening right, I think.
That way we don't have to bend over backwards with awkward approximately-but-maybe-not-exact sorts of phrasings.  It's just a (possible convert plus) copy plus setindex!.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a great idea! Yes, it was a very awkward part of my version of the specification. Your version is much more elegant and precise.
        
          
                base/array.jl
              
                Outdated
          
        
      | function setindex(xs::AbstractArray, v, I...) | ||
| T = promote_type(eltype(xs), typeof(v)) | ||
| ys = similar(xs, T) | ||
| if eltype(xs) !== Union{} | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In what cases will promote_type return Union{}?  And why can't we copy! a Union{} array?
julia> copy!(Array{Union{}}(undef, 0), Array{Union{}}(undef, 0))
0-element Array{Union{},1}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is sometimes useful to specify the container type and shape without specifying its element type (e.g., for implementing multi-dim map in the mutate-or-widen style).  Union{} element type is a nice way to encode this. But you can't copy! from it.
julia> copy!(Vector{Int}(undef, 3), Vector{Union{}}(undef, 3))
ERROR: UndefRefError: access to undefined reference
Stacktrace:
 [1] getindex(::Array{Union{},1}, ::Int64) at ./array.jl:758
 [2] copyto!(::Array{Int64,1}, ::Int64, ::Array{Union{},1}, ::Int64, ::Int64) at ./abstractarray.jl:842
 [3] append! at ./array.jl:926 [inlined]
 [4] copy!(::Array{Int64,1}, ::Array{Union{},1}) at ./abstractarray.jl:709
 [5] top-level scope at REPL[5]:1This error itself is not specific to Union{}; i.e., copy! does not handle undef values.  eltype(xs) !== Union{} is just an easy shortcut to handle a useful case.  But maybe this logic should be moved to copy! function?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just removed this branching for now. Adding this to copy! (in a different PR) seems to be the right direction.
| function setindex end | ||
|  | ||
| function setindex(xs::AbstractArray, v, I...) | ||
| T = promote_type(eltype(xs), typeof(v)) | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we use promote_typejoin instead?
ATM, eltype(setindex([0, 0], 0.5, 1)) is Float64. It'd be Real if we use promote_typejoin.  The former is compatible with how vcat work and the latter is compatible with how collect and broadcast work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd say promote_type is the right choice. Otherwise you'd very often get abstract element types, which really kill performance.
|  | ||
| @testset "setindex" begin | ||
| ==ₜ(_, _) = false | ||
| ==ₜ(x::T, y::T) where T = x == y | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
==ₜ seems so handy, maybe it should actually live in e.g. Test?
| setindex(collection, value, key...) | ||
| Create a new collection with the element/elements of the location specified by `key` | ||
| replaced with `value`(s). | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also mention that the new collection may have a different element type, as determined by promote_type.
| function setindex end | ||
|  | ||
| function setindex(xs::AbstractArray, v, I...) | ||
| T = promote_type(eltype(xs), typeof(v)) | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd say promote_type is the right choice. Otherwise you'd very often get abstract element types, which really kill performance.
        
          
                test/arrayops.jl
              
                Outdated
          
        
      | ==ₜ(_, _) = false | ||
| ==ₜ(x::T, y::T) where T = x == y | ||
|  | ||
| @test Base.setindex(Int[1, 2], 3.0, 2) ==ₜ [1.0, 3.0] | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| @test Base.setindex(Int[1, 2], 3.0, 2) ==ₜ [1.0, 3.0] | |
| @test Base.setindex([1, 2], 3.0, 2) ==ₜ [1.0, 3.0] | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it is good to have Int explicit, since the test is about change of eltype.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the point since that doesn't make any difference?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
emphasis
        
          
                test/arrayops.jl
              
                Outdated
          
        
      | ==ₜ(x::T, y::T) where T = x == y | ||
|  | ||
| @test Base.setindex(Int[1, 2], 3.0, 2) ==ₜ [1.0, 3.0] | ||
| @test Base.setindex(Int[1, 2, 3], :two, 2) ==ₜ [1, :two, 3] | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| @test Base.setindex(Int[1, 2, 3], :two, 2) ==ₜ [1, :two, 3] | |
| @test Base.setindex([1, 2, 3], :two, 2) ==ₜ [1, :two, 3] | 
| I would like to have this functionality in Base. @tkf I guess you don't have much time for this. Would it be okay for you if I take this PR and rebase/finish it? | 
| @jw3126 Are you still interested in doing that? If so, I heartily encourage you to do it! | 
| @MasonProtter I would love to have that. There is also #46453 and I think there is also at least one additional similar PR somewhere. | 
setindexwas documented recently #32738 and there are also some interests in defining it for general arrays (e.g., ArrayInterface.jl, Setfield.jl).I propose the following definition of
setindexfor (possibly) mutable collections. I believe this is more useful than more straightforward implementation likesetindex!(copy(collection), value, key...). This is because it can be used as a building block of the "mutate-or-widen" strategy that is used in functions likecollectandmaterialize.I'm using this definition in BangBang.jl and it turned out to be quite useful for writing efficient
collectetc. for Transducers.jl while avoiding depending on the compiler's inference API.Proposed API
I also included the implementations for arrays and dicts.