独習 Scalaz: 11日目

更新された html5版があるので、よろしくお願いします。

Darren Hester for openphoto.net

昨日はコンフィギュレーションを抽象化する方法として Reader をみた後、モナド変換子を紹介した。

今日はレンズを見てみよう。色んな人がレンズの話をして盛り上がってきてるトピックだし、使われるシナリオもはっきりしてるみたいだ。

進め! 亀

今年の Scalathon で Seth Tisue さん (@SethTisue)shapeless の Lens の話をした。残念ながら僕は聞けなかったけど、使われている例は借りさせてもらう。

scala> case class Point(x: Double, y: Double)
defined class Point
 
scala> case class Color(r: Byte, g: Byte, b: Byte)
defined class Color
 
scala> case class Turtle(
         position: Point,
         heading: Double,
         color: Color)
 
scala> Turtle(Point(2.0, 3.0), 0.0,
         Color(255.toByte, 255.toByte, 255.toByte))
res0: Turtle = Turtle(Point(2.0,3.0),0.0,Color(-1,-1,-1))

ここで不変性を壊さずに亀を前進させたい。

scala> case class Turtle(position: Point, heading: Double, color: Color) {
         def forward(dist: Double): Turtle =
           copy(position =
             position.copy(
               x = position.x + dist * math.cos(heading),
               y = position.y + dist * math.sin(heading)
           ))
       }
defined class Turtle
 
scala> Turtle(Point(2.0, 3.0), 0.0,
         Color(255.toByte, 255.toByte, 255.toByte))
res10: Turtle = Turtle(Point(2.0,3.0),0.0,Color(-1,-1,-1))
 
scala> res10.forward(10)
res11: Turtle = Turtle(Point(12.0,3.0),0.0,Color(-1,-1,-1))

中に入ったデータ構造を更新するには入れ子で copy を呼ばなくてはいけない。Seth 氏の話からまた借りると:

// 命令型
a.b.c.d.e += 1
 
// 関数型
a.copy(
  b = a.b.copy(
    c = a.b.c.copy(
      d = a.b.c.d.copy(
        e = a.b.c.d.e + 1
))))

この余計な copy の呼び出しを何とかしたい。

Lens

Scalaz 7 の Lens をみてみる:

  type Lens[A, B] = LensT[Id, A, B]
 
  object Lens extends LensTFunctions with LensTInstances {
    def apply[A, B](r: A => Store[B, A]): Lens[A, B] =
      lens(r)
  }

他の多くの型クラス同様 LensLensT[Id, A, B] の型エイリアスだ。

LensT

LensT はこうなっている:

import StoreT._
import Id._
 
sealed trait LensT[F[+_], A, B] {
  def run(a: A): F[Store[B, A]]
  def apply(a: A): F[Store[B, A]] = run(a)
  ...
}
 
object LensT extends LensTFunctions with LensTInstances {
  def apply[F[+_], A, B](r: A => F[Store[B, A]]): LensT[F, A, B] =
    lensT(r)
}
 
trait LensTFunctions {
  import StoreT._
 
  def lensT[F[+_], A, B](r: A => F[Store[B, A]]): LensT[F, A, B] = new LensT[F, A, B] {
    def run(a: A): F[Store[B, A]] = r(a)
  }
 
  def lensgT[F[+_], A, B](set: A => F[B => A], get: A => F[B])(implicit M: Bind[F]): LensT[F, A, B] =
    lensT(a => M(set(a), get(a))(Store(_, _)))
  def lensg[A, B](set: A => B => A, get: A => B): Lens[A, B] =
    lensgT[Id, A, B](set, get)
  def lensu[A, B](set: (A, B) => A, get: A => B): Lens[A, B] =
    lensg(set.curried, get)
  ...
}

Store

Store って何だろう?

  type Store[A, B] = StoreT[Id, A, B]
  // flipped
  type |-->[A, B] = Store[B, A]
  object Store {
    def apply[A, B](f: A => B, a: A): Store[A, B] = StoreT.store(a)(f)
  }

とりあえず setter (A => B => A) と getter (A => B) のラッパーらしい。

Lens を使う

turtlePositionpointX を定義してみよう:

scala> val turtlePosition = Lens.lensu[Turtle, Point] (
         (a, value) => a.copy(position = value),
         _.position
       )
turtlePosition: scalaz.Lens[Turtle,Point] = scalaz.LensTFunctions$$anon$5@421dc8c8
 
scala> val pointX = Lens.lensu[Point, Double] (
         (a, value) => a.copy(x = value),
         _.x
       )
pointX: scalaz.Lens[Point,Double] = scalaz.LensTFunctions$$anon$5@30d31cf9

次に Lens で導入される演算子を利用することができる。Kleisli でみたモナディック関数の合成同様に、LensTcompose (シンボルを使ったエイリアスは <=<) と andThen (シンボルを使ったエイリアスは >=>) を実装する。個人的には >=> の見た目が良いと思うので、これを使って turtleX を定義する:

scala> val turtleX = turtlePosition >=> pointX
turtleX: scalaz.LensT[scalaz.Id.Id,Turtle,Double] = scalaz.LensTFunctions$$anon$5@11b35365

Turtle から Double に向かっているわけだから、型は理にかなっている。get メソッドを使って値を取得できる:

scala> val t0 = Turtle(Point(2.0, 3.0), 0.0,
                  Color(255.toByte, 255.toByte, 255.toByte))
t0: Turtle = Turtle(Point(2.0,3.0),0.0,Color(-1,-1,-1))
 
scala> turtleX.get(t0)
res16: scalaz.Id.Id[Double] = 2.0

成功だ! set メソッドを使って新たな値を設定すると新たな Turtle が返ってくる:

scala> turtleX.set(t0, 5.0)
res17: scalaz.Id.Id[Turtle] = Turtle(Point(5.0,3.0),0.0,Color(-1,-1,-1))

これもうまくいった。値を get して、なんらかの関数に適用した後、結果を set したい場合はどうすればいいだろう? mod がそれを行う:

scala> turtleX.mod(_ + 1.0, t0)
res19: scalaz.Id.Id[Turtle] = Turtle(Point(3.0,3.0),0.0,Color(-1,-1,-1))

mod のシンボルを使ったカリー化版として =>= がある。これは Turtle => Turtle 関数を生成する:

scala> val incX = turtleX =>= {_ + 1.0}
incX: Turtle => scalaz.Id.Id[Turtle] = <function1>
 
scala> incX(t0)
res26: scalaz.Id.Id[Turtle] = Turtle(Point(3.0,3.0),0.0,Color(-1,-1,-1))

これで内部値の変化を事前に記述して、最後に実際の値を渡すことができた。これは何かに似てない?

State モナドとしての Lens

これは状態遷移だと思う。実際、LensState は両方とも不変データ構造を使いながら命令形プログラミングを真似ているし相性がいいと思う。incX をこう書くこともできる:

scala> val incX = for {
         x <- turtleX %= {_ + 1.0}
       } yield x
incX: scalaz.StateT[scalaz.Id.Id,Turtle,Double] = scalaz.StateT$$anon$7@38e61ffa
 
scala> incX(t0)
res28: (Turtle, Double) = (Turtle(Point(3.0,3.0),0.0,Color(-1,-1,-1)),3.0)

%= メソッドは Double => Double 関数を受け取って、その変化を表す State モナドを返す。

turtleHeadingturtleY も作ろう:

scala> val turtleHeading = Lens.lensu[Turtle, Double] (
         (a, value) => a.copy(heading = value),
         _.heading
       )
turtleHeading: scalaz.Lens[Turtle,Double] = scalaz.LensTFunctions$$anon$5@44fdec57
 
scala> val pointY = Lens.lensu[Point, Double] (
         (a, value) => a.copy(y = value),
         _.y
       )
pointY: scalaz.Lens[Point,Double] = scalaz.LensTFunctions$$anon$5@ddede8c
 
scala> val turtleY = turtlePosition >=> pointY

これはボイラープレートっぽいので嬉しくない。だけど、これで亀を前進できる! 一般的な %= の代わりに、Scalaz は Numeric な Lens に対して += などの糖衣構文も提供する。具体例で説明する:

scala> def forward(dist: Double) = for {
         heading <- turtleHeading
         x <- turtleX += dist * math.cos(heading)
         y <- turtleY += dist * math.sin(heading)
       } yield (x, y)
forward: (dist: Double)scalaz.StateT[scalaz.Id.Id,Turtle,(Double, Double)]
 
scala> forward(10.0)(t0)
res31: (Turtle, (Double, Double)) = (Turtle(Point(12.0,3.0),0.0,Color(-1,-1,-1)),(12.0,3.0))
 
scala> forward(10.0) exec (t0)
res32: scalaz.Id.Id[Turtle] = Turtle(Point(12.0,3.0),0.0,Color(-1,-1,-1))

これで copy(position = ...) を一回も使わずに forward 関数を実装できた。これは便利だけど、ここまで来るのに準備も色々必要だったから、それはトレードオフだと言える。Lens は他にも多くのメソッドを定義するけど以上で十分使い始められると思う。並べて見てみる:

sealed trait LensT[F[+_], A, B] {
  def get(a: A)(implicit F: Functor[F]): F[B] =
    F.map(run(a))(_.pos)
  def set(a: A, b: B)(implicit F: Functor[F]): F[A] =
    F.map(run(a))(_.put(b))
  /** Modify the value viewed through the lens */
  def mod(f: B => B, a: A)(implicit F: Functor[F]): F[A] = ...
  def =>=(f: B => B)(implicit F: Functor[F]): A => F[A] =
    mod(f, _)
  /** Modify the portion of the state viewed through the lens and return its new value. */
  def %=(f: B => B)(implicit F: Functor[F]): StateT[F, A, B] =
    mods(f)
  /** Lenses can be composed */
  def compose[C](that: LensT[F, C, A])(implicit F: Bind[F]): LensT[F, C, B] = ...
  /** alias for `compose` */
  def <=<[C](that: LensT[F, C, A])(implicit F: Bind[F]): LensT[F, C, B] = compose(that)
  def andThen[C](that: LensT[F, B, C])(implicit F: Bind[F]): LensT[F, A, C] =
    that compose this
  /** alias for `andThen` */
  def >=>[C](that: LensT[F, B, C])(implicit F: Bind[F]): LensT[F, A, C] = andThen(that)
}

Lens 則

Seth さん曰く:

Lens 則は常識的な感覚

(0. 2度 get しても、同じ答が得られる)
1. get して、それを set しても何も変わらない。
2. set して、それを get すると、set したものが得られる。
3. 2度 set して、get すると、2度目に set したものが得られる。

確かに。常識的な感覚だ。Scalaz はコードでこれを表現する:

  trait LensLaw {
    def identity(a: A)(implicit A: Equal[A], ev: F[Store[B, A]] =:= Id[Store[B, A]]): Boolean = {
      val c = run(a)
      A.equal(c.put(c.pos), a)
    }
    def retention(a: A, b: B)(implicit B: Equal[B], ev: F[Store[B, A]] =:= Id[Store[B, A]]): Boolean =
      B.equal(run(run(a) put b).pos, b)
    def doubleSet(a: A, b1: B, b2: B)(implicit A: Equal[A], ev: F[Store[B, A]] =:= Id[Store[B, A]]) = {
      val r = run(a)
      A.equal(run(r put b1) put b2, r put b2)
    }
  }

任意の亀を定義すれば turtleX が大丈夫かチェックできる。これは省くけど、Lens 則を破るような変な Lens はくれぐれも作らないように。

リンク

Jordan West さんによる An Introduction to Lenses in Scalaz という記事があって、飛ばし読みした感じだと Scalaz 6 っぽい。

Edward Kmett さんが Boston Area Scala Enthusiasts (BASE) で発表した Lenses: A Functional Imperative のビデオもある。

最後に、Gerolf Seitz さんによる Lens を生成するコンパイラプラグイン gseitz/Lensed がある。このプロジェクトはまだ実験段階みたいだけど、手で書くかわりにマクロとかコンパイラが Lens を生成してくれる可能性を示している。

また続きは後で。