初心者ぺちぱーがGitHubでScalaレッスンを始めたぞ。前回解いたハノイの塔を修正してtraitを使ってみよう。

Lessen 5: Improve Tower of Hanoi

前回はこんなコードを書いた。

State.scala
class State(val num: Int, val pegs: Map[String, mutable.Stack[Int]]) {
def this(num: Int) {
this(num, Map("A" -> mutable.Stack[Int](), "B" -> mutable.Stack[Int](), "C" -> mutable.Stack[Int]()))
clear(num)
}
def clear(num: Int) {
foreachPegName(pegs(_).clear())
// fill initial disks to peg "A"
(1 to num).reverse.foreach(pegs("A").push(_))
}
def move(a: String, b: String) = moveDisk(pegs(a), pegs(b))
private
def moveDisk(from: mutable.Stack[Int], to: mutable.Stack[Int]) =
if (to.isEmpty || from.head < to.head) {
to.push(from.pop()).head
}
else throw new IllegalArgumentException
def printResult(disk: Int, a: String, b: String) {
println("move disk %d from %s to %s".format(disk, a, b))
printState()
}
def printState() {
foreachPegName(name => println("%s: %s".format(name, pegs(name))))
println("")
}
private
def foreachPegName(func: String => Unit) { List("A", "B", "C").foreach(func) }
}

杭の状態をプリントする部分をStateクラスに持たせていたが、これを外部クラスに移動してみよう。

Trait

PHPでも5.4から導入された機能であるtraitは、Scalaも持っている仕組みだ。foreachPegName()はプリント部分にも必要で、pegsの初期化でも必要となっている。しかし、プリンタークラスと、Stateクラスには直接の継承関係は無さそうだ。そこで、このtraitに持たせることで、共通の機能をまとめることにする。若干やっつけな気がするけど。

PegsContainer.scala
trait PegsContainer {
def foreachPegName(func: String => Unit) { List("A", "B", "C").foreach(func) }
}

Stateクラスはextendsキーワードで、このPegsContainerトレイトを利用する。

class State(val num: Int, val pegs: Map[String, mutable.Stack[Int]]) extends PegsContainer

プリンタークラスはSimplePrinterクラスとしておこう。このクラスもextends PegsContainerでトレイトを利用する。

SimplePrinter.scala
class SimplePrinter(state: State) extends PegsContainer {
def printResult(disk: Int, a: String, b: String) {
println("move disk %d from %s to %s".format(disk, a, b))
printState()
}
def printState() {
foreachPegName(name => println("%s: %s".format(name, state.pegs(name))))
println("")
}
}

HanoiSolverクラスはこのSimplePrinterクラスを利用するように変更する。

class HanoiSolver {
def solve(n: Int) {
val state = new State(n)
val printer = new SimplePrinter(state)
printer.printState()
move(state, printer, n, "A", "B", "C")
}
private
def move(state: State, printer: SimplePrinter, n: Int, a: String, b: String, c: String) {
if (n > 0) {
move(state, printer, n-1, a, c, b)
val disk = state.move(a, c)
printer.printResult(disk, a, c)
move(state, printer, n-1, b, a, c)
}
}
}

Abstract class

HanoiSolverクラスがSimplePrinterクラス以外のプリンタークラスにも対応できるようにabstract classを用意しておこう(traitでもいいはずだけどなんとなく使ってみたかっただけ)。

StatePrinter.scala
abstract class StatePrinter {
def printResult(disk: Int, a: String, b: String)
def printState()
}

ここでは、SimplePrinterの定義を抜き出しただけだ。中身はサブクラスで実装済み。

SimplePrinterクラスでStatePrinterを継承するように変更してみよう。

SimplePrinter.scala
class SimplePrinter(state: State) extends StatePrinter with PegsContainer {
// ...
}

親クラスを継承する場合は、extendsには、継承しているクラス名を書く。利用しているtraitを指定するには、withキーワードを使う。HanoiSolverクラスのmove()メソッドの引数の型はStatePrinterクラスに変更する。

HanoiSolver.scala
class HanoiSolver {
// ...
private
def move(state: State, printer: StatePrinter, n: Int, a: String, b: String, c: String) {
// …
}
}

require precondition

前回の記事を書いた後、事前条件を定義するのにrequireが使えるらしいというのを知ったので、ついでにこれも修正してみよう。コップ本のp.114に記述があった。

State.scala
private
def moveDisk(from: mutable.Stack[Int], to: mutable.Stack[Int]) = {
require(to.isEmpty || from.head < to.head)
to.push(from.pop()).head
}

こうすると、コードの意味も、ただの分岐ではなくて、ハノイの塔のルールっぽく見えるようになったね。

assertions

Scalaには、requireに似たメソッドがいくつかあるようだ。Predefに定義されているようなので、PredefのScala docを見てみよう。

assert

final def assert(assertion: Boolean, message: Any): Unit
def assert(assertion: Boolean): Unit

Tests an expression, throwing an AssertionError if false. Calls to this method will not be generated if -Xelide-below is at least ASSERTION.

式をテストするのに使う。式がfalseだとAssertionErrorを投げる。-Xelide-belowASSERTION以上の場合は、実行されない。

assume

final def assume(assumption: Boolean, message: Any): Unit
def assume(assumption: Boolean): Unit

Tests an expression, throwing an AssertionError if false. This method differs from assert only in the intent expressed: assert contains a predicate which needs to be proven, while assume contains an axiom for a static checker. Calls to this method will not be generated if -Xelide-below is at least ASSERTION.

式をテストするのに使う。式がfalseだとAssertionErrorを投げる。assertとは、意図していることが違う。assertは述語が正しい事を意味し、assumeは静的チェックの前提となる仮定を意味する。-Xelide-belowASSERTION以上の場合は、実行されない。

うーむ。分かったような分かんないような感じだな。。assertは条件がtrueなのかどうかをチェックするだけで、assumeは条件がtrueじゃないとダメだってこと?いや、なんか違う気がする。。これ分からんわー。

require

final def require(requirement: Boolean, message: Any): Unit
def require(requirement: Boolean): Unit

Tests an expression, throwing an IllegalArgumentException if false. This method is similar to assert, but blames the caller of the method for violating the condition.

式をテストするのに使う。式がfalseだとIllegalArgumentExceptionを投げる。assertと似ているが、条件を満たしていない場合、メソッド呼出側の責任とする。

ensuring

ensuringだけはメソッドじゃなくて、クラスで定義されてた。これは戻り値のチェックに使うらしい。クラスの説明が書いてなかった。。

final class Ensuring[A] extends AnyVal

まとめると、こんな感じか。

  • assert: 何らかの条件チェック。失敗するとAssertionErrorを投げる。-Xelide-belowオプションで無視できる。
  • assume: 前提となる仮定のチェック。失敗するとAssertionErrorを投げる。-Xelide-belowオプションで無視できる。
  • require: 事前条件のチェック(引数が満たすべき条件のチェック)。呼出側に責任がある事を明確にする。失敗するとIllegalArgumentExceptionを投げる
  • ensuring: 事後条件のチェック(戻り値が満たすべき条件のチェック)。失敗するとAssertionErrorを投げる。

Done!

さて、修正後のコード全体はこんな感じになった。

class HanoiSolver {
def solve(n: Int) {
val state = new State(n)
val printer = new SimplePrinter(state)
printer.printState()
move(state, printer, n, "A", "B", "C")
}
private
def move(state: State, printer: StatePrinter, n: Int, a: String, b: String, c: String) {
if (n > 0) {
move(state, printer, n-1, a, c, b)
val disk = state.move(a, c)
printer.printResult(disk, a, c)
move(state, printer, n-1, b, a, c)
}
}
}
class State(val num: Int, val pegs: Map[String, mutable.Stack[Int]]) extends PegsContainer {
def this(num: Int) {
this(num, Map("A" -> mutable.Stack[Int](), "B" -> mutable.Stack[Int](), "C" -> mutable.Stack[Int]()))
clear(num)
}
def clear(num: Int) {
foreachPegName(pegs(_).clear())
// fill initial disks to peg "A"
(1 to num).reverse.foreach(pegs("A").push(_))
}
def move(a: String, b: String) = moveDisk(pegs(a), pegs(b))
private
def moveDisk(from: mutable.Stack[Int], to: mutable.Stack[Int]) = {
require(to.isEmpty || from.head < to.head)
to.push(from.pop()).head
}
}
trait PegsContainer {
def foreachPegName(func: String => Unit) { List("A", "B", "C").foreach(func) }
}
abstract class StatePrinter {
def printResult(disk: Int, a: String, b: String)
def printState()
}
class SimplePrinter(state: State) extends StatePrinter with PegsContainer {
def printResult(disk: Int, a: String, b: String) {
println("move disk %d from %s to %s".format(disk, a, b))
printState()
}
def printState() {
foreachPegName(name => println("%s: %s".format(name, state.pegs(name))))
println("")
}
}

Conclustion

今回は、traitを使ってみたり、abstract classを使ってみたりした。まとめ!

  • traitextendswithキーワードで利用する
  • 別のクラスを継承する場合はextendsキーワードを使う
  • 継承+トレイトの場合は、HogeClass extends SuperHogeClass with HogeTraitになる
  • 事前条件はrequire()メソッドで表現できる
  • アサーションは他にも3つあるよ

Reference

Scalaスケーラブルプログラミング第2版
作者
Martin Odersky
Lex Spoon
Bill Venners
出版社
インプレスジャパン
発売日
2011-09-27
メディア
単行本(ソフトカバー)
価格
¥ 4,968