著者:井上 健太
関数型言語でオブジェクトを扱うための型である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
型があればいいことがわかりました。