手続き型プログラミング with Scalaz

@halcat0x15a

今日話すこと

  • Lens
  • State
  • Endo

手続き型とは

  • 逐次実行していく
  • 状態を変化させる

mutableと相性がいい

immutable

Scalaでは基本的にオブジェクトは不変

不変性を壊さずに手続き型プログラミングを行う方法を紹介する

ここではテトリスを題材にする

テトリスのモデル

type Point = (Int, Int)

type Piece = Vector[Point]

type Field = Vector[Vector[Boolean]]

case class Tetriz(field: Field, piece: Piece, position: Point)

Lens

不変オブジェクトの操作にはLensを使う

object Point {
  def x: Lens[Point, Int] = Lens.firstLens
  def y: Lens[Point, Int] = Lens.secondLens
}

ScalazにはScala標準のデータ型に対する様々なLens定義されている

Lensはフィールドに対するgetterとsetterのペアである

val p = (2, 3)
Point.x.get(p) assert_=== 2
Point.y.set(p, 0) assert_=== (2, 0)
Point.x.mod(_ + 1, p) assert_=== (3, 3)

Lensは合成することができる

object Field {
  def bit(x: Int, y: Int): PLens[Field, Boolean] =
    PLens.vectorNthPLens(y) >=> PLens.vectorNthPLens(x)
}

Lensの合成はネストした構造の書き換えを簡単にする

val field = Vector.fill(3)(Vector.fill(3)(false))

Field.bit(1, 1).set(field, true)
  assert_=== Try(field.updated(1, field(1).updated(1, true))).toOption

State

ピースを回転させる

Pointがmutableならば次のように書くだろう

class Point(var x: Int, var y: Int)

object Piece {
  def rotate(r: Int)(piece: Piece) = {
    piece map { p =>
      val x = p.y * r
      p.y = p.x * -r
      p.x = x
      p
    }
  }
}

Stateを使えば同様に記述することができる

object Piece {
  def rotate(r: Int)(piece: Piece): Piece = {
    val inverse = for {
      x <- Point.x
      y <- Point.y
      _ <- Point.x := y * r
      _ <- Point.y := x * -r
    } yield ()
    piece.map(inverse.exec)
  }
}

State[S, A] = S => (S, A)

Stateは現在の状態から計算結果と新しい状態のペアを返す

モナドにより状態を隠蔽したまま手続きを合成することができる

object Tetriz {
  def run[A](x: Int, y: Int, r: Int): State[Tetriz, Unit] =
    for {
      field <- Tetriz.field
      piece <- Tetriz.piece
      position <- Tetriz.position
      _ <- if (r != 0 && !Field.overlaps(Piece.rotate(r)(piece), position)(field))
        Tetriz.piece %= Piece.rotate(r)
      else
        State.state[Tetriz, Unit](())
      piece <- Tetriz.piece
      position <- Tetriz.position
      _ <- if (!Field.overlaps(piece, (Point.x += x) exec position)(field))
        Tetriz.position >=> Point.x += x
      else
        State.state[Tetriz, Unit](())
      position <- Tetriz.position
      _ <- if (!Field.overlaps(piece, (Point.y += y) exec position)(field))
        Tetriz.position >=> Point.y += y
      else
        for {
          _ <- Tetriz.field %= Field.fix(piece, position).run
          _ <- Tetriz.piece := Piece()
          _ <- Tetriz.position := Field.start
        } yield ()
      _ <- Tetriz.field %= Field.check
    } yield ()
}

Endo[S] = S => S

Stateは状態とは別に計算結果を返せるが不必要な場合がある

より軽量な表現としてEndoがある

EndoはMonoidであり結合できる

Monoidは畳み込みと相性がいい

Endo

フィールドにピースを固定する

Fieldがmutableならば次のように書くだろう

type Field = Array[Array[Boolean]]

object Field {
  def fix(piece: Piece, position: Point)(field: Field): Field = {
    piece.foreach { p =>
      field(p.y + position.y)(p.x + position.y) = true
    }
    field
  }
}

不変オブジェクトを繰り返しで変化させるには畳み込みを使う

object Field {
  def fix(piece: Piece, position: Point)(field: Field): Field =
    piece.map(_ |+| position).foldLeft(field)((f, p) => (bit(Point.x.get(p), Point.y.get(p)) := true).exec(f))
}

状態の遷移を畳み込むにはfoldMapとEndoを組み合わせる

object Field {
  def fix(piece: Piece, position: Point): Endo[Field] =
    piece.map(_ |+| position).foldMap(p => Endo(bit(Point.x.get(p), Point.y.get(p)) := true exec))
}

Free

StateもEndoも関数であり合成しすぎるとStackOverflowErrorが起きる

FreeでDSLを定義すればスタックはあふれないし独自の機能も追加できる

規模の大きなものは一からFreeで自作するのもよい

まとめ

  • 不変オブジェクトの操作にはLensを使う

    • 合成できる
    • Stateを作れる
  • 状態の遷移はEndoで表現できる

    • 合成できる
    • 入力列に対して手続きを畳み込める
  • 計算結果を伴う手続きにはStateを使う

    • 合成できる
    • 状態の隠蔽ができる