Scalaプログラミング4 Listを使ってみる2
Beginning Scala programming4
初心者ぺちぱーがGitHubでScalaレッスンを始めたぞ。今回は、4回目の問題をリファクタリングしてみよう。
- Scalaスケーラブルプログラミング第2版
- 作者
- Martin Odersky
- Lex Spoon
- Bill Venners
- 出版社
- インプレスジャパン
- 発売日
- 2011-09-27
- メディア
- 単行本(ソフトカバー)
- 価格
- ¥ 4,968
Lessen 4: Sieve of Eratosthenes
前回はこんなobject
を作ってみた。
object MyFirstPrimeNumber {
def toList(max: Int): List[Int] = {
// step1
val range = 2 to max
// step2
val list = List(range.head)
eratosthenesSieve(list, range)
}
private
def eratosthenesSieve(list: List[Int], range: Seq[Int]): List[Int] = {
// step3
val last = list.last
val range1 = range.filter(i => i % last != 0)
// step4
if (range1.max < math.pow(list.max, 2)) {
list ::: range1.toList
} else {
// back to step2
val list1 = list ::: List(range1.head)
// step3
eratosthenesSieve(list1, range1)
}
}
}
テストコードはこんなのを書いてみた。
import org.scalatest.FunSuite
class MyFirstPrimeNumberTest extends FunSuite {
test("Prime numbers ranging from 2 to 20 are 2, 3, 5, 7, 11, 13, 17, 19") {
expectResult(List(2, 3, 5, 7, 11, 13, 17, 19))(MyFirstPrimeNumber.toList(20))
}
}
Refactor Sieve of Eratosthenes
まずは、val
宣言しているところが冗長な気がするので、一度しか参照されてない部分については、ちょっと直してみよう。
object MyFirstPrimeNumber2 {
def toList(max: Int): List[Int] = {
// step1
val range = 2 to max
// step2
eratosthenesSieve(List(range.head), range)
}
private
def eratosthenesSieve(list: List[Int], range: Seq[Int]): List[Int] = {
// step3
val range1 = range.filter(i => i % list.last != 0)
// step4
if (range1.max < math.pow(list.max, 2)) {
list ::: range1.toList
} else {
// back to step2
// step3
eratosthenesSieve(list ::: List(range1.head), range1)
}
}
}
toList(max)
の7行目のList(range.head)
は、初回は2であることが自明だから、List(2)
でも良さそうだ。
object MyFirstPrimeNumber2 {
def toList(max: Int): List[Int] = {
// step1
// step2
eratosthenesSieve(List(2), 2 to max)
}
private
def eratosthenesSieve(list: List[Int], range: Seq[Int]): List[Int] = {
// step3
val range1 = range.filter(i => i % list.last != 0)
// step4
if (range1.max < math.pow(list.max, 2)) {
list ::: range1.toList
} else {
// back to step2
// step3
eratosthenesSieve(list ::: List(range1.head), range1)
}
}
}
じゃあ、テストしてみよう。クラス名を変えただけで、テストコードの中身は一緒だ。
class MyFirstPrimeNumber2Test extends FunSuite {
test("Prime numbers ranging from 2 to 20 are 2, 3, 5, 7, 11, 13, 17, 19") {
expectResult(List(2, 3, 5, 7, 11, 13, 17, 19))(MyFirstPrimeNumber2.toList(20))
}
}
Testing started at 18:19 ...
- Prime numbers ranging from 2 to 20 are 2, 3, 5, 7, 11, 13, 17, 19
Process finished with exit code 0
よし。ちゃんと動いてそうだな。
Remember FizzBuzz
eratosthenesSieve(list, range)
メソッドをよく見ると、ステップ3で作ったrange1
というコレクションに対して、ステップ4で別のコレクションを作っている。これって、2回目のFizzBuzzで見たコードとよく似てるコードだ。ステップ4もif
式じゃなくて、パターンマッチにしてみると、range
に対して、filter
した後、続けてmatch
と書けそうだ。
object MyFirstPrimeNumber3 {
def toList(max: Int): List[Int] = {
// step1
// step2
eratosthenesSieve(List(2), 2 to max)
}
private
def eratosthenesSieve(list: List[Int], range: Seq[Int]): List[Int] = range.filter {
// step3
i => i % list.last != 0
} match {
// step4
case range1 if range1.max < math.pow(list.max, 2) => list ::: range1.toList
// back to step2
// step3
case range1 => eratosthenesSieve(list ::: List(range1.head), range1)
}
}
Placeholder syntax
コップ本のp.155を見てみよう。関数リテラルを簡潔にするプレースホルダー構文というのがあるらしい。
パラメーターが関数リテラル内で1度しか使われない場合には、1個以上のパラメーターのプレースホルダー(場所を用意していることを示す記号)として、アンダースコアを使うことができる。
例として載っているのは、0よりも大きい数字を返す処理だ。
sumNumbers.filter(_ > 0)
//実は下の表現を簡略化したもの
sumNumbers.filter(x => x > 0)
ということは、MyFirstPrimeNumber3
の11行目はちょっと簡単になる。
object MyFirstPrimeNumber3 {
def toList(max: Int): List[Int] = {
// step1
// step2
eratosthenesSieve(List(2), 2 to max)
}
private
def eratosthenesSieve(list: List[Int], range: Seq[Int]): List[Int] = range.filter {
// step3
_ % list.last != 0
} match {
// step4
case range1 if range1.max < math.pow(list.max, 2) => list ::: range1.toList
// back to step2
// step3
case range1 => eratosthenesSieve(list ::: List(range1.head), range1)
}
}
テストしてみよう。テストコードは中身が一緒でクラス名をMyFirstPrimeNumber3
に変えただけなので省略。
Testing started at 18:49 ...
- Prime numbers ranging from 2 to 20 are 2, 3, 5, 7, 11, 13, 17, 19
Process finished with exit code 0
よし。大丈夫だ。
List operation
さて、コップ本をまた開いてみよう。今度はp.324だ。表17.1 集合の一般的な操作 をよく見てみると、++
演算子というのがある。
val nums = Set(1, 2, 3)
num ++ List(5, 6) // => Set(1, 2, 3, 5, 6)
型が違うコレクションでも自動的に変換して要素を追加したコレクションを作っているように見えるね。ちょっと試してみよう。
scala> val list = List(1, 2)
list: List[Int] = List(1, 2)
scala> val range = 5 to 7
range: scala.collection.immutable.Range.Inclusive = Range(5, 6, 7)
scala> val newList = list ++ range
newList: List[Int] = List(1, 2, 5, 6, 7)
おお!動いた!左側の型に合わせて、要素を追加したコレクションを作る操作なのかな。
そうすると、MyFirstPrimeNumber3
の14行目の
list ::: range1.toList
この部分もtoList
する必要はなくて++
すればいいんだな。
object MyFirstPrimeNumber4 {
def toList(max: Int): List[Int] = {
// step1
// step2
eratosthenesSieve(List(2), 2 to max)
}
private
def eratosthenesSieve(list: List[Int], range: Seq[Int]): List[Int] = range.filter {
// step3
_ % list.last != 0
} match {
// step4
case range1 if range1.max < math.pow(list.max, 2) => list ++ range1
// back to step2
// step3
case range1 => eratosthenesSieve(list ::: List(range1.head), range1)
}
}
Use head instead of last
コップ本のp.297を見てみよう。
headやtailはともに一定の時間で実行されるが、initとlastはリスト全体をたどらないと結果値を計算できないので、リストの長さに比例する計算時間を要する
と書いてある。11行目のlist.last
というのは、追加された値を示すわけだから、これも引数で渡すようにしてみよう。
object MyFirstPrimeNumber4 {
def toList(max: Int): List[Int] = {
// step1
// step2
eratosthenesSieve(2, List(2), 2 to max)
}
private
def eratosthenesSieve(last: Int, list: List[Int], range: Seq[Int]): List[Int] = range.filter {
// step3
_ % last != 0
} match {
// step4
case range1 if range1.max < math.pow(list.max, 2) => list ++ range1
// back to step2
// step3
case range1 => eratosthenesSieve(range1.head, list ::: List(range1.head), range1)
}
}
テストーー
Testing started at 19:24 ...
- Prime numbers ranging from 2 to 20 are 2, 3, 5, 7, 11, 13, 17, 19
Process finished with exit code 0
よしよし。ちゃんと動いてるぞ。
1 is not prime number
そういえば、1って素数じゃなかったよね。
1を渡したときのテストコードを追加してみよう。
test("1 is not prime number") {
expectResult(List())(MyFirstPrimeNumber4.toList(1))
}
Testing started at 19:29 ...
- Prime numbers ranging from 2 to 20 are 2, 3, 5, 7, 11, 13, 17, 19
empty.max
java.lang.UnsupportedOperationException: empty.max
…略
Process finished with exit code 0
あー。。なんか例外出てきた。14行目のrange1.max
がいけないみたいだ。要素が空の場合はmax
使えないよ?ってことか。
1以下の場合は素数の計算をする必要はなくて、List()
を返せばいいし、2の場合はList(2)
を返せばいいわけだから、toList(max)
を変更すれば良さそうだ。これはパターンマッチで出来そうだな。
object MyFirstPrimeNumber5 {
def toList(max: Int): List[Int] = max match {
case max if max <= 1 => List()
case max if max == 2 => List(2)
// step1
// step2
case _ => eratosthenesSieve(2, List(2), 2 to max)
}
private
def eratosthenesSieve(last: Int, list: List[Int], range: Seq[Int]): List[Int] = range.filter {
// step3
_ % last != 0
} match {
// step4
case range1 if range1.max < math.pow(list.max, 2) => list ++ range1
// back to step2
// step3
case range1 => eratosthenesSieve(range1.head, list ::: List(range1.head), range1)
}
}
テストコードにも、2と0と-1の場合を追加してみよう。
class MyFirstPrimeNumber5Test extends FunSuite {
test("Prime numbers ranging from 2 to 20 are 2, 3, 5, 7, 11, 13, 17, 19") {
expectResult(List(2, 3, 5, 7, 11, 13, 17, 19))(MyFirstPrimeNumber5.toList(20))
}
test("2 is prime number") {
expectResult(List(2))(MyFirstPrimeNumber5.toList(2))
}
test("1 is not prime number") {
expectResult(List())(MyFirstPrimeNumber5.toList(1))
}
test("0 is not prime number") {
expectResult(List())(MyFirstPrimeNumber5.toList(0))
}
test("-1 is not prime number") {
expectResult(List())(MyFirstPrimeNumber5.toList(-1))
}
}
Testing started at 19:57 ...
- Prime numbers ranging from 2 to 20 are 2, 3, 5, 7, 11, 13, 17, 19
- 2 is prime number
- 1 is not prime number
- 0 is not prime number
- -1 is not prime number
Process finished with exit code 0
よし。なんとかなったぞ。
Tail recursion
IntelliJ IDEAを使っていると、今回作ったeratosthenesSieve
の左側に、サンジのグルグル眉毛みたいなマークが出ていないだろうか。
これは、この関数が末尾再帰だということを表すマークらしい。
のp.166を読んでみよう。最適化されるみたいね。
Scalaコンパイラーは末尾再帰を検出したら、パラメーターを新しい値に更新した後、再帰呼び出しを関数の冒頭にジャンプするコードに書き換えるのである。
以上からわかるように、問題を解決するために再帰アルゴリズムを使うことを躊躇してはならない。再帰による解は、ループを使った解よりも優美で簡潔になることがよくある。解が末尾再帰になっていれば、実行時に余計なオーバーヘッドがかかったりはしない。
Conclusion
前回書いたコードと比べると、ずいぶんScalaっぽくなったんじゃないか!?どうだろうか。次回はtoList()
を使ったcontains()
メソッドを作ってみようと思う。
まとめ!
- 関数リテラルのプレースホルダー構文いいよ!
++
演算子でList
とRange
から要素を合わせたコレクションを作れる- 末尾再帰はScalaコンパイラーで最適化される