オブジェクト指向を手に入れるまでの軌跡 メタプログラミング Ruby

目次

メタプログラミングRuby / 毎回の講義 / OOへ至る道 / ruby入門 / 講義ドキュ メント / note / poker

新人プログラマに知っておいてもらいたい人類がオブジェクト指向を手に入れるまでの軌跡 - Qiita

Todo オブジェクト指向に至る軌跡

オブジェクト指向プログラミング、あるいはオブジェクト指向言語は、 それに至るまでの様々なアイデアを統合し、再編され、また現実 的な制約の中で歪みながら生まれてきたものだったりする。

プログラミングパラダイムは, 現実世界のプログラミングという人間活動の中で生じた 課題をどのように整理していくかという中で生まれてきた。

簡単な歴史とともに理解していきたいと思う。

Done ソフトウェア危機

ソフトウェア危機 (wikipedia) とは

1960年代の後半、コンピュータが進歩するにつれて、より複雑 なソフトウェアが求められ始める時代、その複雑さをコントロールするた めの道具やアイデアはあまり多くなかった。

プロジェクトは、複雑化する一方なのに、管理手法もなければ、データ型 は基本的な数値でしかなく、変数はメモリアロケーションそのものだった。

また、プログラムの流れは、gotoやjump命令のようにプログラムカウンタ を直にコントロールする抽象度の低いもので制御されることが多かった。

プログラムはフローチャートで記述され、それをマシン語としてパンチす るといったプロジェクトX的な世界のことを考えれば、その理解が正しいの かもしれない。

なんにせよ、そういった当時の人からすると逼迫していたが、今から見る となんとも牧歌的な世界観の中で、構造化プログラミングという概念が生 まれる。

Done 構造化プログラミング

ダイクストラは構造化プログラミングを提案した

ときどき、勘違いされているが構造化プログラミングとは「手続き型言語」 のことでもなければ「gotoを使わないプログラミング」のことでもない。

Todo 構造化プログラミングとは

構造化プログラミング(wikipedia)

  • 構造化プログラミングではプログラミング言語が持つステートメントを 直接使ってプログラムを記述するのではなく、
  • それらを抽象化したステートメントを持つ仮想機械を想定し、
  • その仮想機械上でプログラムを記述する。
  • 普通、抽象化は1段階ではなく階層的である。
    • 各階層での実装の詳細は他の階層と隔離されており、
    • 実装の変更の影響はその階層内のみに留まる(Abstract data structures)。
    • 各階層はアプリケーションに近い抽象的な方から土台に向かって順 序付けられている。
    • この順序は各階層を設計した時間的な順番とは必ずしも一致しない

つまり、現代風に言い換えると「レイヤリングアーキテクチャ」のよう なもので、ある土台の上にさらに抽象化した土台をおき、その上にさら に・・・というようにプログラムをくみ上げていく考え方のことだ。

これは、現在のプログラミングにおいても当たり前となっている考え方 だ。

だから、我々は、ひとつのアーキテクチャないし関数の中で異なる抽象 化レイヤの実装を同居することをさける。

一方、耳目を集めやすいgoto文有害論とともに構造化技法の一部である 構造化定理(任意のフローチャートは、for文とif文で記述できる)が注目 され、手続き型プログラミング言語を現代の形に押し上げていった。

Done モジュラプログラミング

こういった背景のなか、プログラムは大きく複雑になり続ける。至極自然 な流れとして、それを分割しようとしていく。

凝集度と結合度

モジュールの分割には、大きな指針がなかった。現在でもやろうと思え ば全然関係のない機能を1つのモジュールに詰め込むことはできる。

熟練したプログラマとそうでないプログラマで、作り出すモジュールの 品質は違う。その品質の尺度として、凝集度と結合度という概念がしば らくして生まれた。

結合度:よいコラボレーションとわるいコラボレーションを定義した http://ja.wikipedia.org/wiki/%E7%B5%90%E5%90%88%E5%BA%A6

凝集度:よい機能群のまとめ方とわるい機能のまとめ方を定義した http://ja.wikipedia.org/wiki/%E5%87%9D%E9%9B%86%E5%BA%A6

これらは「関心の分離」を行うためにどのようにするべきかという指針でもあった。 http://ja.wikipedia.org/wiki/%E9%96%A2%E5%BF%83%E3%81%AE%E5%88%86%E9%9B%A2

この「関心」とはそのモジュールの「責任」「責務」と言い換えてもい いかもしれない。この責任とモジュールが一致した状態にできるとその モジュールは凝集度が高く、結合度を低くすることができる。

それぞれ悪い例と良い例を見ていき、「責任」「責務」の分解とは何か をとらえていこう。

悪い結合、良い結合

悪い結合としては、あるモジュールが依存しているモジュールの内部デー タをそのまま使っていたり(内容結合)、同じグローバル変数(共通結 合)をお互いに参照していたりというようなつながり方だ。

こうなってしまうとモジュールは自分の足でたっていられなくなる。つ まり、片方を修正するともう片方も修正せざるをえなくなったり、予想 外の動作を強いられることになる。

逆に良い結合としては、定められたデータの受け渡し(データ結合)やメッ セージの送信(メッセージ結合)のように内部構造に依存せず、情報の やり取りが明示的になっている状態を言う。

これはまさにカプセル化とメッセージパッシングのことだよね、と思っ た方は正しい。オブジェクト指向は良い結合を導くために考えだされた のだから。

悪い凝集、良い凝集

凝集度が低い状態とは,つまり悪い凝集とは,何か,

暗合的凝集
アトランダムに選んできた処理を集めたモジュールは 悪い。何を根拠に集めたのかわからないものも悪い凝集だ。
論理的凝集
論理的に似ている処理だからという理由だけで集めて はいけない。

たとえば、入出力の処理だからといって、

function open(type,name){
    switch(type){
    case "json": ... break;
    case "yaml": ... break;
    case "csv" : ... break;
    case "txt" : ... break;
	:
    }
    return result;

}

openという関数にif文やswitch文を大量に入れて、あらゆるopen処理を まとめた関数をイメージしてもらいたい。(その論理的な関係を一つの 記述にまとめたいと思うこと自体は悪い発想じゃないが、同じ場所に書 くことで、もっと大事なデータとの関係が危うくなってしまう。その矛 盾をうまく解決するのが同じメッセージをデータ構造ごとに異なる解釈 をさせるポリモーフィズムだ。)

そういった種類のものがメンテナンスしづらいというのはイメージしや すいだろう。

時間的凝集
他にも同じようなタイミングで実施されるからといっ て、モジュール化するのもの問題がある。たとえば、 initという関数の中ですべてのデータ構造の初期化を するイメージをしてほしい。

一方、良い凝集とはなんなのか、それは

通信的凝集
とあるデータに触れる処理をまとめることであるとか、
情報的凝集
適切な概念とデータ構造とアルゴリズムをひとまとめ にすること。
機能的凝集
それによって、ひとつのうまく定義されたタスクをこ なせるように集めることである。
状態と副作用の支配

よいモジュール分割とはなにか

  • それは、処理とそれに関連するデータの関係性を明らかにして支配し ていくことの重要性だ。

    できれば、完全にデータの存在を隠蔽できてしまえると良いが、現実 のプログラムではそうは行かない場合も多い。

こういった実務プログラミングの中で何が難しいかというと、それが状 態と副作用を持つことだ。

たとえば、

function add(a,b){
    return a+b;
}

このような副作用を持たない関数はテストもしやすく、バグが入り込む隙が少ない。 たとえば、計算機のレジスタ機能をこの関数に導入し、

var r = 0;
function add(a,b){
    r = a+ (isUndefined(b)||r)
    return r
}

このようにすると途端に考慮するべき事柄が増える。関連する状態や副 作用を含めて、関数を大別すると次のようになる。

オブジェクト指向に至るモジュラプログラミングは、こういった状態や 副作用に対して,積極的に命名,可視化,粗結合化をしていくことで 「関心の分離」を実現しようとした。

たとえば、現在でもC言語のプロジェクトなどでは,構造体とそれを引 数とする関数群ごとにモジュールを分割し,大規模なプログラミングを 行っている。構造体と関数群

typedef struct {
    :
} Person;

void person_init(person*p,...){
    :
}

char * person_get_name(person *p){
    :
}

void person_set_name(person *p,char *name){
    :
}

よくあるのは、上記のように構造体の名前のprefixとしてつけ、構造体 のポインタを第一引数として渡す手法だ。

その名残なのか、正確なところはよく知らないが、pythonやperlのオブ ジェクト指向では、自分自身を表すデータが、第一引数として関数に渡 される。

class Person(object):
    def __init__(self, a, b):
	self.a = a
	self.b = b
package Person {
    sub new(){
	my ($class,$a,$b) = @_;
	my $self = bless{},$class;
	$self->init($a,$b);
	return $self;
    }
    sub init {
	my ($self,$a,$b) = @_;
	$self->{a} = $a;
	$self->{b} = $b;
    }
}

あくまで関数の純粋性を犠牲にしないように発展を続けた関数型プログ ラミングと、状態や副作用をデータ構造として主役にしていった手続き 型プログラミングの分かれ目として理解すると面白い。

抽象データ型

よいモジュール化の肝は、状態と副作用を隠蔽し、データとアルゴリズム をひとまとめにすることだった。

それらを言語的に支援するために抽象データ型という概念が誕生した。

抽象データ型は、今で言うクラスのことだ。すなわちデータとそれに関連 する処理をひとまとめにしたデータ型のことだ。ようやくオブジェクト指 向の話に近づいてきた。ダイクストラの構造化プログラミングでは、デー タ処理をどのように抽象化するかが課題として残っていた。

また、データ型と実際のメモリアロケーションは別であるので、新たに変 数を定義するとデータの共有はしない。あるデータ型を実際に存在するメ モリに割り当てることをインスタンス化という。

抽象データ型のポイントは、その内部データへのアクセスを抽象データ型 にひもづいた関数でしか操作することができないという考え方だ。

これはつまり、たとえば、先ほどのC言語の例でいうと

//people.h

typedef struct {
    //内部構造も公開している
} people;

void people_init(people *p,...);

char * people_get_name(people *p);

void people_set_name(people *p,char *name);

このままだと、構造体の内部構造も公開しているので、

people user;
user.age = 10;
printf("%d years old",user.age);

のように内部構造に直接アクセスできてしまう。C言語では、テクニック としてperson.h こちらを公開する

typedef struct sPerson person;

void person_init(person *p,...);

char * person_get_name(person *p);

void person_set_name(person *p,char *name);
//people_private.h こちらはモジュール内で利用する

#include "person.h";

struct sPerson {
    // ここに内部構造
};

//非公開用関数
_person_private(person *p,....);

公開するヘッダと非公開のヘッダを分けることで、情報の隠蔽を行い抽象 データ型としての役目を成り立たせている。

抽象データ型の情報隠蔽とカプセル化

C言語の構造体であっても、ヘッダファイルの定義と実装を分けることで、 抽象データ型の内部構造を隠蔽することができたが、言語機能として外 部からのアクセスに対する制限を明示できるようにサポートした。カプ セル化やブラックボックス化というのは情報隠蔽よりも広い概念ではあ るが、これらの機能によって、「悪い結合」を引き起こさないようにし ている。

JavaやC#などのアクセス修飾子がそれにあたる。

PerlやJavaScriptなどアクセス修飾子の無い言語では、公開と非公開を 明確に区別せず、_privateMethodのようにアンダースコアを先頭につけ ることで、擬似的に公開と非公開を区別する。

いずれにしても、ポイントは抽象化されたデータを取り扱うレイヤは、 抽象化されていない生の階層を直接触ることがないという階層化の考え 方だ。

これによって、複雑化した要求を抽象化の階層を定義していくという現 代的なプログラミングスタイルが確立した。

オブジェクト指向?

最初のオブジェクト指向言語は、1960年代に出現したSimulaという言語だ。

これはシミュレーション記述のために作られた言語であったが、後に汎用言 語となった。

オブジェクト、クラス(抽象データ型)、動的ディスパッチ、継承が既にあ り、ガーベジコレクトまで実装されていたらしい。汎用言語としてそこまで はやることはなかったが、これらの優れたコンセプトは今現在まで生き残っ ている。

Simulaの優れたコンセプトをもとに,2つの,今でも使われている,C言語 拡張が生まれた。

一つはC++。もう一つはObjective-Cである。

C言語はとても実際的なものだったので、それにプリプロセッサの形で優れ たコンセプトを輸入しようとしたのは当然の成り行きといえばそうだ。

SimulaのコンセプトをもとにSmalltalkという言語というか環境が爆誕した。

Smalltalkは、Simulaのコンセプトに「メッセージング」という概念を加え、 それらを再統合した。Smalltalkはすべての処理がメッセージ式として記述 される「純粋オブジェクト指向言語」だ。

そもそもオブジェクト指向という言葉はここで誕生した。

オブジェクト指向という言葉の発明者であるアランケイは後に「オブジェク ト指向という名前は失敗だった」と述べている。メッセージングの概念が軽 視されて伝わってしまうからだという。

何にせよ、このSmalltalkの概念をもとにC言語を拡張したのがObjective-C だ。

Simula & C++のオブジェクト指向

C++の作者であるビャーネ・ストロヴストルップは、オブジェクト指向を 「『継承』機構と『多態性』を付加した『抽象データ型』のスーパーセット」 として整理した。

C++ではメソッドのことをメンバー関数と呼ぶ。これはSimulaがメンバープ ロシージャと読んでいるところに由来する。メソッドは、Smalltalkが発明 した用語だ。

どの処理を呼び出すか決めるメカニズム

さて、継承と多態を足した抽象データ型といっても、なんだか良くわからない。

特に多態がいまいちわかりにくい。オブジェクト指向プログラミングの説明で

string = number.StringValue
string = date.StringValue

これで、それぞれ違う関数が呼び出されるのがポリモーフィズムですよと 呼ばれる。

これだけだとシグネチャも違うので、違う処理が呼ばれるのも当たり前に 見える。

では、こう書いてみたらどうか

string = stringValue(number) // 実際にはNumberToStringが呼ばれる
string = stringValue(date)   // 実際にはDateToStringが呼ばれる

このようにしたときに、すこし理解がしやすくなる。引数の型によって呼 ばれる関数が変わる。こういう関数を polymorphic (poly-複数に morphic- 変化する) な関数という。

これをみたときに"関数のオーバーロード"じゃないか?と思った人は鋭い。 http://ja.wikipedia.org/wiki/%E5%A4%9A%E9%87%8D%E5%AE%9A%E7%BE%A9

多態とは異なる概念とされるが、引数によって呼ばれる関数が変わるとい う意味では似ている。しかし、次のようなケースで変わってくる。

function toString(IStringValue sv) string {
    return StringValue(sv)
}

IStringValueはStringValueという関数を実装しているオブジェクトを表す インターフェースだ。これを受け取ったときに、関数のオーバーロードで は、どの関数に解決したら良いか判断がつかない。関数のオーバーロード は、コンパイル時に型情報を付与した関数を自動的に呼ぶ仕組みだからだ。

stringValue(number:Number) => StringValue-Number(number)
stringValue(date :Date)  => StringValue-Date(date)

function toString(IStringValue sv) string {
    return StringValue(sv) => StringValue-IStringValue (無い!)
}

それに対して、動的なポリモーフィズムを持つコードの場合、次のように 動作してくれるので、インターフェースを用いた例でも予想通りの動作を する。

function StringValue(v:IstringValue){
    switch(v.class){ //オブジェクトが自分が何者かということを知っている。
    case Number: return StringValue-Number(number)
    case Date   : return StringValue-Date(date)
    }
}

このようにどの関数を呼び出すのかをデータ自身に覚えさせておき、実行 時に探索して呼び出す手法を 動的分配*,*動的ディスパッチ と呼ぶ。

このように動的なディスパッチによる多態性はどのような意味があるのか。

それはインターフェースによるコードの再利用と分離である。

特定のインターフェースを満たすオブジェクトであれば、それを利用した コードを別のオブジェクトを作ったとしても再利用できる。

これによって、悪い凝集で例に挙げた論理的凝集をさけながら、 汎用的な処理を記述することができるのだ。

オブジェクト指向がはやり始めた当時は、再利用という言葉が比較的バズっ たが、現在的に言い換えるなら、インターフェースに依存した汎用処理と して記述すれば、結合度が下がり、テストが書きやすくなったり、仕様変 更に強くなったりする。

動的ディスパッチ

動的ディスパッチのキモは、オブジェクト自身が自分が何者であるか知っ ており、また、実行時に関数テーブルを探索して、どの関数を実行する かというところにある。SimulaもC++もvirtualという予約語を用いて、 仮想関数の動的分配をすることを宣言できる。

/*
Vtable for B1
B1::_ZTV2B1: 3u entries
0     (int (*)(...))0
8     (int (*)(...))(& _ZTI2B1)
16    B1::f1

Class B1
   size=16 align=8
   base size=16 base align=8
B1 (0x7ff8afb7ad90) 0
    vptr=((& B1::_ZTV2B1) + 16u)
 */
class B1 {
public:
    void f0(){}
    virtual void f1(){}
    char before_b0_char;
    int member_b1;
};
/*
Class B0
   size=4 align=4
   base size=4 base align=4
B0 (0x7ff8afb7e1c0) 0
 */
class B0{
private:
    void f(){};
    int member_b1;
};

このようにデータ自身にvtable(仮想関数テーブル)へのポインタを埋め込んであり、 それをたどることで解決する。

逆にvirtual宣言をしなければ、仮想関数テーブルをたどるというオーバー ヘッドなしに関数を呼ぶことができる。Javaでは、デフォルトでvirtual 宣言されているのと等価に動的なディスパッチが行われる。C++やC#では、 動的ディスパッチのコストを必要なときにしか利用しないために(ゼロオー バーヘッドポリシー)、virtual宣言を明示的にする必要がある。

objective-Cも同様であるが、関数ポインタを直に取得することでこのオー バーヘッドを回避することができる。

//objectivce-c.m

SEL selector = @selector(f0); 
IMP p_func = [obj methodForSelector : selector ];
// p_funcを保持しておいて、繰り返しなどで
   :
pfunc(obj , selector);   // pfunc使うと、探索コストを減らせる。
// 何か重要でない限りする必要はない。

疑似コードで、この動的なディスパッチを表現するとこのようになる。

//動的ディスパッチの疑似コード

var PERSON_TABLE = {
    "getName" : function(self){return self.name},
};

var object = {
    _vt_ : PERSON_TABLE, // 自分が何ができるか教える
    name : "daichi hiroki"
};

// メソッドを動的に呼び出す
function methodCall(object,methodName){
    // オブジェクト自身を第一引数として束縛する
    return object._vt_[methodName](object)
}

methodCall(object,"getName");

こうなってくると、多態を実現するためには、3つの要素が必要だとわかる。

  • データに自分自身が何者か教える機能
  • メソッドを呼び出した際にそれを探索する機能
  • オブジェクト自身を参照できるように引数に束縛する機能

あとからオブジェクト指向的機能を追加したperl5の例が、これらを端的 に追加しているので見ていこう。

package Person;

sub new {
    my($class,$ref) = @_;
    #リファレンスとパッケージを結びつけるbless関数
    # $classはPersonパッケージを表す
    return bless( $object, $ref );
}
sub get_name{
    my ($self) = @_;
    $self->{name};
}

#メソッドの動的な探索と第一引数に束縛する->アロー演算子
my $person = Person->new({ name => "daichi hiroki"});
$person->get_name;

このなかで、bless関数はリファレンスに対して、リファレンス自身が 「関数を探索するべきモジュールはここですよ。」と教えている。 (blessは祝福するという意味。パッケージのご加護が守護霊みたいにくっ つくイメージ。)

また->演算子を使うことで、自動的に探索と呼び出しを実現している。

あと付けでOOP機能を足そうというときに、たった二つの機能で多態を実 現したPerl5のアプローチにはたぐいまれなセンスを感じる。

継承と委譲
継承

さて、SimulaとC++がもたらした最後の要素は継承だ。継承は、あるク ラスの機能をもったまま、別の機能を追加したもう一つのクラスを作る 仕組みだ。

まずはデータだけで考えてみよう。 生徒と先生の管理をしたいというときに、 二つに共通しているデータ構造は名前、性別、年齢であり、 生徒は追加して、学科と年次を管理し、 先生は追加して、専門と月収を管理したいとする。

typedef struct {
    int age;
    int sex;
    char *name;
} Person;

typedef struct {
    People people;
    int grade;
    int study:
} Student;

typedef struct {
    People people;
    int field;
    int salary;
} Teacher;

Teacher t;
t.people.age = 10;

とするとこのように構造体に構造体を埋め込むことで、共通するデータ 構造を持つことができる。

これに処理を追加する場合、次のようにするだろう。

char * person_get_name(Person *self) {
    return self->name;
}
char * teacher_get_name(Teacher *self){
    return person_get_name((People *)self);
}

char * teacher_get_name_2(Teacher *self){
    return person_get_name(&self.person);
}

Teacher *pt = teacher_alloc_init(30,MALE,"daichi hiroki",MATH,30);
teacher_get_name(pt);

このようにアップキャストして、埋め込んだ構造体内部にアクセスすることができる。 それか、埋め込んだ構造体をそのまま渡すなどして、処理の共通化を実現する。

しかし、これでは処理の共通化をするごとにその呼び出しコードを追加する必要がある。 これをうまく提供してくれるのが 継承機能だ。

public/protectedなメンバー関数やメンバー変数に対して、継承関係をたどって 探すことができる。

そのため

Teacher *t = new Teacher;
t->get_name; // Teacher自体に宣言がなくても、Peopleクラスを探索してくれる。

のように書くことができる。

また、

string nameFormat(People *p)  {
    return sprintf("%s(%d) %s",p->get_name,p->get_age,(p->get_sex == MALE) ? "男性" :"女性");  
}

というような関数があったときに、

Person *p = new Person;
Student *s = new Student;
Teacher *t = new Teacher;

nameFormat(p);
nameFormat(s);
nameFormat(t);

Person自身かそのサブクラスであれば、共通の処理を利用することができる。

この継承関係を言語機能として提供するためにperl5では、もう一つの機能を追加する。 それが@ISAだ。

package Person;
sub get_name{"person"}

package Student;
# @ISAにパッケージを追加するとblessされたパッケージに関数がなかった場合にそちらを探索に行く
our @ISA = qw/Person/;

package Teacher;
our @ISA = qw/Person/;

このようにどこを探索するのかという情報だけ宣言できるようにすれば、 問題なく継承関係を表現することができる。

ちょうど、FQNで表記すると

@Teacher::ISA="Person"という表現になり、teacher is a personという関係が成り立っていることを表現している。

このときのメソッド探索を疑似コードで書くと次のようになる。 動的ディスパッチの疑似コード

var PERSON_TABLE = {
    "getName" : function(self){return self.name}
};

var STUDENT_TABLE = {
    "getGrade" : function(self){return self.grade},
    "#is-a#"  : PERSON_TABLE
};

var object = {
    _vt_ : STUDENT_TABLE, // 自分が何ができるか教える
    name : "daichi hiroki"
};

// メソッドを動的に呼び出す
function methodCall(object,methodName){

    var vt = object._vt_;
    // is-aを順番にたどってmethodを見つけて実行する
    while(vt){
	var method = vt[methodName];
	if( method ) return method(object);
	vt = vt["#is-a#"];
    }
    throw Error;
}

methodCall(object,"getName");
委譲

継承の代わりに委譲という手段を用いているプログラミング言語がある。 これはSimulaとC++の系譜とは少し違うが、動的ディスパッチの話をしたので 簡単に説明する。

これは、クラスベースのオブジェクト指向に対してプロトタイプベース のオブジェクト指向と呼ばれたりする。身近な例ではJavaScriptなどだ。

継承と委譲の違いは先ほどのC言語の例で言えば、すごく単純で埋め込む構造体が ポインタかそうでないかという違いくらいだ。

typedef struct {
    int age;
    int sex;
    char *name;
} Person;

typedef struct {
	Person* person;
    int grade;
    int study:
} Student;

typedef struct {
    Person* person;
    int field;
    int salary;
} Teacher;

委譲は、探索先のオブジェクトを動的に書き換えることができる。

t->person = new Person;

疑似コードで言えば、 動的ディスパッチの疑似コード

var hogetaro = { getName : function(self){return self.name}, name : "hogetaro" };

var object = { prototype : hogetaro, // 次に探索するオブジェクトを決める name : "daichi hiroki" };

// メソッドを動的に呼び出す function methodCall(object,methodName){ // 最初は自分自身 var pt = object; // is-aを順番にたどってmethodを見つけて実行する while(pt){ var method = pt[methodName]; if( method ) return method(object); pt = pt._prototype_; } throw Error; }

methodCall(object,"getName"); object._prototype_ = { getName:function(){return "hello"}}; // プロトタイプは動的に書き換えることができる。 methodCall(object,"getName");

このようになる。 こうやって、prototypeを順番に追って検索していくのをjavascriptではプロトタイプチェーンと読んでいる。luaであれば同じ役割をするのがmetatableというものがある。

こういった委譲によるメソッド探索は、動的継承とも呼ばれている。

このようにメソッドの動的な探索に対して、どのような機構をつけるのかというのが オブジェクト指向では重要な構成要素と言える。

rubyのmoduleやそのinclude,prepend、特異メソッド、特異クラスなどは まさにその例だ。

それらをjavascriptで疑似コード的に実装した例として、こちらを参照してもらいたい。 http://qiita.com/hirokidaichi/items/f653a843208971981c37

オブジェクト指向の要素

このようにオブジェクト指向のための機能は、

抽象データ型:データと処理をひもづける 抽象データ型:情報の隠蔽を行うことができる オブジェクト:データ自身が何者か知っている 動的多態:オブジェクト自身のデータと処理を自動的に探索する 探索先の設定:継承、委譲

ということになる。

Smalltalk & Objective-Cのオブジェクト指向

Smalltalkの作者の一人であるアランケイがオブジェクト指向という言葉について次のように定義づけている。 「パーソナルコンピューティングに関わる全てを『オブジェクト』とそれらの間で交わされる『メッセージ送信』によって表現すること」

C++の世界観とはまた異なっているのがわかると思う。

仮想機械としてのオブジェクト

アランケイの世界観の中では、メモリとCPUとそれに対する命令を持つ機械をさらに抽象化するとしたら、それは同じくデータと処理と命令セットをもつ仮想機械で抽象化されるべきだと考えていた。

これは、構造化プログラミングの中でダイクストラが仮想機械として階層的に抽象化すべきだと言っていたこととかぶる。 個人的には、背景の違いこそあれ同じことを言っているように思う。

オブジェクトは独立した機械と見なせるため、それに対してメッセージを送り、自ら持つデータの責任は自らが負う。 Smalltalkの実行環境もまた仮想機械として作られている。

メッセージング

Smalltalkでメッセージ送信は receiver messageのように記述する。

Objective-Cであれば、C言語の中に次のように書くことでメッセージ送信機構を動かすことができる。 [receiver message] [receiver methodName:args1 with:args]

メッセージ送信と関数呼び出しは厳密には異なる。Objective-Cにとって、実装上はほとんど同じことではあるが。

メッセージとは通信のアナロジーだ。たとえばEメールをイメージしてもらいたい。メールアドレスさえ知っていれば、メッセージは自由に送れる。受信者(レシーバ)はメッセージを受け取っているにすぎないので、その解釈は自由に行うことができる。

このメッセージらしさが出てくる特徴をいくつか紹介しよう。

動的な送信

メッセージ内容もまたオブジェクトにすぎないので、動的に作成し、送ることができる。 たとえば、rubyのObject#sendがその性質をそのまま表現している。 Object#send.rb

class A def hello p "hello" end end

a = A.new

method = "he" + "ll" + "o"

a.send(method)

LL言語では、こういった動的な性質は普通のことになってきているが、 Objective-Cでも、セレクタ型とともにperformSelectorメッセージを送ったり、NSInvocation*を使うことで動的に作られたメッセージを送信することができる。

カスケード式

Smalltalkの機能で、カスケード式というのがある。これは、複数のメッセージを同時にまとめて送るという機能だ。

これもまたメッセージのアナロジーならではと言えるだろう。 addというメッセージを5つ連続で送る例

collection

collection := OrderedCollection new add: 0; add: 1; add: 2; add: 3; add: 4; yourself.

JSON RPCのBulk Requestのイメージに近い。

メッセージ転送

受け取ったメッセージは、仮にメソッド定義がなかったとしても自由に取り扱うことができる。

http://ja.wikipedia.org/wiki/%E3%83%A1%E3%83%83%E3%82%BB%E3%83%BC%E3%82%B8%E8%BB%A2%E9%80%81

rubyのmethod_missingやObjective-CのforwardInvocationがそれにあたる。他にもPerlのAUTOLOADなど、最近の動的型言語には用意されていることが多い。

Smalltalkであれば、doesNotUnderstandメソッドが呼ばれ、その中で煮るなり焼くなりできる。 proxy.rb

class Proxy def method_missing(name, *args, &block) target.send(name, *args, &block) end

def target @target ||= [] end end

たとえば、proxyクラスをこのように定義してあげると すべてのメッセージをtargetのオブジェクトにそのまま転送してあげることができる。

これもメッセージというアナロジーならではの考え方だ。

非同期送信

ほとんどの言語でメッセージの結果を同期的に受け取るようになっているので、意識しづらいが、メッセージというアナロジーである以上、それを同期的に待ち受ける必要はない。

object foo //同期呼び出し future = object @foo //非同期呼び出し

非同期なメッセージパッシングを中心にオブジェクト間の相互のやり取りをモデル化したものとして、アクターモデルがある。

scalaのActorを利用するとこんな感じ actor.scala

class HelloActor extends Actor { def receive = { case "Hello" => println("helloworld") case _ => println("errror") } }

このようにメッセージパッシングというアナロジーを使うことで、様々な性質がオブジェクト指向には加わることになった。

しかし、オブジェクト指向という言葉が意味しているのが、C++の再定義したオブジェクト指向として理解されることで、このメッセージパッシングの要素が意識されなくなってしまったため、前述したようにアランケイはその命名が不適切だったと考えているらしい

http://www.infoq.com/jp/news/2010/07/objects-smalltalk-erlang

この記事は今までの議論の流れをふまえると、理解がしやすいと思う。 特に

私は、オブジェクト指向プログラミングというものに疑問を持ち始めました。Erlangはオブジェクト指向ではなく、関数型プログラミング言語だと考えました。そして、私の論文の指導教官が言いました。「だが、あなたは間違っている。Erlangはきわめてオブジェクト指向です。」 彼は、オブジェクト指向言語はオブジェクト指向ではないといいました。これを信じるかどうかは確かではありませんでしたが、Erlangは唯一のオブジェクト指向言語かもしれないと思いました。オブジェクト指向プログラミングの3つの主義は、メッセージ送信に基づいて、オブジェクト間で分離し、ポリモーフィズムを持つものです。

このくだりは。

さらに面白いのはそのErlangの開発に深く関わっている人物が 「オブジェクト指向はなんでくそったれか!」http://www.sics.se/~joe/bluetail/vol1/v1_oo.html のような記事を出していたりして、なかなか面白いことになってます。

Qiita上に翻訳もあったのでぜひご一読ください。 オブジェクト指向はクソか?

まとめ

オブジェクト指向も構造化プログラミングも問題の抽象化で同じことを見ていた。 C++はSimulaからモジュール化や抽象データ型、動的多態といった良い性質を採用した。 一方、SmalltalkはSimulaの着想をメッセージとオブジェクトという概念で統合した。 それによって、様々な動的な性質を現在の言語にもたらしてきた。

また、メッセージパッシングという概念は、本質的には現在注目を浴びているActorやCSPのような並行モデルと似通っており、興味深い。

あとがき

少しはオブジェクト指向という考え方の背景が見えてきて、 それがより良い設計やコーディングにつながればうれしいです。

この説明は、オブジェクト指向の説明の本流ではない、いわば傍流的なもので はありますが、より実際的で、より技術的理解を必要とするものなので、初学 者向けではなかったかと思います。ですが、これを理解することで、様々な言 語機能の背景を推察することができ、バラバラの事柄が有機的につながること を期待しています。

オブジェクト指向あれこれ

著者: suzuki@cis.iwate-u.ac.jp

Created: 2016-01-16 土 15:09

Emacs 24.5.1 (Org mode 8.2.10)

Validate