nptclのブログ

Common Lisp処理系nptの開発メモです。https://github.com/nptcl/npt

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の実装はとても難しいです。
理由は、実数の範囲指定、andornotがあるからです。
範囲指定とは例えば次のようなことを言います。

(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から説明していきます。

subtypepnot

notとは、その名の通り反対を意味します。
subtypepnotが含まれる場合はどうなるかわかるでしょうか?

例えば下記の例を考えます。

(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ですが、分けて考える必要があります。
上のstringrealは、重複することが全くない、完全なる排他です。
しかし下のrealintegerは、重複する部分があるものです。

両者が完全に排他の場合、右を否定した場合のみtとなります。
これは図にするとわかりやすいのではないでしょうか。

例えば、例として含まれる範囲を|****|で表すとしましょう。

(subtypep 'string 'real)
  string:   ------|****|----------------
  real  :   -----------------|****|-----
                  ★互いに疎で含まれない

分かってもらえるでしょうか。
横軸はてきとうです。
こんな感じだとして、|****|の部分がお互いに含まれないと表現します。
realnotを取ると次のようになります。

(subtypep 'string '(not real))
  string    :   ------|****|----------------
  (not real):   *****************|----|*****
                    ★|ここ|★が含まれる

こうすると、string|****|の部分が、(not real)に 完全に含まれています。

一方、realintegerは次のようになります。

(subtypep 'real 'integer)
  real   :   ------|*****************|---
  integer:   -----------------|****|-----
                  ★realの方が大きいのでnil

realintegerは互いに疎ではなく、重複する部分があります。
よってnotを取ると次のようになります。

(subtypep 'real '(not integer))
  real         : ------|*****************|---
  (not integer): *****************|----|*****

(not integer)realを包括できていません。

こんなふうに、subtypepでは、nilを返却する場合は、 排他的なのか、そうじゃないのかをちゃんと調べないと、 notの場合に対応することができません。

subtypepnotの有無を網羅すると、パターンは次の4通りになります。

(subtypep 'a       'b)
(subtypep '(not a) 'b)
(subtypep 'a       '(not b))
(subtypep '(not a) '(not b))

このうちで、もともとnilだったものがtになるパターンは次の2通りです。

  • (subtypep 'a '(not b))
    • abが排他的の場合はt
  • (subtypep '(not a) '(not b))
    • abを含む場合はt

下の場合の、2つがどちらもnotの判定は楽です。
abを逆にした(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が現れたらtnilを返却するような 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

なんでこんなにあるんでしょうね。
兄弟分である、eqequalequalpさんたちは1つの意味しかありませんよ?

それぞれ説明していきます。

関数eql

普通はこいつです。
2つのオブジェクトが等しいかどうかを調べるときに使います。

関数eqより適当ではないものの、関数equalみたいに 詳しく調査して欲しくないという位置づけだと思います。

厳密に定義されており、下記のどれかが当てはまっているならtです。

  1. eqtの場合。
  2. どちらもnumberであり、同じ型で同じ値の場合
  3. どちらも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))
  ...)

この場合は、第一引数aintegerを、第二引数bstringを指定しています。
注意して欲しいのは、このintegerstringと記述しているのは、 「型」ではなく「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)))
  ...)

じゃあintegerstringは型じゃないのか? と思われるかもしれませんが、型じゃありません。
型のふりをした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-functionaaaを作成します。

(defgeneric aaa (arg))

関数helloeql-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自体が古いから仕方がないといえば仕方がない。 じゃあ自分が書くか。

処理系依存ですが、次のようにすると確認できます。

sbcl

* (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>

clisp

> (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-mopuse-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-classmetaclassだということがわかります。

ではクラス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-ofstandard-classに変わっています。
この時点で確定するってすごいですね。
どうやっているんだろう。
bbbfind-classreferenced-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の実行結果は、

  • clisp -> simple-error
  • sbcl -> unbound-slot
  • ccl -> nil

でした。
まあどれでもいいんじゃないでしょうか。

不完全なクラスはdefclassensure-classでは作成を途中で放棄し、 最大限まで仕事を遅らせて、make-instanceなんかでfinalizeする必要が生じたとき、 ようやく重い腰をあげることがわかりました。

めんどくさい!

Npt Lispの紹介

Nptとは小さいLisp処理系です。
現在開発中です。

https://github.com/nptcl/npt

本ブログは、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にしようかと思ってたんですが、 ちょっときついなと思い、あきらめた経緯があります。

環境はFreeBSDLinux (およびWindowsANSI-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です。

まだ実装してないのですが、関数compileinterpreted-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を指定した場合
  • defmethoddefgenericでエラー回避のため

それぞれ説明します。

続けて&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に格納しています。

defmethoddefgenericでエラー回避のため

ではdefmethoddefgenericの方はどうでしょうか。

defgenericdefmethodには、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マクロ使えという声が聞こえてきそうですが、 私もそう思います。

なお、当たり前ですが、defsetfdefine-setf-expanderで定義されたものの場合は、 関数じゃないのでこの方法では呼び出しできません。

型valuesの使い方

実装していると難しくてイヤになるのが型(typeのこと)です。
型関係の実装で一番難しいのはsubtypepで、ひどいもんです。

今回は難しくはないものの簡単でもない、型valuesについて説明します。
命令valuesではなく、型valuesの方ですので注意。

型とは、普通だったらtypepsubtypepなんかで使うものです。
例えば、オブジェクトが整数かどうかを確認するには次のようにします。

* (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は使わないでおいたほうがいいと思います。