CLOSでオブジェクト指向するメモ1


動機

最近CommonLispを学び始めたので簡単にアウトプットしながら学習を進めていこうと思いメモ程度にまとめました.

第1弾はCLOSと他の(僕の中では)一般的なオブジェクト指向の考え方の違いについてざっくりと書こうと思います.

間違いがあるかもしれませんので,お気づきになられた方はコメントなどでマサカリ飛ばしてください...

参考文献

CLOSとは

Common Lisp Object Systemの略.
Common-LispやEmacs-Lispなどで標準搭載されているオブジェクトシステムのこと.

CLOSはJavaなどとは異なる方法でオブジェクトシステムを実現している.
CLOSのオブジェクトシステムはThe Common Lisp Object System MetaObject Protocol(CLOS MOP)と呼ばれる.

CLOSの特徴

  • 強い動的型付けを持つ
  • 多重ディスパッチ(総称関数)
  • 弱いカプセル化
  • 多重継承
  • 実行メソッド形成
  • ...etc

それぞれについて以下で実例を上げながら解説.

強い動的型付け

「動的型付け」とは(Wikipediaより)

動的型付け(どうてきかたづけ、英: dynamic typing)とは、プログラミング言語で書かれたプログラムにおいて、変数や、サブルーチンの引数や返り値などの値について、その型を、コンパイル時などそのプログラムの実行よりも前にあらかじめ決めるということをせず、実行時の実際の値による、という型システムの性質のことである。

ざっくり言うと,プログラムを実行するときに変数に入っている値によって,その変数の型が決まるということ.

「強い型付け」とは
  • ある関数などが間違った型の引数を受け取った時に,エラーを吐くような型付け
  • 逆に「弱い型付け」は暗黙的にキャストされるような型付け

具体的には以下の様な状況(数字の1と文字列「sample」を足し合わせる状況)など.

強い型付けの例(CommonLisp)
(+ 1 "sample") ;整数型と文字列型の加算
=>実行結果
エラーが起きてプログラムが停止する(実行不可)
弱い型付けの例(JavaScript)
var x = 1; //整数型
var y = "sample"; //文字列型
var z = x + y; //整数型と文字列型の加算
document.write("z= "+z); //答えの出力
=>実行結果
z= 1sample
(数値が文字列に暗黙的にキャストされて処理される)

CommonLispは強い動的型付けなので,実行前に型を明示的に決定する必要がないが,型が間違ってれば,無理矢理実行しようとはせずにエラーを返すのでプログラムの安全性を高めることができる.

多重ディスパッチ

多重ディスパッチとは,引数のデータ型によってメソッドを用意できる多態性の実現方法(だと僕は理解している).

Javaなどの単一ディスパッチの場合,メソッドの多態性を実現するために,指定したオブジェクト(クラス)だけが所有するメソッドを呼び出す,以下の様なコードになる.

単一ディスパッチによる多態性(Java)
//クラス1定義
public class Sample1 {
/*
* なんかいろいろ
*/
 public void echo(){
  System.out.println("Aのクラスです");
 }
}

//クラス2定義
public class Sample2 {
/*
* なんかいろいろ
*/
 public void echo(){
  System.out.println("Bのクラスです");
 }
}

//インスタンス化
Sample1 sam1 = new Sample1();
Sample2 sam2 = new Sample2();

;;メソッド実行
sam1.echo(); //Aのクラスです
sam2.echo(); //Bのクラスです

それぞれの「クラスにメソッドが属している」状態が単一ディスパッチ(で合ってるのか?)
逆に多重ディスパッチをサポートする言語の場合は以下の様な実装になる.

多重ディスパッチによる多態性(CommonLisp)
;;クラス定義
(defclass sample_class1 () ())
(defclass sample_class2 () ())
(defclass sample_class3 () ())

;;インスタンス生成&大域変数に保持
(defparameter *class1* (make-instance 'sample_class1))
(defparameter *class2* (make-instance 'sample_class2))
(defparameter *class3* (make-instance 'sample_class3))

;;総称関数定義
(defgeneric set-val (class1 class2))

;;メソッド定義
(defmethod set-val ((sam1 sample_class1) (sam2 sample_class2))
  (format t "1と2のクラスを引数にとっています"))
(defmethod set-val ((sam1 sample_class1) (sam3 sample_class3))
  (format t "1と3のクラスを引数にとっています"))
(defmethod set-val ((sam2 sample_class2) (sam3 sample_class3))
  (format t "2と3のクラスを引数にとっています"))

;;メソッド実行
(set-val *class1* *class2*) ;=>1と2のクラスを引数にとっています
(set-val *class1* *class3*) ;=>1と3のクラスを引数にとっています
(set-val *class2* *class3*) ;=>2と3のクラスを引数にとっています

メソッドが,必要なクラスを知っているって感じなのかな?

え?でもやっぱりJavaのオーバロードとの違いがわからない...

弱いカプセル化

(僕が個人的に)オブジェクト指向の肝の一つ(だと思っていた)カプセル化がCLOSでは行われない.
slot-value という関数を用いれば,に任意のデータに外部からアクセスできる.
CLOSでカプセル化を行うにはパッケージ管理機能を用いる.
クラスごとに情報隠蔽するのではなく上位のレベルで隠蔽するということ.

多重継承

CLOSは多重継承を許している.
複数のスーパクラスを持つことが出いる.

CommonLispにおける単一継承と多重継承
;;基本クラス定義
(defclass sample1 () ())
(defclass sample2 () ())

;;単一継承例
(defclass sample3 (sample1) ()) ;sample1を継承したクラス
(defclass sample4 (sample2) ()) ;sample2を継承したクラス

;;多重継承例
(defclass sample5 (sample1 sample2) ()) ;sample1とsample2を継承したクラスしたクラス

多重継承の多用はわかりづらいコード,バグの原因になり得ることがあるのでやり過ぎないようにする必要がある.

実行メソッド形成

メソッドの選択,並び替え,実行に関するルール.
解説内容が多いため次回の学習で記事を書く予定.

感想

総称関数の考え方やなど,目からウロコなアイデアがいっぱいでした.
Javaなどで用いられるOOSとは形式が全く異なっていて,機能も豊富そうなのでこれからさらに学習と経験を積んで理解を進めたいと思います.

正直,多態性,多重ディスパッチ,単一ディスパッチのあたりは違いがいまいちわかってない感もあります.
多重ディスパッチとオーバーロードの関係も,よろしければ教えていただきたいです...(^_^;)

次回はクラス定義や総称関数,メソッド結合についてちゃんと書きたいと思います.