はじめに
私自身、関数型プログラミングは初歩の初歩レベルに達しているか怪しいレベルです。
しかもGWの時間のある日に思いつくまま書いています。
どうかお手柔らかにご覧ください。
関数型プログラミングは、オブジェクト指向プログラミングの対抗馬的な感じで、現在は捉えられているような気がします。
オブジェクト指向が、メンバ変数やメソッドをクラスなどの枠に束ねて、表から見えなくても良いものを隠蔽することで、複雑なシステムを使いやすくしていくものであるのに対して(主観です)、関数型は、隠蔽される変数をなくして、表から動作が隠蔽され、いつの間にか状態が変わってしまう事態を避けるものです(主観です)。
関数型をマジでやる時は圏論(圏・関手・自然変換・合成・モナドとか)をやっとけということだそうですし、私もちらっと学んでみて確かにそうだと思いました。
でも、MATLABでは難しいところもある。
研究こそ関数型じゃないか?
MATLAB(Simulinkは別)って大学や企業の研究に使われる用途が多そうなので、その文脈で話を進めます。
個人的には研究の用途において関数型のプログラミングは、ある程度であれば有用だと思います。
オブジェクト指向は基本的に動作の主体がオブジェクトです。何をやらせるにも「オブジェクトが何かをする」で言い表さないといけません。
- 自動運転車が道に沿って走る。
- ロボットアームがものを指定位置に運ぶ。
- etc
「自動運転車が道に沿って走る。」を作りたいときは、車オブジェクトを作り、その中に道の形状を観測するメソッドや、操舵するメソッドなどを組み込みます。
しかし、正直それでは不適切なパターンもあると思います。
- 自動運転車が速度出しすぎでスリップする。
- ロボットアームからものが落ちてしまう。
- etc
これらの事象ってオブジェクトが司っているものではありません。車がスリップするのもアームからものを落としてしまうのも、オブジェクトが持つ特性とは違って、摩擦などの自然法則です。
確かにスリップするのもものを落とすのも、そのオブジェクトの動作ではありますが、固有のものではなく自然法則であるため、他のオブジェクトにも起きることです。そんなものをオブジェクトに包むのは違和感しかありません。せめて関数として独立で持つべきでしょう。
僕が稚拙ながら勉強したところによると、関数化プログラミングは、思想として
- 動作の主体は関数である
- 内部状態を持たない(現実的に難しいことはありそう)
- 関数はできる限り小さくする
- 関数の内容は抽象化しておく
こんな特徴があるかなと思います。そしてこれを実行するにあたって
- 関数を引数にとる関数
- 関数を出力する関数
の存在も作られています。
ただこれだけでなんでもできるようには思えないので、個人的にはオブジェクト指向とうまく組み合わせてやるのが大事なのかなと思います。
MATLABにおける関数型っぽい書き方とは
上で列挙したことをMATLABでできるか考えてみました。
動作の主体が関数
これは普通にできますね。関数を作って呼ぶだけで大丈夫です。
内部状態を持たない
クラスにメソッドを作ったとしても、
function y = Method(self, x)
self.a = self.a + 1;
self.b = self.a + self.c;
self.c = self.c - 1;
y = self.a * x.^2 + self.b * x + self.c;
end
こんなのはダメです。self
の中身が書き換えられているのに出力していません。メンバ変数も大事なパラメータなのに、関数によって処理が隠蔽されてしまうので、他の処理を手前や後ろに差し込んだりしたらそれで誤った結果を出してしまうかもしれません。
また、
for idx = 1:n
output = FuncSomething(idx, ...);
end
idx
がループ内で変化している=内部状態の変化ということらしいです。もっというとループ内で更新されていく変数はもっとダメだそう。ここまでくると流石に厳しすぎんかという気がしますが、一旦飲み込んでおきましょう。
関数を小さくする
関数はシンプルに越したことはありません。オブジェクト指向だとしてもこれは大事でしょうね。
関数の内容を抽象化する
例えば数列の和を取る動作を作りたい時(sumでやれと言いたいですけど例なんで許して)
function y = sum(x)
y = 0;
for lx = x(:)' % 関数型ではインデックスよりも要素ループの方が良いとされている。
y = y + lx;
end
end
こうするのかなと思います。ただこれだと和を取ることしかできませんね。関数型の考え方ではこのコードのあらゆる計算を関数として捉えます。なのでy
の初期値0は外部から与えられるようにするべきだし、y + lx
の足し算も、任意の関数でできるようにするべきです。そうすれば似たような処理を何回も書かなくて良くなります。
function y = reduce(x, init_y, func)
y = init_y;
for lx = x(:)'
y = func(y, lx);
end
end
こうしておけば、関数ハンドルを使って
x = rand(100,1);
fhandle = @add; % ここを入れ替えるとmaxもsumもできる。
y = reduce(x, 1, fhandle);
function y = add(l, r)
y = l + r;
end
function y = greater(l, r)
if l > r
y = l;
else
y = r;
end
end
function y = reduce(x, init_y, func)
y = init_y;
for lx = x(:)'
y = func(y, lx);
end
end
と和の処理が記述でき、さらに初期値も変更できるしmaxやminやその他このreduceに当てはめられる関数であればなんでもできます。結果的にreduceは関数を引数にとる関数になっていますし、@add
は関数を返す関数ですね。
MATLABにできること
このように、MATLABでも一通りの関数型の思想を反映させられそうです。
MATLABっぽい書き方とは
研究者の方々がreduce
みたいな初歩的なものを自分で組むとは到底思いませんし、MATLABにはsum
もmax
もあるので、基本的にはそういう組み込み関数を使うでしょう。
また行列演算においてはかなり豊富な関数が用意されていますし、MATLABの得意分野です。組み込みの機能で実現できることまで関数型で実践する必要はないのかなと思います。
そして、制御系の方は特にかもしれませんが、オブジェクトでやるべき部分と関数型でやるべき部分を切り分けて、整理すべきかなと思います。そして何でもかんでも抽象化していると無駄な手間ってこともあるので、あまり気にしなくてもいいのかなぁとは思いますね。
ただ、純粋関数にこだわるのはいい考えだと思うので、できることならオブジェクトのメソッドも暗黙にメンバを更新せずに、ちゃんと出力するように組んでおくと、のちのテストもしやすいなと思います。でも出力が多すぎてめんどくさくなるのが嫌なところですね。
関数型のよくないところ
関数型のプログラミングのよくないところも上げておかねばなりません。
MATLABにおいて関数型を頑張った場合に一番良くない点は、ファイル数が爆増することです。
関数型はどうしても関数の数が多いので、その量は自ずと増えてしまいます。MATLABの場合それがファイル数に直結してしまうので、あまり抽象化を頑張りすぎるのもよくないのかなと思います。
実際に何か書いてみよう
※何かいい例があったら更新します。