CLOSとExpression Problem
,,,,,,,,,,,,,,,,,,,,,,,,,,,, ,,--―'''""`ヽ'  ̄`ヽ、 / ヾ / ~`ヽ / ヽ;: /"""ヾ ヽ / ;:;;:::'''' l /;:;;:::''' \ i / /;:;;:::''' ヽ ヽ | | ヽ | / ;/ ヽ ヽ / ;:;:ヽ ,,,,;;::'''''ヽ | i / ,,,,;;::::::::::::::: __ ヽ ヽ | | " __ :::: '"ゞ'-' | | | |. - '"-ゞ'-' ::::::.. |. | | ;:| ::::::: | :| | ヽ. ( ,-、 ,:‐、 | | | /ヾ.. | | | | __,-'ニニニヽ . | | .. | `、ヽ ヾニ二ン" / | | ヽ\ / | | l `ー-::、_ ,,..'|ヽ./ ヽ. :人 `ー――''''' / ヽ /;:;:;:;;:;:;: _/ `ー-、 ,.-'" \ー-、 ,.-'" \: \ .,.-''" | /. \ ~>、,.-''" | ,,..-‐'''"" ヾ ,.-''"| /――――、/ オブジェクト指向は操作を追加するのが 関数型言語は型コンストラクタを変更するのが (後からだと)難しい
らしいんですよ.そんでこれをExpression Problemというらしい.最近知りました.
http://d.hatena.ne.jp/osiire/20090516
これ,CLOSみたいなオブジェクトシステムの場合はどうなるんやろうなーと思いました.あっ,LISPは静的型言語じゃないだろとか言わないで,石を投げないで.
CLOS
クラスは一般に,メンバーの一覧とスーパークラスの一覧の定義からなります.メンバーにはフィールドとメソッドがあります.Common LISPでは,フィールドはスロットと呼ばれます.構造体のスロットからの流用でしょう.
クラスを定義してみます.
;;; <sample>クラスの定義 (defclass <sample> () ( ;; スロット一覧 (slot-a :initarg :slot-a) (slot-b :initarg :slot-b) ) ) ;;; <sample>のサブクラスを定義 (defclass <sample-sub> (<sample>) ;;スーパークラスの一覧 ( ;; 新たなスロット (slot-c :initarg :slot-c) ) )
インスタンスを生成するには,make-instanceを使います.スロット定義に :initarg をつけておくことで,生成時にスロットを初期化することができます.
;; <sample>クラスをインスタンス化 (make-instance '<sample>) ;; <sample-sub>クラスをインスタンス化 (make-instance '<sample-sub>) ;; スロットに初期値を与えて初期化 (make-instance '<sample-sub> :slot-a 1 :slot-b 2 :slot-c 3)
スロットにアクセスするためのもっとも基本的な方法は, slot-value を使うことです.
(slot-value (make-instance '<sample-sub> :slot-a 1 :slot-b 2 :slot-c 3) 'slot-a) ;=> 1
メソッドがありませんね.CLOSの際立った特徴は,メソッドがクラスに属さないことです.
ほとんどのオブジェクト指向言語において,オブジェクトはメッセージを受け取り,それに応えるという形で処理を進めます.ですから,オブジェクトがどのようなメッセージに反応できるのか,それはクラスに属するというのは自然な考え方です.
ところが,CLOSにおいて,クラスはデータ構造を表すだけで,どのようなメッセージに応えるかは定義しません.CLOSは,メッセージパッシングによるオブジェクト指向というモデルを採用していないからです.
ではCLOSにおけるメソッドとはなにかといえば,それは単なる関数です.単なる関数なので,高階関数にも渡すことができます.ただし,この関数は型によって動作を変えます.ポリモーフィズムはこれで表現可能です.
それでは継承が表現できないじゃないか,スーパークラスのメソッドを呼び出したいときにはどうするんだ,という話については,実際のコードを見てみましょう.
;; 総称関数の定義 (defgeneric sample-method (obj) (:documentation "自分のクラスを印字する") ) ;; <sample>を受け取ったときの実装 (defmethod sample-method ((obj <sample>)) (print "this is a sample") NIl) ; "this is a sample" ; => NIL ;; <sample-sub>を受け取ったときの実装 (defmethod sample-method ((obj <sample-sub>)) (print "this is a sample-sub") ;; スーパークラスのメソッドを呼び出す (call-next-method) ) ; "this is a sample-sub" ; "this is a sample" ; => NIL
メソッド定義の中で call-next-method を呼び出すことで,スーパークラスのメソッドを呼び出すことができます.ここでいう next とは,そのオブジェクトの型の階層を見た時,次の型,1つ上のクラスを指しているわけです.これで,オブジェクト指向で表現できることは表現できることになります.
なお,CLOSが多重継承を許すことや,2つ以上の引数の型に応じて呼び出されるメソッドが決まることなどから,next-method が何であるかは微妙な問題になるのですが,ここでは省略します.
さらに,メソッドが呼び出される前,呼び出された後に呼ばれる処理の指定といったわけのわからない独特なシステムが続くのですが,これも割愛します.
操作と型コンストラクタを後から追加する
cardクラスがあったとします.そんでこれのサブクラスとしてnum-card, jack, queen, kingがあり,それに対するメソッド,num, plusがあります.numはカードの数を返すメソッド.plusはカードの数を足し合わせるメソッドです.
(defpackage :card (:use :cl) (:export :<card> :<num-card> :<jack> :<queen> :<king> :num :plus )) (in-package :card) ;; 抽象クラスとして使われる<card> (defclass <card> () ()) ;; カードの種類を表すサブクラスを定義 ;; 1から10までのカード.値を意味する value スロットを持つ.ついでにアクセサを定義しておく. (defclass <num-card> (<card>) ((value :initarg :value :reader value-of))) ;; 絵札.スロットを持たない. (defclass <jack> (<card>) ()) (defclass <queen> (<card>) ()) (defclass <king> (<card>) ()) ;; カードの数字を得るためのメソッド ;; 総称関数を定義して (defgeneric num (card)) ;; 型によって場合分けを追加していく (defmethod num ((card <num-card>)) (value-of card)) (defmethod num ((card <jack>)) 11) (defmethod num ((card <queen>)) 12) (defmethod num ((card <king>)) 13) ;; カードの数字による足し算 (defgeneric plus (card1 card2) ) (defmethod plus ((card1 <card>) (card2 <card>)) (+ (num card1) (num card2)))
別のパッケージに,このカードにjokerを足したものを作ります.カードの定義のほとんどは元のものを流用しますが,cardのサブクラスとしてjokerを追加し,さらにcardクラスに対して使える新たなメソッドも定義してみます.
(defpackage :card/joker (:use :cl :card) (:export :<card> :<num-card> :<jack> :<queen> :<king> :<joker> ; 新たなサブ型が追加されます :num :plus :score ; 新たなメソッドが追加されます ) ) (in-package :card/joker) ;; cardを継承してjokerクラスを作れる (defclass <joker> (<card>) ()) ;; 総称関数の新たな場合を追加する (defmethod num ((card <joker>)) 0) ;; 新たな操作も追加してみる (defgeneric score (card) ) (defmethod score ((card <num-card>)) (if (= (num card) 1) 11 (num card))) (defmethod score ((card <jack>)) 10 ) (defmethod score ((card <queen>)) 10 ) (defmethod score ((card <king>)) 10 ) (defmethod score ((card <joker>)) 10 )
これが別の言語,たとえばJavaでは,メソッドを追加することは難しいことになります.メソッドがクラスに属すので,cardクラスに対して呼べる新たなメソッドを追加するためには,cardクラスを再定義する必要があるわけです.またOCamlなどでは,cardを再定義せずとも関数を追加することで操作を追加することができるのですが,サブクラスを定義するのが難しい.card型の新たなコンストラクタを作れないわけです.
しかし,CLOSなら簡単に後からメンバーと操作が追加できてしまいます.やったね.
わからないこと
CLOSがExpression Problemを解決しているのか,これが分からない.
型の継承関係と,総称関数としてのメソッドがあれば,静的型言語でも同じ事ができるんちゃうの,てかOCamlの多相バリアントってそういうもんちゃうの,と思うのですが,よくわかりません.