nptclのブログ

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

クラスをsubtypepしたときの問題点

クラスはsubtypepで継承関係を調査できます。
例えば次の通り。

(subtypep 'standard-class 'class)
  -> t; t

つまり、standard-classclassを継承しているということです。
次の例はどうでしょうか。

(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

さて、これはどういうことなのでしょうか。
sbclcclは予想通りでしたが、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が正解ということになります。
しかしsbclclispでは、nil; tではなくnil; nilになっています。
どういうことなのでしょうか?

この返却は規約に矛盾が生じているため、 判定不可能とするのが正しいという妥協によるものだと思います。
もしnil; tを許容してしまうならば、 built-in-classはどうなんだと突っ込まれてしまいます。

例えば下記の式、

(subtypep 'integer '(not cons))
  -> t; t

は、integerconsは互いに排他のため、当然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になります。

もうどうしようもないですよね。
矛盾が生じた以上は未定義です。
sbclclispnil; nilとして、申し訳ないけどわからないと返却し、 cclclassの場合だけでもtクラスを無視しようという結果になります。

さて、では次の場合はどうなるでしょうか。

(subtypep 'bbb '(not aaa))
  -> nil; t       ;; sbcl, ccl
  -> nil; nil     ;; clisp

sbclcclnil; tであっています。
しかしclispnil; nilとしています。
私の個人的な意見としてはsbclcclが正しいとは思います。
しかしすでにclassの判定に妥協が生じている以上は、 nil; nilとしてしまってもいいのではないでしょうか。

以上です。
結論を書くならば、 subtypepclassをチェックするときには、notを使うべきではない。 となります。