はじめに
関数型プログラミング、難しい!
と思ったことはありませんか。僕はあります。
関数型プログラミングは最近注目度が上がっているようで、「オブジェクト指向 vs 関数型」みたいな記事もありますし、Go 言語や Rust はクラス構文をサポートしてなかったりで、ワードを耳にする人も多いのではないのでしょうか。
ところが関数型プログラミングに関する情報は意外にも少なく、かつ難しい情報が多い気がします。
ここでは関数型プログラミングについて 1mm も知らない人をメインに、基礎概念から説明をしていこうと思います。
かくいう筆者も関数型プログラミングに造詣が深いわけではないので、間違っている箇所等あれば教えてくださればと思います。
そもそも関数とは?
関数型プログラミングと言うのですから、まずは関数って何だというところから入りましょう。
皆さん中 1 のときに関数とはこういうものだと教わったはずです。
xの値が決まると、それに対応してyの値がただ一つに定まるとき、yはxの関数である
いやそれはそうだけど...って感じですよね。
まあまずは関数ってこういうもんだったよね、という復習です。
重要視していること
関数型プログラミングでは重要視されている概念がいくつかあります。
- 式が主役
- 第一級関数
- 不変性(immutable)
- 副作用を排除
他にもあるとは思いますが、ここではこの 4 つを紹介します。
式が主役
プログラミングでは式と文が区別されています。(区別の仕方は言語によると思います)
- 文: if, while, for など。手続きを表すもの
- 式: 値、変数、演算子の組み合わせ。式は評価された値をもつ
式は評価された値を持つ...
はぁ?どういう意味だよ、って感じですね。
まあ深く考える必要はないと思います。
超簡単な例を考えてみましょう。
let x = 'hoge' + 'hoge';
if (hoge === 'hoge') {
console.log('this is hoge');
}
expression.js が式の一例です。statement.js が文の例です。
先程、式は評価された値を持つと書きました。逆にいうと文は値を持たないということです。
それを踏まえて以下のコードを見てみましょう。
let x = if (baz === 'baz');
ありえないですよね。
これは if という文が値を持たないからです。だから変数という値に代入ができないのです。
対して expression.js の例では x の値は'hogehoge'であると分かりますよね。
要するに式というのは結果として値を持つもので、文は持たないというわけです。
*言語によって解釈が違ったりします。Rust では if が式として扱われています。
関数型プログラミングでは、この式をメインにしてプログラムを組み立てます。
第一級関数
第一級関数とはなんでしょうか。簡単に言うと、変数に関数をいれられることです。
???って感じだと思いますが、わかりやすいのはコールバックを使った例です。
let button = document.getElementById('button');
button.addEventListener('click', function() {
console.log('hello from button!');
})
addEventListener の第二引数は何でしょうか、関数ですよね。
引数といっても、関数に渡す変数のことなのですから実態は変数です。
変数に関数が入るといわれると頭が混乱しそうになりますが、皆さん何気にこういう書き方をしてきたと思います。
javascript において変数の中に関数が入るというのは、当たり前のように使われています。
関数型プログラミングを実現するには、言語側が第一級関数をサポートしている必要があります。
不変性(immutable)
関数型プログラミングでは変数の破壊をもっとも嫌います。破壊とはすなわち再代入といって差し支えないでしょう。
故によくある以下のようなコードはご法度です。
let array = [
'takashi',
'ken',
'honda'
];
let retval = [];
for(let i = 0; i < array.length; i++) {
retval.push('my name is ' + array[i]);
}
console.log(retval);
このコードにはご法度な箇所が 2 つあります。
1つ目は配列に関連する箇所です。コメントを付けています。
let array = [
'takashi',
'ken',
'honda'
];
let retval = []; // BAD!
for(let i = 0; i < array.length; i++) {
retval.push('my name is ' + array[i]); // BAD!
}
console.log(retval);
何がダメかというと、先程も言いましたが再代入です。
少し話が逸れますが、push メソッドは破壊的メソッドと言われるもので、元の配列に変更を与えるタイプのメソッドです。
今回の場合はretval
がそれに当たります。
現にretval
の値は、空配列から'my name is takashi'などが入った配列に変わっていますよね。
関数型プログラミングでは不変性を重視するので、変数が書き換わる・再代入されることはご法度です。
故に、push のような破壊的メソッドを使って変数を書き換えるのはダメだというわけです。
もう一つダメな箇所があって、これは少し意外かもしれません。これもコメントを付けています。
let array = [
'takashi',
'ken',
'honda'
];
let retval = [];
for(let i = 0; i < array.length; i++) { // BAD!
retval.push('my name is ' + array[i]);
}
console.log(retval);
問題は for ループの部分ですね。
for ループの処理をおさらいすると、
- 変数 i に 0 が代入される
- i < array.length が true かどうか判定する
- true なら処理を実行する
- 処理が終わったら、変数 i をインクリメントする
でしたね。
問題は変数 i に繰り返し再代入が行われていることです。これがご法度な箇所です。
繰り返しますが、関数型プログラミングでは変数が書き換わる・再代入されることはご法度です。
たとえ for ループだとしても許されないのです。厳しい...。
これらの問題を回避するには例えば以下のようなコードを使います。
const array = [
'takashi',
'ken',
'honda'
];
const bioArray = array.map((name) => 'my name is' + name);
console.log(bioArray);
map は非破壊的メソッドで、もとの変数に変更を与えません。
かつ変数 i を定義したり、インクリメントしたりする必要もありません。
実行結果としては変わりませんが、こちらのコードが関数型的な書き方になっていると思います。
副作用の排除
副作用とはなんでしょうか?
薬でもないのにプログラミングに副作用があるんでしょうか?
先程、不変性が重視されるといいましたが、これにあまりに忠実に守りすぎると不都合が起こります。
例えば、コマンドラインツールを js でかつ、関数型プログラミング的に書いて作ってみたいとします。
コマンドラインツールでは当然、標準出力に文字なりなんなりを出力しないと話になりません。
大分話がもとに戻りますが、そもそもの関数の定義を冒頭で紹介したと思います。
xの値が決まると、それに対応してyの値がただ一つに定まるとき、yはxの関数である
これはこういう風に表せますよね。
y = f(x)
この関数の x に例えば 1 を代入したら、それに対応した y の値(例えば 2)が返ってきます。
プログラム的な解釈をすると、この関数の引数 x に 1 を代入すると、2 が返されると解釈できます。
すなわちこういうことですね。
function multiply(x) {
const y = x * 2;
return y;
}
もっと言うとプログラミングにおいて関数とはこのように言えるはずです。
関数とは引数に対応した、ただ一つの値を返すものである。
こういう性質を完全に守っている関数を、純粋関数といいます。
純粋関数を使うと保守性やコードの見通しが良くなりますし、より関数型的な書き方になります。
しかし純粋関数のみでプログラムを記述するのはまず無理です。
先程のコマンドラインツールの話に戻ります。
コマンドラインから引数などを受け取って内部処理を書いていく、これは純粋関数だけでも書ききれるかもしれません。
しかし困るのは出力をするときです。
標準出力へ文字列を出力する関数を書いたとしても、標準出力は別に返り値を返してくれません。
当然ですよね。出力をしたらそれで終わりな訳で、返すものなど何もないのですから。
すなわち、標準出力へ何かを出力する関数は必然的に非純粋関数にならざるを得ない、と言えるでしょう。
純粋関数では「ある値に対応した値を返す」ことしかできないので、関数自身より外にある変数や関数などなどに作用を与えることができません。
ですからプログラムを組んでいると、今回の標準出力の件のように、必然的に非純粋関数を使う場面がでてくるのです。
このように非純粋関数を使うことで発生する、外部への影響のことを副作用といいます。
しかし関数型プログラミングにおいて、副作用はあまり良くないものとされています。なぜなら副作用は不変性を壊すものだからです。
とはいえ非純粋関数を使わないとプログラムが成り立たないので、関数型プログラミングでは純粋関数を極力使うようにして、副作用は最小に抑えるべきだとされています。
最後に
関数型プログラミングを理解するにはワードと基礎概念の理解が必須になると思います。
あとは、数学的な考え方とプログラミングを結びつけることでしょうか。
関数型プログラミングは数学の考え方の影響を多く受けているので、数学の考え方とプログラムの考え方が乖離していては、根本的な理解が難しいと思います。
特に OOP に慣れ親しんだ人からすると、根本的に考え方が違うので理解が難しいかもしれません。
関数型プログラミングを理解するには、どうしても慣れが必要になると思いますので、焦らず理解を進めていくのがいいと思います。
筆者も関数型プログラミングを学び始めた頃は、第一級関数が理解できずに JavaScript がキライになりました。
それでも徐々に感覚を掴み始めて、今では特に第一級関数に対して嫌悪感は抱かなくなりました。
じゃあ今は好きなのかと言われるとそうでもないんですがね...。
とにかく関数型プログラミングを理解するには、どうしても時間がかかるということです。
皆さんの学習において、この記事が少しでも理解を助けるものになれば幸いです。