「関数型プログラミング」の版間の差分

削除された内容 追加された内容
タグ: Refタグつき記述の除去 ビジュアルエディター
46行目:
関数型プログラミングの[[型システム]]は、[[型付きラムダ計算]]ベースの[[型理論]]に基づいたスタイルで実装されている。型システムの分類に従った対比で述べると、[[命令型言語]]では明示的型付け(''manifest typing'')が多用されるのに対し、関数型では暗示的型付け(''inferred typing'')が多用される。また関数型では、個別名化した部品から全体を組み上げてその構成内容で全体を識別できるようにする構造的型付け(''structural typing'')よりも、共通名化した部品から全体を組み上げてそれに便宜的タグを付けて全体を識別できるようにする記名的型付け(''nominal typing'')の方がよく用いられる。これはアドホック多相に該当するものであり、実例は[[型クラス]]である。
 
関数型初期の[[LISP]]系の[[S式]]は、コンスを実行時に連結するスタイルでデータストラクチャを扱う潜在的型付け(''latent typing'')であり、これは動的型付け(''dynamic typing'')に分類されるものであった。その後のML系を境にして一定の形式を事前に定めた[[静的型付け]](''static typing'')がどちらかと言うと主流になった。その実装の[[代数的データ型]]は、直積と非交和を表現する型構築子の帰納で形成される静的なデータストラクチャであり、パラメトリック多相で総称化された。''Hindley–Milner''型体系は、このパラメトリック多相に完全対応できる型推論機能を提供して強い静的型付けの実装をサポートした。値をメモリ上のビット列ではなく、構造的な代数式と見なすのが関数型プログラミングの特徴である。前者はただのビット列を識別するためにコンパイラがその識別情報を他に持ち、それを参照する為に予め型名を明示(''manifest'')せねばならないが、後者は値自体が識別情報を兼ねているので常時その推論(''inferred'')で型名を識別できる。この時に用いられる[[型推論]]は、代数的データ型のパターンを等価性を審査できる形まで簡約するような型理論に沿った用法だけでなく、ソースコードの前後の文脈までリサーチして型を導き出せるという実用的な機能である。これによって関数型言語での型注釈と型宣言はもっぱら省略される。
 
[[多態性]]三種の三番目であるサブタイピングは、データのセマンティクスまたは振る舞いの多相を扱う性質から関数型とは相容れない部分が多い。関数型の[[代数的データ型]]とオブジェクト指向の[[抽象データ型]]は対象的なデータストラクチャと見なされている。前者がデータのみの表現体であるのに対して、後者はセマンティクスを主にしたデータの表現体である。しかしオブジェクト指向との連携が模索される中で数々の手法も提示されている。動的型付けメインのLISP系ではS式の代わりに、各スロットに任意の変数を[[動的束縛|動的バインディング]]できるレコードを用いる。その動的束縛用レコードを1個以上引数にできる関数によって[[多重ディスパッチ]]が表現される。動的束縛用レコードの型チェックは[[ダックタイピング]]で行われる。静的型付けメインのML系ではパラメトリック多相を備えた総称化抽象データ型が用いられる。その型引数=型変数には抽象データ型が代入される。関数型プログラミングのサブタイピングは総称化本体の継承関係ではなく、型変数の方の継承関係を用いるのが特徴である。総称化本体のジェネリックインスタンスは型変数の方の継承関係で結ばれる。型変数の派生で結ばたジェネリックインスタンス総称してヴァリアントと呼ばれる。ヴァリアントは型変数の継承関係による仮想関数を提供する。この時にアドホック多相に該当する型境界(''type bound'')で型引数=型変数を修飾させて静的な型チェックをサポートさせる事もある。
 
== 歴史 ==
147行目:
 
== 関数型プログラミングの例 ==
関数型プログラミングは「計算とは何か」という数学の理論を基礎にしており、関数型プログラミングがもつ[[計算モデル]]は'''関数モデル'''である<ref>計算モデル2 関数モデル. (中略) 関数モデルに基づくプログラミング言語. 関数型言語. Lisp [http://nous.web.nitech.ac.jp/individual/inuzuka/lecture/PLT/PLT07/ 犬塚信博 (2007)「プログラミング言語論 第1回 イントロダクション」名古屋工業大学]</ref>。たとえば、1 から 10 までの整数を足し合わせるプログラムを考える{{efn|本来は[[等差数列]]の和の公式を用いて定数時間で問題を解く方法が最適解だが、ここではプログラミングスタイルの比較のため数値計算的手法を用いる。}}。[[命令型プログラミング]]では以下のように[[ループ (プログラミング)|ループ]]文を使って変数に数値を足していく(計算機の状態を書き換える)命令を繰り返し実行するという形を取る。
手続き型言語による[[フィボナッチ数列]]<syntaxhighlight lang="c">
 
int fibona (int num) {
* [[Pascal]]による例:
int x = 1, y = 1, tmp = 0;
<syntaxhighlight lang="pascal">
for (int i = 2; i < num; i++) {
program test;
tmp = x;
var total, i : x = yInteger;
begin
y += tmp;
total := 0;
}
for i := 1 to 10 do
return y;
total := total + i;
}
WriteLn(total)
end.
</syntaxhighlight>
 
一方、関数型プログラミングでは、繰り返しには一時変数およびループを使わず、[[サブルーチン|関数]]の[[再帰呼び出し]]を使う。
関数型言語によるフィボナッチ数列<syntaxhighlight lang="haskell">
 
let rec fibona num = if num <= 2 then 1 else fibona (num - 1) + fibona (num - 2)
* [[F Sharp|F#]]による例:
<syntaxhighlight lang="fsharp">
printfn "%d" (let rec sum x = if x > 0 then x + sum (x - 1) else 0
sum 10)
</syntaxhighlight>
<!--
171 ⟶ 177行目:
</syntaxhighlight>
-->
 
ただし再帰呼び出しは[[スタックオーバーフロー]]の危険性やオーバーヘッドを伴うため、注意深く使用しなければならない<ref>[https://msdn.microsoft.com/ja-jp/library/dd233229(v=vs.120).aspx 関数 (F#) | MSDN]</ref>。通例、関数型言語では、[[末尾再帰]]呼び出し (tail-recursive call) の形で書かれた関数をループに展開する[[末尾再帰#末尾呼出し最適化|末尾呼出し最適化]]により、スタックオーバーフローの危険性および再帰のオーバーヘッドを解消できる。[[Scheme]]など、関数型言語の中には末尾再帰呼び出しの最適化を仕様で保証するものもある。再帰関数を末尾再帰に書き換えることが難しいケースも有り、そのような場合は一般的なループが採用される。
 
また、関数型言語は文 (statement) よりも式 (expression) を中心とした言語仕様となっていることも特徴である。前述の例において、再帰関数<code>sum</code>を[[束縛 (情報工学)|束縛]]する<code>let</code>は式である。また、条件分岐の<code>if-then-else</code>も式である。文よりも式で書けることが多いほうが都合がよい。
 
関数型言語は関数型プログラミングをサポートする言語ではあるが、手続き型プログラミングを行なうことも可能である。例えばF#では以下のようなPascal風の書き方もできる。
 
<syntaxhighlight lang="fsharp">
let mutable total = 0
for i = 1 to 10 do
total <- total + i
printfn "%d" total
</syntaxhighlight>
 
ただし[[Haskell]]のようにループ構文をサポートせず、従来の手続き型プログラミングが難しいケースもある。
 
逆に手続き型言語を使って関数型プログラミングを行なうことも可能であるが、末尾再帰呼び出しの最適化がサポートされるかどうかはコンパイラ次第である。
 
== 脚注 ==