純粋関数型言語、副作用、参照透明、モナド、……、あぁ、某都知事の会見を見ているようだ。
純粋関数
意味
関数がf(x)が引数以外の外部の変数に依存せず、戻り値を返す以外に外部に影響を与えないとき、f(x)は純粋関数であるという。
純粋というのは数学関数と対比している。
用語の問題点
純粋ってなんだよ
つまり我々オブジェクト指向屋が扱う関数は不純ってことなんだろうけど、あんまりいい気分しなくない?
単純関数とかで良かったじゃん。
このpureにそういう意味はありませんと言われても遺伝子の優性・劣性と似た議論が始まってしまう。
別に英語でpureだからって律儀に訳す必要はない。
我々電気系の遠い先輩は、convolution integralに畳み込み積分という訳を与えたりしている。
副作用
意味
ある関数に副作用があるとは、その関数を実行したとき、関数の外部に影響を与えること。
例:ファイル書き込み処理を含む関数は副作用がある。
例:メンバ変数に書き込みを行う関数は副作用がある。
転じて、依存関係が多いこと。
用語の問題点
副作用という言葉は主観的である
そもそも副作用という言葉は、薬に使われることが一般的である。
本来の作用とは異なる作用が出てしまうことを副作用というのだ。
……本当にそうだろうか。
ここに、正露丸という薬がある。これは腸の調子を整える薬だ。
ところで、実は正露丸に含まれてる一部の物質には虫歯痛を抑える効果がある。
この効果は、副作用だろうか。
定義に従えば、この薬は腸の調子を整える薬なのだから、副作用なはずだ。
しかし、薬のパッケージを見ると、「効能」=「作用」の方に「虫歯痛(に効く)」と書かれている。
何故か、答えは簡単。これは有用な作用だからだ。
副作用という言葉は「人体に程度の差はあれど悪い影響を与える」作用のことを指して使われる。
プログラミング言語においても同様である。
副作用というのは外部に影響を与える関数を悪とする考え……、つまり「純粋関数型」目線の用語なのだ。
だから、これをオブジェクト指向の考え方や、もっと根っこ(速度を求めて書くC++とか)の考え方に持ち込むと、おかしくなる。
saveXXX関数がファイルに書き込むこと、setXXXメソッドがXXXのメンバ変数を書き換えること。これはその世界においては間違いなく「作用」なのである。
わかりやすいのがこの例( https://qiita.com/suzuki-hoge/items/bad43630ad1ad723ca4a )。
classが存在するオブジェクト指向のコードに純粋関数型由来の副作用を持ち込むことで謎のコードになっている(記事を書いた当人もそれを理解しているようだし、批判する意図はない)。
関数外干渉とか言う名前にしておけば、客観的な名前だし良いと思う。
本質を見失いがちである
副作用を排除する理由に「単体テスト」のしやすさが挙げられる。そもそも関数を単体でテストしようなんて発想が「純粋関数型」なのだが、そこはあまり気にしない。
ところで、先程例に上げた、
例:ファイル書き込み処理を含む関数は副作用がある。
例:メンバ変数に書き込みを行う関数は副作用である。
この2つ、同列に語ってよいのだろうか。
僕はそうは思わない、プログラム内で済んでいるメンバ変数と、どうあがいてもプログラムの外部に出力するIOを同列に語れるはずがない。
もう一度この例( https://qiita.com/suzuki-hoge/items/bad43630ad1ad723ca4a )を見てみよう。ここには、以下のコードが単体テストできないという例が挙げられている。
public void greet() {
System.out.println(
getFirstMessage() + forFullName() + getForGender()
);
}
これの問題は、副作用があることなのだろうか。確かに副作用の定義によればそのとおりである。
しかし僕は、例えばこれがsetNameというメソッドで、メンバ変数に代入する処理なら問題ないと思う。カプセル内で完結しているからだ。
このコードの問題の本質は外部からいじれないIOの処理があることである。普通にコードとしての汎用性がないのだ。依存性の注入……とはちょっと違うが、似たような感じだ。そんなシンプルなことを説明するのに、副作用なんて小難しい言葉を持ち出す必要があるのだろうか。二次関数の最大最小問題を、微分を使って解かれている気分だ。
public void getGreet() {
return getFirstMessage() + forFullName() + getForGender();
}
参照透過
意味
(一例)
関数がf(x)がxにのみ依存するときf(x)は参照透過であるという。
当たり前だろと思うかもしれないがそれは数学の話で、プログラミング言語の関数は普通に引数以外の外部の変数を参照できる。
参考:https://qiita.com/sasanquaneuf/items/3df1001a027e868e9e0e
用語の問題点
定義が一意に定まってない
上の定義も一説によるもの。Wikipediaだと別の定義が与えられている。
ある式が参照透過であるとは、その式をその式の値に置き換えてもプログラムの振る舞いが変わらない(言い換えれば、同じ入力に対して同じ作用と同じ出力とを持つプログラムになる)ことを言う。
みんな大好きハスケル子ちゃんがでてくるあの記事でも、別の定義が与えられている。
ハスケル子「でも純粋関数型言語では」
ハスケル子「再代入という概念がないので」
ハスケル子「一度定義した値は不変です」
ワイ「ほええ」
ハスケル子「つまり」
ハスケル子「一度定義したら、常にx === 3です」
参考:https://qiita.com/Yametaro/items/1de3c2b76b8a4dc2d30d
つまりイミュータブル=参照透過としている。
えぇ……。
名前が性質を示すものである
ではなぜこんな定義がバラバラなのか、それは名前が悪いからである。
「参照透過」は性質なのに、具体的な定義が必要な物の名前なのがよくない。
四角形に「三角形2つ組み合わせて出来てる型」と名前をつけているようなものである。
三角形2つに重なりがあっても四角形なのだろうか。
名前が性質からつけられてしまうと必要条件、十分条件がよくわかんなくなってしまう。
Wikipediaに述べた定義なら純粋関数をそのまま使えばいいし、ハスケル子ちゃんの定義ならイミュータブル=不変とかそんな名前をつければよいのである。
モナド
意味
ここで語るには余白が狭すぎる。
問題点(?)
解説を読んでも謎
はじめに断っておくが、これは僕がわからないゆえの私怨である。
かなり身勝手なことが書いてあるかもしれないので予め了承した上で読んで欲しい。
僕がモナドを勉強して分かったのは、
- 定義がとても厳密
- 抽象的
ということである。
モナドの利点は、メソッドチェーンが出来るとか大体そんなところである。
ただそれだったらモナドである必要がないのである。
JavascriptならjQueryが記憶に新しいか。あれはモナドではない。
モナドはもっと条件が厳しい。Javascriptで配列がギリギリモナドたり得ないみたいなことを説明している記事を見るとわかりやすいかもしれない。
モナドはあまりにも厳密すぎて柔軟性を下げているのではないかなと僕には思われる。
あと、あまりにもこの世に存在するモナドの説明が抽象的すぎる。数学から来た概念なので仕方ないのかもしれないが。
抽象と具体は、具体が先にある。具体を見てから「これを抽象化してみよう」となるのである。普通は。数学科とかのやばいやつは違うのかもしれないけど。
だから、その前提をカットして抽象部分だけ説明されても何も分からんとなってしまうのである。
機械学習とかでよくn次元ベクトルのユークリッド距離を計算することがあるが、なぜこれがすんなり理解できるか。4次元以上なんて人間は認識できないのに。
これは2次元・3次元という具体例が存在するからである。これによって帰納的に4次元以上が理解出来るのである。この帰納は数学的には誤りで、ちゃんとn次元で証明するなりしなければならないが、その証明を以てn次元のユークリッド距離を理解している人は少ないはずだ。
多分、世の中の数学嫌い学生は、抽象的議論ばっかりされて嫌気が差しちゃったのではないかなと思う。
お笑い芸人の陣内智則のネタに「sin、cos、何に使うん」というようなやつがあったりする。
数学好きなら、いくらでも説明できるはずだ。
ぜひ、モナドを理解している人には、その根底にある具体もカットせず説明していただけるととてもありがたい。