Scheme R7RSの形式意味論を読んでいく4
続きです
前回の続きとなります。
Scheme R7RSの形式意味論を読んでいく3 - nptclのブログ
【new】newの実装を考える
これ以降はnew関数が現れるため、メモリに関する説明が必要です。
newは新たにメモリ領域を確保する関数です。
関数の定義は次のようになります。
「」を実行したら、 値を格納できる新しい場所が作成され、 その位置が返却されます。
ただしメモリ割り当てに失敗する場合もあります。
そういうときは、というオブジェクトが返却されるそうです。
よって、new関数の戻り値は純粋なではありません。
new関数を使う時には、典型的な書き方があります。
で内容を確認し、
もしエラーだったらwrong関数を呼び出すようにします。
例えば次のようになります。
new関数の内容を見て行きましょう。
仕様書にある説明は、次の一文だけです。
ならば
これは、新しく作成されたメモリの内容を表しています。
そもそもがを返却する関数なので、
newによって新規に作成されるものは
という列になります。
その第二要素のブール値が、初期値はfalseだという意味です。
つまり、
みたいな要素が作成されるのだと思います。
ここまでは何となく理解できます。
でも、そもそもfalseって何を意味することなの?
説明が全然ないから困るのです。
ここからは私が何となく思ったことを書きます。
合っているのかどうかは分かりません。
しかし無理やりにでも解釈しないと、
後々の話を理解するのは不可能だと思います。
がんばってついてきて下さい。
まず第二要素のfalseは、
確保されたけどまだ使われていない領域です。
この値がtrueにならない限り、
は何回呼び出されても全く同じものを返却します。
実際にが使われている所を見ると、
何度も何度もが呼び出されています。
一見すると、C言語で言うmalloc関数が何度も呼び出されているように見えます。
しかし多分違います。
が新たに違う要素を返却するには、
すでに確保したされた列の第二要素がtrueになっていなければなりません。
その処理はupdate関数が行います。
実行内容は、メモリの位置にある内容を、
に書き換えるという意味です。
この操作を行った後でを呼び出すと、
全く違う位置が返却されるようになります。
上記の考え方が正しいと仮定して話を進めますが、
残念ながら正しくない可能性もあります。
まずの使い方が良くわからないのです。
変なふうに関数の引数に出てきたり、突然あらわれたりと、
どういう扱われ方をしているのか全然わかりません。
もう少し詳しく見て行きましょう。
【new】の扱いを考える
の扱いがあまりにもおかしいので、ちゃんと調べなければなりません。
まずはnewの使い方について見て行きましょう。
newはに新しい要素を追加します。
引数のに対して、破壊的に更新すると考えてもいいと思います。
新しい領域に値を格納する方法が、
という操作です。
ここでというのは、のことです。
は破壊的ではありません。
には全く手を付けずに、値が変更された新しいが作成されます。
この考え方が非常に大事です。
もし複数の値を書き込みたいのであれば、元々のではなく、
の方をたらいまわしにしなければなりません。
newには典型的な書き方がありますので、下記に示します。
この処理は、まずupdate関数が新しい領域に値を割り当てています。
update関数の戻り値は、新しいなので、
をlambda式の引数に渡してbodyを実行しています。
つまり、古いと新しいを、lambda式を使って繋ぎ変えているのです。
問題があり、new関数はエラーチェックが必要です。
そこで次のような書き方をします。
この書き方は、本当に意味が分かりません。
前に説明した「lambda式とマッカーシーの条件」を思い出して下さい。
つまり、次のように書き直すことができます。
この考え方は合っていると思います。
しかし説明がつかないところもあります。
下記の例を見てください。
この例だと、の次にupdateが実行されるので、 lambda式内ののnewは、 別の内容が新規で作成されるのでは?
何か解釈が間違っているかもしれません。
でも、そう言うものだと考えて無視しましょう。
なぜならもっと大きな問題があるからです。
いちばんおかしな部分を説明します。
上記のように苦労してをネストしても、結局はが無視されてしまっています。
そもそもってどこから出てきたんだ?と思いませんか。
はとは違っていて、の引数には存在しません。
しかし、例えば
なんかではメモリから値を取り出さなければいけません。
その取り出している処理はhold関数なのですが、
すでに話題に出した通り、hold関数の中ではが突然出てきています。
そう言う部分はいっぱいあります。
たぶんですが、初めはをたらいまわししていて、
途中からやっぱりやめるわ、と言ってを削除しようとしたものの、
そんなに簡単に削除できなくてめちゃくちゃになったのでは?
よくわからないので、もうについて考えるのをやめます。
無視していいと思います。
一応言っておきますが、私は素人ですので、何か間違っているとは思います。
でもこの辺の解釈に何日もかけてますので、これ以上は時間の無駄です。
Schemeの形式意味論は、メモリ関係やローカル変数などの扱いを確認するのではなく、
実行時に値と継続の関係がどうなっているのかだけに注目したほうがいいでしょう。
【new】エラーのないnew関数
ごめんなさい。
前の説明であったように、あたりの説明はやめます。
そうなると、new関数のエラーと、updateのタイミングなど、
シビアな部分にいちいちお付き合いする必要が無くなります。
自分が使いやすい、新しいnew関数を定義します。
通常のnew関数は、エラーの場合を考慮しています。
でも正直いらないのでは?
というのもLispのシステムは、newにエラーが生じた時点でほぼ続行不可能な状況なので、
abortしてしまう以外やりようがないのです。
Schemeではありませんが、Common Lispのsbclもclispもabortします。
私が作ったnptに至っては、Lispを諦めてC言語上で異常時の対応ができるようになっています。
Schemeの仕様書では、wrong関数に返却があります。
たぶんエラーが生じた後でも続けることができるようになっているのだと思います。
そんなのやめましょう。
エラーが生じたらabortする新しい関数wrong*を定義します。
内容は実装依存です。
こうすることで、new関数もかなり使いやすくなります。
newとupdateを同時にこなす、new*関数を定義します。
の扱いが完全に不明のため、グローバル変数として使うことにしました。
戻り値の確認が無くなったため、非常に使いやすい関数になったと思います。
newの返却は「」なので、
返却値はからだけを取り出すには
「」みたいな書き方をする必要がありましたが、
new*はだけなのでそのまま使うことができます。
new*の例として、cons関数を定義すると次のようになります。
もとの内容と比べて見ると、ずいぶんすっきりしたと思います。
定義を示します。
まずは「エラーのないnew関数」であるnew*を使うように書き直します。
ずいぶんとすっきりした形になりました。
説明をする前に、内部で使用されているtievals関数を見て行きます。
こちらもまずはnew*で書き直します。
これはこれで合っているのですが、 のクロージャーに 全体の値が保存されそうなので、 ちょっと書き方を変えます。
tievalsは再帰呼出を使い、
の全ての要素にnew*を使い列を値を格納します。
入力はの配列ですが、
関数に渡る前まではnew*の列なのでの配列です。
新しく得られた列に対して、関数を実行してが返却されます。
つまり、入力
が、new*によって
になって、最後に
が実行されます。
これも典型的な再帰呼出なので、
Schemeで書くと例えばこんな感じになります。
(define (new x) (* x 10)) ;; ★確認のため10倍する (define (tievals psi eps) (if (null? eps) (psi ()) (let ((e1 (car eps)) ;; eps ↓ 1 (e2 (cdr eps))) ;; eps † 1 (tievals (lambda (alpha) (psi (cons (new e1) alpha))) ;; <new* e1> § α* e2)))) (tievals (lambda (x) (list '< x '>)) '(10 20 30))) -> (< (100 200 300) >)
それでは続けてextends関数の説明をします。
この関数は、変数の列と、位置の列を受け取り、
変数に対応する場所に位置を書き込んでいく関数です。
再帰関数なので、先頭にある要素からひとつずつ処理をして、
残りの処理は自分自身に任せています。
それではに戻りましょう。
sendの部分を見ると、次のようになると思います。
「」の
意味が分からないのですけど、
列に対して無理やりを作ってよ、と言っているのだと思います。
sendの第一引数はなので、これは問題ないと思います。
ではを見て行きます。
この式とは、 引数の列、dynamic-points、継続を引数に取る関数であり、 典型的なSchemeの関数を表しています。
内容は、まず継続から受け取ったの列長さと、 lambda式の引数の列の長さを比べ、 もし違っていたら、それは関数呼び出しの間違いなのでエラーになります。
これは単純に
(define (aaa x y z) ;; I*の列 ...)
みたいな関数があったとして、
(aaa 10 20 30 40) ;; ε*の列
で呼び出した場合、引数の長さが4なのに、
の長さが3で合ってないじゃん。
ということになります。
長さチェックをクリアしたら、ようやくtievals関数の呼び出しです。
第一引数はどういう意味なのでしょうか。
lambda式が2つありますが、内側の方はただ単に
extendsの戻り値を変数に代入しているだけです。
は、
を用いて、引数の変数に値を束縛していきます。
例えば、
だった場合、の変数表には, は, はと
メモリに書き込みます。
それがすんだら次は下記を実行します。
なにやら複雑そうに見えますが、
lambdaのbody部を順番に実行して行っているようです。
まずはの定義を見てみましょう。
もはや見慣れてしまった再帰関数の実行です。
最初に形式意味論の式を見たときは本当に意味不明だったけど、
そろそろ慣れてきたのが驚きです。
まずですが、ただ単に継続を返却するだけです。
次には、
まずを普通に実行します。
ただし、継続を次のように置き換えています。
これは単純に残りのをひたすら実行するだけのようです。
なお引数のは、継続の引数なので、つまりは戻り値です。
上記の式ではは無視されていますので、戻り値が捨てられているということです。
話を戻します。
何かこれおかしくないですか。
まずを実行するには、事前に3つの引数が必要になります。
つまりを
先に実行する必要があります。
実行する順番が変です。
本来であればを全部実行してからが実行されるはずです。
しかし今の状態だと、例えば、
(lambda () (aaa) (bbb) (ccc))
という関数を実行した際、
まずは真っ先に(ccc)
が実行されることになります。
しかも(ccc)
の実行が終わったら、(aaa)
と(bbb)
が実行されるのではなく、
(ccc)
の実行結果がそのまま継続に渡されるので、
(aaa)
と(bbb)
は全てが終わった後に実行されます。
あってるのかな。
ちょっと自信ない。
何か勘違いしている可能性が高いですが話を進めます。
無理やり書き直すならこうなると思います。
この式は、まず真っ先にが実行されます。
ただしは引数にコマンドの継続が必要なので、
適当な継続を渡しました。
あらかじめ用意しておいてください。
用意できないなら
で全然問題ありません。
引数に渡した継続はどうせ無視されます。
の返却値はの引数に渡されますが、
すぐにを実行するのでは捨てられます。
たぶんこれでうまく行くはず。
の実行をまとめます。
まずはローカル変数を作り、継続から変数と値を割り当てます。
そのあとbody部を実行し、最後の式を返却値として継続に渡します。
以上です。
定義を示します。
「エラーのないnew関数」で書き直します。
前に解説した
とほぼ同じです。
違う部分は下記の通り。
- 引数の個数チェックがに
- tieval関数ではなく、tievalrest関数の呼び出しに
引数の個数チェックについては分かると思います。
等しいのではなく、継続の値の方が多い分には問題ありません。
tievalsrest関数は説明する必要があります。
説明してない関数が3つも出てきます。
- dropfirst
- takefirst
- list
まずは先にこれらの関数を説明して行きます。
dropfirstは列の最初から番目までが削除された列を返却します。
takefirstは列の番目より先の列を返却します。
例を見るとすぐわかると思います。
あと、dropfirstはと同じです。
なぜ同じものを関数として作ったのかというと、
おそらくはtakefirstと合わせたかったのでしょう。
それではlistの定義を示します。
この関数にも問題があり、引数の数が一致してません。
しかしいつも通り「暗黙のlambda式」のパターンではなく、
宣言の数は3つで合っているのですが、関数呼び出しが全部2つになっています。
よくよく見てみると、引数のが使われていないので、
list関数は引数を2個に出来そうです。
書き換えるのは少々手間がかかるので、
まずはlist関数の意味を説明して話を進めます。
list関数の目的は、列をリストに変えることです。
引数は列であり、
再帰呼出によってリストを作り、
それを継続に渡して実行します。
もう少し説明しますが、列とは形式意味論だけで使われている
のよう値です。
一方リストとは、Schemeで使われるペアの集合です。
ペアとは、コンスとも呼ばれており、car, cdrの二つの値を取るオブジェクトです。
ペアを(car . cdr)
と表すとき、
10, 20, 30,の値を持つリストは
(10 . (20 . (30 . ())))
と表すことができます。
ペアを作る処理はcons関数であり、
list関数内で呼び出されています。
それではtievalsrestに戻りますが、この関数は引数を3つ受け取ります。
第一引数は、tievalsの時と同様、最終的に呼び出される関数です。
第二引数は、関数呼び出しの引数の列です。
第三引数は、lambda式で必須の引数の個数です。
例えば
(lambda (x y . z) ...)
の場合、必須の引数はxとyなので、は2です。
仮にこの関数が次の引数で呼び出された場合、
tievalsrest関数内でdropfirstが列を削除するので、 次のようにlist関数が実行されます。
list関数は(30 40 50)
というリストを作成した後、
継続内で次の文を実行します。
ここで、
なので、
という文が実行されます。
なお、は列であり、はリストです。
あとは今作成した列にあうように、extends関数でcdr部の変数を加えれば、
前に説明した
と同じになります。
分かりづらい所は、リストを作るときにちゃんコンスで作っているということでしょうか。
最後にlist関数を書き換えます。
list関数はtievalsrest関数でしか呼び出されていないようなので、
引数のを消してしまいましょう。
あわせてcons関数も書き換えたかったのですが、
cons関数内のtwoargを修正するのは手間だったので、
それならダミーの値を渡してしまおうということで
を渡しています。
この値はの説明で出てくるものです。
以前紹介した、書き換えを行ったcons関数を再掲します。
定義を示します。
うーん。
いいんじゃない?
定義を示します。
非常に分かりやすい文です。
まずはを実行し、返却値を継続に渡します。
継続では返却値を元に、かかの実行を選択します。
truish関数の定義を見てみましょう。
要するに、false以外が真ということです。
定義を示します。
こちらも難しくないです。
else項が省略されたら、unspecifiedを返却してくださいとのこと。
注釈があり「別にunspecifiedにこだわらず、
何を返却しても良いよ、でもundefinedだけはやめてね。」
だそうです。
定義を示します。
まずはassignを見てみましょう。
引数の数があっていませんので、暗黙のlambda式として書き直します。
この意味は少しわかりづらいです。
まずはupdate関数で位置に値を代入します。
そのあと、lambda式が実行され、がそのまま返却されます。
それではset!文に戻ります。
まずはによってが評価され、継続に結果が渡されます。
継続内ではlookup関数により、変数の位置が求められます。
そしてsend関数により、set!式の継続にunspecifiedが渡されてます。
その後、assign関数により値の代入処理を行います。
これって本当にあってる?
assign関数が実行するまえに、send関数で継続を実行していませんか?
本来だと継続を実行する前にassignで割り当てなければならないはず。
間違ってるように思えるんだがどうなんだろう。
この手の問題は、lambda式のbody部にもありました。
私が勘違いしている可能性が高いですが、書き直してみます。
重要なのはupdateを実行してから継続を実行していることです。
あと、assignで検索してみると、全部継続にunspecifiedを渡しているようなので、
固定値にしてしまいました。
元の式を次のように書き直します。
続きます
次の投稿に続きます。
Scheme R7RSの形式意味論を読んでいく5(おわり) - nptclのブログ