[ 目次, 前節, 次節, 索引 ]

2.2.4 例: 図形言語



本節では, データ抽象と閉包の能力を示す, 絵を描くための単純な言語を紹介し, また高階手続きを本質的に利用する. この言語は基本図形を繰り返しずらしたり大きさを変えたりして作った図2.9のようなパターンの実験を容易にするよう設計した.22 この言語では組み合されたオブジェクトはリスト構造としてではなく, 手続きとして表現されている. 閉包性を満し, いか様にも複雑なリスト構造を構成するのが容易なcons と同様に, 閉包性を満すこの言語の演算は, いか様にも複雑なパターンを構成するのを容易にする.



図2.9 図形言語で生成した図案

図形言語
1.1節でプログラミングの勉強を始めた時, 言語をその基本機能, 組合せの手段, 抽象の手段に注目して記述することが重要だと力説した. ここでもその方針に従う.

   この図形言語の美しさの一部は ペインタ(painter)というただ一種の要素しかないことにある. ペインタは規定した並行四辺形の フレームの中に合うように, ずらしたり大きさを変えたりした画像を描く. 例えばwaveという基本的ペインタがあり, 図2.10のような素材の線画を作る. 画の実際の形はフレームに依存する. 図2.10の四枚の画像は同じ waveペインタで作ったものだが, 四つの異るフレームに対応している. ペインタはこれより更にすばらしい: rogersという基本的ペインタは図2.11のようなMITの創始者, William Barton Rogersの絵を描く.23 図2.11の四枚の画像は図2.10のwave画像と同じフレームに対応して描かれた.

   画像を組み合せるには, 与えられたペインタから新しいペインタを構成するいろいろな演算を使う. 例えば beside演算は二つのペインタをとり, 第一のペインタの画像をフレームの左半分に描き, 第二のペインタの画像をフレームの右半分に描く新しい合成ペインタを作り出す. 同様に belowは二つのペインタをとり, 第一のペインタの画像を第二のペインタの画像の下に描く合成ペインタを作る. いくつかの演算は一個のペインタを変換して新しいペインタを作る. 例えば flip-vertはペインタを一つとり, その上下逆転の画像を描くペインタを作り, flip-horizは元のペインタの画像を左右逆転したものを描くペインタを作る.



図2.10 四つの異るフレームに対応してwaveペインタが作った画像. 破線で示すフレームは画像の部分ではない.



図2.11 図2.10と同じ四つのフレームに対応して描いたMITの創設者で初代学長であったWilliam Barton Rogersの画像(元の画像はMIT博物館の許可を得て複製した.)

   図2.12はwaveから出発し, 二段階で構成したwave4というペインタの描いたものを示す:

(define wave2 (beside wave (flip-vert wave)))
(define wave4 (below wave2 wave2))




図2.12 図2.10のwaveペインタから出発し, 複雑な形を作る

このような方法で複雑な画像を構築するには, ペインタは言語の組合せの仕方の下で閉じているという事実を利用する. 二つのペインタのbeside belowしたもの自身もペインタである; 従ってそれらを更に複雑なペインタを作る要素として使うことが出来る. consを使ってリスト構造を組み上げるのと同様に, 組合せの仕方の下でのわれわれのデータの閉包性は, ほんのわずかな演算を使いながら複雑な構造を作り上げる能力にとり, 重要である.

   ペインタが組み合せられれば, ペインタを組み合せる代表的パターンを抽象化することが出来るようにしたい. ペインタの演算をSchemeの手続きとして実装しよう. このことは図形言語に特別な抽象機構はいらないということを意味する: 組合せの仕方は通常のSchemeの手続きなので, 手続きを使って出来ることなら, ペインタの演算についても何だって出来ることになる. 例えば wave4のパターンを


(define (flipped-pairs painter)
  (let ((painter2 (beside painter (flip-vert painter))))
    (below painter2 painter2)))
のように抽象化し, wave4
(define wave4 (flipped-pairs wave))
のように, このパターンの具体化として定義出来る.

   再帰的演算も定義出来る. 図2.13や2.14のようにペインタを右の方へ分割, 枝分れさせるものである:


(define (right-split painter n)
  (if (= n 0)
      painter
      (let ((smaller (right-split painter (- n 1))))
        (beside painter (below smaller smaller)))))




図2.13 right-splitcorner-splitの再帰的計画

右だけでなく, 上へも枝分れさせ, バランスのとれたパターンが作れる(問題2.44, 図2.13, 2.14参照):

(define (corner-split painter n)
  (if (= n 0)
      painter
      (let ((up (up-split painter (- n 1)))
            (right (right-split painter (- n 1))))
        (let ((top-left (beside up up))
              (bottom-right (below right right))
              (corner (corner-split painter (- n 1))))
          (beside (below painter top-left)
                  (below bottom-right corner))))))




図2.14 ペインタwaverogersに作用させた再帰的演算 right-splitcorner-split. 四つのcorner-split図形を組み合せると, 図2.9に示したような対称的なsquare-limit図形が出来る:

corner-splitの四つのコピーを適切に配置することで, square-limitというパターンが得られ, それをwaverogersに作用させると, 図2.9が出来る:

(define (square-limit painter n)
  (let ((quarter (corner-split painter n)))
    (let ((half (beside (flip-horiz quarter) quarter)))
      (below (flip-vert half) half))))

問題 2.44


corner-splitで使った手続き up-splitを定義せよ. belowbesideの働きを切り替える他は, right-splitと同様である.
高階演算
ペインタを組み合せたパターンの抽象化に加え, ペインタ演算を組み合せ, パターンを抽象化することで, 高いレベルでの作業が出来る. つまりペインタ演算を操作の要素と見, これらの要素に対する組合せ方法---引数としてペインタ演算をとり, 新しいペインタ演算を作り出す手続き---を書くことが出来る.

   例えばflipped-pairssquare-limitはどちらもペインタの画像の四つのコピーを四角のパターンの中に配置する; それらはコピーの向きだけが異る. ペインタの組合せのパターンを抽象化する一つの方法は, 次の手続きによるもので, それらは四つの一引数ペインタ演算をとり, 与えられたペインタをこの四つの演算で変換し, 結果を四角の中に配置するペインタ演算を作り出す. tl, tr, blおよびbrはそれぞれ上左コピー, 上右コピー, 下左コピーおよび下右コピーに作用させる変換である.


(define (square-of-four tl tr bl br)
  (lambda (painter)
    (let ((top (beside (tl painter) (tr painter)))
          (bottom (beside (bl painter) (br painter))))
      (below bottom top))))
こうなるとflipped-pairssquare-of-fourを使い, 次のように定義出来る:24

(define (flipped-pairs painter)
  (let ((combine4 (square-of-four identity flip-vert
                                  identity flip-vert)))
    (combine4 painter)))
そしてsquare-limit

(define (square-limit painter n)
  (let ((combine4 (square-of-four flip-horiz identity
                                  rotate180 flip-vert)))
    (combine4 (corner-split painter n))))
のように表せる.25

問題 2.45


right-splitup-splitは, 汎用分割演算の具体化として表すことが出来る.
(define right-split (split beside below))
(define up-split (split below beside))
の評価が, すでに定義したものと同じ振舞いの手続きright-splitup-splitを作り出すような性質を持つ手続き splitを定義せよ.
フレーム
ペインタの実装法と組合せ法を示す前に, まず フレームを考えなければならない. フレームは三つのベクタ---原点ベクタと二つの辺ベクタ---で記述出来る. 原点ベクタは平面のある絶対原点からのフレームの原点の変位を指定し, 辺ベクタはフレームの頂点の原点からの変位を指定する. 辺が直交していれば, フレームは長方形である. そうでなければフレームはもっと一般的な平行四辺形である.    図2.15はフレームと関係するベクタを示す. データ抽象に従えば, 三つのベクタをとり, フレームを作る構成子 make- frameと, 対応する三つの選択子 origin-frame, edge1-frameおよび edge2-frameがあるという他は, フレームがどう表現されているかは規定する必要はない(問題2.47参照).



図2.15 フレームは三つのベクタ, 原点と二つの辺[edge]で記述する.

   画像を規定するのに単位方形(0 ≤ x, y ≤ 1)の座標を使おう. 各フレームに, 画像をずらし, 大きさを変えてフレームに合せるのに使う フレーム座標写像(frame coordinate map)を対応づける. 画像はベクタv=(x, y)をベクタ和



に写像することで, 単位方形をフレームに変換する. 例えば(0, 0)はフレームの原点に, (1, 1)は原点と対角線上の反対にある頂点に, (0.5, 0.5)はフレームの中心に写像される. フレーム座標写像は次の手続きで作ることが出来る:26


(define (frame-coord-map frame)
  (lambda (v)
    (add-vect
     (origin-frame frame)
     (add-vect (scale-vect (xcor-vect v)
                           (edge1-frame frame))
               (scale-vect (ycor-vect v)
                           (edge2-frame frame))))))
frame-coord-mapをフレームに作用させると, ベクタを貰いベクタを返す手続きが返ってくることに注意しよう. 引数のベクタが単位方形にあると, 結果のベクタはフレームにある. 例えば
((frame-coord-map a-frame) (make-vect 0 0))
(origin-frame a-frame)
と同じベクタを返す.

問題 2.46


原点からある点へ向う二次元のベクタvx座標とy座標からなる対で表現出来る. 構成子 make-vectと, 対応する選択子 xcor-vect ycor-vectを作り, ベクタのデータ抽象を実装せよ. その選択子と構成子を使い, ベクタの加算, 減算, スカラーによる乗算:



の演算を実行する手続き add-vect, sub-vectおよび scale-vectを実装せよ.

問題 2.47


フレームの構成子に候補が二つある:

(define (make-frame origin edge1 edge2)
  (list origin edge1 edge2))

(define (make-frame origin edge1 edge2)
  (cons origin (cons edge1 edge2)))
構成子のそれぞれに, フレームの実装となる適切な選択子を作れ.
ペインタ
ペインタは, あるフレームを引数として貰うと, そのフレームに合うように特定の画像をずらしたり大きさを変えたりして描く手続きとして表現されている. いい替えれば, pをペインタ, fをフレームとすれば, fを引数としてpを呼び出すことで, fの中にpの画像を描くことが出来る.

   基本的ペインタがどう実装してあるかの細部は, 図形システムの性格や描く絵の種類に依存する. 例えばスクリーン上の二つの指定した点の間に線を引く手続き draw-lineがあるとしよう. そうすれば図2.10のwaveペインタのような線画のペインタを, 次のような線分のリストから作り出すことが出来る:27


(define (segments->painter segment-list)
  (lambda (frame)
    (for-each
     (lambda (segment)
       (draw-line
        ((frame-coord-map frame) (start-segment segment))
        ((frame-coord-map frame) (end-segment segment))))
     segment-list)))
線分は単位方形に対する座標を使って与えられている. リスト内の各線分に対してペインタはフレーム座標写像を使い, 線分端点を変換し, 変換された点の間に線を引く.

   ペインタを手続きとして表現することは, 図形言語において強力な抽象の壁を建てる. われわれはいろいろな図形機能を使い, 多くの基本的ペインタを作り出し,混合出来る. その実装の細部は関係ない. どんな手続きでも, 引数としてフレームをとり, そのフレームに合うように大きさを変え, 何か描く手続きならペインタとして使える.28

問題 2.48


平面上の有向線分はベクタの対---原点から線分の始点へ向うベクタと, 始点から線分の終点へ向うベクタ---で表現出来る. 問題2.46のベクタ表現を使い, この線分の表現を構成子 make-segmentと選択子 start-segmentおよび end-segmentとして定義せよ.

問題 2.49


segments->painterを使い, 次の基本的ペインタを定義せよ:

a. 指定されたフレームの外形を描くペインタ.

b. フレームの向い側の頂点を結んで「X」を描くペインタ.

c. フレームの辺の中点を結んで菱形を描くペインタ.

d. waveペインタ.

ペインタの変換と組合せ
(flip-vertbesideのような)ペインタの演算は, 引数のフレームから作られたフレームに関して, 元のペインタを発動する新しいペインタを作り出すことである. そこで, 例えばflip-vertはペインタが上下逆転するのにどう働くかを知る必要はない. フレームを上下逆転させることだけ知っていればよい: 上下逆転されたペインタは, 元のペインタを, 逆転フレームの中で使う.

   ペインタの演算は, 引数としてペインタとフレームの変換法の情報をとり, 新しいペインタを作る手続きtransform-painterに基づく. 変換されたペインタは, フレームに対して呼び出されると, フレームを変換し, 変換されたフレームに対して元のペインタを呼び出す. transform-painterの引数は(ベクタで表現されている)点で, それが新しいフレームの隅を規定する. フレームへ写像されると, 第一の点は新しいフレームの原点を規定し, 後の二点はその辺ベクタの端点を規定する. そこで, 単位方形の中の引数は, 元のフレームに含まれるフレームを規定することになる.


(define (transform-painter painter origin corner1 corner2)
  (lambda (frame)
    (let ((m (frame-coord-map frame)))
      (let ((new-origin (m origin)))
        (painter
         (make-frame new-origin
                     (sub-vect (m corner1) new-origin)
                     (sub-vect (m corner2) new-origin)))))))

   ペインタの画像を上下逆転する方法はこうだ:


(define (flip-vert painter)
  (transform-painter painter
                     (make-vect 0.0 1.0)   ; 新しいorigin
                     (make-vect 1.0 1.0)   ;edge1の新しい端点
                     (make-vect 0.0 0.0))) ;edge2の新しい端点
transform-painterを使えば, 新しい変換を定義するのは簡単だ. 例えば与えられたフレームの右上の四半分に画像を縮めるペインタを定義することが出来る:

(define (shrink-to-upper-right painter)
  (transform-painter painter
                     (make-vect 0.5 0.5)
                     (make-vect 1.0 0.5)
                     (make-vect 0.5 1.0)))
他には画像を反時計回りに90度回転する変換:29

(define (rotate90 painter)
  (transform-painter painter
                     (make-vect 1.0 0.0)
                     (make-vect 1.0 1.0)
                     (make-vect 0.0 0.0)))
や, あるいは画像をフレームの中央へ押し込む変換:30

(define (squash-inwards painter)
  (transform-painter painter
                     (make-vect 0.0 0.0)
                     (make-vect 0.65 0.35)
                     (make-vect 0.35 0.65)))
がある.

   フレーム変換は二つまたはそれ以上のペインタの組合せ手段を定義する鍵ともなる. 例えばbeside手続きは二つのペインタをとり, それらをそれぞれ引数フレームの左と右半分に描くように変換し, 新しい合成ペインタを作る. 合成ペインタは, フレームを貰うと, まず第一の変換されたペインタを呼び出し, フレームの左半分に描かせ, 次に第二の変換されたペインタを呼び出し, フレームの右半分に描かせる:


(define (beside painter1 painter2)
  (let ((split-point (make-vect 0.5 0.0)))
    (let ((paint-left
           (transform-painter painter1
                              (make-vect 0.0 0.0)
                              split-point
                              (make-vect 0.0 1.0)))
          (paint-right
           (transform-painter painter2
                              split-point
                              (make-vect 1.0 0.0)
                              (make-vect 0.5 1.0))))
      (lambda (frame)
        (paint-left frame)
        (paint-right frame)))))

   ペインタのデータ抽象, 特にペインタの手続きとしての表現が, besideの実装を容易にしているところをよく見よう. beside手続きは, 要素ペインタについての細部はそれぞれのペインタが指示されたフレームに何か書くということ以外, 何も知らなくてよい.

問題 2.50


ペインタを水平に逆転する変換flip-horizと, ペインタを反時計回りに180度と270度回転する変換を定義せよ.

問題 2.51


ペインタに対してbelow演算を定義せよ. belowは引数として二つのペインタをとる. 結果のペインタはフレームを貰うと, 第一のペインタでフレームの下に描き, 第二のペインタで上に描く. belowを二つの異った方法で--- まず上のbesideに似た方法で, 次にbesideと(問題2.50の)適切な回転演算を使って---定義せよ.

頑健な設計のための言語レベル
図形言語は, 手続きとデータを使った抽象についての重要な考え方を使ってきた. 基本的なデータ抽象であるペインタは, 手続き表現を使って実装され, それが言語にいろいろな基本描画能力を一様な方法で扱うことを可能とした. 組合せの方法は閉包性を満足させ, 複雑な形が容易に構成出来るようになった. 最後に, 手続きを抽象化する道具はすべて, ペインタの組合せ手段を抽象化するのに使うことが出来る.

   また言語とプログラムの設計に重要な別の考えも少しは見た. これは 成層設計(stratified design)の考えである. つまり複雑なシステムは, それぞれの言語を使って記述出来るレベルの並びとして構造化されるべきだという考え方である. 各レベルは, そのレベルで基本的と見なせる部品を組み合せて構成する. また各レベルで構成される部品は, 次のレベルで基本要素として使われる. 成層設計の各レベルで使う言語は基本要素, 組合せ手段, その細部のレベルに適した抽象手段を持つ.

   成層設計は複雑システムの工学に広くいきわたっている. 例えば計算機工学では, 抵抗やトランジスタが組み合され, (またアナログ回路の言語を使って記述され,) アンドゲートやオアゲートのような部品を作り出し, ゲート類がディジタル回路設計の言語の基本要素となる.31 これらの部品が組み合されて, プロセッサ, バス回路, メモリーシステムを構成する. それらがまた, 計算機アーキテクチャに適した言語を使って組み合され, 計算機を構成する. 計算機はネットワーク相互接続に適した言語を使って組み合され, 分散システムを構成する.

   成層設計の小さい例として, 図形言語はsegments->painterのための線分のリストを作る点や線を規定する言語や, rogersのようなペインタのための詳細な濃淡法を使って作り出された基本要素(基本ペインタ)を使う. われわれの図形言語の大部分は, besidebelowのような幾何学的組合せ子を使ってのこれらの基本要素の組合せに絞られていた. またbesidebelowを, square-of-fourのような演算が幾何学的組合せ子を組み合せる共通パターンを取り込む言語で操作される基本要素と見て, 高いレベルで仕事をした.

   成層設計は, プログラムを頑健(robust)に作るのに役立つ. つまり仕様の小規模な変更は, なるべくプログラムの相応な小規模な変更しか必要としないようにする. 例えば図2.9に示すwaveに基づく画像を変更したかったとする. waveの要素の細かい形を変えようとする最低レベルで作業することも出来る; corner-splitwaveを複製する方法を変えようとする中レベルで作業することも出来る; square-limitが隅の四つのコピーをどう配置するかという最高レベルで作業することも出来る. 一般に, 成層設計の各レベルには, システムの性格を表す語彙, それを変更する異った種類の能力がある.

問題 2.52


図2.9に示したwavesquare-limitを, 上に示した各レベルで作業して変更せよ. 特に

a. 問題2.49の基本的waveペインタに(例えば笑っているような)線分を加えよ.

b. corner-splitで構成されるパターンを(例えば二つでなく, up-splitright-splitのコピーを使うことで)変更せよ.

c. square-limitsquare-of-fourを使う版を, 隅を異るパターン(例えばRogers氏を, 四角の隅では外側を向せるように)修正せよ.



22 この図形言語は M.C.Escherの木版「Square Limit」のような画像を構成すべく, Peter Hendersonが創作した言語に基づいている(Henderson 1982参照). この木版は, 本節のsquare-limit手続きを使って描かれた配置と似た, 大きさを変えつつ繰り返すパターンで構成してある.

23 William Barton Rogers(1804--1882)はMITの創設者で, また初代総長であった. 地質学者として, また才能ある教師として彼はWilliam and Mary大学とバージニア大学で教えた. 1859年にボストンへ移り, そこでは研究の時間が増え, 「工科大学」設立を計画し, マサチューセッツで最初のガスメータの州検査官を勤めた.

   1861年にMITが設立されると, 初代総長に選ばれた. Rogersは古典を重要視することで, 当時の他の大学教育とは異る「有用な学習」の考えを支持した. それは彼の書いたところによると「自然科学と社会科学における, 広範, 高度でより実用的な教育と訓練の方法に参加する」ものであった. この教育はまた商業学校の狭い教育からも異る筈であった. Rogersは書いている:

世間の押しつける実用的な研究者と科学的な研究者の区別はまったく下らない. またモダンタイムズの経験は全体としてまったくの無価値を示した.

   Rogersは健康上の理由で退職する1870年まで総長を勤めた. 二代目総長 John Runkleが, 1873年の恐慌でもたらされた財政危機の圧力と, HarvardによるMITの乗っとりの試みとの戦いでの働き過ぎで1878年に退任し, Rogersは戻って1881年まで総長の職にあった.

   Rogersは1882年の卒業式の予行で, 大学院のクラスに演説中, 崩れ折れて亡くなった. Runkleは同年行われた追悼演説でRogersの最後の言葉を引用した.

「本日ここに来て, 工科大学の現状を見ると, 私は科学の最初を思い出す. 私の記憶では百五十年前, Stephen Halesは輝くガスについてパンフレットを出版した. その中で彼がいうには彼の研究が示したのは128粒の 瀝青炭が---」

「瀝青炭」これが彼の最後の言葉であった. その時彼は前のテーブルのノートを見ようとするかのように前に屈み, またゆっくり直立の姿勢に戻りながら手をふり, この世の労働と勝利の情景から, 生命の神秘が解明され肉体から離れた精神が無限の未来の新しくしかも不可解な神秘を熟考しながら永遠の満足を見出す「死の向う」へ移された.

Francis A. Walker(第三代MIT総長)の言葉:

彼は人生を忠実に勇敢に生き, 装いを尽くし, 己の職場で, 公職の中に, 騎士さえも望むような高貴な死を遂げた.

24 等価的に次のようにも書ける.
(define flipped-pairs
  (square-of-four identity flip-vert identity flip-vert))
25 rotate180はペインタを180度回転する(問題2.50参照). rotate180の代りに問題1.42のcompose手続きを使って(compose flip-vert flip-horiz)とすることも出来る.

26 frame-coord-mapは次の問題2.46のベクタ演算を使う. ベクタは適当な表現で実装されていると仮定する. データ抽象の故に, ベクタ演算が正しく振舞う限り, ベクタ表現が何であるかには関係しない.

27 segments->painterは次の問題2.48にある線分の表現を使っている. それはまた問題2.23にあったfor-each手続きも使っている.

28 例えば, 図2.11のrogersペインタは濃淡画像から作られた. フレーム内の各点に対し, rogersペインタはフレーム座標写像の下で, その点に写像される画像の点を決め, 然るべく濃淡をつける. いろいろな型のペインタを認めることで, 2.1.3節(そこで, 有理数表現は, 適切な条件を満足すれば, どんなものでもよいことを議論した)で論じた, データ抽象の考え方が利用出来る. ここではペインタはそれが指定されたフレームに何か描きさえすれば, どんな方法ででも実装出来るという事実を使っている. 2.1.3節はまた対が手続きとしてどう実装出来るかも示した. ペインタはデータの手続き表現の第二の例である.

29 rotate90は回転したフレームに合せるため, 画像を伸び縮みもさせるので, 正方形のフレームの時だけが, 純粋な回転である.

30 図2.10と2.11の菱形の画像は, waverogerssquash-inwardsを作用させて作り出した.

31 3.3.4節でそのような言語を説明する.

[ 目次, 前節, 次節, 索引 ]