こんにちは!SmartHR 社で業務委託を承っております osyo と申します。
業務では主に activerecord-bitemporal の開発・保守を行っております。
また、最近は趣味で Ruby にパッチを投げたりしています。
activerecord-bitemporal について詳しく知りたい方はこちらのスライドを読んでみましょう!
さて、先日 Ruby 2.7.0-preview1 がリリースされましたね。
Ruby 2.7.0-preview1 では、RubyKaigi 2019 でも話題になっていたパターンマッチが実験的に導入されたり、irb
がめちゃくちゃ強化されていたりと注目する機能が目白押しになっています。
ちなみに Ruby 2.7.0-preview1 にはわたしが提案・パッチを書いた Time#floor
と Time#ceil
も追加されたりしています。
そんな中、今回は同様に Ruby 2.7.0-preview1 で追加された Numbered parameters(略してナンパラ)という機能の紹介とそれが導入された背景について書かせていただきます。
本記事に書かれている Ruby のコードは Ruby 2.7.0-preview1 版で動作確認を行っています。
$ ruby -v ruby 2.7.0preview1 (2019-05-31 trunk c55db6aa271df4a689dc8eb0039c929bf6ed43ff) [x86_64-linux]
わたしも Numbered parameters を試してみたい!!!という方は ruby-build が Ruby 2.7.0-preview1 に対応済みなので rbenv でシュッとインストールする事が出来ます。
また『ローカルにインストールなんてめんどくさい!!!今すぐ使いたい!!!!』という方は Wandbox というサービスも利用する事が出来ます。
開発版の Ruby でコードを実行したい場合は [Ruby] → [ruby HEAD 2.7.0dev] と選んでコードを入力すればブラウザ上ですぐさま最新の Ruby のコードを実行する事が出来ます。
上記のサイトでは Ruby に限らず様々な言語の処理系をブラウザ上で実行する事が出来るのでおすすめです。
Numbered parameters とは
ではでは、Numbered parameters について書いていきたいと思います。
Numbered parameters とはブロックの仮引数を省略した場合に @1
@2
@3
... という記号で引数を参照する事が出来る機能です。
この記事では Numbered parameters と表記していますが Ruby 2.7.0-preview1 のリリースノート では 番号指定パラメータ
と日本語で明記されています。
某所では略してナンパラと呼ばれているそうです。わたしも最近ではナンパラと呼んでいます。ナンパラ使って行きましょう。
さて、言葉で説明するよりもコードを見たほうがわかりやすいので実際に書いてみましょう。
次のようにブロックの仮引数を省略した場合に @1
という特別な記号で第一引数を参照する事が出来ます。
# ブロックの第一引数を @1 で参照できる %w(homu mami mado).each { puts @1 } # => homu # mami # mado
この @1
は通常の仮引数と同様に扱う事が出来るのでメソッド呼び出しなども行う事も出来ます。
p %w(homu mami mado).map { "#{@1.downcase} : #{@1.upcase}" } # => ["homu : HOMU", "mami : MAMI", "mado : MADO"]
参照する事が出来る仮引数は @1
だけではなくて @2
のように複数の仮引数を参照する事も出来ます。
@2
の場合は第二引数を参照します。
# ブロックの第一引数を @1 で、第二引数を @2 で参照する p %w(homu mami mado).map { @1.size }.inject { @1 + @2 } # => 12
また、引数が渡されなかった仮引数の Numbered parameters を参照すると nil
を返します。
# 引数が渡されていなかった場合は nil を返す p proc { [@1, @2, @3] }.call 1 # => [1, nil, nil]
Numbered parameters は現時点では @1 ~ @100
まで使用する事が出来ます。
@0
ではなくて @1
から始まる事に注意する必要があります。
p proc { [@1, @10, @100] }.call *(1..100) # => [1, 10, 100]
@100
以降を記述するとコンパイルエラーになります。
# Compile error: too large numbered parameter p proc { @101 }.call
このように『ブロックの仮引数を定義せずに引数を参照する事が出来る』というのが Numbered parameters の機能になります。
これにより、次のようなコードが Numbered parameters を使用する事で簡潔に記述する事が出来ます。
Numbered parameters なし
%w(homu mami mado).map(&:upcase) %w(homu mami mado).map(&method(:p)) (1..10).map(&2.method(:**)) [1 ,2, 16].map { |it| it.to_s(16) } %w{72 101 108 108 111}.map { |it| it.to_i.chr } [9, 7, 10, 11, 8].sort { |a, b| a.to_s <=> b.to_s }
Numbered parameters あり
%w(homu mami mado).map { @1.upcase } %w(homu mami mado).map { p @1 } (1..10).map { 2 ** @1 } [1 ,2, 16].map { @1.to_s(16) } %w{72 101 108 108 111}.map { @1.to_i.chr } [9, 7, 10, 11, 8].sort { @1.to_s <=> @2.to_s }
今までは &
渡しでブロックを記述したり、仮引数を定義するのが手間と感じる事が多かったんですが、Numbered parameters を使用する事でより直接的に、より簡潔にブロックを記述する事が出来るようになります。
とても便利ですね。
もうちょっとツッコんだ使い方をしてみる
さてさて、Numbered parameters について簡単に説明しました。
しかし、実際に使ってみるとハマりポイントや「こう書くとどうなるのかな?」みたいなコードがいくつかあったので例を上げてみたいと思います。
配列を渡すと展開される
Numbered parameters では『ブロックの実引数に配列を渡す』と『配列が展開された状態』で仮引数を参照します。
何を言ってるのかわからないと思うので実際のコードを見てみましょう。
次のように @1
を参照しているブロックに対して配列を渡すと @1
は配列の先頭を参照します。
# 配列を渡された場合、配列の先頭が @1 に代入される p proc { @1 }.call [1, 2] # => 1
これは、Numbered parameters の仮引数の受け取り方の仕様が |a,|
に準じているからです。
次のように Ruby では |a|
と |a,|
で実引数の受け取り方が少し異なります。
# |a| の場合はそのまま配列で受け取る p proc { |a| a }.call [1, 2] # => [1, 2] # |a,| とした場合は『配列を展開して』実引数を受け取る p proc { |a,| a }.call [1, 2] # => 1
このように |a|
はそのまま配列を受け取りますが |a,|
の場合は『配列の先頭』を受け取ります。
また、@1
以降も同様に配列が展開された状態で参照します。
# @1 = 3, @2 = -1 p proc { [@1, @2] }.call 3, -1 # => [3, -1] # 配列を渡すと配列が展開された状態で参照される # @1 = 6, @2 = 9 になる p proc { [@1, @2] }.call [6, 9] # => [6, 9]
このように [6, 9]
のような配列を渡した場合、 @1
は [6, 9]
ではなくて 6
になります。
なので、次のように『配列の配列』をイテレーションする場合に注意が必要になります。
# @1 は配列ではないので添字アクセスが出来ない # 配列の要素を直接参照する p [[1, 2, 3], [4, 5, 6], [7, 8, 9]].map { @1 + @2 + @3 } # => [6, 15, 24]
この挙動は慣れるまでにちょっと混乱しそうですね。
ただし、これには利点もあり、例えば Hash
をイテレーションする場合に key
と value
をそれぞれ @1
と @2
で参照する事が出来ます。
homu = { id: 1, name: "Homu", age: 14 } # @1 が key, @2 で value が参照できる p homu.map { "#{@1} is #{@2.class}" } # => ["id is Integer", "name is String", "age is Integer"]
これは便利そうですね。
このように『実引数に配列を渡した時の挙動』っていうのが Numbered parameters を使う上での一番の注意点だと思います。
ちなみに実引数が 配列 + α
の場合だと配列は展開されないのでこの点もちょっと注意する必要はありますね。
# @1 = [1, 2], @2 = 3 p proc { [@1, @2] }.call [1, 2], 3 # => [[1, 2], 3]
仮引数と併用できない
仮引数が定義されているブロックで Numbered parameters を参照するとコンパイルエラーになります。
# Compile error: ordinary parameter is defined # 仮引数と Numbered parameters は併用して使えない p proc { |a| a + @1 }
仮引数を定義しつつ併用して Numbered parameters を参照する事は出来ないので注意してください。
ブロック引数と併用出来ない
現在の仕様では Numbered parameters は『ブロックのブロック引数』を参照する事が出来ません。
proc { # このブロック内でブロック引数を参照したいけどどうすれば…? }.call(1, 2) { @1 + @2 }
なので、明示的にブロック引数を仮引数として定義したくなるんですが、先程書いたように仮引数が定義されていると Numbered parameters は使用する事が出来ません。
# ブロック引数を仮引数で定義すると Numbered parameters で使用できない… proc { |&block| # Compile error: ordinary parameter is defined block.call @1, @2 }.call(1, 2) { @1 + @2 }
では yield
を利用できないのか?と試してみたんですが、そもそも yield
はブロック内で使用する事が出来ずにエラーになります。
# そもそもブロック内で yield が使用できない proc { |a, b| # Compile error: Invalid yield (SyntaxError) yield(a, b) }.call(1, 2){ @1 + @2 }
残念ながらブロック引数を参照したい場合は Numbered parameters ではなくて仮引数を明示的に定義する必要があります。
# 明示的にブロック引数を受け取る場合は # Numbered parameters が使用できないので仮引数を定義する p proc { |a, b, &block| block.call a, b }.call(1, 2) { @1 + @2 } # => 3
不便といえば不便なんですが、実際にこのような使い方をすることは稀だと思うのでブロック引数を受け取りたい場合は素直に仮引数を定義して使うのがよさそうですね。
def
を用いたメソッド定義では使えない
当たり前ではあるんですが、def
を使用したメソッド定義内では Numbered parameters を使用する事は出来ません。
def plus # Compile error: numbered parameter outside block @1 + @2 end p plus 1, 2
ただし、define_method
でメソッドを定義する事で Numbered parameters を使用するようなメソッドを定義する事は出来ます。
# define_method に渡すブロック内で Numbered parameters が参照できる define_method(:plus) { @1 + @2 } p plus 1, 2 # => 3
lambda
で利用する
lambda
でも Numbered parameters を利用する事が出来ます。
ただ、ご存知の方もいると思いますが、Ruby の lambda
は proc
とは微妙に挙動が異っており『ブロックの引数の数を厳密にチェックする』という特性があります。
例えば proc
の場合は『仮引数よりも多い数の実引数』を渡すことが出来ます。
# proc は仮引数よりも多い実引数を渡せる proc { |a, b| }.call 1, 2, 3 # => OK # 通常のブロック構文では proc の挙動に準じている # なので次のように書いてもエラーにはならない (1..10).each_with_index { |a| "2つの実引数が渡されるが仮引数が 1つしかなくてもエラーにならない" }
しかし、lambda
の場合は『仮引数と異なる数の実引数を渡す』とエラーになります。
# lambda の場合は仮引数の数と異なる実引数を渡すとエラーになる lambda { |a, b| }.call 1 # => Error: wrong number of arguments (given 1, expected 2) (ArgumentError) lambda { |a, b| }.call 1, 2, 3 # => Error: wrong number of arguments (given 3, expected 2) (ArgumentError) lambda { |a, b| }.call 1, 2 # => OK
ちなみに
- メソッドの引数に渡すブロックは
proc
-> {}
で定義した場合はlambda
となります。
さて、では lambda
で Numbered parameters を使ってみるとどうなるでしょうか。
# proc は仮引数よりも多い実引数を渡せる proc { @1 + @2 }.call 1, 2, 3 # => OK # lambda の場合は仮引数の数と異なる実引数を渡すとエラーになる # lambda { @1 + @2 }.call 1 # => Error: wrong number of arguments (given 1, expected 2) (ArgumentError) # lambda { @1 + @2 }.call 1, 2, 3 # => Error: wrong number of arguments (given 3, expected 2) (ArgumentError) # lambda { @1 + @2 }.call 1, 2 # => OK
このように lambda
で仮引数を定義した時と同様の挙動になります。
これ、実はめちゃくちゃすごくって、『どの Numbered parameters が使われているのか』をコンパイル時にパースして計算しています。
なので Numbered parameters を使用したブロックに対しても Proc#parameters
の結果が反映されます。
# 仮引数を定義した場合 p proc { |a, b| a + b }.parameters # => [[:opt, :a], [:opt, :b]] # Numbered parameters を使用した場合 # 引数名は nil p proc { @1 + @2 }.parameters # => [[:opt, nil], [:opt, nil]]
これにより lambda
の仮引数と実引数があっているかどうかを実行時に判定する事が出来ます。
当たり前のように結果が返ってきているんですが凄まじくすごい。
eval
内では使用できない
eval
内では @1
は参照出来ません。
# Compile error: `eval': (eval):1: numbered parameter outside block (SyntaxError) proc { eval("@1 + @2") }.call 1, 2
eval
を使う方はまずいないと思うんですが注意する必要があります。
instance_variable_get
で取得できない
@1
は一見するとインスタンス変数のように見えるので #instance_variable_get
で値を取得したくなるんですが残念ながら使用する事は出来ません。
proc { @hoge = 42 # OK p instance_variable_get("@hoge") # Error: in `instance_variable_get': `@1' is not allowed as an instance variable name (NameError) p instance_variable_get("@1") }.call 1, 2
eval
でも参照出来ないので、現時点では動的に値を取得する事は難しそうですね。
Numbered parameters には代入できない
@1
に対して代入したりする事は出来ません。
proc { # Compile error: Can't assign to numbered parameter @1 @1 = 42 }
ネストしたブロックで使用する
Numbered parameters はブロックごとに独立しています。
なので次のようにネストしている場合もそれぞれのブロックごとに Numbered parameters の値は異なります。
p proc { @1 + proc { # 第三引数は渡ってこないので nil p @3 # => nil @1 + @2 }.call(@2, @3) }.call(1, 2, 3) # => 6
参照する引数の名前が @1
@2
@3
と同じなのでちょっと混乱しそうですね。
また、内ブロックから外のブロックの Numbered parameters も参照する事は出来ません。
defined?
に対して @1
を渡す
defined?
に対して @1
を渡すと "local-variable"
が返ってきます。
これはブロックに引数が渡されたかどうか関係なく常に "local-variable"
を返します。
proc { p defined? @1 # => "local-variable" p defined? @2 # => "local-variable" p @1 # => 1 p @2 # => nil }.call 1
これを試してみて『@1
って "local-variable"
なの!?』って思ってしまいました。
個人的には "numbered-parameters"
みたいな名前を返して差別化したほうがいいと思ったんですがどうなんでしょうか。ユースケースはわかりません。
ちなみに defined?
の戻り値が文字列だった事を最近知りました。
binding.irb
上で @1
は参照できない
binding.irb
で irb
を起動した時にそのコンテキストの @1
は参照できませんでした。
proc { @1 + @2 # ここで起動した irb 上で @1 は参照できない binding.irb }.call 1, 2
irb
上だと動的にコードを処理するので当たり前といえば当たり前なんですが、これはかなり不便だと思うのでなんとかしたいんですがなんとかなるのかなこれ…。
話は変わるんですが Ruby 2.7.0-preview1 の irb
がかなり進化しているので Ruby 2.7.0-preview1 を入れた方は一度 irb
を起動して試してみるといいですよ!
構文がハイライトされていたり複数行対応していたり補完してくれたりドキュメントを表示してくれたりとめちゃくちゃ楽しくなっています。
Numbered parameters が導入された経緯
ここからはおまけ的な内容になるんですが、Numbered parameters が導入されるまでにいろいろとあったので経緯などまとめてみたいと思います。
Numbered parameters の発端
Numbered parameters の提案自体は昔からあって、わたしが知ってる限りでは 2011年頃に jordi 氏が提案したのが発端になります。
この頃はまだ Numbered parameters という名称は出てきてませんね。
Issues を見る限りでは当時は特に議論されておらず、それから6年経った2017年の終わり頃から議論が活発化しています。
この時は『どの記号を使用するのかー』みたいな議論が中心ですね。
\1
や @1
、&1
などなどいろいろな記号の提案はされていましたが特に進展はしていませんでした。
Ruby 2.6 で関数合成をする機能が入った
話は変わって Ruby 2.6 では pabloh 氏の提案によって Proc#<<
と Proc#>>
が追加されました。
これは Proc
で関数合成を行うような機能で複数の Proc
を 1つにまとめる事が出来ます。
ppp = proc { |it| p it } twice = proc { |it| it + it } # twice → ppp の順番で proc を呼んでいる # ppp.call(twice.call(42)) と呼んでいるのと等価 (twice >> ppp).call 42 # => 84 # こんな感じで複数の proc をブロック引数として渡せる (1..3).each(&twice >> ppp) # => 2 # 4 # 6
この機能が追加された当時は『やっと Ruby に関数合成が入ったのか〜〜〜』と一部で盛り上がっていました。
関数合成をメソッドチェーンで利用したい
さて Ruby 2.6 では Proc#<<
と Proc#>>
が追加され、それを利用する事で関数合成を行えるようになりました。
ところで以下のようにブロック内で引数に対してメソッドチェーンする事はよくあると思います。
p %w{72 101 108 108 111}.map { |it| it.to_i.chr } # => ["H", "e", "l", "l", "o"]
map { |it| it.to_i }
だけであれば map(&:to_i)
と簡潔に書く事も出来るんですが、2つ以上のメソッドがチェーンされている時は &
渡しでは解決しません。
こういう時についつい map(&:to_i).map(&:chr)
みたいに書きたくなるんですが、これも map
を2回呼び出したりしていてなんか間抜けなコードですよね。
そこで考えたのが Proc
の関数合成を利用する方法です。
Symbol#to_proc
で Proc
化する事が出来るので Proc#>>
を使用する事で以下のように書く事が出来ます。
p %w{72 101 108 108 111}.map(&(:to_i.to_proc >> :chr.to_proc)) # => ["H", "e", "l", "l", "o"]
どうですか、仮引数を書く必要がなくなった代わりに to_proc
が2つ増えてしまいとても見づらいですね。
この書き方ではちょっとうるさいです…さてどうしたものか…。
Symbol
でも関数合成したいという提案をした
そもそも #to_proc
を呼び出すのが冗長なので、以下のように『Symbol
で直接関数合成できればいいのではなかろうか』と考えました。
# Symbol#>> を追加して直接関数合成してしまう p %w{72 101 108 108 111}.map(&(:to_i >> :chr)) # => ["H", "e", "l", "l", "o"]
そこで rdoc のメンテナで reline の作者である aycabta 氏といろいろと議論して『Symbol
でも関数合成をしたい!』という旨の Issues を立てました。
この Issues 自体は本題ではないので具体的な内容は省きますが(気になる方はわたしのまとめを読んでください)結論からいうとこの Issues は Reject されてしまいました。
その理由としてまつもとさんは以下のようにコメントしています
I feel the expression ary.map(&(:to_i << :chr)) is far less readable than ary.map{|x|x.to_i.chr}. And the latter is faster and can take arguments NOW e.g. ary.map{|x|x.to_i(16).chr}. Given these superiorities, this proposal does not sound attractive. Matz. p.s. And this can lead to the default block parameter like it.
可読性に対しては確かに『:to_i >> :chr
って書き方は記号的すぎるよねー』とは思っていたのでこれが原因で Reject されるのは想定の範囲内でした。
しかし、
And this can lead to the default block parameter like it.
と言われるのは全くの予想外でした。
ここで Numbered parameters の話につながります。
Numbered parameters の誕生
早速、件の #4475 を次の開発者会議の議題として取り上げてもらいました。
そして最終的には jeremyevans0 氏が提案していた @1
記法が採用されました。やったー。
そしてそれはすぐに nobu 氏によって実装されましたaccept されてから10時間ぐらいで実装しててやばい。
ちなみにその時のコミットメッセージ に Numbered parameters
と書かれていますね。
わたし自身も前々から #4475 はほしいなーほしいなーと思っていたんですが、まさか Symbol
の関数合成の話からここにつながるとは思っていませんでした。
そしてそれをつなげるまつもとさんすさまじくすごい。
そして炎上へ
Numbered parameters が採用されてめでたしめでたし…で終わればよかったんですが、取り込まれてから Numbered parameters に対する議論が爆発します。
元の Issues はすでに Closed されているんですが、Numbered parameteres の議論は以下の Issues で続いています。
コメントが100件以上書き込まれており、全部は追えていないんですが、いろんな方がいろんな意見を述べていますね。
わたし自身は『@1
って記号はどうなのかなー』と思いつつ Numbered parameters 自体はめちゃくちゃいい機能だと思っていたので、こんなに反対意見があるのはちょっと意外でした。
いまは…
Numbered parameters が実装された当初は 1日に何件ものコメントが書き込まれていましたが RubyKaigi 2019 を区切りにある程度は落ち着きました。
中には Twitter のアンケートで調査している方などもいますね。
そろそろ鎮火したのかなー?と、この記事を書きながら #15723
を読み返していたら jeremyevans0 氏 が@
で単一の引数をキャプチャする機能の提案していました。
proc{@}.call(1) # => 1 proc{@}.call([1, 2]) # => [1, 2] proc{@}.call(1, 2) # => 1 proc{@.abs}.call(-1) # => 1 proc{Math.log(@)}.call(Math::E) # => 1.0 @ # SyntaxError: implicit parameter outside block
この提案を Eregon 氏が今月の開発者会議の議題に追加していたので、そこで改めて議論される事になります。
個人的には @
は @1
以上に情報量が少なく、それこそインスタンス変数の記法と混同してしまうので「うーん…」という気持ちなんですがどうなるんでしょうねえ。
って、書いていると mame 氏がまた新しく Issues を立てていました。
こちらは @
の代わりに it
を使用する、という提案みたいですね。
it
を使った場合、互換性はどうなるのか?というと
- 他に
it
という変数が参照できればit
という変数を参照する it
というメソッドが定義されていて『メソッド呼び出し』っぽければit
というメソッドを呼び出す- そうでなければブロックの第一引数を
it
で参照する
っていう挙動になるみたいです。
個人的にはコンテキストで it
の意味が変わるのは絶対に嫌なんですが( super
と super()
で意味が違うのもとても嫌い)、どうなんでしょうね…うーん…。
本当に Ruby 2.7.0 がリリースされるまでにはどうなってしまうのか。
Numbered parameters は読みづらい?
さてさて、話はちょっと変わって Numbered parameters に否定的な意見として『可読性に問題がある』と書いている方がいます。
例えば #15723 の Issues では sos4nt 氏がこのようなコードを例に上げています。
h = Hash.new { |hash, key| hash[key] = "Go Fish: #{key}" } # vs h = Hash.new { @1[@2] = "Go Fish: #{@2}" }
なるほど、確かに後者は仮引数の名前に意味を持たないのでちょっと可読性に難があるように感じますね。
Numbered parameters はいろいろと議論されていますが、わたしが思う問題点としては、
- 複数の仮引数を Numbered parameters で参照できるべきか
- Numbered parameters で使用する記号をどうするか
の 2つだと考えています。
そしてこの2つは別々に議論すべきだと思います。
1.
に関しては確かに @1[@2] = "Go Fish: #{@2}"
のようなコードをみると @1
のように単一の引数のみ参照できるように宣言してしまった方が乱用を避ける事はできそうですね。
なので、まずは第一引数のみ参照できるようにして後から拡張できるような仕組みにしてしまうのもいいのかな、と個人的には思っています。
2.
に関しては使用できる記号に制限があるという前提があるので @1
みたいな記号にするしかないですかねえ、it
みたいな名前にしてしまうと既存のコードとの互換性が壊れる可能性があるのでちょっと難しいと思います。
ちなみにまつもとさんは上記のようなコードに対して次のようにコメントしています。
Yes, I admit { @1[@2] = "Go Fish: #{@2}" } is cryptic. But {@1 * @2} is not. So use numbered parameters with care (just like other features in Ruby). The possibility to make code cryptic itself should not be the reason to withdraw a feature. The problem is introducing it or this could break existing code. That is the problem. Numbered parameters are a compromise to simplify block expression without breaking compatibility. FYI, I am not fully satisfied with the beauty of the code with numbered parameters, so I call it a compromise. Matz.
仮に複数の Numbered parameters を参照して難読化するようなケースであれば仮引数を明示的に書けばいいんじゃないかなあ、と個人的には思っていますが、このあたりは実際に使ってみないとわからない部分ではあるのでちょっと判断するのが難しそうですね。
ちなみにまつもとさんは I call it a compromise.
とも言っています。
Numbered parameters で解決する未来
では逆に Numbered parameters を便利に使えるコードとはなんでしょか。
例えば map(&:upcase)
は Numbered parameters で記述するとわかりやすく書く事が出来ます。
p %w(homu mami mado).map(&:upcase) # or p %w(homu mami mado).map { @1.upcase }
map(&:upcase)
は慣れていると当たり前のように使っているんですが、かなり記号的な書き方ですね。
コードとしてみれば map { @1.upcase }
という書き方のほうが素直でわかりやすいと思います。
また、先ほど紹介した Proc
を使った関数合成もわかりやすく書く事が出来ます。
ppp = proc { |it| p it } twice = proc { |it| it + it } (1..3).each(&twice >> ppp) # or (1..3).each { ppp.call twice.call @1 }
call
を呼び出している部分がやや冗長ですが、読みやすいといえば読みやすいですね。
更に Ruby に精通している人は method
をブロックに渡す事もありますね。
def twice(a) a + a end p (1...3).map(&method(:twice)) # or p (1...3).map { twice @1 }
これも圧倒的に後者のほうがわかりやすいですね。
また、Ruby ではしばしば [1,2,16].map { |n| n.to_s(16) }
を [1,2,16].map(&:to_s.(16))
のように書きたいという Issues が立ちます。
- Feature #12115: Add Symbol#call to allow to_proc shorthand with arguments - Ruby trunk - Ruby Issue Tracking System
- Feature #15301: Symbol#call, returning method bound with arguments - Ruby trunk - Ruby Issue Tracking System
- Feature #15302: Proc#with and Proc#by, for partial function application and currying - Ruby trunk - Ruby Issue Tracking System
このような要求も Numbered parameters を使用する事でわかりやすく記述する事が出来ます。
[1 ,2, 16].map { |n| n.to_s(16) } # or [1, 2, 16].map(&:to_s.(16)) # or [1 ,2, 16].map { @1.to_s(16) }
今までは Numbered parameters のデメリットばかり見てきましたが Numbered parameters を使う事でいろいろな問題が解決するように見えてきませんか?
わたしは Numbered parameters を使ったコードの方が簡潔で素直なコードなので読みやすいと思います。
あとなんと言っても書いてて気持ちがいいです。
余談ですが、以前 Refinements で foo(&:hoge)
が反映されないバグを修正した事があるんですが、これも foo { @1.hoge }
とするだけで解決しますね。
まとめ
と、いう感じで Numbered parameters について簡単にまとめてみました。
ナンパラ自体はそれほど難しい機能ではないので誰でも簡単に使用する事は出来ると思います。
ただ『単一の引数をキャプチャする』のか『複数の引数をキャプチャする』のかはまだ議論する余地があるかなーとは思っています。
その上で @
以外の記号を使うのか使わないのかを議論すべきかな、と。
中には否定的な方もいると思うんですが、結局のところ『ナンパラが存在する事によって解決する問題』っていうのがとても大きいと思うんですよね。
実際にナンパラが trunk に実装されてから Ruby を書いていて『あれ、これナンパラで書けば簡潔に書けるし解決するじゃん』って思う事は何回かありました。
あとは単純にナンパラを使ったブロック構文がめちゃくちゃ気持ちよく書けるっていうのも重要だと思います。
もうわざわざ |it|
みたいに仮引数を書きたくない…。
さて、今回 Ruby 2.7.0-preview1 がリリースされた事によってより多くの方がナンパラを試してみてそこでまた様々な意見が出ると思います。
Ruby 2.7.0 がリリースされるまでに現状の仕様からどう変わるのか(もしくは変わらないのか、削除されてしまうのか)わかりませんが、今後どうなるのかは注目していきたいと思います。
たしか当時 Matz は「今はみんな反対してるけど、何年かしたらみんな僕に感謝するようになるよ」みたいなことをおっしゃっていたように記憶している。そしておおむねその通りになっているような気がする。さすがだな…
— nagachika (@nagachika) 2019年5月30日
いい話。
Ruby について議論したいという方は!
今回書いたナンパラについて議論してみたい!という方がいれば毎月 Ruby コミッタの方が開催している Ruby Hack Challenge に参加するのがおすすめです。
ここでは直接 Ruby コミッタの方に話を聞く事も出来ますし、議論することも出来ます。
ナンパラに限らず、Ruby 2.7.0 の新機能であるパターンマッチや irb
、既存の言語機能についてなどなど Ruby に関して気になる事があれば意見交換してみるのもいいと思います。
わたしも先日参加したんですが、ナンパラについて他の人の意見を聞いたりしてきました。
そんなに堅苦しい内容のイベントではないので気になる方は気軽に参加してみてはいかがでしょうか。