Hamming number その2

id:akihiro4chawon 氏のエントリの 1つ目の実装を見て Haskell の実装と同じだったので驚いた。


スクリプトHaskell と同じように書き直してみる。

val hamming: Stream[BigInt] = BigInt(1) #:: merge(merge(hamming map {_ * 2}, hamming map {_ * 3}), hamming map {_ * 5})
def merge[T <% Ordered[T]](xs: Stream[T], ys: Stream[T]): Stream[T] = {
  val (x, y) = (xs.head, ys.head)
  if (x < y)      x #:: merge(xs.tail, ys)
  else if (x > y) y #:: merge(xs, ys.tail)
  else            x #:: merge(xs.tail, ys.tail)
}
assert(hamming.take(20) == Stream(1,2,3,4,5,6,8,9,10,12,15,16,18,20,24,25,27,30,32,36))


前回 yield がないと実装できないのかなと思ったのは Scala が 2つとも Haskell と違う実装だったからということも理由にある。一応、Scala の Stream も試してみたがうまくいかなかったからだ。

scala> val xs:Stream[Int] = 1+:xs
java.lang.NullPointerException

Java や Groovy は右辺から評価される*1のできっと Scala もそうなのだと考えてしまったことが間違いの始まりだったらしい。そもそも参照できないなら NullPointerException ではないのだろうと今は思うのだがそのときは気づかなかったのだ。*2

scala> val xs:Stream[Int] = 1#::xs
xs: Stream[Int] = Stream(1, ?)

scala> xs take 3 foreach println
1
1
1

API には見当たらないのだが #:: を使えばできるみたいだ。


昔読んだ小説のメモだが *3

観察に先立って確固とした目標がイメージできなければ人はものをみたことにはならないといっても過言ではない。認識するためには予め用意された仮説が必要なのである。


『今はもうない』 森博嗣

プログラムを書く私には、カエサルの言葉よりしっくりくる。
できないかもと思っていたのでみえていなかったものがあるのだろう。


Scala で実装できるということは Functional Java*4 で実装でき Groovy でも実装できる。
Functional Java の Stream をよく見てみると append には引数が Stream のものと P1 で Wrap されたものがある。
今までは _1 で取り出すのが面倒だなと思っていたが、意味がないのに面倒になっているはずがないのでその当たりを調べながら実装したら動いてしまった。

@Grab('org.functionaljava:fj:3.0')
import fj.*
import static fj.data.Stream.*
import static fj.Equal.intEqual 
import static fj.Equal.streamEqual
import static fj.Show.intShow
import static fj.Show.streamShow

hamming = cons().f(1).f(({ merge(merge(hamming.map({ it * 2 } as F),
                                       hamming.map({ it * 3 } as F)),
                                       hamming.map({ it * 5 } as F)) } as F).lazy().f())
merge = { xs, ys ->
  def (x, y) = [xs.head(), ys.head()]
// println ([x,y])
  x < y ? cons().f(x).f(({ merge(xs.tail()._1(), ys) } as F).lazy().f()) :
  x > y ? cons().f(y).f(({ merge(xs, ys.tail()._1()) } as F).lazy().f()) :
          cons().f(x).f(({ merge(xs.tail()._1(), ys.tail()._1()) } as F).lazy().f())
}
// hamming.take(20).foreach(Effect.f({ println it } as F))
println streamShow(intShow).showS(hamming.take(20))
assert streamEqual(intEqual).eq(hamming.take(20), stream(1,2,3,4,5,6,8,9,10,12,15,16,18,20,24,25,27,30,32,36))

P1 はタメになる部分でそのタメは F#lazy で作る。*5 tail が呼び出されると再帰の 1つ前の merge を実行する。*6
id:amachang 氏の Haskell のリストと遅延評価が少し分かった - IT戦記 を読んで似たようなことを試してはいたのだが評価する位置を間違えて無限ループさせていた。


Functional Java を参考に Groovy に書き直してみる。

class Stream {
  def head, tail_
  static def cons(head, tail_) { new Stream(head: head, tail_: tail_) }
  static def cons_(head, gen)  { cons(head){ cons_(gen(head), gen) } }
  def getTail() { tail_() }
  def map(f)    { cons(f(head)){ this.tail.map(f) } }
  def merge(ys) {
    def (x, y) = [head, ys.head]
    x < y ? cons(x){ this.tail.merge(ys) } :
    x > y ? cons(y){ this.merge(ys.tail) } :
            cons(x){ this.tail.merge(ys.tail) }
  }
  def take(n) {
    def rs = []
    def xs = this
    n.times {
      rs << xs.head
      xs = xs.tail
    }
    rs
  }
  def getAt(n) {
    def xs = this
    for (def i = 0; i < n - 1; i++) {
      xs = xs.tail
    }
    xs.head
  }
  @Override String toString() { "Stream($head, ?)" }
}
assert Stream.cons_(1){ it + 1 }.take(3) == [1,2,3]
assert Stream.cons_(1){ it + 1 }.map { it * 2 }.take(3) == [2,4,6]
def odds  = Stream.cons_(1){ it + 2 }
def evens = Stream.cons_(2){ it + 2 }
assert odds.merge(evens).take(3) == [1,2,3]

hamming = Stream.cons(1){ hamming.map{ 2 * it }.merge(hamming.map{ 3 * it }).merge(hamming.map{ 5 * it }) }
assert hamming.take(20) == [1,2,3,4,5,6,8,9,10,12,15,16,18,20,24,25,27,30,32,36]

この実装では参照をつかんだままになるようで他のテストはメモリが足りなくて実行できない*7のだが最初の 20 個は取得できた。
遅延評価のメモリの話は裏を返せば Queue として使えるということみたいだ。


実装できたのは id:akihiro4chawon 氏と Functional Java のおかげである。感謝。

*1:JavaScript はスコープ内の変数は先に評価される

*2:このことも直接は関係ないのだがさりげなく積み重なっていく

*3:話の内容が思い出せない

*4:JavaScala できるライブラリ

*5:lazy のコメントには Kleisli arrow とあったのでメモしておく

*6:print で確認できる。最後の評価が余分な気がするが

*7:Haskell はそうならないし、Queue での実装よりメモリは必要ないと思う