For Development HEAD DRAFTSearch (procedure/syntax/module):

4.10 定義

Special Form: define variable expression
Special Form: define (variable . formals) body …
Special Form: define variable

[R7RS+ base] この形式はトップレベル (ローカルな束縛が無い状態) とローカルスコープがある状態とで 別の意味を持ちます。

トップレベルでは、この形式は変数variableに対するグローバルな束縛を定義します。 最初の形式では、expressionが評価され、その結果が変数variableの値となります。

(define x (+ 1 2))
x ⇒ 3
(define y (lambda (a) (* a 2)))
(y 8) ⇒ 16

variable同じモジュールの中で既に束縛されていた場合、 二度目以降の定義は代入と同じです。

(define x 3)
(define (value-of-x) x)

(value-of-x x) ⇒ 3

(define x 4)

(value-of-x x) ⇒ 4

variableが現在のモジュールでは束縛されていないけれど、 他のモジュールからインポートされた束縛があった場合、話はちょっとややこしく (おもしろく)なります。詳しくは Scheme多世界へようこそで論じます。

2番目の形式は手続きを定義するための構文的な修飾で、以下の形式と同じです。

(define (name . args) body ...)
  ≡ (define name (lambda args body ...))

3番目の形式は(define variable (undefined))の略記です。 これはR6RSで導入されました(ただし、R7RSには入っていません)。 初期値が重要でないことを示したい場合に使えます。

このフォームがローカルスコープの中に現われた場合、ローカル変数の束縛となります。 (内部define)。

内部defineはlambdaやその他のローカル束縛を作る構文の、本体部分の先頭に置けます。 これらは、下に示すようにletrec*フォームと等価です。

(lambda (a b)
  (define (cube x) (* x x x))
  (define (square x) (* x x))
  (+ (cube a) (square b)))

 ≡

(lambda (a b)
  (letrec* ([cube (lambda (x) (* x x x))]
            [square (lambda (x) (* x x))])
    (+ (cube a) (square b))))

内部defineは実質的にletrec*フォームなので、 相互再帰する内部関数を書けますし、また同じスコープで先に導入された定義を 使って定義される値を計算することもできます。しかし、 内部defineフォームの後に定義される値を使うことはできません。 そういったプログラムを書いてもGaucheは直ちにエラーを報告しませんが、 あとでおかしな結果が出ることがあります。

(lambda (a)
  (define x (* a 2))
  (define y (+ x 1))  ; yの値を計算するのにxを使ってよい
  (* a y))

(lambda (a)
  ;; odd?の中からeven?を参照するのはok。odd?が定義される時点ではeven?
  ;; の値は使われず、odd?が呼ばれた時に初めて使われるから。
  (define (odd? x) (or (= x 1) (not (even? (- x 1)))))
  (define (even? x) (or (= x 0) (not (odd? (- x 1)))))
  (odd? a))

(lambda (a)
  ;; これはダメ。yを定義する時点でxの値を使わないとならないので。
  ;; ただし、すぐにはエラーとならないかもしれない。
  (define y (+ x 1))
  (define x (* a 2))
  (* a y))

束縛を作るフォームのボディー内で、内部defineは同じレベルにあるすべての式より前に 現れなければなりません。例えば次のコードは、defineフォームの 前に式(print a)があるので不正です。

(lambda (a)
  (print a)
  (define (cube x) (* x x x))  ; error!
  (cube a))

束縛を作るフォームのボディー中に、式を置かず内部defineだけを書いておくのも不正ですが、 Gaucheは特にエラーを出しません。

beginは新しいスコープを作らないことに注意してください(式をまとめる参照)。 beginの中に現われるdefineは、あたかもbeginとそれを囲む 括弧が無いかのように振舞います。すなわち、以下の2つの形式は等価です。

(let ((x 0))
  (begin
    (define (foo y) (+ x y)))
  (foo 3))
 ≡
(let ((x 0))
  (define (foo y) (+ x y))
  (foo 3))
Macro: define-values (var …) expr
Macro: define-values (var var1 … . var2) expr
Macro: define-values var expr

[R7RS base][SRFI-244] まずexprが評価され、続いて各値がvarに順に束縛されます。 最初の形式では、expr

(define-values (lo hi) (min&max 3 -1 15 2))

lo ⇒ -1
hi ⇒ 15

二番目の形式では、exprvar var1 …に対応する数か それ以上の値を生成しなければなりません。余った値はリストになってvar2に 束縛されます。

(define-values (a b . c) (values 1 2 3 4))

a ⇒ 1
b ⇒ 2
c ⇒ (3 4)

最後の形式では、exprの生成する全ての値がリストにまとめられ、varに 束縛されます。

(define-values qr (quotient&remainder 23 5))

qr ⇒ (4 3)

define-valuesdefineが許されるところならどこでも使えます。 つまり、内部defineにdefine-valuesを混ぜて使えるということです。

(define (foo . args)
  (define-values (lo hi) (apply min&max args))
  (define len (length args))
  (list len lo hi))

(foo 1 4 9 3 0 7)
 ⇒ (6 0 9)
Special Form: define-constant variable expression
Special Form: define-constant (variable . formals) body …

このフォームはトップレベルでしか使えません。

トップレベルのdefineと同様に、トップレベルでvariableexpressionの値に束縛しますが、さらに次の情報をコンパイラに伝えます: (1)その束縛は変わらない (2)expressionの値はコンパイル時に計算したものと変わらない。 コンパイラはvariableが参照されている箇所を コンパイル時に計算したexpressionの値で置き換えて構わないと考えて最適化を行います。

variableの値をset!で変更しようとするとエラーとなります。 variableを再定義することは許されますが、警告が表示されます。

下のdefine-inlineとの違いは、expressionの値がコンパイル時に 計算され、リテラルとして扱われることです。 例えばxを次のとおり定義したとします:

(define-constant x (vector 1 2 3))

すると、コード(list x)(list '#(1 2 3))と同じコードにコンパイルされます。

この違いは特にAOT (ahead of time)コンパイルをする場合に重要です。

“内部define-constant” にあたるものはありません。宣言が無くても コンパイラはどのローカル束縛が変更されないかを検出して最適化できるからです。

Special Form: define-inline variable expression
Special Form: define-inline (variable . formals) body …

2番目の形式は(define-inline variable (lambda formals body …))の略記です。

このフォームが内部defineの位置に現れた場合は、内部defineと全く同じです。

このフォームがトップレベルに現れた場合、それはインライン可能な束縛をつくります。 インライン可能な束縛とは、コンパイラに対してその束縛が変化しないことを約束するものです。 ただしdefine-constantで導入される定数束縛と違って、expressionの値が コンパイル時に計算可能とは限りません。 従って、コンパイラはdefine-constantで定義される束縛でやっている、 variableの参照を無条件でexpressionのコンパイル時値に置き換えることは できません。

しかし、コンパイラがexpressionの値が手続きになることを決定できれば、 その手続きが呼び出される箇所に手続きの中身をインライン展開することができます。

下の例では、dot3の本体がdot3を呼び出している箇所にインライン展開されています。 さらに、dot3の呼び出しの第2引数が定数ベクタのため、それに対するvector-refが コンパイル時に計算されていることがわかります (CONST -1.0など)。

gosh> (define-inline (dot3 a b)
        (+ (* (vector-ref a 0) (vector-ref b 0))
           (* (vector-ref a 1) (vector-ref b 1))
           (* (vector-ref a 2) (vector-ref b 2))))
dot3
gosh> (disasm (^[] (dot3 x '#(-1.0 -2.0 -3.0))))
CLOSURE #<closure (#f)>
=== main_code (name=#f, code=0x28524e0, size=26, const=4 stack=6):
signatureInfo: ((#f))
     0 GREF-PUSH #<identifier user#x.20d38e0>; x
     2 LOCAL-ENV(1)             ; (dot3 x (quote #(-1.0 -2.0 -3.0)))
     3 LREF0                    ; a
     4 VEC-REFI(0)              ; (vector-ref a 0)
     5 PUSH
     6 CONST -1.0
     8 NUMMUL2                  ; (* (vector-ref a 0) (vector-ref b 0))
     9 PUSH
    10 LREF0                    ; a
    11 VEC-REFI(1)              ; (vector-ref a 1)
    12 PUSH
    13 CONST -2.0
    15 NUMMUL2                  ; (* (vector-ref a 1) (vector-ref b 1))
    16 NUMADD2                  ; (+ (* (vector-ref a 0) (vector-ref b 0))
    17 PUSH
    18 LREF0                    ; a
    19 VEC-REFI(2)              ; (vector-ref a 2)
    20 PUSH
    21 CONST -3.0
    23 NUMMUL2                  ; (* (vector-ref a 2) (vector-ref b 2))
    24 NUMADD2                  ; (+ (* (vector-ref a 0) (vector-ref b 0))
    25 RET

最も極端なケースとして、両方の引数がコンパイル時定数であれば、 dot3の呼び出し自体がコンパイル時に計算されます。

gosh> (disasm (^[] (dot3 '#(1 2 3) '#(4 5 6))))
CLOSURE #<closure (#f)>
=== main_code (name=#f, code=0x2a2b8e0, size=2, const=0 stack=0):
signatureInfo: ((#f))
     0 CONSTI(32)
     1 RET

インライン展開と同様の効果はdot3をマクロにすることでも得られますが、 define-inlineを使っておくとdot3を通常の手続きとしても使うことができます。

(map dot3 list-of-vectors1 list-of-vectors2)

dot3がマクロだと、上のように関数引数としてmapに渡すことはできないでしょう。

インライン展開パスはソースの上から下へと進みます。インライン展開されるためには、 呼び出される前にそれが定義されていなければなりません。

インライン可能な束縛が再定義されるとGaucheは警告を出します。 その再定義は既にインライン展開された呼び出し箇所には影響を及ぼさないからです。 なのでこれは注意して使わなければなりません。モジュール内だけで使うか、 あるいは将来に渡って変わらなさそうな手続きに使うか。 インライン展開は性能に重大な影響を与える場所では効果的ですが、 滅多に使われない手続きをインライン可能に定義する意味はありません。

Special Form: define-in-module module variable expression
Special Form: define-in-module module (variable . formals) body …

この形式はトップレベルでしか使えません。 variableのグローバルな束縛をmodule中に作成します。 moduleはモジュール名を表すシンボルか、モジュールオブジェクトで なければなりません。moduleがシンボルの場合、その名前を持つ モジュールが既に存在している必要があります。

expressionは現在のモジュール中で評価されます。

2番目の形式は次の形式の構文的修飾です。

(define-in-module module variable (lambda formals body ...))

註: シンボルが現在のモジュール中で定義されているか(グローバルな束縛を持つか) を調べるには、module-binds?が使えます (モジュールイントロスペクション参照)。


4.10.1 Scheme多世界へようこそ

複数のトップレベルと複数のスコープ

昔、Schemeの世界は単純でした。トップレベルと呼ばれる単一のグローバルな空間があり、 トップレベル定義はその空間への副作用と考えることが出来ました。名前がそこに まだなければ新たに作り、既にあったなら書き換えます。

ただ、その単純なモデルはスケールしなかったので、 実装ごとに色々なモジュールシステムが試されました。R6RSの重要な目標のひとつは、 Schemeの設計と一貫性のあるモジュールシステム (RnRSでは「ライブラリ」と呼ばれることになります) を決めることでした。 特に、Schemeの衛生的マクロシステムはレキシカルスコープを捕捉するものでしたから、 モジュールシステムもマクロから見て同じように扱えることが望まれました。

現代のSchemeでは、「トップレベル」、あるいは各モジュールは自分のレキシカルスコープを 持っていて、そこでの定義はletrec*セマンティクスで理解できる、とされます。 つまり、マクロシステムは識別子を常に「スコープと関連付けられた名前」として扱えます。

次のようなトップレベル定義があるとします。

(define (odd? n)  (if (zero? x) #f (even? (- n 1))))
(define (even? n) (if (zero? x) #t (odd? (- n 1))))

最初の行に現れるeven?は、次の行で定義される名前だとみなされます。 これは内部defineと比べてみるとはっきりするでしょう。

(let ((even? error))
  (define (odd? n)  (if (zero? x) #f (even? (- n 1))))
  (define (even? n) (if (zero? x) #t (odd? (- n 1))))
  ...)

odd?の定義中に現れるeven?は、letが導入する束縛ではなく、 次の行で定義されるeven?を参照します。

ここまではいいですね。

では、次のコードを考えてみてください。

;; Invalid in RnRS, n >= 6
(import (scheme base) (scheme write))
(define orig-error error)
(define (error . args)
  (write args) (newline)
  (apply orig-error args))

コードの意図は、(scheme base)からインポートされた error元の値を変数orig-errorにセーブしておいて、 errorを再定義してログを出す機能を仕込むというものです。 このテクニックはR6RSより前のSchemeではよく見られました。

でも、トップレベルはスコープであるとした最近のSchemeでは、 (define orig-error error)におけるerrorは 同じスコープで定義されるもの、つまりその下で定義されるerrorを参照しなければ なりません。でないとレキシカルスコープの規則が崩れてしまいます。 とはいえ内側のerrorの値はorig-errorの初期値を計算する時点では まだ計算されていないので、上のコードはRnRS的には無効なコードということになります。

実際、混乱を避けるために、R6RSではインポートした名前とぶつかる名前をトップレベルで 定義することを禁止しています (R7RSでは未定義動作になっています)。 上の例では、error(scheme base)から既にインポートされているので、 トップレベルで再定義するのは文法違反となります。

既存の手続きに機能を付け加える、現代風のやり方は、インポート時にリネームすることです。

(import (except (scheme base) error)
        (rename (scheme base) (error r7rs:error))
        (scheme write))
(define (error . args)
  (write args) (newline)
  (apply r7rs:error args))

Gaucheの選択

GaucheのモジュールシステムはR6RS/R7RS以前からありました。 モジュールは第一級のオブジェクトで、クラスのような継承関係も持てます。 R7RSのライブラリの上位互換ですが、R7RSの未定義動作の部分については 拡張解釈をしています。

まず、インポートしたり継承している名前と同名のトップレベル変数を定義できます。 新たな定義は前の定義を単にシャドウします。

次に、複数のトップレベルフォームがbeginに包まれたり include経由で読まれるなどした場合、それらはひとつのスコープにあるものとして 処理されます。上のorig-errorの例がincludeで読まれたとすると、 最初のerrorはその下で定義されるはずのerrorを参照することになります。 そのスコープのerrorの値は、orig-errorの初期値を計算する時点では 計算されていないので、次のエラーが投げられます:

*** ERROR: uninitialized variable: error

一方、Gaucheは他のS式で囲まれていないトップレベルフォームは、ひとつひとつ 逐次的に処理します。つまりREPLと同じセマンティクスです。 orig-errorの例がそのまま読み込まれた場合、(define orig-error error)errorはR7RSのerrorを参照し、その値がorig-errorに 束縛されます。なぜなら(define orig-error error)を見た時点では、 次にerrorが再定義されるかどうかはまだわからないからです。

最後の動作はREPLをサポートするのに必要な動作ですが、 同じコードをincludeした場合に結果が変わってくることに気をつけてください。 こういった曖昧性のあるコードを書くのを避けることが一番です。

註:トップレベルフォームがbeginで囲まれた場合の動作は、 R7RSとの互換性を高めるために、0.9.9で明確化されました。 それ以前のバージョンでは上の例の動作は未定義でしたが、 REPLセマンティクスで動くことを期待していたコードはあるかもしれません。

移行をスムースにするために、環境変数GAUCHE_LEGACY_DEFINEが定義されている 場合は、Gaucheはトップレベル定義の評価を0.9.8以前と同じにします。 その場合、複数のライブラリが1ファイルで定義されている R7RSとして有効なコードをincludeできない場合があります。



For Development HEAD DRAFTSearch (procedure/syntax/module):
DRAFT