前にElmでLensというライブラリを触っていたところ、フォロワーさんからFocusで良いのではないか?との意見をもらったのを最近思い出して簡単に比較したので記事にしました。
-> 発展編へ
なぜFocus, Lens?
FocusとLensは複雑なレコードを操作(get, set, updateなど)するためのライブラリです。Elmを知らない方はなぜ、わざわざgetter, setterなどを別途ライブラリを持ち出さないといけないか謎だと思います。なぜかを知るために、まずはじめにElmのレコードについて見ていきましょう。Elmのレコードは一見すると、JavaScriptのオブジェクトのコロン(:)がイコール(=)に変わっただけのように見えます。
> r1 = { x = 1, y = 2 }
{ x = 1, y = 2 } : { x : number, y : number1 }
> r1.x
1 : number
大きな違いはイミュータブル(不変)なデータ構造という点です。以下の例では、r2
のy
を2に変えることでr1
と一致します。しかしこのときr2
は更新されているわけではなく、r2
のy
の値を2にした新しいレコードを生成しています。イミュータブルなデータ構造を用いることで、予期せぬ値の変更に怯えなくてよかったり、基本的にデータを比較する時に同値性だけを気にすれば良くなります(Javaではequals
を別途定義したり、JavaScriptでは文字列にして比較する必要があります)。
> r2 = { x = 1, y = 5 }
{ x = 1, y = 5 } : { x : number, y : number1 }
> r1 == { r2 | y = 2 }
True : Bool
> r2
{ x = 1, y = 5 } : { x : number, y : number1 }
それでは、どのようなケースでFocusやLensが必要になってくるのでしょうか。それは以下のようにネストしたデータ構造の場合に必要になります。
type alias Object =
{ position : Point
, velocity : Point
}
type alias Point =
{ x : Float
, y : Float
}
つまり標準では、以下のような操作が出来ません。
> object = Object (Point 3 4) (Point 1 1)
> { object | position = { object.position | x = 5 } }
例えば、object.position.x
を変更するには、以下のような関数が必要になります。古いposition
を変数に束縛し、新しいposition
を作り、object
のposition
に新しいposition
を設定する。なんだか面倒ですね。
setPositionX x object =
let
oldPosition =
object.position
newPosition =
{ oldPosition
| x = x
}
in
{ object
| position = newPosition
}
setPositionX 5 object
FocusとLensを比較してみよう
それでは、2つのライブラリの比較をしてみましょう。
Focus
Focusは、大きなデータ構造big
の小さなデータ構造small
に集中することができます。小さなデータ構造とは、レコードの特定のフィールドのことです。そうすることで大きなデータ構造に対する小さなデータ構造の取得、設定、更新が行なえます。
type Focus big small
それではFocusの取得関数get
について見ていきましょう。第一引数にFocus、第二引数に取得の対象となるデータ構造を渡します。結果としてFocusされたデータ(構造)を取得することができます。
get : Focus big small -> big -> small
簡単なサンプルを見てみましょう。x
は、レコードからフィールドxにフォーカスする関数です。レコード{ x=3, y=4 }
のx
にフォーカスし、取得を行います。
x : Focus { record | x:a } a
get x { x=3, y=4 } == 3
今度は、設定関数set
を見ていきましょう。基本はget
と同じです。フォーカスして、設定したい値を渡すだけで完了です。
set : Focus big small -> small -> big -> big
x : Focus { record | x:a } a
set x 42 { x=3, y=4 } == { x=42, y=4 }
Lens
Lensは、Monocleというモジュール群にあるモジュールの一つです。Lensは、大きなデータ構造a
の小さなデータ構造b
に集中することができます。小さなデータ構造とは、レコードの特定のフィールドのことです。そうすることで大きなデータ構造に対する小さなデータ構造の取得、設定、更新が行なえます。
・・・あれ? そうです。FocusとLensは型変数が違うだけで、定義が全く同じなのです。
type alias Lens a b =
{ get : a -> b
, set : b -> a -> a
}
実際に、Lensの定義をa=big
、b=small
と置き換えてFocusと並べてみるとまったく一致することがわかります。
type alias Lens big small =
{ get : big -> small
, set : small -> big -> big
}
type Focus big small
get : Focus big small -> big -> small
set : Focus big small -> small -> big -> big
使用例
具体的な使用例を見ていきましょう。以下のようなデータがあった場合に、dt(Float)だけ時間が経ったときのObjectを取得する関数step
を定義してみましょう。
type alias Point =
{ x : Float
, y : Float
}
type alias Object =
{ position : Point
, velocity : Point
}
Object (Point 3 4) (Point 1 1)
step : Float -> Object -> Object
まずは、FocusもLensも使用しないstep
関数です。
step : Float -> Object -> Object
step dt object =
let
oldPosition =
object.position
newPosition =
{ oldPosition
| x = oldPosition.x + object.velocity.x * dt
, y = oldPosition.y + object.velocity.y * dt
}
in
{ object
| position = newPosition
}
それでは、FocusとLensを使った方法に切り替えていきましょう。まずは、各フィールドに対するFocus, Lensを定義していきましょう。
まずは愚直に定義した場合の、x
のLensを見ていきましょう。Lensは、get
とset
関数を受け取ることで形成することができます。get
は、Point
型のレコードからxを取得する関数です。set
は、新しいxとPoint型を受取、xを更新した新しいPoint型を生成する関数です。
get : a -> b
set : b -> a -> a
x : Lens { r | x : Float } Float
x =
let
get point =
point.x
set x point =
{ point | x = x }
in
Lens get set
これは以下のように一般化、簡略化することができます。フィールドx
はFloat
型である必要は無いため型変数a
で抽象化します。get
は、.x
のようにフィールドを参照する関数で省略できます。set
は無名関数で置き換えます。
x : Lens { r | x : a } a
x =
Lens .x (\x r -> { r | x = x })
Focusを作るには、create
関数を利用することで生成することができます。Lensとほぼ同じですが、set
ではなく更新関数update
((small -> small) -> big -> big)
を受け取るようです。
create
: (big -> small)
-> ((small -> small) -> big -> big)
-> Focus big small
x : Focus { r | x : a } a
x =
Focus.create .x (\f r -> { r | x = f r.x })
他の関数は、基本的にFocus, Lens関数x
と同じなため省略します。step関数自体の実装を見ていきましょう。
Lensの場合は以下のような実装になります。Lensをcompose(=>)関数で合成し、目的のLensに到達します。この例では、Object
からPosition.x
に焦点を当てるLensです。Lensが完成したら、modify関数に、x
を更新する関数を渡します。今回は、x
とy
を変更する必要があるのでパイプで処理を続けていくだけで終りとなります。
Lens.modify : Lens a b -> (b -> b) -> a -> a
step : Float -> Object -> Object
step dt object =
object
|> Lens.modify (position => x) (\px -> px + object.velocity.x * dt)
|> Lens.modify (position => y) (\py -> py + object.velocity.y * dt)
(=>) : Lens a b -> Lens b c -> Lens a c
(=>) =
compose
infixr 9 =>
Focusの場合には、update
を代わりに用います。Lensのmodify
と全く同じなため、step
の実装も何と全く同じになってしまいます!
update : Focus big small -> (small -> small) -> big -> big
step : Float -> Object -> Object
step dt object =
object
|> update (position => x) (\px -> px + object.velocity.x * dt)
|> update (position => y) (\py -> py + object.velocity.y * dt)
簡単に試せるように全体を用意しておきました。お試しください。
結論
FocusとLens一体何が違ったのでしょうか?
- LensはMonocleの一部のモジュール
- Common
- Iso
- Lens
- Optional
- Prism
- FocusにないLensの汎用関数
modify2, 3
modifyAndMerge
zip
tuple
- Focusは生成時に
get
とupdate
を必要とする - Lensは生成時に
get
とset
を必要とする
生成に関してはLensの方が簡単です。Focusの方は様々な関数を定義するときには使い勝手が良いと思われます。しかし個人的には、LensはFocusと比べて多くの汎用関数を用意しており、ライブラリを使う側としてはLensの方が楽だと感じました。