F#で Lens を使おうと思った日。あるいは簡単な Lens の使い方

こんにちは、あやぴーです。 今日は F# における Lens の話をしようと思います。

ある日のこと…

チームで F# を使って開発をしているとあることに気が付きました。あれ、もしかしてレコードタイプからフィールドの値を取り出すだけのラムダ式が結構書かれている…?

例えば、以下のようなレコードタイプがあるとします。

type Employee = { Name: string; Title: string }
type Department = { Name: string; Leader: Employee }

このとき次のようなデータに対して、 f のような関数がつくられがちです。

let dept =
    { Name = "Super Dept"
      Leader = { Name = "Alice"; Title = "B" } }

let f = fun { Department.Name = n } -> n

f dept
// => "Super Dept"

特に Seq.mapResult.map などで任意のフィールドだけを使いたいことがちらほら出てくるので、どうにかならないかなと感じていました。もちろん、上記の関数 f を適当に getter ぽい名前にしてちゃんとまとめておくのも手だと思います(なお、 fun を使ってラムダ式をつくっていますが、「こういうラムダ式が書かれがち」というのを伝わってほしいのでこの書き方になっています)。それでは次のような関数 g を考えてみます。

let g =
    fun dept newLeaderTitle -> { dept with Leader = { dept.Leader with Title = newLeaderTitle } }

g dept "A"
// =>  { Name = "Super Dept"
//       Leader = { Name = "Alice"
//                  Title = "A" } }

LeaderTitle だけを変更したいので、 with を駆使して現在のデータを維持しつつ Title のみを更新するような関数です。流石におかしい気がしてきます。このようにネストしたデータ構造の前では地道に構造を解いて再構築するなどしか方法がないように思えます。関数プログラミングなので、もっとこう小さな関数を組み合わせて問題を解決するようなアプローチが存在すれば良いのですが…。

そこで、Lens の登場です。

Lens を用いて問題を解く

細かい話は一旦置いておいて、 Lens を使って先程の問題を解いてみましょう。 FSharpPlus を使うので、適当に依存関係にいれるなどして使えるようにしてみます。

そうしたら以下のコードを何も考えずに書きます。

open FSharpPlus.Lens

module Department =
    let inline _name f (dept: Department) =
        f dept.Name
        <&> fun name -> { dept with Department.Name = name }

    let inline _leader f dept =
        f dept.Leader
        <&> fun leader -> { dept with Department.Leader = leader }

module Employee =
    let inline _name f (emp: Employee) =
        f emp.Name
        <&> fun name -> { emp with Employee.Name = name }

    let inline _title f emp =
        f emp.Title
        <&> fun title -> { emp with Employee.Title = title }

あとは自由です。早速、関数 f を置き換えてみます。

let f' = view Department._name

f' dept
// => "Super Dept"

これは期待した通りに動いています。不思議ですね。 view は FSharpPlus.Lens の関数です。次に関数 g も書き換えてみましょう。

let g' = setl (Department._leader << Employee._title)

g' "A" dept
// =>  { Name = "Super Dept"
//       Leader = { Name = "Alice"
//                  Title = "A" } }

よくわからないことが起きている気がします。 << は関数合成の演算子なので、 Department._leaderEmployee._title が関数であることはなんとなく伝わるでしょう。ではそのふたつが関数だとして、 setl を適用するすることで新しい Title と現在の Department を受け取る関数をつくることができ、これによって LeaderTitle の部分を変更することができました。少しだけ実験してみたいので Department._leaderEmployee._title を合成したものに先程の view 関数を適用してみます。

view (Department._leader << Employee._title) dept
// => "B"

dept から LeaderTitle を取得することができました。なんだか面白いですね。

まとめ

まだ原理や仕組みを理解しているわけではないので、細かい解説は他に譲りますが、どうやら Lens というのは先程の Department._name のようなもののことのようです(関数名に _ プレフィックスをつけるのは慣習)。この Lens は先程までの例のように OOP でいうところの getter/setter のような何かとして機能します。ただし、この getter/setter のようなものは関数であり、しかも合成することができるという特徴を持っている、というのがただの getter/setter との違いでしょう。

実際、 Lens には今回紹介した以上に様々なことができるようなのですが、とりあえず関数合成できる getter/setter のようなものを扱える、という程度の理解でもなかなか便利なことが分かったと思います。それと Lens は以下のような定義をすることもできるので、いくつか書き方を覚えておくと良いです。

module Department' =
    let inline _leader dept =
        let getter dept = dept.Leader
        let setter dept leader = { dept with Leader = leader }
        lens getter setter dept

それでは Lens を使ってイミュータブルデータと仲良くしていきましょう。以上、 Lens の簡単な紹介でした。

Page top