2023/7/30 Lenses from Scratchを読む
Getting and Setting
「Getting」と「Setting」というありふれた操作を抽象化して理解する
データを見る時に、
「あるデータ」と「その一部」という関係で捉える
getは「あるデータ」から「その一部」を取ってくる
code:hs
get :: (a, b) -> a
get (part, _) = part
setは「その一部」を変化させて、新しい「あるデータ構造」を得る
code:hs
set :: a' -> (a, b) -> (a', b)
set part (_, b) = (part, b)
Early lenses
Lens型は、get/setterを一緒にしたもの イメージ的にはこんなもの
code:hs
data Lens a b
= Lens { get :: a -> b
, set :: b -> a -> a
}
aが「あるデータ構造」
bが「その一部」
例えばタプルのfirstに対してのLensを用意する
getter/setterを同時に定義する感じ
code:hs
_1 :: Lens (a, b) a
_1 = Lens (\(part, _) -> part) (\part (_, b) -> (part, b))
以下のような感じで使える
code:hs
get _1 :: (a, b) -> a
set _1 :: a -> (a, b) -> (a, b)
getとsetは同じ「その一部」を対象にしてるので、同時に書いちゃおうって感じねmrsekut.icon
Composition
Lensは組み合わせることができる
(>-) :: Lens a b -> Lens b c -> Lens a c
Lens同士を組み合わせることで、データ構造の任意の部分をget/setできる
例えば、a,b,cのようにネストしている構造を考えたとき
①aからbをgetする
e.g. ((1,2), 3)から(1,2)をget
②bの中のcをsetして、新しいb'を得る
e.g. (1,2)を(10,2)にする
③aの中のbをsetして、新しいa'を得る
e.g. ((1,2),3)を((10,2),3)にする
って感じでやる
だから、上記の例のようなことをやりたければ、_1 -> -1みたいなLensがあればできる
現状のコード
code:lv1.hs
data Lens a b
= Lens { get :: a -> b
, set :: b -> a -> a
}
(>-) :: Lens a b -> Lens b c -> Lens a c
la >- lb = Lens (get lb . get la) $ \part whole ->
set la (set lb part (get la whole)) whole
Generalizing to Store
ここの節、むずすぎるmrsekut.icon
まず、上記で定義したLensを組み替えてよく知られた(?)構造を見つける操作をする
code:hs
type Lens a b = (a -> b, b -> a -> a) -- 上記をタプルにしただけ
code:hs
type Lens a b = (a -> b, a -> b -> a) -- setterをflipした
code:hs
type Lens a b = a -> (b, b -> a) -- 共通の第1引数を取り出した
直感として、bとb -> aを以下のように捉えることができる
b
piece
大きな構造aの一部
b -> a
hole
大きな構造の一部bをholeにしたa
((1,2),3)のような構造を考えると、その一部が欠落した\b -> ((b,2),3)みたいな感じ
上のような構造は、一般に(?)Store型と呼ばれるものらしい code:hs
data Store b a = Store b (b -> a)
これはFunctorやComonadのinstanceになるようなものらしい
だから何なんだという感じはするmrsekut.icon
Lensのtutorialに当たって、この概念の導入がどれほど有用なのかわからんmrsekut.icon
また、Coalgebraというよく知られた概念とも関連があることの説明がある
Coalgebraは、seed (種)aから、Functorを適用したf aを生成するような関数のこと
unfoldの一般化
Lensは、Storeであり、Comonadであり、Coalgebraである
code:hs
type Coalg f a = a -> f a
type Lens a b = Coalg (Store b) a
順に見ると、
Lensは、大きなデータ構造からStoreを生成する関数である
code:hs
type Lens a b = a -> Store b a
Storeは、Functorである
なので、Lensは、a -> f aの形をしている
従って、LensはCoalgebraである
What did we gain?
現状のコード
code:lb2.hs
type Lens a b = a -> Store b a
data Store b a = Store b (b -> a)
(>-) :: Lens a b -> Lens b c -> Lens a c
(la >- lb) a = let Store partB holeBA = la a
Store partC holeCB = lb partB
holeCA = holeBA . holeCB
in Store partC holeCA
Laws
getterとsetterが以下の3つを満たす
set s (get s) == s
get (set s v) == v
get (set (set s v1) v2) == v2
Comonadと組み合わせて以下も満たす
extract . l == id
fmap l .l == duplicate l
Can we go further?
Store型に依存しないよりよいinterfaceを探る
Store型を使うと、Lensを使う全てのlibraryがLensライブラリ全体を依存関係として持つ必要が出てきてしまう
その方法としてexperimentという関数を導入する
code:hs
experiment :: Functor f => Store b a -> (b -> f b) -> f a
experiment (Store part hole) inj = hole <$> inj part
この辺もうよくわからんかったmrsekut.icon
以下2つが同型になる
code:hs
Store b a -- Store piece hole
code:hs
(b -> f b) -> f a -- experiment (Store piece hole)
結論として、Lensを以下のように定義し直せる
code:hs
{-# LANGUAGE RankNTypes #-} type Lens a b = forall f. Functor f => (b -> f b) -> a -> f a
これは上述のStoreを用いたLens型と同型なので情報量の過不足がない
Composing Pretexts
code:hs
type Lens a b = Functor f => (b -> f b) -> (a -> f a)
type Coalg f x = x -> f x
-- ↓↓↓こうする
type Lens a b = Functor f => Coalg f b -> Coalg f a
Lensの合成は、関数の合成と見なせる
code:hs
(>-) :: Lens a b -> Lens b c -> Lens a c
la >- lb = la . lb
これすごすぎでしょ。意味がわからんmrsekut.icon
これで、ある型のLensが欲しい場合はfmapだけが必要となった
現状のコード
code:lv3.hs
type Lens a b = Functor f => Coalg f b -> Coalg f a
(>-) :: Lens a b -> Lens b c -> Lens a c
la >- lb = la . lb
But wait, what about s-t-a-b?
実際、libraryではLens s t a bという型が用いられているが、それは何なのか?
上記の関数_1 inj (a, b) = (,b) <$> inj aを型推論させると以下の型が得られる
code:hs
_1 :: Functor f => (a -> f b) -> (a, t) -> f (b, t)
元の型を展開したものは
code:hs
-- _1 :: Lens (a,t) a を展開
_1 :: Functor f => (a -> f a) -> (a, t) -> f (a, t)
こっちの方が、謎に制限が強まっていることがわかる
これは、Lensが全体の型を変更する可能性を排除してることになっている
よって、この制限を緩和して一般化すると
code:hs
type Lens s t a b = Functor f => (a -> f b) -> (s -> f t)
が得られる
結局Coalgebra使ってない(使えない)やんけmrsekut.icon
意味的には
s, tが全体の型
a, bが部分の型
を表す
以下のように書けば元のやつと同じになる
code:hs
type Lens' s a = Lens s s a a
現状のコード
code:lv4.hs
type Lens s t a b = Functor f => (a -> f b) -> (s -> f t)
(>-) :: Lens s t a b -> Lens a b c d -> Lens s t c d
(>-) = (.)