Lensメモ

ここ最近、少し思うところあってHaskellのLensについて勉強していた。

Lensに関するドキュメントをいろいろ探したが、このドキュメントが非常に分かりやすかった。

"Real World Haskell"の共著者であるBryan O'SullivanによるStanford大の講義スライドらしい。Zipperから話を始めてLensの説明をしている。ZipperについてはすごいH本にも解説があるので、Zipperをある程度理解していれば比較的カンタンに理解できる内容だろう。

ただ、講義スライドの(41)〜(47)あたりはこれだけでは少々わかりにくいと思った。ここはZipperからLensに話が移行するところであり、Lensを使ってview関数とover関数を再定義している。

Lensライブラリ

HaskellにおけるLensの実装はいくつか存在する。

lensパッケージは非常に強力だが、その分読み解くのは困難である。一方、lens-family-coreパッケージはコンパクトであり、講義スライドでも教材として勧めている。

Lensの型

Lensは以下のデータ型を持つ。

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

右側のカッコはHaskellでは省略可能だが、あった方が理解しやすい。それぞれの型変数は以下のような意味を持つ。

  • a = 変換前の部分データ
  • b = 変換後の部分データ
  • s = 変換前の全体データ
  • t = 変換後の全体データ

とりあえずFunctor fを無視すれば、Lensとは「そのLensが注目する部分データ(part)に対する変換関数」から「その部分データを含む全体データ(whole)に対する変換関数」を作る関数である。

ここで言う「部分」と「全体」とは相対的な意味である。2つのLensの関数合成l1 . l2では、l2の「全体」はl1にとっての「部分」となる。このように、関数合成によって複雑なデータ構造の特定の部分に注目できるのがLensの強みである。

Constantファンクター

Lensは「変換関数の変換関数」なので、Lensからデータ構造へのsetter関数を作るのは比較的分かりやすい。fとして「何もしない」ファンクターであるIdentityファンクターを使えばいい。

一方、全体データから部分データを取ってくるgetterは、fとしてConstantファンクターをLensに適用することで作ることができる。

このConstantファンクターというのは初めて知ったが、なかなか変態的な動作をするファンクターである。

newtype Constant a b = Constant { getConstant :: a }

instance Functor (Constant a) where
    fmap _ (Constant x) = Constant x

instance (Monoid a) => Applicative (Constant a) where
    pure _ = Constant mempty
    Constant x <*> Constant y = Constant (x `mappend` y)

通常、fmap fは関数fをファンクターの内部のデータに適用するものだが、Constantファンクターはこれをガン無視する。これで本当にファンクター則が成立するのかと思ったが、たしかに成立するようだ。

Applicativeのインスタンス定義もかなり変態的である。通常、pure vは値vをそのままファンクターに包むが、Constantファンクターはやはりこれをガン無視し、しれっとmempty関数を呼び出している。なお、ConstantをApplicativeとして使う場合、その中に持つ値はMonoidでないといけない。

この変態的なファンクターをLensに組み合わせることで、Lensからgetterを作ることができる。

PrismとTraversal

lensパッケージにはLensの他にもいくつかデータ型が定義されている。この中にPrismとTraversalという型がある。

これについてはこちらのチュートリアルが分かりやすい。

Lensは全体データの内部にある1つの部分データに注目する。しかし、データ構造によっては注目したいデータが常に存在するとは限らない。また、全体データの中の複数の部分に注目したい場合もある。PrismとTraversalはこのような時に使う。

  • Prismは0個または1個の部分データに注目する。
  • Traversalは0個以上の任意の数の部分データに注目する。

なお、Prismはどちらかというと「データの場合分けの1つのケースに注目する」という機能なようだ。

例えば、Either型はこのような定義になっている。

data Either a b = Left a | Right b

これに対し、lensパッケージにはそれぞれの場合に注目するPrismが存在する。

_Left :: Prism (Either a c) (Either b c) a b 
_Right :: Prism (Either c a) (Either c b) a b

Either型はLeftとRightのどちらか片方のみを持ち、両方を持つわけではない。従って、対象データがLeftな場合に_Right Prismを使うと注目部分データは0個になる。この場合、getterはmemptyモノイドを返し、setterは何も効果を及ぼさない。

Prismが注目するのは「場合」であって、実際に存在するデータは1つである。そのため、Prismの場合は「部分」から「全体」を作るという、逆の操作も可能らしい。

>>> 5 ^. re _Left
Left 5

このへん、代数データ型のあるHaskellのような言語でないとあまり意味のない概念かもしれない。

Traversalは0個以上の任意の数の部分データに注目する。通常のgetter(^.)をTraversalに対して使う場合、注目する部分データはMonoidでなければならず、それらがmappendで結合された結果が返される。これはConstantファンクターの型制約と動作による。