いものやま。

雑多な知識の寄せ集め

「Lucid, the Dataflow Programming Language」を翻訳してみた。(その11)

前回の続き。

2.6 Iswimでの呼び出しと束縛

手続き的解釈の適用には注意が必要である。
というのも、そうでなければ、スコープ規則に関して間違いが生じる可能性があるからだ。
特に、グローバル変数を含む関数定義の正しい解釈について注意する必要がある。

関数定義の右辺には、仮引数ではなく、右辺の中で関連する定義がない変数が出現する場合がある。
以下の B Cのように使用される変数は、関数定義のグローバル(グローバル変数)と呼ばれる:

f(X, Y) = X + B * Y + C;

グローバル変数を伴った「手続き定義」を言語が許可するときは、グローバルが意味(=値)を持つコンテキストでの関数呼び出しの実行において、グローバルの意味(=値)に微妙な疑問が生じる。
たとえば、

f(3, U)
where
    B = 7;
    C = 5;
    f(X, Y) = X + B * Y + C;
    U = P - Q
    where
        Q = 2;
        B = 3;
        P = f(B, Q);
    end
end

という(where節の)内側での fの「呼び出し」について考えてみる。
この結果はどうあるべきだろうか?
 Pの値は何になるだろうか?

上記のIswimの「逐次的な」解釈に単純に従えば、単に X Yに3と2を割り当て、 X + B * Y + Cという式を評価するだけである。
ところで、評価を開始するとき、 Bの値は何になっているのだろうか?
変数 Bには、この呼び出しが実行される直前に3が割り当てられている。
 Bの値はそのまま3であり、したがって呼び出しの結果が 3 + 3 * 2 + 5、すなわち14であると予想するのは論理的だろう。
一方、(外側のwhere節の)主題で fの呼び出しが実行されると、(外側のwhere節の)本体の最初の文の実行後に Bの値は7になっていることが予想される。
これを少し計算すると、最終的に得られる値は92であると予想される。
(※ f(3, U) = 3 + 7 * (14 - 2) + 5 = 92

しかし、この予想は間違っている。
Iswimの形式的意味論は、この節の値が148であることを示す。
これは、内側の fの呼び出し中に Bが(3ではなく)7を割り当てられていると仮定すると、得られる値である。
(※ P = 3 + 7 * 2 + 5 = 22なので、 f(3, U) = 3 + 7 * (22 - 2) + 5 = 148

この例は、Iswimで従来の手続き的な解釈を用いるときの1つの落とし穴を示している。
Iswimの形式的意味論は、言語が「静的束縛(static binding)」と呼ばれるものを持つことを意味していて、すなわち、グローバル変数は関数が呼び出される環境ではなく、定義されている環境(またはコンテキスト)から値を取得している。
上記の関数 fは、グローバル Bが7と定義されている節で定義されているので、関数がどこで使用されていても Bの値は7となる。
PASCALやALGOLのような本来の命令型言語も静的束縛を持っているにもかかわらず、静的束縛はIswimの逐次的な命令的解釈の観点からはむしろ理解しにくい場合がある。
(※この例の場合、命令型言語であれば、変数 Bをローカルな変数として「明示的に」定義することでグローバルな Bを隠してしまうようにすれば(シャドーイング)、グローバルな Bは(実質的に)静的に束縛されている状態になるわけだけど、Iswimの文を命令型言語と同じように見ると、ローカルを定義しているのではなく、定義済みのグローバルに値を再割り当てしているように見えるので、このような間違いを起こしやすい、ということ)

一方、純粋に宣言的な方法でIswimを解釈し、その定義を任意の仮引数に対して真となる方程式と考えると、静的束縛は完全に自然なものとなる。
以下の定義を考えてみる:

C = 5;
B = 7;
f(X, Y) = X + B * Y + C;

最初の定義は Cが5であり、2つ目の定義は Bが7であることを述べている。
そして、3つ目の定義は、 X Yにどんな値がこようとも、 f(X, Y)の値が X + B * Y + Cであることを述べている。
しかし、 B Cの値がそれぞれ5と7であることは既に分かっているので、 f(X, Y)の値は X Yの値が何であっても X + 7 * Y + 5であると考えることが出来る。

Iswimの宣言的な見方は、Iswimの形式的意味にとても近いので、正しい答えを導き出す。
Iswimの解釈は、値を個々の変数に、関数を関数変数に関連付ける。
Iswimの割り当て/呼び出しモデル(=手続き的な解釈)は、(関数ではなく)「手続き本体」を関数変数に関連付けている(ように考える)ので、誤解を招く。
手続き本体は、グローバル変数の出現を含むことができるテキスト(式)であるが、実際の数学の関数はテキストではなく(それは値の集合間の対応)、「グローバル変数」を持つことは出来ない。

Iswimの命令的解釈には、関数について誤解を招く可能性がある別の微妙な問題がある。
以下のプログラムを考えてみる:

f(3, 4)
where
    f(X, Y) = if X < 1 then 0 else f(x - 1, f(X, Y + 1) fi;
end

この出力はどうなるだろうか?
形式的意味論(および宣言的解釈)であれば、答えは0でなければならない。
変数 fは、常に0を返す2変数の関数として定義されていて、それがこの方程式を満たす唯一の解である。
(※第2引数がなんであれ、第1引数は常に値が減っていくので、どこかで X \lt 1となるため)
しかし、命令的解釈では、 fの呼び出しがいつまでたっても終わらず、プログラムの出力は \perpになると指し示されてしまうだろう。
問題は、命令的解釈に従えば、 f(3, 4)の呼び出しによって f(3, 5)が呼び出され、それはさらに f(3, 6)を呼び出し、 f(3, 7)の呼び出し、 f(3, 8)の呼び出し、と無限に続いてしまうことにある。
実際、このプログラムをPASCALで書き直すと、ずっと深いネストされた呼び出しを生成するので、実行は永遠に終わらない。

関数呼び出しに対する命令型のアプローチの問題は、結果が生成される前に、すべての実引数の値が手続き本体で実際に必要とされることを前提としていることである。
命令型の方法は、手続き本体を実行する前に、シンプルにそれらの値(=実引数の値)をすべて計算する。
実際、結果を出すのに仮引数の値をすべて使わなくてもいい関数を定義できることは、とても有益であるーーたとえ、特定の結果を出すのに、引数のどれが必要となるかを前もって正確に言うことは出来ないとしても。
例えば、PASCALプログラマが言語にif-then-else式(※C言語の3項演算子?:と考えればいいはず)がないのに嘆いて、そのギャップを以下のように定義されたcond関数で埋めようとしたとする:

function cond(P : bool; X, Y : integer) : integer;
begin
    if P then cond := X else cond := Y
end;

そして、階乗の関数を(ローカル変数への)割り当てなしで次のように記述したとしよう:

function fac(n : integer) : integer;
begin
    fac := cond(n < 1, 1, n * fac(n - 1))
end

この目論見はうまくいかず、facの呼び出しは何も値を返さない。
なぜなら、cond関数を呼び出す前に、n * fac(n - 1)を評価しようとするので、nの値によって処理が分岐せず、facの呼び出しが無限に続いてしまうからだ。(※ここ、原文のままだと意味が通じないと思ったので、文を書き変えた)

しかし、Iswimでは、このアイディアは機能する。
すなわち、ユーザは自身で選択関数を定義することが出来る。
以下のプログラムの出力は、形式的意味論に従えば、6となる:

fac(3)
where
    fac(n) = cond(n<1, 1, n * fac(n-1))
    where
        cond(p, x, y) = if p then x else y fi;
    end
end

その理由は、cond(p, x, y)の値は常にif p then x else y fiの値と同じだからであるーーたとえyの値が \perpであったとしてもーー言い換えれば、たとえyの計算が永遠に終わらないものであったとしても。
もちろん、命令的な観点だと、上記のプログラムが決して値を返さないことを示唆することになる。
その素朴な見方(=命令的な観点)は、単に間違っていることになる。

以上から、Iswim( Z)の逐次的な命令的観点は不適切であり、(素朴に従うのだとしても)間違ってさえいるというのは明らかである。
しかし、解釈を修正し、完全に正しいにも関わらずに従来通りであるような実装の基礎として使用することは可能である。
そのためには、グローバルの束縛が動的であることを保証する必要が出てくる(それはそこまで難しくない)。
しかし、関数の仮引数が(事前に評価される)「値」によってではなく、(必要に応じて評価される)「名前」によってであるようにすることも保証する必要がある。
これははるかに困難であるが、不可能ではないーーALGOLはオプションで「名前」による呼び出しが出来る。
それでも、命令的観点は、たとえそれを動かせることが出来たとしても、推薦されるべきではない。
それは単にIswimの「精神」とは違っている。


ということで、前回でIswimのプログラムを手続き型のように解釈する方法を説明したけど、その解釈で出てくる問題を指摘しているのが、今回の話。

まぁ、今の時代から見れば、変数の静的束縛はラムダ式でのキャプチャと考えればいいし、引数の評価のタイミングも、引数に関数呼び出しをベタに書くんじゃなくてラムダ式を書けば評価が遅延されるよね、とかあるけど。
もちろん、引数の式をどの順番で評価するかとかはコンパイラ実装依存となっているので、式に副作用があったりすると、微妙な問題を生み出したりするというのは正しいわけだけど。

そして、評価の遅延の話が出てきてるけど、それについては次の節が「遅延評価」のようなので、また次回に。

今日はここまで!