[ruby-core:114342] [Ruby master Feature#19830] Allow `Array#transpose` to take an optional size argument

Issue #19830 has been reported by tomstuart (Tom Stuart). ---------------------------------------- Feature #19830: Allow `Array#transpose` to take an optional size argument https://bugs.ruby-lang.org/issues/19830 * 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/

Issue #19830 has been updated by nobu (Nobuyoshi Nakada). Why not `ary.transpose[1]&.join`? ---------------------------------------- Feature #19830: Allow `Array#transpose` to take an optional size argument https://bugs.ruby-lang.org/issues/19830#change-104079 * 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/

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/

Issue #19830 has been updated by nobu (Nobuyoshi Nakada). It doesn't feel elegant or concise to feed the element size to me. For what use cases do you want it actually? ---------------------------------------- Feature #19830: Allow `Array#transpose` to take an optional size argument https://bugs.ruby-lang.org/issues/19830#change-104082 * 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/
participants (2)
-
nobu (Nobuyoshi Nakada)
-
tomstuart (Tom Stuart)