Scheme R7RSの形式意味論を読んでいく1
はじめに
Schemeの仕様書を見ていました。
ものはここにあります。
small.r7rs.org
https://small.r7rs.org/
さらに日本語訳もいくつかあるようです。
私はここを利用させていただきました。
R7RS 日本語訳 (milkpot様)
http://milkpot.sakura.ne.jp/scheme/r7rs.html
内容は難しいわけでもなく、まあ無難に読み進めていくことができるのですが、
7章の「7.2. Formal semantics」だけは一体なんなんだ。
本当になんなのこれ。
意味が分からない。
ということで長い間どうしたらいいものかと悩んでいました。
「形式意味論」だそうです。
この形式意味論なるものは、Schemeの仕様書を見るまで知りませんでした。
しかしながら知らなかったのは単純に私が無知だったためであり、
40年前から研究されてきたものだとしてびっくり。
情報っぽい大学に行った人なら知ってるみたいですね。
自分はそんなんじゃないので本当に全然知らず、
こんなのやる必要があるのかずっと悩んでました。
でもやります。
本投稿は、素人なりにR7RSの形式意味論を理解するというものです。
形式意味論とは何なのか
「Denotational Semantics」というものらしいです。
日本語訳では表示的意味論、形式意味論、などいっぱい呼び方があります。
~的と~論ってどっから来たんでしょうか。
個人的には「表示意味」が簡潔でいいと思いました。
まあそれはいいとして、日本語訳では「形式意味論」と書かれていたのでそれでいこうと思います。
形式意味論とは、かなりプログラムに近い設計書のことです。
Schemeの仕様書だけ見てると、何の説明も無くいきなり出てくるためびっくりします。
特に自分のような何も知らない人が見るとやる気をなくします。
ずっと「本当にこれを理解しなきゃダメなの?」と思ってました。
それでちゃんと読んでいこうと決心したわけですが、苦労しますよ。
多分どんな人でも苦労すると思います。
形式意味論には、その名の通り「Denotational Semantics」なる本があるので、
もしかしたらそれを読むと簡単に理解できるのかもしれません。
今回は何も知らない自分が覚悟を決めて読んでみたという内容になります。
何も知らないというのがポイントです。
この手の解説は、みんなベテランがベテランに向けて
解説しているものばかりなのではないでしょうか。
私は完全な素人なので、素人が素人に教える視点で話を進めることができると思います。
とはいっても、Lispの経験はまあまああるので、
そのあたりは不親切になるかもしれません。
便利なページの紹介
先にとても分かりやすい所を紹介します。
- 集合論 (数理大好き・tepika_math様)
http://proofcafe.org/k27c8/math/math/set_theory/ - R7RSの形式的意味論を読む (takl様)
https://qiita.com/takl/items/bed48daf47dc071fa828
なんでいきなり集合論なんだというと、
どうも形式意味論って集合論の考え方をベースにしているようなのです。
最初に読んでおくと理解しやすいと思います。
ここでは自分でSchemeを実装できるようになることを優先して理解を進めます。
自分は学者でも何でもないので、やっぱり作ることが目的となります。
形式意味論を正確に理解することを目的としていないのでご注意ください。
とはいっても、そのうちちゃんと勉強したいですね。
それでは順番に見て行きます。
7.2. Formal semantics
仕様書のPDFやらなにやらを同時に見つつ話を進めます。
まずは「7.2 形式意味論」から。
ここでは記法について説明しています。
, sequence formation
単なる列です。
英語ではよくsequenceと言います。
Lispだと(10 20 30)
みたいに書かれるやつです。
集合ではなく列なので順番があります。
こんな感じで表現されます。
空の列なんかもありますので注意。
, th member of the sequence (1-based)
列の要素を取り出します。
Schemeのlist-ref
や、Common Lispのnth
, elt
みたいなもんです。
ただし位置は、0開始ではなく1から始まるとのこと。
例えば、は、
列の2つ目ということなのでです。
ちなみに、Schemeのファイルでは、
この記号の事をそのまんま「elt」定義してました。
Common Lispだ!
, length of sequence
列の長さです。
Schemeでいうlength
関数です。
例えば、は、要素数の3になります。
, concatenation of sequences and
列の連結です。
例えば、は、
です。
Common Lispだとconcatenate
関数になるかと思います。
, drop the first members of sequence
列の、最初から個の要素を削除します。
例えば、だと、
との2つ削除されるため、です。
Common Lispだとsubseq
関数がそのまま当てはまりますね。
* (subseq '(a b c d e f) 2) (C D E F)
余談というか、よく使われるLispっぽい操作を下記に示します。
(car s)
は、
(cdr s)
は、
(cons x y)
は、
となります。
再帰呼出なんかで出てきますので、覚えておくといいと思います。
, McCarthy conditional "if then else "
if
文です。
マッカーシーの条件と言います。
直接書かれている通り、そのまんまif
です。
みたいな感じだと、結果はになります。
どんな使われ方をするかというと、
みたいな感じになります。
, substitution " with for "
に値を格納する際に使います。
はハッシュテーブルのようなものであり、
が値でがキーです。
格納すると言ってもそのものを書き換えるのではありません。
新たにとが結びついている新しいが返却されます。
in , injection of into domain
をの型として扱うという意味です。
正直、この辺はよくわかりません。
ただ使い方を見ていると、という値ができたから、
それを無理矢理D型として扱って下さいと言ってるように見えます。
無理矢理とはどういうことかというと、
「どうせあなたは処理系を作ってるんだろうから、
メモリの書き換えでも何でもいいから
無理矢理キャストして下さい」という意味に感じます。
あと、は型と書いてますが、実際は集合です。
あとでふわっと説明します。
, projection of to domain
をの型として扱うという意味です。
in と同じじゃんということですが、
正直何が違うのかよくわかりませんでした。
こっちの方は、新規で割り当てられたメモリに対して明示的に型を指定したりだとか、
あと変数に対して「この要素を取り出してね」みたいな
操作を行う際に使われていました。
絶対ダメなんだろうけど、何となく雰囲気で使って行きたいと思います。
この章後それ以降
ゴチャゴチャ書いてますが省略します。
話しを進めて行って、必要になったらその都度説明します。
7.2.1. Abstract syntax
抽象構文です。
どういう意味か分かりませんが、型の説明などがされています。
まずは集合について説明します。
集合というのは要素の集まりであり、例えば
のようにした場合、
「集合は、要素で構成される」
という意味になります。
よく使われるのは「」という記号であり、
これは「含まれる」ということを意味します。
例えば要素は、集合に含まれるため、
が成り立つということになります。
形式意味論では、集合を「型」とみなすことができます。
具体的にどういうことなのか、説明を見て行きましょう。
, constants, including quotations
定数についての説明です。
ここで言う定数とは、100とか"Hello"のことです。
また、(quote xxx)
とか'xxx
も定数に含まれます。
とは、
がに含まれるということを意味します。
よって、は要素であり、は集合です。
とはいっても、というアルファベットが要素ではありません。
とは、この世にあるすべての定数の集合だと考えてください。
は要素というよりも変数です。
は集合というよりもなら「型」とみなします。
つまり変数は、型であると言っています。
式の中にが出てきたら、それは型の変数であるという意味です。
「型」というのは、プログラミングでよく出てくる「タイプ」のことです。
例えば「100はinteger
型」とかの型です。
この考え方はずっと出てきます。
繰り返しになりますが、ということは、
「変数は、型である」という意味になります。
注意点を一つ。
仕様書では、を定数以外にも、継続を表す集合という意味で使っています。
なんで同じ記号なのか知りませんが、
どっちのことなのかは文脈で判断するしかありません。
たぶん、K(アルファベット)とΚ(Kappa)の違いなんだと思います。
そんなの分かんないよ。
継続の集合については、あとでちゃんと説明します。
, identifiers (variables)
は識別子の型です。
プログラミングだと識別子より「変数」と読んだ方が自然だと思います。
定数の時と同じように
「変数が集合に含まれる」ということです。
が変数全体の集合で、が変数そのものです。
Schemeでは、変数とはシンボルのことです。
「変数は、型である」という意味です。
, expressions
とは式のことです。
は具体的な式を表しており、今後めちゃくちゃ出てきます。
とかみたいに、に関する表記が出てきたら、
型の変数なんだなと思って下さい。
式とは、10
とか、(+ 10 20 30)
のようなものです。
この式というものの定義が形式意味論の中核であったりします。
, commands
ここは混乱しました。
「」と「」の演算子の優先順位ってどうなってるの?
最初にであり、続けてです。
自分はこの辺の表記を本当に全く知らないため、
がという等号の結果に含まれるって
どういうことなんだと真剣に悩んでいました。
だってとが一緒ならなんで分けるの?って事になりません?
では、なぜ分けるのかというと、は返却値込みの式を表しているようで、 とは、返却値を無視した式ということで分けて考えるようです。
ということで、とは、今のところ同じです。
どういう時に使うかというと、例えばlambda
のbody部を見てみます。
(lambda () 100 ;; Γ1, ここはCom型、値は捨てるから (+ 10 20 30) ;; Γ2, ここはCom型、値は捨てるから 'hello) ;; E1, ここはExp型、なぜなら戻り値に意味があるから
こんな感じの分類になります。
の定義
の説明になります。
まず次のような形になっています。
これは導出です。
マッカーシーの条件(if文のこと)ではないので注意。
とは、右のようなことを意味します。
右辺に現れる縦棒のはorのことで、
区切られたどれかに当てはまりますよと言っています。
つまり「は、か、か、
のどれかになります。」
ということです。
それでは右辺の内容を、ひとつずつ見て行きましょう。
なので、というのは定数のことです。
は、定数も含んでいますよということになります。
100
とか'hello
みたいな定数が評価されると、
型のとして扱われるようになります。
は変数です。
変数には値が束縛されていますので、
その値がとして扱われます。
関数の呼び出しです。
具体的に言うと
(+ 10 20 30)
みたいなやつですね。
表記についていろいろ説明が必要です。
は、なので、式になります。
上の例で言うと、+
の部分に該当します。
続いてというのは、「の0個以上の繰り返し」を意味しています。
つまり、
みたいな感じになります。
も当然です。
上の例にあてはめると、
10が、
20が、
30がになります。
かなり面倒なことに、今回のようにアスタリスクである「*」が付いた場合、
0個以上の繰り返しではなく、「」という一つの変数を表すケースもあります。
大抵の場合は、その変数には、複数の値が格納されている列が格納されています。
では、繰り返しなのか、変数そのものなのかというのは、
見ただけでは分かりませんので、その時の文脈で自分で判断するしかありません。
lambda式です。
は変数がずらずら並ぶので、
(lambda () ...) (lambda (x) ...) (lambda (x y z) ...)
みたいな感じになります。
とは、が0個以上連なるという意味ですが、
最後以外の式は戻り値が無視できるので、
ではなくで表されています。
戻り値はが返却します。
ふと思ったのですが、は必須なんですね。
Common Lispではbody部が空だった場合はnil
が返却されるのですが、
Schemeでは必ずが必要なようです。
つまり(lambda ())
はダメで、
(lambda () 100)
のように、最後に何か書きましょう。
lambda式のdotted listの場合です。
例えばこんな感じ。
(lambda (x y . z) ...)
ちなみに定義ではこんなのも許されてしまうようです。
(lambda (. z) ...)
なんだよこれ。
ダメに決まってんだろ。
lambda式の引数全体を表す表現です。
つまりこんな感じ。
(lambda x ...)
if
文の場合です。
の返却時に応じて実行する式が変わります。
もしfalse
ではなかった場合(真の場合)はを実行し値を返却します。
もしfalse
だった場合はを実行し値を返却します。
普通の関数であれば、の値に関係なく、とは両方とも実行されてしまいますが、if
文は実行を制御できるので特別扱いされているのです。
if
文のelse
項が省略された場合です。
もしがfalse
ではなかった場合(真の場合)にのみ、を実行して返却します。
もしfalse
だった場合はは実行されませんが、if
文全体の戻り値は規定されていないとのこと。
これはCommon Lispとは違っていますね。
Common Lispだと、(if e0 e1)
は(if e0 e1 nil)
と同じです。
この文は、変数に値を代入する命令です。
set!
式の戻り値は規定されていないとのこと。
余談ですが、Common Lispのsetq
とはいろいろ違っています。
setq
の場合は、
(setq v1 e1 v2 e2 ... vn en)
という書式であり、左から右へ変数vi
に値ei
を代入していくというもの。
setq
の戻り値は最後の値であるen
です。
しかしもし(setq)
と呼ばれた場合はnil
が返却されます。
さらにvi
はシンボルですが、もしdefine-symbol-macro
かsymbol-macrolet
が代入の対象になった場合は、そこだけ抜き出されてsetf
式に展開されます。
もう何が何だかわかりませんね。
続きます
次の投稿に続きます。
Scheme R7RSの形式意味論を読んでいく2 - nptclのブログ