selflearn @ ウィキ

SICP(1.3 高階手続きによる抽象)

最終更新:

kato

- view
メンバー限定 登録/ログイン

『1.3 高階手続きによる抽象』

まとめ

まずlambdaとletを使うことで「手続き(関数)には必ず命名が必要」という概念が取り払われることを説明している。
そして手続きを渡したり返したりできることによって、手続き間の強力な糊付けが行えることを説明している。

われわれはプログラマとして、プログラムの根底にある抽象部を見つけ、より強力な抽象化が出来るよう、その上に構成し、一般化するよう努めなければならない。

しかし、これはプログラムを可能な限り抽象的に書くべきということではない。経験を積んだプログラマは、自分の仕事に適した抽象のレベルを選ぶことを知っている。

各小節の内容

先の方を少し見てみると、いよいよλが出てきていた。問題1.28の解答例で使われていたletも説明されているし、少しずつ本番かな。

1.3.1 引数としての手続き

手続きを別の手続きに引数として渡すことで、受け取った側ではその手続きを自由に使用できる。
ひな型という抽象化した枠を作り、実際の処理を使い分けるというのは、Javaのインタフェースに似てるなあ。デザインパターンで何と言うんだっけ?

1.3.2 lambdaを使う手続きの構築

ここではlambdaと、そのsyntax sugarであるletについて説明している。

lambda
lambdaは手続きに名前が付かない他は、defineと同様に手続きを作り出すのに使う。
(lambda (<formal-parameters>) <body>)

let
最初に局所変数を任意数定義した上で評価を開始できる。
(let ((<var1> <exp1>)
      (<var2> <exp2>)
           :
      (<varn> <expn>))
     <body>)
上記のように使用し、<var1>は値<exp1>を持ち<var2>は値<exp2>を持ち・・・という前提で式<body>が評価されることになる。

lambda,letは名前に束縛されていない式なだけなので、名前が対応づけられたときにはdefineと同じ意味になる。つまり、以下の3つは同じ1引数を持つ手続きplus4を定義する。

  • (define (plus4 x) (+ x 4))
  • (define plus4 (lambda (x) (+ x 4)))
  • (define (plus4 x) (let ( (a (+ x 4)) ) a)) ← ちょっと無理矢理?

というわけで、

ついに来たよ。λだ!

これまでλという存在は知っていたけれど、何者なのかがよく分かっていなかった。
「名前なしの手続きを作ったって、名前が無いから後で再利用できなくなるんじゃないの?」と思っていたし。

でも、これまでの演習で抽象化を繰り返す中、どうしても名前付きの手続きが増えてきて、それに埋もれてしまってきていた。squareなんかは色んな手続きの中でしょっちゅう使ったから名前がついていた方がありがたいけれど、明らかに特定の手続きの中でしか使わない手続きについても毎回名前を付けるのは、面倒だし意味が無いと思うようになっていた。

だから今はむしろλは必要なんだと思っている。それに再利用できない、というのも同じ手続きの中であれば再帰(反復)的に定義・使用することになるため、何ら問題ないことが分かったし。
実感できるよう、演習で手を動かしていたのも無駄じゃなかったかな。

ところで、lambda,let式は手続きとして使ったり、演算子としても使える。
したがって、

((lambda (x y) (+ x y)) 3 4) → 3 + 4 → 7

としてもいいし、演習問題1.32で定義したアキュムレータの引数combinerとしても使える。

(accumulate (lambda (x y) (+ x y)) 0 (lambda (x) (* x x)) 0 inc 5)

うーん・・・便利だなあ。ただしletはこれまで使ってこなかった書式なので、ちょっと慣れが必要かな。まず外部の変数を使うなどして局所変数を定義しておき、その後評価部を書くんだね。

そして、この局所変数のスコープはよく覚えておかないと。例示されている式:

(define x 2)
(let ((x 3)
      (y (+ x 2)))
  (* x y))

は12が返る。なぜかと言うとこれは次の式と同じだから。局所変数の定義部は外側の変数が使われる。

(define outer_x 2)
(let ((inner_x 3)
      (y (+ outer_x 2)))
  (* inner_x y))

letがlambdaのどういうsyntax sugarであるかを意識することが大切。あと、この書籍においてはlambdaやletも単なる道具でしかなくて、「計算機科学*1」「計算機プログラム*2の構造と解釈」を学ぶのが目的ということも。


1.3.3 一般的方法としての手続き

1.1.4 合成手続き1.3.1 引数としての手続きで学んだ抽象化の概念を推し進めて、lambdaやletに慣れましょう、という節。

例題として、区間二分法による関数の零点と関数の不動点を探索している。

区間二分法
連続関数fに対して、二点a,bがf(a)<0<f(b)を満たすなら、fはaとbの間に少なくとも1つの零点を持つ。
不動点
xがf(x)=xを満たすとき、xを関数fの不動点(fixed point)という。

1.3.4 値として返される手続き

Schemeでは評価結果として、手続きそのものを返してもよい。ということで、とにかく以下のような記述が問題ないことを身体に染み込ませないといけない。
(2007/1/13時点ではものすごく違和感を感じているので・・・)
(define (average-damp f)
  (lambda (x) (average x (f x))))

gosh> ((average-damp square) 10)
55
上のコードは、average-dampが引数とその2乗との平均値を返す手続きを返す、ということを表している。問題1.4でもifの結果によって+か-のどちらかを返し、それを元に続く値を計算するのがあったけれど、今回はそのより一般的な方法として紹介されている。

そして、このaverage-dampを使うとfixed-pointは次のように書き換えられる。
この辺は大事なところだから、古いsqrt手続きを全て書き写しているよ。
初版:(1.1.7(p.13)のsqrt)
(define (sqrt x)
  (define (sqrt-itr guess x)
    (if (good-enough? guess x)
        guess
        (sqrt-itr (improve guess x) x)))
  (sqrt-itr 1.0 x))

旧版その2:(1.3.3(p.39)の不動点探索)
(define (sqrt x)
  (fixed-point (lambda (y) (/ x y)) 1.0))

旧版その3:(1.3.3(p.39)の不動点探索。平均緩和を使用)
(define (sqrt x)
  (fixed-point (lambda (y) (average y (/ x y))) 1.0))

新版その1:(不動点探索、平均緩和を使用)
(define (sqrt x)
  (fixed-point (average-damp (lambda (y) (/ x y))) 1.0))

新版その2:(Newton法を使用)
(define (sqrt x)
  (fixed-point (average-damp (lambda (y) (/ x y))) 1.0))

最終版:さらに抽象化
(define (fixed-point-of-transform g transform guess)
  (fixed-point (transform g) guess))
新版その1'
(define (sqrt x)
  (fixed-point-of-transform  (lambda (y) (/ x y))
                             average-damp
                             1.0))
新版その2'
(define (sqrt x)
  (fixed-point-of-transform  (lambda (y) (- (square y) x))
                             newton-transform
                             1.0))
以上のように変化・抽象化できる。プリミティブな記述からだんだん処理そのものを隠していき、かつ変更に対して柔軟な設計なっているのが分かる。

ここで新版その1を見ると、旧版その2・その3と大して違っていないように見える。しかし、式の構造を見ると、「ある式」に対して「平均緩和」を使用し「不動点を探索する」ことで平方根を得ていることがとても分かりやすくなっている。

すごいのが、average-dampを追加しても上位のfixed-point手続きに手を加えずに済んでいる点。リファクタリングを行う場合、大抵は変更の余波はその上下に広がるものだけれど、Lispでは局部の変更のみで対処できている。
分かりやすい例ではあったけれど、手続きを返戻値として渡せるすごさを垣間みた感じ。

あと、本節ではよく言われる「ファーストクラス要素*3」の定義についての説明が載っている。
  1. 変数として名前がつけられる
  2. 手続きに引数として渡せる
  3. 手続きの結果として返される
  4. データ構造に組み込める(2章で紹介されるらしい)
確かにLispはどれも満たしている。Cの関数はどうだろう。4)が中途半端だからダメだ。でもC++ならクラスの中に組み込めるから、ファーストクラスになるのかな。

タグ:

SICP scheme
ウィキ募集バナー
注釈

*1 Computer Science.

*2 Computer Program.カタカナで書いた方がしっくりきますね。

*3 ファーストクラスオブジェクトだったり、身分だったり。