ネストしたレコードの更新(Functional lenses)

下記記事にて、リフレクションを用いてネストしたレコードの更新を実装してみました。
ネストしたレコードの更新 (F#) - 何でもプログラミング

今回はFunctional lensesを利用して同様の内容を実装してみたいと思います。

Lens型定義

Lens型は対象オブジェクトから値を取得するGetと、値を更新した新しいオブジェクトを返すSetからなります。

type Lens<'Type, 'Field> =
    { Get : 'Type -> 'Field
      Set : 'Field -> 'Type -> 'Type
    }


Lens関数群定義

Lensを合成するcomposeが最も重要なものとなります。

module Lens =
    let create get set = 
        { Get = get; Set = set; }

    let set (lens : Lens<'a, 'b>) (value : 'b) (object : 'a) =
        lens.Set value object

    let update (lens : Lens<'a, 'b>) (f : 'b -> 'b) (object : 'a) =
        lens.Set (lens.Get object |> f) object

    let compose (lens1 : Lens<'a, 'b>) (lens2 : Lens<'b, 'c>) =
        { Get = lens1.Get >> lens2.Get
          Set = lens2.Set >> update lens1
        }

// composeを楽にする演算子
let inline (>-) (lens1 : Lens<'a, 'b>) (lens2 : Lens<'b, 'c>) =
    Lens.compose lens1 lens2


実際の利用

下記のレコードが定義されているとします。

type Container = { Value : int }
type Child = { Container : Container }
type Root = { Child : Child }

let record1 = { Child = { Container = { Value = 100 } } }

まずは各レコードのLensを定義します。

module Container =
    let value = Lens.create (fun x -> x.Value) (fun x y -> { y with Value = x })
module Child =
    let container = Lens.create (fun x -> x.Container) (fun x y -> { y with Container = x })
module Root = 
    let child = Lens.create (fun x -> x.Child) (fun x y -> { y with Child = x })

レコードの更新は、下記のように記述します。

現在値を参照する場合はLens.updateを利用します。

let record2 = record1 |> Lens.set (Root.child >- Child.container >- Container.value) 200


リフレクションと比較

リフレクションを利用したほうがLensの定義も必要なく簡潔に記述できます。

一方Lensを利用するとパフォーマンスを期待でき、リフレクションのない言語でも応用可能です。