2.  集合と同一性

同一性とは x = x と言えるということです.どんな論理の教科書でも,どんな集合の教科書でも,まず x = x の確認から入ります.数理哲学の本ではこれだけで,延々と1章を充てたりしますが,ここではCommon Lisp をベースとして,集合概念のCLOS実現に向けての議論をします.

セマンティックウェブの世界でもこの議論はあります.論理における記号(シンボル,a とか x )に相当するものは,セマンティックウェブの世界ではURI あるいは IRI です.ですから以後の議論で,シンボル a とか x が出てきたとき,それをセマンティックウェブの世界では URI あるいは IRI のことだと思ってください.ある記号(シンボル)を考えた時,同じ記号が違うものであるということはありません.この言い方だと何をいっているのかわかりませんから,同じ記号が違う意味であるということはありません,と言いかえると,もっともらしく聞こえますが,意味ってなに,意味論では意味を確立したいのに,いきなり意味っていったらどうどうめぐりになりますから,表示的意味論(denotational semantics)ではこれを同じ記号が異なる指示物を指示(denote)することはない,と言います.これでもやはりその指示物ってなに,という話になりますが,意味というよりは少しだけ安心できて,分かった気になれる.議論は指示するということの中身に移りますから,それはあとの議論にしておいて,x が異なるものを指示することはない,ということだけ了解できれば,議論を先に進めることができます.(本当は,時間と場所で指示物がかわるとか,実際にはいろんな問題がLODの話だけでも出てくるのだけれど,今は触れません.)

でも,異なる記号 xy が同じものを指示する,ということはあり得て,セマンティックウェブの世界ではそれはありになっています.そうすると考えなくてはいけないいろんな問題が出てきて,この辺はまだ十分には開拓されていないところになります.論理の世界,集合論の世界,プログラミングの世界では,異なる記号が同じものを指示するということはありません.これを unique name assumption (UNA) 唯一名仮説 と言います.

さて,Lisp の世界ではインターンされたシンボルはシステム中で唯一であることが保証されていますから,以後単にシンボルと言った場合には,インターンされたLispのシンボルのことを言うものとします.くどいようですが,それはセマンティックウェブの世界では URI もしくは IRI になります.

そこで同一性の議論に戻りますが,x = x はLisp ではシンボルについては (eq 'x 'x),(eql 'x 'x),(equal 'x 'x),(equalp 'x 'x) と 4 種類の述語が使えます.ところが一般のLisp オブジェクトになると,この真偽が述語によっていろいろに分かれます.今集合を set という名前のクラスのインスタンスと定義します.そうすると集合としての等しいという意味を Lisp 上で実現しなくてはなりません.

cg-user(1): (defpackage :zf
              (:shadow #:set)
              (:export #:set))
#<The zf package>
cg-user(2): (in-package :zf)
#<The zf package>
zf(3): (defclass set () ((elements :initarg :elements)))
#<standard-class set>
zf(4): (defparameter foo (make-instance 'set :elements `(0 1 2)))
foo
zf(5): foo
#<set @ #x20fe5612>
zf(6): (slot-value foo 'elements)
(0 1 2)

上記例では,まず始めに package 宣言をしました.これはAllegro ではすでに set という名前が common-lisp パッケージで使われていてエラーとなるため,独自の名前空間を設けてそこで定義しようというわけです.defpackage 中の:shadow で他のパッケージからの移入をシャドウィングしておいて,この名前空間からの set を:export で移出しています.use-package (:use) がありませんが,無いときは(:use common-lisp) がデフォールトです.注意が必要なことは (:use some-package) とすると,common-lisp の名前空間は導入されません.ですから普通は (:use common-lisp some-package) とします.:shadow や :export で指定するものは文字列でもシンボルでもいいのですが(シンボルの場合用いられるものはその名前だけ),Allegro の modern lisp のように大文字小文字を区別するシステムでは,もし文字列を用いた場合にはAllegro modern lisp では何の問題もないのですが,他のシステムに移植しようとすると,通常のANSI Common Lisp では小文字を含むシンボルはすべて zf:|foo| みたいに書かないといけなくなって,移植性が悪くなります.defpackage 中の記述をシンボルにしておけば modern ならその通りに,そうでなければ通常のシンボルと同様に,小文字も大文字に変換されますから,移植性の問題は起こりにくくなります.

CLOS のクラス定義は defclass で行います.ここで set はクラス名です.CLOS では名前なしのクラスも実は作ることはできますが,普通はすべてのクラスは名前付きです.名前の次には direct-super-classes をリストで定義しますが,空リストの場合にはデフォールトとして cl:standard-class が用いられます.その次にスロット定義リストがきますが,構造体のときとは違ってスロット定義を纏めてリストとして与えなければなりませんから,構造体よりも括弧が一つ多くなりますね.今ここでは elements という名前のスロットを定義しました.スロット無しの場合にはただの空リストを与えなければなりません.elements の次の :initarg キーワードで初期化のときのキーワードを指定しています.こんな風に (make-instance 'set :elements ...) というように使います.make-instance は与えたクラス名の実現体を一つ作ります.通常実現体には名前はありません.名前を付けたいときは自前で name スロットを定義することになります.スロット値を得たり,セットしたりするには slot-value を用います.

では次の結果はどうでしょうか

zf(8): (defparameter bar (make-instance 'set :elements `(0 1 2)))
bar
zf(9): bar
#<set @ #x210018f2>
zf(10): (eq foo bar)
nil
zf(11): (eql foo bar)
nil
zf(12): (equal foo bar)
nil
zf(13): (equal foo bar)
nil

今先に定義した集合 foo と全く同じ定義の集合 bar を定義しました.集合演算の立場からいうと,二つの集合には何の違いもありません.集合としては,foo に含まれる要素がすべて bar にもあり, bar に含まれる要素がすべて foo にもあれば,両者は等価です.ですから,この両者を等しいとする演算が必要です.私がJava が唯一優れていると思う点はここです.オブジェクト指向言語であるJava ではクラス定義をするとほとんど必ずその等価性を定義する関数を定義します.ここでも同様に集合の等価性を定義するメソッド equals を定義します.

(defmethod equals ((s1 set) (s2 set))
  (and (every #'(lambda (e1) (member e1 (slot-value s2 'elements)))
(slot-value s1 'elements))
       (every #'(lambda (e2) (member e2 (slot-value s1 'elements)))
(slot-value s2 'elements))))

メソッド定義では,引数パラメータの位置に,引数パラメータとその引数特定子(典型的にはクラス名)を括弧でくくって書きます.そうすると引数特定子がクラスの場合には,そのクラスに所属する引数が与えられたときに,該当メソッドが実行されます.今二つの引数が集合であればこのメソッドが実行されて,めでたく集合 foo と集合 bar は等しいと判断されます.

zf(15): (equals foo bar)
t

でもちょっと待ってください.今 every の中の member で要素かどうかを調べていますが,この例では数字が要素でした.そして member のテストキーワードオプションが与えられない場合には,判定には eql が使われます.もしここに集合が要素に来たら,この判定はうまくいきません.そこでこれをこのように変更します.

zf(17): (defmethod equals ((s1 set) (s2 set))
  (and (every #'(lambda (e1) (member e1 (slot-value s2 'elements) :test #'equals))
              (slot-value s1 'elements))
       (every #'(lambda (e2) (member e2 (slot-value s1 'elements) :test #'equals))
              (slot-value s2 'elements))))
#<standard-method equals (set set)>
zf(18): (defmethod equals (s1 s2)
         (eql s1 s2))
#<standard-method equals (t t)>a

メソッド定義における引数特定子がない場合にはすべてのLispオブジェクトの型階層の最上位である t が与えられます.通常のオブジェクト指向プログラムと同様に,クラス(型)階層の上下関係に従ったメソッドの継承とメソッドの隠ぺいがありますから,この入力コマンド行18行目の定義により,どんな引数の組み合わせにおいても,最後のよりどころになるのは,この定義になり,eql にて判定されるということになります.

集合論において,この集合の同一性は公理の内で最初に導入される,外延性公理と呼ばれます.