Project

General

Profile

Actions

Feature #16261

closed

Enumerable#each_splat and Enumerator#splat

Added by zverok (Victor Shepelev) over 4 years ago. Updated over 4 years ago.

Status:
Rejected
Assignee:
-
Target version:
-
[ruby-core:95419]

Description

UPD: After discussion in comments, method names changed to "splat"-based.

New methods proposal.

Prototype code:

module Enumerable
  def each_splat
    return to_enum(__method__) unless block_given?
    each_entry { |item| yield(*item) } # unpacking possible array into several args
  end
end

class Enumerator
  def splat
    return to_enum(:splat) unless block_given?
    each_entry { |item| yield(*item) }
  end
end

Supposed documentation/explanation:

For enumerable with Array items, passes all items in the block provided as a separate arguments. t could be useful if the provided block has lambda semantics, e.g. doesn't unpack arguments automatically. For example:

files = ["README.md", "LICENSE.txt", "Contributing.md"]
content = [fetch_readme, fetch_license, fetch_contributing] # somehow make a content for the files

files.zip(content).each_splat(&File.:write) # writes to each file its content

When no block passed, returns enumerator of the tuples:

[1, 2, 3].zip([4, 5, 6]).each_splat.map(&:+) # => [5, 7, 9] 

Related issues 2 (1 open1 closed)

Related to Ruby master - Feature #4539: Array#zip_withAssignedmatz (Yukihiro Matsumoto)Actions
Related to Ruby master - Feature #5044: #zip with block return mapped resultsRejectedmatz (Yukihiro Matsumoto)Actions

Updated by shevegen (Robert A. Heiler) over 4 years ago

Hmmmm.

A slight issue I see with the name "tuple", and then the implicit name addition
".each_tuple", which would then (indirectly) elevate the term tuple.

I know the word tuple from e. g. using tuple in python, but I much prefer ruby's
way to name things (not only because I used ruby for a longer time than python,
but because I think the names in ruby make more sense in general e. g. Array/Hashes
versus List/Dictionaries).

I am not sure if we have "tuples" in ruby core/stdlib yet. I did however google
and find it in Rinda ... so at the least Rinda in stdlib has tuples. :P
https://ruby-doc.org/stdlib-2.6.5/libdoc/rinda/rdoc/Rinda/Tuple.html
(Not sure about ruby core, though.)

There is also a slight issue with intrinsic complexity (in my opinion), but this
is a lot due to one's personal style and preferences, so I will not comment
much on that part - some ruby users prefer simplicity, others prefer more
flexibility in usage (aka more complex use cases). But I think the name itself
should be considered as well; for the use of .each_tuple, ruby users would
first have to understand what a tuple is. Compare this to e. g. .each_pair
which is a LOT simpler to understand even to genuinely new people. I also
admit that this is not a very strong argument per se, since we have other
variants of .each* already, such as .each_with_index - but I still think
we should be careful which .each* variants are added. I also have no
alternative name proposal, my apologies.

Updated by shan (Shannon Skipper) over 4 years ago

This reminds me of a neat post showing applicatives in pictures: http://adit.io/posts/2013-04-17-functors,_applicatives,_and_monads_in_pictures.html#applicatives

In Haskell:

[(*2), (+3)] <*> [1, 2, 3]
#=> [2,4,6,4,5,6]

Or with this proposal in Ruby:

[2.:*, 3.:+].product([1, 2, 3]).each_splat.map(&:call)
#=> [2, 4, 6, 4, 5, 6]

Updated by Dan0042 (Daniel DeLorme) over 4 years ago

It's worth pointing out the desired difference with regards to lambdas a bit more explicitly:

[1, 2, 3].zip([4, 5, 6]).map(&:+)            # ArgumentError (wrong number of arguments (given 0, expected 1))
[1, 2, 3].zip([4, 5, 6]).each_tuple.map(&:+) # => [5, 7, 9]

But in that case it seems to me the behavior you want is the opposite of a tuple. Where a tuple is a struct-like set of n elements like [1, 4], what you want here is to destructure that tuple in order to pass each element as an argument of a lambda. So it should be called maybe each_splat and the inverse operation would be called each_tuple.

Or how about something like this based on Enumerator? (hopefully without the hacks)

class Enumerator
  def splat
    return to_enum(:splat) unless block_given?
    #each{ |item| yield(*item) } #this doesn't always work
    each{ |first,*rest| yield(first,*rest) } #hacky solution
  end
  def tuple
    return to_enum(:tuple) unless block_given?
    #each{ |*item| yield(item) } #this doesn't always work
    each{ |first,*rest| yield([first,*rest]) } #hacky solution
  end
end

pairs = [10, 20, 30].zip([10, 16, 20])
pairs.each.map(&:to_s)       #=> ["[10, 10]", "[20, 16]", "[30, 20]"]
pairs.each.tuple.map(&:to_s) #=> ["[10, 10]", "[20, 16]", "[30, 20]"]
pairs.each.splat.map(&:to_s) #=> ["10", "14", "1a"]

%i[a b c].each_with_index.map(&:inspect)       # ArgumentError (wrong number of arguments (given 1, expected 0))
%i[a b c].each_with_index.tuple.map(&:inspect) # => ["[:a, 0]", "[:b, 1]", "[:c, 2]"]
%i[a b c].each_with_index.splat.map(&:inspect) # ArgumentError (wrong number of arguments (given 1, expected 0))

Updated by zverok (Victor Shepelev) over 4 years ago

@Dan0042 (Daniel DeLorme) super-good points, thanks!

I'd say that Enumerable#each_tuple/Enumerable#each_splat + Enumerator#tuple/Enumerator#splat is a most powerful and straightforward combination.

Updated by Dan0042 (Daniel DeLorme) over 4 years ago

Note that each{ |*item| yield(item) } doesn't work because of #16166.

Updated by Eregon (Benoit Daloze) over 4 years ago

FYI there is Enumerable#each_entry:

Calls block once for each element in self, passing that
element as a parameter, converting multiple values from yield to an array.

I think many methods already yield multiple arguments rather than an Array of arguments, zip being one of the exception.
So I'm not sure in how many cases such a method would be useful.

Updated by Dan0042 (Daniel DeLorme) over 4 years ago

@Eregon (Benoit Daloze) Thank you very much for the enlightenment!

That means the code above could be rewritten like this. And at that point it's doubtful if tuple is even needed.

class Enumerator
  def splat
    return to_enum(:splat) unless block_given?
    each_entry{ |item| yield(*item) }
  end
  def tuple
    return to_enum(:tuple) unless block_given?
    each_entry{ |item| yield(Array===item ? item : [item]) }
  end
end
Actions #8

Updated by duerst (Martin Dürst) over 4 years ago

Actions #9

Updated by duerst (Martin Dürst) over 4 years ago

  • Related to Feature #5044: #zip with block return mapped results added

Updated by duerst (Martin Dürst) over 4 years ago

Dan0042 (Daniel DeLorme) wrote:

It's worth pointing out the desired difference with regards to lambdas a bit more explicitly:

[1, 2, 3].zip([4, 5, 6]).map(&:+)            # ArgumentError (wrong number of arguments (given 0, expected 1))
[1, 2, 3].zip([4, 5, 6]).each_tuple.map(&:+) # => [5, 7, 9]

What you want to do here is in many other languages done with zip_with:

[1, 2, 3].zip_with([4, 5, 6], :+) # => [5, 7, 9]

There is already an issue for this, issue #4539, which is open and waits for Matz's approval.

Updated by zverok (Victor Shepelev) over 4 years ago

@duerst (Martin Dürst)

What you want to do here is in many other languages done with zip_with

I used zip only as a simplest way to construct an example. In our current codebase we have a fare share of internal methods defined with two pairs of braces, like def similar?((word1, word2)), because this allows us, for example, to say things like (imagine calculating some diffs):

diff_pairs.reject(&method(:similar?)).select(&method(:same_paragraph?)).map(&method(:calculate_closeness))

In other places, we are still just rely on map { |foo, bar, baz|, select and so on.

A very small amount of initial data of such chains is produced with zip, but even when it is, zip_with can't help with select/reject/group_by and other Enumerable methods.

Actions #12

Updated by zverok (Victor Shepelev) over 4 years ago

  • Description updated (diff)
Actions #13

Updated by zverok (Victor Shepelev) over 4 years ago

  • Subject changed from Enumerable#each_tuple to Enumerable#each_splat and Enumerator#splat

Updated by knu (Akinori MUSHA) over 4 years ago

I agree this feature would be a nice addition.

Actually I had exactly the same idea, presented at Rails Developer Meetup 2019: https://www.slideshare.net/akinorimushaevolution-of-enumerator (Japanese)

There's a subtle difference between Hash#each/map and Hash#select/reject in how they yield each key-value pair.

{a:1,b:2}.select{|x|p x}
# :a
# :b
{a:1,b:2}.each{|x|p x}
# [:a, 1]
# [:b, 2]

I guess this was an unintended difference, but we cannot fix it by now for compatibility reasons, and each_splat would be one way to work around it.

Updated by matz (Yukihiro Matsumoto) over 4 years ago

  • Status changed from Open to Rejected

As far as I understand, the code with the proposal [1, 2, 3].zip([4, 5, 6]).each_tuple.map(&:+) can be written as following with numbered parameters:

[1, 2, 3].zip([4, 5, 6]).map{_1 + _2}

which is quite plain and shorter. So I reject the idea for the time being. Maybe we will revisit the idea once we re-introduce the method reference operator in the future.

Matz.

Actions

Also available in: Atom PDF

Like0
Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0