私は文系卒のクソザコWEB系エンジニア1です。私と同じく文系出身で「関数型言語何もわからん……」となった方のために、私が関数型言語を学ぶ中で疑問に感じた内容にフォーカスしてまとめることにしました。関数型言語初心者ですので誤った記載があるかもしれませんが、その際はコメント欄等で温かなマサカリを投げつけてくだされば嬉しいです。
そもそも「関数型言語」「関数型プログラミング」って?
いろいろな記事を当たってみましたが、こちらの記事の
「関数型プログラミングの定義」などという存在しないものを誰かにねだらないほうがいいと思います。
という表現が一番しっくりときました。つまり、よくわかりません。
なるほど。では、どんな言語が「関数型言語」を名乗っているの?
確かに、具体例を挙げることなら簡単です。以下の表の通りです2:
大分類 | 小分類 | 例 |
---|---|---|
関数型言語 | 純粋関数型言語 | Haskell, Elm |
非純粋関数型言語 | Scala, F# | |
手続き型言語 | 命令型言語 | COBOL, C |
オブジェクト指向言語 | C++, Java |
Scala は関数型言語なの?
非純粋関数型言語の側面を含むマルチパラダイム言語です。Java より並列処理が得意ということもあり、Gatling という負荷試験ツールに採用されたりしています。3 一方既存の Java 資産を活用したり、手続き型の書き方をした方がいいと思われる場合には、手続き型のような書き方をすることもできます。
Rust は関数型言語なの?
非純粋関数型言語の側面を含むマルチパラダイム言語です。Rust はデータの所有権と借用の概念を持っており、データの競合をコンパイル時に防ぐことができます。また、変数はデフォルトでイミュータブル(不変)になっているため、副作用(後述)の発生が局所化されています。
関数型言語の特徴として挙げられがちなポイントは?
主観ですが、以下の二点について言及されることが多かったように思います:
- 参照透過性と副作用の抑制
- 高階関数とカリー化・部分適用
参照透過性って? 副作用がないと何が嬉しいの?
参照透過性とは、変数の値がイミュータブル(不変)である性質のことです。変数の示す値が途中で変化せず、いつ参照しても値が同じであるため、このように呼ばれます。変数がイミュータブルであると、コード全体の見通しが良くなり、ヒューマンエラーの発生を抑止することができます4。参照透過である変数は、その実体と置換しても意味が変化しません。
参照透過性は戻り値が引数にのみ依存して決まる関数にも付与されます。つまり、環境5からの影響を受けず、環境に対して影響を与えない関数です。このとき、関数に同じ引数を与えると、必ず同じ戻り値を返します。参照透過である関数は、引数が同じであれば、その戻り値に置換しても意味が変化しません。
副作用とは、その処理の実行前後で環境に外部から観測可能な変化を与えてしまうことです。たとえばミュータブル(可変)な変数へ値を再代入する操作は外部から観測可能な変化ですので、副作用を発生させる操作であると言えるでしょう。このような副作用を伴う処理にはバグを埋め込みやすいという意見があります6。
また、副作用が無いコードには副作用があるコードと比較して処理の並列化が行いやすいというメリットがあります。環境に対する変化を発生させないため、マルチスレッド化を施してもデータ競合関連のトラブル(デッドロック等)を発生させにくいのです。
副作用を発生させない(状態を持たない、安全な)関数を純粋関数と呼び、そうでない(状態を持つ、安全でない)関数を非純粋関数と呼びます。また、すべての変数がイミュータブルな関数型言語を純粋関数型言語と呼び、そうでないものを非純粋関数型言語と呼びます7。
高階関数って? カリー化・部分適用って?
高階関数とは引数として関数を受け取ったり、戻り値として関数を返したりする関数のことです。JavaScript だと Array.prototype.forEach()
あたりが相当します。
カリー化とは、二つ以上の引数を取る関数を、一つの引数だけを取る関数の組み合わせに書き換えることです。次に挙げる例のように、高階関数を用いて実現されます:
// 2つの数字を引数に取り、双方を比べて大きな方を返却する関数:
const max = (x, y) => {
return x > y ? x : y
}
max(1, 2) // 2
// カリー化を施します:
const max = x => {
return y => {
return x > y ? x : y
}
}
// あるいはもっと単純に:
const max = x => y =>{
return x > y ? x : y
}
max(1)(2) // 2
部分適用とは、複数の引数を受け取る関数を、より少ない数の引数を受け取る関数に変換することです。引数の値を固定していると言っても良いでしょう。カリー化が施された関数は、簡単に部分適用を行うことができます:
// 「1つの数字を引数に取り、1と比べて大きな方を返却する関数」に変換します:
const max1 = max(1)
max1(2) // 2
部分適用により関数のインターフェース(パラメータの個数)をカスタマイズできるため、関数⇔関数の情報の受け渡しをスムーズに行えます。関数型言語ではシェルにおけるパイプラインのような感覚で関数を合成するため、部分適用によるインターフェースの変換が便利な手法になるのです8。
なんで「代入する(assign)」じゃなくて「束縛する(bind)」っていうの?
関数型言語のドキュメントでは「変数に値を代入する」ではなく「変数に実体9を束縛する」と表現されていることがあります。これは変数がイミュータブルであり、宣言と同時に特定の実体で固定されるためです。
Vue.js の関数型コンポーネントと関係あるの?
Vue.js のコンポーネントは data
を持っています。つまり、状態を保持しているということです。一方、関数型コンポーネントには data
がありません。自分自身では状態を持たず、親コンポーネントの状態を props
で渡すことによって動作します。つまり関数型コンポーネントは引数として「状態」を受け取り、戻り値として VNode
を返却する「ただの関数( just functions )」であると言えます。
関数型コンポーネントの主なメリットは描画コストの低減(=レンダリング高速化)ですが、状態が親コンポーネント側に集中する設計になることで、結果的にコンポーネント内の見通しがよくなる場合もあります。なるほど、参照透過性に関する特長と通じるものがありますね。
Redux や Vuex と関係あるの?
Redux と Vuex は純粋関数型言語である Elm から大きな影響を受けています。たとえば、Redux の Reducer や Vuex の Mutation は純粋関数でなければならないとされています。これは Reducer(変化させる + er)という名前が示すように、引数として受け取った state を変化させて返却する以外のことをしてはならないということです。
ちなみに、Redux の middleware の定義にはカリー化を用います:
const middleware = store => next => action => {
next(action);
}
関数型言語は銀の弾丸なの?
関数型言語は銀の弾丸ではありませんが、関数型プログラミングの考え方が有用な場面はあると考えます。上の二項目がまさしくその例でしょう。関数型プログラミングの特性を十全に活かすことはできないかもしれませんが、その知見を活用することは難しくありません。
オブジェクト指向のほうがわかりやすくない?
まず、オブジェクト指向と関数型プログラミングそのものは互いに相反しあうものではないという前提を共有したいと思います。そうでなければ、Scala や Rust のようなマルチパラダイム言語は存在し得ないし、Java にラムダ式が取り入れられることもなかったでしょう。Redux や Vuex に Elm の思想が取り入れられることも無かったかもしれません。しかし、関数型プログラミングの特性を活かしきるためには純粋関数型言語を選ばざるを得ず、そういう意味で純粋関数型言語とオブジェクト指向言語は対立軸にあります。
私はオブジェクト指向と西洋哲学には共通した部分があると認識しています10。プラトンの「イデア論」やアリストテレスの「形相と質料」などは特にわかりやすい例です。すなわち、人間が世界を理解するために生み出され、古代から連綿と受け継がれてきた思考方法を、これもまた世界の一部であるプログラミングに適用しているわけですから、わかりやすくて当然です。また、プログラミング初学者でも義務教育等を通じて西洋哲学にはどっぷり影響を受けているはずであり、なじみやすい概念もまたオブジェクト指向であると考えられます。
……というのは私が文系人間だからかもしれませんが。
参考リンク
参考文献が多すぎてどちらが本文かわからなくなったので折り畳みました。インターネットは偉大。
関数型言語について
Rust について
Scala について
参照透過性と副作用について
- 純粋関数型JavaScriptのつくりかた
- 純粋関数型言語と参照透過性
- 「参照透過である」とは、何から何への参照がどういう条件を満たすことを言うのか
- プログラミングで言う「副作用」とは何ですか?副作用があるのは良くないとかHaskellなどの関数型言語は「IOモナド (IO monad) 」で副作用なしにプログラムできると聞きますがそれは何ですか?
カリー化と部分適用について
- 食べられないほうのカリー化入門
- 高階関数、カリー化、部分適用
- カリー化と部分適用
- curry化と部分適用の違いを自分なりにまとめてみる
- 部分適用っておいしいの? on Arduino
- JavaScript/TypeScript でお手軽パイプライン
束縛という用語について
Vue.js について
Redux / Vuex について
- Redux 入門 〜Reduxの基礎を理解する〜
- Redux 基礎:Middleware 編
- 関数型プログラミングとRedux | Reduxが必要なとき/不要なとき(翻訳)
- Elmの型で読むReduxやVuexのアーキテクチャ
- Reduxが分からない人のためにReduxを概念から説明してみる
オブジェクト指向と西洋哲学について
-
数学については圏論はおろか数2Bに毛が生えた所までしか知らない身分なのですが、並列処理が求められる場面でScalaを利用することになったり、高速性が求められる場面でRustを学ぶ機会があったりして、関数型言語あるいは関数型プログラミングという概念に興味を持ちました。 ↩
-
なお、オブジェクト指向言語にも(非純粋な)関数型プログラミングの機能を含んでいる言語は多数存在しますし、非手続き型言語には関数型言語以外の枠組みも存在しますが、ここでは簡単のために触れないことにします。 ↩
-
Scalaの「並列コレクション」を利用するとコレクションに対する処理を簡単にマルチスレッド化することができます。また、AkkaというScala製のアクタベースの並列実行ライブラリが組み込まれています(かつてはオリジナルのアクタベース並列実行機能を持っていましたが、現在は非推奨になっています)。 ↩
-
このあたりは関数型言語特有というより変数のミュータビリティに関する議論です。JavaScriptでもvarよりlet、letよりconstを使っていこうという風潮がありますよね。 ↩
-
スコープ外の変数など ↩
-
あなたがJavaScript経験者ならミュータブルなグローバル変数の変更・参照タイミングにまつわる失敗経験を思い浮かべて頂くととわかりやすいかもしれません。変数への再代入を許可すると、そのようなデータの競合に伴うトラブルの原因になるかもしれません。俺のサイドエフェクトもそう言っています。 ↩
-
純粋関数型言語でもI/Oのようにアクションが求められる場面はありますが、この場合はモナドを利用します。 ↩
-
JavaScriptでもパイプライン演算子の導入が検討されています(現在 Stage 1 Draft です)。また、カリー化を用いてお手軽にパイプラインを実現する方法についての記事もあります。ド文系なので関数合成について具体的な例で理解できて助かりました。 ↩
-
ここで値ではなく実体と表現しているのは、実体として値以外のもの(式など)を束縛できる場合もあるためです。式が遅延評価される言語の場合、宣言と値の固定が同時である保証はありません。 ↩