はじめに
この記事は「Ruby Advent Calendar 2021」の22日目の記事です。
Ruby2.7から入ったパターンマッチ、皆さん使ってますか?
便利ですよね。
自分はここ数年仕事では JavaScript(TypeScript) を触るのがメインだったんですが、よく使われる構文として分割代入があります。
分割代入もパターンマッチも使い勝手が似ている部分が多いので分割代入に慣れているとパターンマッチが非常に手に馴染みます。(個人的所感)
ただ分割代入でできてパターンマッチでできないことがあります。
デフォルト値(既定値)の設定です。
この記事ではパターンマッチでデフォルト値を設定したい場合はどうするかを考えていきます。
分割代入の場合
まず JavaScript の分割代入の場合ですが、以下のようにしてデフォルト値の設定ができます。
const obj = { a: 1 }
const { a, b = 2 } = obj // obj の b という key が undefined の場合にデフォルト値が設定される
foo(a, b)
obj の特定の key に値が入っていない == obj の特定の key の値が undefined
なのでこれでデフォルト値が設定されます。
一方 Ruby のパターンマッチの場合、in節の中でデフォルト値を設定できないのでマッチ前かマッチ後のどちらかでデフォルト値を設定してやる必要があります。
パターンマッチの場合
方法1: マッチ後にデフォルト値を設定する
1行in ならマッチしなかった変数は nil で初期化されるのでマッチ後に代入すればいけそうです。
hash = { a: 1 }
hash in { a:, b: }
b ||= 2
foo(a, b)
ただし以下のように最初にマッチしなかった key 以降はパターンの検証が行われないため、残念ながらこの方法では思い通りにいきません。
hash = { a: 1, c: 3 }
hash in { a:, b:, c: } # => b にマッチしないため c は nil
b ||= 2
foo(a, b, c) # 第3引数で 3 を渡しているつもり
方法2: マッチ前にデフォルト値を設定する
今度は先にデフォルト値を設定してみます。
hash = { a: 1 }
default = { b: 2 }
default.merge(hash) => { a:, b: }
foo(a, b)
この方法の場合は key が必ず存在するので 1行in ではなく右代入でいけます。
また、方法1でうまくいかなかったケースも対応できます。
hash = { a: 1, c: 3 }
default = { b: 2 }
default.merge(hash) => { a:, b:, c: }
foo(a, b, c)
ただし merge に渡す Hash でデフォルト値を設定したい key に nil が入っている場合 nil で上書きされてしまいます。
hash = { a: 1, b: nil }
default = { b: 2 }
default.merge(hash) => { a:, b: }
foo(a, b) # => b は nil
このため、merge に渡す Hash から nil を排除する必要があります。
hash = { a: 1, b: nil }
default = { b: 2 }
default.merge(hash.reject { |_, v| v.nil? }) => { a:, b: }
foo(a, b)
ここまでやってようやくやりたいことが実現できました
願望
はい、できたことはできたんですが分割代入のデフォルト値の設定と比較するとどうしても面倒に感じてしまうところがあります。
また、ネストした構造になってくると更に手間になってきます
hash = { a: { b: { c: nil }, d: 2 }, e: { f: nil } }
default = { a: { b: { c: 1 }, e: { f: 3 } } }
assigned = hash.reject do |_, v|
# nil を除外するためのロジック
end
default.merge(assigned) => { a: { b: { c: }, d: }, e: { f: } }
foo(c, d, f)
なので、パターンマッチ(or 右代入)にデフォルト値を設定できる機能がほしいなぁと思う今日このごろです
最後に
何か他にもっとスマートなやり方があれば教えてください〜