[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))
[R7RS base][SRFI-244] まずexprが評価され、続いて各値がvarに順に束縛されます。 最初の形式では、expr
(define-values (lo hi) (min&max 3 -1 15 2)) lo ⇒ -1 hi ⇒ 15
二番目の形式では、exprはvar 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-values
はdefine
が許されるところならどこでも使えます。
つまり、内部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)
このフォームはトップレベルでしか使えません。
トップレベルのdefine
と同様に、トップレベルでvariableを
expressionの値に束縛しますが、さらに次の情報をコンパイラに伝えます:
(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
” にあたるものはありません。宣言が無くても
コンパイラはどのローカル束縛が変更されないかを検出して最適化できるからです。
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は警告を出します。 その再定義は既にインライン展開された呼び出し箇所には影響を及ぼさないからです。 なのでこれは注意して使わなければなりません。モジュール内だけで使うか、 あるいは将来に渡って変わらなさそうな手続きに使うか。 インライン展開は性能に重大な影響を与える場所では効果的ですが、 滅多に使われない手続きをインライン可能に定義する意味はありません。
この形式はトップレベルでしか使えません。 variableのグローバルな束縛をmodule中に作成します。 moduleはモジュール名を表すシンボルか、モジュールオブジェクトで なければなりません。moduleがシンボルの場合、その名前を持つ モジュールが既に存在している必要があります。
expressionは現在のモジュール中で評価されます。
2番目の形式は次の形式の構文的修飾です。
(define-in-module module variable (lambda formals body ...))
註: シンボルが現在のモジュール中で定義されているか(グローバルな束縛を持つか)
を調べるには、module-binds?
が使えます
(モジュールイントロスペクション参照)。
• 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のモジュールシステムは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できない場合があります。