2016年9月26日

実践common lisp第16章

Common LispにおけるOOPへの回答がCLOSですね
一般的に言われているOOPの機能で取り上げられることが多いものは、継承、カブセル化あたりだと思うんですけどCLOSはどっちもサポートが薄いですね
このあたりが特徴的と言うところかもしれませんけど、本来のOOPとしての要求は別に継承でもカプセル化でもないはずなんだよね
いろいろ紆余曲折あっての歴史的な経緯によって継承とカプセル化が取り上げられることが多いのだけど両方共実はイマイチ使い道がないって言うかまあ別に不要って言うか
継承については、実装の継承って言うのはCLOSにおいてはまったく無視されていて、インターフェイスの継承についてのみサポートされている点が素晴らしいところですね
カプセル化なんて別にOOP使わなくてもクロージャで解決できる話でさ、C#とかC++がOOPでやっちゃったから話がややこしくなっちゃってるだけなんじゃないかなと思ってます
頻繁に目にするOOPに対する誤解みたいなものもこのカプセル化を勘違いしちゃってる点が多いと思うし
ここに一発で言い表している素晴らしい表現があるので引用させてもらうと
”オブジェクト指向の基本的なアイデアは、データ型を定義してからそのデータ型に操作を連携させることがプログラムを体系化する強力な手法になるというものだ”
と言うのはまったくその通りで、データ型に対する操作なんだよね、本当にやりたいことって
と言うことで総称関数と言うややこしそうな面倒な名称のものがここに登場します
要は様々なデータ型に対して同じ名称でそのデータ型に対する操作が定義できるというだけなんだけどね
と、言うわけでこのあたりで例示させてもらうと、よく一般的にはこういうコードを見かけるのだけど
(defclass beatles() ((name :initarg :name :accessor beatles-name)))
(let ((members (list (make-instance 'beatles :name "John Lennon")
                     (make-instance 'beatles :name "Paul McCartney")
                     (make-instance 'beatles :name "George Harrison"))))
  (mapcar (lambda(m) (format t "hi, my name is ~A~%" (beatles-name m))) members))
これって悪い例なんですよね
クラスのインスタンスそれぞれに名前を設定して、メンバー関数で名前を呼び出すとかっていうものですね
これって使い方が圧倒的に間違っていると言うか、全然問題が解決されていないって言うかまあそんな感じで、こんなことするためにわざわざクラスとか用意されてないんだよね
よく目にする解決策だとdefmethodとか言う謎の機能がまったく不要だ
じゃあどういうふうにするといいかと言うとこんな感じになる
(defclass john-lennon() ())
(defclass paul-mccartney() ())
(defclass george-harrison() ())
(defmethod get-name((m john-lennon)) "John Lennon")
(defmethod get-name((m paul-mccartney)) "Paul McCartney")
(defmethod get-name((m george-harrison)) "George Harrison")
(let ((members (list (make-instance 'john-lennon)
                     (make-instance 'paul-mccartney)
                     (make-instance 'george-harrison))))
  (mapcar (lambda(m) (format t "hi, my name is ~A~%" (get-name m))) members))
まず、必要なだけデータ型を定義する
ここでは三人分のデータ型を定義する
そしてそれぞれのデータ型に対して操作を個別に定義する
悪い例の方だとインスタンスを生成する時にどんな名前かわかっていないといけないわけよ
それって至極面倒なことなんでそうなるとこんな感じの補助関数が必要になってくる
(defun make-john-lennon-instance()
  (make-instance 'beatles :name "John Lennon"))
そしてmembersリストを生成する段階でmake-john-lennon-instance関数を呼び出すって言うのは本当に本末転倒と言うかなにやってるんだかわかったもんじゃない
素直にjohn lennonインスタンスの名前はJohn Lennonだということで話は簡単になると言う素晴らしい解決策がdefclassとdefmethodを利用することによって可能となる素晴らしいCLOS
さらに、この例で言うと継承もカプセル化も利用してない
別に継承もカプセル化も利用しなくてもOOPを十分、存分に利用できるということが分かるともっとプログラミングが楽しくなると思うのだけど
だから各インスタンにいちいち名前とか設定するなんてやめて総称関数的なやり方で表現できるように努めると幸せになれるんじゃないかなと思います

参考までにJavaScriptで同等のコードを掲載します
function beatles(name){
    this.getname = function(){
        return name;
    };
}
[new beatles("John Lennon"), new beatles("Paul McCartney"), new beatles("George Harrison")].forEach(function(a){
    console.log("Hi, my name is " + a.getname());
});

function johnlennon(){
    this.getname = function(){
        return "John Lennon";
    };
}
function paulmccartney(){
    this.getname = function(){
        return "Paul McCartney";
    };
}
function georgeharrison(){
    this.getname = function(){
        return "George Harrison";
    };
}
[new johnlennon(), new paulmccartney(), new georgeharrison()].forEach(function(a){
    console.log("Hi, my name is " + a.getname());
});