Issue #19830 has been updated by tomstuart (Tom Stuart).
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](https://github.com/ruby/ruby/pull/8006) 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.
----------------------------------------
Feature #19830: Allow `Array#transpose` to take an optional size argument
https://bugs.ruby-lang.org/issues/19830#change-104081
* Author: tomstuart (Tom Stuart)
* Status: Open
* Priority: Normal
----------------------------------------
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.
--
https://bugs.ruby-lang.org/