エンジニアの澤田です。
この連載では、社内のRuby (on Rails)コードで気になった箇所の問題点やそこから発展して関連事項を議論しています。
連載6回目から受け渡しのパターンマッチングを考察しています。 パターンマッチングの文法的な要素のうち、オブジェクトを渡す側について連載6回目でまとめ、定数や変数で受ける側の要素について連載7回目でまとめました。
連載8回目の今回は、これらに関してPRで見かけたコードの紹介とそれについての議論をします。よく見かける問題のないユースケースにも触れます。1
【バックナンバー】
- マネーフォワード社内PRに見られるRubyの書き方について - (1) 配列の生成
- マネーフォワード社内PRに見られるRubyの書き方について - (2) ハッシュの生成
- マネーフォワード社内PRに見られるRubyの書き方について - (3) 文字列の生成や検証
- マネーフォワード社内PRに見られるRubyの書き方について - (4) 真理値
- マネーフォワード社内PRに見られるRubyの書き方について - (5) 文の環境
- マネーフォワード社内PRに見られるRubyの書き方について - (6) 受け渡しのパターンマッチング 1
- マネーフォワード社内PRに見られるRubyの書き方について – (7) 受け渡しのパターンマッチング 2
配列に入った引数を渡す
配列に不定数の引数が入っていて、それを別のメソッドに渡したいときには 連載6回目の*foo
の形を使います。
array = ["foo", "bar", "baz"] ["foo"].push(*array) # => ["foo", "foo", "bar", "baz"]
array = [["s", "t", "u"], ["x", "y", "z"]] ["r", "u", "b", "y"].difference(*array) # => ["r", "b"]
key_sequence # => [:a, :b, :c, :d, :e] {a: {b: {c: {d: {e: :foo}}}}}.dig(*key_sequence) # => :foo
不定数の要素の入った配列をレシーバーと引数に分ける
メソッドString#prepend
, String#concat
, Array#concat
, Array#union
, Hash#merge
, Hash#merge!
, Array#product
, Array#zip
は、不定数の引数を取り、レシーバーや各引数の間に(順序以外に)本質的な役割の違いがありません(前者3つについては、筆者がお願いして不定数の引数を取れるようにしてもらいました2)。これらのレシーバーと引数に当たる概念を別別に変数に持つよりは、1つの配列の中にまとめて持っている方が自然なことがあります(1つのオブジェクトに次次と破壊的な変更を施していきたい場合は、この限りではありません)。そうすると、メソッドを適用するときに、その配列をどのようにしてレシーバーと引数に分割するかが問題になります。
これらのメソッドのうち、String#prepend
, String#concat
, Array#concat
, Array#union
, Hash#merge
, Hash#merge!
については、その演算に関しての単位元をレシーバーにすることによって、配列をレシーバーと引数に分割しないで済みます。
strings = ["a", "b", "cd"] arrays = [["a", "b"], ["c"], ["d", "a"]] hashes = [{"a" => 1}, {"b" => 2, "c" => 3}, {"d" => 4, "e" => 5}] "".prepend(*strings) # => "abcd" "".concat(*strings) # => "abcd" [].concat(*arrays) # => ["a", "b", "c", "d", "a"] [].union(*arrays) # => ["a", "b", "c", "d"] {}.merge(*hashes) # => {"a"=>1, "b"=>2, "c"=>3, "d"=>4, "e"=>5} {}.merge!(*hashes) # => {"a"=>1, "b"=>2, "c"=>3, "d"=>4, "e"=>5}
これは分割するときと比べて、きれいなコードのスタイルで、高速です。また、この方法は、元の配列内の1つ目のオブジェクトに破壊的な影響を与えたくない場合にも適しています。しかし、この方法はArray#product
, Array#zip
には使えません。
素朴にレシーバーと引数を分離するには、多重代入と*foo
型変数を使います。
arrays = [["a", "b"], ["c"], ["d", "a"]] first, *rest = arrays first.product(*rest) # => [["a", "c", "d"], ["a", "c", "a"], ["b", "c", "d"], ["b", "c", "a"]] first.zip(*rest) # => [["a", "c", "d"], ["b", nil, "a"]]
この方法は高速です。しかし変数付与の一手間がかかり、コードがきれいではありません。
ほぼ同等かわずかに遅い実行速度で配列の分割を避けるには、Symbol#to_proc
とProc#call
(もしくはその.()
表記)を使うことも出来ます。
arrays = [["a", "b"], ["c"], ["d", "a"]] :product.to_proc.(*arrays) # => [["a", "c", "d"], ["a", "c", "a"], ["b", "c", "d"], ["b", "c", "a"]] :zip.to_proc.(*arrays) # => [["a", "c", "d"], ["b", nil, "a"]]
このように、Symbol#to_proc
によって作られるProc
オブジェクトは、ここで扱っているタイプのメソッドとよく適合します。call
メソッドが呼ばれた際に、1つ目のの引数をレシーバー、残りの引数を引数として元のシンボルで表されるメソッドに渡すのです。次のコード:
:foo.to_proc.call(bar_1, bar_2, ..., bar_n)
は
bar_1.foo(bar_2, ..., bar_n)
と同じです。
group_by
, chunk
などのキーを捨てる
連載7回目で、不要なオブジェクトを受けるのに_
が使えると述べました。これは規約であり、使わなくてもプログラムは正しく動きますが、他のプログラマや将来の自分がコードを読むときに余計なことに頭を使うのを防げます。この使いどころの一つは、Enumerable#group_by
, Enumerable#chunk
などに連なるmap
などのコードブロックのキーに対応する部分です。group_by
, chunk
などのメソッドでは、返り値のキー(や内側の配列の左側の要素)は、対応する配列の各要素の関数になっているので、情報が冗長で不要であることがあります。例えば、配列array
:
array = ["a", "a", "b", "a", "a", "b", "b", "b", "a"]
の中で同一要素の連続する長さを得るには、chunk
を使って、まず、同一要素ごとの塊を得ることが出来ます:
array.chunk(&:itself).to_a # => [["a", ["a", "a"]], ["b", ["b"]], ["a", ["a", "a"]], ["b", ["b", "b", "b"]], ["a", ["a"]]]
しかし、対を表している配列の左側の要素は右側の配列の要素と同じで、不要です。これに続くmap
では、_
を使ってこういう値を捨てます。
array.chunk(&:itself).map{|_, array| array.length} # => [2, 1, 2, 3, 1]
委譲に**
を使う
メソッドが受け取った仮引数を別のメソッドに丸ごと渡す(委譲する)ときがあります。そのようなとき、*foo
の形で位置引数とキーワード引数を共に引き受けているコードがありました。
[見かけた書き方]
def foo(*args, &block) ... bar(*args, &block) ... end
筆者は、上のようなコードを見たとき、キーワード引数を拾えていないのではないかという間違った指摘をしてしまい、そのコードを書いた人に、これで良いのだということを教えてもらったことがありました。Ruby 3になるまでは、キーワード引数はハッシュと区別されないので、上のコードで*args
に回収され、うまくいくのです。
ただし、**kwargs
のような仮引数を入れたほうが、キーワード引数がある場合も考慮に入っているという意図が明確に表されるので、良いとは思います。
Ruby 3ではキーワード引数とハッシュは区別されるようになり、*args
でキーワード引数を回収できなくなります。Ruby 3でキーワード引数を含む委譲を行うには、上のコードではうまくいかず、次のようにする必要があります。
def foo(*args, **kwargs, &block) ... bar(*args, **kwargs, &block) ... end
変数の交換
変数x
, y
がそれぞれある値を持っているとします。
x # => "foo" y # => "bar"
これらの参照内容を交換しようとして、プログラミングに慣れていない人がたまに次のような間違い:
[見かけた書き方]
x = y # => "bar" y = x # => "bar"
を犯す場合があります。これだとx
, y
が共に元のy
の値になってしまいます。社内ではこのようなコードを見たことはありません。しかし、次のようにしているコードがありました。
[見かけた書き方]
tmp = x # => "foo" x = y # => "bar" y = tmp # => "foo"
2行目でx
を上書きするときに元のx
の内容をどこかに残しておくために、1行目でx
の内容をtmp
に退避させています。これは後に3行目のy
の上書きで使っています。これは、他のプログラミング言語でよくある方法です。
しかし、Rubyではこのようにする必要はありません。 多重代入を使って次のように出来ます。
x, y = y, x
これを見て、左辺と右辺の値を左から順に対応させて上の間違った方のコードのように展開されるのではないか、それ故に上手くいかないのではないか、と心配になる人もいるかも知れません。しかし、そのようには展開されません。これは 連載6回目で扱ったように、代入式の右辺の配列リテラルの[]
を省略しています。そして代入式ではまず右辺の単一のオブジェクトが評価されます。ここでは[y, x]
が["bar", "foo"]
と評価されます。その後に、連載7回目で扱ったように、この配列が2つの変数x
, y
に対して分配されます。従って、この方法で問題ないのです。
配列の要素を個別に付与する
ある属性とその値など、対になっている情報が配列で得られたときに、配列全体を変数に付与してから、その中身を取り出すという操作をしているコードがありました。
[見かけた書き方]
key_and_value = "foo=bar".split("=") # => ["foo", "bar"] key = key_and_value[0] # => "foo" value = key_and_value[1] # => "bar"
目的の要素にたどり着くために、途中の配列の付与は必要ありません。多重代入を使って直接いけます。
key, value = "foo=bar".split("=") # => ["foo", "bar"] key # => "foo" value # => "bar"
いっぺんに得られる値
ある整数を別の整数で割ったときの商と余りの両方が欲しいときがあります。これらを以下のように個別に求めているコードがありました。
[見かけた書き方]
quotient = 47 / 5 # => 9 remainder = 47 % 5 # => 2
商も余りも同じわり算という演算に関連したものであり、このように個別に計算しているのはどうも無駄だという気がしてきませんか。Rubyにはこれらを同時に配列に入れて与えるメソッドdivmod
があります。これと多重代入を使うと、次のように商と余りをいっぺんかつ個別に付与できます。
quotient, remainder = 47.divmod(5) # => [9, 2] quotient # => 9 remainder # => 2
ハッシュの復数の値を個別に付与する
ハッシュから特定の値を取り出して個別の変数に付与するコードをよく見かけます。
[見かけた書き方]
hash # => {foo: "foo", bar: "bar", baz: "baz", qux: "qux"} a = hash[:bar] b = hash[:qux] c = hash[:foo]
これはHash#values_at
と多重代入の組み合わせで簡潔に書けます。
a, b, c = hash.values_at(:bar, :qux, :foo)
配列の内部へのアクセス
配列の中に複雑に埋め込まれた要素にアクセスするために、配列に[]
メソッドを繰り返し適用してアクセスしているコードがありました。
[見かけた書き方]
[[:a, [:b, :c]], ...].each do |array| a = array[0] b = array[1][0] c = array[1][1] ... end
このようなときは、()
による再帰的なパターンマッチングを使えます。
[[:a, [:b, :c]], ...].each do |a, (b, c)| ... end
inject
によるオブジェクトの加工
連載1回目や連載2回目で扱ったように、単純なオブジェクトをイテレーターで連続的に加工していて、そのオブジェクトの内部構造を参照する場合は、再帰的パターンマッチングの一つの使い所です。
次のコードは、うるう日、うるう秒を考えないものとして、1億秒が何年何日何時間何分何秒かを求めます。
[60, 60, 24, 365].inject([100_000_000]) do |(r, *a), d| r.divmod(d) + a end # => [3, 62, 9, 46, 40]
3年62日9時間46分40秒という結果が得られました。目的の配列は(r, *a)
に相当する部分です。この中で、左端の要素r
はそのイテレーションで演算を施したい対象で、残りの*a
はもう計算が終わって変更する必要のない部分です。計算した商と余りを配列a
の左端に足していっています。
each_with_object
によるオブジェクトの加工
同様に、単純なオブジェクトをイテレーターで連続的に加工していて、イテレーションする要素の内部構造を参照する場合もあります。
次の例は、文字ごとの重み(投票数など)を表すハッシュが与えられて、文字キーを小文字にして表記ゆれを吸収した上で重みの合計を計算するコードです。
{"a" => 1, "A" => 2, "B" => 2, "c" => 3, "C" => 1}.each_with_object(Hash.new(0)) do |(k, v), h| h[k.downcase] += v end # => {"a"=>3, "b"=>2, "c"=>4}
加工されるオブジェクトは、値0
をデフォルトとして返すハッシュHash.new(0)
として生成され、ブロック中では引数h
としてイテレーションの間中引っ張り回されます。一方のイテレーションされる要素は、ハッシュのキーと値の対を表す配列です。このキーと値にk
, v
として個別にアクセスするために、再帰的パターンマッチングを用いて(k, v)
のようにしています。
複数のオブジェクトを伴うイテレーション
複数のオブジェクトを連れ回すときは、each_with_object
やwith_object
を重ねます。このとき、各段で一まとまりの配列になるので、再帰的パターンマッチングを使います。
次の例は、配列に2回以上含まれる要素を、2回目に登場する順に抽出するコードです。
["a", "b", "c", "d", "b", "c", "a", "c"].each_with_object([]).with_object([]) do |(elem, first_occurrences), second_occurrences| if !first_occurrences.include?(elem) then first_occurrences << elem elsif !second_occurrences.include?(elem) then second_occurrences << elem end end # => ["b", "c", "a"]
1回登場した要素を記録する配列first_occurrences
と2回目に登場した要素を登場順に記録する配列second_occurrences
の2つを連れ回しています。欲しい結果はsecond_occurrences
なので、これがパターンマッチングの外側になるようにしています。
あるいは、効率化のために、次のようにハッシュを使うことも出来ます。
["a", "b", "c", "d", "b", "c", "a", "c"].each_with_object({}).with_object({}) do |(elem, first_occurrences), second_occurrences| if !first_occurrences[elem] then first_occurrences[elem] = true elsif !second_occurrences[elem] then second_occurrences[elem] = true end end .keys # => ["b", "c", "a"]
イテレーションする要素の内部構造を判定する
array # => [1, 2, 5, 6, 7, 8, 10]
の連続する番号をまとめ、"1, 2, 5–8, 10"
という文字列に変型することを考えます。
array.chunk_while{|x, y| x.next == y}.to_a # => [[1, 2], [5, 6, 7, 8], [10]]
となることを使い、このto_a
の代わりにmap
, join
を続けて次のように出来ます。
array .chunk_while{|x, y| x.next == y} .map do |first, middle = nil, *, last| if middle then "#{first}–#{last}" elsif last then "#{first}, #{last}" else first end end .join(", ") # => "1, 2, 5–8, 10"
map
のブロック引数の箇所で(1) 多重代入、(2) 連載7回目で述べたA配列を参照して要素がないときのデフォルトのnil
, (3) 随意引数のデフォルト指定, (4) *
による残りの要素の回収、の組み合わせを使っています。
chunk_while
の返り値の配列に埋め込まれた配列は、少なくとも1 つ要素を持ちます。配列の1つ目の要素がfirst
に付与されます。要素が2つ以上あれば、last
に最後の要素が付与され、なければnil
が付与されます。3つ以上要素があれば、middle
に2つ目の要素が付与され、なければnil
が付与されます。4つ以上要素があれば、残りは*
に対応し、捨てられます。
コードブロック中のメソッド呼び出しのレシーバーや引数の省略
上の節で述べたSymbol#to_proc
の性質と 連載6回目で述べた&
によるProc
オブジェクトのコードブロックへの変換を使って、
{|arg_1, arg_2, ..., arg_n| arg_1.foo(arg_2, ..., arg_n)}
という形のコードブロックを省略して
&:foo
と書くことが出来ます。
最もよく現れる形がn = 1、つまり、引数がない場合です。
{|arg_1| arg_1.foo}
例えばこんな感じに使われます。
["a", "b", "c"].map(&:upcase) # => ["A", "B", "C"]
n = 2の場合はinject
メソッドに現れます。
{|bar_1, bar_2| bar_1.foo(bar_2)}
次のような使い方があります。
(1..6).inject(&:*) # => 720
しかし、inject
は第1引数がシンボルならそれがメソッド名を表すので、次のようにも書けます。
(1..6).inject(:*) # => 720
また、以前は、次のように、埋め込まれた配列やハッシュのキーをたどっていくという使い所がありました。
hash # => {a: {b: {c: {d: {e: :foo}}}}} key_sequence # => [:a, :b, :c, :d, :e] key_sequence.inject(hash, &:[]) # => :foo key_sequence.inject(hash, :[]) # => :foo
しかし、前の節で示したように、Array#dig
, Hash#dig
メソッドの出現により、このようにする必要はなくなりました。また、この方法は、埋め込まれた途中のキーが存在する場合にしか使えないという制約もあります。
コードブロック中の関数型メソッド呼び出しの引数の省略
Method#to_proc
の性質と&
を使って、
{|arg_1, arg_2, ..., arg_n| bar.foo(arg_1, arg_2, ..., arg_n)}
という形のメソッドの呼び出しを含むコードブロックを省略して
&bar.method(:foo)
と書くことが出来ます。
特にbar
がKernel
の場合、これを省略して次のように書けます。
[2, [5], 1].map(&method(:Array)) # => [[2], [5], [1]]
明示的なbar
が必要な場合には次のような用法があります。
[2, 3, 5].map(&Math.method(:sqrt)) # => [1.4142135623730951, 1.7320508075688772, 2.23606797749979]
bar
が明示的な場合は、Ruby 2.7から導入される.:foo
記法を用いて次のようにも書けます。
[2, 3, 5].map(&Math.:sqrt) # => [1.4142135623730951, 1.7320508075688772, 2.23606797749979]
ハッシュを使った写像
次のような感じのコードを見かけました。
[見かけた書き方]
array # => ["a", "b", "c", "d", "b", "c", "a", "c"] array.map do |e| case e when "a" then "foo" when "b" then "bar" when "c" then "baz" when "d" then "qux" end end # => ["foo", "bar", "baz", "qux", "bar", "baz", "foo", "baz"]
このコードでは、対応関係が制御構造としてハードコーディングされています。この関係が滅多に変わることのない固定されたものならばこれで良いですが、そうではなく、今後変化していく可能性のあるものの場合、その度に制御構造を書き換えなくてはいけません。そういう状況では、対応関係(データ)と制御構造(ロジック)を分離するほうが望ましいと考えられます。
case
構文と守備範囲の近いオブジェクトにハッシュがあります。ハッシュを使って対応関係をデータとして分離するとすると、次のようなコードが考えられます。
hash = {"a" => "foo", "b" => "bar", "c" => "baz", "d" => "qux"} array.map{|e| hash[e]} # => ["foo", "bar", "baz", "qux", "bar", "baz", "foo", "baz"]
Hash#to_proc
と&
を使えば、全くブロックを書く必要がありません。
array.map(&hash)
# => ["foo", "bar", "baz", "qux", "bar", "baz", "foo", "baz"]
まとめ
連載6回目、連載7回目と概念的な記事が続きましたが、今回ようやく実践的な内容に移ることが出来ました。Rubyのパターンマッチングは工夫次第で非常に多くのことが出来ます。
最後に
マネーフォワードでは、Rubyと寝食を共にしたいエンジニアを募集しています。 ご応募お待ちしています。
【採用サイト】 ■マネーフォワード採用サイト ■Wantedly
【マネーフォワードのプロダクト】 ■お金の見える化サービス 『マネーフォワード ME』 iPhone,iPad Android
■ビジネス向けバックオフィス向け業務効率化ソリューション 『マネーフォワード クラウド』
■金融商品の比較・申し込みサイト 『Money Forward Mall』
■本業に集中できる新しいオンライン融資サービス 『Money Forward BizAccel』
- この記事はRuby開発者の一人である弊社の卜部昌平氏に目を通してもらいました。記事に間違いがあれば、それは筆者の責に帰するものです。↩
- https://bugs.ruby-lang.org/issues/12333↩