抽象化とレイテンシと遅延評価

http://karetta.jp/book-node/gauche-hacks/023107を見て思ったこと。
レイヤーを重ねて抽象化したコードはわかりやすくて保守性が高いのだけれども、場合によってはレイテンシが発生します。
抽象化を進めてレイヤーが増えていくと、各レイヤーで発生しているレイテンシが積み重なって無視できないものになることもあります。

      • -

先のSchemeの例だと、100要素ならば問題なくても、100万要素になるとちょっと問題があるかもしれません。10億要素になるとかなり問題になるはずです。

(for-each print
  (map
    (lambda (x) (cond ((= (modulo x 15) 0) "FizzBuzz")
                      ((= (modulo x 5) 0) "Buzz")
                      ((= (modulo x 3) 0) "Fizz")
                      (else x)))
    (iota 100 1)))

というのは、(iota 100 1)で100要素のリストを作って、その次に(map ...)で別の100要素のリストを作り、そのあとで(for-each print ...)でそのリストの要素を1つずつ印字しています。最初の100要素のリストを作り終わるまで、次の(map ...)の処理は開始しません。
つまり、ここで要素数分の遅延が発生しているわけです。(ついでに、それに対応するメモリも使っています。)

      • -

レイテンシの発生しない書き方にするなら、

(define (fizzbuzz f n)
  (define (g x)
    (if (> x n) #t
        (begin
          (f (cond ((= (modulo x 15) 0) "FizzBuzz")
                   ((= (modulo x 5) 0) "Buzz")
                   ((= (modulo x 3) 0) "Fizz")
                   (else x)))
          (g (+ x 1)))))
  (g 1))

と書いたり

(define (fizzbuzz)
  (let ((x 0))
    (define (g)
      (set! x (+ x 1))
      (cond ((= (modulo x 15) 0) "FizzBuzz")
	    ((= (modulo x 5) 0) "Buzz")
	    ((= (modulo x 3) 0) "Fizz")
	    (else x)))
    g))

と書いたりして、結果をため込まない書き方にしなければいけないわけです。(FizzBuzzの条件分岐のところを関数引数を使って括り出してやると、もっとよいと思います)

      • -

私は、結構レイテンシが気になるので、先のschemeのような書き方は無意識に避けてしまいます。
しかし、下の2つは最初の例に比べると簡明さに欠けるとは思います。

      • -

私がCleanを愛するのはまさにこの点です。
遅延評価の処理系では、最初の書き方でもレイテンシが発生しないので、レイテンシを小さく抑えながら、抽象化による簡明な記述を行うことが可能になるのです。