SchemeAndImpl / 2.Introduction


SchemeAndImpl

導入

この章では、何かしらのプログラムを書き始めるにあたって十分なほどのSchemeの基本的な特徴について、ざっと紹介します。

この章では、Schemeの大体半分くらいの考え方を紹介し、ささっと進んでいきます。あとのほうの章では、これらの特徴をもっとたっぷりと説明し、デモも行い、他の先進的な特徴についても紹介する予定です。

この章は、次の章の前半部分と一緒に読んでもらうように考えています。そちらは会話的にSchemeを使ったチュートリアルを含んでいます。次の章に進んで実際にやってみて欲しいときには、そのように指示を出します。Schemeに親しんだ後には、それは基本的なリファレンスになうでしょう。次の章で基本的な例を引くことができ、また後の章では先進的な技術を見るでしょう。

もしあなたがプログラミング言語のコンセプトについてよく知っているのなら、特に、Lispでプログラムしたことがあるなら、この章はSchemeがどんなものか感覚をつかむだけにして、ざっと流すことができるでしょう。もしプログラミング言語のコンセプトが分かっていれば、このセクションを難なく読み通すことができるでしょう。

(私のクラスの学生たちに注意:これをざっと読み飛ばしたりはしないように。この章で、hunk(かたまり)という指示が出てきたら、チュートリアルのほうの対応するhunkをやってみるように。---PRW*1)

Schemeで実際にプログラムを組むつもりであれば、ただこの章をまっすぐ読み進むのではなくて、指示が出てきたときにその指示にしたがって、次の章の該当する部分を読んで見てください。

Schemeとは何か (Hunk A)

================================================================== Hunk A の始まり: ==================================================================

まずは、コンピュータ用語(ジャーゴン)の嵐となります。無視してもらっても結構です。

Schemeは、静的スコープであって、ブロック構造で、動的に型が指定され、大部分は関数的な言語です。それはLispの一形態です。ブロック構造で限りのない広がりのファーストクラスの手続きを持ちます。パラメータは値渡しですが、その値は参照です。ファーストクラスの継続を持ちますが、それにより、新しい制御抽象の構築することが可能です。それは静的で「衛生的な*2」マクロを持ち、それにより、新しい統辞の定義が可能となり、古い統辞を再定義することも可能です。

もし何の意味か分からなければ、心配しないで*3。読み進めてください。

Schemeは会話的で安全な言語として設計されています。通常のSchemeシステムは本当に自分の欲しい順番でSchemeプログラムの部品を走らせることのできる、会話的なシステムです。走らせてみて、自分のプログラムが終わらなくてもデータが消えたりはしません--Schemeは次にどうしたいかを聞いてくるので、あなたはデータを調べて、Schemeにプログラムの別の部分を走らせてくれるように指示を出すこともできます。

Schemeは、会話的システムが一般にクラッシュしないという意味で安全です。システムをクラッシュする過ちをあなたが犯したなら、Schemeはそれを察知し、それをどうするかについて聞いてきます。それで、あなたはシステムの状態を調べて、変えて、続行することができます。これは、C や C++ といった普通の「編集−コンパイルーリンクー実行ークラッシュ」サイクルの「バッチ」プログラミングとは随分と違ったプログラムおよびデバッグスタイルです。

Schemeの基礎:Schemeの基礎をなす特徴

Schemeの基礎的な特徴について短く触れていきます。明確にするために少しコードも示します。

式:式からなるコード

Lispのように、Schemeは接頭辞のある式として、括弧でグループにされて、書かれます。接頭辞とは、操作(operation)の対象となるオペランドの最初に来るオペレーションの名前を意味します。

Schemeでは、式(expression)(算術演算子のような)と文(statement)(ifloopdeclarationのような)の間に区別はありません。それらはすべて「式」です。---とても普遍的な(general)術語です。

接頭式:括弧でくくられた接頭式

C や Pascal では、引数を bar と baz として、foo 手続きを呼ぶには、こうなります。

foo(bar,baz);

ですが、Schemeにおいては、このように書きます。

(foo bar baz)

手続きの名前が括弧の中に、引数と同じく入っていることに注意してください。それに慣なれるように。OSのシェルコマンドのようなもの -- 例えば、rm foodir bar -- だと考えれば、違和感は薄らぐかも知れません。それが括弧で区切られているのです。

ちょうど C におけるように、式はネストできます。引数を計算するためにネストした手続き呼び出し式を含んだ、手続き foo の呼び出しを示します。

(foo (bar x) (baz y))

これは C で書けば、これとほぼ同じことです。

foo(bar(x),baz(y));

C や Pascal のように、手続きを呼ぶ際の引数の式は、実際に手続きを呼び出す前に評価されます。Scheme の専門用語では、手続きは実際の引数の値に対して、適用されるといいます。

Schemeには特別な文字がほとんどないことにすぐ気が付くでしょう、そして 式は大抵、括弧やスペースで区切られていることにも。例えば、a-variable は、単一の識別子であり、引き算の式ではありません。 Schemeでの識別子は英数字だけではなく、!, ?, -, _ といった他の文字も含むことができます。 長い識別子はしばしば、句であったりしますので、意味をはっきりさせるために、単語を区切るのにハイフンを使います。 例えば、list-of-first-ten-lists という変数が使えます。 before-tax-total+taxestimate+epsilonのように、 +, -, *, / といった文字が識別子の中で使えます。

Schemeでは、識別子を構築するルールが自由なので、スペースが重要となっています。特別な文字(普通、() のこと)で区切られているのでなければ、識別子の間に1つ以上のスペース(あるいは改行)を、区切りがはっきりするように置かなければなりません。 例えば、(+ 1 a) といった足し算の式は、(+1 a)(+1a)(+ 1a) のようには書けません。(それは、( + 1 a ) のように書くことはできます、なぜならトークンの間の余分なスペースは無視されるからです。)


値と副作用:式は値を返すが、副作用があるかも知れない

Scheme の式は、式と命令(statement)を指します。それらは値を返しますが、副作用も持っている可能性があります。とういのは、変数やオブジェクトの状態を代入によって変えてしまうかも知れないからです。

Scheme での変数代入の操作は、set!で、「セット・バン」と発音します。変数に値 3を代入したちときには、こう書きます。

(set! foo 3)

which is pretty much equivalent to C's これは、C でいえば、これとほぼ同じです。

foo = 3;

ここで気を付けて欲しいのは、なんでも接頭辞の記法を使うので、(set! foo 3) も、関数呼び出しのように見えるのですが、これは本当は呼び出しではないという点です。違った種類の式に過ぎないのです。

Schemeプログラムではあまり多くの代入を使うべきではありません。後でも説明しますが、大抵それは悪いスタイルの兆候です。あまり副作用を起こさずにプログラムできるスタイルもお見せします。もし、副作用が必要なときには、そうもできますが。

ただ値を返すだけではなく、引数を変更してしまう手続きを書くならば、その名前がびっくりマーク(!)で終わるようにするのが、良いスタイルです。これによって、自分やほかの人がコードを読んだときに、この手続きが、新しいデータ構造などの値をただ返すだけではなく、既にあるものを何か変えてしまうのだと思い出させてくれます。 状態を変更する標準的なSchemeの手続きはこのように名前が振られています。

ですが、大抵の Scheme の手続きは何も変更しないのです。例えば、標準手続きである reverse ですが、これはリストを引数に取り、同じ要素のリストを逆順に並べて返します。それは、オリジナルを変更することなしに、オリジナルのリストを逆順にコピーしたものを返しているのです。もしあなたが、同じリストを返すことにして、リストの中身の要素が逆順に並ぶように変更してしまうのなら、それをreverse! と呼ぶべきです。 これでreverse!に渡したリストは、変えられてしまうかもしれないと警告を発していることになります。

副作用のある手続きを例に出します。display です。display は値を取り、それを画面やファイルに印字できる形で書きます。ひとつの引数を与えれば、それは「標準出力」に書きます。それは、既定では、端末であったり他の画面だったりします。

例として、あなたが 1022という数値の印字形式をユーザに見せたいとしたなら、 この式を使えます。

(display 1022)

この式を実行することの副作用は、1022 をユーザの画面に書き出すことです。(display は、自動的に数値を文字列に変換しているので、あなたが読むことができるのです。)

注意していただきたいのは、display には、最後にびっくりマークがついていないことで、なぜかといえば、印字してもらおうとして渡した引数には副作用が及ばないからです。あるデータ構造を渡しても、変更されないと確信してよいのです。displayには、確かに副作用はありますが、書き出す画面(またはファイル)の状態を変えるのです。

displayはとても柔軟であり、通常のSchemeのオブジェクトを印字できる形式で出力でき、もっと複雑なデータ構造であっても出力できます。

他の多くの事柄は別にしても*4、基本的にdisplay は文字列を印字できます。(文字列は、またひとつのSchemeオブジェクトなのです。二重引用符のなかにリテラル文字列が書けます、"このように"。そして Schemeはその文字の連なりを持っておくために文字列オブジェクトを作成します。)

(display "Hello, world!) は、標準出力にHello, world! と出力する副作用があります。大抵はユーザの画面に出力されます。

このことは、(display "Hello, world!) をデバッグ用に大変有益なものとしています。対話的にプログラムを書くときには、ほとんど例はありませんが。同じような手続きであるwrite はデータ構造をファイルに保存するために使われます。それからそれをメモリから read を使って読み込むことができます。

後の章で、display に2番目の引数を渡して、ファイルへ書くやり方をお見せするつもりです。今は、ただ1つの引数を使用していてください。いくつかの物をdisplay に渡してみたりはしないでください

変数定義と手続き定義

Schemeでは変数を定義するときには、define が使えます。

(define my-variable 5)

これは、my-variable のためにスペースを配置して、その領域を値 5で初期化するようにSchemeに指示することになります。

Scheme では、変数には初期値をいつも与えるので、初期化していない変数や、それゆえのエラーというのはないのです。

Scheme の値はいつもオブジェクトへのポインタであるので、われわれがリテラルの 5を使うとき、Schemeはそれを 5というオブジェクトへのポインタという意味に解します。数値は自らを指すポインタを持つことのできる、オブジェクトであり、この点は他のデータ構造と同様です。(実際には、大抵の Schemeno実装は数値へのポインタのオーバーヘッドを避けるトリックを2,3使っています。ですが、言語レベルではそれは見えません。それに気付いている必要もないです。)

上記の定義の後、その結果として生じている状況はこのように描けます。

      +-------+
  foo |   *---+--->5
      +-------+

define 式は3つのことを行ないます。

  • それは、Schemeに対し、今のスコープで fooという名前の変数を持つつもりだ、とSchemeに宣言します。(スコープについてはずっと後で話します。)
  • それは Scheme に、変数のための領域を実際に割り当ててくれるように告げます。領域は束縛(binding)と呼ばれます --- われわれは変数 foo をメモリのある特定箇所に「束縛」し、それゆえにその領域を foo という名前で参照することができます。
  • それは Scheme に、その領域に何の初期値を入れるかを教えます。

これら3つの事柄は、他の言語で変数を定義したときにも起きていることです。Schemeでは、これら3つに名前が付いているわけです。*5

図では、箱はSchemeが変数のために領域を割り当てたことを表しています。箱の横にある foo とうい名前は、その領域に foo という名前をわれわれが与えたことを指します。矢印は箱の中の値が、整数オブジェクト 5へのポインタであることを意味しています。(整数オブジェクトがどのように実現されているのかは心配しないでください。大したことではありません。)

defineでは、新しい手続きも定義できます。

(define (two-times x)
   (+ x x))

ここでわれわれは two-times という、x という1つの引数を取る手続きを定義しました。その手続きは加算の手続きである + を呼び出し、引数の値をその値自身に加算し、その結果を返します。

変数定義と手続き定義の文法的な違いに気をつけてください。手続き定義では、名前の周りに括弧があって、引数の名前は、括弧の中に続いています。

これは手続きの呼ばれ方と似ています。手続きの呼び出し式(two-times 5)を考えてみてください。これは 10を返しますね。定義の(two-times 5)にそっくりで、違う点は仮引数の変わりに実際の引数 5を与えている点です。

ここで、知っておくべきプログラミング言語の専門用語があります。あなたが手続きに渡す引数はときに実引数と呼ばれます。手続きの内部の引数変数は、仮引数(formal parameters) --- それは実行時に手続きに渡される実際のものを表すわけですが。「実」とは手続きに実際に渡すという意味で、「仮(formal)」というのは、手続きの内部ではそう呼んでいるという意味です。大抵は、私が「引数」のことを話すときには、「実引数」のことです。ときどき「引数変数」について話すときには、それは「仮引数」と同じことです。

引数が0個である手続きも定義できますが、手続きの周りは括弧でくくらないといけません、それは手続きを定義しているのだとはっきりさせるためです。それを呼ぶときにも括弧でくくってください、それが手続きの呼び出しだとはっきりさせるためです。

例えばこれは、初期値が 15 である変数の定義です。

(define foo 15)

一方これは、呼ばれたときには、15を返す手続きの定義です。

(define (foo) 15)
      +-------+
  foo |   *---+--->#<procedure>
      +-------+

この図は、手続きを定義するときは、本当はその値が手続き(へのポインタ)となるような変数を定義しているのです。今のうちは、それを気にかける必要はそれほどありません。知っておかなければならない主要なことは、foo という名前で手続きを呼び出すことができるということです。例えば、手続きの呼び出し式 (foo) は 15を返します、というのは手続きの本体が為していることはただ 15 という値を返すことだけだからです。

大抵、われわれは手続きの定義をこのようにインデント付けします。本体を新しい行ではじめ、何文字かをインデントします。

(define (foo)
   15)

このことにより、手続きの定義だということがはっきりします。

ほとんどの演算子は、手続きである

C, Pascal といった普通のプログラミング言語では、手続き呼び出しと他の種類の式の間には、あまりうまくない区別があります。Cでは、例えば (a + b) は式ですが、foo(a,b) は、手続き呼び出しです。Cでは、+ といった演算子では、手続きと同じことはできないのです。

Scheme では、事はずっと意味的にも文法的にも同じ形なのです。加算のようなもっとも基本的な操作は手続きであり、式を書くための統一された文法 -- 括弧付きの接頭記法があります。なので、Scheme では (a + b) となるのではなく、(+ a b) と書くわけです。そして、foo(a,b)と書くのではなく、(foo a b) と書くわけです。どちらの場合も、ただ演算子があって、演算対象が続き、それらすべてが括弧の中にあるというだけです。

どんな手続き呼び出しの式(コンビネーションとも呼ばれます)であっても、すべての値は、実際に手続きを呼び出す前に計算されています。(これはC でも Pascalでも変わりありません。)

定義と代入:定義は領域に名前を付け、代入は記憶された値を変更する

2通りのやり方で、変数に値を与えることができたことに気付いたでしょうか。変数を定義して初期値を指定するか、あるいは、変数の値を変更するために set! を使うかです。

これら2つの間の違いは、define は変数のために領域を割り当ててからその領域に名前を与えますが、 set! はそうしないということです。set! を使う前には変数は define されていなければなりません。 For example, if there's not already a definition of quux, the expression (set! quux 15) is an error, and Scheme will complain. You're asking Scheme to put (a pointer to) 15 in the storage named by quux---but quux doesn't name any storage yet, so it makes no sense. たとえば、quux はまだ定義されていないならば、式 (set! quux 15) はエラーとなり、 Scheme は文句を言うでしょう。あなたは Scheme に 15(へのポインタ)を quux という名前の領域に入れてください、とお願いしているのです --- ですが、quux はまだどの領域の名前ともなっていません、なので意味を成さないのです。

これは、私があなたにこう言うようなものです。「これを Philboyd に上げてください」とあなたに何かを手渡したとしましょう(まぁ、鉛筆など)。もしあなたが Philboyd という名前の人を誰も知らなければ、たぶん不平をいうでしょう。set! もそんなものです。Philboydに何かをしてとあなたに頼むのが意味を成すためには、われわれが「Philboyd」という単語が何を指すのか事前に合意している必要があります。define は、識別子に意味を与える方法であり、-- ある領域を参照するようにして -- そこにある値を与えるということをするわけです。

特殊フォーム:特殊フォームは手続きでない

Schemeのほとんどの操作(operation)は手続き呼び出しなのですが、いくつか知っておいて欲しい、ちょっと違った振る舞いをする式があるのです。それらは特殊フォームと呼ばれます。

手続き呼び出しと特殊フォームは文法的には似ていて、-- 両方とも、括弧の中にある文法に沿った単語の連なりです。例えば (foo bar baz) のような。ですが、それらは意味論的には互いに非常に異なっています。それがあなたが特殊フォームを知る必要があるのと、それを手続き呼び出しと混同しないようにする必要がある理由です。

左括弧の後の最初のものが、もし、defineset! のように、特殊フォーム(special form)を形作るものだったら、Scheme はその種類の式については、何か特殊(special)なことを行ないます。そうでなければ、Scheme は括弧の中の式を手続き呼び出しと認識し、手続き呼び出しの普通のやり方で評価します。

(これが特殊フォームが「特殊フォーム」と呼ばれるゆえんです。--- Scheme はその種類の式は、特別扱いが必要なのだと、ただ手続きを呼ぶようなわけにはいかないのだと、認識するのです。)

あなたは5つか6つある重要な特殊フォームの中の2つをすでに見ています。define と代入演算子の働きをする set! です。

set! が手続きではないことを気に留めてください。手続きでない理由は、その1番目の引数が、引数として実際の値を渡す前に計算評価されるような式ではないからです。それは値を置く場所の名前です。(例:(set! a b) とういなら b の値を取り出して、a という名前の記憶領域 に入れることになります。)

それと同じように、define は最初の引数を特別に扱います。最初の引数である変数や手続きの名前は、評価されてdefine に渡される式ではないのです。それは単に名前であって、あなたは define に対して、ある記憶領域を確保し、その名前をその領域を指すのに使うように指示しているのです。

他にわれわれが見ていくであろう特殊フォームは、

  • 制御構造:if, cond, case, *6, and, or
  • ローカル変数を定義するフォーム:let とその派生(variant)である letrec , let*
  • ループ構造:名前付き letdo
  • quotequasiquote、これにより複雑なデータ構造がテキスト的なリテラル(textual literals)としてコードに書けるようになります。
  • lambda 、これによりとても有益なやり方で新しい手続きを作成できます。

あと少し、とても特殊な特殊フォーム、define-syntax があり、これにより、自分自身が作った特殊フォームを「マクロ」として定義できるようになります。


制御構造は式である:制御構造は値を返す式である

Scheme の制御構造は式であり、値を返します。if 式は、C の if-then ステートメントのようでもありますが、"then" 節と "else" 節は、これもまた値を返す式なのです。if 式は評価されたほうのサブ式が返す値を返すことになります。

例えば、

(if (< a b)
    a
    b)

は、変数 a か変数 b のどちらか小さい方の値を返します(あるいは、2つの値が等しければ、b の値を返します。) もし、C での 3項演算子になじみがあれば、これは(a < b) ? a : b のようでしょう。Scheme では、if ステートメントも if のような3項演算子も必要ありません。なぜなら if「ステートメント」は式だからです。

すべての式が値を返すのですが、そうだとしても、すべての値が使われるわけではない -- if 式の値を無視してもよいということは覚えておいてください。if 特殊フォームは、何が実行されるかを制御するために使われるか、値を返すために使われるか、その両方であるかです。それはあなたに託されます。

何でも値を返すということは、明示的に return ステートメントを書く必要は決してないということを意味します。そういうことなので、Schemeにはそういったものがありません。2つの数値のうち小さいほうを返す関数を書こうとしてみましょう。C ではこのようになるでしょう。

int min(int a, int b)
{
   if (a < b)
      return a;
   else
      return b;
}

Scheme では、このようにするだけです。

(define (min a b)
   (if (< a b)
       a
       b))

どちらの節が取られても、ふさわしい変数の値(a または b)が if 節の値として、全体の値として返されます。そしてそれが手続き呼び出しの戻り値となるのです。

もちろん、"else"節なしの1つの節しか持たない if を書くことも出来ます。

(if (some-test)
    (some-action))

1節の if が返す値は、条件が偽と評価されたときに未定義*7となります。なので、返ってくる値を使う場合は、2節のif を使うべきで、明示的にどちらの場合にも値を返すような仕様とするべきです。

制御の流れはトップダウンであり、式のネストを下がっていくことに気を留めてください。--- if はどのサブ式が評価されるかを制御していて、それは大半の言語の制御ステートメントと同様です。

begin を使って、他の式を順に評価する式を書くことも可能です。例えば、

(begin (foo)
       (bar))

foo を呼んでから、 bar を呼ぶ。制御構造からいえば、 (begin ... ) 式は Pascal でいう begin ... end であり、 C でいう { ... } です。(われわれは end キーワードを必要としません。なぜなら、括弧を閉じることでその仕事が為されますので。)

Scheme の begin 式は単なるコード・ブロックではなく、値を返す式です。 begin は連なったなかの最後の式の値を返します。例えば、上の begin 式は、 bar を呼び出した結果返される値を返します。

手続きの本体は begin と同じように働きます。もし本体がいくつかの式を含んでいるなら、それらは順序立てて評価され、最後の値は手続き呼び出しが返す値として返されます。

foo を呼んで、それから bar を呼んで、 bar 呼び出しの結果を返す手続き baz の定義を、ここに載せます。

(define (baz)
   (foo)
   (bar))

*1 たぶん著者の名前
*2 hygienic.「保健的な」という訳もあります
*3 訳もよくないですし
*4 Among many other things 「何はさておき」とも訳せるか
*5 訳注:宣言、束縛、初期化といったところであろうか。
*6 sort-circuiting logical operators という形容がand と or の前についていました。意味がとれずに訳出していません。
*7 todo: 訳者memo。この訳語は揃える