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) 12 months ago
Why not ary.transpose[1]&.join
?
Updated by tomstuart (Tom Stuart) 12 months 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) 12 months ago
It doesn't feel elegant or concise to feed the element size to me.
For what use cases do you want it actually?