Feature #18366
closedEnumerator#return_eval
Description
Some Enumerable
methods return one or more of the receiver's elements according to the return value of a block it takes. Often, we want such evaluated value rather than the original element.
For example, suppose we want to know the character width sufficient to fit all the strings in an array:
a = ["Hello", "my", "name", "is", "Ruby"]
We either have to repeat the evaluation of the block:
a.max_by(&:length).length # => 5
or create a temporal array:
a.map(&:length).max # => 5
both of which seem not to be optimal.
I propose to have a method Enumerator#return_eval
that returns the evaluated value(s) of the block:
a.max_by.return_eval(&:length) # => 5
a.min_by.return_eval(&:length) # => 2
a.minmax_by.return_eval(&:length) # => [2, 5]
["Ava Davidson", "Benjamin Anderson", "Charlie Baker"]
.sort_by.return_eval{_1.split.reverse.join(", ")} # => ["Anderson, Benjamin", "Baker, Charlie", "Davidson, Ava"]
Updated by baweaver (Brandon Weaver) almost 3 years ago
Interesting. It seems the common usecase you have isolated is similar to the idea of composing some function with the idea of map
, much like we may see with filter_map
:
[1, 2, 3].filter_map { |v| v * 2 if v.even? }
# vs
[1, 2, 3].filter { |v| v.even? }.map { |v| v * 2 }
I have mused on the idea of making a more generic composition mechanic for Enumerable methods, but this can be awkward given Ruby's OO semantics.
This does get close to the idea of Transducers (read more here) in which you can combine effects to bypass the inefficiencies as demonstrated by Rich Hickey in Clojure.
Anyways, I feel like we're trying to approximate composition of Enumerable methods, which syntactically is a hard task to do but could be incredibly useful. Not sure how I'd go about it myself, but this is an interesting start to the idea.
Updated by sawa (Tsuyoshi Sawada) almost 3 years ago
baweaver (Brandon Weaver) wrote in #note-2:
It seems the common usecase you have isolated is similar to the idea of composing some function with the idea of
map
, much like we may see withfilter_map
:[1, 2, 3].filter_map { |v| v * 2 if v.even? }
Thanks for mentioning that. The use cases of filter_map
in general is more complex than what can be done by Enumerator#return_eval
since it needs both the filtering condition and the mapped value, but indeed, certain sub-cases can be handled:
["Ms. Foo", "Dr. Bar", "Baz"].select{_1.match?(/\b[A-Z]\w+\./)}.map{_1[/\b[A-Z]\w+\./]} # => ["Ms.", "Dr."]
["Ms. Foo", "Dr. Bar", "Baz"].select.return_eval{_1[/\b[A-Z]\w+\./]} # => ["Ms.", "Dr."]
Updated by shan (Shannon Skipper) almost 3 years ago
Just a thought, but another option to achieve the aims of this proposal might be to add return_eval: true
kwargs for Enumerable methods. On the transducer front Brandon mentions, I've wished we had Enumerable kwargs to set the reducing function and an accumulator other than an Array.
For example.
module TransducerSelect
refine Array do
def select(acc: [], step: :<<, step_eval: false)
unless block_given?
return to_enum(__method__) { size if respond_to?(:size) }
end
each do
yielded = yield _1
step_value = step_eval ? yielded : _1
acc.public_send(step, step_value) if yielded
end
acc
end
end
end
using TransducerSelect
["Ms. Foo", "Dr. Bar", "Baz"].select(step_eval: true){_1[/\b[A-Z]\w+\./]}
#=> ["Ms.", "Dr."]
["Ms. Foo", "Dr. Bar", "Baz"].select acc: Set.new, step: :add, step_eval: true do
_1[/\b[A-Z]\w+\./]
end
#=> #<Set: {"Ms.", "Dr."}>
["Ms. Foo", "Dr. Bar", "Baz"].select acc: $stdout, step: :puts, step_eval: true do
_1[/\b[A-Z]\w+\./]
end
#>> Ms.
#>> Dr.
#=> #<IO:<STDOUT>>
Updated by Eregon (Benoit Daloze) almost 3 years ago
I would advise against making Enumerable methods more complex with additional arguments, it'll only make them slower or more complicated to JIT compile, in addition to making their role less clear.
How would return_eval
work? Could you write it in Ruby or as pseudo-code?
Updated by matz (Yukihiro Matsumoto) almost 3 years ago
- Status changed from Open to Rejected
- There's no real-world use case shown
- the term
eval
is not sufficient (unless it works aseval
) -
return
is even worse
3 strikes. Rejected. Reopen if those are addressed.
Matz.
Updated by mame (Yusuke Endoh) almost 3 years ago
a.lazy.map(&:length).max
Updated by sawa (Tsuyoshi Sawada) almost 3 years ago
mame (Yusuke Endoh) wrote in #note-8:
a.lazy.map(&:length).max
Thank you. That seems to do the work.
I have no objection against this being closed.