クラスをsubtypepしたときの問題点
クラスはsubtypep
で継承関係を調査できます。
例えば次の通り。
(subtypep 'standard-class 'class) -> t; t
つまり、standard-class
はclass
を継承しているということです。
次の例はどうでしょうか。
(defclass aaa () ()) (defclass bbb (aaa) ()) (subtypep 'bbb 'aaa) -> t; t (subtypep 'aaa 'bbb) -> nil; t
何ら問題なく理解できると思います。
問題は否定したときです。
以前subtypep実装の基本 - nptclのブログ では、
右側が否定の場合は、比較する2つの型が完全に排他かどうかをチェックする必要があると書きました。
例として挙げた、
(defclass aaa () ()) (defclass bbb (aaa) ())
は、継承関係にあるため完全に排他とは言えません。
つまり、右を否定した場合は必ずnil; t
が返却されるはずです。
では実際にはどうなるでしょうか。
(subtypep 'aaa '(not bbb)) -> nil; t ;; sbcl, ccl -> nil; nil ;; clisp (subtypep 'bbb '(not aaa)) -> nil; t ;; sbcl, ccl -> nil; nil ;; clisp
さて、これはどういうことなのでしょうか。
sbcl
とccl
は予想通りでしたが、clisp
は第二返却値がnil
のため、
つまりは分からないと言って判定を放棄したことになります。
排他的な場合はどうなるでしょうか。
テスト用に次のクラスを用意します。
(defclass ccc () ()) (defclass ddd () ())
両者は一見すると排他的に見えます。
subtypep
の結果を次に記載します。
(subtypep 'ccc '(not ddd)) -> nil; nil ;; sbcl, clisp -> t; t ;; ccl
大きく分かれました。
これから上記の結果について私の見解を書いて行くわけですが、
実の所これらの結果はかなりわかりづらいですし、
ちゃんとした説明はできないかもしれません。
まずは次の結果から。
(subtypep 'ccc '(not ddd)) -> t; t ;; ccl
クラスccc
とクラスddd
は継承関係ではないため、
互いに排他として見るならば、返却値t
は一見して正しいように見えます。
しかし実はクラスccc
もクラスddd
もどちらも同じt
というクラスを含んでいるため、
互いに排他ではないのです。
(class-precedence-list (find-class 'ccc)) -> (#<STANDARD-CLASS CCC> #<STANDARD-CLASS STANDARD-OBJECT> #<BUILT-IN-CLASS T>) (class-precedence-list (find-class 'ddd)) -> (#<STANDARD-CLASS DDD> #<STANDARD-CLASS STANDARD-OBJECT> #<BUILT-IN-CLASS T>)
完全に排他ではないということならば、subtypep
の右がnot
の場合は
t
ではなくnil
が正解ということになります。
しかしsbcl
とclisp
では、nil; t
ではなくnil; nil
になっています。
どういうことなのでしょうか?
この返却は規約に矛盾が生じているため、
判定不可能とするのが正しいという妥協によるものだと思います。
もしnil; t
を許容してしまうならば、
built-in-class
はどうなんだと突っ込まれてしまいます。
例えば下記の式、
(subtypep 'integer '(not cons)) -> t; t
は、integer
とcons
は互いに排他のため、当然t
が返却されます。
しかしどちらの型もCLOSオブジェクトの写像を持っており、
両方ともt
クラスを含んでいます。
(class-precedence-list (find-class 'integer)) -> (#<BUILT-IN-CLASS INTEGER> #<BUILT-IN-CLASS RATIONAL> #<BUILT-IN-CLASS REAL> #<BUILT-IN-CLASS NUMBER> #<BUILT-IN-CLASS T>) (class-precedence-list (find-class 'cons)) -> (#<BUILT-IN-CLASS CONS> #<BUILT-IN-CLASS LIST> #<BUILT-IN-CLASS SEQUENCE> #<BUILT-IN-CLASS T>)
型として判定した場合はt
であり、クラスとして判定した場合はnil
になります。
もうどうしようもないですよね。
矛盾が生じた以上は未定義です。
sbcl
とclisp
はnil; nil
として、申し訳ないけどわからないと返却し、
ccl
はclass
の場合だけでもt
クラスを無視しようという結果になります。
さて、では次の場合はどうなるでしょうか。
(subtypep 'bbb '(not aaa)) -> nil; t ;; sbcl, ccl -> nil; nil ;; clisp
sbcl
とccl
はnil; t
であっています。
しかしclisp
はnil; nil
としています。
私の個人的な意見としてはsbcl
とccl
が正しいとは思います。
しかしすでにclass
の判定に妥協が生じている以上は、
nil; nil
としてしまってもいいのではないでしょうか。
以上です。
結論を書くならば、
subtypep
でclass
をチェックするときには、not
を使うべきではない。
となります。