Skip to content
関数型
マイコン老年の今時のプログラム技術
2020-07-16

普段の仕事で忙しくて、新技術をかまけていた頃に、関数型という話を一番最初に聞いたのはC#系のLinq( https://ja.wikipedia.org/wiki/%E7%B5%B1%E5%90%88%E8%A8%80%E8%AA%9E%E3%82%AF%E3%82%A8%E3%83%AA )でした。データ加工のための分岐やループの処理をSQL風の書き方で1行で書いてしまうという形。関数型とは言えないだろうが制御を条件にしてしまうという意味で、一番最初に感じを理解するにはLinqはとっつきやすい気がする。ただ今の関数型からは全然足りなかったとは思う。

値段のリストをすべて10円値上げする

int[] amountList = {100,110,95,120};
// えーっと、値段のすべてを取り出して、それらに10円を足して、それをリストにする
ArrayBuilder buf = new ArrayBuilder<Int>(); // まずリストの結果を集める場所を作る
for(int amount : amount) { // 値段のリストから1つずつ値段を取り出す
 int addedAmount = amount + 10; // 値段を+10 する
 buf.add(addedAmount); // 新しい値段を値段リストに入れる
} 
int[] ans = buf.toArray(); // 計算した結果をリストとしてまとめて取り出す
// ああ、仕事した。。。
int[] amountList = {100,110,95,120};
// えーっと、値段のすべてを取り出して、それらに10円を足して、それをリストにする
ArrayBuilder buf = new ArrayBuilder<Int>(); // まずリストの結果を集める場所を作る
for(int amount : amount) { // 値段のリストから1つずつ値段を取り出す
 int addedAmount = amount + 10; // 値段を+10 する
 buf.add(addedAmount); // 新しい値段を値段リストに入れる
} 
int[] ans = buf.toArray(); // 計算した結果をリストとしてまとめて取り出す
// ああ、仕事した。。。

手元で数字を入れた箱をあっちこっちに動かして、制御の分岐をあっちこっちに動かして、結果が出来ました。という感じの処理になる。メモリ管理が自動化されていない頃なら、さらに場所を作るときに元のリストの長さはいくつか調べて、その長さのメモリを確保して。。。とかも必要になるところです。これそのものは正しいですし、CPUはこんな感じで実際処理しています。

最初の仕様「値段のリストをすべて10円値上げする」を「値段のすべてを取り出して、それらに10円を足して、それをリストにする」という制御の仕様に変換しなおして、その処理を書く。

こういうのがどんどん重なっていくとそれなりにしんどくなってくる。それが従来の手続き型です。

val amountList = Seq(100,110,95,120)
// 値段のリストをすべて10円値上げする
val ans = amountList.map(_+10) // 1行の仕様=1行のソース
val amountList = Seq(100,110,95,120)
// 値段のリストをすべて10円値上げする
val ans = amountList.map(_+10) // 1行の仕様=1行のソース

関数型の場合、すべての仕事を変換と直列流れにするということだから、「値段のリストをすべて10円値上げする」→関数型『「値段のリスト」を「10円値上げしたリスト」に変換する』 となる。

制御っていらないじゃない という話にしなければならない。

では条件を少し変更して「120円以上のものだけ10円値上げする」であれば制御があるのではないか。でもここを少し複雑にしたとしても結局「120円以上」は変換する条件となり、条件が複雑になっただけ(マシン語にしたときそれはgo toが含まれる制御になるがそれはコンピュータの都合である)

// 値段のリストの120円以上を10円値上げする
val ans2 = amountList.map(a=>if (a >= 120) a+10 else a)
// 値段のリストの120円以上を10円値上げする
val ans2 = amountList.map(a=>if (a >= 120) a+10 else a)

ifが出てきたんだからこれ制御でしょ。とは思う。でもこれも条件を付けた変換である(と解釈する)

// amountが120以上ならamountをamount+10に変換する さもなくば amountに変換する
val addedAmount = if(amount > 120) amount+10 else amount
// amountが120以上ならamountをamount+10に変換する さもなくば amountに変換する
val addedAmount = if(amount > 120) amount+10 else amount

制御ではなくすべてを変換でまとめていく。

業務の仕様(ビジネスロジック)は概ね箇条書きで書く。箇条書きで書けるものは「データの変換」くらいには落とし込める(落とし込めない場合、論理的に仕様が完成していないことが多い) 。手続き型の頃はこの仕様をさらに制御の仕様に書き直す必要があった。しかし関数型の場合は制御の仕様に書き換えなくてもよい。

極端な話「仕様書の1行が、プログラムソースの1行に合致する」くらいまで行くことを狙っているのである。

関数型が話題になった頃のプログラム言語は、初期化やコンピュータ制御の都合をソースから極力排除する仕組みになっている。これも仕様書にない(省略された)記述をプログラムソースからなくすためである。

ここから先のプログラムは「データを別データに変換する」の作業を延々書き連ねていくことになる。集合の数学的変換演算が得意な人の独壇場ですね。

行が減る代わりに1行の中に表現されるものが多くなる(1行が長くなる)。けど仕様書とは乖離が少なくなる。1つの処理がやっていることをサブルーチン/関数みたいに見えないボックスにしてしまうのではなく、確かに1行でやっていることが書かれていて確認できる。なによりディスプレイの1画面で見通せることが多くなる。

レガシープログラマとしては便利だとはわかるものの、進んでいるうちに不安になる。

そこは納得いくまでライブラリのソース,デコンパイルソース,JVMとにらめっこすればよいと思う。便利さとケンカしても勝ち目はない。