Lensメモ
ここ最近、少し思うところあってHaskellのLensについて勉強していた。
Lensに関するドキュメントをいろいろ探したが、このドキュメントが非常に分かりやすかった。
- Zippers and lenses: http://www.scs.stanford.edu/14sp-cs240h/slides/lenses-slides.html#%281%29
"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ファンクターというのは初めて知ったが、なかなか変態的な動作をするファンクターである。
- Data.Functor.Constant: http://hackage.haskell.org/package/transformers/docs/Data-Functor-Constant.html
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という型がある。
これについてはこちらのチュートリアルが分かりやすい。
- A Little Lens Starter Tutorial: https://www.fpcomplete.com/school/to-infinity-and-beyond/pick-of-the-week/a-little-lens-starter-tutorial
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ファンクターの型制約と動作による。