スコープというのは、要は「有効範囲」のことです。ここで扱う話は、難しくいうと空間的有効範囲と時間的有効範囲とが交差したものという感じですか。ここでは、Forth一般にある変数についてだけ書きます。Mopsではクラス-オブジェクトやモジュールがあるので、ここでは触れない要素もあって、もうちょっと複雑です。まあ、それでも常識で済む程度の話なんですけどね。
変数のスコープを考えるというのは、結局、色々な種類の変数があって、それを分類する、という目的がひとつあります。なので、まずはMopsにはどんな変数があるかを整理してみます。前に出てきたのもありますが、整理ということで。オブジェクト指向関係は省きますが。
大域的
データスタック
Mopsの特徴である
データスタックから始めましょう。データスタックは、コードの何処からでもアクセスできます。そういう意味では、空間的には無制限です。全体が有効範囲なわけで、いわゆる大域変数に属します。ところが、そのスタックアイテムの時間的有効範囲は、使用するまで、です。使ったらなくなります。時間的有効範囲は、したがって、極端に短いものとなっています。
準大域的
いわゆる変数
"VARIABLE"、"VALUE"は前にでました。これらの空間的有効範囲は、定義がロードされて以後、ずっと、ということになります。これらは大域変数とも呼ばれますが、典型的に大域変数と呼ばれているもののスコープはコードの全域とされます。これに対して、VARIABLE等は定義以前には利用できないので、完全な意味では大域変数というわけではありません。値の時間的有効期限は、明示的に以前の値を廃棄して新しい値を格納するまでということになります。全く新たに格納する場合もあれば、格納されている値を増減させる場合もあります。いずれにしても、関数型言語が嫌う破壊的賦値ということになるんでしょうか。変数としての使用可能範囲という意味での時間的範囲については、Mops/Forthはコンパイル段階と実行段階に切れ目がないのでMops環境の実行時間も含めて考えると、定義以後はいつでも、ということになります。
定数
定数とは、いわば数値に名前をつけることです。そうすることで、コードを読みやすくし、保守しやすくします。直接数値でコードを書くと、それを修正するには該当箇所を全部いちいち直さないといけませんが、定数にしておけば、その宣言部分を変えるだけで一挙に全体を変更できます。定数は、動作としては、値を変えることができない"VALUE"と考えることができます。スコープは"VARIABLE"等と全く同じです。"CONSTANT"とともに宣言します。その値は、宣言時のトップスタック値(スタックの一番上にあった数値)を使って初期化されます。
10 CONSTANT myConstant
初期値は計算された結果でも、もちろん、かまいません。
浮動小数点変数
浮動小数点数を格納するための変数としても、VARIABLEとVALUEに対応するものがあります。浮動小数点数ライブラリをロードすれば利用できるようになります。PowerMopsでは
// zfloating point<enter>
とします。ソースコードファイル読み込みの場合のファイル名に限り、空白も1文字と見なされるので、余分な空白を付けないように注意してください。
iMopsでは、初めから浮動小数点数が利用できるようになっており、特別に
ファイルをロードする必要はありません。以下、iMopsでは、ファイル"zfloating point"のロードを取り除いて考えてください。
さて、PowerMopsでは浮動小数点数は、原則として8バイトで表現されたものを用いることになっています。いわゆるダブル精度の小数です。有効桁数は10進数で16桁くらいですか。これは、PowerPCというマシンがそうであることを反映したものです。
浮動小数点数用のVARIABLEは"FVARIABLE"、VALUEは"FVALUE"です。値は浮動小数点スタックを経由して出し入れします。"FVALUE"はVALUEと同様、宣言時に初期値が必要ですが、それはもちろん、浮動小数点数スタックに置きます。スコープや有効期限は、VARIABLE等と全く同じです。
// zfloating point
FVARIABLE myFVar
0.0 FVALUE myFVal
値の出し入れは次のようにおこないます。
0.5 myFVar F! \ 0.5を格納
myFVar F@ \ 格納されていた値を小数スタックにコピー
0.3 -> myFVal \ 0.3を格納
myFVal \ 格納されていた値を小数スタックにコピー
小数スタックと整数スタックは全く別個の存在であることに注意してください。そのため、浮動小数点変数では次のような書き方ができます。
FVARIABLE myFVar1
FVARIABLE myFVar2
FVARIABLE myFVar3
FVARIABLE myFVar4
0.1 0.2 0.3 0.4 myFVar1 myFVar2 myFVar3 myFVar4 F! F! F! F!
というのは、F!やF@も変数としてはそのアドレス値も用いているのであり、アドレス値は整数であるため、浮動小数点数に関しては格納すべき値とアドレス値とが同一スタック上で交錯することがなくなるからです。
その他の変数
以上はセル幅きっちりの変数でした。その他にも"CREATE"と"ALLOT"を用いることで、好きな長さの変数を、バイト単位で指定して定義することができます。いわゆる配列(アレイ)をつくるのに利用されます。これらのスコープや有効期限は"VARIABLE"等と同じになります。
ワード"CREATE"は、次にくる文字列を名前として、辞書中の現在利用可能なデータ領域の先頭と結びつけます。それから"ALLOT ( n -- )"でトップスタック値バイト分だけ、辞書中のデータ領域にスペースを確保して、その分を使用不能にします。以後は、この確保されたデータ領域は、初めに設定された名前を通じて管理されることになります。1セル(4バイト)幅の、10アイテムのアレイをつくってます。全部べたでつくります。
1 CELLS CONSTANT ITEMWIDTH
CREATE MyArray 10 ITEMWIDTH * ALLOT
: thItem ( addr u -- addr+u*width ) 0 max 10 min ITEMWIDTH * + ;
: thItem@ ( addr u -- x ) thItem @ ;
: thItem! ( x addr u -- ) thItem ! ;
124 MyArray 5 thItem! \ 第5要素に124を格納
MyArray 5 thItem@ \ 第5要素をスタックにコピー
ちなみに、上の場合、初めの要素は第0要素なので、第5要素というのは、六番目のセルであることに注意してください。"CELLS ( n -- 4n )"は、現在のところ、スタック上の数値を4倍するだけです。1セル4バイトということですね。まあ、maxとかminはついでです(^^;;)。アイテム番号がアレイの範囲外だったりすると予想外の場所にアクセスしてしまうので、はみ出した場合は、それぞれの端の値にアクセスするようにしてみました。"max"と"min"の動作は想像できると思いますが、"a b max"とすればa,bのうち大きい方だけ(同じならどっちかひとつ)がスタックに残り、"a b min"とすればa,bのうち小さい方だけ(同じならどっちかひとつ)がスタックに残ります。その他は追跡できると思います。
局所的
局所変数というのは、要するに、ひとつのワード(関数/サブルーチン)からしか利用できない変数のことです。つまり、スコープが1ワード定義内ということになります。これにもいくつか種類があります。
リターンスタック
リターンスタックを局所変数として扱うのは一見奇妙な感じですが、Forth系での一般的なリターンスタックの使い方をみれば、局所変数に他なりません。
リターンスタックそのものには、どのワードからもアクセスすることができます。そういう意味ではリターンスタックのスコープは大域(global)と考えられるかもしれません。ですが、実際には、リターンスタックはリターンスタックとしても使われている、つまり、ワードを呼び出したときに、戻る場所のアドレスを保存するのにも用いられています。この部分は自動的におこなわれるので、不用意に値を突っ込んでそのままにすると、格納したはずの値が何処にあるのか分からなくなります。そのため、リターンスタックには、ひとつのワード定義内で、格納と取り出しのバランスがとれていなければなりません。また、自動的に配置されたリターンスタックを不用意に掻き回してしまうと、こっぴどくクラッシュしてしまう危険があるので、まず、格納し、そして、その格納したワード定義内のどこかできちんとそれを取り出す、というように気を配るべきでしょう。
古典的なForth実装では、リターンスタックには、まさにリターンアドレスしかないことになっており、そのことを前提とすれば、いろいろなトリックが可能でした。現在でも、それが可能になっているForth系環境もあります。ですが、多くのForthが効率を求めてネイティブコードに移行した結果、現在では、リターンスタックの内容は実装によって様々であり、それについて特定の仮定を置いたコードを書くのは危険であるといわれています。リターンスタックを使うのは、一時的な変数として使うのに止めておくのがよいでしょう。
ちなみに、リターンスタックにアクセスするワードは、コンパイル状態でしか利用できません。Forth規格でも、インタープリテーションセマンティクスは無定義となっています。
データスタックからリターンスタックに値を移すワードは">R ( x -- ) ( r: -- x )"、逆にリターンスタックからデータスタックへと値を取り出すワードは"R> ( -- x ) ( r: x -- )"です。ちなみに"r:"と標したスタック
コメントは、リターンスタックへの効果を表現しています。
現実的な利用法は、データスタックにアイテムが溜まり過ぎて操作できなくなるのを防ぐため、後で処理してもよいアイテムを一時的にリターンスタックに退避させ、必要になったときに取り出すという使い方です。
というわけで、局所変数としてのリターンスタックの値の存続期限は、それを取り出すまでということになります。また、格納した値は、同じワード定義内で取り出すべきことになりますから、そのワードの実行の始まりと終わりまでの間より短い、ということになります。
<BUILDS (Create) - DOES>
"<BUILDS - DOES>"対を用いたワードによってつくり出されるワードには、固有のデータ領域が割り振られます。このデータ領域は、そのワードからしかアクセスできなません。ですから、その変数としてのスコープは、ひとつのワード定義内、ということになります。その意味で、これは一種の局所変数です。
ところが、そのデータ領域(変数)自体は、辞書の中に固定的に確保されているものであるため、ワードの実行が終わった後も、その値を保持します。ですから、値の有効期限としては大域変数に等しく、当のワードの実行時間を超えて存続し、いずれかの実行時に値を付け替えるまで、ということになります。値を変えない(つまりワード固有定数としての)使い方ももちろん可能です。
一般のプログラミング言語においては、これは、STATIC変数に類似しています。訳すと、静的変数、ですかね。STATIC変数というのは、変数としては一個で、その値は大域変数のように存続しますが、それへのアクセスが認められた特別な範囲内のプロセス(ルーチン)からしか参照できない変数のことを言います。C言語などのPublic Static変数は、実質的には大域変数ですが、それにアクセスしたい関数は、その定義内でそのスタティック変数を使うよ、と宣言しないといけないという形式になっています。Mopsにもクラスのスタティック変数というのがありますが、これは別の機会に説明します。他言語ではともかく、この"<BUILDS - DOES>"で与えられる変数は、まさにそのワードからしか参照できないので、private static変数とでもいうべきものに当たります。
すこし変な例を書いてみましょう。これはMops上では動きますが、辞書を保存したり、インストースしたりすると動かなくなるので注意してください(動くようにもできますが、とりあえず、簡単にしておきます )。
: Switcher <BUILDS FOR , NEXT
DOES> swap 1- 0 max CELLS + @ EXECUTE ;
: 1stWord ." Executing the first word!" cr ;
: 2ndWord ." Executing the second word!" cr ;
: 3rdWord ." Executing the third word!" cr ;
: 4thWord ." Executing the fourth word!" cr ;
\ XT登録は逆順
xts{ 4thword 3rdword 2ndword 1stword } Switcher MySwitch ( num -- )
\ 結果
1 mySwitch<enter>
Executing the first word!
4 mySwitch<enter>
Executing the fourth word!
おもしろいでしょ。ただし、決して"mySwitch"に5以上の数値を喰わせないこと!(^^;;)こういった類のForth系の細かいことは、そのうち触れる機会もあるんじゃないかな~、と思います。ってか、Mops細説とかで少し触れたことはあります。
このデータは"mySwitch"に固有のデータで、ワードを生成する時点で、4つのXTで初期化されています。外から触る方法はありません。
いわゆるLocals
Forth系にも局所変数はあります。ただ、どうも一部のForthプログラマの間では評判が悪いようです。ものによっては効率が悪いなどという事情もあるようですが、Mopsの局所変数は極めて効率がよく、うまく使えば強力な手段になります。これは、まさに局所変数であり、ひとつのワードの定義内からしか参照できませんし、遅くともそのワードの実行を抜けた時点で廃棄されてしまいます。値の付け替えはできますので、値の存続期限は、値の付け替えか、またはワードのプロセスを抜けるかのどちらか早い方まで、ということになります。
Mopsの局所変数は、名前付き引数(named parameters)と自分で初期化する局所変数に分かれます。ワード定義のときに、中括弧({}、英語ではcurly bracesとかいわれる)で囲って宣言することで使用可能となります。
: AWord { parm1 parm2 \ param3 %fparam -- }
....
このワードは4つの局所変数を持つことになります。"\"の左側にある二つが"名前付き引数"で、これらは、実行のときに、データスタック上のアイテムの値で初期化されます。左側がスタックの深い方、右側が浅い方、という順になります。"\"の右側、"--"までの間に宣言された"param3"と"%fparam"は、ワード定義内で、スタックの値からプログラマが自分で明示的に初期化して使います。ただし、変数名の頭に"%"を付けた場合、その変数は浮動小数点変数であることを意味します。ですから、"%fparam"は、浮動小数点数スタックと値を遣り取りすることになります。局所変数は[F]VALUEと見なしてかまいません。名前を書くと、そこで値がスタックに置かれ、"-> param3"でスタック上の値を格納します。
Mopsにおいても局所変数はスタックに対して補助的な地位にありますが、Mopsの局所変数は、すべてレジスタ変数となります。つまり、値の参照のためいちいちメモリーにはアクセスしません。そのため非常に効率がいいのです。その結果、標準Forthでは定番のリターンスタックを局所変数として使う方法よりも、Mopsでは局所変数を使った方が速くなる場合が多くなります。ただし、スタック操作で容易にできるものを局所変数でするのは避けた方がよいでしょう。データスタックもまたレジスタを最大限に利用するため、実はデータスタックを使うのが一番効率がいいのです。局所変数は全てがレジスタ変数とはいっても、レジスタの個数には制限があります。現在のところ全局所変数合わせて9個が上限となっています。それを超える数の局所変数を宣言すると、コンパイルエラーになります。
ここまできて、関数型言語が、変数の値を付け変えられないようにしたいと考えている理由が分かった気がします(^^;;)。変数、つまり名前を値(データ)と同一視するという考え方からは、変数の存続期間と値の存続期間が一致していると、話がものすごく単純になるんですね。変数の値の付け替えなどというのは、ランタイムに、いったいいつ起こるか分からないわけで、原則的に不確定な要素です。その変数の有効期間中は、同じ名前のものは同じ値として扱うことができるというのは、論理的には明快でいいですね。
逆にForthの変数を無名のもの(データスタックと、一時利用のリターンスタック)が典型的と考えると、Forthでは値の存続期間だけが基準という見方もできますね。実に儚いいのちです(^^;;)。そう考えると逆に、名前の有効期間が存続するせいで全く別個の値に同一性の仮象を与えてしまう有名変数というのは、Forthの中では出来損ないであるかのようにも思えてきます。って、そんなことはないか。
名前の競合
名前のある変数については、同じ名前の変数を定義してしまった場合、競合問題が生じます。データスタック、リターンスタック、"<BUILDS - DOES>"のデータ域には名前はありません。結局、問題になるのは、(準)大域変数と局所変数だけということになります。
局所変数間
局所変数については、同じ名前の局所変数を宣言することはできません。コンパイルエラーになります。
大域変数間
他方、[F]VARIABLE, [F]VALUEは同じ名前の変数を宣言することができます。この場合、最も最近に定義されたものがそれ以後は排他的に有効となります。つまり、大域変数といえども、同名の変数を宣言してしまえば、それ以前に定義された同じ名前の変数にはアクセスできなくなります。もっとも、同じ名前の変数を複数宣言すると混乱しますが。
他のForth系環境だと、辞書を区切って、以前に定義された変数やワードにアクセスすることができるようにしてあるものも多いようです。この場合、同じ名前の複数のワードや変数を目的別に定義して呼び出すことができるようです。まあ、変数じゃなくて、同名のワードを複数定義できるとこが狙いらしいですが。「Vocabularyの機構」とかいわれているのがそれらしく、商用のForthでは常識のようです。多分、多人数での開発に便利なのでしょう。Mopsの辞書(比喩じゃないです^^;;)にはそのような複雑な機構はありません(多分)。
大域変数と局所変数
同名の大域変数と局所変数を宣言した場合には、そのスコープに応じて、狭い方優先で処理されます。つまり、そのワード定義内では、その名前では局所変数が参照され、定義の外では、前も後も、大域変数が参照されます。その結果、そのワード定義内では、局所変数と同名の大域変数を参照することはできなくなるので、この点は注意が必要です。
最終更新:2019年07月15日 18:03