Lensとは、初級者Haskellerの苦い思い出
さあさあ、やって参りました。
Haskell何個目かの鬼門にして、素晴らしく便利なアイツ、Lensの時間です。
初級者HaskellerにとってのLensといえば、「これを知らなきゃ初級者Haskellerにもなれない」と突然突きつけられて、何かと思って蓋を開ければ地獄のような型が覗く、恐怖と畏怖の対象でしょう。
調べてみても、様々な言葉で説明されるLensは、いかにもつかみどころのないものに思えます。
- Lensはgetterとsetterのペア
- レンズは余状態余モナドの余代数だった
-
type Lens s t a b = forall f. Functor f => (a -> f b) -> s -> f t
- [Control.Lens.Combinators#t:Lens] (https://hackage.haskell.org/package/lens-5.0.1/docs/Control-Lens-Combinators.html#t: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;
}
}
このようなgetX
やsetX
を、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のペアですから、fst
とsnd
を使って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として最低限の仕事ができるようになります。試してみましょう。
先ほど出したS
とA
の例を使いましょう。
まずは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余代数の射に移すような関数であることが分かります。
これまで通りget
とset
を定義して、お馴染みの演算子(^.)
, (.~)
を定義してみましょう。
これまでとは違い、かなり複雑です。
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
のときはf
がConst a
ですから、Lensは下のような型に特殊化されます。
type Getting s a = (a -> Const a a) -> s -> Const a s
set
のときはf
がIdentity
ですから、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
おわり
- Lensをgetterとsetterのペアから構成できることを確認しました。
- Lensを余状態余モナドの余代数から構成できることを確認しました。また、これがgetterとsetterのペアと同型であることを確認しました。
-
lensパッケージの
Lens
はgetterにもsetterにもなるふしぎ関数であることを確認しました。
今回実装したLensは型引数を2つしか取りませんが、これをもう少し一般化して型引数を4つ取るようにすると普段使っているLensになります。(getterとsetterの例でやりましたね)
また、さらに一般化していくとopticsと呼ばれる様々なLensの亜種を得ることができ、逆にLensはopticに制限を書けたものだという理解もできるようになります。
opticについては、こちらのスライドが分かりやすいので、ぜひ読んでみてください: Opticから見たLens/Prism
参考: