LensちゃんマジLens

なんとゆーか、もともと手元にあるものを使ってちくちく何かをするのが好きなので、有名なライブラリとか大きいパッケージとか見ても尻込みしてしまってなかなか手を付けられないタイプなのですが、ここへ来て色々後回しにしてたツケが回ってきた感あります。

はい、そんな感じでここ数日、ようやくcabalの洗礼を受ける事ができました、ちゅーんさんです。
んで、仕事の間を縫ってゲーム作ったりなんかも、カタツムリの歩みで進めていたりもするのですが、急遽Lensを理解する必要がでてきたので今日はLensの導入やります。

あ、あとYesodを覚えようと思ったのですが、どうもこのライブラリ、一通り使えるようになるまでが修羅の道らしく、髪の毛を掻きむしりながらcabalと格闘してやっとインストールができたと思ったら今度はyesod develで詰んだり*1してます。
そんなこんなで、今日はLensの導入やりますってば。

てば。


早速Lensちゃんをインストールしてきました。
そもそもcabalのバージョン低いままずっと使ってたので、upgradeするのにも一苦労だったのですが、一旦それが済めば...

cabal install lens

で一発ですね。らくちんらくちん。

わりとでっかいライブラリだからコンパイルに時間かかりますので、コーヒーでも飲みながら待ちましょう。
ちなみにブログ主はほうじ茶飲みながらスーパーマリオ3Dワールドやってました。思いの外ガチゲーですね、これ。

で、先述のように、でっかいライブラリなので、一回のエントリで本質に迫ったり細部まで網羅したりとかどー考えても無理なので、基本的な機能を使って色々遊んでみるのに留めましょう。


とりあえず、Lensを導入する事によって一番嬉しいのは、任意のデータ構造に対して「可読性が高く」「片安全」なアクセサを得る事ができる点かと思われます。

まず、チュートリアルでいきなり出てくる次のサンプルコードから。

Prelude Control.Lens> ("Hoge", "Piyo", "Huga", "Hogera")^._1
"Hoge"
Prelude Control.Lens> ("Hoge", "Piyo", "Huga", "Hogera")^._2
"Piyo"
Prelude Control.Lens> ("Hoge", "Piyo", "Huga", "Hogera")^._3
"Huga"
Prelude Control.Lens> ("Hoge", "Piyo", "Huga", "Hogera")^._4

こんな感じで、タプルに添字でアクセスできるように(見えるように)なります。
このサンプル、アレです。確かに「ふぉぉ、すげぇ!」ってなるんですけど、パッと見何処で区切って読んだら良いかわかりません。
もちろん、(^.)が演算子で、`_x`は関数名ですね。はい。

Prelude Control.Lens> :i (^.)
(^.) :: s -> Getting a s t a b -> a
  	-- Defined in `Control.Lens.Getter'
infixl 8 ^.
Prelude Control.Lens> :i _1
class Field1 s t a b | s -> a, t -> b, s b -> t, t a -> s where
  _1 :: (Indexable Int p, Functor f) => p a (f b) -> s -> f t
  	-- Defined in `Control.Lens.Tuple'


とりあえず、型定義がカオスなので、中身の理解は一旦置いておきましょう。
例えば、「タプルの中のタプルの中のタプル中の要素にアクセスしたい!」って時とか、関数合成(.)と組み合わせて次のように書くとすごく読み易いです。

Prelude Control.Lens> (1, ('a', 'b', ("Hoge", "Piyo")), 2)^._2._3._1
"Hoge"

この仕組は、関数合成した結果の型を見れば一目瞭然...

Prelude Control.Lens> :t _2._3._1
_2._3._1
  :: (Functor f, Field3 s1 t1 s2 t2, Field2 s t s1 t1,
      Field1 s2 t2 a b, Indexable Int p) =>
     p a (f b) -> s -> f t

では無いですねw

まぁ、合成する前の型と同じ型になっているので、結果として同じ動きをしているのは納得できます。

Prelude Control.Lens> :t _1
_1
  :: (Functor f, Field1 s t a b, Indexable Int p) =>
     p a (f b) -> s -> f t
Prelude Control.Lens> :t _1._2
_1._2
  :: (Functor f, Field2 s1 t1 a b, Field1 s t s1 t1,
      Indexable Int p) =>
     p a (f b) -> s -> f t
Prelude Control.Lens> :t _1._2._3
_1._2._3
  :: (Functor f, Field3 s2 t2 a b, Field2 s1 t1 s2 t2,
      Field1 s t s1 t1, Indexable Int p) =>
     p a (f b) -> s -> f t

なになに?「タプルの中のタプルの中のタプル中の要素に関数を適用した結果が欲しい!」ですって?
よかろう、ならばto関数を使い給え。

Prelude Control.Lens> (1, ('a', 'b', ("Hoge", "Piyo")), 2)^._2._3._1.to length
4
Prelude Control.Lens> (1, ('a', 'b', ("Hoge", "Piyo")), 2)^._2._3._1.to (map Data.Char.toUpper)
"HOGE"

で、今度は「タプルの中のタプルの中のタプル中の要素を書き換えたい!」みたいな時に活躍するのが(.~)演算子であります。

Prelude Control.Lens> :t (.~)
(.~) :: ASetter s t a b -> b -> s -> t

この演算子は左辺のアクセッサ関数で指定された場所を右辺の値で書きかえる関数を返す高階関数になってますので、次のようにして任意のフィールドの値を書きかえる事ができます。

Prelude Control.Lens> _2._3._1 .~ 99999 $ (1, ('a', 'b', ("Hoge", "Piyo")), 2)
(1,('a','b',(99999,"Piyo")),2)

やりようではありますが、同じ事を従来の方法でやろうとするとけっこう大変です。

Prelude Control.Lens> (\(a1, (b1, b2, (_, c2)), a3) -> (a1, (b1, b2, (99999, c2) , a3))) (1, ('a', 'b', ("Hoge", "Piyo")), 2)
(1,('a','b',(99999,"Piyo"),2))

と、いっちゃんベタな方法で書きましたが、見通し悪い上、一時変数に塗れててとっても格好悪いです、これ。
さぁ、Lensのパワーがだんだん見えてきました。


といって、まるでLensちゃんがタプルを便利に使うための機能みたいな扱いになってしまうとあまりにも可哀想なので、再度(^.)と(.~)の型定義を見てみましょう。

Prelude Control.Lens> :t (^.)
(^.) :: s -> Getting a s t a b -> a
Prelude Control.Lens> :t (.~)
(.~) :: ASetter s t a b -> b -> s -> t

型引数が多くて細部の憶測は立てづらいですが、各々GettingとかASetterという型を引数に乗っているのが確認できます。

Prelude Control.Lens> :i Getting
type Getting r s t a b = (a -> Accessor r b) -> s -> Accessor r t
  	-- Defined in `Control.Lens.Getter'
Prelude Control.Lens> :i ASetter
type ASetter s t a b = (a -> Mutator b) -> s -> Mutator t
  	-- Defined in `Control.Lens.Setter'

それぞれ、AccessorとMutatorという型を引数に取る関数の型の別名である事が解ります。

Prelude Control.Lens> :i Accessor
newtype Accessor r a = Accessor {runAccessor :: r}
  	-- Defined in `Control.Lens.Internal.Getter'
instance Functor (Accessor r)
  -- Defined in `Control.Lens.Internal.Getter'
instance Gettable (Accessor r)
  -- Defined in `Control.Lens.Internal.Getter'
Prelude Control.Lens> :i Mutator
newtype Mutator a
  = Control.Lens.Internal.Setter.Mutator {Control.Lens.Internal.Setter.runMutator :: a}
  	-- Defined in `Control.Lens.Internal.Setter'
instance Monad Mutator -- Defined in `Control.Lens.Internal.Setter'
instance Functor Mutator
  -- Defined in `Control.Lens.Internal.Setter'
instance Traversable Mutator
  -- Defined in `Control.Lens.Internal.Setter'
instance Settable Mutator
  -- Defined in `Control.Lens.Internal.Setter'
Prelude Control.Lens> 

Monad、Functorは説明不要として、ここでTraversable、Gettable、Settableという型クラスが出てきました。任意の型がこれらの型クラスのインスタンスとなっていれば、同じようにして簡単に任意のフィールドへアクセスできるようになるという事ですね。この型クラスを理解する事がLensの理解に繋がりそうです。

あ、あと、使うだけなら大した問題では無いのでここまで触れていなかったのですが、(^.)と(.~)はそれぞれ、view関数と、set関数のaliesになってます。

Prelude Control.Lens> view _2 ('a', 'b', 'c')
'b'
Prelude Control.Lens> set _2 "Hoge" $ ('a', 'b', 'c')
('a',"Hoge",'c')


んでっ!
「俺の作ったデータ型もLensで格好良くアクセスできるようにしてーけど、型定義とかややこしいし面倒くさいんじゃねーの?」というあなた!

ご安心下さい、任意の型を超楽チンにLensのインスタンスにする必殺技*2が予め用意されているのです!

というのが以下のサンプルコードで・・・

{-# LANGUAGE TemplateHaskell, MultiParamTypeClasses #-}
{-# LANGUAGE FunctionalDependencies, FlexibleInstances #-}

module Main where
import Control.Lens

data Hoge a = Hoge {
  _foo :: a,
  _bar :: Int
  } deriving (Show, Eq)

makeLenses ''Hoge

なんかゴチャゴチャとGHC拡張付けないとコンパイル通らないのが気持ち悪いですが、純粋なHaskellだとちょっと非力なので我慢我慢。
こんだけの定義でHoge型に対してviewやらsetやら使えるようになるんだから安いもんです。

"makeLenses ''型名" っていう記述がミソみたいですね。
どうやらこの一行を入れてやると、任意の型の各フィールドに対するアクセサ関数がコンパイル時に自動で生成されるっぽいです。
【15:21 修正】もともとmakeClassyを使っていましたが、こちらは型クラスも合わせて生成する構文らしく、反面一部の記述が制限されるので、makeLensesに差し替えました

実際にghciで使ってみたのが以下になります。

*Main> :t foo
foo :: Functor f => (a0 -> f a1) -> Hoge a0 -> f (Hoge a1)
*Main> :t bar
bar :: Functor f => (Int -> f Int) -> Hoge a0 -> f (Hoge a0)
*Main> (Hoge "Foo" 1)^.foo
"Foo"
*Main> (Hoge "Foo" 1)^.bar
1
*Main> ("Nyan" ,(Hoge "Foo" 1))^._2.foo
"Foo"
*Main> (Hoge (Hoge "Foo" 999) 10)^.foo.bar
999
*Main> _2.foo .~ "Bar" $ ("Nyan" ,(Hoge "Foo" 1))
("Nyan",Hoge {_foo = "Bar", _bar = 1})

やだ何コレ面白い


そんなワケで、可愛い可愛いLensちゃん*3のお話でした。

凄いは凄いんですけどどーしてこうなるのかちょっと外見だけだと検討付かないですねw
という疑問の良い解説になりそうな文章がWikiにありました。

https://github.com/ekmett/lens/wiki/Derivation(英)

近日中にコレを読んだら、スライド作りに励もうかと思います。でわでわノシノシ。



【15:35 追記】

(.~)を使ったSetterですが、flip ($)と外延的等価な演算子(&)がControl.Lens.Combinatorsで定義されていまして・・・

*Main> :t flip ($)
flip ($) :: b -> (b -> c) -> c
*Main> :t (&)
(&) :: a -> (a -> b) -> b

それを使うと、OOの代入文みたいな順序で記述できます。
こっちのほうが読みやすくて良いですね。

*Main> ((1, 2, 3), "Hoge", "Piyo")&_1._2 .~ True
((1,True,3),"Hoge","Piyo")

*1:cabal: Cannot find the program 'ghc' at 'yesod-ghc-wrapper' or on the path・・・ゲフゥ・・・そんなぁ、ちゃんとPATH切ってあるのにー!!

*2:TemplateHaskellですね、ハイ

*3:ちょっとおデブなのが気になりますが!