[Contents]   [Back]   [Prev]   [Up]   [Next]   [Forward]  


4. 基本概念

4.1 変数、構文キーワード、領域

識別子は構文の型に名前をつけたり、値が保管される場所に名前をつけたりすることができる。構文の型に名前をつける識別子を構文キーワードと呼び、構文キーワードは構文にバインドされているといういい方をする。プログラムのある箇所で有効な可視のバインディングすべての集合は、その箇所で有効な環境と考えられる。値が保管される記憶域内のある場所(10)に名前をつける識別子を変数と呼び、そのとき変数は、その記憶域にバインドされるという。変数がバインドされている記憶域に保管される値を、変数の値と呼ぶ。用語が乱用されたために、変数が値に命名する、もしくは変数が値にバインドされると表現される場合がある。これはあまり正確ではないが、この習慣によって混乱が生ずることは滅多にない。

式の中には新種の構文を作成して、その構文に構文キーワードをバインドするのに使用されるものがある。一方で新しい記憶域を生成して、生成した記憶域に変数をバインドする式もある。このような式をバインディング構成手続きという。構文キーワードをバインドするバインディング構成手続きについてはsection 5.3 マクロで説明する。

変数バインディングを構成するもっとも基本的なものはlambda式である。その他すべての変数バインディング構成手続きは、lambda式に置き換えて説明できるからである。lambda式以外のバインディング構成手続きには、let式、let*式、letrec式、do式がある(section 5.1.4 手続き、 section 5.2.2 バインディング構成手続き、section 5.2.4 繰り返し参照)。

AlgolとPascalに同じく、しかしCommon Lispを除くその他のほとんどのLisp方言とは異なり、Schemeはブロック構造化された静的スコープを持っている。プログラム内で変数がバインドされた記憶域の一つ一つに、プログラムテキストのそのバインディングが見える内部領域の一つが対応する。この領域は、バインディングを作成した特定のバインディング構成手続きによって定まる。例えばバインディングがlambda式で作成されたとすれば、領域はそのラムダ式全体である。識別子を呼び出すとそのたびに、その変数が使用された領域のもっとも内側を構成する、変数バインディングへの参照が行なわれる。

識別子が使用された領域にその変数のバインディングが存在しない場合、トップレベル環境に変数のバインディングが存在すれば(section 5. 式参照)、識別子の呼び出しによってトップレベル環境のバインディングが参照される。識別子のバインディングがどこにも存在しない場合は、識別子はアンバウンド(11)であるという。

4.2 型の非共有性

いかなるオブジェクトも、次に述べる述語関数の二つ以上を同時に満足することはない。

boolean?          pair?
symbol?           number?
char?             string?
vector?           port?
procedure?%

上記述語関数は、論理型、ペア型、シンボル型、数値型、char型(もしくは文字型)、文字列型、ベクタ型、ポート型、手続き型をそれぞれ定義するものである。

論理型を独立させてはいるが、Schemeのあらゆる値は条件テストのための論理値として使用できる。section 7.3.1 論理式で説明する通り、条件テストにおいては#fを除くすべての値が真に評価される。section 7.3.1 論理式に説明する通り、本報告書では、語句"真"を使用した場合は#fを除くあらゆるSchemeの値を表し、語句"偽"を使用した場合は#fを表す。

4.3 外部表現

Scheme(およびLisp)の重要な概念の一つは、一連の文字としてのオブジェクトの外部表現である。例えば整数28の外部表現は文字列"28"であり、整数8と13からなるリストの外部表現は、文字列"(8 13)"である。

オブジェクトの外部表現は必ずしもただ一つだけとは限らない。整数28は"#e28.000"とも"#x1c"とも表現され、前段のリストは"( 08 13 )"とも"(8 . (13 . ()))"とも表現される(section 7.3.2 ペアとリスト参照)。

オブジェクトには標準の外部表現が存在するものが多いが、中には手続きのように標準の外部表現が存在しないものもある(ただし特別な処理系の場合はその外部表現を定義してもよい)。

プログラム内に外部表現を書けば、対応するオブジェクトを手に入れることができる(quote、section 5.1.2 リテラル式参照)。

外部表現は入出力にも使用できる。手続きread(section 7.6.2 入力)は外部表現を解析し、手続きwrite(section 7.6.3 出力)は外部表現を生成する。この二つが相俟って、優雅で強力な入出力機能が提供される。

一連の文字"(+ 2 6)"は、整数8に評価される式ではあるが、整数8の外部表現ではないことに注意しなければならない。これは整数8の外部表現ではなく、要素が三つのリストの外部表現であり、その要素はそれぞれシンボル+と、整数2と6である。Schemeの構文は、式であるいかなる文字列も、同時に何らかのオブジェクトの外部表現であることを特徴とする。これにより、ある文字列がデータを示すのかプログラムを示すのかが文脈から明らかでないために一種の混乱が生まれる可能性があるが、インタープリタとコンパイラのようなプログラムをデータとして処理する(もしくはその逆を行なう)プログラムを書くことが容易になる以上、これはSchemeが強力である理由ともなる。

section 7. 標準手続きの該当する節では、各種オブジェクトの外部表現の構文で、そのオブジェクトを処理するプリミティブ(12)を説明している。

4.4 記憶モデル

ペア、ベクタ、文字列のような変数とオブジェクトは、記憶域内の位置もしくは連続した場所を暗黙に指し示すものである。例えば文字列は、文字列内に存在する文字と同数の記憶域内の場所を指し示している(この場所はマシンの完全1ワードに対応する必要はない)。string-set!手続きを使用すれば、そのうちの一箇所に新しい値を保管することができるが、文字列が指し示す記憶域は以前と同じ場所である。

変数の参照や、carvector-refstring-refなどの手続きによって取り出されたオブジェクトは、取り出す前に最後に保管された記憶域内のオブジェクトと、eqv?(section 7.1 同値を調べる述語関数)の意味で等しい。

記憶域内のすべての場所には、それが使用されているかどうかを示す印が一つ一つに付けられている。いかなる変数もオブジェクトも、未使用の記憶域を参照することはない。本報告書で変数もしくはオブジェクトに記憶域が割り当てられていると言及する時は、未使用の記憶域から適当な数の場所が選択され、ついで変数やオブジェクトがそれを指し示す前に、選択された場所に使用中を示す印が付けられたことを意味している。

定数(リテラル式の値)を読み取り専用メモリに置いておくのが望ましい処理系は多い。これを表現するにあたっては、記憶域を指し示すすべてのオブジェクトに、そのオブジェクトが変更可能か変更不可かを示すフラグが連結されていると想像してみるのが適当である。定数と、symbol->string手続きから返される文字列はしたがって変更不可のオブジェクトであり、一方本報告書に挙げるその他の手続きで生成されるすべてのオブジェクトは変更可能である。変更不可のオブジェクトが指し示す記憶域に新しい値を保管しようとした場合はエラーである。

4.5 正しい終端再帰

Schemeの処理系は正しく終端再帰を行なうものでなければならない。下記に定義するような構文脈で発生する手続き呼び出しは、「終端呼び出し」である。アクティブな終端呼び出しの数に制限がないScheme処理系は、正しい終端再帰を行なっている。呼び出された手続きがともかく返ることができる場合、その手続きはアクティブである。これには、現在のコンティニュエーションにしたがって返ることができる手続き呼び出しと、後で呼び出されるcall-with-current-continuationによって捕獲された、以前のコンティニュエーションにしたがって返ることができる手続き呼び出しが含まれる。コンティニュエーションを捕獲できなかった場合手続きは一回だけは返ることができ、その場合にアクティブな手続き呼び出しはまだ返ってきていない手続き呼び出しということになる。形式文法による正しい終端再帰の定義は、[PROPERTAILRECURSION]で説明されている。

理論的根拠:

終端呼び出しで使用されるコンティニュエーションの構文が、その 呼び出しを含む手続きに渡されるコンティニュエーションの構文と 同一であることから、アクティブな終端呼び出しに余分な空間が 不要であることが直観的に理解できる。実装が正しくない処理系では 手続き呼び出しの際に新しいコンティニュエーションを利用する 場合があるが、その新しいコンティニュエーションに返った直後に、 手続きに渡されたコンティニュエーションに返ることになる。 正しい終端再帰を行なう処理系では、直接もとのコンティニュエー ションに返る。

正しい終端再帰は、SteelとSussmanによるSchemeの当初のバージョン における中心概念の1つであった。2人の最初のSchemeインタープリタ には、関数とアクタの2つが実装された。制御の流れはアクタを 使用して表現され、アクタと関数とは、アクタが呼び出し元に返る のではなく別のアクタに結果を渡すという意味で、異なるもので あった。この終端再帰語法においてはアクタの1つ1つが、別のアクタ への終端呼び出しで終了していた。

後にSteelとSussmanは、この当初のインタープリタにおいてアクタを 処理するコードが関数を処理するコードと同一であり、したがって 言語にアクタと関数の2つを含める必要がないことに気がついた。

終端呼び出しは、終端文脈で発生する手続き呼び出しである。終端文脈は帰納的に定義される。終端呼び出しは、必ず特定のラムダ式に関して定義される点に注意しなければならない。

ある種の組み込み手続きも終端呼び出しを実行する必要がある。applycall-with-current-continuationに渡される第1引数と、call-with-valuesに渡される第2引数は、終端呼び出し経由で呼び出さなければならない。同様にeval手続きでは、引数がeval手続き内部の終端位置にあるかのように評価しなければならない。

以下の例で、唯一の終端呼び出しはfの呼び出しである。gへの呼び出しもhの呼び出しも終端呼び出しではない。xへの参照は終端文脈にあるがこれは呼び出しではなく、したがって終端呼び出しではない。

  (lambda ()
    (if (g)
        (let ((x (h)))
          x)
        (and (g) (f))))

注: 処理系には上記hの呼び出しのようなある種の非終端呼び出しについて、終端呼び出しであるかのように評価できると認識することが許される。ただしこれは必須ではない。上記の例の場合、let式をhへの終端呼び出しとしてコンパイルしてもよい。(hが予期せぬ数の値を返す可能性は無視できる。この場合letの結果は明示的に不定であり、処理系に依存するからである。)


[Contents]   [Back]   [Prev]   [Up]   [Next]   [Forward]