Just $ A sandbox

プログラミングと計算機科学とかわいさ

Lensで行こう!

続編:Lensで行こう!(2):Isoへの拡張 - みょんさんの。

Lensとは

Lens(http://hackage.haskell.org/package/lens-3.7.1.2)というパッケージがあります。
非常に大きなパッケージで、中には非常に便利な函数群がたくさん揃っています。

私が調べた限りでは、 "Lens package in Haskell(HaskellのLensパッケージ)" の解説記事はData.Lens(http://hackage.haskell.org/package/lenses-0.1.6)がやたらとヒットするのですが、こちらよりもControl.Lensとその周りのパッケージ群(この段落の最初にリンクが貼ってある方です)の方が色々と便利でたくさんの函数が揃っているので、ぜひConrol.Lensの方を使ってください*1

さて、Lensって何が出来るの?というところから、この巨大なパッケージを探って行きましょう。

タプルにアクセス

簡単な例から始めましょう。
例えばタプルのgetter,setterのようなものが欲しくなったとします。
そこで以下のようなview,set,_1,_2という函数が定義されているとします(ここではview,setを定義するためにLens'という型を使っています)。

get :: Lens' a b -> a -> b
set :: Lens' a b -> b -> a -> a

_1 :: Lens' (a,b) a
_2 :: Lens' (a,b) b

-- すると

>>> view _1 ("hello", "world")
"hello"
>>> set _2 "hi" ("hello", "world")
("hello","hi")

上のように動いてくれると嬉しい、という話なのですが、Control.Lensでは素晴らしいことにこれがすでに定義されています*2
また、他にもいくつか便利な函数があるので実行結果を併せて以下に示します。

>>> import Control.Lens
>>> view _1 ("hello", "world")
"hello"
>>> set _2 "hi" ("hello", "world")
("hello","hi")

>>> ("hello", "world")^._1 -- a ^.b == view b a
"hello"
>>> _2 .~ "hi" $ ("hello", "world") -- a.~b $ c == set a b
("hello","hi")
>>> ("hello",("world","!!!"))^.(_2._1) -- _1や_2を合成することも可能
"world"
>>> 7 ^. (to (+2)) -- to函数を使えば(a->b)のような函数をgetterのようにしたりも
9
>>> ("a","b","c") & _3 %~ (++ ("d")) -- (%~)函数を使って特定の値に函数を適用させることも可能
("a","b","cd")

上に紹介したような、タプルへのアクセスや値の更新の際に便利な函数はこのへん(Control.Lens.Getter)とかこのへん(Control.Lens.Setter)を見ればよいかと思います。

フィールドへのアクセス

あとはデータ型のフィールドへのアクセスを簡単にするという使い方もよくされます。

{-# LANGUAGE TemplateHaskell #-}
data Foo a = Foo { _bar :: Int, _baz :: Int, _quux :: a }
$(makeLenses ''Foo)

-- 上のように宣言してやれば、
-- bar, baz :: Simple Lens (Foo a) Int
-- quux :: Lens (Foo a) (Foo b) a b
-- が自動生成されます

-- あとはFoo型のfooを定義して
-- foo ^. bar で値を取得したり
-- baz .~ 42 $ foo で値を更新したりできます

さて、Lensはどういうデータに対して使えるのでしょうか。これを知るためにはLensの内部実装と型について少し考えなくてはいけません。
以下ではこの辺のことを考えていきます。

Lens型,Getting型,Setting型

Lens型について考えましょう。Control.Lens.Typeを見ると

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

と書かれています。
何やら複雑そうですが、今の段階では分からなくて構いません。Lens型よりも、とにかくレンズを作るにあたって一番重要なview,set,(^.),(.~)について型を見てみましょう。

type Getting r s t a b = (a -> Accessor r b) -> s -> Accessor r t
type Setting s t a b = (a -> Mutator b) -> s -> Mutator t

view :: MonadReader s m => Getting a s t a b -> m a
view l = Reader.asks (runAccessor# (l Accessor))

set :: Setting s t a b -> b -> s -> t
set l b = runMutator# (l (\_ -> Mutator b))

(^.) :: s -> Getting a s t a b -> a
s ^. l = getConst (l Const s)

(.~) :: Setting s t a b -> b -> s -> t
l .~ b = runIdentity (l (\_ -> Identity b))

-- newtype Const a b = Const { getConst :: a }
-- newtype Identity a = Identity { runIdentity :: a }

…さらにややこしい?まぁあまり身構えないでください。全部をわかろうとはしなくても良いと思います。

ちなみに、GettingとSettingに出てくるAccessorとMutatorという見慣れない型クラスは、どちらもエラーメッセージを上手く処理するためについている機構で、AccessorはConstファンクター(Control.Applicative)の代わり、MutatorはIdentityファンクター(Data.Functor.Identity)の代わりです。
よってAccessorは2つの型のうち後ろを無視する、Mutatorはそれをそのまま使うファンクターだと思えば良いことになります。

Getting型はConstファンクターを作る函数(a -> Const r b)と別の型sからConstファンクター値(Const r t)を作る函数です。
Setting型はIdentityファンクターを作る函数(a -> Identity b)と別の型sからIdentityファンクター値(Identity t)を作ります。
viewはGetting(Getting a s t a b == (a -> Const a b) -> s -> Const a t)を受け取ってReaderモナド(読み取り専用モナド; MonadReader s m => m a)を返します(つまりReaderモナドを作ってそこに値を保持させます)。
setはSetting(Setting s t a b == (a -> Identity b) -> s -> Identity t)と型をいくつか(b,s)受け取って、Identityファンクターで保持されていた値(t)を返します。

なんとなくイメージできてきたでしょうか?
さて、あとはこれを組み合わせましょう!

l :: (a -> Const a b) -> s -> Const a t
---------
(^.) :: s -> Getting a s t a b -> a
s ^. l = getConst (l Const s)
       = (\(Const a b) -> a) (Const a t)
       = a

l :: (a -> Identity b) -> s -> Identity t
---------
(.~) :: Setting s t a b -> b -> s -> t
l .~ b $ s = runIdentity (l (\_ -> Identity b)) s
           = (\(Identity t) -> t) (\s -> Identity t) s
           = (\s -> t) s
           = t

(^.)の方はgetterそのものになっているのが分かりますね!*3
(.~)の方は、(\s -> t)の部分で与えられたsを任意のtに変換できるので上手くsetterとして働くことができます。

これで(^.),(.~)が正しくgetter,setterの役割を果たしてくれること、そしてGetting,Settingが上手く定義できればデータを選ばずにこれらを使えることが分かりました
いつでも使えるというのはとても嬉しいですね。

さて、GettingとSettingはAccessorとMutatorという型クラスを利用していましたが、これらはどちらもファンクターでした。
つまり、(Accessor r)とMutatorはどちらもFunctorのインスタンスになっています。

type Getting r s t a b = (a -> Accessor r b) -> s -> Accessor r t
type Setting s t a b = (a -> Mutator b) -> s -> Mutator t
-------- ↓ --------
type Getting' s t a b = (a -> (Accessor r) b) -> s -> (Accessor r) t
type Setting s t a b = (a -> Mutator b) -> s -> Mutator t
-------- ↓ --------
type Lens s t a b = forall f. Functor f => (a -> f b) -> s -> f t

そうです、GettingとSettingはどちらもLensの特殊な形に過ぎません!

この記事の最初にタプル、そして次に少しだけ例としてフィールドへのアクセスの例を示しました。
それらがControl.Lensをインポートするだけで上手く行ったのは(上手く行っているように見えたのは)全てはLens型として上手く振る舞えるように(^.)や(.~)や_1といった函数群が定義されているからです。

もうお分かりかと思いますが、最後にLensの型をもう一度確認しましょう

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

Lensは型aからfのファンクター値を作る函数(a -> f b)と型sを受け取って合成し、fのファンクター値(f t)を合成できる型です!
このレンズはもらった値の一部を反射させるレンズとしても、もらった値を一部更新して反射させるレンズとしても使えるスグレモノなわけです。

作り方も簡単、Functorを用意するだけです!あとはLens型からGettingとSettingが作れるので、これを使ってgetterとsetterを用意してあげれば自分で定義した型に対してもgetterとsetterを使えるようになります。

とても便利ですね!

まとめ

今回は(おそらくLensライブラリで最初に触れるだろう、そして誰にとっても分かりやすいであろう)Getting,Setting,Lensの型周りのことを調べてまとめさせて頂きました。
しかし、おそらくLensパッケージの力はこれだけでは無いです。
MonadicFold, Fold, Action, Traversal, Prism, Isoなどについては今回は一切触れていませんが、むしろIsoやPrismといった部分のほうがLensパッケージの本質的な部分とも言えるかもしれません。
とにかくよく分からなければ使ってみてください。最初の方に例として示したことを理解して、その便利さを実感すれば、きっともっとLensについて興味が湧くと思います。

Happy Haskell Programming with Lens package!

参考文献

  1. http://hackage.haskell.org/package/lens-3.7.1.2
  2. Haskell for all: Lenses Data.Lensesの説明がされていますがこちらは今回扱っているControl.Lensとは別物なので注意してください
  3. The Comonad.Reader » Mirrored Lenses

追記:2012/12/28

パッケージ名はLensesではなくてLensだというご指摘を頂いたので修正しました。

*1:http://stackoverflow.com/questions/13282874/data-lens-or-control-lens

*2:余談ですが、あるデータ型に対してそれの1番目や2番目のデータだけを抜き出して(一部だけ反射させて)考えたりするところがなんとなく、レンズのような気がしませんか?

*3:viewについては省略しますがReaderモナドに値を保持させているだけで一緒です