見出し画像

Retro-gaming and so on

RE: プログラミング学習日記 2022/06/24〜

星田さんの記事に対するコメント。

まず、もう一回、Schemeの・・・と言うよりGaucheのレコード型に対しておさらいしておこう。
Schemeのレコード型は現在、R7RSでも定義されて公式化したが、基本はSRFI-9が形作ったものだ。



define-record-typeの基本書式は

define-record-type レコード型名
 定義方法
 述語名
 (アクセサ名)...

となっている。
上の写真の例だと

  • レコード型名 -> foo
  • 定義方法 -> (make-foo x y z) : こういう「データ定義用の某」をコンストラクタと呼ぶ。
  • 述語名 -> foo?
  • アクセサ名 -> hogefugapiyo: 実質的にこれら「アクセサ」は定義した構造体の要素にアクセスする為の専用関数定義/名だ
SRFI-9/R7RSのdefine-record-typeはかなり明示的で、「暗黙に色々やってくれる」と言うわけじゃない。ユーザーがコンストラクタの名前とか述語を自ら定義せなアカン様になっている。
っつーことはだぜ?今レコード型として定義してるデータ型と全く関係ない名前を敢えて付ける事も可能なわけだ。


これを自由と捉えるか、あるいは「脈絡ナシ」と捉えるか、ってのは人によって違うわけだが。
あるレコード型によるデータ型定義に於いて、コンストラクタをmake-何とか、にするのはあくまで習慣なわけだけど。習慣を強制すべき、ってのに反対する人もいるわけだが、特に理由が無ければ従うべきなんじゃないか、って人も多いと思う(コーディング規約、ってヤツだな)。
いずれにせよ、SRFI-9だと自由度が高すぎるんだ。
そこでGaucheはSRFI-9の仕様にSRFI-99を混ぜている。
SRFI-99が提案したのはコンストラクタ定義部分で#tを与えると自動でmake-何とかと言う名前でコンストラクタを定義する事。述語定義箇所で#tを与えると何とか?と言う述語を自動で定義してくれる。
また、コンストラクタ定義部分で#tを与えた場合、アクセサ定義部分で与えられた情報を基にして自動的にコンストラクタをmake-何とかのスタイルで定義してくれる。



次にレコード型のミューテータに付いて。
ミューテータってのは日本語で変更子、とか呼ばれるけど、要するにデータ内容を破壊的に変更する為の機能だ。
元々、SRFI-9/99やR7RSの場合、

(コンストラクタの要素 アクセサ名 ミューテータ名)

と言うカタチでミューテータを設定する。



Gaucheの場合、これらをもっとシンプルに使える為に、SRFI-99の書式に従った上に、ミューテータを自動で設定する時にアクセサ名定義時にカッコで囲むとアクセサ名-set!で自動的にミューテータを設定してくれるわけ。


ここまでがSRFI-9/99、R7RS及びGaucheでのレコード型の話だ。

ここからはまた別の話になる。SRFI-17の「一般化set!」の話だ。
その話をする前に、ちとANSI Common Lispに話を移す。
何となくANSI Common Lispのsetf = Schemeのset! って思ってるかもしれないけど、実は違う(※1)。
例えば次の例を見てみよう。

CL-USER> (defparameter *v* (vector 1 2 3))
*V*
CL-USER> (setf (aref *v* 0) 0)
0
CL-USER> *v*
#(0 2 3)
CL-USER>

これは、大域変数*v*を1, 2, 3と要素を3つ持つベクタとして設定し、第一要素を0に書き換えている。
注目して欲しいのは、

(setf (aref *v* 0) 0)

と言う「要素の書き換え」方法だ。
これはRacketで確かめて欲しいけど、通用しない書き方だ。

> (define *v* (vector 1 2 3))
> (set! (vector-ref *v* 0) 0)
. set!: not an identifier in: (vector-ref *v* 0)
>

ANSI Common LispのarefとScheme/Racketのvector-refはどちらもベクタの要素番号にアクセスしてそこの値を返す、と言う作用は同じだ。
問題はsetfset!の違いだ。Scheme/Racketのset!はあくまで「なんかの変数」の値を変更するだけのミューテータなんだけど、setfは「アクセスした要素の中身を書き換える」と言う作用がある。そして今「アクセスした」と書いたが「何の」とは言ってない。今はベクタを例に出したけど、何もベクタに限らないのだ。
上のようなシチュエーションでScheme/Racketで使う手・・・と言うよりも、Scheme/Racketでの「ミューテータ」と言うのは「データ型によって違う」ってのが答えになってる。
上の例だと単純にこうするだろう。

> (vector-set! *v* 0 0)
> *v*
'#(0 2 3)
>

vector-set!はベクタ専用のミューテータであってそれ以上じゃない。
逆に、ANSI Common Lispの場合は、アクセサさえ定義されていれば全部一律setfでどーにでも変更出来るんだ。
ANSI Common Lispのsetfのパワーが欲しい(※2)。その要求に対しての回答がSRFI-17の「一般化set!」だ。
一般化set!ならvector-set!みたいなベクタ専用のミューテータは必要がなくなる。

> (require srfi/17)
> (set! (vector-ref *v* 0) 1)
> *v*
'#(1 2 3)
>

なお、Gaucheの場合、SRFI-17の一般化set!がデフォルトで使えるようになってる模様だ。



つまり、Gaucheでの発想、デフォルトで一般化set!が使える、以上、一般化set!でレコード型の要素変更は行えなければならない、と言う事になる。



オンラインマニュアルで構造体の破壊的変更方法を確認。覚えられんw

と言うわけで、オンラインマニュアルの後半は、「レコード型の話」じゃなくって「一般化set!」の話なんだ。

※1: ANSI Common LispでSchemeのset!にあたるのは古き良きsetqだ。setfsetqを元にして組み立てられたおっそろしく複雑なマクロ、ってのがその正体だ。

※2: ただし、一般的にはこの考え方はSchemeコミュニティには受け入れられていない。
以前から書いてるが、ANSI Common Lispは「総称指向」で、結果、データ型が違っても似たような作用があるのなら単一の関数なりマクロにまとめてしまうのが仕様上の性質で、setfみたいなマクロが出てくるのは当然だと言える。
反面、Schemeでは、「データ型が違えば関数も違う」と言う考え方のプログラミング言語で、データ型に合わせてミューテータも違うべきだ、と言う考え方が受け入れられている。
  • Xでシェアする
  • Facebookでシェアする
  • はてなブックマークに追加する
  • LINEでシェアする

最近の「RE: プログラミング学習日記」カテゴリーもっと見る

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