Feature #19830
openAllow `Array#transpose` to take an optional size argument
Description
One benefit of supplying an initial value to Enumerable#inject
is that it avoids an annoying edge case when the collection is empty:
>> [1, 2, 3].inject(:+)
=> 6 # good
>> [].inject(:+)
=> nil # bad
>> [].inject(0, :+)
=> 0 # good
A similar edge case exists for Array#transpose
:
>> [[1, :a], [2, :b], [3, :c]].transpose
=> [[1, 2, 3], [:a, :b, :c]] # good
>> [].transpose
=> [] # bad
Although no explicit nil
is produced here, the subtle problem is that the caller may assume that the result array contains arrays, and that assumption leads to nil
s in the empty case:
>> [[1, :a], [2, :b], [3, :c]].transpose.then { _2.join }
=> "abc"
>> [].transpose.then { _2.join }
undefined method `join' for nil:NilClass (NoMethodError)
If we allow Array#transpose
to take an optional argument specifying the size of the result array, we can use this to always return an array of the correct size:
>> [[1, :a], [2, :b], [3, :c]].transpose(2)
=> [[1, 2, 3], [:a, :b, :c]] # good
>> [].transpose(2)
=> [[], []] # good
By avoiding an unexpectedly empty result array, we also avoid unexpected downstream nil
s:
>> [[1, :a], [2, :b], [3, :c]].transpose(2).then { _2.join }
=> "abc"
>> [].transpose(2).then { _2.join }
=> ""
Here is a patch which adds an optional argument to Array#transpose
to support the above usage: https://github.com/ruby/ruby/pull/8167
Something similar was requested eleven years ago in #6852. I believe this feature addresses the problem expressed in that issue without compromising backward compatibility with existing callers of #transpose.
Updated by nobu (Nobuyoshi Nakada) over 1 year ago
Why not ary.transpose[1]&.join
?
Updated by tomstuart (Tom Stuart) over 1 year ago
nobu (Nobuyoshi Nakada) wrote in #note-1:
Why not
ary.transpose[1]&.join
?
That avoids the exception but returns nil
instead of taking advantage of #join
’s knowledge of the “correct” result for an empty array, i.e. the empty string. If the caller was expecting #transpose
to return an array of arrays, they’ll also be expecting #join
to return a string, so the safe navigation operator just pushes the nil
problem even further downstream.
So why not ary.transpose[1]&.join || ""
or (ary.transpose[1] || []).join
or even ary.transpose[1].to_a.join
? Yes, any of them will work. The optional size argument is intended to provide a more general and elegant solution so that these case-by-case nil
workarounds can be avoided.
Here is an example which is closer to a problem I encounter in real programs:
>> ary = [[1, :a], [2, :b], [3, :c]]
>> ary.transpose.then { |numbers, letters| [numbers.sum, letters.join] }
=> [6, "abc"]
>> ary = []
>> ary.transpose.then { |numbers, letters| [numbers.sum, letters.join] }
undefined method `sum' for nil (NoMethodError)
How should we avoid the exception and get the result we want in this case, i.e. [0, ""]
? I would like to avoid having to hardcode the correct “empty array result” value every time (e.g. [numbers&.sum || 0, letters&.join || ""]
) because #sum
and #join
already have these built in, and it’s inconvenient to default each parameter to the empty array (e.g. [(numbers || []).sum, (letters || []).join]
) if they’re used in multiple places.
I usually solve the problem on entry to the block by making its parameters optional:
>> ary.transpose.then { |numbers = [], letters = []| [numbers.sum, letters.join] }
=> [0, ""]
This allows the body of the block to avoid having to deal with the possibility of numbers
and letters
being nil
; I know that #sum
and #join
will give the correct results automatically. But it’s verbose, and sometimes I forget to do it, and until recently it didn’t work with YJIT.
With the optional size argument to #transpose
I only have to say how many array parameters the block expects:
>> ary.transpose(2).then { |numbers, letters| [numbers.sum, letters.join] }
=> [0, ""]
As well as being more concise, I think this is easier to get right every time.
Updated by nobu (Nobuyoshi Nakada) over 1 year ago
It doesn't feel elegant or concise to feed the element size to me.
For what use cases do you want it actually?