オブジェクト指向
ポエム

まだ「オブジェクト指向はこうあるべき」で消耗してるの?

有史以来、人々は「こうあるべき」で戦争を続けてきた

この記事は、「こうあるべき」をやめようという記事であると同時に、新たなる「こうあるべき」を人々に押し付ける新たな火種である。

参考までに、オブジェクト指向を巡っては過去にも何度か盛り上がったことがあり、前回の盛り上がりはオブジェクト指向と10年戦ってわかったことだったかと記憶している。かの有名なmatzは、このように言った。

今回はオブジェクト指向が5000%理解できる記事を皮切りに、オブジェクト指向が0.05%も理解できない記事【オブジェクト指向】自然言語を基準に設計することの問題点など、数多くの記事が出てきている。オブジェクト指向がn%理解できる記事まとめには、より多くの記事がまとめられている。

オブジェクト指向は便利だから普及した

さて、本題に入ろう。

果たして、オブジェクト指向は考え方として正しかったから普及したのだろうか。単に、便利だったから普及したのでは?
いくら正しくても、便利じゃなかったら、みんな面倒くさがって使わないよ。

「どうあるべきか」「現実はどうであるか」よりも「どう使えたら便利か」

よくある説明の「Animalを継承したDogpochi.say()したら"ワンワン"」の何がマズいか。
そのように書くことで得られるメリットがさっぱり分からないのがマズい。
犬が動物なのは知ってるが、犬が動物だったら動物を継承しないといけないのか。わざわざそんなことを考えて一体、自分の抱えている問題をどう解決してくれるのか。それがさっぱり伝わってこないのがマズい。

オブジェクト指向を便利に使おう、という観点でいうと。「犬が動物だから動物を継承しないといけない」のではなく、「動物を継承して犬を作ると、動物に共通するものを定義しなおす必要がなくなる。また、犬を他の動物と同じように扱えるから、もしそうしたいなら便利」という話になる。

よくある説明をさらに読み続けると pochi.say() の下の行に tama.say() で"にゃーん"と書かれているかもしれない。
けれど、プログラミングを始めたばかりの人が、その説明を読んで、便利さに気づけることは少ないだろう。

一方で、プログラミング経験がある程度ある人なら、インタフェースが共通である嬉しさ、つまり 何らかの動物.say() で、どんな動物でも似たような動作をするという嬉しさに気づくだろう。また、静的型付けな言語を使ってる人なら、Animalを取る関数を書いときゃDogにもCatにも使えることや、Animalの入る配列を用意すりゃDogCatも入ることにも気づくだろう。

C言語でもオブジェクト指向っぽい実装はありうる

唐突だが、C言語の話をしよう。C++の間違いではない。C言語の話だ。
オブジェクト指向の話になると、オブジェクト指向を取り入れた言語の構文の話になりがちなので、あえてそこから離れるためだ。

オブジェクト指向じゃない言語のはずの、C言語でもオブジェクト指向っぽい実装はある。
darknetは、Cで書かれたニューラルネットワークのプロジェクトだ。ディープラーニングに少し詳しい人なら、リアルタイム物体認識のYOLOというものを聞いたことがあると思う。このプロジェクトはYOLOの作者が開発しており、YOLOの実装もdarknetでされている。

ソースのファイルに、なんたら_layer.cってファイル名が多いのが気になる。

$ ls *layer.c
activation_layer.c     crnn_layer.c             iseg_layer.c      maxpool_layer.c        shortcut_layer.c
avgpool_layer.c        crop_layer.c             l2norm_layer.c    normalization_layer.c  softmax_layer.c
batchnorm_layer.c      deconvolutional_layer.c  layer.c           region_layer.c         upsample_layer.c
connected_layer.c      detection_layer.c        local_layer.c     reorg_layer.c          yolo_layer.c
convolutional_layer.c  dropout_layer.c          logistic_layer.c  rnn_layer.c
cost_layer.c           gru_layer.c              lstm_layer.c      route_layer.c

layer.cというファイルと、なんたら_layer.cというたくさんのファイルがある。
ニューラルネットワークは、何段もの、いくつかの種類の "layer" を積み重ねて構成される。いろんなネットワークが作れるように、いろんなレイヤーを用意しているっぽい。

例えばmaxpoolレイヤーがどうなっているのか見てみる。Cでは、maxpool_layer.cで定義された外部から呼び出せる関数はmaxpool_layer.hで宣言される習わしなので、それを見てみる。

maxpool_layer.h(一部抜粋)
typedef layer maxpool_layer;

image get_maxpool_image(maxpool_layer l);
maxpool_layer make_maxpool_layer(int batch, int h, int w, int c, int size, int stride, int padding);
void resize_maxpool_layer(maxpool_layer *l, int w, int h);
void forward_maxpool_layer(const maxpool_layer l, network net);
void backward_maxpool_layer(const maxpool_layer l, network net);

ふむ。ではconvolutional_layer.hは?

convolutional_layer.h(一部抜粋)
convolutional_layer make_convolutional_layer(int batch, int h, int w, int c, int n, int groups, int size, int stride, int padding, ACTIVATION activation, int batch_normalize, int binary, int xnor, int adam);
void resize_convolutional_layer(convolutional_layer *layer, int w, int h);
void forward_convolutional_layer(const convolutional_layer layer, network net);
void update_convolutional_layer(convolutional_layer layer, update_args a);
image *visualize_convolutional_layer(convolutional_layer layer, char *window, image *prev_weights);

なんとなく似ている。

さて。darknetでは、設定ファイルによってネットワークの形を作ることができ、各レイヤーはnet.layers[]配列に入ることになっている。

parse_network_cfg@parser.c
    while(n){
        params.index = count;
        fprintf(stderr, "%5d ", count);
        s = (section *)n->val;
        options = s->options;
        layer l = {0};
        LAYER_TYPE lt = string_to_layer_type(s->type);
        if(lt == CONVOLUTIONAL){
            l = parse_convolutional(options, params);
        }else if(lt == DECONVOLUTIONAL){
            l = parse_deconvolutional(options, params);
        }else if(lt == LOCAL){
            l = parse_local(options, params);
        }else if(lt == ACTIVE){
        // 略
    }

設定ファイルの中身に応じて、各レイヤーをパースし、layer型の構造体にしていく。
オブジェクト指向言語だと、それぞれのレイヤーは、そのレイヤーに応じてlayer型を継承した型にすると思う。Cでは継承の概念がないので、Darknetでは、全部layer型となっている。layer型はどのレイヤーが来ても困らないように、かなり多数のメンバ変数を持たせている。恐らく、子レイヤーで新たなメンバ変数が必要になったら、layerに新たに付け足すか、別の層のために用意したものを使い回すのであろう。

なお、Cの言語仕様としては継承の概念はないが、Cの黒魔術により、余分にメモリを確保してlayerの後ろを使うこともできる。(実際、そのようにしてCで継承のようなことをしているソースも見たことがある)

では、メソッドはどうしているのか。大変いい質問だ。
オブジェクト指向言語風にいうコンストラクタにあたる、make_なんたら_layer()に答えがある。

l.forward, l.backwardは、メソッドのように使われているが、どうやって作っているのか見てみる。

make_maxpool_layer
maxpool_layer make_maxpool_layer(int batch, int h, int w, int c, int size, int stride, int padding)
{
    maxpool_layer l = {0};
    l.type = MAXPOOL;
    l.batch = batch;
    l.h = h;
    // 略
    l.forward = forward_maxpool_layer;
    l.backward = backward_maxpool_layer;

forward_maxpool_layer, backward_maxpool_layerはヘッダで定義されていた関数だ。
つまりこれは、関数ポインタ!

Cには、最近の言語と違って「関数はオブジェクト」なんて概念は無いが、関数もメモリ上に置かれるものである以上は、アドレス(単なる数値と思っていい)を持ち、そのアドレスを指定することで関数を呼び出すことができる。
つまり、変数の中に関数(のアドレス)を入れることができ、呼び出すことができる。

こうすることで、まるでCがオブジェクト指向言語だったかのように、配列に入れたnet.layers[]の各要素について、l.forward()を呼び出しができる。

forward_network@network.c
    for(i = 0; i < net.n; ++i){
        net.index = i;
        layer l = net.layers[i];
        // 略
        l.forward(l, net);
        // 略
    }

l.forward()が指す関数は、レイヤーの種類ごとに異なる。第一引数にl自身が入れられているのは、Pythonでいうところのselfにあたると思ってほしい。
言語としてはオブジェクト指向をサポートしていないので、こういうのを自分で書かないといけないのがつらいところだ。

オブジェクト指向は便利だ

今の御時世、なぜ作者がC言語を使ったのかは私は知らない。
けれど、なぜこのようなオブジェクト指向的な書き方をしたのかは、想像がつく。
オブジェクト指向的な書き方は、便利なのだ。

つまり、コード量を減らし、コードを把握しやすくできる。
似たものを似たような操作方法で操ることができる。
各レイヤーの違いを意識する必要がない場面では、違いについて一切考えなくてもプログラミングできる。

オブジェクト指向的な書き方は、たとえオブジェクト指向的でない言語を使って書いていても、自然に選択肢に入るような、便利な書き方なのである。

あ! こんなところにもオブジェクト指向が!

ファイルを開く操作を考えてみよう。

ファイルを開いて書く
FILE *fp = fopen("/path/to/file", "w");
fputs("test", fp);
fclose(fp);
Pythonではこうなる
f = open("/path/to/file", "w")
f.write("test")
f.close()

最近は妙に、Pythonの細かい文法に関するマサカリコメントをよく頂く気がするので、念の為に書いておくが、Pythonではwithを使って書いた方が閉じ忘れがなくていいという話は、今はする必要がない。

重要なのは「ファイルって何だ?」という話だ。

ハードディスクなりSSDなりに保存されているバイト列、とでも言うのが適切なのだろうか。
いや、もしかしたらネットワークドライブかもしれない。
Linuxには、procファイルシステムというものがあり、そこには実行中のプロセスに関する情報が書かれたファイルがある。それって、ディスクに保存されてるの??

FUSE (Filesystem in Userspace)というものもある。これは仮想ファイルシステムを作るためのライブラリで、ファイルが開かれたとき、書き込みがされたとき、などの動作を定義すると、自分オリジナルのファイルシステムが作れるライブラリである。
C書けないよって人はここにPythonで動くサンプルが載ってる。

ファイルの開き方、書き方は、皆が知っているとおり、共通した動作だ。
けれど、このような共通のインタフェースの裏では、見知らぬ仕組みが適切に選択され、適切に動いている。どんなファイルシステムか、デバイスは何か、あるいはprocファイルシステムやFUSEのように、ソフトウェア的なものかもしれない。

「ファイル」という仕組みは、オブジェクト指向でよく言われる概念をきれいに実装している。
「ファイル」は、コンピュータの世界で最も成功したオブジェクト指向だと私は思っている。
そして、オブジェクト指向は、便利で分かりやすいインタフェースを作ろうとすると自然に現れる選択肢であると。

この記事で何を伝えたかったのか

オブジェクト指向は素晴らしい。よいものだ。
なぜ素晴らしいのか。なぜよいのか。

確かに、思想が優れているという一面もあるのかもしれない。
けれど私は「利便性」に着目したい。

オブジェクト指向自体を正しく使うのではなく、オブジェクト指向を使うことによって、書くコード自体がシンプルで分かりやすく綺麗になる。
オブジェクト指向を使わされる、オブジェクト指向に使われるのではなく、オブジェクト指向をうまく使うことで、よりよいコードを、より効率的に書ける。
私はそういうエンジニアでありたいし、そういうエンジニアが増えてほしい。

オブジェクト指向自体の「あるべき論」も、私は好きだ。私は宗教戦争大好き人間だ。
けれど、「こうあるべき」よりも「利便性」に着目してオブジェクト指向を見つめ直すと新たな世界が見えてくるかもしれない。

そういう思いから、新たな宗派を立ち上げてみた。