読者です 読者をやめる 読者になる 読者になる

lensパッケージのオプティクス(弱い順)

lensではオプティクスと呼ばれる様々な構造が定義されている。これらの関係を把握していれば、ドキュメントから欲しいものを見つけるのが楽になる。この記事では弱い順にオプティックの数々を紹介していく。

Fold

type Fold s a = forall f. (Applicative f, Contravariant f) => (a -> f a) -> s -> f s

Contravariantがついているのでわかりにくいが、これは本質的に以下の型と等価だ。mappend*>memptyfmap absurd $ contramap absurd $ pure ()に相当する。

type Fold s a = forall r. (Monoid r) => (a -> r) -> s -> r

で、これは何かと言えば、foldMap :: Foldable f => (a -> r) -> f a -> rの一般化そのものだ。foldMapf aの要素aを畳み込むが、Fold s asの中のaを畳み込む。当然、FoldableでできることはFoldでもできる。そのあたりはモノイドと継続渡しの道具箱で触れている。

FoldはそのままではfoldMapの一般化としては使いにくいため、Control.Lens.Foldモジュールでユーティリティが定義されている。Foldableメソッドとしてfooがあったとき、Foldableの代わりにFoldを受け取るfooOfという関数が提供されている。

Getter

Getterはより強く、Functorしか要求しない。

type Getter s a = forall f. (Functor f, Contravariant f) => (a -> f a) -> s -> f s

こちらは、Applicativeに由来するモノイドの力を必要としないため、forall r. (a -> r) -> s -> rという関数と等価になる。これはs -> aという関数を継続渡しスタイルにしたものだ。したがって、Getter s as -> aと同じものとして見てよい。

view :: ((a -> Const a a) -> s -> Const a s) -> s -> a
view l = getConst . l Const

Setter

FoldやGetterと独立した概念としてSetterがある。ASetterSetter型の特殊な場合であるが、本質的な差はない。

type ASetter s t a b = (a -> Identity b) -> s -> Identity t

over :: ASetter s t a b -> (a -> b) -> s -> t
over l f = runIdentity . l (Identity . f)

こちらは(a -> b) -> (s -> t)と等価だ。セッターとして使えるが、セットした際の型の変化を許すようになっている。

Traversal

FoldがfoldMapの一般化だったのに対し、TraversalはTraversableクラスのメソッドtraverse :: (Traversable t, Applicative f) => (a -> f b) -> t a -> f (t b)の一般化になっている。

type Traversal s t a b = forall f. Applicative f => (a -> f b) -> s -> f t

そのため、traversemapMの代わりを作る感覚でTraversalを定義できる。例えば、リストの偶数番目の要素に対するtraverseEvenは以下のようになる。

traverseEven :: Traversal [a] [a] a a
traverseEven f (x:y:xs) = (:) <$> f x <*> fmap (y:) (elementsEven f xs)
traverseEven _ xs = pure xs

TraversalFoldであり、Setterでもあるのが特に重要である。例えばover elementsEven (*3)は偶数番目の要素を3倍にするし、sumOf elementsEvenは偶数番目の要素の合計を求める関数になる。

Lens

Traversal複数の要素を扱うのに対し、Lensと呼ばれる構造はただ1つの対象しか扱わない。

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

LensTraversalでもあり、SetterかつGetterである。レコードのフィールドの表現に使えるだけあって人気も高く、パッケージ名になっているほどである。

Lenstraverseと似たような感覚で作れる。Traversalは0個や複数の要素を扱うのにpure<*>を使うが、要素が1つならばfmapだけでよいというのはわかりやすい。

Review

ReviewはGetterを逆転させたものだ。回りくどいが、Review t bb -> tという関数と等価である。

type AReview t b = Tagged b (Identity b) -> Tagged t (Identity t)

(#) :: AReview t b -> b -> t
(#) l = runIdentity . unTagged . l . Tagged . Identity

Prism

type Prism s t a b = forall p f. (Choice p, Applicative f) => p a (f b) -> p s (f t)

PrismTraversalかつReviewである。これは作りにくいので、自分で定義する際はprism :: (b -> t) -> (s -> Either t a) -> Prism s t a bという関数を使おう。

_Just :: Prism (Maybe a) (Maybe b) a b

_JustMaybeに対するtraverseとしての機能を持つが、Reviewでもあるため、_Just # 42からJust 42を得るということができる。代数的データ型のコンストラクタと似たような雰囲気で、値を埋め込んだり取り出したりできる。

Iso

type Iso s t a b = forall p f. (Profunctor p, Functor f) => p a (f b) -> p s (f t)

IsoLensかつPrismであり、こちらは要素が必ず1つあることが保証されている。したがって同型な二つのデータ型にならIsoを定義できる。こちらはヘルパー関数なしでも作りやすい。

enum :: Enum a => Iso Int Int a a
enum = dimap toEnum (fmap fromEnum)

Equality

type Equality s t a b = forall p f. p a (f b) -> p s (f t)

EqualityIsoよりさらに強い。Equalityになる値はidただ一つしかないため、型の等価性を表現する。等価性を採取して他の場所に運びたいときに使えるが、その目的ならbaseパッケージのData.Type.Equalityがあるため、使う場面はかなり少ない。

まとめ

構造を「使いたい」と思ったときは弱いほうから、「作りたい」と思ったら強いほうから、対応するモジュールを調べてみよう。この力の関係を知っていれば、よりよくlensを活用できるに違いない。