* [ruby-core:122350] [Ruby Feature#21387] Proposal to add Data#[]
@ 2025-05-31 6:10 ybiquitous (Masafumi Koba) via ruby-core
2025-05-31 7:16 ` [ruby-core:122351] " zverok (Victor Shepelev) via ruby-core
` (4 more replies)
0 siblings, 5 replies; 6+ messages in thread
From: ybiquitous (Masafumi Koba) via ruby-core @ 2025-05-31 6:10 UTC (permalink / raw)
To: ruby-core; +Cc: ybiquitous (Masafumi Koba)
Issue #21387 has been reported by ybiquitous (Masafumi Koba).
----------------------------------------
Feature #21387: Proposal to add Data#[]
https://bugs.ruby-lang.org/issues/21387
* Author: ybiquitous (Masafumi Koba)
* Status: Open
----------------------------------------
## Proposal
I propose to add a new instance method `#[]` to the `Data` class, similar to [`Struct#[]`](https://docs.ruby-lang.org/en/3.4/Struct.html#method-i-5B-5D).
If writing the method signature in RBS, it would be like this:
```ruby
class Data
def []: (name: String | Symbol) -> untyped
end
```
Requirements:
- `Data#[]` accepts a member name as a `Symbol` or `String`, e.g., `data[:id] == data["id"]`.
- `Data#[]` returns a value associated with the given `name`, e.g., `data[:id] == data.id`.
- `Data#[]` raises `NameError` if the given `name` is not one of the members, e.g., `data[:invalid]`.
Note: Please assume that `data = Data.define(:id).new(id: 100)` is given in the examples above.
## Motivation
In Active Support Core Extensions of Rails, I found a use case that `Data#[]` would be helpful with `Enumerable#pluck`.
Please look at this example:
```ruby
# data_test.rb
require "bundler/inline"
gemfile do
source "https://rubygems.org"
gem "activesupport", "8.0.2"
end
require "active_support/core_ext/enumerable"
StructPerson = Struct.new(:name, :age)
DataPerson = Data.define(:name, :age)
struct_people = [
StructPerson.new("Bob", 25),
]
puts "Struct:"
puts "=> #{struct_people.pluck(:name)}"
data_people = [
DataPerson.new("Charlie", 35),
]
puts "Data:"
begin
puts "=> #{data_people.pluck(:name)}"
rescue => e
puts e.detailed_message(syntax_suggest: true)
end
```
Running this script outputs below:
```shell
$ ruby data_test.rb
Struct:
=> ["Bob"]
Data:
undefined method '[]' for an instance of DataPerson (NoMethodError)
map { |element| element[key] }
^^^^^
```
Note: This output resulted on `ruby 3.4.4 (2025-05-14 revision a38531fd3f) +PRISM [arm64-darwin24]`.
The error reason is that the `Enumerable#pluck` extension expects all the elements in the array to respond to `[]`.
See also the `pluck` code here:
https://github.com/rails/rails/blob/v8.0.2/activesupport/lib/active_support/core_ext/enumerable.rb#L145-L152
If `Data#[]` was implemented as follows,
```ruby
# data_patch.rb
class Data
def [](name)
unless members.include?(name.to_sym)
raise NameError, "no member '#{name}' in data"
end
public_send(name)
end
end
```
The script is successful:
```shell
$ ruby -r ./data_patch.rb data_test.rb
Struct:
=> ["Bob"]
Data:
=> ["Charlie"]
```
Ref:
- https://guides.rubyonrails.org/v8.0.2/active_support_core_extensions.html#pluck
- https://api.rubyonrails.org/v8.0.2/classes/Enumerable.html#method-i-pluck
Although `Enumerable#pluck` is just an example, I guess that there would be other cases where `Data` objects should respond to `[]`.
## Reasoning
>From the long discussion in #16122 that introduced `Data`, I understand that `Data#[]` was rejected in #16122#note-28 because `[]` was like `Enumerable`.
I also agree with rejecting `Data#each` since `Data` is not an `Enumerable`, but `[]` seems acceptable to me.
Reasons:
- `[]` is sometimes used for non-container objects, such as [`ActiveRecord::AttributeMethods#[]`](https://api.rubyonrails.org/v8.0.2/classes/ActiveRecord/AttributeMethods.html#method-i-5B-5D).
- Considering `Data#to_h` is provided, it seems reasonable that we could access a `Data` member's value through `[]`.
- From the similarity between `Struct` and `Data`, some people might expect `Hash`-like accessing via `[]`.
- Unless `[]` is provided, we have to call `public_send` or convert to a hash via `to_h` when we want to get member values with names. It'd be inefficient and easy to make mistakes.
Let me show an example:
```ruby
Person = Data.define(:name, :age)
charlie = Person.new("Charlie", 35)
member_names_from_user_input = ["age", "hash"] # "hash" is a malicious input.
# Using #public_send
member_names_from_user_input.map { charlie.public_send(it) }
#=> [35, -1363726049241242821]
# Using #to_h and Hash#fetch with string keys
member_names_from_user_input.map { charlie.to_h.fetch(it) }
#=> key not found: "age" (KeyError)
# Using #to_h and Hash#fetch with symbol keys
member_names_from_user_input.map { charlie.to_h.fetch(it.to_sym) }
#=> key not found: :hash (KeyError)
# Using #[]
member_names_from_user_input.map { charlie[it] }
#=> no member 'hash' in data (NameError)
```
The last example is most elegant for the same goal. That's why I propose to add `Data#[]`.
However, there might be downsides that I have overlooked, so I'd appreciate it if you could let me know.
Thanks.
--
https://bugs.ruby-lang.org/
______________________________________________
ruby-core mailing list -- ruby-core@ml.ruby-lang.org
To unsubscribe send an email to ruby-core-leave@ml.ruby-lang.org
ruby-core info -- https://ml.ruby-lang.org/mailman3/lists/ruby-core.ml.ruby-lang.org/
^ permalink raw reply [flat|nested] 6+ messages in thread
* [ruby-core:122351] [Ruby Feature#21387] Proposal to add Data#[]
2025-05-31 6:10 [ruby-core:122350] [Ruby Feature#21387] Proposal to add Data#[] ybiquitous (Masafumi Koba) via ruby-core
@ 2025-05-31 7:16 ` zverok (Victor Shepelev) via ruby-core
2025-05-31 13:41 ` [ruby-core:122353] " ybiquitous (Masafumi Koba) via ruby-core
` (3 subsequent siblings)
4 siblings, 0 replies; 6+ messages in thread
From: zverok (Victor Shepelev) via ruby-core @ 2025-05-31 7:16 UTC (permalink / raw)
To: ruby-core; +Cc: zverok (Victor Shepelev)
Issue #21387 has been updated by zverok (Victor Shepelev).
The design goal of `Data` was to be as close to "just a simple atomic object" as possible and convenient. It is a "value object," not a "container". `#[]` is closer to a container protocol and starts to erode the design.
For a particular justification examples:
For the first one, instead of `data_people.pluck(:name)` you can just use `data_people.map(&:name)`.
For the second one (user's input as data members)--I don't think it is the language's goal to support particular cases like this. If it is realistic, then implementing it yourself is a couple of lines--and gives the program's author freedom to handle this "security problem" any way they find suitable:
```ruby
Person = Data.define(:name, :age) do
def [](key)
members.include?(key) or raise SecurityError, "They tried to still the #{key}!"
public_send(key)
end
end
```
Introducing `#[]` is like opening a can of worms on the "why it is not more like Hash/Struct" question. Like, if we support dynamic key passing (and generally treating _fields_ as _container keys_), why can't we have "enumerate everything dynamically"? Why don't we have `#[]=`? Etc. etc.
----------------------------------------
Feature #21387: Proposal to add Data#[]
https://bugs.ruby-lang.org/issues/21387#change-113496
* Author: ybiquitous (Masafumi Koba)
* Status: Open
----------------------------------------
## Proposal
I propose to add a new instance method `#[]` to the `Data` class, similar to [`Struct#[]`](https://docs.ruby-lang.org/en/3.4/Struct.html#method-i-5B-5D).
If writing the method signature in RBS, it would be like this:
```ruby
class Data
def []: (name: String | Symbol) -> untyped
end
```
Requirements:
- `Data#[]` accepts a member name as a `Symbol` or `String`, e.g., `data[:id] == data["id"]`.
- `Data#[]` returns a value associated with the given `name`, e.g., `data[:id] == data.id`.
- `Data#[]` raises `NameError` if the given `name` is not one of the members, e.g., `data[:invalid]`.
Note: Please assume that `data = Data.define(:id).new(id: 100)` is given in the examples above.
## Motivation
In Active Support Core Extensions of Rails, I found a use case that `Data#[]` would be helpful with `Enumerable#pluck`.
Please look at this example:
```ruby
# data_test.rb
require "bundler/inline"
gemfile do
source "https://rubygems.org"
gem "activesupport", "8.0.2"
end
require "active_support/core_ext/enumerable"
StructPerson = Struct.new(:name, :age)
DataPerson = Data.define(:name, :age)
struct_people = [
StructPerson.new("Bob", 25),
]
puts "Struct:"
puts "=> #{struct_people.pluck(:name)}"
data_people = [
DataPerson.new("Charlie", 35),
]
puts "Data:"
begin
puts "=> #{data_people.pluck(:name)}"
rescue => e
puts e.detailed_message(syntax_suggest: true)
end
```
Running this script outputs below:
```shell
$ ruby data_test.rb
Struct:
=> ["Bob"]
Data:
undefined method '[]' for an instance of DataPerson (NoMethodError)
map { |element| element[key] }
^^^^^
```
Note: This output resulted on `ruby 3.4.4 (2025-05-14 revision a38531fd3f) +PRISM [arm64-darwin24]`.
The error reason is that the `Enumerable#pluck` extension expects all the elements in the array to respond to `[]`.
See also the `pluck` code here:
https://github.com/rails/rails/blob/v8.0.2/activesupport/lib/active_support/core_ext/enumerable.rb#L145-L152
If `Data#[]` was implemented as follows,
```ruby
# data_patch.rb
class Data
def [](name)
unless members.include?(name.to_sym)
raise NameError, "no member '#{name}' in data"
end
public_send(name)
end
end
```
The script is successful:
```shell
$ ruby -r ./data_patch.rb data_test.rb
Struct:
=> ["Bob"]
Data:
=> ["Charlie"]
```
Ref:
- https://guides.rubyonrails.org/v8.0.2/active_support_core_extensions.html#pluck
- https://api.rubyonrails.org/v8.0.2/classes/Enumerable.html#method-i-pluck
Although `Enumerable#pluck` is just an example, I guess that there would be other cases where `Data` objects should respond to `[]`.
## Reasoning
>From the long discussion in #16122 that introduced `Data`, I understand that `Data#[]` was rejected in #16122#note-28 because `[]` was like `Enumerable`.
I also agree with rejecting `Data#each` since `Data` is not an `Enumerable`, but `[]` seems acceptable to me.
Reasons:
- `[]` is sometimes used for non-container objects, such as [`ActiveRecord::AttributeMethods#[]`](https://api.rubyonrails.org/v8.0.2/classes/ActiveRecord/AttributeMethods.html#method-i-5B-5D).
- Considering `Data#to_h` is provided, it seems reasonable that we could access a `Data` member's value through `[]`.
- From the similarity between `Struct` and `Data`, some people might expect `Hash`-like accessing via `[]`.
- Unless `[]` is provided, we have to call `public_send` or convert to a hash via `to_h` when we want to get member values with names. It'd be inefficient and easy to make mistakes.
Let me show an example:
```ruby
Person = Data.define(:name, :age)
charlie = Person.new("Charlie", 35)
member_names_from_user_input = ["age", "hash"] # "hash" is a malicious input.
# Using #public_send
member_names_from_user_input.map { charlie.public_send(it) }
#=> [35, -1363726049241242821]
# Using #to_h and Hash#fetch with string keys
member_names_from_user_input.map { charlie.to_h.fetch(it) }
#=> key not found: "age" (KeyError)
# Using #to_h and Hash#fetch with symbol keys
member_names_from_user_input.map { charlie.to_h.fetch(it.to_sym) }
#=> key not found: :hash (KeyError)
# Using #[]
member_names_from_user_input.map { charlie[it] }
#=> no member 'hash' in data (NameError)
```
The last example is most elegant for the same goal. That's why I propose to add `Data#[]`.
However, there might be downsides that I have overlooked, so I'd appreciate it if you could let me know.
Thanks.
--
https://bugs.ruby-lang.org/
______________________________________________
ruby-core mailing list -- ruby-core@ml.ruby-lang.org
To unsubscribe send an email to ruby-core-leave@ml.ruby-lang.org
ruby-core info -- https://ml.ruby-lang.org/mailman3/lists/ruby-core.ml.ruby-lang.org/
^ permalink raw reply [flat|nested] 6+ messages in thread
* [ruby-core:122353] [Ruby Feature#21387] Proposal to add Data#[]
2025-05-31 6:10 [ruby-core:122350] [Ruby Feature#21387] Proposal to add Data#[] ybiquitous (Masafumi Koba) via ruby-core
2025-05-31 7:16 ` [ruby-core:122351] " zverok (Victor Shepelev) via ruby-core
@ 2025-05-31 13:41 ` ybiquitous (Masafumi Koba) via ruby-core
2025-05-31 16:42 ` [ruby-core:122354] " zverok (Victor Shepelev) via ruby-core
` (2 subsequent siblings)
4 siblings, 0 replies; 6+ messages in thread
From: ybiquitous (Masafumi Koba) via ruby-core @ 2025-05-31 13:41 UTC (permalink / raw)
To: ruby-core; +Cc: ybiquitous (Masafumi Koba)
Issue #21387 has been updated by ybiquitous (Masafumi Koba).
@zverok Thank you for the feedback!
My example of `data_people.pluck(:name)` was not so good, sorry. A strength of `pluck` is to accept multiple keys like `data_people.pluck(:name, :age)`, so `.map` isn't a proper alternative in this case.
For the second example of `member_names_from_user_input = ["age", "hash"]`, the use case may not be so typical, as you pointed out, although it's not so smart for every `Data.define` in an app or library to implement `#[]` repeatedly.
> It is a "value object," not a "container".
I understand your intention in the words and that you're very careful to extend the current scope of `Data` more than necessary.
However, I'm still curious why I've come up with this `#[]` idea.
- `Data` objects can be initialized with keyword arguments, like `Hash` objects.
- `Data` objects can be converted to `Hash` objects via `#to_h`.
I think that these `Data` features (or interfaces) would easily make people recognize the similarity between `Data` and `Hash`. Especially when `Data` objects are serialized as JSON and used to communicate with external systems, I bet that the similarity stands out to people.
I also don't think `Data` should mimic `Hash` mostly, but unless this similarity essentially exists, providing a few methods helping `Data` programming seems acceptable to many folks, and I don't think it violates the `Data` design policy.
Also, from the perspective of Duck Typing, it seems to make sense that people expect `Data` to respond to `#[]` like `Hash`.
That's why I still think `Data#[]` is a good compromise to maximize its usability and keep its design policy as-is.
----------------------------------------
Feature #21387: Proposal to add Data#[]
https://bugs.ruby-lang.org/issues/21387#change-113497
* Author: ybiquitous (Masafumi Koba)
* Status: Open
----------------------------------------
## Proposal
I propose to add a new instance method `#[]` to the `Data` class, similar to [`Struct#[]`](https://docs.ruby-lang.org/en/3.4/Struct.html#method-i-5B-5D).
If writing the method signature in RBS, it would be like this:
```ruby
class Data
def []: (name: String | Symbol) -> untyped
end
```
Requirements:
- `Data#[]` accepts a member name as a `Symbol` or `String`, e.g., `data[:id] == data["id"]`.
- `Data#[]` returns a value associated with the given `name`, e.g., `data[:id] == data.id`.
- `Data#[]` raises `NameError` if the given `name` is not one of the members, e.g., `data[:invalid]`.
Note: Please assume that `data = Data.define(:id).new(id: 100)` is given in the examples above.
## Motivation
In Active Support Core Extensions of Rails, I found a use case that `Data#[]` would be helpful with `Enumerable#pluck`.
Please look at this example:
```ruby
# data_test.rb
require "bundler/inline"
gemfile do
source "https://rubygems.org"
gem "activesupport", "8.0.2"
end
require "active_support/core_ext/enumerable"
StructPerson = Struct.new(:name, :age)
DataPerson = Data.define(:name, :age)
struct_people = [
StructPerson.new("Bob", 25),
]
puts "Struct:"
puts "=> #{struct_people.pluck(:name)}"
data_people = [
DataPerson.new("Charlie", 35),
]
puts "Data:"
begin
puts "=> #{data_people.pluck(:name)}"
rescue => e
puts e.detailed_message(syntax_suggest: true)
end
```
Running this script outputs below:
```shell
$ ruby data_test.rb
Struct:
=> ["Bob"]
Data:
undefined method '[]' for an instance of DataPerson (NoMethodError)
map { |element| element[key] }
^^^^^
```
Note: This output resulted on `ruby 3.4.4 (2025-05-14 revision a38531fd3f) +PRISM [arm64-darwin24]`.
The error reason is that the `Enumerable#pluck` extension expects all the elements in the array to respond to `[]`.
See also the `pluck` code here:
https://github.com/rails/rails/blob/v8.0.2/activesupport/lib/active_support/core_ext/enumerable.rb#L145-L152
If `Data#[]` was implemented as follows,
```ruby
# data_patch.rb
class Data
def [](name)
unless members.include?(name.to_sym)
raise NameError, "no member '#{name}' in data"
end
public_send(name)
end
end
```
The script is successful:
```shell
$ ruby -r ./data_patch.rb data_test.rb
Struct:
=> ["Bob"]
Data:
=> ["Charlie"]
```
Ref:
- https://guides.rubyonrails.org/v8.0.2/active_support_core_extensions.html#pluck
- https://api.rubyonrails.org/v8.0.2/classes/Enumerable.html#method-i-pluck
Although `Enumerable#pluck` is just an example, I guess that there would be other cases where `Data` objects should respond to `[]`.
## Reasoning
>From the long discussion in #16122 that introduced `Data`, I understand that `Data#[]` was rejected in #16122#note-28 because `[]` was like `Enumerable`.
I also agree with rejecting `Data#each` since `Data` is not an `Enumerable`, but `[]` seems acceptable to me.
Reasons:
- `[]` is sometimes used for non-container objects, such as [`ActiveRecord::AttributeMethods#[]`](https://api.rubyonrails.org/v8.0.2/classes/ActiveRecord/AttributeMethods.html#method-i-5B-5D).
- Considering `Data#to_h` is provided, it seems reasonable that we could access a `Data` member's value through `[]`.
- From the similarity between `Struct` and `Data`, some people might expect `Hash`-like accessing via `[]`.
- Unless `[]` is provided, we have to call `public_send` or convert to a hash via `to_h` when we want to get member values with names. It'd be inefficient and easy to make mistakes.
Let me show an example:
```ruby
Person = Data.define(:name, :age)
charlie = Person.new("Charlie", 35)
member_names_from_user_input = ["age", "hash"] # "hash" is a malicious input.
# Using #public_send
member_names_from_user_input.map { charlie.public_send(it) }
#=> [35, -1363726049241242821]
# Using #to_h and Hash#fetch with string keys
member_names_from_user_input.map { charlie.to_h.fetch(it) }
#=> key not found: "age" (KeyError)
# Using #to_h and Hash#fetch with symbol keys
member_names_from_user_input.map { charlie.to_h.fetch(it.to_sym) }
#=> key not found: :hash (KeyError)
# Using #[]
member_names_from_user_input.map { charlie[it] }
#=> no member 'hash' in data (NameError)
```
The last example is most elegant for the same goal. That's why I propose to add `Data#[]`.
However, there might be downsides that I have overlooked, so I'd appreciate it if you could let me know.
Thanks.
--
https://bugs.ruby-lang.org/
______________________________________________
ruby-core mailing list -- ruby-core@ml.ruby-lang.org
To unsubscribe send an email to ruby-core-leave@ml.ruby-lang.org
ruby-core info -- https://ml.ruby-lang.org/mailman3/lists/ruby-core.ml.ruby-lang.org/
^ permalink raw reply [flat|nested] 6+ messages in thread
* [ruby-core:122354] [Ruby Feature#21387] Proposal to add Data#[]
2025-05-31 6:10 [ruby-core:122350] [Ruby Feature#21387] Proposal to add Data#[] ybiquitous (Masafumi Koba) via ruby-core
2025-05-31 7:16 ` [ruby-core:122351] " zverok (Victor Shepelev) via ruby-core
2025-05-31 13:41 ` [ruby-core:122353] " ybiquitous (Masafumi Koba) via ruby-core
@ 2025-05-31 16:42 ` zverok (Victor Shepelev) via ruby-core
2025-06-01 19:48 ` [ruby-core:122362] " Eregon (Benoit Daloze) via ruby-core
2025-06-02 10:17 ` [ruby-core:122378] " ybiquitous (Masafumi Koba) via ruby-core
4 siblings, 0 replies; 6+ messages in thread
From: zverok (Victor Shepelev) via ruby-core @ 2025-05-31 16:42 UTC (permalink / raw)
To: ruby-core; +Cc: zverok (Victor Shepelev)
Issue #21387 has been updated by zverok (Victor Shepelev).
> However, I'm still curious why I've come up with this `#[]` idea.
>
> * `Data` objects can be initialized with keyword arguments, like `Hash` objects.
> * `Data` objects can be converted to Hash objects via `#to_h`.
>
> I think that these Data features (or interfaces) would easily make people recognize the similarity between Data and Hash.
I am not sure about this argument. The "can be initialized with keyword arguments" and "can be converted to Hash" are probably the most common features among all kinds of data-representing/data-modelling objects.
The logical leap "so they are like Hash, and should have more Hash-like behavior" doesn't work for me personally.
But others might have other opinions.
Nowadays, I don't have the capacity to be involved in Ruby's development; I just explained the initial design idea. Let the core team decide.
----------------------------------------
Feature #21387: Proposal to add Data#[]
https://bugs.ruby-lang.org/issues/21387#change-113498
* Author: ybiquitous (Masafumi Koba)
* Status: Open
----------------------------------------
## Proposal
I propose to add a new instance method `#[]` to the `Data` class, similar to [`Struct#[]`](https://docs.ruby-lang.org/en/3.4/Struct.html#method-i-5B-5D).
If writing the method signature in RBS, it would be like this:
```ruby
class Data
def []: (name: String | Symbol) -> untyped
end
```
Requirements:
- `Data#[]` accepts a member name as a `Symbol` or `String`, e.g., `data[:id] == data["id"]`.
- `Data#[]` returns a value associated with the given `name`, e.g., `data[:id] == data.id`.
- `Data#[]` raises `NameError` if the given `name` is not one of the members, e.g., `data[:invalid]`.
Note: Please assume that `data = Data.define(:id).new(id: 100)` is given in the examples above.
## Motivation
In Active Support Core Extensions of Rails, I found a use case that `Data#[]` would be helpful with `Enumerable#pluck`.
Please look at this example:
```ruby
# data_test.rb
require "bundler/inline"
gemfile do
source "https://rubygems.org"
gem "activesupport", "8.0.2"
end
require "active_support/core_ext/enumerable"
StructPerson = Struct.new(:name, :age)
DataPerson = Data.define(:name, :age)
struct_people = [
StructPerson.new("Bob", 25),
]
puts "Struct:"
puts "=> #{struct_people.pluck(:name)}"
data_people = [
DataPerson.new("Charlie", 35),
]
puts "Data:"
begin
puts "=> #{data_people.pluck(:name)}"
rescue => e
puts e.detailed_message(syntax_suggest: true)
end
```
Running this script outputs below:
```shell
$ ruby data_test.rb
Struct:
=> ["Bob"]
Data:
undefined method '[]' for an instance of DataPerson (NoMethodError)
map { |element| element[key] }
^^^^^
```
Note: This output resulted on `ruby 3.4.4 (2025-05-14 revision a38531fd3f) +PRISM [arm64-darwin24]`.
The error reason is that the `Enumerable#pluck` extension expects all the elements in the array to respond to `[]`.
See also the `pluck` code here:
https://github.com/rails/rails/blob/v8.0.2/activesupport/lib/active_support/core_ext/enumerable.rb#L145-L152
If `Data#[]` was implemented as follows,
```ruby
# data_patch.rb
class Data
def [](name)
unless members.include?(name.to_sym)
raise NameError, "no member '#{name}' in data"
end
public_send(name)
end
end
```
The script is successful:
```shell
$ ruby -r ./data_patch.rb data_test.rb
Struct:
=> ["Bob"]
Data:
=> ["Charlie"]
```
Ref:
- https://guides.rubyonrails.org/v8.0.2/active_support_core_extensions.html#pluck
- https://api.rubyonrails.org/v8.0.2/classes/Enumerable.html#method-i-pluck
Although `Enumerable#pluck` is just an example, I guess that there would be other cases where `Data` objects should respond to `[]`.
## Reasoning
>From the long discussion in #16122 that introduced `Data`, I understand that `Data#[]` was rejected in #16122#note-28 because `[]` was like `Enumerable`.
I also agree with rejecting `Data#each` since `Data` is not an `Enumerable`, but `[]` seems acceptable to me.
Reasons:
- `[]` is sometimes used for non-container objects, such as [`ActiveRecord::AttributeMethods#[]`](https://api.rubyonrails.org/v8.0.2/classes/ActiveRecord/AttributeMethods.html#method-i-5B-5D).
- Considering `Data#to_h` is provided, it seems reasonable that we could access a `Data` member's value through `[]`.
- From the similarity between `Struct` and `Data`, some people might expect `Hash`-like accessing via `[]`.
- Unless `[]` is provided, we have to call `public_send` or convert to a hash via `to_h` when we want to get member values with names. It'd be inefficient and easy to make mistakes.
Let me show an example:
```ruby
Person = Data.define(:name, :age)
charlie = Person.new("Charlie", 35)
member_names_from_user_input = ["age", "hash"] # "hash" is a malicious input.
# Using #public_send
member_names_from_user_input.map { charlie.public_send(it) }
#=> [35, -1363726049241242821]
# Using #to_h and Hash#fetch with string keys
member_names_from_user_input.map { charlie.to_h.fetch(it) }
#=> key not found: "age" (KeyError)
# Using #to_h and Hash#fetch with symbol keys
member_names_from_user_input.map { charlie.to_h.fetch(it.to_sym) }
#=> key not found: :hash (KeyError)
# Using #[]
member_names_from_user_input.map { charlie[it] }
#=> no member 'hash' in data (NameError)
```
The last example is most elegant for the same goal. That's why I propose to add `Data#[]`.
However, there might be downsides that I have overlooked, so I'd appreciate it if you could let me know.
Thanks.
--
https://bugs.ruby-lang.org/
______________________________________________
ruby-core mailing list -- ruby-core@ml.ruby-lang.org
To unsubscribe send an email to ruby-core-leave@ml.ruby-lang.org
ruby-core info -- https://ml.ruby-lang.org/mailman3/lists/ruby-core.ml.ruby-lang.org/
^ permalink raw reply [flat|nested] 6+ messages in thread
* [ruby-core:122362] [Ruby Feature#21387] Proposal to add Data#[]
2025-05-31 6:10 [ruby-core:122350] [Ruby Feature#21387] Proposal to add Data#[] ybiquitous (Masafumi Koba) via ruby-core
` (2 preceding siblings ...)
2025-05-31 16:42 ` [ruby-core:122354] " zverok (Victor Shepelev) via ruby-core
@ 2025-06-01 19:48 ` Eregon (Benoit Daloze) via ruby-core
2025-06-02 10:17 ` [ruby-core:122378] " ybiquitous (Masafumi Koba) via ruby-core
4 siblings, 0 replies; 6+ messages in thread
From: Eregon (Benoit Daloze) via ruby-core @ 2025-06-01 19:48 UTC (permalink / raw)
To: ruby-core; +Cc: Eregon (Benoit Daloze)
Issue #21387 has been updated by Eregon (Benoit Daloze).
I think it's easy enough to add `[]` for your Data subclass where you want it.
I think it's best to avoid adding extra methods to Data as it seems not unlikely some might want [] to not be defined or to be defined differently.
@zverok makes a good point that Data shouldn't be used like a Hash with dynamic keys (especially not become OpenStruct-like).
It's a data structure with well-known fields and these should be accessed directly, the case of accessing them generically is the exception and can already be done through `to_h`.
----------------------------------------
Feature #21387: Proposal to add Data#[]
https://bugs.ruby-lang.org/issues/21387#change-113505
* Author: ybiquitous (Masafumi Koba)
* Status: Open
----------------------------------------
## Proposal
I propose to add a new instance method `#[]` to the `Data` class, similar to [`Struct#[]`](https://docs.ruby-lang.org/en/3.4/Struct.html#method-i-5B-5D).
If writing the method signature in RBS, it would be like this:
```ruby
class Data
def []: (name: String | Symbol) -> untyped
end
```
Requirements:
- `Data#[]` accepts a member name as a `Symbol` or `String`, e.g., `data[:id] == data["id"]`.
- `Data#[]` returns a value associated with the given `name`, e.g., `data[:id] == data.id`.
- `Data#[]` raises `NameError` if the given `name` is not one of the members, e.g., `data[:invalid]`.
Note: Please assume that `data = Data.define(:id).new(id: 100)` is given in the examples above.
## Motivation
In Active Support Core Extensions of Rails, I found a use case that `Data#[]` would be helpful with `Enumerable#pluck`.
Please look at this example:
```ruby
# data_test.rb
require "bundler/inline"
gemfile do
source "https://rubygems.org"
gem "activesupport", "8.0.2"
end
require "active_support/core_ext/enumerable"
StructPerson = Struct.new(:name, :age)
DataPerson = Data.define(:name, :age)
struct_people = [
StructPerson.new("Bob", 25),
]
puts "Struct:"
puts "=> #{struct_people.pluck(:name)}"
data_people = [
DataPerson.new("Charlie", 35),
]
puts "Data:"
begin
puts "=> #{data_people.pluck(:name)}"
rescue => e
puts e.detailed_message(syntax_suggest: true)
end
```
Running this script outputs below:
```shell
$ ruby data_test.rb
Struct:
=> ["Bob"]
Data:
undefined method '[]' for an instance of DataPerson (NoMethodError)
map { |element| element[key] }
^^^^^
```
Note: This output resulted on `ruby 3.4.4 (2025-05-14 revision a38531fd3f) +PRISM [arm64-darwin24]`.
The error reason is that the `Enumerable#pluck` extension expects all the elements in the array to respond to `[]`.
See also the `pluck` code here:
https://github.com/rails/rails/blob/v8.0.2/activesupport/lib/active_support/core_ext/enumerable.rb#L145-L152
If `Data#[]` was implemented as follows,
```ruby
# data_patch.rb
class Data
def [](name)
unless members.include?(name.to_sym)
raise NameError, "no member '#{name}' in data"
end
public_send(name)
end
end
```
The script is successful:
```shell
$ ruby -r ./data_patch.rb data_test.rb
Struct:
=> ["Bob"]
Data:
=> ["Charlie"]
```
Ref:
- https://guides.rubyonrails.org/v8.0.2/active_support_core_extensions.html#pluck
- https://api.rubyonrails.org/v8.0.2/classes/Enumerable.html#method-i-pluck
Although `Enumerable#pluck` is just an example, I guess that there would be other cases where `Data` objects should respond to `[]`.
## Reasoning
>From the long discussion in #16122 that introduced `Data`, I understand that `Data#[]` was rejected in #16122#note-28 because `[]` was like `Enumerable`.
I also agree with rejecting `Data#each` since `Data` is not an `Enumerable`, but `[]` seems acceptable to me.
Reasons:
- `[]` is sometimes used for non-container objects, such as [`ActiveRecord::AttributeMethods#[]`](https://api.rubyonrails.org/v8.0.2/classes/ActiveRecord/AttributeMethods.html#method-i-5B-5D).
- Considering `Data#to_h` is provided, it seems reasonable that we could access a `Data` member's value through `[]`.
- From the similarity between `Struct` and `Data`, some people might expect `Hash`-like accessing via `[]`.
- Unless `[]` is provided, we have to call `public_send` or convert to a hash via `to_h` when we want to get member values with names. It'd be inefficient and easy to make mistakes.
Let me show an example:
```ruby
Person = Data.define(:name, :age)
charlie = Person.new("Charlie", 35)
member_names_from_user_input = ["age", "hash"] # "hash" is a malicious input.
# Using #public_send
member_names_from_user_input.map { charlie.public_send(it) }
#=> [35, -1363726049241242821]
# Using #to_h and Hash#fetch with string keys
member_names_from_user_input.map { charlie.to_h.fetch(it) }
#=> key not found: "age" (KeyError)
# Using #to_h and Hash#fetch with symbol keys
member_names_from_user_input.map { charlie.to_h.fetch(it.to_sym) }
#=> key not found: :hash (KeyError)
# Using #[]
member_names_from_user_input.map { charlie[it] }
#=> no member 'hash' in data (NameError)
```
The last example is most elegant for the same goal. That's why I propose to add `Data#[]`.
However, there might be downsides that I have overlooked, so I'd appreciate it if you could let me know.
Thanks.
--
https://bugs.ruby-lang.org/
______________________________________________
ruby-core mailing list -- ruby-core@ml.ruby-lang.org
To unsubscribe send an email to ruby-core-leave@ml.ruby-lang.org
ruby-core info -- https://ml.ruby-lang.org/mailman3/lists/ruby-core.ml.ruby-lang.org/
^ permalink raw reply [flat|nested] 6+ messages in thread
* [ruby-core:122378] [Ruby Feature#21387] Proposal to add Data#[]
2025-05-31 6:10 [ruby-core:122350] [Ruby Feature#21387] Proposal to add Data#[] ybiquitous (Masafumi Koba) via ruby-core
` (3 preceding siblings ...)
2025-06-01 19:48 ` [ruby-core:122362] " Eregon (Benoit Daloze) via ruby-core
@ 2025-06-02 10:17 ` ybiquitous (Masafumi Koba) via ruby-core
4 siblings, 0 replies; 6+ messages in thread
From: ybiquitous (Masafumi Koba) via ruby-core @ 2025-06-02 10:17 UTC (permalink / raw)
To: ruby-core; +Cc: ybiquitous (Masafumi Koba)
Issue #21387 has been updated by ybiquitous (Masafumi Koba).
@zverok @eregon Thanks for the feedback.
Reconsidering my motivation, I may have expected `Data` to be something like an *immutable* `Struct` (or even an *immutable* `Hash`).
I'm still attracted to this idea of an immutable `Hash`-like data structure, but I must agree with the possibility of interface pollution that `Data` would suffer like `OpenStruct`, as you worry. As long as there is no fascinating use case, we should probably stop enhancing the `Data` interface.
Also, I can work around the issue by extending `Data` only in my app, not monkey patching, like this:
```ruby
module DynamicallyAccessibleData
def [](name)
unless members.include?(name.to_sym)
raise NameError, "no member '#{name}' in data"
end
public_send(name)
end
end
Person = Data.define(:name, :age).include(DynamicallyAccessibleData)
person = Person.new(name: "Charlie", age: 35)
person[:name] #=> "Charlie"
person[:age] #=> 35
person[:foo] #=> NameError: no member 'foo' in data
```
I'll leave the ticket open for a while since anyone else might be interested, but if no one reacts, I'll close it.
Otherwise, please close it if you find it clear that nobody does. Thanks.
----------------------------------------
Feature #21387: Proposal to add Data#[]
https://bugs.ruby-lang.org/issues/21387#change-113524
* Author: ybiquitous (Masafumi Koba)
* Status: Open
----------------------------------------
## Proposal
I propose to add a new instance method `#[]` to the `Data` class, similar to [`Struct#[]`](https://docs.ruby-lang.org/en/3.4/Struct.html#method-i-5B-5D).
If writing the method signature in RBS, it would be like this:
```ruby
class Data
def []: (name: String | Symbol) -> untyped
end
```
Requirements:
- `Data#[]` accepts a member name as a `Symbol` or `String`, e.g., `data[:id] == data["id"]`.
- `Data#[]` returns a value associated with the given `name`, e.g., `data[:id] == data.id`.
- `Data#[]` raises `NameError` if the given `name` is not one of the members, e.g., `data[:invalid]`.
Note: Please assume that `data = Data.define(:id).new(id: 100)` is given in the examples above.
## Motivation
In Active Support Core Extensions of Rails, I found a use case that `Data#[]` would be helpful with `Enumerable#pluck`.
Please look at this example:
```ruby
# data_test.rb
require "bundler/inline"
gemfile do
source "https://rubygems.org"
gem "activesupport", "8.0.2"
end
require "active_support/core_ext/enumerable"
StructPerson = Struct.new(:name, :age)
DataPerson = Data.define(:name, :age)
struct_people = [
StructPerson.new("Bob", 25),
]
puts "Struct:"
puts "=> #{struct_people.pluck(:name)}"
data_people = [
DataPerson.new("Charlie", 35),
]
puts "Data:"
begin
puts "=> #{data_people.pluck(:name)}"
rescue => e
puts e.detailed_message(syntax_suggest: true)
end
```
Running this script outputs below:
```shell
$ ruby data_test.rb
Struct:
=> ["Bob"]
Data:
undefined method '[]' for an instance of DataPerson (NoMethodError)
map { |element| element[key] }
^^^^^
```
Note: This output resulted on `ruby 3.4.4 (2025-05-14 revision a38531fd3f) +PRISM [arm64-darwin24]`.
The error reason is that the `Enumerable#pluck` extension expects all the elements in the array to respond to `[]`.
See also the `pluck` code here:
https://github.com/rails/rails/blob/v8.0.2/activesupport/lib/active_support/core_ext/enumerable.rb#L145-L152
If `Data#[]` was implemented as follows,
```ruby
# data_patch.rb
class Data
def [](name)
unless members.include?(name.to_sym)
raise NameError, "no member '#{name}' in data"
end
public_send(name)
end
end
```
The script is successful:
```shell
$ ruby -r ./data_patch.rb data_test.rb
Struct:
=> ["Bob"]
Data:
=> ["Charlie"]
```
Ref:
- https://guides.rubyonrails.org/v8.0.2/active_support_core_extensions.html#pluck
- https://api.rubyonrails.org/v8.0.2/classes/Enumerable.html#method-i-pluck
Although `Enumerable#pluck` is just an example, I guess that there would be other cases where `Data` objects should respond to `[]`.
## Reasoning
>From the long discussion in #16122 that introduced `Data`, I understand that `Data#[]` was rejected in #16122#note-28 because `[]` was like `Enumerable`.
I also agree with rejecting `Data#each` since `Data` is not an `Enumerable`, but `[]` seems acceptable to me.
Reasons:
- `[]` is sometimes used for non-container objects, such as [`ActiveRecord::AttributeMethods#[]`](https://api.rubyonrails.org/v8.0.2/classes/ActiveRecord/AttributeMethods.html#method-i-5B-5D).
- Considering `Data#to_h` is provided, it seems reasonable that we could access a `Data` member's value through `[]`.
- From the similarity between `Struct` and `Data`, some people might expect `Hash`-like accessing via `[]`.
- Unless `[]` is provided, we have to call `public_send` or convert to a hash via `to_h` when we want to get member values with names. It'd be inefficient and easy to make mistakes.
Let me show an example:
```ruby
Person = Data.define(:name, :age)
charlie = Person.new("Charlie", 35)
member_names_from_user_input = ["age", "hash"] # "hash" is a malicious input.
# Using #public_send
member_names_from_user_input.map { charlie.public_send(it) }
#=> [35, -1363726049241242821]
# Using #to_h and Hash#fetch with string keys
member_names_from_user_input.map { charlie.to_h.fetch(it) }
#=> key not found: "age" (KeyError)
# Using #to_h and Hash#fetch with symbol keys
member_names_from_user_input.map { charlie.to_h.fetch(it.to_sym) }
#=> key not found: :hash (KeyError)
# Using #[]
member_names_from_user_input.map { charlie[it] }
#=> no member 'hash' in data (NameError)
```
The last example is most elegant for the same goal. That's why I propose to add `Data#[]`.
However, there might be downsides that I have overlooked, so I'd appreciate it if you could let me know.
Thanks.
--
https://bugs.ruby-lang.org/
______________________________________________
ruby-core mailing list -- ruby-core@ml.ruby-lang.org
To unsubscribe send an email to ruby-core-leave@ml.ruby-lang.org
ruby-core info -- https://ml.ruby-lang.org/mailman3/lists/ruby-core.ml.ruby-lang.org/
^ permalink raw reply [flat|nested] 6+ messages in thread
end of thread, other threads:[~2025-06-02 10:18 UTC | newest]
Thread overview: 6+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2025-05-31 6:10 [ruby-core:122350] [Ruby Feature#21387] Proposal to add Data#[] ybiquitous (Masafumi Koba) via ruby-core
2025-05-31 7:16 ` [ruby-core:122351] " zverok (Victor Shepelev) via ruby-core
2025-05-31 13:41 ` [ruby-core:122353] " ybiquitous (Masafumi Koba) via ruby-core
2025-05-31 16:42 ` [ruby-core:122354] " zverok (Victor Shepelev) via ruby-core
2025-06-01 19:48 ` [ruby-core:122362] " Eregon (Benoit Daloze) via ruby-core
2025-06-02 10:17 ` [ruby-core:122378] " ybiquitous (Masafumi Koba) via ruby-core
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).