「関数型プログラミング」の版間の差分
編集の要約なし |
削除依頼を追加 |
||
(8人の利用者による、間の185版が非表示) | |||
1行目: | 1行目: | ||
<!--削除についての議論が終了するまで、下記のメッセージ部分は除去しないでください。もしあなたがこのテンプレートを除去した場合、差し戻されます。またページが保護されることもあります。--> |
|||
{{Sakujo/本体|2021年2月24日|関数型言語}} |
|||
<!-- 削除についての議論が終了するまで、上記部分は削除しないでください。 --> |
|||
{{独自研究|date=2014年4月}} |
{{独自研究|date=2014年4月}} |
||
{{プログラミング言語|index=かんすうかたけんこ}} |
|||
'''関数型言語'''(かんすうがたげんご、{{lang-en-short|functional language}}、関数型プログラミング言語とも)は、関数型プログラミングを基本スタイルとする[[プログラミング言語]]の総称である{{efn|{{lang-en-short|functional programming language}}}}。 |
|||
[[ファイル:Orange_lambda.svg|代替文=|境界|右|フレームなし|209x209px]] |
|||
== 関数型プログラミング == |
|||
{{プログラミング・パラダイム}} |
|||
広く認められた関数型プログラミングの正確な定義は存在しないが、関数型プログラミングと呼ばれるパラダイムは[[命令型プログラミング]]と比較してプログラムに対する見方が次のように異なる<ref name="faq">{{cite web|url=http://www.cs.nott.ac.uk/~gmh/faq.html|title=Frequently Asked Questions for comp.lang.functional|accessdate=May 14, 2015}}</ref>。 |
|||
'''関数型言語'''({{lang-en-short|''functional language''}})は、'''関数型プログラミング'''のスタイルまたは[[プログラミングパラダイム|パラダイム]]を扱う[[プログラミング言語]]の総称である。関数型プログラミングは関数定義と関数適用を基礎にした[[宣言型プログラミング]]の一形態であり、関数は[[引数]]への適用から先行式の評価を後続式の適用につなげて終端の[[評価戦略|評価]]を導き出す[[式 (プログラミング)|式]]の[[ツリー構造]]として定義される。式の評価に伴う[[副作用 (プログラム)|副作用]]の発生には大きな注意が払われる。関数は引数ないし返り値として渡せる[[第一級オブジェクト]]として扱われる。 |
|||
*[[命令型プログラミング]]: プログラムは計算機の内部状態を変更する命令実行の列<ref>計算モデル1 状態モデル. 計算とは、計算機の内部状態を変えてゆくもの。(中略) 状態モデルに基づくプログラミング言語. 命令型言語. (中略) 状態を変えるための命令手順書の形式. [http://nous.web.nitech.ac.jp/individual/inuzuka/lecture/PLT/PLT07/ 犬塚信博 (2007)「プログラミング言語論 第1回 イントロダクション」名古屋工業大学]</ref> |
|||
* 関数型プログラミング: プログラムは関数とその適用の組み合わせ |
|||
関数型プログラミングは[[数理論理学]]と代数系をルーツにし、[[ラムダ計算]]と[[コンビネータ論理]]を幹にして構築され、[[LISP]]言語が実装面の先駆になっている。関数の数学的な純粋性を指向したものは純粋関数型言語と個別に定義されている。[[命令型プログラミング]]言語では単に有用な構文スタイルとして扱われている事が多い。[[高階関数]]、[[第一級関数]]、[[カリー化]]、[[クロージャ]]、[[継続]]、[[イテレータ]]、[[ジェネレータ (プログラミング)|ジェネレータ]]、[[再帰]]、[[型推論]]、[[パターンマッチング]]、[[評価戦略|名前渡し]]、[[遅延評価]]、[[Cons (Lisp)|コンス]]、[[代数的データ型]]、[[ポリモーフィズム|型の多相]]、[[イミュータブル]]、[[モナド (プログラミング)|モナド]]などが{{誰範囲|date=2020年5月|関数型プログラミングの様式項目として挙げられる}}。 |
|||
すなわち[[命令型プログラミング]]は計算機(あるいは[[CPU]])の状態を変える命令をプログラムとして書くという見方を基礎としており、これはその発祥が計算機の命令 (instruction/command) や構造に密接にかかわっていることによる。一方、関数型プログラミングは「計算とは何か」という数学の理論を基礎にしており、関数型プログラミングがもつ[[計算モデル]]は'''関数モデル'''である<ref>計算モデル2 関数モデル. (中略) 関数モデルに基づくプログラミング言語. 関数型言語. Lisp [http://nous.web.nitech.ac.jp/individual/inuzuka/lecture/PLT/PLT07/ 犬塚信博 (2007)「プログラミング言語論 第1回 イントロダクション」名古屋工業大学]</ref>。 |
|||
== 一般的な関数型スタイル == |
|||
たとえば、1 から 10 までの整数を足し合わせるプログラムを考える{{efn|本来は[[等差数列]]の和の公式を用いて定数時間で問題を解く方法が最適解だが、ここではプログラミングスタイルの比較のため数値計算的手法を用いる。}}。[[命令型プログラミング]]では以下のように[[ループ (プログラミング)|ループ]]文を使って変数に数値を足していく(計算機の状態を書き換える)命令を繰り返し実行するという形を取る。 |
|||
最も身近な関数型プログラミングとは、データ集合に対する反復作用であり、リスト処理などと呼ばれているものである。手続き型言語やオブジェクト指向言語において、いわゆる関数型と呼ばれる構文が多用されるのもリスト処理の分野である。関数型思想の原点は「[[LISP]]」であるが、その実用性を知らしめた最初の言語は[[表計算ソフト|表計算]]処理に効率性を発揮した「[[APL]]」であった。リストを走査して各要素を順々に取り出すプロセスと、各要素に作用するプロセスを別々の関数にまとめて、前者に後者を渡すようにした仕組みが一般に言われる関数型の基本例である。他の関数を引数として受け取る事ができる関数は'''[[高階関数]]'''と呼ばれ、引数として渡す事ができる関数は'''[[第一級関数]]'''と呼ばれる。この二つの機能が関数型の支柱である。そのようなスタイルとしての意味での関数型プログラミングにおける代表的な機能または構文には以下のようなものが挙げられる。 |
|||
'''ラムダ式''' / '''[[無名関数]]''' / '''[[クロージャ]]''' |
|||
* [[Pascal]]による例: |
|||
: ラムダ式と無名関数は同じものである。無名関数は<code>引数→式</code>(例:<code>x→x+1</code>)の形式で表現され、与えられた引数を加工して結果値を返す働きをする。高階関数への引数として使われることが多い。無名関数の式内に外部データを含んだものはクロージャと呼ばれる。クロージャの結果値はその時の外部データの状態に左右される事になる。 |
|||
<source lang="pascal"> |
|||
program test; |
|||
var total, i : Integer; |
|||
begin |
|||
total := 0; |
|||
for i := 1 to 10 do |
|||
total := total + i; |
|||
WriteLn(total) |
|||
end. |
|||
</source> |
|||
'''map''' / '''filter''' / '''reduce''' |
|||
一方、関数型プログラミングでは、繰り返しには一時変数およびループを使わず、[[サブルーチン|関数]]の[[再帰呼び出し]]を使う。 |
|||
: リスト処理用の高階関数であり、対象リストと無名関数を引数にする。mapはリスト内の各要素を無名関数の結果値に置き換える高階関数である。filterはリスト内の各要素を無名関数の真偽判定値で真なら抽出し、その抽出要素のリストを生成する高階関数である。reduceはリスト内の各要素の総和の結果値を生成する高階関数である。reduceの無名関数は前要素までの総和と現要素の二引数を取る。総和の和は和に限らず無名関数内で好きな計算にできる。 |
|||
'''名前渡し''' / '''[[遅延評価]]''' |
|||
* [[F Sharp|F#]]による例: |
|||
: 引数を当てはめた無名関数(コードブロックまたはプロセス)を未計算のまま高階関数に渡せる仕組みを名前渡しという。予め組み立てた想定プロセスを全て高階関数に渡しておき、その高階関数側で必要になったプロセス結果値だけを求めるようにして計算量を減らすのが主な用途になる。引数確定時とは別タイミングで計算することを遅延評価という。遅延評価では高階関数側で無名関数の確定引数を設定し直したり、[[クロージャ]]ならばその時の計算タイミングの外部データ状態に従った結果値を得ることが可能になる。 |
|||
<source lang="fsharp"> |
|||
printfn "%d" (let rec sum x = if x > 0 then x + sum (x - 1) else 0 |
|||
sum 10) |
|||
</source> |
|||
<!-- |
|||
<source lang="haskell"> |
|||
let |
|||
sum x = if x == 0 then 0 |
|||
else x + sum (x - 1) |
|||
in |
|||
sum 10 |
|||
</source> |
|||
--> |
|||
'''[[型推論]]''' |
|||
ただし再帰呼び出しは[[スタックオーバーフロー]]の危険性やオーバーヘッドを伴うため、注意深く使用しなければならない<ref>[https://msdn.microsoft.com/ja-jp/library/dd233229(v=vs.120).aspx 関数 (F#) | MSDN]</ref>。通例、関数型言語では、[[末尾再帰]]呼び出し (tail-recursive call) の形で書かれた関数をループに展開する[[コンパイラ最適化]]により、スタックオーバーフローの危険性および再帰のオーバーヘッドを解消する。[[Scheme]]など、関数型言語の中には末尾再帰呼び出しの最適化を仕様で保証するものもある。 |
|||
: 型推論は[[静的型付け]]の機能であり、コンパイル時の解析でソースコード内のあらゆる関数適用や変数束縛が求める等価性と値のそれが一致しているかをチェックする。値の等価性は推論的型付け視点の「型」になる。同じ型でなかったら計算不可の型エラーになってコンパイルエラーになる。型の推論はプリミティブ記述、データ構築子定義、変数束縛(''Var'')関数定義(''Abs'')関数適用(''App'')等式(''Let'')型構築子定義(''Gen'')型構築子宣言(''Inst'')といったソースコード内のあらゆる記述ポイントを拾い上げて総合解析するという専用の型推論アルゴリズムで行われる。端的に言うとプリミティブという原子の組み合わせとその写像による変遷を精密に辿ってそれぞれの型を判別していると考えてよい。推論的型付けでは変数/引数/返り値に対する型宣言と型注釈は不要になり、むしろ計算全体の整合性を損ねるものとして倦厭される。[[命令型プログラミング|命令型言語]](手続き型やオブジェクト指向)の型推論アルゴリズムは簡素化されているので、型推論の使用対象はローカル変数や引数渡しが中心になる。この型推論の利点は、ローカル変数に型テンプレート的な表現の幅を持たせてソースコードの保守修正作業を容易にすることである。従来の型宣言と型注釈を用いる{{仮リンク|明示的型付け|en|Manifest typing|label=}}と、[[型推論]]の共存はC言語世代プログラミングに対する一つのパラダイムシフトでもある。 |
|||
== 特徴 == |
|||
また、関数型言語は文 (statement) よりも式 (expression) を中心とした言語仕様となっていることも特徴である。前述の例において、再帰関数<code>sum</code>を[[束縛 (情報工学)|束縛]]する<code>let</code>は式である。また、条件分岐の<code>if-then-else</code>も式である。文よりも式で書けることが多いほうが都合がよい。 |
|||
{{出典の明記| date = 2020年5月6日 (水) 02:29 (UTC)| section = 1}} |
|||
ここでは関数型プログラミング本来の考え方([[プログラミングパラダイム|パラダイム]])に基づいて説明する。[[文 (プログラミング)|ステートメント]]を基本文にする[[命令型プログラミング|命令型言語]]と、[[式 (プログラミング)|式]]を基本文にする関数型言語はどちらも最終的には[[命令型プログラミング|命令型パラダイム]]に沿った[[機械コード]]に落とし込まれる事になるが、双方の間にはプログラミングに対する考え方に大きな隔たりがあると言える。 |
|||
=== 式と関数 === |
|||
関数型言語は関数型プログラミングをサポートする言語ではあるが、手続き型プログラミングを行なうことも可能である。例えばF#では以下のようなPascal風の書き方もできる。 |
|||
関数型プログラムの基本文は[[式 (プログラミング)|式]](''expression'')である。式は個体(''individual'')である値(''value'')と写像(''mapping'')である関数(''function'')の二つから構成される。関数の定義には[[演算子]](''operator'')も含まれている。値は[[基本データ型]](プリミティブ)と{{仮リンク|複合データ型(コンポジット)|en|Composite data type|label=}}および[[ラムダ計算]]で言われる変数(''variable'')を意味する。変数は[[束縛変数]]と[[自由変数と束縛変数|自由変数]]を指す。評価(''evaluation'')される前の式は、ラムダ計算で言われるネーム(''name'')と同義になる。ネームは数学上の数式または代数式に相当するものである。式内の変数部分が確定される前の式はラムダ抽象(''abstraction'')と同義になる。式内の変数部分を確定するのはラムダ適用(''application'')と同義になる。この式=ネームが評価されると値になり、これはラムダ計算で言われる簡約(''reduction'')と同義になる。式は値と同一視されるのですなわち式と値は相互再帰の関係にある。式内の値は他の式の評価値である事があり、その式内にもまた他の値があるといった具合である。この解釈は[[高階論理|高階述語論理]]''と呼ばれる。高階述語論理=[[高階関数]]の解釈下で引数または返り値として扱われる関数は[[第一級関数]]と呼ばれる。'' |
|||
関数型プログラミングの関数は”関数の型”(''function type'')で分類される[[存在量化子|存在量]]の値である。プログラム的には式に引数を結び付ける機能であり、これは関数を引数に[[写像|適用]]する(''applying a function to an argument'')とされる。関数の式内の仮引数(''parameter'')箇所に渡された実引数(''argument'')が[[パターンマッチング]]手法で当てはめられ、先行(その時)または遅延(その後)タイミングで評価されて結果値が導出される。この仮引数箇所は束縛変数と呼ばれる。関数呼び出し時とは異なるタイミングで内容が決定される変数箇所は自由変数と呼ばれる。letとwhereで特定の式に向けて定義される変数はその式への束縛変数になる。なお関数型パラダイムでの自由変数の意味合いは他の[[宣言型プログラミング|宣言型パラダイム]]とはやや異なっている。関数適用時に用いられる[[パターンマッチング]]手法は、仮引数パターンの[[選言]]的な列挙を可能にしている。このパターンマッチングは”関数の型”に沿った等値性(''equality'')で仮引数と実引数を照合する。更にそれに[[ガード (プログラミング)|ガード]]と呼ばれる値の比較照合と範囲照合を加えることもできる。仮引数が非交和型の場合はその中で列挙されている型との等価性(''equivalent'')でも仮引数と実引数を照合できる。渡される実引数によっては[[ボトム型]]になる関数もありこれは部分関数(''partial function'')と呼ばれる。ボトム型は式ないし関数の評価の失敗した終着点を意味する。演算子はデフォルトの式内容を持ち、その引数が単項演算子なら1個、二項演算子なら2個に限定された関数と同義である。 |
|||
<source lang="fsharp"> |
|||
let mutable total = 0 |
|||
for i = 1 to 10 do |
|||
total <- total + i |
|||
printfn "%d" total |
|||
</source> |
|||
”関数の型”は「第1引数の型→第x引数の型→評価値の型」というように形式化されておりこれはカリー化(''currying'')と呼ばれる。例として関数funcの型を<code>func::A→B→C</code>とするとこの場合、A型値に適用されたfuncは<code>B→C</code>という”関数の型の値”を返す事になり、それをB型値に適用するとC型の評価値が返る事になる。左からの引数にひとつひとつ適用する形にして、<code>B→C</code>のような中間的な”関数の型の値”が導出されるようにする仕組みが関数の[[カリー化]]である。カリー化は写像の[[量化]](''quantify'')を扱う[[二階述語論理]]の表現手法でもある。カリー化によって関数funcの型は<code>func::A→(B→C)</code>と読み替えられるようになり、この場合にAにのみ適用して<code>B→C</code>という”関数の型の値”のまま保留することは部分適用(''partial application'')と呼ばれる。またカリー化による重要概念に関数合成(''function composition'')がある。これは二項の合成演算子<code>.</code>を関数<code>f::B→C</code>に適用すると<code>(*→B)→(*→C)</code>が導出され、それを関数<code>g::A→B</code>に適用すると関数<code>f.g::A→C</code>が導出されるというものである。合成演算子の左側の[[定義域]]と右側の[[値域]]が同じ型の場合のみ合成できる。高階関数的な連結である<code>f(g A)</code>と働きかた的には同じであるが、[[パイプライン処理]]の方に該当する関数連結(関数チェーン)と、カリー化に則った関数合成は異なる概念である。カリー化による部分適用や合成演算子から導出された”関数の型の値”を、任意の変数に束縛して扱うのはポイントフリースタイル(''point-free style'')と呼ばれる。ポイントフリースタイルの変数を値に適用すると、評価された値が返されるかまたは”関数の型の値”が返される事になる。ポイントフリースタイル変数は<code>var value</code>の書式で自身の右の値を暗黙的に引数として取る。ポイントフリースタイルは[[高階述語論理]]と[[存在量化子]]の表現手法でもあり、[[継続渡しスタイル]]にも応用される。引数を部分適用された演算子はセクションと呼ばれてポイントフリースタイルでよく用いられる。[[カリー化]]準拠の”関数の型”は[[型理論]]の指数型(''quotient type'')に分類されるものである。 |
|||
ただし[[Haskell]]のようにループ構文をサポートせず、従来の手続き型プログラミングが難しいケースもある。 |
|||
関数は名前付きと名前無しの二通りある。名前無しの関数は専らラムダ抽象を模した構文で定義される。式内に自由変数を内包しない方は単に[[無名関数]]と呼ばれ、自由変数を内包する方はそれを囲い込むという意味で[[クロージャ]]と呼ばれる。自由変数は外部データへの接点になる。[[無名関数]]は引数をピュア[[写像|マッピング]]する純粋関数である。[[クロージャ]]の引数の[[写像|マッピング]]は式内の自由変数に影響され、またその自由変数に作用する事もあるという副作用要素を閉包した非純粋関数である。リスト処理時にリストの各要素への作用子として渡される無名関数またはクロージャは[[反復子|イテレータ]]と呼ばれる。同様にリスト処理時に渡されて各要素を参照しながらそれらの総和値または選別リストまたは更新リストを生成する方は[[ジェネレータ (プログラミング)|ジェネレータ]]と呼ばれる。これは[[イミュータブル]]重視時に多用される。関数の名前は、それに結び付けられた式または式ツリーの[[不動点]]の表現になる。自式の不動点を式内に置いて新たな引数と共に[[高階論理|高階述語論理]]の式として評価する手法は[[再帰]]と呼ばれる。関数の終端式での再帰は実引数の更新+先端式へのアドレスジャンプと同等に見なせるのでもっぱらそちらに最適化されてこれは[[末尾再帰]]と呼ばれる。末尾再帰は論理性を損なわずにスタックフリーの無制限ループを可能にする実装概念として重視されている。 |
|||
逆に手続き型言語を使って関数型プログラミングを行なうことも可能であるが、末尾再帰呼び出しの最適化がサポートされるかどうかはコンパイラ次第である。 |
|||
== |
=== 値とデータ構造 === |
||
関数型プログラミングの値(''value'')は型(''type'')で分類される[[定数 (プログラミング)|定数]]または[[全称量化子|全称量]]の[[変数 (プログラミング)|変数]]である。これは[[基本データ型]](プリミティブ)と{{仮リンク|複合データ型(コンポジット)|en|Composite data type|label=}}のいずれかで表現される。プリミティブは数値、論理値、文字値、文字列を指す。コンポシットはプリミティブを任意に組み合わせた複合体であり、例としては[[構造体]]や[[共用体]]などを指す。その組み合わせ方に焦点を当てた用語が[[データ構造]](''data structure'')である。データ構造という概念には[[再帰]]、[[アノテーション]]、[[ガード (プログラミング)|ガード]]、[[操作的意味論|操作的意味]]といった暗黙情報をも含められるので、コンポジットの具体的形式といった意味で用いられる。関数型言語で用いられるデータ構造の代表は、[[代数的データ型]]と[[S式]]である。双方ともデータ構築子(''data constructor'')から構築される。まず、プリミティブがデータ構築子によってまとめられる。データ構築子はC言語の構造体/共用体と同性質のものであり、むしろ言い換えるとC言語は直積型のデータ構造を構造体にし、非交和型のデータ構造を共用体にしてペア定義している。データ構築子は入れ子構造と、自身を入れ子にした再帰構造も定義できる。プリミティブとデータ構築子を任意に組み合わせて代数的データ型やS式といったデータ構造が構築される。データ構造内のプリミティブとデータ構築子の組み合わせ方はパターン(''pattern'')と呼ばれる。そのパターンが型になり、パターンの構築が型付けになり、パターンを[[量化]](''quantify'')すると型付け値になり、これはターム(''term'')と呼ばれる。タームは冒頭の値(''value'')を指す。データ構築子のパターンの末端は必ずプリミティブになるので、パターン内の全てのプリミティブの値を決定することが量化になる。お互いのパターンがマッチするターム同士は等価(''equivalent'')とされる。この等価は同じ型と読み替えてもよい。等価性はあらゆる計算の可否(計算可能性)を決定する。計算とは関数適用または演算子適用を指し、それらが求める仮引数と実引数にするタームが等価であればその計算は成立する事になる。データ構造のパターンは基礎パターンに分解されて解釈される。基礎パターンは[[型理論]]に従って直積型、非交和型、ユニオン型、オプション型、帰納型、ユニット型などに分類されている。 |
|||
<!--関数型プログラミングではプログラムの構成にC言語のように関数を多用する<ref>The C language is purely functional http://conal.net/blog/posts/the-c-language-is-purely-functional</ref>。[[Wikipedia:検証可能性#通常は信頼できないとされる情報源]]-->関数型プログラミング(パラダイム)に合意された定義がないことと同じく、広く認められた関数型言語の正確な定義は存在しない。関数型プログラミングでは関数を[[第一級オブジェクト]]として扱う。理論的な計算モデルとして第一級オブジェクトとしての関数を扱える[[ラムダ計算]]や[[項書き換え]]を採用している。 |
|||
[[S式]]は[[二分木|二分木構造]]のデータ構造である。これはコンス(''cons'')と呼ばれる二項のデータ構築子の連結で形成される。コンスは二つの要素を持つ[[タプル]]であり、要素はプリミティブまたは他のコンスのどちらかである。S式はコンスを実行時に連結して任意のパターンを構築する[[動的型付け]]の値である。コンスは要素二つの[[直積集合|直積型]](''product type'')であり、コンスの連結による要素の並びは[[線形リスト|リスト]]と呼ばれる。コンスの要素は形式化されていない[[非交和|非交和型]](''sum type'')でもあり、要素の識別はプログラマ側の裁量に委ねられている。コンスの組み合わせによるパターンは任意の識別名に結び付けられる。 |
|||
コンピュータプログラムは通例入力を受け取って何らかの処理を行ない、出力を返すことを目的として書かれる。数学の関数<math>y = f(x)</math>において、<math>x</math>を入力、<math>y</math>を出力と考えると、コンピュータプログラムはある種の関数であると考えることができる。ここで、入力や出力は記憶装置中のファイルのようなものばかりではなく、キーボードや[[ポインティングデバイス]]によってユーザーから与えられる情報や、画面への表示といった入出力形態も考えられる。関数型プログラミングにおいては実際にそれらを扱う関数としてモデル化する。 |
|||
[[代数的データ型]]は{{仮リンク|AND-OR木構造|en|And–or tree|label=}}のデータ構造である。これは[[直積集合|直積型]](''product type'')と[[非交和|非交和型]](''sum type'')を表現する多項のデータ構築子の組み合わせで形成される。データ構築子は任意個数の要素を持つものであり、要素はプリミティブまたは他のデータ構築子のどちらかである。代数的データ型はデータ構築子を事前に組成定義して任意のパターンを構築する[[静的型付け]]の値である。直積型は[[タプル]]または[[構造体|レコード]]のパターンを表わす。レコードは指定フィールド取得用関数を随伴させたものである。非交和型は[[列挙型]]または[[共用体|タグ共用体]]のパターンを表わす。前者は等値性(''equality'')で識別される非交和である。後者は等価性(''equivalent'')で識別される非交和であり、こちらはユニオン型(''union type'')とも呼ばれる。ユニット型(''unit type'')は空集合のパターンを表わし、実装面ではnilまたはvoidの表現になる。ユニット型とそうでない型の二択の非交和型はオプション型(''option type'')とされ、実装面ではMaybe値の表現になる。データ構築子を再帰的にネスティングするパターンは帰納型(''inductive type'')とされる。非交和型と帰納型とユニット型の組み合わせは[[連結リスト]]や[[二分木]]のパターンを表わす。データ構築子の組み合わせによるパターンは任意の型構築子(''type constructor'')に結び付けられて同時にそれが識別名義になる。データ構築子がパターンの表現に用いられるのに対して、型構築子はパターン内の要素(プリミティブないしデータ構築子)の多相化に用いられる。多相化はパターン内の要素を型変数(''type variable'')に置き換え、型構築子への型引数(''type parameter'')で要素の型を決定するという形で行われる。型構築子が必要とする型引数の個数および形態によるパターンは[[カインド (型理論)|カインド]](''kind'')と呼ばれる。型構築子はカインドによって分類される。代数的データ型は識別名義と実装内容を分離して双方を自由に組み合わせるという意味でしばしば抽象化される。これは仮名義の型名(=型構築子)を用いてプログラムを記述しておき、プログラム冒頭でその仮名義に実際の型名を当てはめるものであり、その仮名義の定義は型シノニムまたは型エイリアスと呼ばれる。 |
|||
純粋関数型言語では、[[参照透過性]]が常に保たれるという意味において、全ての[[式 (プログラミング)|式]]や関数の評価時に[[副作用 (プログラム)|副作用]]を生まない。純粋関数型言語である{{lang|en|[[Haskell]]}}や{{lang|en|[[Clean]]}}は非[[正格]]な評価を基本としており、引数はデフォルトで[[遅延評価]]される。一方、{{lang|en|[[Idris]]}}は純粋だが正格評価を採用している。入出力などを[[参照透過性]]を保ったまま実現するために、たとえば {{lang|en|Haskell}} では[[モナド (プログラミング)|モナド]]、{{lang|en|Clean}} では{{仮リンク|一意型|en|Uniqueness type}}という特殊な型を通して一貫性のある表現を提供する。 |
|||
=== 評価戦略 === |
|||
非純粋関数型言語では、参照透過性を壊す、副作用があるような式や関数も存在する。{{lang|en|LISP}}などでデータ構造の破壊的変更などの副作用を多用したプログラミングを行うと、それはもはや手続き型プログラミングである。多くの場合、非純粋関数型言語の[[評価戦略]]は正格評価(先行評価)であるが、遅延評価する部分を明示することで、無限リストなどを扱えるものもある。 |
|||
関数型プログラミングの[[評価戦略]](''evaluation strategy'')は、関数を値にする評価タイミングと、引数欄内関数の評価タイミング(''call-by-What'')の二つを定義している。関数を値にする評価タイミングは、正格評価(''strict evaluation'')と非正格評価(''non-strict evaluation'')の二つに大別されている。正格評価の関数は引数確定と同時に評価されて値になる。この評価タイミングに注目した方は[[先行評価]](''eager evaluation'')と呼ばれる。引数確定と同時に[[ボトム型]](評価失敗)が発生することも包括した呼称が正格評価である。非正格評価の関数は、引数確定されても未評価のまま保留状態にされる。後続式で改めて他の関数/演算子の引数にされた時に初めて評価されて値になり、または改めて変数に束縛された時に初めて評価されて値になる。この評価タイミングに注目した方は[[遅延評価]](''lazy evaluation'')と呼ばれる。評価されるまでボトム型の発生が保留されることも包括した呼称が非正格評価である。これが遅延評価のデフォルトタイミングであるが、好きなタイミングで遅延評価できる無名関数/クロージャもあり、それは[[継続]](''continuation'')と呼ばれる。その任意タイミングの評価値導出はcall/cc(現行継続呼出)と呼ばれる。遅延評価は必要以外の評価をスキップして処理を高速化するが、評価値と未評価関数の区別が難しくなるというジレンマがある。 |
|||
引数欄内関数の評価タイミング(''call-by-What'')には、値渡し(''call by value'')と名前渡し(''call by name'')がある。値渡しは先行評価に相当するものであり、関数の評価値が引数として渡される。名前渡しは遅延評価に相当するものであり、未評価のまま保留された関数が引数として渡される。なお、双方ともに引数確定されていない場合はただの第一級関数(関数の型の値)として渡されることになる。また名前渡しの亜流に必要渡し(''call by need'')があり、これは一度名前渡しされた関数+引数はその評価値を[[メモ化]]されて、同じ関数+引数が再度名前渡しされた時はそのメモ化評価値の方を渡すという仕組みである。必要渡しは純粋関数型言語で実装されている。 |
|||
{{lang|en|[[JavaScript]]}}や{{lang|en|[[Java]]}}など{{いつ範囲|date=2018年10月|近年}}の[[高水準言語]]には、関数型言語の機能や特徴を取り入れているものがあるが、変数の値やオブジェクトの状態を書き換えるプログラミングスタイルを通常とするため、関数型言語とは分類されない。一方{{lang|en|[[LISP]]}}は、その多くが副作用のある式や関数が多数あり、手続き型スタイルでのプログラミングがされることも多いが、理論的なモデル(「[[純LISP|純{{lang|en|LISP}}]]」)の存在や副作用を使わないプログラミングが基本であること、ないし主には歴史的理由から、関数型言語だとされることが多い。なお、{{lang|fr|Pascal}}では「手続き」と呼ばれるような値を返さない[[サブルーチン]]を、C言語では<!--<code>void</code>型の値を返す関数と捉える--><!--void型の値というものは存在せず、存在しないものについて、それを返す関数と「捉える」ことは常人には困難-->「関数」と呼んでいるが、これは単にルーチンについて、細分類して別の呼称を付けているか、細分類せず総称しているか、という分類と呼称の違いに過ぎず、「{{lang|fr|Pascal}}は手続き型言語で、C言語は関数型言語」<ref>[[共立出版]]『{{lang|en|ANSI C/C++}}辞典』ISBN 4-320-02797-3 など</ref>という一部の書籍に見られる記述は明確に誤りである。また、{{lang|en|OCaml}}や{{lang|en|Haskell}}などでは、「自明な値(例えば<code>()</code>)を返す」と、値を返さない(<code>Void</code>など)は違うものであり、後者は停止しないか例外を出す(そのため結果がない)ようなプログラムを表す。 |
|||
データ構造でも遅延評価の概念は扱われており、[[帰納]]、[[再帰]]、[[無限]]、[[極限]]といった代数表現の実装手段になっている。代数的データ型では[[共用体|タグ共用体]]、[[線形リスト|連結リスト]]、[[再帰データ型]]は遅延評価対象である。連結リストは無限リストと構造上同義であり遅延評価が無限性質の実装を可能にしている。 |
|||
なお、「関数型言語である」と「関数型プログラミングをする」は同値ではなく、関数型には分類されない言語で関数型プログラミングをすること{{efn|関数型プログラミングのエッセンスとして、[[MISRA C]]のように[[C言語]]でも副作用を極力用いないプログラミングを推奨しているコーディング標準もある。}}や、関数型プログラミングを基本とする言語の上で他のパラダイムを実現する例もある<ref name="Novatchev">{{cite web | url = http://arxiv.org/abs/cs/0509027 | author = Oleg Kiselyov, Ralf Lämmel | title = Haskell's overlooked object system | accessdate = Sep 10, 2005}}</ref>。<!--<ref>「関数型言語」に関するFAQ形式の一般的説明 https://qiita.com/esumii/items/ec589d138e72e22ea97e</ref>[[Wikipedia:検証可能性#通常は信頼できないとされる情報源]]--> |
|||
=== 参照透過性 === |
|||
[[データフロープログラミング]]言語も関数型言語と共通した特徴を部分的に持つ。 |
|||
[[参照透過性]](''referential transparency'')とは、関数は同じ引数値に対する同じ評価値を恒久的に導出し、その評価過程において現行計算枠外の情報資源に一切の影響を及ぼさないというプロセス上の枠組みを意味する。現行計算枠外のいずれかの情報要素が変化するのと同時にいずれかの関数の評価過程も変化してしまう現象が[[副作用 (プログラム)|副作用]](''side effect'')と呼ばれる。参照透過性=副作用の排除でもある。副作用のない関数を純粋関数(''pure function'')と呼ぶ。副作用が伴なう操作の代表例は値の再代入と入出力処理とシステムコールである。この参照透過性は、副作用発生部分とそうでない部分を分けて処理構成の見通しを良くするなどのプログラム設計の視点から語られる事が多く、[[手続き型プログラミング|手続き型]]や[[オブジェクト指向プログラミング|オブジェクト指向]]ではその利点から解釈してよいものであるが、関数型パラダイムではプロセス[[有向グラフ]]の整合性を維持するという視点から解釈する必要がある。プロセス有向グラフとはプログラム初期値からのあらゆる個体(値)と写像(関数)の繋がりを表わす図式である。あらゆる値の変遷が初期値まで遡れるという参照透過性が保証された関数型プログラムは、その[[正当性 (計算機科学)|正当性]]の[[形式的検証]]および[[数学的証明]]の自動化が可能になりこれが本質的な利点になる。プロセス[[有向グラフ]]の解析と模型化は、[[プロセス代数]]と呼ばれ[[並行プログラミング]]の支柱になる。関数型の世界で値の再代入がタブーとされるのは、それが言わば履歴の改竄になってプロセス有向グラフの整合性を崩すからである。従って[[束縛変数]]と、旧値の更新を新値の生成で代替した[[イミュータブル]]が重視される。ループは関数の[[再帰]]で表現され、分岐は[[選言]][[パターンマッチング|パターンマッチングと]][[ガード (プログラミング)|ガード]]で表現される。以上が参照透過性の仕様上の意味である。 |
|||
参照透過性の実装はコーディングレベルで副作用を排除できる部分への対応と、そうではない部分への対応に大別される。前者は再代入などを指しプログラマに委ねられる問題になる。後者は入出力処理やシステムコールなどを指しこちらではそうはいかない。後者の参照透過性は、副作用原因になる全ての情報要素を関数の引数に収納し、副作用結果になった全ての情報要素を関数の返り値に収納するという形式で表現される。情報要素には理論上は関連するコンピュータ環境状態が全て内包されている事になるが、その忠実な実践は当然不可能なので、コンピュータ環境状態を抽象化した「抽象環境」で代用される。抽象環境は言語処理系から提供される特殊データである。この抽象環境を関数の引数とその返り値に常時含めて、コーディングレベルの副作用排除はプログラマが解決する事で理論上は参照透過性が順守されている事になる。この論理的なトリック方式が参照透過性の実装上の意味である。抽象環境を用いて参照透過性を維持しつつ再代入/入出力/システムコールなどを行うための機能には、{{仮リンク|派生構造型システム|en|Substructural type system}}と[[モナド (プログラミング)|モナド]]がある。派生構造型システムは、抽象環境をライナー型(''linear type'')にして関数に渡される度にその中のIDデータをユニーク更新させる。抽象環境にユーザーデータを注入する仕組みはアフィン型(''affine type'')、抽象環境からユーザーデータを抽出する仕組みは関連型(''relevant type'')と呼ばれる。毎時ユニーク更新されるライナー型のIDデータは、各種入出力によって実際には変化しているコンピュータ環境の時系列状態を完全に抽象化して、それらを理論上各個照会可能にしているマッピングキーである。これによってコンピュータ環境状態の変化もプロセス有向グラフで論理的に辿れることになるので同時に参照透過性も維持されていることになる。派生構造型システムの実装例には{{仮リンク|ユニークネス型|en|Uniqueness type}}がある。ライナー型、アフィン型、関連型を組み合わせての特にその応用コーディングは煩雑なボイラープレートコードになりがちだったので、それらを[[圏論]]と[[代数的構造]]由来のアイディアでより平易かつ簡潔にした手法が[[モナド (プログラミング)|モナド]]である。 |
|||
=== 型システム === |
|||
{{型システム}} |
|||
関数型プログラミングの[[型システム]](''type system'')は、[[型付きラムダ計算|型付けラムダ計算]]準拠の[[型理論]]に基づいて構築されている。ここでは型システム上の対比に沿った形で記述する。関数型言語の二大系統の内、[[ML (プログラミング言語)|ML系]]は[[静的型付け]]ベースであり、[[LISP|LISP系]]は[[動的型付け]]ベースである。 |
|||
'''静的型付け''' |
|||
関数型言語の静的型付けでは、性質や役割による[[セマンティクス|意味づけ]]によって値を分類する明示的型付け(''manifest typing'')よりも、計算可能性に基づく[[等価性]]によって値を分類する推論的型付け(''inferred typing'')が主流である。前者の意味づけとはプログラマによる型定義、型宣言、型注釈を指しており人間寄りの視点である。後者の等価性とは値を関数/演算子の引数にできるかどうかの判別を指し、値への関心がそこで計算可能かどうかに絞られているので計算機寄りの視点である。明示的型付けではソースコード上の型宣言と型注釈から値の型が特定されるのに対し、推論的型付けでは[[型推論]]機能で特定される。型推論とは専用のアルゴリズムによる解析によってソースコード内のあらゆる値/変数/引数/返り値それぞれの等価性を導き出す機能である。推論的型付けでは値への関心をその計算可能性に絞っているので、型宣言と型注釈は不要になりまたは相容れないものとなる。例としてint型を型シノニムで金額型と数量型にした場合、明示的型付けではこの両者は区別されるが、推論的型付けでは区別されない。ソースコードの解析でどちらもint型準拠の等価と見られるからである。推論的型付けで値の意味づけ性も表現する場合は、データ構築子で値を包む[[ボックス化]]が用いられる。データ構築子(''data constructor'')は与えられた要素を直積または非交和でまとめるのと同時に[[型理論]]で言われる文脈(''context'')を各要素の等価性に上乗せ付加するものでもある。 |
|||
静的型付けにおける[[データ構造]]のパターン(型)はコンパイル前ないし実行前に全て事前形成される。その実装例である[[代数的データ型]]はデータ構築子の組み合わせでパターンを構築し、パラメトリック多相に基づいて[[ジェネリックプログラミング|総称化]]したパターン内の要素=型変数を、型構築子への型引数の組み合わせで特定した。''Hindley–Milner''型体系はこのパラメトリック多相に対応した[[型推論]]機能を提供している。型構築子(''type constructor'')は必要とする型引数の個数によって分類され、これは[[カインド (型理論)|カインド]](''kind'')と呼ばれる。カインドは総称記号である<code>*</code>の写像で型構築子の型種を表現する。型引数を必要としない型構築子と必要な型引数を全て付与された型構築子はプロパータイプと呼ばれ<code>*</code>と表現される。プロパータイプは[[全称量化子|全称量]]の型である。型引数を1個必要とするものは<code>*→*</code>になり、2個必要なら<code>*→*→*</code>になる。これらは[[存在量化子|存在量]]の型になる。<code>*→*→*</code>に型引数が1つ付与されると<code>*→*</code>になり更に1つ付与すると<code>*</code>のプロパータイプになる。全称量の型付け値(ターム)は普通に扱えるが、存在量の型付け値はその一部分が抽象化(大抵は環境依存値と同義)されたままの特別な値と見なされて一定の制限下で扱われる。 |
|||
推論的型付け下の関数の扱いでは、人為的表記による意味づけを重視した記名的型付け(''nominal typing'')が取り入れられており、これで推論的型付けの枠組み内での[[多重定義|関数オーバーロード]]が表現されている。ここでの人為的表記による意味づけとは、型構築子/データ構築子/”関数の型”それぞれのパターン内の型変数に、[[型理論]]で言われる文脈(''context'')を付加することを指している。文脈の付加は制約(''constraint'')と呼ばれる。文脈の付加はアドホック多相と考えられており、代表的な実装例はそれと[[ジェネリックプログラミング]]を組み合わせた[[型クラス]]である。型クラスは、引数/計算値/評価値などを[[総称型]]化したジェネリック関数群の”関数の型”と式内容を定義できる機能であり、同時に推論的型付けと共存する関数オーバーロードの実装と、特定の意味づけ型を扱うための関数モジュールを定義するための手段になっている。型クラスの定義構文では上述のジェネリック関数群が定義され、その型クラス名が文脈記号になる。型構築子の定義に文脈を付加すると、その型クラスのジェネリック関数群にその型構築子=型を当てはめた関数群がコンパイル時に自動生成される(deriving)。また文脈を付加して当てはめ関数群を自動生成(instance)した上で、その型構築子=型のための当てはめ関数の式内容を個別定義(where)できる構文もある。この双方がジェネリック関数の特有インスタンス化になる。明示的型付けでは型注釈を付けた引数パターンの列挙というシンプルな手段で関数オーバーロードを表現できるが、推論的型付けでは等価性に上乗せした文脈という二段階の手段が必要になる。記名的型付けと併せた推論的型付けでのオーバーローディング関数の選択決定は、始めに仮引数と実引数の型クラスのみに注目した照合が行われ、次にその型クラスの制約内での型推論照合が行われるという形になる。 |
|||
'''動的型付け''' |
|||
動的型付けにおける[[データ構造]]のパターン(型)はコンパイル前ないし実行前の事前形成に加えて、実行中の随時にも事後形成できる。その実装例である[[S式]]は、二項データ構築子([[Cons (Lisp)|コンス]])の実行時の連結で形式化されていないパターンを構築し、プログラマの裁量による任意の実行時チェックでパターンの意味づけと計算に用いるための等価性を判別するといったものである。パターンの組み合わせが明確に形式化されておらずその識別をプログラマの裁量に委ねており、またチェックタイミングも委ねられている事から、これは潜在的型付け(''latent typing'')と呼ばれている。潜在的型付けは動的型付けの原型的位置づけである。 |
|||
もう一つの実装例として動的なレコード(''record'')がある。この動的レコードは内部的には[[動的配列]]と同じものであり、配列の各スロットに任意の値の[[参照 (情報工学)|参照]]が納められて、そのスロットは[[構造体]]のフィールドと同じものになる。スロットは増設削減可能である。レコードのタグ名はそのまま型名になる。レコードを量化した値([[インスタンス]])には、システム側が別途用意する型情報が結び付けられており、変数への束縛ないし代入および関数/演算子への引数代入時に毎回自動的に型判別される。型情報と型判別タイミングが形式化されているのでこれは動的型付けとなる。動的型付けのインスタンスを関数の引数にすることは動的な[[多重定義|関数オーバーロード]]を自然表現し、これはオブジェクト指向に倣って[[多重ディスパッチ]]とも呼ばれる。レコード・フィールドのアクセスは、フィールド名関数をインスタンスに適用するという方法で行われる。オブジェクト指向の<code>instance.field</code>が関数型では<code>field instance</code>のようになる。同じフィールド名関数から得られる値の型は、適用するインスタンスの型構造による実行時多態になる。この仕組みは構造的型付け(''structural typing'')に沿ったものである。 |
|||
=== モナド === |
|||
{{Quotation|''A monad is just a monoid in the category of endofunctors, what's the problem?''<br/>(モナドは自己関手の圏のただのモノイドだよ。何か問題でも?)|Philip Wadler}}[[モナド (プログラミング)|モナド]](''monad'')の説明でよく引用されるこの文言はその特徴を明快に表したヒントと言われる。モナドは[[圏論]]由来の自己[[関手]]の合成の仕組みで[[参照透過性]]を維持しつつ、[[代数的構造]]由来の[[モノイド]]の仕組みで非純粋関数の連結体を平易に表現する機能である。非純粋関数の連結体がそのまま一連の[[副作用 (プログラム)|副作用]]内包プロセスになりそれを参照透過性の枠内で実行できる。モナドはプログラム的には、関数の量化を扱う[[二階述語論理]]の実装である[[カリー化]]の活用形態である関数合成と部分適用の応用手法であり、参照透過性欄で説明した派生構造型システムのライナー型の役割を[[圏 (数学)|圏]]に見立てたデータ構築子に持たせている。またデザインパターン的には、関数を引数値に適用する(''applying a function to an argument'')通常の関数とは正反対に、モナドでは値を関数に適用する(''applying a value to a function'')形態を取っており、値を同名関数に反復適用しての特殊な再帰を表現できる。モナドでは値に特殊な演算子を適用することでそれを関数に適用できるようにしている。 |
|||
モナドの世界では、基本値から導出されるモナド値をモナド関数に次々と適用して状態遷移を表現する。モナド値(''monadic value'')とはMaybe値、例外発生、入出力環境、システムコール、クロージャ、継続といったあらゆる副作用プロセスを扱うためのオブジェクトである。モナド関数(''monadic function'')とはそのオブジェクトのメソッドと考えてよいものである。モナド値とモナド関数は合成演算子を通した表裏一体の存在であり、これがモノイドを指している。合成演算子をモナド値に適用したものをモナド関数に適用して導出されたモナド値を、また同じ手順でモナド関数に適用するといった繰り返しになる。これはモナド合成体(''monadic composition'')と呼ばれる。モナド値は<code>MA</code>のように型表現され、<code>A</code>は基本値、<code>M</code>は基本値を包むコンテキストまたはコンテナである。モナド関数は<code>(A→MB)</code>を基本とする関数の型の値であり、与えられた基本値からモナド値を操作して新たなモナド値を返す。モナド値は<code>MA</code>の型を基本とし、対象内容によって<code>M(A?)</code>、<code>M(A+E)</code>、<code>M(W×A)</code>、<code>E→MA</code>、<code>S→M(A×S)</code>、<code>(A→MR)→MR</code>といった型の値または関数の型の値にリフト(''lift'')される。これはモナド変換子(''monad transformer'')と呼ばれる。また、モナド値の<code>MA</code>の<code>M</code>を空データにして事実上の<code>A</code>にしモナド関数も事実上の<code>(A→B)</code>にした恒等モナド(''identity monad'')もあるがこれは全くの便宜目的である。モナド値は状態遷移の記録媒体であり、η自然変換演算子を基本値に適用することで導出される。基本値+αから成るモナド値または基本値を包含するモナド値はデータ構築子として表現されて自己関手の圏の枠組みとなり、そのデータ構築子を束縛した型構築子への型引数でモナド値内の型決定がなされる。モナドは大まかに言うと以下の六つの要素から実装されている。 |
|||
* モナドの文脈を付加された型構築子。一つの副作用内包プロセスを扱う関数群モジュールに見立てられる。 |
|||
* 基本値+αのコンテナであるモナド値=データ構築子。圏に見立てられる。 |
|||
* μ自然変換演算子(join)kleisli-star拡張演算子(bind)といった合成用の二項演算子。 |
|||
*基本値とη自然変換演算子(return)とモナド値。モナド値+二項演算子でモノイドに見立てられる。 |
|||
* モナド関数。パターンマッチング分岐可能でモジュール内の操作を一手に扱う。自己関手内容に見立てられる。 |
|||
*ファンクタ文脈からの持ち上げ演算子(fmap)は補助的機能。関手に見立てられる。 |
|||
モナド値は付加モナドと自由モナド(Maybe/例外/有限リストモナドなど)以外では、実質的に存在量の値になるので普通の値のように扱うことは出来ない。ただし付加/自由モナドでも参照透過性を維持するためには存在量と同等に扱う必要が出てくる。<code>return</code>を基本値に適用してモナド値を表現することからモナド処理は始まる。基本値とは扱うモナドに合わせた任意の値である。そのモナド値は圏としてのユニークIDを持つことになる。ここで基本値を<code>A</code>としそのモナド値を<code>MA</code>とする。<code>MA</code>にbindを適用して<code>(A→MB)→MB</code>という写像を導出する。その写像は先の<code>MA</code>と同じ圏IDを備えたものになる。その写像をモナド関数<code>(A→MB)</code>に適用すると、そのモナド関数内では渡された<code>A</code>やその他の値などに<code>return</code>を適用して表現されるモナド値の圏IDは<code>MA</code>のもので共通化される。モナド関数内においての<code>return</code>は基本値をモナド値に代入する機能と見てよい。<code>return</code>は用途別関数にそれぞれラッピングされて使われるのが普通である。空引数からモナド値を表現する<code>return</code>もありこれはモナドプリミティブ(''monadic primitive'')と呼ばれ、この場合はモナド値の圏IDが暗黙引数になっている。モナド値はファイルハンドルのようなものと考えると分かりやすくなり、モナド値を直接引数にできる専用関数も存在する。モナド関数内ではモナド値から基本値を取り出す演算子が有効になる。それは<code>A←MA</code>のように表現されてコモナド(''comonad'')と呼ばれる機能になる。抽出した基本値からの処理の中で再度<code>return</code>が行われる。モナド関数は自己関手内容に見立てられているので、その中では<code>return</code>の繰り返しによる事実上の再代入処理が許されている。その論理的な辻褄合わせの要点になる<code>bind</code>の正当性および計算可能性を表現するためにファンクタ則とモナド則の等式がプログラム内で定義されている。ここでいわゆる圏論の知識が必要になるがその説明は先送りする。モナド関数はモナド値を返しそれに再度<code>bind</code>を適用できるのでこれがモノイドを意味している。モナド関数の外での<code>return</code>は毎時ユニークな圏IDのモナド値を表現するので同じ基本値でもその都度異なる圏が表現される事になり、これが[[自然変換]]([[関手圏]]の[[射 (圏論)|射]])演算子の呼称由来になっている。 |
|||
モナドは'''ファンクタ'''(''functor'')の派生文脈にされることが多いが、これは<code>bind</code>を形成するクライスリ射と<code>join</code>の合成の持ち上げ(関手)に<code>fmap</code>が使われるからである。ファンクタ文脈は関手<code>fmap</code>を持つ。そのままファンクタの機能名で呼ばれることが多い<code>fmap</code>は、関数<code>(A→B)</code>から関数<code>(TA→TB)</code>を導出する関手=関数である。この関数は<code>TA</code>に適用できて<code>TB</code>を導出できる。<code>T</code>は基本値を包むコンテキストまたはコンテナでありその代表例はリストである。基本値に対する作用をコンテキストで拡張解釈できるのがファンクタの利点である。例えば基本値への+1という作用をリストのコンテキストで拡張解釈するのはリストの全要素に+1するという意味になる。これはリストをそのまま計算対象にできる利便性に繋がる。ファンクタの派生文脈に'''アプリカティブ'''(''applicative'')がある。アプリカティブは、ファンクタのコンテキストに包まれた関数を「コンテキストに包まれた先頭引数→コンテキストに包まれた残り引数&評価値」という1引数の関数に変換する演算子<code><*></code>を持つ。<code><*></code>は<code>F(A→B)</code>から<code>(FA→FB)</code>を導出する演算子である。<code>F</code>はコンテキスト、<code>(A→B)</code>は元の関数、<code>(FA→FB)</code>は1引数の関数である。この<code>FB</code>は多相であり実際は値<code>F*</code>関数<code>F(*→*)</code> 関数<code>F(*→*→*)</code>などになっているのでそれに<code><*></code>を再び適用できる。アプリカティブは、2個以上の引数の関数をファンクタするための機能と考えてよい。2個以上の引数の関数は<code>fmap</code>でそのまま持ち上げられないのでアプリカティブ関手の<code>pure</code>が用いられる。これは<code>A→FA</code>と表現され<code>A</code>が関数、<code>F</code>がコンテキストである。<code>pure</code>によって好きな引数個数の関数をコンテキストで包み<code><*></code>をそれに適用して導出された関数を<code>FA</code>に適用できる。アプリカティブ関手<code>pure::A→FA</code>とモナドのη自然変換演算子<code>return::A→MA</code>は同じ働きに見えるが、前者は関数(関数の型の値)を純粋に持ち上げるだけの[[関手]]なのに対して、後者は持ち上げる関手を毎時垂直合成していく[[関手圏]]の[[射 (圏論)|射]]([[自然変換]])であるという性質上の違いがある。 |
|||
== 歴史 == |
== 歴史 == |
||
1930年代に数学者[[アロンゾ・チャーチ]]によって発明された[[ラムダ計算]]は[[写像|関数適用]]を計算単位にした[[形式体系]]であり、1937年に数学者[[アラン・チューリング]]自身により[[チューリング完全]]の性質が明らかにされて、[[チューリングマシン]]と等価な[[計算模型]]である事が証明されている。この経緯からラムダ計算は関数型プログラミングの基底に据えられた。ラムダ計算と同等の[[計算理論]]に[[コンビネータ論理]]があり、1920年代から1930年代にかけて数学者[[ハスケル・カリー]]らによって発明されている。こちらは関数型プログラミングの原点である[[高階論理|高階述語論理]]式の基礎モデルにされた。チャーチはラムダ計算を拡張してその各タームに型を付与した[[型付きラムダ計算|型付けラムダ計算]]も考案しており、これは関数型プログラミングにおける[[型理論]]と[[型システム]]の源流になった。 |
|||
{{lang|en|LISP}}は、その基本機能のモデルとして関数型の純{{lang|en|LISP}}を持つなどといった特徴がある、最初の関数型言語である。今日広く使われている{{lang|en|LISP}}方言のうち特に{{lang|en|[[Scheme]]}}は関数型としての特徴が強い。 |
|||
'''1950年代''' |
|||
現代的な関数型プログラミング言語の祖としてはアイディアが1966年に発表された{{lang|en|[[ISWIM]]}}が挙げられるが、1970年前後までは関数型プログラミング言語の歴史は{{lang|en|LISP}}の発展が主である。1970年代にプロジェクトが開始された{{仮リンク|ロジック・フォー・コンピュータブル・ファンクションズ|en|Logic for Computable Functions}}のための言語として[[ML (プログラミング言語)|ML]]が作られている。 |
|||
初の関数型プログラミング言語とされる「[[LISP]]」は、1958年に[[マサチューセッツ工科大学]]の計算機科学者[[ジョン・マッカーシー]]によって開発された。LISPの関数はラムダ計算の形式を元に定義され再帰可能に拡張されており、式のリスト化とその遅延評価および高階評価など幾つかの関数型的特徴を備えていた。LISPは数多くの”方言”を生み出しているが、その中でも「[[Scheme]]」「[[Dylan]]」「[[Racket]]」「[[Clojure]]」「Julia」は関数型の特徴を明確にした言語である。1956年に公開された「[[Information Processing Language]]」の方が先駆であるが、こちらはアセンブリベースの[[低水準言語]]なので前段階扱いである。IPLが備えていた[[ニーモニック・コード|ニーモニックコード]]のリストをオペランドにできるジェネレータ機能はLISPに影響を与えたと言われる。高階オペランド演算は高階関数と同じ働きをし、メモリ一括処理のストリング命令の効率を高めるなどした。 |
|||
また{{lang|en|LISP}}において、変数のスコープに静的スコープを採用した{{lang|en|Scheme}}が誕生したのが1975年である。 |
|||
'''1960年代''' |
|||
1977年、{{lang|en|FORTRAN}}の設計と[[バッカス・ナウア記法]]の発明の業績でこの年の[[チューリング賞]]を受賞した[[ジョン・バッカス]]は、{{lang|en|''Can Programming Be Liberated From the von Neumann Style?: A Functional Style and Its Algebra of Programs''}}<ref>「プログラミングはフォン・ノイマン・スタイルから解放されうるか?: 関数型プログラミング・スタイルとそのプログラム代数」、[[米澤明憲]]訳『ACMチューリング賞講演集』([[共立出版]])pp. 83-156</ref>と題した受賞記念講演で関数型プログラミングの重要性を訴えた。講演では[[FP (プログラミング言語)|FP]]という関数型プログラミング言語の紹介もした(サブタイトルの後半の「プログラムの代数」はこれを指す)が、これは{{lang|en|[[APL]]}}(特に、[[高階関数]]の意味がある記号({{lang|en|APL}}の用語ではオペレーター([[作用素]])という))の影響を受けている。 |
|||
1964年に計算機科学者[[ケネス・アイバーソン]]が開発した「[[APL]]」は、計算タイプを定義された関数記号に各種配列データを適用する機能を中心にした言語であり、特に[[スプレッドシート]]処理に対する効率性が認められて、1960年代以降の商業分野と産業分野に積極導入された。APLは多次元配列などのデータ集合に対する関数適用の有用性を特に証明した言語になった。そのデータ集合処理を注目されたAPLからは「[[J言語]]」「S」「A」「K」「Q」といった派生言語が生み出されている。APLの記号構文は一つの手本になりその流れは後年の「[[FP (プログラミング言語)|FP]]」から「[[Haskell]]」にも汲まれた。続く1966年に発表された「[[ISWIM]]」は手続き型と関数型を組み合わせたマルチパラダイムの原点的言語であり、[[ALGOL]]を参考にした構造化プログラミングに高階関数とwhereスコープが加えられていた。60年代の関数型プログラミングの歴史はもっぱらLISPの発展を中心にしていたが、ISWIMは後年の「ML」「Scheme」のモデルにされている。 |
|||
バッカスの{{lang|en|FP}}は広く使用されることはなかったが、この後関数型プログラミング言語の研究・開発は広まることとなった。1985年に{{lang|en|[[Miranda]]}}が登場した。1987年に、遅延評価の純粋関数型プログラミング言語の標準の必要性が認識され{{lang|en|Haskell}}の策定が始まった。1990年に{{lang|en|Haskell}} 1.0仕様がリリースされた。同じく1990年には{{lang|en|ML}}の標準である{{lang|en|[[Standard ML]]}}もリリースされている。 |
|||
'''1970年代''' |
|||
{{lang|en|Clean}}は1987年に登場したが、発展の過程で{{lang|en|Haskell}}の影響を受けている。1996年に、ML処理系のひとつであった{{lang|en|Caml}}に[[オブジェクト指向]]を追加した{{lang|en|OCaml}}が登場した。また日本ではSMLに独自の拡張を施した{{lang|en|[[SML#]]}}が開発されている。 |
|||
[[スタンフォード大学]]と[[エディンバラ大学]]で実施された[[対話型証明系|対話型]][[自動定理証明]]プロジェクト「''Logic for computable functions''」の中で1973年に導入された「[[ML (プログラミング言語)|ML]]」は[[代数的データ型]]、[[多態性|パラメトリック多相]]、[[型推論]]などを備えた関数型言語であり、計算機科学者[[ロビン・ミルナー]]によって開発された。また、1975年に[[MIT人工知能研究所]]の計算機科学者[[ガイ・スティール・ジュニア|ガイ・スティール]]と工学者[[ジェラルド・ジェイ・サスマン|ジェラルド・サスマン]]が設計してAIリサーチ用に導入された「[[Scheme]]」は任意タイミング評価(call/cc)可能な[[継続]]と[[ガーベジコレクション]]を備え、レキシカルスコープで構造化が図られており[[末尾再帰]]を最適化していた。MLとScheme双方の登場は関数型プログラミングのマイルストーンになった。同年代に代数的データ型を初めて導入し[[クリーネの再帰定理]]を証明実装した「Hope」と、関数の数学的純粋性を初めて重視した「SASL」も発表されている。1977年、[[バッカス・ナウア記法|BNF記法]]と[[FORTRAN]]開発の功績でこの年の[[チューリング賞]]を受けた計算機科学者[[ジョン・バッカス]]は「''Can Programming Be Liberated From the von Neumann Style? -A Functional Style and Its Algebra of Programs-''」と題した記念講演を行い、一説にはこれを境にして関数型(''functional'')というパラダイム名が定着したと言われているが、同時に発表された「[[FP (プログラミング言語)|FP]]」は関数水準(''function-level'')言語として紹介されている。[[ノイマン型]]からの脱却を題目に掲げたバッカスは、FPのプログラムをアトム+関数+フォーム(=高階関数)の階層構造と定義し、[[プロセス代数|代数]]を用いるフォームの結合で構築されると提唱した。 |
|||
21世紀に入ると、[[Java仮想マシン|{{lang|en|Java}}仮想マシン]]や[[共通言語基盤]]({{lang|en|CLI}})をランタイムとする関数型プログラミング言語を実装しようという動きが現れ、{{lang|en|[[Scala]]}}・{{lang|en|[[Clojure]]}}・{{lang|en|[[F Sharp|F#]]}}などが登場した。 |
|||
'''1980年代''' |
|||
MLの開発者ミルナーが発表していた型推論アルゴリズムが1982年に証明されると、パラメトリック多相に対応した[[型推論]]機能を眼目にした''Hindley–Milner''型体系が確立されて代数的データ型の実用性が高められた。80年代に入ると共に方言が多様化していたLISPとMLの両コミュニティで一定の標準化が求められるようになり、1983年に''Hindley–Milner''型体系を導入した「[[Standard ML]]」が発表された。それに対して1984年に発表された「[[Common Lisp]]」では[[手続き型プログラミング|手続き型パラダイム]]が強調されて関数型の枠組みからやや外れた方向性を示したので、関数型言語の枢軸はMLに置かれた。1985年にはML方言の代表格となる「Caml」が公開された。同じく1985年にSASLの後継として発表された「[[Miranda]]」は、遅延評価を標準にしながら関数の数学的純粋性を追求した言語であり、関数型プログラミング研究用[[オープン標準|オープンスタンダード]]のコンセンサスで1987年から策定が開始された[[Haskell]]のモデルになりその進捗を大きく後押しした。それと前後してMirandaは1987年公開の純粋関数型言語「[[Clean]]」にも大きな影響を与えている。Cleanは後発のHaskellをも叩き台にして改良を続けた。また関数型と[[並行計算]]の適性が認識される中で1986年の通信業界で開発された「[[Erlang]]」は[[並行プログラミング]]指向の面で特に注目を集めている言語である。1988年公開の「[[Wolfram (プログラミング言語)|Wolfram]]」はAPLスタイルのデータ集合処理に機能を豊富にしたパターンマッチングやイテレーションを加えた言語で90年代を通して改良が続けられていた。 |
|||
'''1990年代''' |
|||
1990年に関数型プログラミングの第二のマイルストーンと言える純粋関数型言語「[[Haskell]]」が初リリースされた。Haskellは遅延評価と型理論の文脈を形式化した型クラスと圏論由来のデザインパターンであるモナドの導入を特徴にしていた。1992年に[[動的型付け]]レコードと[[多重ディスパッチ]]を扱う関数型言語「[[Dylan]]」が登場した。1993年に[[ベクトル]]、[[行列]]、[[表 (データベース)|表テーブル]]などのデータ構造を扱えて[[統計的検定]]、[[時系列分析]]、[[データ・クラスタリング|クラスタリング]]分野に特化した関数型言語「[[R言語|R]]」が発表された。1995年にLISPの[[マクロ (コンピュータ用語)|マクロ]]機能を大幅に強化したコンポーネント指向により各分野に合わせた[[ドメイン固有言語]]として振る舞える「[[Racket]]」が登場した。1996年にはML方言のCamlにオブジェクト指向視点の[[抽象データ型]]を導入した「[[OCaml]]」が公開された。90年代の関数型プログラミングの歴史では関数の数学的純粋性に則った[[参照透過性]]の重視の他、[[オブジェクト指向プログラミング|オブジェクト指向]]との連携の模索が目立っていた。日本ではStandard MLに独自の拡張を施した「SML#」が発表されている。風変りなものに[[コンビネータ論理]]の形式に立ち返った「[[Unlambda]]」がある。1995年に公開された「Mercury」は関数型と[[論理型プログラミング|論理プログラミング]]の合の子のような言語であり、[[自動推論]]分野への応用に特化されていた。 |
|||
'''2000年代''' |
|||
2000年代になると関数型プログラミングへの注目度は更に高まり、マルチパラダイムに応用された関数型言語が様々に登場した。2003年のJava仮想マシン動作でオブジェクト指向と関数型を融合した「[[Scala]]」、2005年のマイクロソフト製のML方言「[[F Sharp|F#]]」、2007年のJava仮想マシン動作のLISP方言「[[Clojure]]」など数々のポピュラー言語が生み出されている。また、[[カリー=ハワード同型対応|カリー=ハワード同型対応]]の理論に基づいた{{仮リンク|プルーフアシスタント|en|Proof asistant|label=}}によるプログラム正当性の数学的証明を指向した関数型言語が支持され、2004年に「Epigram」2007年に「[[Agda]]」および純粋関数型「Idris」が発表されている。これらの言語では数学者[[ペール・マルティン=レーフ|マルティン・レーフ]]の{{仮リンク|直感的型理論|en|Intuitionistic type theory}}に基づく[[依存型]]を中心にした型システムが実現されている。関数型構文の有用性がより広く認識されるに従い、オブジェクト指向言語やスクリプト言語にも積極的に導入されるようになった。産業分野からも注目されるようになり、[[Constructive Solid Geometry|CSG]]幾何フレームワーク上で動く[[CAD]]への導入も始められた。しかし関数型コンセプトに馴染まないオペレーターが定数化規則による値の再代入制限に困惑して設計作業に支障をきたすなどの弊害も明らかになっている。 |
|||
== 代表的な関数型言語 == |
== 代表的な関数型言語 == |
||
'''[[LISP]]''' (1958年) |
|||
{|class="wikitable sortable" |
|||
!言語 |
|||
!純粋さ |
|||
!型付け |
|||
|- |
|||
|{{lang|en|[[Clean]]}}||純粋||強い、静的 |
|||
|- |
|||
|{{lang|en|[[Clojure]]}}||非純粋||動的 |
|||
|- |
|||
|{{lang|en|[[Erlang]]}}||非純粋||動的 |
|||
|- |
|||
|{{lang|en|[[F Sharp|F#]]}}||非純粋||強い、静的 |
|||
|- |
|||
|{{lang|en|[[Haskell]]}}||純粋||強い、静的 |
|||
|- |
|||
|{{lang|en|[[Idris]]}}||純粋||強い、静的 |
|||
|- |
|||
|{{lang|en|[[Lazy K]]}}||純粋||型なし |
|||
|- |
|||
|{{lang|en|[[LISP]]}}||非純粋||動的 |
|||
|- |
|||
|{{lang|en|[[Miranda]]}}||純粋||強い、静的 |
|||
|- |
|||
|{{lang|en|[[ML (プログラミング言語)|ML]]}}||非純粋||強い、静的 |
|||
|- |
|||
|{{lang|en|[[SML#]]}}||非純粋||強い、静的 |
|||
|- |
|||
|{{lang|en|[[Standard ML]]}}||非純粋||強い、静的 |
|||
|- |
|||
|{{lang|en|[[OCaml]]}}||非純粋||強い、静的 |
|||
|- |
|||
|{{lang|en|[[Scala]]}}||非純粋||強い、静的 |
|||
|- |
|||
|{{lang|en|[[Scheme]]}}||非純粋||動的 |
|||
|- |
|||
|{{lang|en|[[Unlambda]]}}||非純粋||型なし |
|||
|} |
|||
: 動的型付け、先行評価 |
|||
従来の手続き型と分類されるプログラミング言語においても、関数型プログラミングを行ないやすくなる機能を備えているものもある。[[C言語]]および[[C++]]は[[関数へのポインタ]]をサポートし、関数をオブジェクトのように扱うことができるが、関数ポインタによって[[第一級関数]]をサポートしているとみなされてはいない。なお、C# 3.0、[[C++11]]、Java 8など、後発の規格においてラムダ式([[無名関数]])をサポートするようになった言語もある。 |
|||
'''[[APL]]''' (1964年) |
|||
=== その他の関数的性質を持つ言語 === |
|||
* {{lang|en|[[APL]]}} |
|||
: 配列プログラミング言語、動的型付け、先行評価 |
|||
* {{lang|en|[[XSL Transformations|XSLT]]}} |
|||
'''[[ISWIM]]''' (1966年)← LISP、[[ALGOL]] |
|||
: 静的型付け、先行評価 |
|||
[[ML (プログラミング言語)|'''ML''']] (1973年)← ISWIM |
|||
: 静的型付け、先行評価 |
|||
'''[[Scheme]]''' (1975年)← LISP、ISWIM |
|||
: LISP方言、動的型付け、先行評価 |
|||
[[FP (プログラミング言語)|'''FP''']] (1977年)← APL |
|||
: 関数水準言語、動的型付け、先行評価 |
|||
'''[[Standard ML]]''' (1983年)← ML、Hope、[[Pascal|PASCAL]] |
|||
: ML方言、静的型付け、先行評価 |
|||
'''Caml''' (1985年)← ML |
|||
: ML方言、静的型付け、先行評価 |
|||
'''[[Miranda]]''' (1985年)← ML、Hope、SASL |
|||
: 純粋関数型言語、静的型付け、遅延評価 |
|||
'''[[Erlang]]''' (1986年)← LISP、[[Prolog]]、[[Smalltalk]] |
|||
: 動的型付け、先行評価、並行計算 |
|||
'''[[Clean]]''' (1987年)← Miranda |
|||
: 純粋関数型言語、静的型付け、遅延評価 |
|||
'''[[Haskell]]''' (1990年)← Scheme、Standard ML、Miranda、FP |
|||
: 純粋関数型言語、静的型付け、遅延評価 |
|||
'''[[Dylan]]''' (1993年)← Scheme、[[Common Lisp Object System|CLOS]]、[[ALGOL]] |
|||
: LISP方言、動的型付け、先行評価 |
|||
[[R言語|'''R''']] (1993年)← Scheme、[[Common Lisp Object System|CLOS]] |
|||
: 配列プログラミング言語、動的型付け、先行評価 |
|||
'''[[Racket]]''' (1995年)← Scheme、[[Eiffel]] |
|||
: LISP方言、動的型付け、先行評価 |
|||
'''[[OCaml]]''' (1996年)← Caml、Standard ML、[[Modula-3]] |
|||
: ML方言、静的型付け、先行評価、オブジェクト指向 |
|||
'''[[Scala]]''' (2003年)← Scheme、Standard ML、Haskell、Erlang、[[Smalltalk]]、[[Java]] |
|||
: 静的型付け、先行評価、オブジェクト指向、並行計算 |
|||
[[F Sharp|'''F#''']] (2005年)← Standard ML、Haskell、Erlang、Scala、[[Python]]、[[C♯]] |
|||
: ML方言、静的型付け、先行評価、並行計算 |
|||
'''[[Clojure]]''' (2007年)← Scheme、Haskell、Erlang、[[Java]] |
|||
: LISP方言、動的型付け、先行評価、並行計算 |
|||
'''[[Rust (プログラミング言語)|Rust]]''' (2010年)← Scheme、Standard ML、Haskell、Erlang、[[C♯]] |
|||
: 静的型付け、先行評価、並行計算 |
|||
== 関数型プログラミングの例 == |
|||
アルゴリズムの[[Hello world|Hello World]]と言える[[フィボナッチ数列|フィボナッチ数]]を求めるプログラムは、チュートリアルなどでよく引き合いに出されるものであり、本稿でも手続き型言語との比較を兼ねて取り上げる。一般的な手続き型言語によるソースコードは以下のようになる。<syntaxhighlight lang="pascal"> |
|||
FUNCTION fibona (num: INTEGER): INTEGER; |
|||
VAR |
|||
x, y, tmp: INTEGER; |
|||
BEGIN |
|||
x := 1; |
|||
y := 1; |
|||
FOR i := 2 TO num DO |
|||
BEGIN |
|||
tmp := x; |
|||
x := y; |
|||
y := y + tmp; |
|||
END; |
|||
fibona := y; |
|||
END; |
|||
</syntaxhighlight> |
|||
それに対して一般的な関数型言語によるソースコードは以下のようになる。<syntaxhighlight lang="haskell"> |
|||
let rec fibona num = if num < 2 then 1 else fibona (num-2) + fibona (num-1) |
|||
</syntaxhighlight> |
|||
コード行の羅列であるテキスト的な手続き型プログラミングと比較すると関数型プログラミングの方は、ガードとリミットによる分岐終点ルールで枠組みされたリーフ値と再帰関数のノードによるツリー化手順が一目で把握可能であり、ソースコードから式のツリー構造が直感的に浮かび上がってくる。同様のアルゴリズムで後続値とのペア(2-tuple)を表示するものは以下のようになる。<syntaxhighlight lang="haskell"> |
|||
let rec fibona num = |
|||
if num = 0 then (1, 1) else let (x, y) = fibona (num-1) in (y, x+y) |
|||
in |
|||
fibona 5 |
|||
result is (5, 8) |
|||
</syntaxhighlight> |
|||
<!-- |
|||
<syntaxhighlight lang="haskell"> |
|||
let |
|||
sum x = if x == 0 then 0 |
|||
else x + sum (x - 1) |
|||
in |
|||
sum 10 |
|||
</syntaxhighlight> |
|||
--> |
|||
== 脚注 == |
== 脚注 == |
||
{{脚注ヘルプ}} |
{{脚注ヘルプ}}'''注釈'''{{Notelist}}'''出典'''{{Reflist}} |
||
=== 注釈 === |
|||
{{Notelist}} |
|||
=== 出典 === |
|||
{{Reflist}} |
|||
== 外部リンク == |
== 外部リンク == |
||
* [http://www.sampou.org/haskell/article/whyfp.html なぜ関数プログラミングは重要か] |
* [http://www.sampou.org/haskell/article/whyfp.html なぜ関数プログラミングは重要か] |
||
* [http://www.topxml.com/xsl/articles/fp/ {{lang|en|The Functional Programming Language XSLT - A proof through examples}}] |
* [http://www.topxml.com/xsl/articles/fp/ {{lang|en|The Functional Programming Language XSLT - A proof through examples}}] |
||
{{Normdaten}} |
{{Normdaten}} |
||
{{プログラミング言語の関連項目}} |
{{プログラミング言語の関連項目}} |
2021年2月24日 (水) 14:07時点における版
この記事には独自研究が含まれているおそれがあります。 |
プログラミング・パラダイム |
---|
関数型言語(英: functional language)は、関数型プログラミングのスタイルまたはパラダイムを扱うプログラミング言語の総称である。関数型プログラミングは関数定義と関数適用を基礎にした宣言型プログラミングの一形態であり、関数は引数への適用から先行式の評価を後続式の適用につなげて終端の評価を導き出す式のツリー構造として定義される。式の評価に伴う副作用の発生には大きな注意が払われる。関数は引数ないし返り値として渡せる第一級オブジェクトとして扱われる。
関数型プログラミングは数理論理学と代数系をルーツにし、ラムダ計算とコンビネータ論理を幹にして構築され、LISP言語が実装面の先駆になっている。関数の数学的な純粋性を指向したものは純粋関数型言語と個別に定義されている。命令型プログラミング言語では単に有用な構文スタイルとして扱われている事が多い。高階関数、第一級関数、カリー化、クロージャ、継続、イテレータ、ジェネレータ、再帰、型推論、パターンマッチング、名前渡し、遅延評価、コンス、代数的データ型、型の多相、イミュータブル、モナドなどが関数型プログラミングの様式項目として挙げられる[誰?]。
一般的な関数型スタイル
最も身近な関数型プログラミングとは、データ集合に対する反復作用であり、リスト処理などと呼ばれているものである。手続き型言語やオブジェクト指向言語において、いわゆる関数型と呼ばれる構文が多用されるのもリスト処理の分野である。関数型思想の原点は「LISP」であるが、その実用性を知らしめた最初の言語は表計算処理に効率性を発揮した「APL」であった。リストを走査して各要素を順々に取り出すプロセスと、各要素に作用するプロセスを別々の関数にまとめて、前者に後者を渡すようにした仕組みが一般に言われる関数型の基本例である。他の関数を引数として受け取る事ができる関数は高階関数と呼ばれ、引数として渡す事ができる関数は第一級関数と呼ばれる。この二つの機能が関数型の支柱である。そのようなスタイルとしての意味での関数型プログラミングにおける代表的な機能または構文には以下のようなものが挙げられる。
- ラムダ式と無名関数は同じものである。無名関数は
引数→式
(例:x→x+1
)の形式で表現され、与えられた引数を加工して結果値を返す働きをする。高階関数への引数として使われることが多い。無名関数の式内に外部データを含んだものはクロージャと呼ばれる。クロージャの結果値はその時の外部データの状態に左右される事になる。
map / filter / reduce
- リスト処理用の高階関数であり、対象リストと無名関数を引数にする。mapはリスト内の各要素を無名関数の結果値に置き換える高階関数である。filterはリスト内の各要素を無名関数の真偽判定値で真なら抽出し、その抽出要素のリストを生成する高階関数である。reduceはリスト内の各要素の総和の結果値を生成する高階関数である。reduceの無名関数は前要素までの総和と現要素の二引数を取る。総和の和は和に限らず無名関数内で好きな計算にできる。
名前渡し / 遅延評価
- 引数を当てはめた無名関数(コードブロックまたはプロセス)を未計算のまま高階関数に渡せる仕組みを名前渡しという。予め組み立てた想定プロセスを全て高階関数に渡しておき、その高階関数側で必要になったプロセス結果値だけを求めるようにして計算量を減らすのが主な用途になる。引数確定時とは別タイミングで計算することを遅延評価という。遅延評価では高階関数側で無名関数の確定引数を設定し直したり、クロージャならばその時の計算タイミングの外部データ状態に従った結果値を得ることが可能になる。
- 型推論は静的型付けの機能であり、コンパイル時の解析でソースコード内のあらゆる関数適用や変数束縛が求める等価性と値のそれが一致しているかをチェックする。値の等価性は推論的型付け視点の「型」になる。同じ型でなかったら計算不可の型エラーになってコンパイルエラーになる。型の推論はプリミティブ記述、データ構築子定義、変数束縛(Var)関数定義(Abs)関数適用(App)等式(Let)型構築子定義(Gen)型構築子宣言(Inst)といったソースコード内のあらゆる記述ポイントを拾い上げて総合解析するという専用の型推論アルゴリズムで行われる。端的に言うとプリミティブという原子の組み合わせとその写像による変遷を精密に辿ってそれぞれの型を判別していると考えてよい。推論的型付けでは変数/引数/返り値に対する型宣言と型注釈は不要になり、むしろ計算全体の整合性を損ねるものとして倦厭される。命令型言語(手続き型やオブジェクト指向)の型推論アルゴリズムは簡素化されているので、型推論の使用対象はローカル変数や引数渡しが中心になる。この型推論の利点は、ローカル変数に型テンプレート的な表現の幅を持たせてソースコードの保守修正作業を容易にすることである。従来の型宣言と型注釈を用いる明示的型付けと、型推論の共存はC言語世代プログラミングに対する一つのパラダイムシフトでもある。
特徴
ここでは関数型プログラミング本来の考え方(パラダイム)に基づいて説明する。ステートメントを基本文にする命令型言語と、式を基本文にする関数型言語はどちらも最終的には命令型パラダイムに沿った機械コードに落とし込まれる事になるが、双方の間にはプログラミングに対する考え方に大きな隔たりがあると言える。
式と関数
関数型プログラムの基本文は式(expression)である。式は個体(individual)である値(value)と写像(mapping)である関数(function)の二つから構成される。関数の定義には演算子(operator)も含まれている。値は基本データ型(プリミティブ)と複合データ型(コンポジット)およびラムダ計算で言われる変数(variable)を意味する。変数は束縛変数と自由変数を指す。評価(evaluation)される前の式は、ラムダ計算で言われるネーム(name)と同義になる。ネームは数学上の数式または代数式に相当するものである。式内の変数部分が確定される前の式はラムダ抽象(abstraction)と同義になる。式内の変数部分を確定するのはラムダ適用(application)と同義になる。この式=ネームが評価されると値になり、これはラムダ計算で言われる簡約(reduction)と同義になる。式は値と同一視されるのですなわち式と値は相互再帰の関係にある。式内の値は他の式の評価値である事があり、その式内にもまた他の値があるといった具合である。この解釈は高階述語論理と呼ばれる。高階述語論理=高階関数の解釈下で引数または返り値として扱われる関数は第一級関数と呼ばれる。
関数型プログラミングの関数は”関数の型”(function type)で分類される存在量の値である。プログラム的には式に引数を結び付ける機能であり、これは関数を引数に適用する(applying a function to an argument)とされる。関数の式内の仮引数(parameter)箇所に渡された実引数(argument)がパターンマッチング手法で当てはめられ、先行(その時)または遅延(その後)タイミングで評価されて結果値が導出される。この仮引数箇所は束縛変数と呼ばれる。関数呼び出し時とは異なるタイミングで内容が決定される変数箇所は自由変数と呼ばれる。letとwhereで特定の式に向けて定義される変数はその式への束縛変数になる。なお関数型パラダイムでの自由変数の意味合いは他の宣言型パラダイムとはやや異なっている。関数適用時に用いられるパターンマッチング手法は、仮引数パターンの選言的な列挙を可能にしている。このパターンマッチングは”関数の型”に沿った等値性(equality)で仮引数と実引数を照合する。更にそれにガードと呼ばれる値の比較照合と範囲照合を加えることもできる。仮引数が非交和型の場合はその中で列挙されている型との等価性(equivalent)でも仮引数と実引数を照合できる。渡される実引数によってはボトム型になる関数もありこれは部分関数(partial function)と呼ばれる。ボトム型は式ないし関数の評価の失敗した終着点を意味する。演算子はデフォルトの式内容を持ち、その引数が単項演算子なら1個、二項演算子なら2個に限定された関数と同義である。
”関数の型”は「第1引数の型→第x引数の型→評価値の型」というように形式化されておりこれはカリー化(currying)と呼ばれる。例として関数funcの型をfunc::A→B→C
とするとこの場合、A型値に適用されたfuncはB→C
という”関数の型の値”を返す事になり、それをB型値に適用するとC型の評価値が返る事になる。左からの引数にひとつひとつ適用する形にして、B→C
のような中間的な”関数の型の値”が導出されるようにする仕組みが関数のカリー化である。カリー化は写像の量化(quantify)を扱う二階述語論理の表現手法でもある。カリー化によって関数funcの型はfunc::A→(B→C)
と読み替えられるようになり、この場合にAにのみ適用してB→C
という”関数の型の値”のまま保留することは部分適用(partial application)と呼ばれる。またカリー化による重要概念に関数合成(function composition)がある。これは二項の合成演算子.
を関数f::B→C
に適用すると(*→B)→(*→C)
が導出され、それを関数g::A→B
に適用すると関数f.g::A→C
が導出されるというものである。合成演算子の左側の定義域と右側の値域が同じ型の場合のみ合成できる。高階関数的な連結であるf(g A)
と働きかた的には同じであるが、パイプライン処理の方に該当する関数連結(関数チェーン)と、カリー化に則った関数合成は異なる概念である。カリー化による部分適用や合成演算子から導出された”関数の型の値”を、任意の変数に束縛して扱うのはポイントフリースタイル(point-free style)と呼ばれる。ポイントフリースタイルの変数を値に適用すると、評価された値が返されるかまたは”関数の型の値”が返される事になる。ポイントフリースタイル変数はvar value
の書式で自身の右の値を暗黙的に引数として取る。ポイントフリースタイルは高階述語論理と存在量化子の表現手法でもあり、継続渡しスタイルにも応用される。引数を部分適用された演算子はセクションと呼ばれてポイントフリースタイルでよく用いられる。カリー化準拠の”関数の型”は型理論の指数型(quotient type)に分類されるものである。
関数は名前付きと名前無しの二通りある。名前無しの関数は専らラムダ抽象を模した構文で定義される。式内に自由変数を内包しない方は単に無名関数と呼ばれ、自由変数を内包する方はそれを囲い込むという意味でクロージャと呼ばれる。自由変数は外部データへの接点になる。無名関数は引数をピュアマッピングする純粋関数である。クロージャの引数のマッピングは式内の自由変数に影響され、またその自由変数に作用する事もあるという副作用要素を閉包した非純粋関数である。リスト処理時にリストの各要素への作用子として渡される無名関数またはクロージャはイテレータと呼ばれる。同様にリスト処理時に渡されて各要素を参照しながらそれらの総和値または選別リストまたは更新リストを生成する方はジェネレータと呼ばれる。これはイミュータブル重視時に多用される。関数の名前は、それに結び付けられた式または式ツリーの不動点の表現になる。自式の不動点を式内に置いて新たな引数と共に高階述語論理の式として評価する手法は再帰と呼ばれる。関数の終端式での再帰は実引数の更新+先端式へのアドレスジャンプと同等に見なせるのでもっぱらそちらに最適化されてこれは末尾再帰と呼ばれる。末尾再帰は論理性を損なわずにスタックフリーの無制限ループを可能にする実装概念として重視されている。
値とデータ構造
関数型プログラミングの値(value)は型(type)で分類される定数または全称量の変数である。これは基本データ型(プリミティブ)と複合データ型(コンポジット)のいずれかで表現される。プリミティブは数値、論理値、文字値、文字列を指す。コンポシットはプリミティブを任意に組み合わせた複合体であり、例としては構造体や共用体などを指す。その組み合わせ方に焦点を当てた用語がデータ構造(data structure)である。データ構造という概念には再帰、アノテーション、ガード、操作的意味といった暗黙情報をも含められるので、コンポジットの具体的形式といった意味で用いられる。関数型言語で用いられるデータ構造の代表は、代数的データ型とS式である。双方ともデータ構築子(data constructor)から構築される。まず、プリミティブがデータ構築子によってまとめられる。データ構築子はC言語の構造体/共用体と同性質のものであり、むしろ言い換えるとC言語は直積型のデータ構造を構造体にし、非交和型のデータ構造を共用体にしてペア定義している。データ構築子は入れ子構造と、自身を入れ子にした再帰構造も定義できる。プリミティブとデータ構築子を任意に組み合わせて代数的データ型やS式といったデータ構造が構築される。データ構造内のプリミティブとデータ構築子の組み合わせ方はパターン(pattern)と呼ばれる。そのパターンが型になり、パターンの構築が型付けになり、パターンを量化(quantify)すると型付け値になり、これはターム(term)と呼ばれる。タームは冒頭の値(value)を指す。データ構築子のパターンの末端は必ずプリミティブになるので、パターン内の全てのプリミティブの値を決定することが量化になる。お互いのパターンがマッチするターム同士は等価(equivalent)とされる。この等価は同じ型と読み替えてもよい。等価性はあらゆる計算の可否(計算可能性)を決定する。計算とは関数適用または演算子適用を指し、それらが求める仮引数と実引数にするタームが等価であればその計算は成立する事になる。データ構造のパターンは基礎パターンに分解されて解釈される。基礎パターンは型理論に従って直積型、非交和型、ユニオン型、オプション型、帰納型、ユニット型などに分類されている。
S式は二分木構造のデータ構造である。これはコンス(cons)と呼ばれる二項のデータ構築子の連結で形成される。コンスは二つの要素を持つタプルであり、要素はプリミティブまたは他のコンスのどちらかである。S式はコンスを実行時に連結して任意のパターンを構築する動的型付けの値である。コンスは要素二つの直積型(product type)であり、コンスの連結による要素の並びはリストと呼ばれる。コンスの要素は形式化されていない非交和型(sum type)でもあり、要素の識別はプログラマ側の裁量に委ねられている。コンスの組み合わせによるパターンは任意の識別名に結び付けられる。
代数的データ型はAND-OR木構造のデータ構造である。これは直積型(product type)と非交和型(sum type)を表現する多項のデータ構築子の組み合わせで形成される。データ構築子は任意個数の要素を持つものであり、要素はプリミティブまたは他のデータ構築子のどちらかである。代数的データ型はデータ構築子を事前に組成定義して任意のパターンを構築する静的型付けの値である。直積型はタプルまたはレコードのパターンを表わす。レコードは指定フィールド取得用関数を随伴させたものである。非交和型は列挙型またはタグ共用体のパターンを表わす。前者は等値性(equality)で識別される非交和である。後者は等価性(equivalent)で識別される非交和であり、こちらはユニオン型(union type)とも呼ばれる。ユニット型(unit type)は空集合のパターンを表わし、実装面ではnilまたはvoidの表現になる。ユニット型とそうでない型の二択の非交和型はオプション型(option type)とされ、実装面ではMaybe値の表現になる。データ構築子を再帰的にネスティングするパターンは帰納型(inductive type)とされる。非交和型と帰納型とユニット型の組み合わせは連結リストや二分木のパターンを表わす。データ構築子の組み合わせによるパターンは任意の型構築子(type constructor)に結び付けられて同時にそれが識別名義になる。データ構築子がパターンの表現に用いられるのに対して、型構築子はパターン内の要素(プリミティブないしデータ構築子)の多相化に用いられる。多相化はパターン内の要素を型変数(type variable)に置き換え、型構築子への型引数(type parameter)で要素の型を決定するという形で行われる。型構築子が必要とする型引数の個数および形態によるパターンはカインド(kind)と呼ばれる。型構築子はカインドによって分類される。代数的データ型は識別名義と実装内容を分離して双方を自由に組み合わせるという意味でしばしば抽象化される。これは仮名義の型名(=型構築子)を用いてプログラムを記述しておき、プログラム冒頭でその仮名義に実際の型名を当てはめるものであり、その仮名義の定義は型シノニムまたは型エイリアスと呼ばれる。
評価戦略
関数型プログラミングの評価戦略(evaluation strategy)は、関数を値にする評価タイミングと、引数欄内関数の評価タイミング(call-by-What)の二つを定義している。関数を値にする評価タイミングは、正格評価(strict evaluation)と非正格評価(non-strict evaluation)の二つに大別されている。正格評価の関数は引数確定と同時に評価されて値になる。この評価タイミングに注目した方は先行評価(eager evaluation)と呼ばれる。引数確定と同時にボトム型(評価失敗)が発生することも包括した呼称が正格評価である。非正格評価の関数は、引数確定されても未評価のまま保留状態にされる。後続式で改めて他の関数/演算子の引数にされた時に初めて評価されて値になり、または改めて変数に束縛された時に初めて評価されて値になる。この評価タイミングに注目した方は遅延評価(lazy evaluation)と呼ばれる。評価されるまでボトム型の発生が保留されることも包括した呼称が非正格評価である。これが遅延評価のデフォルトタイミングであるが、好きなタイミングで遅延評価できる無名関数/クロージャもあり、それは継続(continuation)と呼ばれる。その任意タイミングの評価値導出はcall/cc(現行継続呼出)と呼ばれる。遅延評価は必要以外の評価をスキップして処理を高速化するが、評価値と未評価関数の区別が難しくなるというジレンマがある。
引数欄内関数の評価タイミング(call-by-What)には、値渡し(call by value)と名前渡し(call by name)がある。値渡しは先行評価に相当するものであり、関数の評価値が引数として渡される。名前渡しは遅延評価に相当するものであり、未評価のまま保留された関数が引数として渡される。なお、双方ともに引数確定されていない場合はただの第一級関数(関数の型の値)として渡されることになる。また名前渡しの亜流に必要渡し(call by need)があり、これは一度名前渡しされた関数+引数はその評価値をメモ化されて、同じ関数+引数が再度名前渡しされた時はそのメモ化評価値の方を渡すという仕組みである。必要渡しは純粋関数型言語で実装されている。
データ構造でも遅延評価の概念は扱われており、帰納、再帰、無限、極限といった代数表現の実装手段になっている。代数的データ型ではタグ共用体、連結リスト、再帰データ型は遅延評価対象である。連結リストは無限リストと構造上同義であり遅延評価が無限性質の実装を可能にしている。
参照透過性
参照透過性(referential transparency)とは、関数は同じ引数値に対する同じ評価値を恒久的に導出し、その評価過程において現行計算枠外の情報資源に一切の影響を及ぼさないというプロセス上の枠組みを意味する。現行計算枠外のいずれかの情報要素が変化するのと同時にいずれかの関数の評価過程も変化してしまう現象が副作用(side effect)と呼ばれる。参照透過性=副作用の排除でもある。副作用のない関数を純粋関数(pure function)と呼ぶ。副作用が伴なう操作の代表例は値の再代入と入出力処理とシステムコールである。この参照透過性は、副作用発生部分とそうでない部分を分けて処理構成の見通しを良くするなどのプログラム設計の視点から語られる事が多く、手続き型やオブジェクト指向ではその利点から解釈してよいものであるが、関数型パラダイムではプロセス有向グラフの整合性を維持するという視点から解釈する必要がある。プロセス有向グラフとはプログラム初期値からのあらゆる個体(値)と写像(関数)の繋がりを表わす図式である。あらゆる値の変遷が初期値まで遡れるという参照透過性が保証された関数型プログラムは、その正当性の形式的検証および数学的証明の自動化が可能になりこれが本質的な利点になる。プロセス有向グラフの解析と模型化は、プロセス代数と呼ばれ並行プログラミングの支柱になる。関数型の世界で値の再代入がタブーとされるのは、それが言わば履歴の改竄になってプロセス有向グラフの整合性を崩すからである。従って束縛変数と、旧値の更新を新値の生成で代替したイミュータブルが重視される。ループは関数の再帰で表現され、分岐は選言パターンマッチングとガードで表現される。以上が参照透過性の仕様上の意味である。
参照透過性の実装はコーディングレベルで副作用を排除できる部分への対応と、そうではない部分への対応に大別される。前者は再代入などを指しプログラマに委ねられる問題になる。後者は入出力処理やシステムコールなどを指しこちらではそうはいかない。後者の参照透過性は、副作用原因になる全ての情報要素を関数の引数に収納し、副作用結果になった全ての情報要素を関数の返り値に収納するという形式で表現される。情報要素には理論上は関連するコンピュータ環境状態が全て内包されている事になるが、その忠実な実践は当然不可能なので、コンピュータ環境状態を抽象化した「抽象環境」で代用される。抽象環境は言語処理系から提供される特殊データである。この抽象環境を関数の引数とその返り値に常時含めて、コーディングレベルの副作用排除はプログラマが解決する事で理論上は参照透過性が順守されている事になる。この論理的なトリック方式が参照透過性の実装上の意味である。抽象環境を用いて参照透過性を維持しつつ再代入/入出力/システムコールなどを行うための機能には、派生構造型システムとモナドがある。派生構造型システムは、抽象環境をライナー型(linear type)にして関数に渡される度にその中のIDデータをユニーク更新させる。抽象環境にユーザーデータを注入する仕組みはアフィン型(affine type)、抽象環境からユーザーデータを抽出する仕組みは関連型(relevant type)と呼ばれる。毎時ユニーク更新されるライナー型のIDデータは、各種入出力によって実際には変化しているコンピュータ環境の時系列状態を完全に抽象化して、それらを理論上各個照会可能にしているマッピングキーである。これによってコンピュータ環境状態の変化もプロセス有向グラフで論理的に辿れることになるので同時に参照透過性も維持されていることになる。派生構造型システムの実装例にはユニークネス型がある。ライナー型、アフィン型、関連型を組み合わせての特にその応用コーディングは煩雑なボイラープレートコードになりがちだったので、それらを圏論と代数的構造由来のアイディアでより平易かつ簡潔にした手法がモナドである。
型システム
型システム |
---|
主要カテゴリ |
静的型付け vs 動的型付け 強い vs 弱い 明示的 vs 型推論 名前的 vs 構造的 ダックタイピング |
マイナーカテゴリ |
部分型 再帰型 部分構造型 依存型 漸進的型付け フロータイピング 潜在的型付け |
型理論のコンセプト |
直積型 - 直和型 交差型 - 共用型 単一型 - 選択型 帰納型 - 精製型 トップ型 - ボトム型 函数型 - 商型 全称型 - 存在型 一意型 - 線形型 |
関数型プログラミングの型システム(type system)は、型付けラムダ計算準拠の型理論に基づいて構築されている。ここでは型システム上の対比に沿った形で記述する。関数型言語の二大系統の内、ML系は静的型付けベースであり、LISP系は動的型付けベースである。
静的型付け
関数型言語の静的型付けでは、性質や役割による意味づけによって値を分類する明示的型付け(manifest typing)よりも、計算可能性に基づく等価性によって値を分類する推論的型付け(inferred typing)が主流である。前者の意味づけとはプログラマによる型定義、型宣言、型注釈を指しており人間寄りの視点である。後者の等価性とは値を関数/演算子の引数にできるかどうかの判別を指し、値への関心がそこで計算可能かどうかに絞られているので計算機寄りの視点である。明示的型付けではソースコード上の型宣言と型注釈から値の型が特定されるのに対し、推論的型付けでは型推論機能で特定される。型推論とは専用のアルゴリズムによる解析によってソースコード内のあらゆる値/変数/引数/返り値それぞれの等価性を導き出す機能である。推論的型付けでは値への関心をその計算可能性に絞っているので、型宣言と型注釈は不要になりまたは相容れないものとなる。例としてint型を型シノニムで金額型と数量型にした場合、明示的型付けではこの両者は区別されるが、推論的型付けでは区別されない。ソースコードの解析でどちらもint型準拠の等価と見られるからである。推論的型付けで値の意味づけ性も表現する場合は、データ構築子で値を包むボックス化が用いられる。データ構築子(data constructor)は与えられた要素を直積または非交和でまとめるのと同時に型理論で言われる文脈(context)を各要素の等価性に上乗せ付加するものでもある。
静的型付けにおけるデータ構造のパターン(型)はコンパイル前ないし実行前に全て事前形成される。その実装例である代数的データ型はデータ構築子の組み合わせでパターンを構築し、パラメトリック多相に基づいて総称化したパターン内の要素=型変数を、型構築子への型引数の組み合わせで特定した。Hindley–Milner型体系はこのパラメトリック多相に対応した型推論機能を提供している。型構築子(type constructor)は必要とする型引数の個数によって分類され、これはカインド(kind)と呼ばれる。カインドは総称記号である*
の写像で型構築子の型種を表現する。型引数を必要としない型構築子と必要な型引数を全て付与された型構築子はプロパータイプと呼ばれ*
と表現される。プロパータイプは全称量の型である。型引数を1個必要とするものは*→*
になり、2個必要なら*→*→*
になる。これらは存在量の型になる。*→*→*
に型引数が1つ付与されると*→*
になり更に1つ付与すると*
のプロパータイプになる。全称量の型付け値(ターム)は普通に扱えるが、存在量の型付け値はその一部分が抽象化(大抵は環境依存値と同義)されたままの特別な値と見なされて一定の制限下で扱われる。
推論的型付け下の関数の扱いでは、人為的表記による意味づけを重視した記名的型付け(nominal typing)が取り入れられており、これで推論的型付けの枠組み内での関数オーバーロードが表現されている。ここでの人為的表記による意味づけとは、型構築子/データ構築子/”関数の型”それぞれのパターン内の型変数に、型理論で言われる文脈(context)を付加することを指している。文脈の付加は制約(constraint)と呼ばれる。文脈の付加はアドホック多相と考えられており、代表的な実装例はそれとジェネリックプログラミングを組み合わせた型クラスである。型クラスは、引数/計算値/評価値などを総称型化したジェネリック関数群の”関数の型”と式内容を定義できる機能であり、同時に推論的型付けと共存する関数オーバーロードの実装と、特定の意味づけ型を扱うための関数モジュールを定義するための手段になっている。型クラスの定義構文では上述のジェネリック関数群が定義され、その型クラス名が文脈記号になる。型構築子の定義に文脈を付加すると、その型クラスのジェネリック関数群にその型構築子=型を当てはめた関数群がコンパイル時に自動生成される(deriving)。また文脈を付加して当てはめ関数群を自動生成(instance)した上で、その型構築子=型のための当てはめ関数の式内容を個別定義(where)できる構文もある。この双方がジェネリック関数の特有インスタンス化になる。明示的型付けでは型注釈を付けた引数パターンの列挙というシンプルな手段で関数オーバーロードを表現できるが、推論的型付けでは等価性に上乗せした文脈という二段階の手段が必要になる。記名的型付けと併せた推論的型付けでのオーバーローディング関数の選択決定は、始めに仮引数と実引数の型クラスのみに注目した照合が行われ、次にその型クラスの制約内での型推論照合が行われるという形になる。
動的型付け
動的型付けにおけるデータ構造のパターン(型)はコンパイル前ないし実行前の事前形成に加えて、実行中の随時にも事後形成できる。その実装例であるS式は、二項データ構築子(コンス)の実行時の連結で形式化されていないパターンを構築し、プログラマの裁量による任意の実行時チェックでパターンの意味づけと計算に用いるための等価性を判別するといったものである。パターンの組み合わせが明確に形式化されておらずその識別をプログラマの裁量に委ねており、またチェックタイミングも委ねられている事から、これは潜在的型付け(latent typing)と呼ばれている。潜在的型付けは動的型付けの原型的位置づけである。
もう一つの実装例として動的なレコード(record)がある。この動的レコードは内部的には動的配列と同じものであり、配列の各スロットに任意の値の参照が納められて、そのスロットは構造体のフィールドと同じものになる。スロットは増設削減可能である。レコードのタグ名はそのまま型名になる。レコードを量化した値(インスタンス)には、システム側が別途用意する型情報が結び付けられており、変数への束縛ないし代入および関数/演算子への引数代入時に毎回自動的に型判別される。型情報と型判別タイミングが形式化されているのでこれは動的型付けとなる。動的型付けのインスタンスを関数の引数にすることは動的な関数オーバーロードを自然表現し、これはオブジェクト指向に倣って多重ディスパッチとも呼ばれる。レコード・フィールドのアクセスは、フィールド名関数をインスタンスに適用するという方法で行われる。オブジェクト指向のinstance.field
が関数型ではfield instance
のようになる。同じフィールド名関数から得られる値の型は、適用するインスタンスの型構造による実行時多態になる。この仕組みは構造的型付け(structural typing)に沿ったものである。
モナド
A monad is just a monoid in the category of endofunctors, what's the problem?
(モナドは自己関手の圏のただのモノイドだよ。何か問題でも?) — Philip Wadler
モナド(monad)の説明でよく引用されるこの文言はその特徴を明快に表したヒントと言われる。モナドは圏論由来の自己関手の合成の仕組みで参照透過性を維持しつつ、代数的構造由来のモノイドの仕組みで非純粋関数の連結体を平易に表現する機能である。非純粋関数の連結体がそのまま一連の副作用内包プロセスになりそれを参照透過性の枠内で実行できる。モナドはプログラム的には、関数の量化を扱う二階述語論理の実装であるカリー化の活用形態である関数合成と部分適用の応用手法であり、参照透過性欄で説明した派生構造型システムのライナー型の役割を圏に見立てたデータ構築子に持たせている。またデザインパターン的には、関数を引数値に適用する(applying a function to an argument)通常の関数とは正反対に、モナドでは値を関数に適用する(applying a value to a function)形態を取っており、値を同名関数に反復適用しての特殊な再帰を表現できる。モナドでは値に特殊な演算子を適用することでそれを関数に適用できるようにしている。
モナドの世界では、基本値から導出されるモナド値をモナド関数に次々と適用して状態遷移を表現する。モナド値(monadic value)とはMaybe値、例外発生、入出力環境、システムコール、クロージャ、継続といったあらゆる副作用プロセスを扱うためのオブジェクトである。モナド関数(monadic function)とはそのオブジェクトのメソッドと考えてよいものである。モナド値とモナド関数は合成演算子を通した表裏一体の存在であり、これがモノイドを指している。合成演算子をモナド値に適用したものをモナド関数に適用して導出されたモナド値を、また同じ手順でモナド関数に適用するといった繰り返しになる。これはモナド合成体(monadic composition)と呼ばれる。モナド値はMA
のように型表現され、A
は基本値、M
は基本値を包むコンテキストまたはコンテナである。モナド関数は(A→MB)
を基本とする関数の型の値であり、与えられた基本値からモナド値を操作して新たなモナド値を返す。モナド値はMA
の型を基本とし、対象内容によってM(A?)
、M(A+E)
、M(W×A)
、E→MA
、S→M(A×S)
、(A→MR)→MR
といった型の値または関数の型の値にリフト(lift)される。これはモナド変換子(monad transformer)と呼ばれる。また、モナド値のMA
のM
を空データにして事実上のA
にしモナド関数も事実上の(A→B)
にした恒等モナド(identity monad)もあるがこれは全くの便宜目的である。モナド値は状態遷移の記録媒体であり、η自然変換演算子を基本値に適用することで導出される。基本値+αから成るモナド値または基本値を包含するモナド値はデータ構築子として表現されて自己関手の圏の枠組みとなり、そのデータ構築子を束縛した型構築子への型引数でモナド値内の型決定がなされる。モナドは大まかに言うと以下の六つの要素から実装されている。
- モナドの文脈を付加された型構築子。一つの副作用内包プロセスを扱う関数群モジュールに見立てられる。
- 基本値+αのコンテナであるモナド値=データ構築子。圏に見立てられる。
- μ自然変換演算子(join)kleisli-star拡張演算子(bind)といった合成用の二項演算子。
- 基本値とη自然変換演算子(return)とモナド値。モナド値+二項演算子でモノイドに見立てられる。
- モナド関数。パターンマッチング分岐可能でモジュール内の操作を一手に扱う。自己関手内容に見立てられる。
- ファンクタ文脈からの持ち上げ演算子(fmap)は補助的機能。関手に見立てられる。
モナド値は付加モナドと自由モナド(Maybe/例外/有限リストモナドなど)以外では、実質的に存在量の値になるので普通の値のように扱うことは出来ない。ただし付加/自由モナドでも参照透過性を維持するためには存在量と同等に扱う必要が出てくる。return
を基本値に適用してモナド値を表現することからモナド処理は始まる。基本値とは扱うモナドに合わせた任意の値である。そのモナド値は圏としてのユニークIDを持つことになる。ここで基本値をA
としそのモナド値をMA
とする。MA
にbindを適用して(A→MB)→MB
という写像を導出する。その写像は先のMA
と同じ圏IDを備えたものになる。その写像をモナド関数(A→MB)
に適用すると、そのモナド関数内では渡されたA
やその他の値などにreturn
を適用して表現されるモナド値の圏IDはMA
のもので共通化される。モナド関数内においてのreturn
は基本値をモナド値に代入する機能と見てよい。return
は用途別関数にそれぞれラッピングされて使われるのが普通である。空引数からモナド値を表現するreturn
もありこれはモナドプリミティブ(monadic primitive)と呼ばれ、この場合はモナド値の圏IDが暗黙引数になっている。モナド値はファイルハンドルのようなものと考えると分かりやすくなり、モナド値を直接引数にできる専用関数も存在する。モナド関数内ではモナド値から基本値を取り出す演算子が有効になる。それはA←MA
のように表現されてコモナド(comonad)と呼ばれる機能になる。抽出した基本値からの処理の中で再度return
が行われる。モナド関数は自己関手内容に見立てられているので、その中ではreturn
の繰り返しによる事実上の再代入処理が許されている。その論理的な辻褄合わせの要点になるbind
の正当性および計算可能性を表現するためにファンクタ則とモナド則の等式がプログラム内で定義されている。ここでいわゆる圏論の知識が必要になるがその説明は先送りする。モナド関数はモナド値を返しそれに再度bind
を適用できるのでこれがモノイドを意味している。モナド関数の外でのreturn
は毎時ユニークな圏IDのモナド値を表現するので同じ基本値でもその都度異なる圏が表現される事になり、これが自然変換(関手圏の射)演算子の呼称由来になっている。
モナドはファンクタ(functor)の派生文脈にされることが多いが、これはbind
を形成するクライスリ射とjoin
の合成の持ち上げ(関手)にfmap
が使われるからである。ファンクタ文脈は関手fmap
を持つ。そのままファンクタの機能名で呼ばれることが多いfmap
は、関数(A→B)
から関数(TA→TB)
を導出する関手=関数である。この関数はTA
に適用できてTB
を導出できる。T
は基本値を包むコンテキストまたはコンテナでありその代表例はリストである。基本値に対する作用をコンテキストで拡張解釈できるのがファンクタの利点である。例えば基本値への+1という作用をリストのコンテキストで拡張解釈するのはリストの全要素に+1するという意味になる。これはリストをそのまま計算対象にできる利便性に繋がる。ファンクタの派生文脈にアプリカティブ(applicative)がある。アプリカティブは、ファンクタのコンテキストに包まれた関数を「コンテキストに包まれた先頭引数→コンテキストに包まれた残り引数&評価値」という1引数の関数に変換する演算子<*>
を持つ。<*>
はF(A→B)
から(FA→FB)
を導出する演算子である。F
はコンテキスト、(A→B)
は元の関数、(FA→FB)
は1引数の関数である。このFB
は多相であり実際は値F*
関数F(*→*)
関数F(*→*→*)
などになっているのでそれに<*>
を再び適用できる。アプリカティブは、2個以上の引数の関数をファンクタするための機能と考えてよい。2個以上の引数の関数はfmap
でそのまま持ち上げられないのでアプリカティブ関手のpure
が用いられる。これはA→FA
と表現されA
が関数、F
がコンテキストである。pure
によって好きな引数個数の関数をコンテキストで包み<*>
をそれに適用して導出された関数をFA
に適用できる。アプリカティブ関手pure::A→FA
とモナドのη自然変換演算子return::A→MA
は同じ働きに見えるが、前者は関数(関数の型の値)を純粋に持ち上げるだけの関手なのに対して、後者は持ち上げる関手を毎時垂直合成していく関手圏の射(自然変換)であるという性質上の違いがある。
歴史
1930年代に数学者アロンゾ・チャーチによって発明されたラムダ計算は関数適用を計算単位にした形式体系であり、1937年に数学者アラン・チューリング自身によりチューリング完全の性質が明らかにされて、チューリングマシンと等価な計算模型である事が証明されている。この経緯からラムダ計算は関数型プログラミングの基底に据えられた。ラムダ計算と同等の計算理論にコンビネータ論理があり、1920年代から1930年代にかけて数学者ハスケル・カリーらによって発明されている。こちらは関数型プログラミングの原点である高階述語論理式の基礎モデルにされた。チャーチはラムダ計算を拡張してその各タームに型を付与した型付けラムダ計算も考案しており、これは関数型プログラミングにおける型理論と型システムの源流になった。
1950年代
初の関数型プログラミング言語とされる「LISP」は、1958年にマサチューセッツ工科大学の計算機科学者ジョン・マッカーシーによって開発された。LISPの関数はラムダ計算の形式を元に定義され再帰可能に拡張されており、式のリスト化とその遅延評価および高階評価など幾つかの関数型的特徴を備えていた。LISPは数多くの”方言”を生み出しているが、その中でも「Scheme」「Dylan」「Racket」「Clojure」「Julia」は関数型の特徴を明確にした言語である。1956年に公開された「Information Processing Language」の方が先駆であるが、こちらはアセンブリベースの低水準言語なので前段階扱いである。IPLが備えていたニーモニックコードのリストをオペランドにできるジェネレータ機能はLISPに影響を与えたと言われる。高階オペランド演算は高階関数と同じ働きをし、メモリ一括処理のストリング命令の効率を高めるなどした。
1960年代
1964年に計算機科学者ケネス・アイバーソンが開発した「APL」は、計算タイプを定義された関数記号に各種配列データを適用する機能を中心にした言語であり、特にスプレッドシート処理に対する効率性が認められて、1960年代以降の商業分野と産業分野に積極導入された。APLは多次元配列などのデータ集合に対する関数適用の有用性を特に証明した言語になった。そのデータ集合処理を注目されたAPLからは「J言語」「S」「A」「K」「Q」といった派生言語が生み出されている。APLの記号構文は一つの手本になりその流れは後年の「FP」から「Haskell」にも汲まれた。続く1966年に発表された「ISWIM」は手続き型と関数型を組み合わせたマルチパラダイムの原点的言語であり、ALGOLを参考にした構造化プログラミングに高階関数とwhereスコープが加えられていた。60年代の関数型プログラミングの歴史はもっぱらLISPの発展を中心にしていたが、ISWIMは後年の「ML」「Scheme」のモデルにされている。
1970年代
スタンフォード大学とエディンバラ大学で実施された対話型自動定理証明プロジェクト「Logic for computable functions」の中で1973年に導入された「ML」は代数的データ型、パラメトリック多相、型推論などを備えた関数型言語であり、計算機科学者ロビン・ミルナーによって開発された。また、1975年にMIT人工知能研究所の計算機科学者ガイ・スティールと工学者ジェラルド・サスマンが設計してAIリサーチ用に導入された「Scheme」は任意タイミング評価(call/cc)可能な継続とガーベジコレクションを備え、レキシカルスコープで構造化が図られており末尾再帰を最適化していた。MLとScheme双方の登場は関数型プログラミングのマイルストーンになった。同年代に代数的データ型を初めて導入しクリーネの再帰定理を証明実装した「Hope」と、関数の数学的純粋性を初めて重視した「SASL」も発表されている。1977年、BNF記法とFORTRAN開発の功績でこの年のチューリング賞を受けた計算機科学者ジョン・バッカスは「Can Programming Be Liberated From the von Neumann Style? -A Functional Style and Its Algebra of Programs-」と題した記念講演を行い、一説にはこれを境にして関数型(functional)というパラダイム名が定着したと言われているが、同時に発表された「FP」は関数水準(function-level)言語として紹介されている。ノイマン型からの脱却を題目に掲げたバッカスは、FPのプログラムをアトム+関数+フォーム(=高階関数)の階層構造と定義し、代数を用いるフォームの結合で構築されると提唱した。
1980年代
MLの開発者ミルナーが発表していた型推論アルゴリズムが1982年に証明されると、パラメトリック多相に対応した型推論機能を眼目にしたHindley–Milner型体系が確立されて代数的データ型の実用性が高められた。80年代に入ると共に方言が多様化していたLISPとMLの両コミュニティで一定の標準化が求められるようになり、1983年にHindley–Milner型体系を導入した「Standard ML」が発表された。それに対して1984年に発表された「Common Lisp」では手続き型パラダイムが強調されて関数型の枠組みからやや外れた方向性を示したので、関数型言語の枢軸はMLに置かれた。1985年にはML方言の代表格となる「Caml」が公開された。同じく1985年にSASLの後継として発表された「Miranda」は、遅延評価を標準にしながら関数の数学的純粋性を追求した言語であり、関数型プログラミング研究用オープンスタンダードのコンセンサスで1987年から策定が開始されたHaskellのモデルになりその進捗を大きく後押しした。それと前後してMirandaは1987年公開の純粋関数型言語「Clean」にも大きな影響を与えている。Cleanは後発のHaskellをも叩き台にして改良を続けた。また関数型と並行計算の適性が認識される中で1986年の通信業界で開発された「Erlang」は並行プログラミング指向の面で特に注目を集めている言語である。1988年公開の「Wolfram」はAPLスタイルのデータ集合処理に機能を豊富にしたパターンマッチングやイテレーションを加えた言語で90年代を通して改良が続けられていた。
1990年代
1990年に関数型プログラミングの第二のマイルストーンと言える純粋関数型言語「Haskell」が初リリースされた。Haskellは遅延評価と型理論の文脈を形式化した型クラスと圏論由来のデザインパターンであるモナドの導入を特徴にしていた。1992年に動的型付けレコードと多重ディスパッチを扱う関数型言語「Dylan」が登場した。1993年にベクトル、行列、表テーブルなどのデータ構造を扱えて統計的検定、時系列分析、クラスタリング分野に特化した関数型言語「R」が発表された。1995年にLISPのマクロ機能を大幅に強化したコンポーネント指向により各分野に合わせたドメイン固有言語として振る舞える「Racket」が登場した。1996年にはML方言のCamlにオブジェクト指向視点の抽象データ型を導入した「OCaml」が公開された。90年代の関数型プログラミングの歴史では関数の数学的純粋性に則った参照透過性の重視の他、オブジェクト指向との連携の模索が目立っていた。日本ではStandard MLに独自の拡張を施した「SML#」が発表されている。風変りなものにコンビネータ論理の形式に立ち返った「Unlambda」がある。1995年に公開された「Mercury」は関数型と論理プログラミングの合の子のような言語であり、自動推論分野への応用に特化されていた。
2000年代
2000年代になると関数型プログラミングへの注目度は更に高まり、マルチパラダイムに応用された関数型言語が様々に登場した。2003年のJava仮想マシン動作でオブジェクト指向と関数型を融合した「Scala」、2005年のマイクロソフト製のML方言「F#」、2007年のJava仮想マシン動作のLISP方言「Clojure」など数々のポピュラー言語が生み出されている。また、カリー=ハワード同型対応の理論に基づいたプルーフアシスタントによるプログラム正当性の数学的証明を指向した関数型言語が支持され、2004年に「Epigram」2007年に「Agda」および純粋関数型「Idris」が発表されている。これらの言語では数学者マルティン・レーフの直感的型理論に基づく依存型を中心にした型システムが実現されている。関数型構文の有用性がより広く認識されるに従い、オブジェクト指向言語やスクリプト言語にも積極的に導入されるようになった。産業分野からも注目されるようになり、CSG幾何フレームワーク上で動くCADへの導入も始められた。しかし関数型コンセプトに馴染まないオペレーターが定数化規則による値の再代入制限に困惑して設計作業に支障をきたすなどの弊害も明らかになっている。
代表的な関数型言語
LISP (1958年)
- 動的型付け、先行評価
APL (1964年)
- 配列プログラミング言語、動的型付け、先行評価
- 静的型付け、先行評価
ML (1973年)← ISWIM
- 静的型付け、先行評価
Scheme (1975年)← LISP、ISWIM
- LISP方言、動的型付け、先行評価
FP (1977年)← APL
- 関数水準言語、動的型付け、先行評価
Standard ML (1983年)← ML、Hope、PASCAL
- ML方言、静的型付け、先行評価
Caml (1985年)← ML
- ML方言、静的型付け、先行評価
Miranda (1985年)← ML、Hope、SASL
- 純粋関数型言語、静的型付け、遅延評価
Erlang (1986年)← LISP、Prolog、Smalltalk
- 動的型付け、先行評価、並行計算
Clean (1987年)← Miranda
- 純粋関数型言語、静的型付け、遅延評価
Haskell (1990年)← Scheme、Standard ML、Miranda、FP
- 純粋関数型言語、静的型付け、遅延評価
Dylan (1993年)← Scheme、CLOS、ALGOL
- LISP方言、動的型付け、先行評価
- 配列プログラミング言語、動的型付け、先行評価
- LISP方言、動的型付け、先行評価
OCaml (1996年)← Caml、Standard ML、Modula-3
- ML方言、静的型付け、先行評価、オブジェクト指向
Scala (2003年)← Scheme、Standard ML、Haskell、Erlang、Smalltalk、Java
- 静的型付け、先行評価、オブジェクト指向、並行計算
F# (2005年)← Standard ML、Haskell、Erlang、Scala、Python、C♯
- ML方言、静的型付け、先行評価、並行計算
Clojure (2007年)← Scheme、Haskell、Erlang、Java
- LISP方言、動的型付け、先行評価、並行計算
Rust (2010年)← Scheme、Standard ML、Haskell、Erlang、C♯
- 静的型付け、先行評価、並行計算
関数型プログラミングの例
アルゴリズムのHello Worldと言えるフィボナッチ数を求めるプログラムは、チュートリアルなどでよく引き合いに出されるものであり、本稿でも手続き型言語との比較を兼ねて取り上げる。一般的な手続き型言語によるソースコードは以下のようになる。
FUNCTION fibona (num: INTEGER): INTEGER;
VAR
x, y, tmp: INTEGER;
BEGIN
x := 1;
y := 1;
FOR i := 2 TO num DO
BEGIN
tmp := x;
x := y;
y := y + tmp;
END;
fibona := y;
END;
それに対して一般的な関数型言語によるソースコードは以下のようになる。
let rec fibona num = if num < 2 then 1 else fibona (num-2) + fibona (num-1)
コード行の羅列であるテキスト的な手続き型プログラミングと比較すると関数型プログラミングの方は、ガードとリミットによる分岐終点ルールで枠組みされたリーフ値と再帰関数のノードによるツリー化手順が一目で把握可能であり、ソースコードから式のツリー構造が直感的に浮かび上がってくる。同様のアルゴリズムで後続値とのペア(2-tuple)を表示するものは以下のようになる。
let rec fibona num =
if num = 0 then (1, 1) else let (x, y) = fibona (num-1) in (y, x+y)
in
fibona 5
result is (5, 8)
脚注
注釈
出典