HOME | 16. 継続 | もうひとつの Scheme 入門 | 18. 非決定性 | download | 書き込む |
全体的に遅延評価を取り入れた言語としては Haskell が有名ですが、 Scheme も部分的に遅延評価を取り入れています。
> (define laz (delay (+ 1 2))) > laz #<promise:laz> > (promise? laz) #t > (force laz) 3 > (* 10 (force laz)) 30ここで注意したいのはプロミスは force によって消費されず、プロミスのままだということです。 つまり、force はプロミスから値を計算しますが、プロミスに副作用は及ぼしません。 従ってプロミスは何度でも使いまわすことができます。
無限リストは、
(<val> . <promise>) (1)の様に、car 部が確定した値で、cdr 部がプロミスからなっているコンスセルで表します。 cdr 部のプロミスを force するとまた、(1) のようなコンスセルが生成するような入れ子構造を 作ることによって無限リストを表します(図1)。コンスセルの入れ子による表現は、通常のリストと同じですが、 cdr 部をプロミスにすることによって通常のリストとは異なり、無限リストが表現できるようになります。
[code 1]
01: ;;;;; basic functions and a macro 02: 03: ;;; car for lazy evaluation 04: (define lazy-car car) 05: 06: ;;; cdr for lazy evaluation 07: (define (lazy-cdr ls) 08: (force (cdr ls))) 09: 10: ;;; lazy cons 11: (define-syntax lazy-cons 12: (syntax-rules () 13: ((_ a b) (cons a (delay b))))) 14: 15: ;;; lazy map 16: (define (lazy-map fn . lss) 17: (if (memq '() lss) 18: '() 19: (lazy-cons (apply fn (map lazy-car lss)) 20: (apply lazy-map fn (map lazy-cdr lss))))) 21: 22: ;;; lazy filter 23: (define (lazy-filter pred ls) 24: (if (null? ls) 25: '() 26: (let ((obj (lazy-car ls))) 27: (if (pred obj) 28: (lazy-cons obj (lazy-filter pred (lazy-cdr ls))) 29: (lazy-filter pred (lazy-cdr ls)))))) 30: 31: ;;; returns n-th item of the lazy list 32: (define (lazy-ref ls n) 33: (if (= n 0) 34: (lazy-car ls) 35: (lazy-ref (lazy-cdr ls) (- n 1)))) 36: 37: ;;; returns first n items of the ls 38: (define (head ls n) 39: (if (= n 0) 40: '() 41: (cons (lazy-car ls) (head (lazy-cdr ls) (- n 1)))))
(inf-seq a0 f) は再帰的な定義になっており、 その定義から、初項が a0 第2項が (f a0) であり、 第 n+1 項は (f an) で表されることが簡潔に表現されています。 inf-seq を使うと等差数列と等比数列はそれぞれ (ari a0 d), (geo a0 r) の様に 定義できます。ここで、a0, d, r はそれぞれ 初項、公差、公比を表します。
[code 2]
01: ;;;; sequences 02: 03: ;;; infinite sequences represented by a_(n+1) = f(a_n) 04: (define (inf-seq a0 f) 05: (lazy-cons a0 (inf-seq (f a0) f))) 06: 07: ;;;arithmetic sequence 08: (define (ari a0 d) 09: (inf-seq a0 (lambda (x) (+ x d)))) 10: 11: ;;; geometric sequence 12: (define (geo a0 r) 13: (inf-seq a0 (lambda (x) (* x r))))inf-seq で無限数列ができることを確認してみましょう(sample 2)。 geo を使って初項 1、公比 2 の等比数列 g1 と 初項 1、公比 1/2 の等比数列 g2 を作り、head を使って最初の 10 項を表示させます。ちゃんと数列ができていることが確認されます。
次に、等差数列と lazy-filter について調べてみましょう。 まず、(ari 1 1) で、 (1 2 3 ....) という数列 ar1 を 作ります。head で、最初の 10 項を表示させると 1 から 10 までが表示されました。 さらに、lazy-filter を使って偶数を取り出し、最初の 10 項を表示させると 2 から 20 までが表示されます。
[sample 2]
> (define g1 (geo 1 2)) ;初項 1 公比 2 の等比数列 > (define g2 (geo 1 (/ 1 2))) ;初項 1 公比 1/2 の等比数列 > (head g1 10) (1 2 4 8 16 32 64 128 256 512) > (head g2 10) (1 1/2 1/4 1/8 1/16 1/32 1/64 1/128 1/256 1/512) > (head (lazy-map * g1 g2) 10) (1 1 1 1 1 1 1 1 1 1) > (define ar1 (ari 1 1)) ;初項 1 公差 1 の等差数列 > (head ar1 10) (1 2 3 4 5 6 7 8 9 10) > (head (lazy-filter even? ar1) 10) (2 4 6 8 10 12 14 16 18 20)
fib(1) = 1 fib(2) = 1 fib(n+1) = fib(n) + fib(n-1)で表される数列です。lazy-cons と lazy-map を使うと、[code 3] に示す様に、数学上の定義のままの コードが書けます。しかも O(n) のオーダーで各項が計算されます。[sample 3] の値は瞬時に 計算されます。
[code 3]
01: (define fib 02: (lazy-cons 1 03: (lazy-cons 1 04: (lazy-map + fib (lazy-cdr fib)))))
> (head fib 20) (1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 6765) > (lazy-ref fib 100) 573147844013817084101
a(n+1) = (a(n) + N/a(n)) / 2 (2)もし、この近似がある極限値 a に収束するなら、
a = (a + N/a) / 2 したがって、 2a = a + N/a a = N/a a*a = N a = squareroot(N)となり、確かに極限値 a は N の平方根です。 (2) 式より、次項は前項だけの関数なので、近似値の数列は inf-seq で表せます。 初期値は 1 に固定しましたが、この数列は収束が早いので問題はありません。
[code 4] に平方根を求めるプログラムを示します。
[code 4]
01: ;;; Newton-Raphson method 02: (define (newton-raphson n) 03: (inf-seq 1 (lambda (x) (/ (+ x (/ n x)) 2)))) 04: 05: ;;; returning a reasonable answer. 06: ;;; If the ratio of successive terms is in (1 - eps) and (1 + eps), 07: ;;; or the following term is zero, 08: ;;; the function returns it. 09: (define (lazylist->answer ls eps) 10: (let ((e1 (- 1.0 eps)) 11: (e2 (+ 1.0 eps))) 12: (let loop ((val (lazy-car ls)) 13: (ls1 (lazy-cdr ls))) 14: (let ((val2 (lazy-car ls1))) 15: (if (or (zero? val2) (< e1 (/ val val2) e2)) 16: (exact->inexact val2) 17: (loop val2 (lazy-cdr ls1))))))) 18: 19: ;;; 20: (define (my-sqrt n eps) 21: (lazylist->answer (newton-raphson n) eps))
> (my-sqrt 9 0.0000001)
3.0
そこで、lazylist-diff に示すように 初期値 h0 から初めてその値を半分にしていく遅延リスト (geo h0 0.5) を作り、 それに対応する近似値の微分リストを作ります。
(lazylist->answer (lazylist-diff h0 f x) eps)を使って素直に値を求めても良いのですが、収束が遅いので、収束を早くする関数 super を使って 収束を早めます。 収束を早めるテクニックについては なぜ関数プログラミングは重要か を見てください。 収束を加速させるアルゴリズムはそれなりに複雑で、通常の反復を使ったコーディングではかなりの量になります。 遅延評価を使うとそれが、数学の定義に近い形で簡潔に表現できます。また、プログラムがモジュール化されているので そのまま他の問題に適応できます。4.3.3. 節で示す数値積分は [code 5] で示した収束加速関数をそのまま再利用しています。
[code 5]
01: ;;; differentiation 02: 03: ;;; primitive function for differentiation 04: (define (easydiff f x h) 05: (/ (- (f (+ x h)) (f x)) h)) 06: 07: ;;; create a lazy list of approximation for differentiation 08: (define (lazylist-diff h0 f x) 09: (lazy-map (lambda (h) (easydiff f x h)) (geo h0 0.5))) 10: 11: ;;; eliminate error from the approximation 12: (define (elimerror n ls) 13: (let ((a (lazy-car ls)) 14: (b (lazy-second ls)) 15: (c (expt 2 n))) 16: (lazy-cons 17: (/ (- (* b c) a) (- c 1)) 18: (elimerror n (lazy-cdr ls))))) 19: 20: ;;; estimate `n' in elimerror 21: (define (order ls) 22: (let* ((a (lazy-car ls)) 23: (b (lazy-second ls)) 24: (c (lazy-ref ls 2)) 25: (d (- (/ (- a c) (- b c)) 1.0))) 26: (cond 27: ((< d 2) 1) 28: ((<= 2 d 16) (inexact->exact (round (log2 d)))) 29: (else 4)))) 30: 31: ;;; 32: (define (log2 x) 33: (/ (log x) (log 2))) 34: 35: ;;; improve convergency of the lazy list of the approximation 36: (define (improve ls) 37: (elimerror (order ls) ls)) 38: 39: ;;; return the second value of the lazy list 40: (define (lazy-second ls) 41: (lazy-car (lazy-cdr ls))) 42: 43: ;;; further improve the convergency of the list 44: (define (super ls) 45: (lazy-map lazy-second (inf-seq ls improve))) 46: 47: 48: ;;; calculate the differentiation of function `f' at x within error eps 49: ;;; h0 is initial window width 50: (define (diff f x h0 eps) 51: (lazylist->answer (super (lazylist-diff h0 f x)) eps))
> (diff sin 0.0 0.1 0.0000001) 0.9999999999999516 > (diff exp 0.0 0.1 0.000001) 0.9999999991733471
実行例に示すように正常に動作します。
[code 6]
01: ;;; integration 02: 03: ;;; primitive integration 04: (define (easyintegrate f a b) 05: (* (/ (+ (f a) (f b)) 2) (- b a))) 06: 07: ;;; create the lazy list of approximation for integration 08: (define (lazylist-integrate f a b) 09: (let ((mid (/ (+ a b) 2))) 10: (lazy-cons (easyintegrate f a b) 11: (lazy-map + (lazylist-integrate f a mid) 12: (lazylist-integrate f mid b))))) 13: 14: ;;; integrate function `f' in a range of `a' and `b' within error `eps' 15: (define (integrate f a b eps) 16: (lazylist->answer (super (lazylist-integrate f a b)) eps))
> (integrate sin 0 pi 0.0000001) 2.000000002272428 > (integrate exp 0 1 0.0000001) 1.7182818277724858 > (- (exp 1) 1) 1.718281828459045
通常のプログラム言語では繰り返しはそれ用の構文を使って書く必要があったので プログラムのモジュール化には限界があります。一方、遅延評価リストを使うと データに繰り返し計算されるという性質を持たせることができるので、 プログラムを簡潔に書くことができます。
遅延評価についてさらに詳しく知りたい人は Haskell 関連のページを google で探ってみてください。よろしかったら拙作 Haskell のお勉強も見てみてください。
このページで示したコードは 付録につけておきますので気が向いたら ダウンロードして遊んでみてください。
HOME | 16. 継続 | もうひとつの Scheme 入門 | 18. 非決定性 | download | 書き込む |