オブジェクト指向してないオブジェクト指向言語と,オブジェクト指向してる並行プログラミング言語

オブジェクト指向言語ってのが,全然オブジェクト指向じゃなくて,Erlangが本当のオブジェクト指向言語かもしれないという衝撃的な記事を見て,もう一度オブジェクト指向について考えてみた.

元ネタ
オブジェクト指向プログラミングは間違いだったか?
http://www.grant-olson.net/python/intro-to-stackless-python

現実世界のオブジェクトを記述できないオブジェクト指向

一般的なオブジェクト指向言語といえば,C++Javaなどがある.今,オブジェクト指向言語で,母,父,弟,自分で夕食をとる関数を定義すると,おそらく,次のようになる.

take_dinner()
{
    mother.eat(dinner);
    father.eat(dinner);
    brother.eat(dinner);
    me.eat(dinner);
}

プログラミング的にはこれで正しいように思われるが,実際の動作として良く考えてみると,これは,まず母が食べ,母が食べ終わったとに父が食べ,父が食べ終わったあとに弟が食べ,弟が食べ終わって,ようやく自分が食べていることになる.現実的には,四人同時に食べるのが動作として正しいだろう.しかしながら,このプログラムはそうなってはいない.

では,オブジェクト指向的に並行して食事をするという事はどう書けばよいのだろうか.pthreadでも使えば出来そうであるけれど,普通,マルチスレッドはオブジェクト指向言語オブジェクト指向言語であるための要求には含まれていない.オブジェクト指向は,データ構造と手続きを一纏めにして抽象化するのは上手くいったが,現実的なオブジェクトを記述するには少し能力不足であるのだ.


次に,オブジェクト間の通信についても考えてみる.今,ping, pongを交互にやり取りするだけの単純なプログラムを考えてみると,オブジェクト指向言語では次のようになると考えられる.

class sender {
public:
    void ping() { cout << "ping" << end; p_receiver->pong(); }
    receiver *p_receiver;
};

class receiver {
public:
    void pong() { cout << "pong" << end; p_sender->ping(); }
    sender *p_sender;
};

begin_game()
{
    receiver r;
    sender   s;

    r.p_sender = &s;
    s.p_receiver = &r;

    s.ping();
}

これはC++で書いたプログラムであるが,これを実行するとどうなるかというと,関数呼び出しが深すぎるためスタックオーバーフローが発生し,プログラムは途中で停止してしまう.オブジェクト間で通信を行ないたいと思って関数を呼び出しただけでこの有様である.この例だと,末尾再帰の時はスタックに積まなければ良いと思うかも知れないが,末尾再帰で上手くいかない場合も普通にありえるので,問題の解決にはならない.

これはC++だけでなく,JavaPythonRubyなど他のオブジェクト指向言語でも起きる問題である.まともにやろうとしたら,まず,メッセージをやり取りする為のクラスを作って,関数呼び出しをきちんと処理しなければならない.どうも,オブジェクト指向というのは,オブジェクト間でメッセージをやり取りするという最も肝心な事でさえ,あまり重要視してないように見える.

このように,よくよく考えてみると,実のところオブジェクト指向言語ってのは,全然オブジェクト指向していないのでは無いかと思えてくる.

オブジェクト指向と並行プログラミング

では,並行プログラミング言語ErlangとStakless Pythonでこれらを書いてみよう.

Erlangで書いた,家族で夕食をとるプログラムは次のようになる.

-module(dinner).
-export([take_dinner/0, family/0]).

take_dinner() ->
    mother ! dinner,   %% ディナーを皆に配る
    father ! dinner,
    brother ! dinner,
    me ! dinner,
    ok.

family() ->
    register(mother, spawn(fun() -> people(mother) end)),     %% 家族のプロセスを作成
    register(father, spawn(fun() -> people(father) end)),
    register(brother, spawn(fun() -> people(brother) end)),
    register(me, spawn(fun() -> people(me) end)).

people(Name) ->
    receive
        MSG ->
            io:format("~p: start ~p\n", [Name, MSG]),
            receive
            after 5000 -> ok    %% 食べ終えるのに5秒かかる
            end,
            io:format("~p: finish ~p\n", [Name, MSG])
    end.

実行して確かめてみると,ちゃんと動いてることが確認できる.

$ erl
1> c(dinner).
{ok,dinner}
2> dinner:family().    %% oop的に家族インスタンスを作成
true
3> dinner:take_dinner().  %% 夕食を与える
mother: start dinner
father: start dinner
brother: start dinner
me: start dinner
ok
mother: finish dinner    %% 5秒後に皆が食べ終わる
father: finish dinner
brother: finish dinner
me: finish dinner
4>

このプログラムでは,母,父,弟,自分を意味するプロセスを生成し,生成したプロセスに対してdinnerというメッセージを送信している.此処で言うプロセスとは,まさにOOPで言うところのオブジェクトそのものである.オブジェクト指向プログラミングでは記述が難しかった,現実的なオブジェクトの振る舞いが,アクターモデルに基づく並行プログラミングでいとも簡単に記述できてしまった.Erlangの最初の開発者であるJoe Armstrongの指導教官が「Erlangはきわめてオブジェクト指向である」と言った理由も頷ける.


Stackless Pythonping pongのプログラムを書くと次のようになる.

import stackless

ch1 = stackless.channel()
ch2 = stackless.channel()

class sender:
    def ping(self):
        ch2.send('ping')
        while 1:
            msg = ch1.receive()
            print(msg)
            ch2.send('ping')

class receiver:
    def pong(self):
        while 1:
            msg = ch2.receive()
            print(msg)
            ch1.send('pong')

s = sender()
r = receiver()

stackless.tasklet(s.ping)()
stackless.tasklet(r.pong)()

stackless.run()

Stackless Pythonインタプリタで実行すると,スタックオーバーフローを引き起こすこともなく,永遠にプログラムは実行され続ける.

$ python pingpong.py
ping
pong
ping
pong
...

オブジェクト指向では面倒だったメッセージのやりとりも,アクターモデルに基づく並行プログラミングでは,スタックを気にすること無く簡単にかけてしまう.Stackless PythonのStacklessは,オブジェクト間のメッセージ通信にかかるスタックの積み重ねを気にしなくて良いというところから来ているようだ.


アクターモデルの並行プログラミングというと,マルチコアCPUの性能をフルに引き出すためや,C10K問題に対応するための言語という宣伝がされがちだが,実は,真にオブジェクト指向的な記述を可能にするプログラミングパラダイムだったのだ.