LoginSignup
21
17

More than 1 year has passed since last update.

Lensとは

Last updated at Posted at 2021-05-08

Lensとは、初級者Haskellerの苦い思い出

 さあさあ、やって参りました。

 Haskell何個目かの鬼門にして、素晴らしく便利なアイツ、Lensの時間です。

 初級者HaskellerにとってのLensといえば、「これを知らなきゃ初級者Haskellerにもなれない」と突然突きつけられて、何かと思って蓋を開ければ地獄のような型が覗く、恐怖と畏怖の対象でしょう。

 調べてみても、様々な言葉で説明されるLensは、いかにもつかみどころのないものに思えます。

 一方で、Lensは使うだけなら簡単です。
 「よく分からないが、このようにすれば動く」というもやもやを胸に秘め、初級者HaskellerはLensを知ってひとつ大人になるのです。

 それぞれの説明を確かめながら、Lensについての理解を深めていきましょう。

Lensとは、getterとsetterのペアである

 一番平易な説明から見ていきましょう。
 getterやsetterという単語は、オブジェクト指向言語を学んだことのある人には馴染みがあります。

class S {
    private A a;

    public A getA() {
        return this.a;
    }

    public void setA(A a) {
        this.a = a;
    }
}

 このようなgetXsetXを、getterやsetterと呼ぶのでした。

 Haskellでは下のような関数で表しますね。

data S = S A

getA :: S -> A
getA (S a) = a

setA :: S -> A -> S
setA (S _) a = S a

 型に注目すると、getAの型はS -> A, setAの型はS -> A -> Sとなっています。
 Sがオブジェクトの型でAがフィールドの型ですね。

Lensとは、getterとsetterのペアである

 ということなので、getterとsetterのペアにLensと名付けてみます。

type Lens s a = (s -> a, s -> a -> s)

 こんな単純なものが果たして本当にあの複雑怪奇なLensなのでしょうか。疑わしいので、実際に確かめてみましょう。

 まずはお馴染の演算子(^.), (.~)を定義します。
 この演算子は引数の順番を入れ替えただけの単純なgetterとsetterです。

 先ほど実装したLensは単純なgetterとsetterのペアですから、fstsndを使ってgetterとsetterを取り出すことができます。
 取り出したgetterとsetterを使って(^.), (.~)を定義してみます。

get :: Lens s a -> (s -> a)
get = fst

set :: Lens s a -> (s -> a -> s)
set = snd

(^.) :: s -> Lens s a -> a
(^.) = flip get
infixl 8 ^.

(.~) :: Lens s a -> a -> s -> s
(l .~ a) s = set l s a
infixr 4 .~

 それと、Lensを合成する演算子と、getterとsetterからLensを作るコンストラクタを定義します。

compose :: Lens a b -> Lens b c -> Lens a c
compose (ab, aba) (bc, bcb) = (ac, aca)
  where
  ac = bc . ab
  aca a = aba a . bcb (ab a)
infixr 9 `compose`

lens :: (s -> a) -> (s -> a -> s) -> Lens s a
lens = (,)

 これだけあれば、Lensとして最低限の仕事ができるようになります。試してみましょう。
 先ほど出したSAの例を使いましょう。

 まずはgetterとsetterの組でLensを作成します。

data S = S A deriving (Show)

getA :: S -> A
getA (S a) = a

setA :: S -> A -> S
setA (S _) a = S a

a :: Lens S A
a = lens getA setA

data A = A Int deriving (Show)

getX :: A -> Int
getX (A x) = x

setX :: A -> Int -> A
setX (A _) x = A x

x :: Lens A Int
x = lens getX setX

 作ったLensを使ってみます。

>>> :{
    let s = S (A 100)
    print $ s ^. a `compose` x
    print $ s & a `compose` x .~ 200
:}
100
S (A 200)

 この使用感は間違いなくLensと言っていいでしょう。
 唯一の文句といえば、合成を.ではなくcomposeと書かなければならない点でしょうか。

 本当にgetterとsetterのペアからLensを構成することができるとは、驚きですね。

 ところで、lensパッケージのLensに比べると型引数が少ないのが気になった人がいるかもしれません。
 getterとsetterとで別の型引数を使うように変えてみると、私たちの知るLensの型になります。

type Lens s t a b = (s -> a, s -> b -> t)

 この章のコードはこちらに載せてあります。

Lensとは、余状態余モナドの余代数である

 何を言ってるのか分からないかもしれませんが、そういうことらしいです。

 とりあえず、今までのように字面を追ってみましょう。
 余状態余モナドとは、Storeコモナドとも呼ばれているもので、次のようなものです。

type Store s a = (s -> a, s)

 型だけ見ると、getterとオブジェクトのペアのように見えますね。

 余代数というのはF余代数のことです。
 ある自己関手$F : C → C$があった時に、次のようなある対象と射の組$(A, α : A → F A)$をF余代数と呼びます。

 Haskellで表すなら次のような感じでしょうか。

type FCoalgebra a = forall f. Functor f => a -> f a

 モナドが関手でもあるように、コモナドも関手ですので、このfにはStoreコモナドを入れることができます。

type Lens s a = s -> Store a s

 できました。これが余状態余モナドの余代数です。

 エイリアスを展開すると、getterとsetterのペアの同型であることが分かります。

type Lens s a = s -> (a -> s, a)

 これはgetterとsetterのペアと同じ計算能力を持つということですから、同じようにしてLensにできそうですね。

GetterとSetterの組とStoreコモナドの余代数の同型射の例
type GetterSetter s a = (s -> a, s -> a -> s)
type StoreCoalgebra s a = s -> (a -> s, a)

fromStoreCoalgebra :: StoreCoalgebra s a -> GetterSetter s a
fromStoreCoalgebra f = (snd . f, fst . f)

toStoreCoalgebra :: GetterSetter s a -> StoreCoalgebra s a
toStoreCoalgebra (getter, setter) s = (setter s, getter s)

 改めて、まずはお馴染の演算子(^.), (.~)を定義してみましょう。

get :: Lens s a -> s -> a
get l s = let (_, a) = l s
           in a

set :: Lens s a -> s -> a -> s
set l s = let (a, _) = l s
           in a

(^.) :: s -> Lens s a -> a
s ^. l = get l s
infixl 8 ^.

(.~) :: Lens s a -> a -> s -> s
(l .~ a) s = set l s a
infixr 4 .~

 簡単ですね。

 次に、Lensを合成する演算子と、getterとsetterからLensを作るコンストラクタを定義します。
 ここまでできてしまえば、もうLensと言っても差し支えないのでした。

compose :: Lens a b -> Lens b c -> Lens a c
compose f g a = (ba . cb, c)
    where
    (ba, b) = f a
    (cb, c) = g b

lens :: (s -> a) -> (s -> a -> s) -> Lens s a
lens get set s = (set s, get s)

 できましたね。
 後はgetterとsetterでの実装と同じように利用することができます。

Lensとは、forall f. Functor f => (a -> f b) -> s -> f t

 何が何やら。しかしこれが本命です。

 lensパッケージのLensの定義を読むと、次のように書いてあります。

type Lens s t a b = forall f. Functor f => (a -> f b) -> s -> f t
type Lens' s a = Lens s s a a

 型引数の数が少ない、Lens'の方で読み替えてみましょう。

type Lens' s a = forall f. Functor f => (a -> f a) -> s -> f s

 Storeコモナドの実装に似ている気がします。a -> f aはF余代数でしたね。
 このことから、あるF余代数の射を別のF余代数の射に移すような関数であることが分かります。

 これまで通りgetsetを定義して、お馴染みの演算子(^.), (.~)を定義してみましょう。
 これまでとは違い、かなり複雑です。

get :: Lens s a -> s -> a
get l s = getConst $ l Const s

set :: Lens s a -> s -> a -> s
set l s a = runIdentity $ l (const $ Identity a) s

(^.) :: s -> Lens s a -> a
s ^. l = get l s
infixl 8 ^.

(.~) :: Lens s a -> a -> s -> s
(l .~ a) s = set l s a
infixr 4 .~

 getのときとsetのときとで関手fの解釈が変わっています。

 getのときはfConst aですから、Lensは下のような型に特殊化されます。

type Getting s a = (a -> Const a a) -> s -> Const a s

 setのときはfIdentityですから、Lensは下のような型に特殊化されます。

type Setter s a = (a -> Identity a) -> s -> Identity s

 「渡される関手によってgetterやsetterになる関数」がLensの正体ということだったわけですね。

 Lensの合成とLensのコンストラクタも作ります。
 ここで、このLensの実装ではcomposeが関数合成と同じになることに気が付きます。つまり、今までcomposeと書いていた部分を.で繋げられるようになるということです。さすが本命バージョンです。

compose :: Lens a b -> Lens b c -> Lens a c
compose = (.)

lens :: (s -> a) -> (s -> a -> s) -> Lens s a
lens get set f s = set s <$> f (get s)

おまけ: Lens圏

 これまで実装してきたLensたちは、どれもcompose :: Lens a b -> Lens b c -> Lens a cを備えていました。
 この型、よく見ると関数合成にそっくりです。(事実、最後のLensは関数合成そのままで実装しましたね)

 実は、この合成を使ってCategoryを実装することができます。
 これをLens圏と呼びます。

instance Category Lens where
  id = lens id (const id)
  (.) = flip compose

おわり

  1. Lensをgetterとsetterのペアから構成できることを確認しました。
  2. Lensを余状態余モナドの余代数から構成できることを確認しました。また、これがgetterとsetterのペアと同型であることを確認しました。
  3. lensパッケージのLensはgetterにもsetterにもなるふしぎ関数であることを確認しました。

 今回実装したLensは型引数を2つしか取りませんが、これをもう少し一般化して型引数を4つ取るようにすると普段使っているLensになります。(getterとsetterの例でやりましたね)
 また、さらに一般化していくとopticsと呼ばれる様々なLensの亜種を得ることができ、逆にLensはopticに制限を書けたものだという理解もできるようになります。

 opticについては、こちらのスライドが分かりやすいので、ぜひ読んでみてください: Opticから見たLens/Prism

参考:

21
17
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
21
17