実は同じネタを @miyamo_madoka さんが書いているのですが、同じ記事があっても書きたかった そして自分の意見も書きたかったので書きます。
ネストしたRecordを更新したい
Elmではアプリケーションの状態(Model)はシングルステートで扱うため、以下のようにネストしたRecordを定義することはよくあります。他の言語でもEntityなどを定義する際には同じような状況になるかと思います。
type alias Model =
{ character : Character }
type alias Character =
{ name : String, point : Point }
type alias Point =
{ x : Int, y : Int }
ElmのRecordのSetter(指定した値を更新した新しいレコードを返す)構文は、以下のようになります。
> record = { x = 1, y = 2 }
{ x = 1, y = 2 }
-- : { x : number, y : number1 }
> { record | x = 2 }
{ x = 2, y = 2 }
-- : { x : number, y : number1 }
> record
{ x = 1, y = 2 }
-- : { x : number, y : number1 }
それでは本題のネストしたRecordの更新をしようとしてみましょう。
{ model | character.point.x = 2, character.point.y = 2 }
・・・と、こんな構文はないとシンタックスエラーを吐かれてしまいます。
どう更新するか
それでは、どのようにすれば更新することができたのか? 素晴らしい解決策を提示します。新しいPointを生成します。あとは中段であるcharacterをmodelから取り出し、characterのpointをnewPointで更新します。最後にnewCharactorでmodelを更新します。
updateLowPoint : Int -> Int -> Model -> Model
updateLowPoint x y model =
let
newPoint =
{ x = x, y = y }
character =
model.character
newCharacter =
{ character | point = newPoint }
in
{ model | character = newCharacter }
-- updateLowPoint 2 2 currentModel
え・・・? 当たり前に誰でも思いつく方法ですって・・・? その通りです。Elmの優れている点は誰もが読み書きできるコードに落ち着くと言う点です。
同じような更新処理を共通化する
単に新しいPointに書き換えるだけであれば、先程のように書けば済みます。仮にxをインクリメントするような処理を書くとしましょう。
incrementX : Model -> Model
incrementX model =
let
point =
model.character.point
incrementedXPoint =
{ point | x = point.x + 1 }
character =
model.character
newCharacter =
{ character | point = incrementedXPoint }
in
{ model | character = newCharacter }
同じようなコードが出てきました!こうなればやることは簡単です。重複コードを減らしてみましょう。
setPoint : Point -> Model -> Model
setPoint newPoint model =
let
character =
model.character
newCharacter =
{ character | point = newPoint }
in
{ model | character = newCharacter }
updateCharacterPoint : Int -> Int -> Model -> Model
updateCharacterPoint x y model =
let
newPoint =
{ x = x, y = y }
in
-- このように書くことで、メソッド呼び出しのように書け ネストを意識させない直感的なコードになります。
model |> setPoint newPoint
incrementX : Model -> Model
incrementX model =
let
point =
model.character.point
incrementedXPoint =
{ point | x = point.x + 1 }
in
-- このように書くことで、メソッド呼び出しのように書け ネストを意識させない直感的なコードになります。
model |> setPoint incrementedXPoint
setPointと言うネストした更新対象であるRecordのsetterを明示的に関数にしてあげるだけで、直感的に更新でき 誰にでも書け、誰にでも読める素晴らしいコードとなりました。
ネストしたレコードに対するアプローチの比較
ここからは私自身の経験と推測に過ぎませんが、他の言語等で取っているアプローチを取っていたらどうなっていたか意見を述べたいと思います。
構文のネスト対応
もし、最初に引っかかったような構文自体がネストしたRecordの更新に対応しているとしましょう。その場合、今回のような簡単なケースは良いのですが、RecordにListや別のデータ構造の更新構文が無ければ中途半端な扱いづらさになります。また、更新と参照の概念は並列に考えなければなりません。例えば、途中の例で示したincrementX
のように新しい値を入れるだけでなく元の値を参照し更新するようなケースでは、基本イミュータブルな考えであるElmとは相性が悪くなってしまいます。今のElmのように基本1階層だけの更新制限がある場合、1階層のみにフォーカスした更新だけを考えれば良く、これこそが誰でも考え付き、読み書きができるアプローチにつながるのだと思います。
また、jqというコマンドがあります。これはコマンド一つでネストしたJSONの参照や操作が行えるスグレモノです。しかし、マニュアルがたったコマンド一つに対してボリュームがElm Guide並にあり 熟練した使い手で無ければ読み書きも困難になります。これを言語自体に組み込んでしまうと誰もが読み書きでき、扱いやすい言語とはならなくなってしまうことは想像に難くないでしょう。
メタプログラミング(リフレクション)
あまりメタプログラミングには明るくないので、個人的に馴染みのあるリフレクションに話を限定しますが、一度面倒だと思ったことに対してリフレクションを用いてしまうと、一時的には楽ができとても幸せな気分になりますが、少し違った書き方をするだけで動かなくなっていしまったり実装がブラックボックスになってしまい、逆に内部ではどう動いているのだろうかと想像しながら書かなくてはいけなくなってしまいます。そうなっては本末転倒になってしまいます。
まとめ
今回のネストしたRecordの更新に限らずElmの困ったことに対するアプローチはとても地味で愚直で、時には面倒だと感じてしまいます。しかし、面倒なだけで解決策は自明で他人が書いたプログラムでも問題なく読み解け拡張も楽です。また重複したコードに対しては関数型のアプローチなどが普段に使えるため、最終的にはスッキリしたコードを表現するのも難しくはありません。その場合でも読み書きが楽でシンプルであるという法則は崩れにくいです。面倒だと感じたら、他の言語等ではどんなアプローチを取ることができ、どんなメリット・デメリットがあるか立ち返ってみると良いかもしれません。実は一番理にかなっていたり、他の優れた手法があるならばElmを一度捨ててしまうのもとても良い選択です。まずはドンドン面倒なコードを量産して楽しいモノづくりをしてみるのも手ではないでしょうか。それでは素敵なElmライフを!