-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
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
Support vectors as indices to NamedTuple #32664
base: master
Are you sure you want to change the base?
Conversation
3cdb89f
to
6045cff
Compare
I think what I should probably do instead of this code is have indexing with a vector return a new named tuple. This matches the subsetting behaviour that tuples and arrays already exhibit when indexed with a vector. (1, 3, 5, 7)[2:3] == (3, 5)
(x = 1, y = 3, z = 5)[1:1] == (x = 1,)
(x = 1, y = 3, z = 5)[1:2] == (x = 1, y = 3) |
base/namedtuple.jl
Outdated
@@ -103,6 +103,8 @@ firstindex(t::NamedTuple) = 1 | |||
lastindex(t::NamedTuple) = nfields(t) | |||
getindex(t::NamedTuple, i::Int) = getfield(t, i) | |||
getindex(t::NamedTuple, i::Symbol) = getfield(t, i) | |||
getindex(t::NamedTuple, v::AbstractVector{Symbol}) = [t[i] for i in 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.
This maps all v::AbstractVector
to Vector
.
I wonder if we can make this more generic, as in something like map(Fix1(getindex, t), v)
? For example, that definition would mean if v
were a SVector
then the output could also be a SVector
.
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.
Replied below. This seems like a fine implementation if we want to move towards the semantics in your Julep.
base/namedtuple.jl
Outdated
@@ -103,6 +103,8 @@ firstindex(t::NamedTuple) = 1 | |||
lastindex(t::NamedTuple) = nfields(t) | |||
getindex(t::NamedTuple, i::Int) = getfield(t, i) | |||
getindex(t::NamedTuple, i::Symbol) = getfield(t, i) | |||
getindex(t::NamedTuple, v::AbstractVector{Symbol}) = [t[i] for i in v] | |||
getindex(t::NamedTuple, i) = values(t)[i] |
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.
Can you explain what the semantics of this are? What are some examples where this helps you, and what do you expect as output?
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.
@ararslan suggested the latter as a fallback so that indexing methods for Tuple will be used.
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.
Indexing NamedTuple
s only differs from that for Tuple
s when indexing with Symbol
s. Everything else should be identical, so we can get it all for free by using a fallback like this.
From the linked issue:
Does this describe the behaviour you are suggesting? nt = (x=1, y=3, z=5)
nt[[1,2]] == nt[1:2] == [1,3]
nt[(1,2)] == (1,2)
nt[(:x, :y)] == (1,2)
nt[[:x, :y]] == [1,2]
nt[(a=:x, b=:z)] == (a=1, b=5)
nt[(x=:x, y=:y)] == (x=1, y=3)
# And not this one (because b does not have the indices :x, :y)
nt[(:x, :y)] == (x=1, y=3) I think I prefer this last interpretation for a vector of symbols, and maybe (probably not) it is OK because I dislike that the current way to subset a namedtuple is What do you think? |
Supporting more exotic vector types does not seem to be free: julia> map(Base.Fix1(getindex, nt), OffsetVector([:x, :y], -1))
OffsetArray(::Array{Real,1}, 0:1) with eltype Real with indices 0:1:
#undef
-1
julia> map(Base.Fix1(getindex, nt), OffsetVector([:x, :y], 0))
OffsetArray(::Array{Real,1}, 1:2) with eltype Real with indices 1:2:
π = 3.1415926535897...
-1
julia> map(Base.Fix1(getindex, nt), OffsetVector([:x, :y], 1))
OffsetArray(::Array{Real,1}, 2:3) with eltype Real with indices 2:3:
π = 3.1415926535897...
-1
julia> map(Base.Fix1(getindex, nt), OffsetVector([:x, :y], 2))
ERROR: BoundsError: attempt to access OffsetArray(::Array{Real,1}, 3:4) with eltype Real with indices 3:4 at index [3:5]
Stacktrace:
[1] copyto!(::OffsetArray{Real,1,Array{Real,1}}, ::Int64, ::OffsetArray{Irrational{:π},1,Array{Irrational{:π},1}}, ::Int64, ::Int64) at ./abstractarray.jl:785
[2] collect_to!(::OffsetArray{Irrational{:π},1,Array{Irrational{:π},1}}, ::Base.Generator{OffsetArray{Symbol,1,Array{Symbol,1}},Base.Fix1{typeof(getindex),NamedTuple{(:x, :y, :z),Tuple{Irrational{:π},Int64,Char}}}}, ::Int64, ::Tuple{Base.IdentityUnitRange{UnitRange{Int64}},Int64}) at ./array.jl:641
[3] collect_to_with_first!(::OffsetArray{Irrational{:π},1,Array{Irrational{:π},1}}, ::Irrational{:π}, ::Base.Generator{OffsetArray{Symbol,1,Array{Symbol,1}},Base.Fix1{typeof(getindex),NamedTuple{(:x, :y, :z),Tuple{Irrational{:π},Int64,Char}}}}, ::Tuple{Base.IdentityUnitRange{UnitRange{Int64}},Int64}) at ./array.jl:630
[4] _collect(::OffsetArray{Symbol,1,Array{Symbol,1}}, ::Base.Generator{OffsetArray{Symbol,1,Array{Symbol,1}},Base.Fix1{typeof(getindex),NamedTuple{(:x, :y, :z),Tuple{Irrational{:π},Int64,Char}}}}, ::Base.EltypeUnknown, ::Base.HasShape{1}) at ./array.jl:624
[5] collect_similar(::OffsetArray{Symbol,1,Array{Symbol,1}}, ::Base.Generator{OffsetArray{Symbol,1,Array{Symbol,1}},Base.Fix1{typeof(getindex),NamedTuple{(:x, :y, :z),Tuple{Irrational{:π},Int64,Char}}}}) at ./array.jl:548
[6] map(::Function, ::OffsetArray{Symbol,1,Array{Symbol,1}}) at ./abstractarray.jl:2018
[7] top-level scope at none:0 SVectors work. |
base/namedtuple.jl
Outdated
@@ -103,6 +103,8 @@ firstindex(t::NamedTuple) = 1 | |||
lastindex(t::NamedTuple) = nfields(t) | |||
getindex(t::NamedTuple, i::Int) = getfield(t, i) | |||
getindex(t::NamedTuple, i::Symbol) = getfield(t, i) | |||
getindex(t::NamedTuple, v::AbstractVector{Symbol}) = [t[i] for i in 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 think this is wrong and/or surprising. I'd do it as
getindex(t::NamedTuple, v::AbstractVector{Symbol}) = [t[i] for i in v] | |
getindex(t::NamedTuple, v::AbstractVector{Symbol}) = (; map(i->i => t[i], t)...) |
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 particular, indexing a tuple with a vector gives you back a tuple, so by analogy, indexing a named tuple with a vector should give you back a named tuple.
@ararslan I see what you mean by the analogy with tuples, but maybe it wouldn’t surprise you that I think the non-scalar tuple indexing is slightly wrong :) Basically I’d love to see the language evolve towards the kinds of ideas in #24019 rather than diffuse away from that. The tuple-vector indexing isn’t so bad because it actually preserves the key values (it happens that (Note: I’m also unclear why named tuples need two sets of indices - symbols and integers - when |
I feel like there needs to be consensus on what outputs to expect from the test cases I listed in order to continue. Here are those test cases from the earlier comment with outputs as I imagined appropriate for the rule that @andyferris suggested in their Julep: nt = (x=1, y=3, z=5)
nt[[1,2]] == [1,3]
nt[(1,2)] == (1,2)
nt[1:2] == ???
nt[(:x, :y)] == (1,2)
nt[[:x, :y]] == [1,2]
nt[(a=:x, b=:z)] == (a=1, b=5)
nt[(x=:x, y=:y)] == (x=1, y=3)
# And not this one (because b does not have the indices :x, :y)
nt[(:x, :y)] == (x=1, y=3) My understanding is that @ararslan is suggesting this: nt = (x=1, y=3, z=5)
nt[[1,2]] == nt[1:2] == nt[(1,2)] == nt[(:x, :y)] == (x=1, y=3)
# And these are probably errors:
nt[(a=:x, b=:z)]
nt[(x=:x, y=:y)] |
nt = (x=1, y=3, z=5)
nt[[1,2]] == nt[1:2] == nt[(1,2)] == nt[(:x, :y)] == (x=1, y=3) Correct, I am. Though a case could be made to have
nt[(a=:x, b=:z)]
nt[(x=:x, y=:y)] Yes definitely. |
ac36608
to
598dd41
Compare
Latest commit adds tests and implements this behaviour: nt = (x=1, y=3, z=5)
nt[[1,2]] == nt[1:2] == nt[[1,2]] == nt[[:x, :y]] == (x=1, y=3)
# Indexing with a tuple is an error, as for Tuples.
nt[(1,2)]
nt[(:x, :y)]
# And these are errors:
nt[(a=:x, b=:z)]
nt[(x=:x, y=:y)] I've removed the fallback method for now. |
598dd41
to
aed81fd
Compare
Is this blocked waiting on a resolution to #30845 or not? There are now tests and NEWS. I haven't added a compat note in a docstring because there are no getindex docstrings in namedtuple.jl and it's not clear where one should go. The behaviour is basically the same as that already documented for abstract arrays and implemented for arrays and tuples. |
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's a little annoying, but since Bool <: Integer
, we need to also consider the logical indexing case. It's just as easy to support it as disallow it — and as much as it gives me the heeby-geebies, we support it for tuples. So may as well support it here.
Also since you support vectors of Integer
, we should probably also support all Integer
s in the scalar case. I think I got these ad-hoc changes right here.
I'm not sure where to best put the docs/compat annotation. I guess we could just add a new method-specific docstring for |
Should we support indexing with Tuple(1:3)[Base.to_index([true, true, false])] == (1,2)
Tuple(1:3)[Base.to_index([true])] == (1,) |
There's this difference of behaviour between tuple and named tuple when indexing with repeated indices too that I'd like to check everyone is OK with. julia> (1, 2, 3)[[1,1,1]]
(1, 1, 1)
julia> (x=1, y=-1, z=3)[[1,1]]
(x = 1,) |
Co-Authored-By: Matt Bauman <[email protected]>
Co-Authored-By: Jameson Nash <[email protected]>
5c6be15
to
6945414
Compare
Is there anything outstanding for this to be merged? |
I think the main outstanding question is just whether this now does what anyone expected of it. Beyond the generic map-like or broadcast-like (iteration-based or indexing-based) possibilities for this (or deprecating this entirely for all |
I don't see any examples of compelling use cases for this, and it's not clear how it should work so I think we can wait on this. |
As I reconsider this, I'm now against the semantics as they're implemented here and completely agree with Andy's initial comment (#32664 (comment)). We've found the idiom "the axes of the indices become the axes of the result" to be a very clear and powerful design to help steer these sorts of questions — and this behavior goes against that. So if |
This PR was linked to from slack just now, and I was confused until I saw how old it is. I don't think I ever saw this discussion, and it seems like no one that reviewed #38878 did either (or didn't remember it). Perhaps it doesn't actually matter, since that PR only implements |
Fix #32662