17
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

ElmでFocusとLensの比較

Last updated at Posted at 2017-10-09

前に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

大きな違いはイミュータブル(不変)なデータ構造という点です。以下の例では、r2yを2に変えることでr1と一致します。しかしこのときr2は更新されているわけではなく、r2yの値を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を作り、objectpositionに新しい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=bigb=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は、getset関数を受け取ることで形成することができます。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

これは以下のように一般化、簡略化することができます。フィールドxFloat型である必要は無いため型変数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を更新する関数を渡します。今回は、xyを変更する必要があるのでパイプで処理を続けていくだけで終りとなります。

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は生成時にgetupdateを必要とする
  • Lensは生成時にgetsetを必要とする

生成に関してはLensの方が簡単です。Focusの方は様々な関数を定義するときには使い勝手が良いと思われます。しかし個人的には、LensはFocusと比べて多くの汎用関数を用意しており、ライブラリを使う側としてはLensの方が楽だと感じました。

17
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?