レンズを使って、入れ子のレコードを更新します(イミュータブルなデータを扱うプログラミング言語向け)。
入れ子になったイミュータブルなレコードのフィールドを更新するのは、なかなか面倒です。 こうした場合のコードは、構造が深くなるほど煩雑になります。 たとえば、Personの中にAddressがあり、そのAddressにStreetがあり、さらにそのStreetにNumberがあるとします。このNumberを更新するには、新しいNumberを持つStreetを新しく作り、次にその新しいStreetを持つAddressを新しく作り、最後にその新しいAddressを持つPersonを新しく作る必要があります。 もう混乱してきましたか?
この問題を解決する方法の1つが、レンズを使うことです。
レンズを使って、レコードにアクセスする関数をいくつか実装してみましょう。 テストスイートは、レンズをまったく使わないやり方にも対応しているので、別のアプローチを試すこともできます。
Rocでは、レコードの更新構文がほとんどの用途をうまくカバーしてくれるので、レンズはあまり使われません。たとえば、次のような入れ子のレコードを考えてみましょう。
bob = { hair: { color: Brown, length: Long }, mouth: Smiling, height: 6.0 }
では、Bobに散髪してあげましょう。
new_bob = { ..bob, hair: { ..bob.hair, length: Short } }
レンズは必要ありません! とはいえ、Rocではレンズは珍しいものの、HaskellやScalaのような他の言語ではよく使われるので、Rocでどのように実装できるのか気になるかもしれません。また、パラメーター付きの型や、関数を含むレコードといった、Rocの少し高度な機能を練習するのにも良い方法です。実際、Rocでレンズを表す方法の1つは次のとおりです。
Lens(whole, part) := {
get : whole -> part,
set : whole, part -> whole,
}
これはパラメーター付きの型です。wholeとpartには任意の型を入れることができます。wholeは通常レコードで、partは任意のフィールドの型です。getフィールドには、与えられたwholeの特定のpartを返す関数が入り、setフィールドには、与えられたwholeのコピーを返す関数が入ります。ただし、特定のpartが与えられたpartに置き換えられたコピーです。
たとえば、ある人の髪を取得したり設定したりするレンズは次のとおりです。
hair_lens = {
get: |person| person.hair,
set: |person, hair| { ..person, hair },
}
このレンズを使うと、次のようにしてBobの髪に注目できます。
bob_hair = bob |> hair_lens.get
また、次のようにして髪を変えることもできます。
new_hair = { ..bob.hair, length: Short }
new_bob = bob |> (hair_lens.set)(new_hair)
注意:hair_lens.setを囲む括弧は必須です。これがないと、Rocはhair_lensの型の中からsetという名前の関数を探してしまいます。
レンズは、それらを合成し始めると特に便利になります。深く入れ子になった部分を取得したり設定したりできるようになるのです。これは、次のような関数を実装することで実現できます。
compose : Lens(outer, inner), Lens(inner, part) -> Lens(outer, part)
この関数を実装すると、次のようにして髪の長さのレンズを作れます。
length_lens = {
get: |hair| hair.length,
set: |hair, length| { ..hair, length },
}
hair_length_lens = compose(hair_lens, length_lens)
これで、Bobをもう一度散髪できます。
set_hair_length = hair_length_lens.set
new_bob = bob |> set_hair_length(Short)
入れ子になった部分に任意の変換を適用できる関数を実装したいと思うかもしれません。
over : Lens(whole, part), whole, (part -> part) -> whole
実装できたら、overを次のように使えます。
cut_hair = |current_length| match current_length {
Long => Short
Short => Bald
Bald => Bald
}
new_bob = hair_length_lens |> over(bob, cut_hair)
この演習では、次の入れ子のレコードのさまざまな入れ子部分をコードが変更できるかどうかをテストが確認します。
test_person : Person
test_person = {
name: { forenames: "Jane Joanna", surname: "Doe" },
birth: {
born_at: { street: "Longway", house_number: 1024, place: "Springfield", country: "United States" },
born_on: { year: 1984, month: 4, day: 12 },
},
address: { street: "Shortlane", house_number: 2, place: "Fallmeadow", country: "Canada" },
}
まずはRocのレコード更新構文を使ってテストに合格し、そのあとでレンズを使う方法を試してみることをおすすめします。