変数
プログラムにとって変数とは何でしょうか。それは、データを一時的に格納して、後のための取っておく"箱"のようなものです。"一時的"、というのは、そのデータは適宜入れ替えることができるからです。新たにデータを格納すると、対応部分に格納してあった古いデータは上書きされ、消されてしまいます。機械においてみれば、変数は記憶装置の一区画です。ただし、この逆は成り立ちません。
変数のために利用される記憶装置は、場合によっては、中央演算装置の中にあるレジスタと呼ばれる少量の記憶装置で済まされることもありますが、大抵はメモリーと呼ばれている外部記憶装置部分です。変数は値を維持し、適宜、値を入れ替えることができるわけですから、このメモリーもそういう性質をもっているわけです。ここまでは当然の前提としてきましたが、記憶装置一般としては適宜記憶内容を変更できるものばかりではありません。いわゆるメモリーのように、記憶内容を随時入れ替えられる記憶装置はRAM(確か、ランダムアクセスメモリー:Random Access Memory;意味としては、randomly accessible memoryでしょうかね。あまり"書き込み可能"という含みがない気がするのが不思議。)とよばれます。これに対して、決まったデータを読み出すことしかできないものはROM(リードオンリーメモリー:Read Only Memory)と呼ばれます。
さて、プログラムにおける変数の話に戻ります。変数の役割には、おおまかにいって三つの側面があります。一つは、データを保存、保持しておくことです。もう一つは、データの伝達の媒介となることです。もちろん、これらの側面は結びついています。何らかの処理で得られたデータを変数に格納することで、それは時間的に後の段階まで保存されます。そして、その変数の値を次の処理に渡すことによって、前の処理と、次の処理とのデータの伝達が可能になるわけです。
そして、三つ目は、プログラムを抽象化して、最小限の汎用性と柔軟性を与える、という側面があります。これは少し抽象的なのですが、例えば、
a = 7 + 2;
などとやっても、プログラムとしてはほとんど意味がありません。aの値は9で決まってしまっています。7+2は計算できても、それ以外の足し算は視野に入ってきません。これを、
long a,b,c; //全部longの場合の、宣言の略記法です。
void Addition (long x, long y )
{
a = x;
b = y;
c = a + b;
}
のようにすれば(コードは怪しいですが)、関数に二つのパラメターを与えるだけで一般的にその和が得られるようになります。つまり、プログラムは変数を用いることによって、具体的な値から切り離され、もっと一般的な構造を記述することができるようになるのです。
数学との関係
変数に名前をつけ、その名前で特定する場合には、変数は、数学の文字式の中の計算される文字に外見が類似してきます。実際、「データ(値)を入れる箱」というイメージは、数学で最初にx,y等がでてくるときの最も良くある導入の方法ではないかと思います。実際、プログラミング言語の方も、数式との類似性を(少なくとも初期の頃は)ずっと追いかけてきたのです。もちろんこれでかまいませんが、数学の方は、もっと抽象化します。つまり、もはや、文字に値が割り当てられるということにはあまり拘泥することなく、計算の規則にしたがって、文字そのものについて計算を始めるようになります(無限級数の収束とかを考えないFormal云々という流儀があります。)--もっとも値に立ち返ることができる必要はあるのですが。コンピュータはそこまでは行けません。変数には常に値があって、計算されるのはその値です。実際に、文字式そのものを計算できるように見せたアプリケーションは、確かに存在しています。しかし、それは普通の変数とは異なる、アプリケーションによって抽象化され、操作された処理を踏んでいるはずであって、実際の動作における変数については、その計算とはつまりその値の計算を意味している、という事態になっているでしょう。
変数名が見えないMops
しかし、以前の回に現れたMopsの足し算コードは
3 5 +
でした。これには変数が見えません。それでは計算が抽象化できていないかというと、そうではありません。これは、そのときにも触れた
データスタックという変数が介在しているからです。Mopsでは数字を書いたとたん、それは変数(データスタック)に格納されますし、演算子は自分に必要な分の入力を、その変数から取り出して使うことになっているのです。ですから、取り立てて変数の名前付きでの宣言と、変数を使った式の表示を書くまでもなく、"+"という演算子自体が既に抽象化、一般化されている、と考えることができます。つまり、"+"演算子は何もしなくても原則として変数(データスタック)に結びついているのです。まあ、確かに、分かりにくいっちゃあ、わかりにくいですが。
タイプ
データは全て数値で表現されます。ですが、それぞれに意味の違いがあります。もっとも大きな違いは、整数と小数(浮動小数点数)の別です。これらは、数値の表現型式が違いますから、表わそうとしている値が同じでも、電気的なデータとしては異なっています。ですから、どちらに属するかで、同じデータが全然違う値として解釈されてしまうことになるので、きちんと区別する必要があります。もう一つは少し微妙な違いで、整数とアドレスです。実体としてはアドレスは整数でしかありません(アドレス値に小数を使っている例は、私は見たことはありません。普通は、整数、特に、負の数なし(unsigned)の整数が使われます)。しかし、整数は単なる値を示しているに過ぎないのに対して、アドレスはメモリーの場所を示しています。さらにメモリーアドレスも、そこにどんな値を格納している、あるいは格納する予定であるのか、を区別し出すと、値の意味の違いは非常に多様になります。この意味の類型は、データのタイプと呼ばれます。
C言語などでは、これらは変数の型(Type)を指定することによって区別しようとしています。Mopsでは、小数と整数は区別しますが、アドレスと整数の区別はプログラマが自分で自覚的に区別しないといけません。まあ、変数に型があっても、自覚的に区別しないといけないのは同じですが。Mopsではアドレスも、そのままで、普通の整数と全く同じように、
四則演算(足し算、引き算、かけ算、割り算)ができます。つまり、C言語とかでは、アドレス(ポインタ値)の計算(特に増減)は普通の整数の計算と同じに考えられない場合があることを暗にほのめかしているわけです。まあ、それはここでは触れないでおきます。アドレスへの足し算/引き算が何も考えず自由にできることは便利ではあります。かけ算/割り算はやめた方がいいですね。意味がないことが多いでしょう(オフセットは別 -- アドレスのオフセットは、もうアドレスじゃなくて数値だと思いますが。ちなみに、オフセットというのは番地のズレ幅のことで、二つのドレスの値の差のことをいわんとしています。)。
タイプはデータの属性
このように、タイプ、型というのは、もともとはデータ、つまり変数に格納される値の方の属性です。変数の属性ではありません。ある変数に格納する値のタイプを予め決めておくことによって、変数と値を同じものであるかのように扱う、というのが、変数の型の意味だったわけです。実践的には、変数に型情報を付け加えておくことによって、プログラマがある値をその型に合わない取り扱い方をしたりすること(つまり、ミス)を防ぎ、加えて、より効率的なマシンコードを生成することが目的です。この型情報というのは、機械への情報ではありません。主としてコンパイラへの情報です。コンパイラがこの情報を用いて、あわよくば、効率的なコードを吐き出せるように、というわけです。機械は、データの型など全く感知しません。データはデータ。みな一緒くたです。変数の型を操作することになれたプログラマーは、変数に型がないというのをいぶかしく思うそうです。しかし、機械には意味がない情報なのですから、なくたってかまわないのです。ただ、機械は"タイプ"を感知しないからこそ、間違ったプログラムを避けるため、プログラマは"データの"タイプを意識しなければいけないのです。しかし、それは"変数の"タイプとは同一概念ではありません。データの型/タイプというのは、そのデータの持つ意味の類型のことであって、変数の型というのは、これに依存する便法のようなものなのです。ちょっと抽象的というか、観念的な話ですが、元々そういうものなんです、この"タイプ"ってのは。
Mops/Forthでは、値は無名の変数域であるスタック(この仕組みは他所で述べます)に、何でもかんでも突っ込めるわけですし、大域変数のために準備された変数も、一様に1セル幅の何でも入る変数です。ですから逆に、プログラマはデータの型に自分で気を配る必要があるのです。Forth系の言語には、これと同様にプログラマに任されていることが他にも結構あります。悪口を言う人は言語の仕事の放棄だみたいな言い方をするのですが、逆に言えば、プログラマに最大限の自由が与えられているともいえるわけです。自分の手でファインチューニング可能、なんて、如何にも手作りっぽくていいじゃないですか、と私は思いますが。まあ、変数の型には直接関係ない話ですが。
データ格納形式と16進数表記
さて、Mopsの場合、OSはC言語系のプログラミング言語で書かれているので、OSとの継ぎ目のところで、変数の型にも少し配慮する必要があります。変数の型そのものが問題になるわけではないんですが、何バイト幅の変数であるかは意識しなければならないことがあるのです。C言語でいえばcharは1バイト、short intは2バイト、long intは4バイトです。ポインタは1セルになります。小さな幅に値を格納するのは、いってみればメモリーを節約するということで、一定程度以上大きくなり得ない数値を格納するときには、メモリーの幅を細かく刻んで取って、不使用部分を減らすというわけです。
ここで、一定のメモリー幅へのデータ格納形式を見てみましょう。実はこれは機械によって違うのですが、PowerMopsが動くPowerPCについて見てみます。
その前に、16進数表記法に慣れておくと便利です。16進法というのは、要は、16になって初めて桁が繰り上がる表記法です。例えば、16進数で20は普通の10進法では32なわけです。n桁めを16n-1の個数として計算すれば10進表記に変換できます。あまりしたくありませんが。ただ、ひと桁の数字は0から9までの10個しかないので、10を越えたときにはアルファベットを使うことになっています。10進法でいえばAが10、Bが11、Cが12、Dが13、Eが14、Fが15です。16は繰り上がるので要りませんね。16進法では10進法の15まではひと桁の数となるのです。
なんでこんなめんどくさい表記を使うかというと、16進法では、ひと桁がちょうど4ビット、つまり2進法での4桁に当たる点が、ものすごく便利だからです。4ビットは、ハーフバイトとか、ニブル(nibble、nybbleとつづることもあるようです)と呼ばれます。1バイト8ビットですから、16進数2桁でちょうど1バイトなわけです。
さてこの利点を利用して、メモリーへのデータ格納形式をみましょう。変数に結びついている格納領域のアドレスというのは、基本的にはその先頭のアドレスをいいます。1バイト数である"7E"(16進表記ですよ)を、1/2/4バイトの変数に格納すると、だいたい次のようになります。
7E 1バイト
007E 2バイト
0000007E 4バイト
右側の空いている部分は、まだ使おうと思えば使えるわけです。小さい数を1セルに入れると、結構、0でメモリーを無駄に使いますね。もちろん、上の0一つで2進法の4桁分(4ビット)の0ということですよ。(ちなみに、Intel x86互換(pentiumとか)の場合、バイトの並びかたが右から--0も右に詰める -- になるようです。バイトの並べ方をエンディアンといいます。PowerPCはビッグエンディアン、x86系はリトルエンディアンというようです。名前の由来は知りません。がわかりました。ただ、記憶がもうあやふやですが。確か、ガリバー旅行記だという話です。内容は、スパゲッティだったか、パスタだったか、ともかく細長い食べ物を、太い方から食べるべき(big-endian:つまり、大きい(big)端(end)派)とする側と細い方から食べるべき(little-endian:つまり、小さい(little)端(end)派)する側とで対立し、その争いが元で戦争をするとか言う話だそうです。PPCは、低い方のメモリー、つまり、先に、大きい数値になる高い桁分に対応するバイトを格納するのでbig-endian、x86系は逆に小さい値になる低い桁分に当たるバイトを先に格納するのでlittle-endianということになるわけです。)
符号付き数と符号なし数
Mops/Forthでは、どんな数値も、大抵は1セル幅のスタックに写してから利用します。1,2バイト変数のような1セルに満たない変数に格納された数値もそうです。すると、バイト幅を拡張しなければなりません。単純に頭に0を詰めればいいと思われるかもしれませんが、実はそうともいえない場合があるのです。というのは、符号付き数、つまり、負の数を含んでいる場合があるからです。
符号なし整数、つまり、0が正の数だけ表す場合は、普通に、第n桁→2n-1という換算で数値表現できます。では負の数はどうするのでしょう。実は、別に負の数に特別なやり方があるわけではなくて、ある形のデータを負の数と解釈することによって実現されているのです。1バイトでみてみましょう。例えば、-1は全ビット1で表現されます。2進数表記してみます。真ん中にわかりやすく区切りをいれて、
1111 1111
ちなみに、16進数では2進数の1111はFなので、"FF"と表記できます。
これが、なんで-1なのか、です。1を足してみましょう。すると、右端の桁は1+1で1繰り上がって0。上の桁は全部1なので、どんどん繰り上がって、全部0です。左端の桁も0で、1繰り上がりますが、もう桁がないので、繰り上がりは捨てられてしまいます。すると、全桁0になります。全桁0はもちろん0に当たります。つまり、1を足すと0になる数。-1じゃないですか!
こんな風にして、一貫した形で、負の数の表現を定めることができます。考え方としては、最上位の繰り上がりが捨てられるわけですから、1バイトは全体が256周期の数になっているわけです。つまり、256足すと同じ数に戻る。これを考えると、"負の数の表現→符号なし数での値"という対応は次のようになります。
-n → 256 - n .... 但し、n=0のときは0
あれこれ考えているうちに納得できるのですが、負の数の表現というのは、必ず一番左の桁が1になっています。というか、このように決めれば単純でかつ論理的に一貫した表現法が可能になる、と。そこで、この規約がコンピュータでは採用されます。そして、この、左端の桁が1かどうかで挙動を変えれば符号付き数のバイト幅を拡張になり、何も考えずに左に0を詰めれば符号なし数のバイト幅の拡張になるわけです。負の数のバイト幅拡張では、具体的には、左に0ではなくて1を詰めるということになります。つまりもとの左端の桁が0なら0を詰め、1なら1を詰める、というのが符号付整数のバイト幅拡張方法となるわけです。
ちなみに、2バイトの場合には、周期が6万5536、4バイトの場合は42億9496万7296を周期として、同じように考えることができます。って、考えたくないですね、普通。ビットでマイナスに変換する手順としては、もとの数の一番右の1のところまで右側の桁はもとの数と同じにします。それより左の桁は、ビットを反転します。つまり、もとの数のその桁が0なら1、1なら0を置くのです。これで、-1を掛けた数、つまり、加法の逆元(足して0になる数)が得られるはずです。
Mopsの変数
Mopsの標準的な変数は"Variable"という宣言で生成される変数です。便宜上、このサイトでは、Variable変数と呼んでいます。
VARIABLE MyVariable1
この変数の幅は、前にも触れたように1セル(4バイト)です。この変数は、他のプログラミング言語の変数とは挙動が違っています。この変数の名前を書いて呼び出すと、この変数に値を格納する領域として割り当てられたメモリの先頭のアドレスがスタック上には積まれます。つまり、値そのものではなく、ポインタが積まれるわけです。値を取り出すときには、このアドレスに対して、値を取り出すという操作をするわけです。この操作をするワードは"@"です。
MyVariable1 @
"@"は1セルに値がきっちりと入っていることを前提としています。ところが、前に触れたように、
システムコールから小さい幅の値をもらった場合には、先頭詰め、ないし左詰めで値が入り、後半が空いてしまいます。この場合には、違う取り出し方をしないといけません。1バイト幅なら"C@"、2バイト幅なら"W@"を使います。3バイト幅というのは使いません。
MyVariable1 C@ \ 頭の1バイト分を値としてスタックに写す
MyVariable1 W@ \ 頭の2バイト分を値としてスタックに写す
スタックの細かい仕組みは他所で説明しますが、スタックは1セル均一ですから、1バイト/2バイトの値を取り出すというのは、その値を4バイトに変換しているわけです。
C@、W@は、アドレスの頭から1バイト/2バイトを、符号なしの数としてスタックに取り出します。つまり、頭に0を詰めるだけです。
符号付で取り出したい場合には、バイト幅に応じて"C@X"、および"W@X"というワードを使います。Xは符号拡張(sign-eXtension)のXのようです。
スタックから変数に値を格納する場合には、バイト幅に合わせて、"C!"、"W!"、"!"を使います。これは引数として、スタック上に、格納する値と変数のアドレスの両方が必要になります。例えば、
5 MyVariable1 !
とやれば、5がMyVariable1に、4バイト数として格納されます。C!ないしW!は、スタック上の値の左側を削り落としてそれぞれ1バイトないし2バイトにし、頭詰めでMyVariable1のメモリ領域に格納します。変数の格納域は右側に空きができますが、そこには何の操作もしません。
Value
Mops/Forthにはもう一つ変数の形式があります。それはValue宣言で作り出すものです。このサイトでは便宜上、Value変数と呼んでいます。この変数は、その名の通り、その値(value)と同一視できる変数です。他のプログラミング言語では、変数というのはそういうものだと考えられていますね。この変数は、変数名を書いて呼び出せば、アドレスを媒介にすることなく、直接にそこに格納されている値がスタックに積まれます。このタイプの変数には、宣言時に初期値を与える必要があります。例えば、
13 Value MyValue1
とすれば、初期値13でValue変数"MyValue1"が生成されます。既に述べたように、値を取り出すには、
MyValue1
とするだけです。このタイプの変数に値を格納するには、"->"というワードを使います。スタックを媒介にします。
5 -> myValue1
なお、"->"にあたるForth標準ワードは"TO"です。
この変数も、格納領域は1セル(4バイト)です。しかし、値の出し入れはいつもスタックを媒介にして行われるので、バイト幅の違い云々について考えることはありません。
大域(Global)変数
Forth伝来の、Variable変数およびValue変数は、ともに大域変数のための変数です。大域変数というのは、プログラムの実行時間全体を通じて、使っていると否とにかかわらず常に存在し、どのプロセスからも値を参照し、操作できる変数をいいます。変数の"値を参照し、操作できる"ことを、その変数に"アクセスできる"という言い方をします。厳密にいうとこれらの変数は、どのプロセスからもアクセスできるわけではありません。宣言前に定義されたワードからは、これらの変数も呼び出せないからです。C言語なんかだと、大域変数は全ての関数の定義に先立って宣言しないといけないので、まさに大域変数なのですが、Mops/Forthではそうではありません。それでも、宣言後に定義されるワードからのアクセスには制限はありません。その意味で大域変数といえるでしょう。
大域変数の多用は、一般に嫌われます。少し前の教科書、あるいはC言語の解説なんかでは今でも、大域変数は、必要なくなってもプログラムの終了までずっとメモリーを占拠しているので、メモリー効率が悪い、ということを理由にしているものもあります。しかし、最近ではむしろ、大域変数は危険であるということが強調されています。どこの関数からもアクセスできるので、いろいろな関数がこの変数の値を変えているかもしれず、それが起こったときにはミスの発見が難しい(巨大なソースコードのあちらこちらにちりばめられている可能性がある)と。
冷静に考えれば、ちゃんとわかりやすい名前にしておけば、そんな間抜けなことするかい!とも思われますが、もっと冷静に考えると、プログラムは一人で書くとは限らず、別々の部分は、数ヶ月、あるいは数年離れて書かれることもあり得るわけです。まあ、大域変数を使うことより、安易にあちこちからアクセスして、値を変えたりすることが問題なんですが。
そういうわけで、特定の部分からしかアクセスできない局所(local)変数の方が一般に好まれるわけです。また、使わなくなったらそれに使っていたメモリー領域を再利用できるようにすれば、メモリー効率もいいというわけです。Mops/Forthではこのような局所変数のために、主としてスタックを使います。スタックの機構については後に回します。
Mops/Forthでは、その独特な機構から、モジュール(サブルーチンよりも大きな機能的にまとまりのある部分)毎に大域変数を設定することも、実はできます。またMopsにはモジュールと呼ばれる独立のファイルをつくって、変数やワードなどをその中に閉じ込めることもできます。実際には様々な名前の閉じ込め方法があるわけです。
最終更新:2019年07月12日 20:18