Project

General

Profile

Actions

Feature #18366

closed

Enumerator#return_eval

Added by sawa (Tsuyoshi Sawada) about 2 months ago. Updated about 2 months ago.

Status:
Rejected
Priority:
Normal
Assignee:
-
Target version:
-
[ruby-core:106305]

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"]
Actions #1

Updated by sawa (Tsuyoshi Sawada) about 2 months ago

  • Description updated (diff)

Updated by baweaver (Brandon Weaver) about 2 months 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.

Actions #3

Updated by sawa (Tsuyoshi Sawada) about 2 months ago

  • Description updated (diff)

Updated by sawa (Tsuyoshi Sawada) about 2 months 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 with filter_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) about 2 months 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) about 2 months 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) about 2 months ago

  • Status changed from Open to Rejected
  • There's no real-world use case shown
  • the term eval is not sufficient (unless it works as eval)
  • return is even worse

3 strikes. Rejected. Reopen if those are addressed.

Matz.

Updated by mame (Yusuke Endoh) about 2 months ago

a.lazy.map(&:length).max

Updated by sawa (Tsuyoshi Sawada) about 2 months 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.

Actions

Also available in: Atom PDF