Feature #16261
closedEnumerable#each_splat and Enumerator#splat
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]
Updated by shevegen (Robert A. Heiler) about 5 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) about 5 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) about 5 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) about 5 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) about 5 years ago
Note that each{ |*item| yield(item) }
doesn't work because of #16166.
Updated by Eregon (Benoit Daloze) about 5 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) about 5 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
Updated by duerst (Martin Dürst) about 5 years ago
- Related to Feature #4539: Array#zip_with added
Updated by duerst (Martin Dürst) about 5 years ago
- Related to Feature #5044: #zip with block return mapped results added
Updated by duerst (Martin Dürst) about 5 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) about 5 years ago
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.
Updated by zverok (Victor Shepelev) about 5 years ago
- Description updated (diff)
Updated by zverok (Victor Shepelev) about 5 years ago
- Subject changed from Enumerable#each_tuple to Enumerable#each_splat and Enumerator#splat
Updated by knu (Akinori MUSHA) about 5 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) about 5 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.