mutableと相性がいい
Scalaでは基本的にオブジェクトは不変
不変性を壊さずに手続き型プログラミングを行う方法を紹介する
ここではテトリスを題材にする
type Point = (Int, Int)
type Piece = Vector[Point]
type Field = Vector[Vector[Boolean]]
case class Tetriz(field: Field, piece: Piece, position: Point)
不変オブジェクトの操作には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
ピースを回転させる
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は現在の状態から計算結果と新しい状態のペアを返す
モナドにより状態を隠蔽したまま手続きを合成することができる
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 ()
}
Stateは状態とは別に計算結果を返せるが不必要な場合がある
より軽量な表現としてEndoがある
EndoはMonoidであり結合できる
Monoidは畳み込みと相性がいい
フィールドにピースを固定する
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))
}
StateもEndoも関数であり合成しすぎるとStackOverflowErrorが起きる
FreeでDSLを定義すればスタックはあふれないし独自の機能も追加できる
規模の大きなものは一からFreeで自作するのもよい
不変オブジェクトの操作にはLensを使う
状態の遷移はEndoで表現できる
計算結果を伴う手続きにはStateを使う