Money Forward Developers Blog

株式会社マネーフォワード公式開発者向けブログです。技術や開発手法、イベント登壇などを発信します。サービスに関するご質問は、各サービス窓口までご連絡ください。

20230215130734

マネーフォワード社内PRに見られるRubyの書き方について – (7) 受け渡しのパターンマッチング 2

エンジニアの澤田です。

この連載では、社内のRuby (on Rails)コードで気になった箇所の問題点やそこから発展して関連事項を議論しています。前回から、1つのテーマで長くなりそうなときは、複数の記事に短く分割する方針にしています(とは言いつつも、分割した記事が長くなり、それをさらに分割した今回の記事もまた長くなってしまいました)。前回の「マネーフォワード社内PRに見られるRubyの書き方について - (6) 受け渡しのパターンマッチング 1」から受け渡しのパターンマッチングを考察しています。

今回は続きです。最近Ruby開発者たちが活発に議論を重ねている番号付きブロック引数やcase構文のパターンマッチングについても言及します。1


【バックナンバー】


オブジェクトを渡して定数や変数で受けるときのパターンマッチングの問題のうち、渡す側の文法的な要素について連載6回目でまとめました。今回の7回目はそれを定数や変数で受ける側の要素についてまとめます。8回目は6, 7回目に関係するPRで見かけたコードの紹介とそれについての議論をします。

定数や変数で受けるときのパターンマッチングの要素

連載6回目で扱ったオブジェクトを渡す環境は、連載5回目で扱った文の環境①–⑧の一部でした。今回扱う定数や変数でオブジェクトを受ける環境は、文の環境とは別の環境です。

オブジェクトを受ける環境を「複数のオブジェクトを受けることが出来るか」と、出来るとすれば、「引数の数をチェックするか」という基準で分類します。分類した環境に❶–❸の名前を付けて下に列挙します。コード例の...が該当します。コード例は

❶ 複数のオブジェクトを受けることが出来ない環境

a. 代入式の左辺

代入式の右辺は、ちょうど1つのオブジェクトしか受け取れません(ただし、連載5回目で述べたように、右辺の配列リテラルの[]が省略されて、一見、複数のオブジェクトが並んでいるかのように見えることもあります。2)。その意味で、それを受け取る代入式の左辺は、複数のオブジェクトを受けられません(後の節で述べるように、左辺に複数の定数や変数が現れる場合にも、直接に複数のオブジェクトを受けているのではなく、受け取った単一のオブジェクトを分配していると考えます)。

... = foo

❷ 複数のオブジェクトを受けることが出来、引数の数をチェックしない環境

a. ラムダオブジェクトの生成やメソッド定義に関わらないコードブロックの仮引数

foo{|...| bar}

❸ 複数のオブジェクトを受けることが出来、引数の数をチェックする環境

a. ラムダリテラルの仮引数

->(...){foo}

b. ラムダオブジェクトの生成に関わるコードブロックの仮引数

lambda{|...| bar}

c. (特異)メソッド定義式の仮引数

def bar(...)
  foo
end

d. メソッド定義に関わるコードブロックの仮引数

define_method(:bar){|...| foo}

以下、定数や変数でオブジェクトを受けるパターンマッチングの文法的な要素についてまとめます。

1. 単一の定数や変数

単一の定数や変数がオブジェクトを受ける最も簡単な形です。後の節で登場する**foo型の変数や*foo型の変数でない定数や変数のことを「単純定数」、「単純変数」と呼ぶことにします。❶には定数やあらゆる種類の変数が書けます。❷, ❸にはローカル変数以外を書くと統語エラーになります(引数とローカル変数に区別がないことは連載5回目で述べました)。次の節で登場するキーワード引数と区別するときには「位置引数」と言います。❶–❸で使わないオブジェクトは_または_に名前を続けた_fooの形の変数によって受けることが出来ます。それでも、__fooは参照することが可能です。

❶では、代入式の右辺(連載5回目の⑤c)から単一のオブジェクトを受け取ります。

❷では、付随するメソッドからそのメソッドに固有の任意の数のオブジェクトの組み合わせを渡され、そのうちの1つ目を付与されます。余ったオブジェクトは捨てられます。

instance_exec("a", "b"){|foo| foo} # => "a"

❸a, bでは、callメソッドから、❸c, dでは、定義されたメソッドの呼び出しから、それぞれ任意の数の実引数の組み合わせ(連載5回目の⑤d, e)を渡され、渡された引数がちょうど1つならそれを付与され、そうでなければ引数エラーが発生します。

->(foo){foo}.call("a") # => "a"
->(foo){foo}.call("a", "b") # >> ArgumentError: wrong number of arguments (given 2, expected 1)
def bar(foo); foo end; bar("a") # => "a"
def bar(foo); foo end; bar("a", "b") # >> ArgumentError: wrong number of arguments (given 2, expected 1)

2. キーワード引数

❷, ❸で、中心的な役割を果たす引数や頻繁に使われる引数が位置引数に適しているのに対して、従属的な役割を果たす引数や同類のものがいくつもある引数は、位置引数にするとどの引数が何を表しているのか分かりづらくなるため、値と共に引数名を明示すると便利で、また、そうすることによってその引数が省略しやすくなります。これが「キーワード引数」です。:foo => valueまたはfoo: valueの形で与え、foo:の形で受けます。これは、シンボルをキーとするハッシュリテラルの{}を省略したものと同じ形をしています。3

instance_exec(foo: 1){|foo:| foo} # => 1

ただし、Ruby 3になるまでは、連載6回目で述べたハッシュリテラルの{}の補完規則により、上に示した形でキーワード引数を与えてもハッシュを渡していることになり、次のコードと等価です。

instance_exec({foo: 1}){|foo:| foo} # => 1

従ってまた、キーワード引数はハッシュのシンボルキーに対する値を受け取ります。ハッシュとキーワード引数の区別の曖昧さもしくは複雑さをなくすために、Ruby 3からはこのようなハッシュのシンボルキーに対する値をキーワード引数として解釈することはなくなる予定です。

3. 随意引数のデフォルト

❷, ❸で、位置引数やキーワード引数は随意的にすることが出来、そのときに「デフォルト」を以下のように示します。

foo = bar
foo: bar

4. 全オブジェクトの配列化とハッシュの取り出し

本稿の範囲内で繰り返される概念があるので、それを導入しておきます。

複数の定数や変数にいくつかのオブジェクトを分配して付与するとき、渡された全オブジェクトはまず配列に変換されます。4 これには2通りの仕方があります。5 本稿では、以下のようにA配列, B配列と名前を付けることにします。

A配列6

受け渡しに「表記ゆれ」を許したいときに使われます。

  • 与えられたオブジェクトがちょうど1個で、それに対してメソッドto_aryが定義されている場合は、得られる配列7の複製を得る。

      ["a"] → ["a"]
      ["a", "b"] → ["a", "b"]
    
  • それ以外の場合は、与えられたオブジェクト全てを要素とする配列を得る。

      (オブジェクトが0個) → []
      "a" → ["a"]
      "a", "b" → ["a", "b"]
      ["a"], ["b"] → [["a"], ["b"]]
    

定数や変数にA配列のn番目の要素が割り当てられる際に、対応する要素がなければ、nilが付与されます。また、A配列の要素がどの定数や変数にも付与されずに余ることがあります。

B配列

受け渡しを忠実に行いたいときに使われます。

  • 与えられたオブジェクト全てを要素とする配列を得る。

      (オブジェクトが0個) → []
      "a" → ["a"]
      ["a"] → [["a"]]
      ["a", "b"] → [["a", "b"]]
      "a", "b" → ["a", "b"]
      ["a"], ["b"] → [["a"], ["b"]]
    

変数にB配列の要素が割り当てられる際に、変数とB配列の要素の数が合わなければ、引数エラーが発生します。

A配列では、["a"]"a"と同じ["a"]に変換され、また、引数の数が合わなくても調整される、というように違いが吸収される、つまり表記ゆれを許すことと引き換えに情報が一部喪失します。対して、B配列では["a"][["a"]]と埋め込まれ、引数の数が合わなければ引数エラーが発生する、というように情報が維持されるという違いがあります。

また、Ruby 3になるまでは、キーワード引数にオブジェクトを付与するとき、配列から以下のようにしてハッシュを取り出す操作があります。得られたハッシュを元の配列から「取り出したハッシュ」、残りを元の配列から「ハッシュを取り出した残り」と呼ぶことにします。

取り出したハッシュ

  • 与えられた配列が空でなく、最後の要素に対してメソッドto_hashが定義されている場合は、得られるハッシュ8からシンボルキーと対応する値だけを抽出したハッシュを(作って)取り出し、元のハッシュが空になったなら、配列から除く(シンボル以外のキーが残っているならそのままにする)。

      ["a", {a: 1, "b" => 2}] → # 取り出したハッシュ: {:a=>1}, ハッシュを取り出した残り: ["a", {"b"=>2}]
      ["a", {"b" => 2}] # 取り出したハッシュ: {},  ハッシュを取り出した残り: ["a", {"b"=>2}]
      ["a", {a: 1}] → # 取り出したハッシュ: {:a=>1}, ハッシュを取り出した残り: ["a"]
      [{}, {a: 1}] → # 取り出したハッシュ: {:a=>1}, ハッシュを取り出した残り: [{}]
      [{}] → # 取り出したハッシュ: {}, ハッシュを取り出した残り: []
    

    ただし、Ruby 2.7になるまでは、得られるハッシュがシンボルキーとシンボルでないキーを共に含むなら、引数エラーを生じさせる。

      ["a", {a: 1, "b" => 2}] → # ArgumentError: non-symbol key in keyword arguments: "b"
    
  • それ以外の場合は、空ハッシュを(作って)取り出し、配列はそのままとする。

      [] → # 取り出したハッシュ: {}, ハッシュを取り出した残り: []
      ["a", "b"] → # 取り出したハッシュ: {}, ハッシュを取り出した残り: ["a", "b"]
    

5. 多重代入

❶–❸に複数の定数や変数をコンマで区切って並べ、同時に値の付与を受けることが出来ます。キーワード引数は位置引数より後で、キーワード引数同士の順序は任意です。9

foo, Bar = ...
baz{|foo, bar| ...}
->(foo, bar){...}
def baz(foo, bar); ... end

❶, ❷では、n番目の定数や変数には、全オブジェクトのA配列のn番目の要素が付与されます。

  • 全オブジェクトが1つでto_aryが未定義の場合

      foo, Bar = "a"; foo # => "a"
      foo, Bar = "a"; Bar # => nil
    
      instance_exec("a"){|foo, bar| foo} # => "a"
      instance_exec("a"){|foo, bar| bar} # => nil
    
  • 全オブジェクトが1つでto_aryが配列を返す場合

      foo, Bar = ["a", "b", "c"]; foo # => "a"
      foo, Bar = ["a", "b", "c"]; Bar # => "b"
    
      instance_exec(["a", "b"]){|foo, bar| foo} # => "a"
      instance_exec(["a", "b"]){|foo, bar| bar} # => "b"
      instance_exec(["a", "b"]){|foo, bar, baz| baz} # => nil
      instance_exec(["a", "b", "c"]){|foo, bar| foo} # => "a"
    
  • 全オブジェクトが0個または複数の場合

      instance_exec("a", "b"){|foo, bar| foo} # => "a"
      instance_exec("a", "b"){|foo, bar| bar} # => "b"
      instance_exec("a", "b"){|foo, bar, baz| baz} # => nil
      instance_exec("a", "b", "c"){|foo, bar| foo} # => "a"
    

一方、❸では、n番目の引数には、全オブジェクトのB配列のn番目の要素が付与されます。

  • 数が合う場合

      ->(foo, bar){foo}.call("a", "b") # => "a"
      ->(foo, bar){bar}.call("a", "b") # => "b"
    
      def baz(foo, bar); foo end; baz("a", "b") # => "a"
      def baz(foo, bar); bar end; baz("a", "b") # => "b"
    
  • 数が合わない場合

      ->(foo, bar){foo}.call("a") # >> ArgumentError: wrong number of arguments (given 1, expected 2)
      ->(foo, bar){foo}.call("a", "b", "c") # >> ArgumentError: wrong number of arguments (given 3, expected 2)
    
      def baz(foo, bar); foo end; baz("a") # >> ArgumentError: wrong number of arguments (given 1, expected 2)
      def baz(foo, bar); foo end; baz("a", "b", "c") # >> ArgumentError: wrong number of arguments (given 3, expected 2)
    

6. **foo型の引数

**を引数に前置した**fooの形を❷, ❸に置き、fooにハッシュを付与出来ます。また、参照する必要がない不定数個のキーワードと値の対を**だけで後に引数名を続けずに受けることが出来ます。

Ruby 3からは、分解されたハッシュのキーと値の対やキーワード引数と値の対の並びを格納したハッシュがfooに付与されるようになる予定です。

Ruby 3になるまでは、「2. キーワード引数」節でも述べたように、連載6回目**により、分解されたハッシュ引数や直接にキーワード引数として書いたものは実際にはハッシュとして解釈され、純粋なキーワード引数は存在しません。代わりに、次の手順でfooに付与されます。

この節では、受ける環境に**fooしかない場合だけを考えます。

❷では、fooには、全オブジェクトのA配列から取り出したハッシュが付与されます。

  • A配列が空の場合

      instance_exec(){|**foo| foo} # => {}
      instance_exec([]){|**foo| foo} # => {}
    
  • A配列の最後の要素に対してto_hashが未定義の場合

      instance_exec("a"){|**foo| foo} # => {}
      instance_exec(["a"]){|**foo| foo} # => {}
    
  • A配列の最後の要素に対してto_hashがハッシュを返す場合

      instance_exec("a", {a: 1, "b" => 2}){|**foo| foo} # => {:a=>1}
      instance_exec(["a", {a: 1, "b" => 2}]){|**foo| foo} # => {:a=>1}
      instance_exec({"b" => 2}){|**foo| foo} # => {}
    

一方、❸では、fooには、全オブジェクトのB配列から取り出したハッシュが付与されます。

  • B配列が空の場合

      ->(**foo){foo}.call() # => {}
    
      def bar(**foo); foo end; bar() # => {}
    
  • B配列の最後の要素に対してto_hashが未定義の場合

      ->(**foo){foo}.call("a") # >> ArgumentError: wrong number of arguments (given 1, expected 0)
    
      def bar(**foo); foo end; bar("a") # >> ArgumentError: wrong number of arguments (given 1, expected 0)
    
  • B配列の最後の要素に対してto_hashがハッシュを返す場合

      ->(**foo){foo}.call({a: 1}) # => {:a=>1}
      ->(**foo){foo}.call("a", {a: 1}) # >> ArgumentError: wrong number of arguments (given 1, expected 0)
    
      def bar(**foo); foo end; bar({a: 1}) # => {:a=>1}
      def bar(**foo); foo end; bar("a", {a: 1}) # >> ArgumentError: wrong number of arguments (given 1, expected 0)
    

7. *foo型の定数や変数

*を定数や変数に前置した*fooの形を❶–❸に置き、0個以上のオブジェクトを配列に格納出来ます。また、参照する必要がない不定数個のオブジェクトを*だけで後に変数名を続けずに受けることが出来ます。この節では、受ける環境に*fooしかない場合だけを考えます。

❶では、fooには、全オブジェクトのA配列が付与されます。

*foo = "a"; foo # => ["a"]
*foo = ["a"]; foo # => ["a"]
*foo = ["a", "b"]; foo # => ["a", "b"]

一方、❷, ❸では、fooには、全オブジェクトのB配列が付与されます。

instance_exec("a"){|*foo| foo} # => ["a"]
instance_exec(["a"]){|*foo| foo} # => [["a"]]
instance_exec("a", "b"){|*foo| foo} # => ["a", "b"]
instance_exec(["a", "b"]){|*foo| foo} # => [["a", "b"]]
->(*foo){foo}.call("a") # => ["a"]
->(*foo){foo}.call(["a"]) # => [["a"]]
->(*foo){foo}.call("a", "b") # => ["a", "b"]
->(*foo){foo}.call(["a", "b"]) # => [["a", "b"]]
def bar(*foo); foo end; bar("a") # => ["a"]
def bar(*foo); foo end; bar(["a"]) # => [["a"]]
def bar(*foo); foo end; bar("a", "b") # => ["a", "b"]
def bar(*foo); foo end; bar(["a", "b"]) # => [["a", "b"]]

❷は、他の節で扱う規則ではA配列を使うのに、この節の場合でだけB配列を使うことに注意して下さい。上のコード例や次の節のコード例と重複しますが、以下で**barの有無だけで*fooに付与されるオブジェクトの埋め込みの度合いが変わるのは、一貫性がない仕様のように筆者には思えます。

instance_exec(["a"]){|*foo| foo} # => [["a"]]
instance_exec(["a"]){|*foo, **bar| foo} # => ["a"]

8. 単純定数や単純変数, **bar, *bazの組み合わせ

単純定数や単純変数(の多重代入), **bar, *bazを組み合わせる場合を考えます。10

Ruby 3からは、ハッシュとキーワード引数の区別がなされ、**foo型の引数はキーワード引数(だけ)を受けるようになる予定です。そうすると、**foo型の引数は単純定数や単純変数、*foo型の引数とはオブジェクトの分配に際して相互作用しなくなります。下に掲げるRuby 3までについての記述から、**foo型の引数が関わる段階を除外して下さい。

Ruby 3までは、次の順に段階が進みます。

全オブジェクトのA配列もしくはB配列
↓ 単純定数や単純変数への付与、それらの要素の除外
残った配列
**foo型の引数があれば、ハッシュの取り出し、**foo型の引数への付与
残った配列
*foo型の定数や変数への付与

受け取りの環境に記述する順番では*foo型の変数が**foo型の変数よりも先でなければならないのに、付与される順番では**foo型の変数が*foo型の変数よりも先であることに注意して下さい。

❶, ❷では、全オブジェクトのA配列から出発します。

  • A配列の唯一の要素をめぐってfoo, **bar, *bazで競合するとき、その要素がfooに付与され、**bar*bazが空になることから、fooへの付与が**bar*bazに先行することが分かります。

      foo, *baz = "a"; foo # => "a"
      foo, *baz = "a"; baz # => []
      foo, *baz = ["a"]; foo # => "a"
      foo, *baz = ["a"]; baz # => []
    
      instance_exec({a: 1}){|foo, *baz, **bar| foo} # => {:a=>1}
      instance_exec({a: 1}){|foo, *baz, **bar| bar} # => {}
      instance_exec({a: 1}){|foo, *baz, **bar| baz} # => []
      instance_exec([{a: 1}]){|foo, *baz, **bar| foo} # => {:a=>1}
      instance_exec([{a: 1}]){|foo, *baz, **bar| bar} # => {}
      instance_exec([{a: 1}]){|foo, *baz, **bar| baz} # => []
    
  • A配列の唯一の要素をめぐって**bar, *bazで競合するとき、その要素が**barに付与され、*bazが空になることから、**barへの付与が*bazに先行することが分かります。

      instance_exec({a: 1}){|*baz, **bar| bar} # => {:a=>1}
      instance_exec({a: 1}){|*baz, **bar| baz} # => []
      instance_exec([{a: 1}]){|*baz, **bar| bar} # => {:a=>1}
      instance_exec([{a: 1}]){|*baz, **bar| baz} # => []
    
  • A配列の唯一の要素が**barに付与することの出来ないものであるとき、*baz**barの「おこぼれ」を受け取ります。

      instance_exec("a"){|*baz, **bar| bar} # => {}
      instance_exec("a"){|*baz, **bar| baz} # => ["a"]
      instance_exec(["a"]){|*baz, **bar| bar} # => {}
      instance_exec(["a"]){|*baz, **bar| baz} # => ["a"]
    
  • Ruby 2.7からは、**barによるハッシュの取り出しで残されたシンボルキーでない部分は、ハッシュとして残り、*bazに回収されることが分かります。

      instance_exec({a: 1, "b" => 2}){|*baz, **bar| bar} # => {:a=>1}
      instance_exec({a: 1, "b" => 2}){|*baz, **bar| baz} # => [{"b"=>2}]
    

一方、❸では、全オブジェクトのB配列から出発します。

  • fooへの付与は**bar*bazに先行します。

      ->(foo, *baz, **bar){foo}.call({a: 1}) # => {:a=>1}
      ->(foo, *baz, **bar){bar}.call({a: 1}) # => {}
      ->(foo, *baz, **bar){baz}.call({a: 1}) # => []
    
      def qux(foo, *baz, **bar); foo end; qux({a: 1}) # => {:a=>1}
      def qux(foo, *baz, **bar); bar end; qux({a: 1}) # => {}
      def qux(foo, *baz, **bar); baz end; qux({a: 1}) # => []
    
  • ちょうど1つの配列を渡したとき、それが配列のままfooに付与されていることから、B配列が使われていることが分かります。

      ->(foo, *baz, **bar){foo}.call([{a: 1}]) # => [{:a=>1}]
    
      def qux(foo, *baz, **bar); foo end; qux([{a: 1}]) # => [{:a=>1}]
    
  • B配列が使われるときは、*bazによる引数の数の調整がない限り、引数の数が合わなければ引数エラーが生じます。

      ->(foo, **bar){}.call("a", "b") # >> ArgumentError: wrong number of arguments (given 2, expected 1)
    
      def qux(foo, **bar); end; qux("a", "b") # >> ArgumentError: wrong number of arguments (given 2, expected 1)
    
  • **barへの付与は*bazに先行します。

      ->(*baz, **bar){bar}.call({a: 1}) # => {:a=>1}
      ->(*baz, **bar){baz}.call({a: 1}) # => []
    
      def qux(*baz, **bar); bar end; qux({a: 1}) # => {:a=>1}
      def qux(*baz, **bar); baz end; qux({a: 1}) # => []
    
  • *baz**barのおこぼれを受け取ります。

      ->(*baz, **bar){bar}.call("a") # => {}
      ->(*baz, **bar){baz}.call("a") # => ["a"]
    
      def qux(*baz, **bar); bar end; qux("a") # => {}
      def qux(*baz, **bar); baz end; qux("a") # => ["a"]
    
  • Ruby 2.7からは、*baz**barによるハッシュの取り出しの残りを回収します。

      ->(*baz, **bar){bar}.call({a: 1, "b" => 2}) # => {:a=>1}
      ->(*baz, **bar){baz}.call({a: 1, "b" => 2}) # => [{"b"=>2}]
    
      def qux(*baz, **bar); bar end; qux({a: 1, "b" => 2}) # => {:a=>1}
      def qux(*baz, **bar); baz end; qux({a: 1, "b" => 2}) # => [{"b"=>2}]
    

9. ()によるパターンマッチングの再帰

定数や変数の並びを()で囲んだものを❶–❸の定数や変数の位置に置くと、その部分は1つのまとまりとして値が割り当てられ、次にその内部について再帰的にパターンマッチングが行われます。^11 11

以下の再帰的パターンマッチングの段階を追ってみます。

(foo, bar), baz = [["a"], "b"]; foo # => "a"
(foo, bar), baz = [["a"], "b"]; bar # => nil
instance_exec(["a"], "b"){|(foo, bar), baz| foo} # => "a"
instance_exec(["a"], "b"){|(foo, bar), baz| bar} # => nil
instance_exec([["a"], "b"]){|(foo, bar), baz| foo} # => "a"
instance_exec([["a"], "b"]){|(foo, bar), baz| bar} # => nil
->((foo, bar), baz){foo}.call(["a"], "b") # => "a"
->((foo, bar), baz){bar}.call(["a"], "b") # => nil
def qux((foo, bar), baz); foo end; qux(["a"], "b") # => "a"
def qux((foo, bar), baz); bar end; qux(["a"], "b") # => nil

1段階目では、❶, ❷ではA配列、❸ではB配列を経由して、(foo, bar)["a"]baz"b"が割り当られます。前者に注目すると、再帰段階でfoo, bar["a"]が分配されます。再帰段階では、割り当てられる全オブジェクトは常にちょうど1個になります。ここで、もし全オブジェクト["a"]のA配列["a"]が使われるとすると、fooには"a"が付与されるはずで、もしB配列[["a"]]が使われるとすると、fooには["a"]が付与されるはずです。現実には、上の❶–❸の全てのコード例でfoo"a"が付与されていることから、再帰段階では、受け取りの環境に関わらす、割り当てられた(ちょうど1つの)全オブジェクトのA配列が使われることが分かります。

10. &fooによるコードブロックのProcオブジェクトへの変換

&を引数に前置した&fooの形を❷, ❸に置き、メソッド呼び出しに付随するコードブロックをProcオブジェクトに変換したものをfooに付与出来ます。12

proc{|&foo| foo.parameters}.call{|x| p x} # => [[:opt, :x]]
->(&foo){foo.parameters}.call{|x| p x} # => [[:opt, :x]]
def bar(&foo); foo.parameters end; bar{|x| p x} # => [[:opt, :x]]

11. 引数を受ける環境の省略

❷, ❸b, dで、頻出する特定のパターンマッチングの型に現れる引数に対して、引数を受ける環境を省略出来るようにする試みが、最近、Ruby開発者達によってなされています。

今のところ、Ruby 2.7から採用される予定の記法は、@1, @2, ...によってブロックに与えられたA配列またはB配列の1番目の要素, 2番目の要素, ...を参照するというものです。「番号付きブロック引数(numbered block parameter)」と呼ばれています。13

A配列を使う❷では、

[["a", "b"], ["c", "d", "e"]].map{|_, foo, *| foo} # => ["b", "d"]

を省略して

[["a", "b"], ["c", "d", "e"]].map{@2} # => ["b", "d"]

のように書けるようになります。A配列を使う場合には引数全体の数の照合が行われないので、番号付きブロック引数を使わない方の書き方で*を省略しても同じ結果になります。

❷で番号付きブロック引数を使った書き方が可能なのは、実はこのことと関係があります。どういうことかと言うと、B配列を使う❸では、

->(_, _, x, *){x}.call("foo", "bar", "baz", "qux") # => "baz"

を省略して

->{@3}.call("foo", "bar", "baz", "qux") # >> ArgumentError: wrong number of arguments (given 4, expected 3)

と書こうとすると、引数エラーが出てしまいます。これはB配列を使う付与が引数の数の照合を行うからであり、番号付きブロック引数を使わない方の書き方で*をなくしても全く同じ引数エラーが出ます。

B配列を使った引数の照合を行う付与では、使いたい引数のことだけでなく、全引数の数を考えなくてはならず、数が合わなければ*を入れて調整するなりする必要があるのです。番号付きブロック引数を使う書き方でも、4引数あることに基づいて、使わない@4に言及するなりの方策があります。14 そうなってくると、果たしてこれが記述を簡素化していることになるのだろうかという疑問がわいてこないでもありません。

一方、この記法に異を唱える人達もいます。15 主な代案は、「1. 単一の定数や変数」節で扱ったパターンマッチングで付与されるオブジェクトを参照出来れば十分だというものです。記法の候補として@, it, _などが挙がっています。仮に@ を使うとすると、

[2, 3, 5].map{|i| "%.3f" % Math.sqrt(i)} # => ["1.414", "1.732", "2.236"]

を省略して

[2, 3, 5].map{"%.3f" % Math.sqrt(@)} # => ["1.414", "1.732", "2.236"]

のように書けるようになります。

しばらく紛糾が続きそうです。

12. case構文中のinによるパターンマッチング

ここまでオブジェクトの受け渡しのパターンマッチングについて述べてきましたが、本節ではオブジェクトを比較する際のパターンマッチングに触れます。

「1. 単一の定数や変数」節で述べた受け渡しのパターンマッチングに相当する比較の構文が、==, <, >, ===, equal?などのメソッドを使うものです。

foo === bar

これより複雑な比較をするものとして、case構文中のwhenがあります。whenの引数(複数ならそのいずれか)を===の左辺(レシーバー)に、caseの引数を右辺(引数)として評価するものです。

case foo
when object1  then ...
when object2, object3 then ...
else ...
end

Ruby 2.7では、これに加えて、case構文の中でinが使えるようになる予定です。16

case foo
in pattern1 then ...
in pattern2 then ...
else ...
end

inの引数がパターンマッチングの形式を表し、その内部の要素がcaseの引数の内部の要素と===で比較されるというものです。17 これは、変数に値を付与する能力もあり、比較のパターンマッチングであると同時に受け取りのパターンマッチングでもあります。

case [1, :a, "b"]
in [Numeric, sym, "b"] then sym
else
end # => :a

多重代入の構文と同様に、パターンマッチングの要素の区切りには、配列内のオブジェクトの区切りと同じコンマ,を使います。それに伴い、代替の区切りにはwhenのときのコンマではなくパイプ|を使います。**foo型の変数や*foo型の変数も受け取りのパターンマッチングと同じように使えます。inのパターンマッチングでは同じ名前の変数を複数使うことが可能で、一時的にある箇所で変数への値の付与を止めるには^fooの形を使います。

「4. 全オブジェクトの配列化とハッシュの取り出し」節では、与えられたオブジェクトを引数に対応させるためにto_ary, to_hashを用いて配列化したりハッシュを取り出したりする仕組みを述べましたが、inの引数に対してこれに相当することを行うのが、deconstructメソッドとdeconstruct_keysメソッドです。

inによるパターンマッチングは新しく、これから改良され、仕様が変更されていく可能性があります。この構文によりRubyの表現力がまた一段と向上するでしょう。楽しみです。

まとめ

Rubyの受け渡しのパターンマッチングは、一見すると万能で簡便に見え、でも深く追求すると複雑にも見えてきます。筆者は試行錯誤しながらようやく本稿の内容にたどり着くことが出来ました。このような体系を設計し実装するのは、それを解明するよりも一段と困難だったはずであり、それをしたまつもとゆきひろさんを始めとするRuby開発者達に筆者は敬意を持つものです。

最後に

マネーフォワードでは、Rubyと寝食を共にしたいエンジニアを募集しています。 ご応募お待ちしています。

【採用サイト】 ■マネーフォワード採用サイトWantedly

【マネーフォワードのプロダクト】 ■お金の見える化サービス 『マネーフォワード ME』 iPhone,iPad Android

ビジネス向けバックオフィス向け業務効率化ソリューション 『マネーフォワード クラウド』

おつり貯金アプリ 『しらたま』

おトクが飛び出すクーポンサービス 『tock pop』

金融商品の比較・申し込みサイト 『Money Forward Mall』

くらしの経済メディア 『MONEY PLUS』

本業に集中できる新しいオンライン融資サービス 『Money Forward BizAccel』


  1. この記事はRuby開発者の一人である弊社の卜部昌平氏に目を通してもらいました。また、弊社の黒田翔太氏から助言を受けました。記事に間違いがあれば、それは筆者の責に帰するものです。
  2. Ruby 1.9の時代のRuby開発者による記述 http://www.a-k-r.org/d/2007-08.html#a2007_08_17 では、右辺に複数のオブジェクトが並んでいるとしていますが、このような代入式の返り値が配列になることから、本連載では、単一の配列であるという立場を取ります。
  3. キーワード引数を位置引数よりも前の位置で使うと統語エラーになります。

    instance_exec(){|foo:, bar|} # >> SyntaxError: unexpected local variable or method
    
  4. 現実にRubyの実装内部で配列オブジェクトを作っているかどうかは問わず、説明のための便宜だと思って下さい。
  5. このような違いが生まれるに至った経緯については、Ruby開発者による http://www.a-k-r.org/d/2007-08.html#a2007_08_16 を参照。
  6. Rubyに実在するメソッドを引き合いに出すと、Kernel#Arrayが似ている概念ですが、Kernel#Arrayto_aryを試みて定義されていなかったらto_aを試みるのに対して、A配列ではto_aryのみを試みる点が異なります。
  7. to_aryは配列を返すように定義されていなければなりません。そうでないと、タイプエラーが生じます。
  8. to_hashはハッシュを返すように定義されていなければなりません。そうでないと、タイプエラーが生じます。
  9. ❷, ❸では、__foo型の引数を除き、1回のパターンマッチングで同じ引数名を複数回使うと統語エラーになります。

    bar{|foo, foo|} # >> SyntaxError : duplicated argument name
    
    ->(foo, foo){} # >>  SyntaxError : duplicated argument name
    
    def bar(foo, foo); end # >>  SyntaxError : duplicated argument name
    

    __fooによる不要なオブジェクトのマーキングの場合はそのような制約はありません。この場合、どの位置の__fooへの付与が優先的か定まっていないので、それらを参照するべきではありません。

    ❶にもこのような制約はありません。そのような場合、後の付与が優先します。

    foo, foo = ["a", "b"]; foo # => "b"
    
  10. **foo型の引数は単純引数, キーワード引数, *foo型の引数よりも前の位置で使ったり、1回のパターンマッチングで複数個使ったりすると統語エラーになります。*foo型の定数や変数は1回のパターンマッチングで複数個使うと統語エラーになります。
  11. 再帰的段階ではキーワード引数が使えません。

    instance_exec(){|foo, (bar, baz:)|} # >> SyntaxError: unexpected tLABEL
    
    ->(foo, (bar, baz:)){} # >> SyntaxError: unexpected tLABEL
    
    def qux(foo, (bar, baz:)); end # >> SyntaxError: unexpected tLABEL
    

    これはハッシュとキーワード引数が区別されるRuby 3のもとでは納得の行く仕様です。キーワード引数は配列の一部ではあり得ず、パターンマッチングの1段階目にしか現れ得ないからです。Ruby 3以前でも、キーワード引数は位置引数よりも前の位置で使えないという制約があるので、この制約は妥当だと考えられます。

  12. &foo型の引数は単純引数, キーワード引数, *foo型の引数, **foo型の引数よりも前の位置で使ったり、1回のパターンマッチングで複数個使ったりすると統語エラーになります。

    instance_exec(){|&foo, bar|} # >> SyntaxError: unexpected ',', expecting '|'
    instance_exec(){|&foo, &bar|} # >> SyntaxError: unexpected ',', expecting '|'
    
  13. https://youtu.be/WZu-WVzbEOA?t=2295 参照
  14. 番号付きブロック引数 の目的は引数を受ける環境を省略することなので、これらを共に用いると、統語エラーになります。

    ->(*){@3}.call("foo", "bar", "baz", "qux") # >> SyntaxError: (eval):2: ordinary parameter is defined
    
  15. https://bugs.ruby-lang.org/issues/15723 参照
  16. https://bugs.ruby-lang.org/issues/14912, https://youtu.be/WZu-WVzbEOA?t=2358 参照
  17. いずれの条件にもマッチしない場合にはエラーが出ます。