読者です 読者をやめる 読者になる 読者になる

Scalaで話すように素数

Scala

最近Scalaが面白いので、例によって「話すように素数」をやってみました。

Python
話すようにプログラムするPythonチュートリアル - なんたらノート 第二期
続・話すようにプログラムするPythonチュートリアル - なんたらノート 第二期

Ruby
Rubyで話すようにしてみた - なんたらノート 第二期

さてScalaでは…

普通に書いたもの
def is_prime_number(n:Int):Boolean = {
    !(2 until n).exists((m) => {n % m == 0})
}
def prime_number_until(n:int):Seq[Int] = {
    (1 to n).filter(is_prime_number)
}
println(prime_number_until(100))

(2 to n) とか (2 until n) は、厳密に書くと(2).to(n)という、「オブジェクトのメソッドに引数1個渡す」表現なのですが、Scalaではこのパターンの構文の場合、ドットと括弧を省略できるそうです。省略記法がRubyよりRuby的。

それをもっと短く
println(1 to 100 filter((n) => !(2 until n exists(n % _ == 0))))

ワンライナーでこの長さ(w

型指定しなくても、全部型推論でいけてしまいました。

_を使って高階関数に渡す関数を作ると、無名関数がただの式みたいになってしまいます。

(m) => n % m == 0 //これは
n % _ == 0    //これに変換できる

functionやlambdaって綴るより圧倒的に短いし、平たく読めます。

ダックタイピングなPythonと比べて、filter等の関数が素直にインスタンスのメソッドになっています。また、any+imapのテクニック(最初の1つが見つかった時点でTrueを返す)が最初からexistsという関数として提供されています。

「それ未満の素数で割れるか」で実装

メモ化とか遅延評価を気にしてみる。

import scala.collection.mutable.HashMap

def memoize[A,B](f:(A)=>B):(A)=>B = {
  val memo = HashMap[A,B]()
  (n:A) => { if (!memo.contains(n)) memo(n) = f(n); memo(n) }
}

val is_prime_number = memoize((n:Int) =>
  !(prime_number_until(n-1) exists (n % _ == 0))
)

def prime_number_until(n:Int):Seq[Int] = 2 to n filter is_prime_number
  
println(prime_number_until(100))

型あり言語だけど、型をパラメータ化できるので、一般的なメモ化を実装しやすくて素敵です。入出力をイミュータブルな特性を持った型に限定するともっといいのかな。

遅延なんですが、なんと、シーケンシャルなデータの操作結果がいきなり遅延評価プロクシ(配列コピーじゃなくてイテレータ)だったりするので、とくに工夫してないのに、シーケンスの全コピーを避けることができていました。ただし、知らない間に展開されてしまうと急に重くなるので、抽象的なオブジェクトのまま操作するよう、意識しておく必要がありそうです。なんだかPython3000先取りの気分。


まださわりたてなので、よくわかっていません。もしかしたら、もっといい方法があるかも。

ちょっとやってみた感想は、「どこで型や括弧を省略できるかを間違う」ことが多く、短く書こうとすると、すぐにコンパイラが意図しない解釈をしてしまって大変でした。わかりきっている場合以外は、まず省略せずに書いて、それから可読性のために省略表記にしていく、って感じがいいのだろうか。

追記事項:
xs.find(...) == None を !xs.exists(...) に置き換えました。