subtypep実装の基本
正直、あっているかどうかわからないのですが、
確認のために記載していこうと思います。
今回の投稿だけでは終わらないので、気が向いたら続きを書きます。
subtypep
の使い方
subtypep
とは、左の型が右の型に含まれているかどうかを確認する関数です。
こんな感じで覚えておけばいいと思います。
(subtypep 小 大) のとき t
正確には等しい時もt
となります。
例をあげます。
;; integerはrealに含まれる。 (subtypep 'integer 'real) →t; t ;; stringはstringに含まれる(同じなので) (subtypep 'string 'string) →t; t
返却値は二つありますが、第二返却値がnil
の場合は
判断できなかったという意味になります。
型satisfies
を用いると、たぶん大体はnil; nil
となります。
(subtypep 'integer '(satisfies hello)) →nil; nil
実験では、hello
という関数が存在しなくてもnil; nil
が返却されました。
つまり関数を実行すらしていないということです。
なお、第二返却値がnil
の場合は、必ず第一返却値はnil
です。
t; nil
のような返却のパターンはありません。
よって、返却値は下記の3通りとなります。
第1 | 第2 | 意味 |
---|---|---|
t |
t |
含まれる |
nil |
t |
含まれない |
nil |
nil |
わからない |
subtypep
の実装はとても難しいです。
理由は、実数の範囲指定、and
、or
、not
があるからです。
範囲指定とは例えば次のようなことを言います。
(subtypep '(integer 10 20) '(real * 40)) →t; t
整数10~20
は、実数40
以下に含まれます、という回答になります。
and
, or
, not
はそのままなので、
あらためて説明する必要はないかと思います。
例えばこんな感じ。
(subtypep 'integer '(not character)) →t; t (subtypep '(integer 30 80) '(or (real 10 50) (real 40 100))) →t; t
範囲指定とand
, or
, not
を一気に説明するのは無理なので、
今回はnot
から説明していきます。
subtypep
のnot
型not
とは、その名の通り反対を意味します。
subtypep
にnot
が含まれる場合はどうなるかわかるでしょうか?
例えば下記の例を考えます。
(subtypep 'integer 'real) →t; t
含まれる場合は、not
にすると、返却値はたぶんnil
になるのではないでしょうか。
(subtypep '(not integer) 'real) →nil; t
あっていました。
じゃあ右をnot
にすると?
(subtypep 'integer '(not real)) →nil; t
こっちもあっていますね。
では次の例を考えましょう。
(subtypep 'string 'real) →nil; t
元々の返却値がnil
の場合は、否定したらt
になるのでしょうか?
(subtypep '(not string) 'real) →nil; t
なりませんでした。
でも右をnot
にするとt
になります。
(subtypep 'string '(not real)) →t; t
じゃあ、もともとnil
の場合は、右をnot
にすると返却値は必ずt
になるのか?
と言うと、そうでもありません。
(subtypep 'real 'integer) →nil; t (subtypep 'real '(not integer)) →nil; t
どういうこと?
これを理解するには、型の範囲をちゃんと考えなければいけません。
次の2例を考えます。
(subtypep 'string 'real) →nil; t (subtypep 'real 'integer) →nil; t
どちらも返却はnil
ですが、分けて考える必要があります。
上のstring
とreal
は、重複することが全くない、完全なる排他です。
しかし下のreal
とinteger
は、重複する部分があるものです。
両者が完全に排他の場合、右を否定した場合のみt
となります。
これは図にするとわかりやすいのではないでしょうか。
例えば、例として含まれる範囲を|****|
で表すとしましょう。
(subtypep 'string 'real) string: ------|****|---------------- real : -----------------|****|----- ★互いに疎で含まれない
分かってもらえるでしょうか。
横軸はてきとうです。
こんな感じだとして、|****|
の部分がお互いに含まれないと表現します。
real
のnot
を取ると次のようになります。
(subtypep 'string '(not real)) string : ------|****|---------------- (not real): *****************|----|***** ★|ここ|★が含まれる
こうすると、string
の|****|
の部分が、(not real)
に
完全に含まれています。
一方、real
とinteger
は次のようになります。
(subtypep 'real 'integer) real : ------|*****************|--- integer: -----------------|****|----- ★realの方が大きいのでnil
real
とinteger
は互いに疎ではなく、重複する部分があります。
よってnot
を取ると次のようになります。
(subtypep 'real '(not integer)) real : ------|*****************|--- (not integer): *****************|----|*****
(not integer)
はreal
を包括できていません。
こんなふうに、subtypep
では、nil
を返却する場合は、
排他的なのか、そうじゃないのかをちゃんと調べないと、
not
の場合に対応することができません。
subtypep
でnot
の有無を網羅すると、パターンは次の4通りになります。
(subtypep 'a 'b) (subtypep '(not a) 'b) (subtypep 'a '(not b)) (subtypep '(not a) '(not b))
このうちで、もともとnil
だったものがt
になるパターンは次の2通りです。
(subtypep 'a '(not b))
a
とb
が排他的の場合はt
(subtypep '(not a) '(not b))
a
がb
を含む場合はt
下の場合の、2つがどちらもnot
の判定は楽です。
a
とb
を逆にした(subtypep 'b 'a)
を調べればいいだけですから。
問題は排他的の場合です。
これは計算だったりアルゴリズムで算出できるものではなく、
実装する段階で、これこれのパターンは排他ですよという情報を
自分で返却するようにしなければなりません。
どうやって実装するのかというと、地道にif
文を並べて作りましょう。
subtypep
実装の基本の第一段階は、
not
, and
, or
が含まれない場合の関数subtypep!
を作成し、
次の4パターンを返却をさせることです。
(subtypep! 'a 'b) :true 含まれる t; tに対応 :false 含まれない nil; tに対応 :exclude 排他 nil; tに対応 :invalid わからない nil; nilに対応
では、:exclude
を考慮して、not
が現れたらt
とnil
を返却するような
subtypep!
を作ればいいのかと思うかもしれませんが、
それだと機能が足りません。
:exclude
を考慮して、:true
, :false
, :exclude
(と:invalid
)を返却するような
subtypep!
を作成するのです。
これらの値は、のちに作成するand
, or
でも使用します。
では、ひとまずand
, or
がない場合の
『not
ありsubtypep!
』のアルゴリズムを示します。
(subtypep! a b)
の場合 (★not
なしの場合)(subtypep! a b)
をそのまま返却
(subtypep! (not a) b)
の場合(subtypep! b a)
が (★逆です):true
なら:exclude
を返却:exclude
なら:false
を返却- その他ならそのまま返却
(subtypep! a (not b))
の場合(subtypep! a b)
が:true
なら:exclude
を返却:exclude
なら:true
を返却- その他ならそのまま返却
(subtypep! (not a) (not b))
の場合(subtypep! b a)
が (★逆です):true
なら:true
を返却:exclude
なら:false
を返却- その他ならそのまま返却
これでどうでしょうか。
eqlには3種類の意味がある
表題の通り、Common Lispでは下記の意味があります。
- 関数
eql
- 型
eql
eql-specializer
なんでこんなにあるんでしょうね。
兄弟分である、eq
、equal
、equalp
さんたちは1つの意味しかありませんよ?
それぞれ説明していきます。
関数eql
普通はこいつです。
2つのオブジェクトが等しいかどうかを調べるときに使います。
関数eq
より適当ではないものの、関数equal
みたいに
詳しく調査して欲しくないという位置づけだと思います。
厳密に定義されており、下記のどれかが当てはまっているならt
です。
eq
がt
の場合。- どちらも
number
であり、同じ型で同じ値の場合 - どちらも
character
であり、同じ文字の場合
1つめのeq
はそのままアドレスを比較します。
2つめは、同じ型が要求されているので、数値として等しいだけの場合はダメ。
* (eql 0.0 0) →nil * (eql 0.0 0.0) →t * (eql 0.0d0 0.0s0) →nil * (eql 0.0d0 0.0d0) →t
3つめはそのままです。
* (eql #\a #\a) →t * (eql #\a #\A) →nil
型eql
Type-specifier
と呼ばれるものであり、
型という中で、値がeql
かどうかを調査します。
例えば次の通り。
* (typep 10 '(eql 10)) →t * (typep (cons nil nil) `(eql ,(cons nil nil))) →nil
余談ですが、型を評価する際に、内部で別の形式に変形することが良くあります。
例えば
(or (integer 10 200) (integer 100 300))
を
(integer 10 300)
のように変えたりします。
型eql
の場合も
(eql 10)
を
(integer 10 10)
に変形することがあります。
だから何だってわけじゃないんですが。
eql-specializer
これはmethod
を定義するときに使うものです。
method
は、引数に値の特性を指定することができます。
例えば下記の通り。
(defmethod aaa ((a integer) (b string)) ...)
この場合は、第一引数a
にinteger
を、第二引数b
にstring
を指定しています。
注意して欲しいのは、このinteger
やstring
と記述しているのは、
「型」ではなく「class
およびeql-specializer
」です。
下記の例の引数は、型(eql 10)
ではなく、eql-specializer
の指定です。
(defmethod bbb ((a (eql 10))) ...)
何が違うんだとしか思えないでしょう。
integer
と書けて、(eql 10)
と書けるなら、型じゃないのか?
でも型じゃないんです。
型が指定できないので
(defmethod bbb ((a (satisfies oddp))) ...)
や
(defmethod bbb ((a (not (eql 10)))) ...)
はエラーです。
こいつもエラーなので注意。
(defmethod bbb ((a (real 10 20))) ...)
じゃあinteger
とstring
は型じゃないのか?
と思われるかもしれませんが、型じゃありません。
型のふりをしたclos
オブジェクトです。
clos
の世界とlisp
の世界をある程度似せるために、
symbol
だけで表記された標準の型については、
同じclass
が用意されている場合があります。
何が用意されているのかは処理系依存ですが、
find-class
関数を使うことで調査できます。
clisp
の例を示します。
(find-class 'integer nil) →#<BUILT-IN-CLASS INTEGER> (find-class 'atom nil) →nil
なお、eql-specializer
の引数は、
defmethod
宣言時に一度だけ評価されます。
method
が呼び出されるたびに評価されるわけではありません。
例を示します。
呼び出すたびに値が増えていく関数hello
を作成します。
(let ((value 0)) (defun hello () (incf value)))
generic-function
のaaa
を作成します。
(defgeneric aaa (arg))
関数hello
をeql-specializer
の引数として、method
を定義します。
;; ここで(hello)は1を返却 (defmethod aaa ((arg (eql (hello)))) :hello) →#<STANDARD-METHOD ((EQL 1))>
実行してみます。
;; eql-specializerが機能している (aaa 1) →:HELLO ;; 何度か実行する (aaa 1) →:HELLO (aaa 1) →:HELLO ;; 確認 (hello) →2 ;; エラー (aaa 2) →no-applicable-method
forward-referenced-classとは何か
そんなクラス知らねえと言われても仕方がないほどマイナーなものです。
こいつは、だいたいはmop関連のパッケージに隠れています。
ANSI Common Lispには制定されていませんが、
CLOSを構築する際に必要となるので標準みたいなものでしょう。
まだあんまり読めてないんですが、 The Art of the Metaobject Protocolっていう専門書に載ってないんですよね。 検索しても日本語の解説は出てこない。 Common Lisp自体が古いから仕方がないといえば仕方がない。 じゃあ自分が書くか。
処理系依存ですが、次のようにすると確認できます。
* (find-class 'sb-mop::forward-referenced-class) #<STANDARD-CLASS SB-MOP:FORWARD-REFERENCED-CLASS>
ccl
? (find-class 'ccl::forward-referenced-class) #<STANDARD-CLASS FORWARD-REFERENCED-CLASS>
> (find-class 'clos::forward-referenced-class) #<STANDARD-CLASS FORWARD-REFERENCED-CLASS>
これは一体何なのかというと、defclass
で先行して存在しないsuperclass
を指定したときに
仮決めとして指定されます。
クラスを先行して設定した例をもとに説明します。
(defclass aaa (bbb) ()) ;; ★まだbbbは存在しない (defclass bbb () ((hello :initform "zzz"))) (slot-value (make-instance 'aaa) 'hello)) →"zzz"
では内部でどうなっているのか見てみます。
モップを使いますので、sbclならsb-mop
をuse-package
してください。
* (use-package 'sb-mop)
なんか清掃用具みたいですね。
まずは綺麗な状態から、再びクラスaaa
を作成します。
* (defclass aaa (bbb) ()) ;; ★まだbbbは存在しない #<STANDARD-CLASS COMMON-LISP-USER::AAA>
この状態でsuperclassの内容を確認します。
* (class-direct-superclasses (find-class 'aaa)) (#<FORWARD-REFERENCED-CLASS COMMON-LISP-USER::BBB>)
存在しないはずのクラスbbb
は、forward-referenced-class
と出てきています。
この表記から、forward-referenced-class
はmetaclass
だということがわかります。
ではクラスbbb
を作成します。
* (defclass bbb () ((hello :initform "zzz"))) #<STANDARD-CLASS COMMON-LISP-USER::BBB>
superclassはどうなっているでしょうか。
ここは処理系によって分かれました。
sbclの例を示します。
* (class-direct-superclasses (find-class 'aaa)) (#<STANDARD-CLASS COMMON-LISP-USER::BBB>)
class-of
がstandard-class
に変わっています。
この時点で確定するってすごいですね。
どうやっているんだろう。
bbb
をfind-class
にreferenced-class
として登録していたのか、
あるいはclass-direct-superclasses
の実行契機で見直したのか。
cclも確定していました。
? (class-direct-superclasses (find-class 'aaa)) (#<STANDARD-CLASS BBB>)
clispはまだ保留中です。
> (class-direct-superclasses (find-class 'aaa)) (#<FORWARD-REFERENCED-CLASS BBB>)
いずれにせよ、finalizeした時点で確定します。
classをfinalizeさせます。
> (make-instance 'aaa) #<AAA #x000801C57458>
あらためてsuperclassを確認します。
> (class-direct-superclasses (find-class 'aaa)) (#<STANDARD-CLASS BBB>)
なるほど。
ということは、forward-referenced-class
を持っている場合は
当然class-precedence-list
は作成できない?
[1]> (defclass aaa (bbb) ()) ;; ★まだbbbは存在しない #<STANDARD-CLASS AAA :INCOMPLETE> [2]> (class-precedence-list (find-class 'aaa)) *** - The class #<STANDARD-CLASS AAA :INCOMPLETE> has not yet been finalized. The following restarts are available: ABORT :R1 Abort main loop Break 1 [3]>
finalizeしないとダメだそうです。
finalizeがされたかどうかの確認は次のようになります。
> (class-finalized-p (find-class 'aaa)) NIL
ちなみにclass-precedence-list
の実行結果は、
でした。
まあどれでもいいんじゃないでしょうか。
不完全なクラスはdefclass
やensure-class
では作成を途中で放棄し、
最大限まで仕事を遅らせて、make-instance
なんかでfinalizeする必要が生じたとき、
ようやく重い腰をあげることがわかりました。
めんどくさい!
Npt Lispの紹介
Nptとは小さいLisp処理系です。
現在開発中です。
本ブログは、NptというLispの開発をするにあたり、
難しかったり引っかかった部分をブログに記載して行くことで、
実装の確認をあらためてしていこうという目的がありました。
それなのにNptの説明が一切ないのもどうかと思うので、
せっかくなので簡単に説明します。
Nptは「C言語のみで作成できるANSI Common Lisp」を目指しています。
半分以上はできたのかなと思っています。
ちなみにCLtL2は対象外。
現時点でバージョンはv0.1.8
となっていますが、
もしANSI Common Lispの関数を全部実装できたら
不完全であっても強制的にv1.0.0
に格上げして、
後はひたすらテストとバグとりをして行くことを考えています。
開発方針
開発方針を示します。
短所となりえる特徴を示します
将来やってみたいこと
- Threadセーフにしたい
- sqlite3みたいにamalgamationをやってみたい
順に説明します。
C99でコンパイルできること
NptはもともとC言語に組み込んで使うことを考えていました。
いろんな環境で使えるようにC11ではなく、C99をベースに作成しています。
本当はC89にしようかと思ってたんですが、
ちょっときついなと思い、あきらめた経緯があります。
環境はFreeBSD、Linux (およびWindows、ANSI-C)
FreeBSDは単純に自分が使っているからであり、 Linuxはたぶんみなさんが使っているからだろうという理由です。 Gentoo Linuxで動作確認をしています。
Windowsの開発は、優先順位は落ちますが、
現時点ではコンパイルとテストコードの実施は確認しています。
開発環境はWindows 10 64bit、Visual Studio 2017を利用しています。
ANSI-C環境とは、C99標準ライブラリのみで利用できる制限付きANSI Common Lispのことです。
今は対象外としていますが、もしかしたら利用できるようにするかもしれません。
一応コンパイル時に-DLISP_ANSIとすることで利用できるのですが、
テストケースを対応させていないので、どこまで正しいかわかりません。
たぶん次のようにコンパイルすると、勝手にANSI-Cモードになります。
$ cc *.c -lm
確認してみます。
$ ./a.out (find-class 'standard-object) #<STANDARD-CLASS STANDARD-OBJECT> ^D $ ./a.out --version npt Version 0.1.8 ----- Memory size 64bit Fixnum size 64bit Lisp mode ANSI-C ★ここで確認できる Thread mode remove Version 0.1.8 Build information 2019/03/30-00:19:59 ----- Execute mode standalone Release mode release Degrade mode release -----
可能な限りANSI Common Lispの機能のみを実装する
使い方の一つとしてC言語の組み込みを目的としているため、 拡張機能はかなり制限すると思います。
とは言っても、nptは組み込み以外でも、 作者が自分自身で利用することを考えていますので、 不便にならないよう、何らかの方法で実装はしたいです。
もしかしたらforkして別の名前で開発するかもしれません。
利用に制限はない
ライセンスを緩く設定しています。
もし気に入ってくれたのなら、好きに使ってください。
機械語の翻訳をしない
コンパイル機能は実装しません。
開発方針で実装しないと決めているので、 仮に実装できたとしてもしません。
つまりcompiled-function
を作成する方法はないということです。
存在自体はしており、例えば関数car
なんかは
C言語で作成してあるので、compiled-function
です。
まだ実装してないのですが、関数compile
はinterpreted-function
を生成する予定です。
つまりeval
とほぼ同じです。
もし、IntelのCPU対応でコンパイル作りたい!なんて思い始めたら、 たぶんnptをforkして別プロダクトで作ると思います。
拡張機能は極力実装しない
GUI機能とかsocket通信機能とかは、たぶん実装しません。
これも同じですね。
作りたくなったら、forkします。
実行速度が遅い
現時点で最大の欠点です。
バグがあるとかだったら直せばいい話ですが、
こちらに関しては決定的な解決が思い浮かびません。
たぶん最適化を作っていくことである程度は改善できると思います。
利用方法によって、早い部分と遅い部分はあると思いますが、 単純にファイルを一文字ずつ読み込むプログラムを作ったところ、 sbclと比べてnptは10000倍時間がかかり、 clispと比べてnptは7倍時間がかかったという結果でした。
改善はしていきたいですが、遅い部分だと場合によっては使い物に ならないかもしれません。
開発が不安定
作者の生活が不安定なため、中断する可能性は十分あります。
Threadセーフにしたい
拡張機能の実装には消極的ですが、
自分としてはThread機能を実装したいという思いがあります。
もしかしたらThreadセーフだけでも採用するかもしれません。
sqlite3みたいにamalgamationをやってみたい
nptのソースファイルをまとめて数個のファイルにすることです。
例えば
みたいな2つのファイルにする配布形態も用意できたらいいなと思います。
現在の足りない部分
実装できていないANSI Common Lispの大きな機能は下記の通り。
- CLOSほぼ全部 ★現在開発中
structure
全部loop
マクロ全部- pretty printing全部
- 環境に関する関数
core
ファイルの読み書きfasl
ファイルの読み書き
大まかに列挙しましたが、細かい部分はその他にも穴が開いていたりします。
例えばadjust-array
の大半の機能だったり、関数isqrt
の存在だったり。
機能の欠落は「common_*.c」というファイルの最後を見ると 分かるかもしれません。
引数&keyの内容を指定しない場合を考える
例えば
(defmethod aaa (value &key) ...)
のように&key
だけを指定して、その後の変数を記載しない場合があります。
全くの無意味のように見えますが、下記2つの場合において意味があります。
- 続けて
&allow-other-keys
を指定した場合 defmethod
かdefgeneric
でエラー回避のため
それぞれ説明します。
続けて&allow-other-keys
を指定した場合
&allow-other-keys
の場合は簡単に説明できます。
例えば下記の通り。
(defun aaa (value &key &allow-other-keys) ...)
このようにすることで、関数aaa
の引数にkey-value
の形を要求させることができます。
関数aaa
を呼び出すことを考えましょう。
例えば、下記の場合は問題ないです。
(aaa 100 :aaa 200 'bbb 300)
しかし、key-value
の形になっていない場合はエラーです。
(aaa 100 :aaa 200 'bbb) →★エラー、valueが足りない (aaa 100 :aaa 200 300 400) →★エラーの可能性あり、keyは本来symbolじゃないとダメ
関数aaa
では、引数を指定した所でただ内容を切り捨てています。
そうではなく引数&key
に加えて、&rest
だったり
マクロなら&whole
や&body
と組み合わせるなら、
もっとちゃんとした使い方ができると思います。
(defun aaa (value &rest args &key &allow-other-keys) ...)
この例では、key-value
の形を要求していることに加えて、
その内容を&rest
にてargs
に格納しています。
defmethod
かdefgeneric
でエラー回避のため
ではdefmethod
かdefgeneric
の方はどうでしょうか。
defgeneric
とdefmethod
には、lambda-list
を設定するときに、
守らなければいけない条件がいくつかあります。
そのうちの一部を下記に示します。
defgeneric
に&rest
か&key
があるなら、defmethod
に&rest
か&key
が必要。defmethod
に&rest
か&key
があるなら、defgeneric
に&rest
か&key
が必要。
では下記の例を見てみましょう。
(defgeneric aaa (value &rest args))
defgeneric
の引数には&rest
が含まれているので、
defmethod
では&rest
か&key
が必要になります。
続けて次の例を見ていきます。
(defmethod aaa (value &key hello) ...)
上記のmethod
では&key
があるので問題なしです。
次はどうでしょうか。
(defmethod aaa (value) ...)
上記はエラーです。
defgeneric
に&rest
があるのに、method
には&rest
も&key
もありません。
では次の例はどうでしょうか。
(defmethod aaa (value &key) ...)
面白いことに、&key
があるから問題ないとみなされます。
(defmethod aaa (value) ;; ★エラー ...) (defmethod aaa (value &key) ;; ★問題なし ...)
上記2例は、受け取ることができる引数は全く同じであるにもかかわらず、
下の方だけが合法で、lambda-list
のエラーチェックを回避できるのです。
lambdaと記載できる場所はどこか
よく目にする(lambda ...)
という表記はかなり特殊なものであり、
マクロを抜きにするならば、限定された場所でしか記述できません。
マクロのlambda
とリードマクロの#'
があるため、
式の中で気軽に記載することができるのです。
マクロの例としてはこんな感じ。
(setq *call* (lambda () :hello))
このlambda
マクロは、次のように展開します。
(setq *call* #'(lambda () :hello))
さらにリードマクロ#'
より、次のように展開されます。
(余計なことを言うと、リードマクロの展開タイミングはここじゃないかもしれない)
(setq *call* (function (lambda () :hello)))
最終的にspecial operatorのfunction
の引数に展開されるわけです。
じゃあfunction
の引数のlambda
はマクロ展開されないのか?
されません。
function
の引数はquote
の引数と同じように特殊な場所であり、
評価対象ではないのでマクロ展開されないのです。
ではマクロのlambda
を考えない場合は、lambda
と記載できる場所はどこでしょうか?
答えは下記の通り。
function
の引数- 関数呼び出し時の関数名
function
の引数は上記の例で説明しました。
関数呼び出し時の関数名とは、普通に関数を呼び出すときのリストの第一要素の事です。
((lambda () :hello)) →:HELLO
これだとわかりづらいですよね。
((lambda (a b) (+ a a b b)) 10 20) →60
どうでしょうか。
次の実行とほぼ同じとなります。
(defun testplus (a b) (+ a a b b)) (testplus 10 20) →60
以上で、lambda
の記載できる場所の説明は終わりです。
function
と関数呼び出しの構文の違い
こっちが本題だったりしますが、
special operatorの(function name)
と、関数呼び出しの(name ...)
では、
どちらもsymbol
と(lambda ...)
を記載することができました。
両者は同じものを許容するのでしょうか?
実は違います。
(function name)
が許容するものは下記の通り。
symbol
(lambda ...)
(setf name)
それに対して、(name ...)
が許容するものは次の通り。
symbol
(lambda ...)
つまり関数呼び出しでは、(setf name)
を直接呼び出すことが許されていません。
これはちょっと意外でした。
【参考】http://www.lispworks.com/documentation/HyperSpec/Body/03_abab.htm
If the car of the compound form is not a symbol, then that car must be a lambda expression, in which case the compound form is a lambda form.
つまりは下記のコードは規約違反です。
(defun (setf aaa) (v c) (rplaca c v) v) (let ((c (cons 10 20))) ;; (setf (aaa c) 200) ((setf aaa) 200 c) ;; ★エラー (format t "~A~%" c))
面白いことに、clispではこのコードが全く問題なく動作します。
clispによる実行結果 (200 . 20)
setf
を直接呼び出したいならfuncall
/apply
を使いましょう。
(defun (setf aaa) (v c) (rplaca c v) v) (let ((c (cons 10 20))) ;; (setf (aaa c) 200) (funcall #'(setf aaa) 200 c) ;; ★問題なし (format t "~A~%" c))
実行結果
(200 . 20)
そんな馬鹿なことしてないでsetf
マクロ使えという声が聞こえてきそうですが、
私もそう思います。
なお、当たり前ですが、defsetf
かdefine-setf-expander
で定義されたものの場合は、
関数じゃないのでこの方法では呼び出しできません。
型valuesの使い方
実装していると難しくてイヤになるのが型(type
のこと)です。
型関係の実装で一番難しいのはsubtypep
で、ひどいもんです。
今回は難しくはないものの簡単でもない、型values
について説明します。
命令values
ではなく、型values
の方ですので注意。
型とは、普通だったらtypep
やsubtypep
なんかで使うものです。
例えば、オブジェクトが整数かどうかを確認するには次のようにします。
* (typep 100 'integer) T * (typep :hello 'integer) NIL
上記で記載した'integer
というのが「型」です。
今回話題とするのは、型values
だけです。
この型は他の型とはかなり違っており、そもそもtypep
では使えません。
* (typep 100 'values) →エラー * (typep 100 '(values integer)) →エラー
型values
が使えるのは、次の2点限定となります。
- 型
function
の第2引数 - special operatorの
the
型values
というのは、値の返却値を指定する型なので、
そんなものを指定したい場合が上記の2点しか存在しないのです。
その上記2点だけという制約により、(not (values ...))
や
(and (values ...) ...)
という書き方が許されないことになります。
実装するうえでnot
, and
, or
が許容されないのは本当にありがたいことで、
この辺りの苦労はそのうちsubtypep
と一緒に話したいと思っています。
たぶんsubtypep
の実装ってどこにも情報がないと思いますので。
それでは型values
の例を示します。
(declaim (ftype (function * (values integer)) aaa)) →関数aaaは、第一返却値がinteger (the (values integer string) (call 10 20 30)) →call関数は第一返却値がintegerで第二返却値がstring
型values
は&optional
と&rest
も使えます。
例えば次の通り。
(values integer &optional string fixnum) (values t &rest integer)
型values
にはとても分かりづらいことが1つあります。
それはデフォルトでは&rest t
が指定されるということです。
(values integer)
と記載した場合、
(values integer &rest t)
と同じ意味になります。
さらに型function
でもthe
でも、values
と記載しなかった場合は、
自動的に(values xxx)
という意味になります。
つまり、次の3つの表記はすべて同一となります。
(the integer ...)
(the (values integer) ...)
(the (values integer &rest t) ...)
それでは&rest t
とはどういう意味でしょうか。
&rest t
はそれ以降の型が全て型t
であるという意味になります。
つまり、
(values integer)
は
(values integer t t t t t t .....)
という意味になるのです。
もし、ある関数の返却値がinteger
たった1つであるとわかっている場合、
(values integer)
と記載するのではなく、
(values integer &rest nil)
と記載するのが正しいことになります。
よくマクロに関係する関数なんかでは
(defun aaa () (values))
みたいに、返却値が全くないことを指定することがあります。
上記の関数の命令(values)
は正しいのですが、
もし返却値の型をvalues
で表現したい場合、
(values)
ではなく
(values &rest nil)
となります。
型(values)
だと何でも許容する型という意味になるので、
全く逆の意味として認識されることになるのです。
とはいっても、&rest t
が指定されていて困ることはあまりないでしょう。
厳密にoptimizerを利用したい場合は、
&rest nil
を指定しないとうまく行かないかもしれません。
最後に、型values
の&allow-other-keys
について話します。
ANSI Common Lispの規格書には、
型values
には&allow-other-keys
が指定できると記載されています。
http://clhs.lisp.se/Body/t_values.htm
Type Specifier VALUES values value-typespec value-typespec::= typespec* [&optional typespec*] [&rest typespec] [&allow-other-keys]
つまりこんな感じ
(values integer &allow-other-keys)
しかし型values
の引数に&key
の指定は許されて無いので、&allow-other-keys
は間違いじゃないかと言われています。
https://www.cliki.net/Issue%20VALUES-%26ALLOW-OTHER-KEYS
Issue VALUES-&ALLOW-OTHER-KEYS Problem Description: (values &allow-other-keys) matches the syntax for the VALUES type specifier, but the description doesn't say what it means. Because the syntax does not allow &key, &allow-other-keys was probably a mistake.
たぶんそのとおりであり、間違いなのでしょう。
引数&allow-other-keys
は使わないでおいたほうがいいと思います。