Project

General

Profile

Feature #16261

Enumerable#each_splat and Enumerator#splat

Added by zverok (Victor Shepelev) 30 days ago. Updated 22 days ago.

Status:
Open
Priority:
Normal
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

Related to Ruby master - Feature #4539: Array#zip_withAssignedActions
Related to Ruby master - Feature #5044: #zip with block return mapped resultsRejectedActions

History

Updated by shevegen (Robert A. Heiler) 30 days 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) 29 days 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) 26 days 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) 26 days 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) 26 days ago

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

Updated by Eregon (Benoit Daloze) 26 days 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) 25 days 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
#8

Updated by duerst (Martin Dürst) 25 days ago

#9

Updated by duerst (Martin Dürst) 25 days ago

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

Updated by duerst (Martin Dürst) 25 days 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) 25 days 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.

#12

Updated by zverok (Victor Shepelev) 22 days ago

  • Description updated (diff)
#13

Updated by zverok (Victor Shepelev) 22 days ago

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

Also available in: Atom PDF