著者:井上 健太
関数型言語でオブジェクトを扱うための型であるLensについて、関数型言語Haskellを用いて解説します。
まず、オブジェクト指向の特徴であるgetterとsetterを型を使って表してみます。
getterとsetterの型
オブジェクトの型をsとし、そこに格納する変数の型をaとした時、getter関数get、setter関数setはそれぞれ以下の型になります。
get :: s -> a
set :: s -> a -> s
ただし、この2つの関数をオブジェクトとみなすためには、任意のオブジェクトsと入力a、bに対し、以下の性質を満たす必要があります。
get (set s a) = a
set s (get s) = s
set (set s b) a = set s a
1つ目はオブジェクトに値aをsetした直後にgetすると同じ値が得られること、
2つ目はgetした値をそのままsetしたものは元のオブジェクトと等しいこと、
3つ目はsetを複数回繰り返しすことは、最後のsetだけ適用するのと変わらないことを表しています。
getter、setterからLensを作る
オブジェクトを扱う関数としてはsetとgetがわかりやすいですが、この2つを合わせた以下のようなMyLens型の関数が作れます。
type MyLens s a = forall f. Functor f => (a -> f a) -> s -> f s
実際にgetとsetからこの型の関数が定義でき、その逆も定義できます。
{- get & set -> lens -}
mkMyLens :: (s -> a) -> (s -> a -> s) -> MyLens s a
mkMyLens get set f s = set s <$> f (get s)
{- lens -> get -}
mkGet :: MyLens s a -> s -> a
mkGet lens = getConst . lens Const
{- lens -> set -}
mkSet :: MyLens s a -> s -> a -> s
mkSet lens s a = runIdentity $ lens (\_ -> Identity a) s
さらに、この定義から任意のgetとsetに対し、以下の性質を満たすことも分かります。
mkGet (mkLens get set) s = get s
mkSet (mkLens get set) s a = set s a
つまり、getとsetの2つをMyLens型1つで表現できるということです。
しかしながら、Haskellの実際のLensの型はこのMyLensとは異なります。
実際のLens
ライブラリ上では、上で定義したMyLensではなく、これを一般化したLensという型を使います。
type Lens s t a b = forall f. Functor f => (a -> f b) -> s -> f t
先ほどのMyLensと比較すると、型引数がより一般化したものになっていて、実際にMyLensはLensを使って記述できます。
type MyLens s a = Lens s s a a
実はMyLensはLens'という名前で既に実装されています。
type Lens' s a = Lens s s a a
LensとGetter、Setterとの関係
このLensに対応するGetter、Setterの型はそれぞれ
Lens s t a b :: forall f. Functor f => (a -> f b) -> s -> f t
get :: s -> a
set :: s -> b -> t
になっており、上の例のようにこのgetとsetからLens型の関数を作れ、逆にLens型の関数からgetとsetを作れます。
{- get & set -> Lens -}
lens :: (s -> a) -> (s -> b -> t) -> Lens s t a b
lens sa sbt afb s = sbt s <$> afb (sa s)
{- Lens -> get -}
myView :: MonadReader s m => Lens s t a b -> m a
myView lens = Control.Monad.Reader.asks (getConst . lens Const)
myView' :: Lens s t a b -> s -> a
myView' = myView
{- view :: MonadReader s m => Getting a s a -> m a -}
{- type Getting r s a = (a -> Const r a) -> s -> Const r s -}
{- Lens -> set -}
set :: ASetter s t a b -> b -> s -> t
set l b = runIdentity . l (\_ -> Identity b)
mySet :: Lens s t a b -> s -> b -> t
mySet lens = flip $ set lens
{- type ASetter s t a b = (a -> Identity b) -> s -> Identity t -}
viewの方はライブラリではLens s s a a上のみで定義されていますが、上のmyViewのように、一般化してLens s t a b上で定義できます。
また、MyLensの時と同様に任意のgetとsetに対し、以下の性質が成り立ちます。
myGet (lens get set) s = get s
mySet (lens get set) s a = set s a
まとめ
関数型言語でオブジェクトを表すのにLens型があればいいことがわかりました。
