見出し画像

Retro-gaming and so on

Racketのパラメータ

さて、前回「おっさんの悪あがき」と切り捨てたダイナミック・スコーピング。
ところが、Schemeの最新仕様であるR7RSではこの古のダイナミック・スコーピングが何と復活してるのだ。
背景としては、どうやらマルチスレッドに対するプログラミングや、並列コンピューティングに於いてダイナミック・スコーピングが有効だ、って事らしい。
しかし、仕様書をパッと見ただけではイマイチ意味が分からない。前回も書いた通り、そもそも現代世界を生きる我々としては、レキシカル・スコーピングの方に原則慣れているのだ。
そんなわけで、一旦fluid-letでダイナミック・スコーピングの背景のアイディアに慣れていた方が良い、と言う事だったのである。

元々、Schemeでは敢えて伝統的なLispのスコーピングルールであるダイナミック・スコーピングを排するような設計が行われていた。
しかし、おっさんの悪あがきで(笑)、やっぱダイナミック・スコーピングを扱える変数が欲しい、っちゅうんで、SRFI-39と言う仕様要求が提案されていた。
そしてR7RS-smallと言う仕様で、目出度くSRFI-39と言う仕様要求がSchemeの仕様書に取り入れられたわけだ。意味はサッパリ分からんけどよ

このSRFI-39ベースのダイナミック・スコーピング絡みのパラメータ。ANSI Common Lispのdefparameterとは全然違うのがまた混乱の元なんだ。
ANSI Common Lispのdefparameterはあくまで「(スペシャル)変数」を定義する為のモノで、スコーピングがダイナミックであろうと「変数」の範疇は超えない。
一方、R7RSのmake-parameterは変数を生成するわけではない。パラメータ・オブジェクト、と言う新種のデータ型を生成するのが意図のようだ。
しかも、R7RSの仕様を見る限り、じゃあ、そのパラメータ・オブジェクトと言うのは何なんだ、ってのは定義されていない。要するに、それに絡む「述語」が全く存在しないんだ。これはSchemeらしくない。
ぶっちゃけた話、R7RSに準じた実装にしてるScheme処理系だとparameter?の類の述語が実装側の判断で用意されている事と思う。そして、パラメータ・オブジェクトの正体は恐らく単なるラムダ式だろう。つまり、procedure?にも反応するわけだが、Schemeだと「複数の述語が1つのデータ型に対して#tを返す」のは多分あんま望ましくない、って考えられてるのではないか(※1)。つまりこの存在自体がやっぱりSchemeらしくないのだ。
もう1つ、R7RSのパラメータには問題があると思う。それはパラメータ・オブジェクトの「再定義」はどうなるんだ、ってのが「未定義」になってるんだ。
R7RSでのパラメータの例を見ると、

(define radix
 (make-parameter
  10
  (lambda (x)
   (if (and (exact-integer? x) (<= 2 x 16))
    x
    (error "invalid radix"))))

とした時、

(radix 16) =⇒ 規定されていない

となっている。
もうちょっと砕けて言うと、この例の場合、大域変数radixはパラメータとして10が束縛されてるわけだが、その大域環境で16に値を変更したい、と思った場合、結果が未定義だ、と言う事だ。
厄介なのは、Schemeの標準仕様上だと、set!を使ってこれを再束縛が出来ない、って辺りである。そうするとこの仕様だと一旦defineでパラメータオブジェクトを定義しちまうと、parameterizeマクロを使わない限りその値は一切変更不可能だ、と言う事だ。
もちろん、関数型プログラミング観点だとこの性質は望ましいのかもしれないけど、「出来ない」って言われるとちと混乱するよな。
実際問題、元ネタのSRFI-39だとパラメータオブジェクトに属してる値はset!を使わなくても簡単に変更可能だ。

(define radix
 (make-parameter 10))

(radix)           ==>  10
(radix 2)
(radix)           ==>  2

初期状態で10と定義されたパラメータオブジェクト(radixに束縛されている)は大域環境で2を与えると以降はずーっと2となる。
我々が「変数に望む」機能はまさしくこの通りなんだが、どうもR7RSだとこの部分を削除したい、ように見える。
余計、一体これは何の為にあるんだか、良く分からなくなるわけだ。パラメータは単純な変数として使えない(※2)、と言う事になる。

とまぁ、Schemeの話をダラダラと書いてきたが。

「あれ?RacketはもはやSchemeではない、って言ってなかったっけ?R7RSはもはや関係ないでしょ?」

と考えたアナタ。エラい。その通りだ。
いや、単純にRacketにもパラメータがある、と言う話をしたかったんだ。現行の標準Schemeの話は単なる前フリである。
Racketに限定すればRacket固有の話をすればいいだけ、であり、上のような(どう見ても?)「R7RSの不備」なんかは関係がなくなる。ただ、まぁ、一般論としてダイナミック・スコーピングに纏わるアレコレ、を取り上げておきたかったわけだ。
まぁ、この辺もRacketのドキュメンテーションがワヤクチャな為、理解するのが困難だ、って事があるわけだが。

Racketもダイナミック・スコーピングを取り入れてる理由は、上に掲げた理由と同じ、つまり、マルチスレッドプログラミング対応の為、である。そのために、古のfluid-letよりSRFI-39で提案されたパラメータを本体に取り込んだようだ。
独習Scheme三週間のレキシカル変数の例を辿って、Racketのmake-parameterと、fluid-let代わりのparameterizeの動作を見てみよう。make-parameterは単独でパラメータを生成するが、parameterizeは極論、make-parameterで作ったパラメータ専用のfluid-letである。

(define counter (make-parameter 0))

このS式により大域変数counterが定義されたが、その値は単なる0ではなく0を中に含むパラメータオブジェクトだ。
ここで定義された大域変数counterは結果パラメータオブジェクトとなり、それに値を与えると(というのがそもそもヘンな表現になるが)、別の値を含んだパラメータオブジェクトに更新される。
fluid-letで見たような関数bump-counterはこのような性質を鑑みると次のように書ける。

(define (bump-counter)
 (counter (add1 (counter)))
 (counter))

先にも書いたが、大域変数counterはパラメータオブジェクトであり、値を再設定する際にはset!を用いる必要がない。単に関数のように引数を与えると、その値を持ったパラメータオブジェクトは更新される。
また、パラメータオブジェクトの中身を取り出すには無引数関数のようにカッコでオブジェクトを包む。
この状態でbump-counterを三回呼び出すと次のようになる。

> (bump-counter)
1
> (bump-counter)
2
> (bump-counter)
3
>

そしてこの時点で大域変数counter(のパラメータ・オブジェクトの中身)は次のようになっている。

> (counter)
3
>

set!を全く使わないのに、値が更新されてるのが分かるだろう。
そしてこれがSchemeのR7RSでは「未定義」とされた動作だ。Racketのmake-parameterはむしろ原作のSRFI-39の要求仕様に近くなっている。
次はparameterizeの使い方、だ。上にも書いたが、事実上、これはパラメータ・オブジェクト用のfluid-letだ。
ここでインタプリタで次のように打ち込んでみる。

(parameterize ((counter 99))
 (display (bump-counter)) (newline)
 (display (bump-counter)) (newline)
 (display (bump-counter)) (newline))

結果を見てみよう。

> (parameterize ((counter 99))
 (display (bump-counter)) (newline)
 (display (bump-counter)) (newline)
 (display (bump-counter)) (newline))
100
101
102
>

ご覧の通り、独習Scheme三週間で見た通りの、fluid-letと全く同じ結果が返ってくる。
そしてこれを走らせる前は大域変数counter(が束縛してるパラメータ・オブジェクトが抱えた値)は3だった筈だ。ダイナミックスコープを抜けた時点でcounterの値は元に戻ってる筈なのだが・・・・・・。

> (counter)
3
>

fluid-letと同様に元に戻ってるのが確認出来た。これでparametizefluid-let同様の結果を得られる事が確定したわけだ。

まぁね。使いどこが難しい、って言えば難しいんだけど、R7RSにも取り入れられた、って事は「レキシカル・スコーピングのLispでも(局所的)ダイナミック・スコーピングの重要性」が今後上がっていく、って事を表してるって言えるだろう。
「おっさんの悪あがき」が悪あがきどころじゃなく、今後重要な位置を占めていく可能性がある、ってこった。うん、ダイナミック・スコーピングは「過去の技術」から「これからの技術」になってく、って事だよな。
ハッキリ言って、今現在はダイナミック・スコーピングに向き合うには「良い時期だ」とは思ってる。何故なら今の(おっさんを除いた)人たちは通常「ダイナミック・スコーピング」が何なのか全く知らない。
そんな中でLispを使ってればそういう(実際は古いんだけど・笑)「進んだ概念」に触れるチャンスがあって、実際今ここで「触れる事が出来る」って証明になってると思う。
これからのプログラミングは、レキシカルとダイナミック、両方嫌でも付き合っていかないとならない可能性が高いんだ。

※1: 実際はんなこたぁねぇが、ANSI Common LispやSchemeはデータ型のヒエラルキーが割に明解に作られていて、親データに対するサブセットがかなり厳密に定義されているように見える。
要するに、procedure?parameter?はどういう親子関係になってるのか、と問うと、直感を排して言う限り「分からん」としか言いようがない。結果としてparameter?は定義されていない、って事なんじゃないか。
このテのややこしい事はcall/ccなんかにも現れていて、要するに簡単に「継続」とか言っても、continuation?を仕様上簡単に言えないのでは、と言う疑いがある。これも仕様上だと明らかにprocedure?で#tを返すし、この二者にヒエラルキーがあるのかどうか、直感性を排するとなんとも言いようがない。

※2: そしてそもそも、パラメータを「呼び出す」時にカッコがある事を考えると、やっぱこいつの正体は単なるラムダ式だ。
  • Xでシェアする
  • Facebookでシェアする
  • はてなブックマークに追加する
  • LINEでシェアする

最近の「プログラミング」カテゴリーもっと見る

最近の記事
バックナンバー
人気記事