コンピュータサイエンスにおいて、関数型プログラミングとは、関数を適用・合成することによってプログラムを構築するプログラミングパラダイムである。これは宣言型プログラミングパラダイムであり、関数定義は、プログラムの実行状態を更新する一連の命令文ではなく、値を他の値にマッピングする式のツリー構造となる。
関数型プログラミングでは、関数は第一級のエンティティとして扱われます。つまり、他のデータ型と同様に、関数は名前(ローカル識別子を含む)にバインドしたり、引数として渡したり、他の関数から返したりすることができます。これにより、小さな関数をモジュール式に組み合わせる、宣言的で構成可能なスタイルのプログラムを作成できます。
関数型プログラミングは、すべての関数を決定論的な数学関数、つまり純粋関数として扱う関数型プログラミングのサブセットである純粋関数型プログラミングと同義とみなされることがあります。純粋関数は、特定の引数で呼び出されると常に同じ結果を返し、可変状態やその他の副作用の影響を受けません。これは、命令型プログラミングでよく見られる、副作用(プログラムの状態を変更したり、ユーザーから入力を受け取ったりするなど)を持つ可能性のある不純な手続きとは対照的です。純粋関数型プログラミングの支持者は、副作用を制限することで、プログラムのバグが少なくなり、デバッグやテストが容易になり、形式検証に適していると主張しています。[ 1 ] [ 2 ]
関数型プログラミングは、関数のみに基づく形式的な計算システムであるラムダ計算から発展した学術分野にルーツがあります。関数型プログラミングは歴史的に命令型プログラミングほど人気がありませんでしたが、現在では、 Common Lisp、Scheme、[ 3 ] [ 4 ] [ 5 ] [ 6 ] Clojure、Wolfram Language、[ 7 ] [ 8 ] Racket、 [9 ] Erlang、[ 10 ] [ 11 ] [ 12 ] Elixir、 [13] OCaml 、 [ 14 ] [ 15 ] Haskell、[ 16 ] [ 17 ]およびF# [ 18 ] [ 19 ]など、多くの関数型言語が産業界や教育現場で使用されています。Leanは、数学の定理を検証するためによく使用される関数型プログラミング言語です。[ 20 ]関数型プログラミングは、Web のJavaScript [ 21 ] 、統計のR [ 22 ] [ 23 ]、金融分析のJ、K、Q 、 XMLのXQuery / XSLT [ 24 ] [ 25 ]のように、特定の分野で成功を収めている言語の鍵でもあります。SQLやLex / Yaccのようなドメイン固有の宣言型言語は、可変値を許可しないなど、関数型プログラミングの要素を使用しています。 [ 26 ]さらに、D [ 27 ] (2007 年のD2のリリース以降)、C++ ( C++11以降) 、 C# [ 28 ] Kotlinなど、他の多くのプログラミング言語は関数型スタイルでのプログラミングをサポートしているか、関数型プログラミングの機能を実装しています。[ 29 ] Perl、 [ 30 ] PHP、 [ 31 ] Python、 [ 32 ] Go、 [ 33 ] Rust、 [ 34 ] Raku、 [ 35 ] Scala、 [ 36 ]およびJava(Java 8 以降)。 [ 37 ]
ラムダ計算は、1930年代にアロンゾ・チャーチによって開発された、関数適用から構築された形式的な計算システムです。1937年にアラン・チューリングは、ラムダ計算とチューリングマシンが計算の等価モデルであることを証明し、[ 38 ]ラムダ計算がチューリング完全であることを示しました。ラムダ計算は、すべての関数型プログラミング言語の基礎を形成しています。同等の理論的定式化である組み合わせ論理は、 1920年代と1930年代にモーゼス・シェーンフィンケルとハスケル・カリーによって開発されました。 [ 39 ]
チャーチは後に、より弱いシステムである単純型ラムダ計算を開発しました。これは、すべての項にデータ型を割り当てることでラムダ計算を拡張したものです。[ 40 ]これが静的型付け関数型プログラミングの基礎となっています。
最初の高水準関数型プログラミング言語であるLisp は、1950 年代後半にマサチューセッツ工科大学(MIT)に在籍していたジョン・マッカーシーによってIBM 700/7000 シリーズの科学計算機向けに開発されました。 [ 41 ] Lisp の関数は、チャーチのラムダ記法を使用して定義され、再帰関数を可能にするためにラベル構造が追加されました。[ 42 ] Lisp は関数型プログラミングの多くのパラダイム的特徴を初めて導入しましたが、初期の Lisp はマルチパラダイム言語であり、新しいパラダイムが進化するにつれて、多数のプログラミングスタイルをサポートするようになりました。SchemeやClojureなどの後の方言、およびDylanやJuliaなどの派生言語は、クリーンな関数型コアを中心に Lisp を簡素化および合理化しようとしましたが、Common Lisp は、置き換えられた多数の古い方言のパラダイム的特徴を維持および更新するように設計されました。[ 43 ]
情報処理言語(IPL) (1956 年) は、コンピュータベースの関数型プログラミング言語の最初のものとして挙げられることがあります。[ 44 ]これは、シンボルのリストを操作するためのアセンブリスタイルの言語です。関数を引数として受け取る関数に相当するジェネレータの概念があり、低レベルのプログラミング言語であるため、コードはデータになり得るので、IPL は高階関数を持つとみなすことができます。しかし、IPL は、ミューテーション リスト構造や同様の命令型機能に大きく依存しています。
ケネス・E・アイバーソンは1960年代初頭にAPLを開発し、1962年の著書『プログラミング言語』 (ISBN)でその内容を説明した。 9780471430148APLは、ジョン・バッカスのFPに最も大きな影響を与えた。1990年代初頭、アイバーソンとロジャー・ホイはJを作成した。1990年代半ば、以前アイバーソンと共同研究していたアーサー・ホイットニーはKを作成し、Kは後継のQとともに金融業界で商業的に使用されている。
1960年代半ば、ピーター・ランディンは関数型プログラミング言語のための最初の抽象マシンであるSECDマシン[ 45 ]を発明し[ 46 ]、ALGOL 60とラムダ計算の間の対応関係を説明し[ 47 ] [ 48 ]、ISWIMプログラミング言語を提案した[ 49 ]。
ジョン・バッカスは、 1977年のチューリング賞受賞講演「プログラミングはフォン・ノイマン・スタイルから解放されるか? 関数型スタイルとそのプログラム代数」で関数型プログラミングを紹介した。 [ 50 ]彼は、関数型プログラムを「プログラム代数」を可能にする「組み合わせ形式」によって階層的に構築されるものと定義している。現代の言葉で言えば、これは関数型プログラムが構成性の原理に従うことを意味する。[ 51 ]バッカスの論文は関数型プログラミングの研究を普及させたが、現在関数型プログラミングに関連付けられているラムダ計算スタイルではなく、関数レベルプログラミングを強調していた。
1973 年にエジンバラ大学のRobin Milnerが言語ML を作成し、David Turner がセント アンドリュース大学で言語SASL を開発しました。また、1970 年代にはエジンバラで Burstall と Darlington が関数型言語NPL を開発しました。[ 52 ] NPL はKleene 再帰方程式に基づいており、プログラム変換に関する彼らの研究で初めて紹介されました。[ 53 ]その後、Burstall、MacQueen、および Sannella はML の多相型チェックを組み込んで言語Hopeを作成しました。[ 54 ] ML は最終的にいくつかの方言に発展し、現在最も一般的なのはOCamlとStandard MLです。
1970年代、ガイ・L・スティールとジェラルド・ジェイ・サスマンは、ラムダ論文集および1985年の教科書『コンピュータプログラムの構造と解釈』で説明されているように、Schemeを開発した。Schemeは、レキシカルスコープを使用し、末尾呼び出し最適化を必須とする最初のLisp方言であり、関数型プログラミングを促進する特徴を備えている。
1980年代、ペル・マルティン=レーフは直観主義型理論(構成型理論とも呼ばれる)を開発し、関数型プログラムを依存型として表現された構成的証明と関連付けました。これは対話型定理証明への新しいアプローチにつながり、その後の関数型プログラミング言語の開発に影響を与えました。[ 55 ]
デイビッド・ターナーによって開発された遅延評価型関数型言語「ミランダ」は、1985年に初めて登場し、 Haskellに大きな影響を与えた。ミランダは独自規格であったため、Haskellは1987年に合意形成を経て、関数型プログラミング研究のためのオープンスタンダードを形成するに至った。実装版のリリースは1990年以降も継続されている。
最近では、CGALフレームワーク上に構築されたOpenSCAD言語のパラメトリックCADなどのニッチな分野で利用されているが、値の再割り当てに制限があるため(すべての値が定数として扱われる)、関数型プログラミングの概念に馴染みのないユーザーの間で混乱が生じている。[ 56 ]
関数型プログラミングに特有の概念やパラダイムがいくつかあり、それらは一般的に命令型プログラミング(オブジェクト指向プログラミングを含む)には馴染みのないものです。しかし、プログラミング言語はしばしば複数のプログラミングパラダイムに対応しているため、「主に命令型」言語を使用しているプログラマーは、これらの概念の一部を利用している可能性があります。[ 61 ]
高階関数とは、他の関数を引数として受け取るか、あるいは他の関数を結果として返すことができる関数のことです。微積分学における高階関数の例としては、微分演算子が挙げられます。関数の導関数を返す。
高階関数は、関数を他の関数の引数や戻り値として使用できるという点で、第一級関数と密接に関連しています。両者の違いは微妙です。「高階」は、他の関数に作用する関数の数学的な概念を表すのに対し、「第一級」は、使用に制限のないプログラミング言語の要素を表すコンピュータサイエンスの用語です(したがって、第一級関数は、数値などの他の第一級要素と同様に、他の関数の引数や戻り値として、プログラム内のどこにでも出現できます)。
高階関数は、部分適用(カリー化)を可能にします。これは、関数を引数に一つずつ適用し、適用するたびに次の引数を受け取る新しい関数を返す手法です。これにより、例えば、後続関数を自然数1に部分適用した加算演算子として簡潔に表現できます。
純粋関数(または式)には副作用(メモリや入出力)がありません。つまり、純粋関数にはいくつかの有用な特性があり、その多くはコードの最適化に利用できます。
命令型プログラミング言語のほとんどのコンパイラは純粋関数を検出し、純粋関数呼び出しに対して共通部分式の除去を実行しますが、一般的にこの情報を公開しないプリコンパイル済みライブラリに対しては必ずしもこれを実行できないため、これらの外部関数を含む最適化が妨げられます。gcc などの一部のコンパイラは、プログラマが外部関数を明示的に純粋としてマークして、このような最適化を可能にするための追加のキーワードを追加します。Fortran 95も関数を純粋として指定できます。[ 62 ] C++11 はconstexpr同様の意味を持つキーワードを追加しました。
関数型言語における反復(ループ)は、通常、再帰によって実現されます。再帰関数は自身を呼び出し、基本ケースに到達するまで操作を繰り返します。一般に、再帰ではスタックを維持する必要があり、スタックは再帰の深さに比例してメモリを消費します。そのため、命令型ループの代わりに再帰を使用すると、処理コストが非常に高くなる可能性があります。しかし、末尾再帰と呼ばれる特殊な形式の再帰は、コンパイラによって認識され、命令型言語で反復を実装するために使用されるコードと同じコードに最適化できます。末尾再帰の最適化は、コンパイル時にプログラムを継続渡しスタイルに変換するなど、さまざまな方法で実現できます。
Scheme言語標準では、実装が適切な末尾再帰をサポートすることを要求しており、これは、アクティブな末尾呼び出しの数に制限がないことを意味します。[ 63 ] [ 64 ]適切な末尾再帰は単なる最適化ではなく、ユーザーが再帰を使用してループを表現でき、そうすることでメモリを節約できることを保証する言語機能です。[ 65 ]さらに、その名前とは裏腹に、末尾再帰だけでなく、すべての末尾呼び出しを考慮します。適切な末尾再帰は通常、コードを命令型ループに変換することで実装されますが、実装によっては他の方法で実装される場合もあります。たとえば、Chicken は意図的にスタックを保持し、スタックオーバーフローを許容します。ただし、この場合、ガベージコレクタがスペースを回収し、[ 66 ]末尾再帰をループに変換しないにもかかわらず、アクティブな末尾呼び出しの数に制限がないようにします。
再帰の一般的なパターンは、高階関数を用いることで抽象化することができ、カタモルフィズムとアナモルフィズム(または「フォールド」と「アンフォールド」)が最も分かりやすい例である。このような再帰スキームは、命令型言語におけるループなどの組み込み制御構造と同様の役割を果たす。
ほとんどの汎用関数型プログラミング言語は無制限の再帰を許可し、チューリング完全であるため、停止問題が決定不能になり、等式推論の不健全性を引き起こす可能性があり、一般的に言語の型システムによって表現される論理に矛盾を導入する必要があります。Rocqのような特殊目的の言語は、整礎再帰のみを許可し、強く正規化します (非停止計算は、コデータと呼ばれる無限の値のストリームでのみ表現できます)。結果として、これらの言語はチューリング完全ではなく、特定の関数を表現することは不可能ですが、無制限の再帰によって導入される問題を回避しながら、幅広いクラスの興味深い計算を表現することができます。整礎再帰に限定され、他のいくつかの制約がある関数型プログラミングは、完全関数型プログラミングと呼ばれます。[ 67 ]
関数型言語は、厳密評価(即時評価)を使用するか、非厳密評価(遅延評価)を使用するかによって分類できます。これらの概念は、式が評価される際に関数の引数がどのように処理されるかを指します。技術的な違いは、失敗する計算や分岐する計算を含む式の表示的意味論にあります。厳密評価では、失敗する部分項を含む項の評価はすべて失敗します。たとえば、Python の次のステートメントが該当します。
print ( len ([ 2 + 1 , 3 * 2 , 1 / 0 , 5 - 4 ]))厳密評価では、リストの3番目の要素でゼロ除算が発生するため、この関数は失敗します。遅延評価では、length関数は値4(つまり、リスト内の項目の数)を返します。これは、length関数の評価時にリストを構成する項の評価が行われないためです。簡単に言うと、厳密評価では、関数呼び出しの前に必ず関数引数が完全に評価されます。遅延評価では、関数呼び出し自体を評価するために引数の値が必要でない限り、関数引数は評価されません。
関数型言語における遅延評価の一般的な実装戦略はグラフ削減である。[ 68 ]遅延評価は、 Miranda、Clean、Haskellなど、いくつかの純粋関数型言語でデフォルトで使用されている。
Hughes 1984 は、データ ストリームのプロデューサーとコンシューマーの独立した実装を容易にすることで、関心の分離を通じてプログラムのモジュール性を向上させるメカニズムとして遅延評価を主張しています。 [ 2 ] Launchbury 1993 は、特にプログラムのストレージ要件の分析において遅延評価がもたらすいくつかの困難について説明し、そのような分析を支援する操作的意味論を提案しています。 [ 69 ] Harper 2009 は、厳密評価と遅延評価の両方を同じ言語に含め、言語の型システムを使用してそれらを区別することを提案しています。[ 70 ]
特に1970年代にHindley–Milner型推論が開発されて以来、関数型プログラミング言語は、コンパイル時にすべての無効なプログラムを拒否し、偽陽性エラーのリスクがある型付きラムダ計算を使用する傾向がありました。これに対し、Lispとその派生言語( Schemeなど)で使用されている型なしラムダ計算は、コンパイル時にすべての有効なプログラムを受け入れ、偽陰性エラーのリスクがあります。これは、有効なプログラムを拒否しないのに十分な情報がある場合、実行時にすべての無効なプログラムを拒否するためです。代数的データ型を使用すると、複雑なデータ構造の操作が便利になります。強力なコンパイル時型チェックが存在すると、テスト駆動開発などの他の信頼性技術がない場合でもプログラムの信頼性が向上します。また、型推論により、ほとんどの場合、プログラマはコンパイラに型を手動で宣言する必要がなくなります。
Rocq、Agda、Cayenne、Epigramなどの研究指向の関数型言語は、型が項に依存することを可能にする直観主義型理論に基づいています。このような型は依存型と呼ばれます。これらの型システムには決定可能な型推論がなく、理解やプログラミングが困難です。[ 71 ] [ 72 ] [ 73 ] [ 74 ]しかし、依存型は高階論理で任意の命題を表現できます。したがって、 Curry-Howard同型性により、これらの言語で適切に型付けされたプログラムは、コンパイラが認証済みコードを生成できる形式的な数学的証明を記述する手段となります。これらの言語は主に学術研究(形式化された数学を含む)で関心を集めていますが、工学でも使用され始めています。Compcertは、 Rocqで記述され形式的に検証された言語Cのサブセットのコンパイラです。[ 75 ]
一般化代数データ型(GADT)と呼ばれる限定的な依存型は、依存型プログラミングの利点の一部を提供しつつ、その不便さのほとんどを回避する方法で実装できます。 [ 76 ] GADT は、グラスゴーHaskell コンパイラ、OCaml [ 77 ]、Scala [ 78 ]で利用可能であり、Java や C# などの他の言語への追加として提案されています。[ 79 ]
関数型プログラムには代入文がありません。つまり、関数型プログラムでは、変数の値は一度定義されると変更されません。これにより、どの変数も実行のどの時点でも実際の値に置き換えることができるため、副作用が発生する可能性がなくなります。したがって、関数型プログラムは参照透過性があります。[ 80 ]
C言語の代入文を考えてみましょうx = x * 10。これは変数に代入される値を変更します。変数xの初期値がxだったとすると、変数を 2 回連続して評価すると、それぞれ と になります。明らかに、を または に置き換えるとプログラムの意味が変わるため、この式は参照透過性がありません。実際、代入文は決して参照透過性を持つことはありません。1x10100x = x * 1010100
さて、別の関数を考えてみましょう。例えば、は入力 x を暗黙的に変更しないため、そのような副作用がなく、透過的です。関数型プログラムはもっぱらこのタイプの関数を使用するため、参照透過的です。intplusOne(intx){returnx+1;}
純粋関数型データ構造は、命令型データ構造とは異なる方法で表現されることが多い。[ 81 ]例えば、定数アクセスおよび更新時間を持つ配列は、ほとんどの命令型言語の基本要素であり、ハッシュテーブルやバイナリヒープなどの多くの命令型データ構造は配列に基づいている。配列は、純粋関数型実装が可能なマップやランダムアクセスリストに置き換えることができるが、アクセスおよび更新時間は対数的である。純粋関数型データ構造は、データ構造の以前のバージョンを変更せずに保持する特性である永続性を持つ。Clojure では、永続データ構造は、命令型データ構造の関数型代替として使用される。例えば、永続ベクトルは、部分更新にツリーを使用する。挿入メソッドを呼び出すと、すべてのノードではなく一部のノードが作成される。[ 82 ]
関数型プログラミングは命令型プログラミングとは大きく異なります。最も大きな違いは、関数型プログラミングでは副作用を回避する点にあります。命令型プログラミングでは、状態や入出力の実装に副作用が用いられます。純粋な関数型プログラミングは副作用を完全に排除し、参照透過性を提供します。
高階関数は、従来の命令型プログラミングではほとんど使用されません。従来の命令型プログラムでは、ループを使用してリストを走査および変更することがよくあります。一方、関数型プログラムでは、関数とリストを受け取り、各リスト項目にその関数を適用して新しいリストを生成して返す高階の「マップ」関数を使用するのが一般的です。
以下の 2 つの例 ( Javaで記述) は同じ効果を実現します。配列内のすべての偶数を 10 倍してすべてを加算し、最終的な合計を変数に格納しますresult。
従来の命令型ループ:
int [] numList = { 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 }; int result = 0 ; for ( int i : numList ) { if ( i % 2 == 0 ) { result += i * 10 ; } }高階関数を用いた関数型プログラミング:
import java.util.Arrays ;int [ ] numList = { 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 } ; int result = Arrays.stream ( numList ) .filter ( n - > n % 2 == 0 ) .map ( n - > n * 10 ) .reduce ( 0 , Integer :: sum ) ;関数型プログラミングが提供する抽象化によって、複雑な命令型コードを大量に構築する際に発生する可能性のあるオフバイワンエラーなどの特定の問題を回避できる、より堅牢なコードの開発につながる場合があります(グリーンスパンの第10のルールを参照)。
銀行口座の残高管理など、状態管理を用いることで最も自然に実装できると思われるタスクがいくつかあります。純粋関数型プログラミングは、これらのタスクや、ユーザー入力の受け入れや画面への出力といった入出力タスクを、異なる方法で実行します。
純粋関数型プログラミング言語Haskell は、圏論から派生したモナドを使用してこれらを実装しています。[ 83 ]モナドは、純粋性を損なうことなく、命令型で可変状態 (および I/O などの他の副作用) を伴う計算のモデリングを含む (ただしこれに限定されない) 特定のタイプの計算パターンを抽象化する方法を提供します。既存のモナドは、適切なテンプレートと例があればプログラムに簡単に適用できますが、多くの学生は、たとえば新しいモナドを定義するように求められた場合 (特定のタイプのライブラリで必要になる場合がある) に、概念的に理解するのが難しいと感じています。[ 84 ]
関数型言語では、不変の状態を渡すことによって状態をシミュレートすることもできます。これは、関数が状態をパラメータの 1 つとして受け取り、結果とともに新しい状態を返し、古い状態は変更しないことで実現できます。[ 85 ]
非純粋関数型言語は通常、可変状態を管理するためのより直接的な方法を備えています。たとえば、 Clojure は、現在の状態に純粋関数を適用することで更新できる管理参照を使用します。このようなアプローチは、計算を表現する好ましい方法として純粋関数の使用を促進しつつ、可変性を可能にします。[ 86 ]
プログラムの副作用を追跡するために、ホーア論理や一意性などの代替手法が開発されてきた。現代の研究言語の中には、副作用の存在を明示するために効果システムを使用するものもある。 [ 87 ]
関数型プログラミング言語は、一般的に、 CやPascalなどの命令型言語よりもCPUとメモリの使用効率が低い。[ 88 ]これは、配列などの可変データ構造が、現在のハードウェアを使用して非常に簡単に実装できるという事実に関連している。フラット配列は、ディープパイプラインの CPU で非常に効率的にアクセスでき、キャッシュを介して効率的にプリフェッチされ (複雑なポインタ追跡なし)、または SIMD 命令で処理される。また、同等に効率的な汎用不変の対応物を作成することも容易ではない。純粋関数型言語の場合、可変メモリは、対数アクセス時間を持つ純粋関数型データ構造 (バランスツリーなど) で表現できるため、最悪の場合の速度低下は使用されるメモリセルの数の対数である。[ 89 ]ただし、このような速度低下は普遍的ではない。集中的な数値計算を実行するプログラムの場合、OCamlやCleanなどの関数型言語は、The Computer Language Benchmarks Gameによると、C よりわずかに遅いだけである。[ 90 ]大規模な行列や多次元データベースを扱うプログラムのために、配列関数型言語( JやKなど)は速度最適化を考慮して設計されました。
データの不変性は、多くの場合、命令型言語では安全でない仮定をコンパイラが行えるようにすることで実行効率を高め、インライン展開の機会を増やすことにつながります。[ 91 ]永続的な不変データ構造を扱う際に暗黙的に発生するコピー処理は計算コストが高いように見えるかもしれませんが、Clojureのような関数型プログラミング言語では、形式的に不変なデータ間で安全なメモリ共有を行うメカニズムを実装することでこの問題を解決しています。[ 92 ] Rust は、不変参照[ 93 ]とライフタイムと呼ばれる概念[ 94 ]を含むデータ不変性へのアプローチで際立っています。
アイデンティティと状態が分離され、共有なしスキームを持つ不変データは、並行操作は通常アトミックであるためロックの必要性を排除できるため、特定の並行ハザードのリスクを軽減または排除することで、並行プログラミングや並列プログラミングにより適している可能性があります。たとえば、クラスはこのように実装され、その一部は並行使用に適さない対応するクラスの不変バリアントです。[ 95 ]関数型プログラミング言語では、共有状態と同期の代わりにメッセージパッシングメカニズム(各アクターが状態、動作、子アクター、およびメッセージキューのコンテナであるアクターモデルなど)を活用する並行モデルがよくあります。 [ 96 ] [ 97 ]このアプローチは、 Erlang / ElixirまたはAkkaで一般的です。java.util.concurrent
遅延評価は、漸近的にプログラムの実行速度を向上させる場合もあれば、最大でも定数倍程度しか低下させない場合もあります(ただし、不適切に使用するとメモリリークを引き起こす可能性があります)。Launchbury 1993 [ 69 ]は、遅延評価によるメモリリークに関連する理論的な問題について論じており、O'Sullivan et al. 2008 [ 98 ]は、それらの分析と修正に関する実践的なアドバイスを提供しています。しかし、参照解除されたコードとデータを多用する最も一般的な遅延評価の実装は、ディープパイプラインとマルチレベルキャッシュを備えた最新のプロセッサではパフォーマンスが低下します(キャッシュミスで数百サイクルのコストがかかる場合があります)。[ 99 ]
関数型プログラミング言語の中には、「 map」や「filter 」といった高階関数などの抽象化を、基となる命令型操作ほど効率的に最適化しないものがあるかもしれません。例として、 Clojureで5が偶数かどうかをチェックする次の2つの方法を考えてみましょう。
(偶数? 5 ) ( .equals ( mod 5 2 ) 0 )Ryzen 7900X GNU/Linux PC 上でLeiningen REPL 2.11.2 を使用し、Java VMバージョン 22 および Clojure バージョン 1.11.1 で動作するCriteriumツールを用いてベンチマークを行ったところ、最初の実装は次のように実装されました。
( defn even? "nが偶数の場合はtrueを返し、nが整数でない場合は例外をスローします" { :added "1.0" :static true } [ n ] ( if ( integer? n ) ( zero? ( bit-and ( clojure.lang.RT/uncheckedLongCast n ) 1 )) ( throw ( IllegalArgumentException. ( str "引数は整数である必要があります: " n )))))の平均実行時間は 4.76 ms ですが、基となるJava.equalsメソッドを直接呼び出す2 番目の実行時間の平均は 2.8 μs で、約 1700 倍高速です。これは、 の実装に含まれる型チェックと例外処理に起因する部分があります。たとえば、Go用のlo ライブラリは、ジェネリクスを使用して関数型プログラミング言語で一般的なさまざまな高階関数を実装しています。ライブラリの作者が提供するベンチマークでは、 の呼び出しは同等のループよりも 4% 遅く、同じ割り当てプロファイルを持っています。[ 100 ]これは、のインライン化などのさまざまなコンパイラ最適化に起因する可能性があります。[ 101 ]even?mapfor
Rustの特徴の一つは、ゼロコスト抽象化です。これは、それらを使用しても実行時のオーバーヘッドが一切発生しないことを意味します。これは、コンパイラがループアンローリングを使用することで実現されます。ループの各反復処理は、命令型であろうとイテレータを使用していようと、ループ制御コードのオーバーヘッドなしに、独立したアセンブリ命令に変換されます。反復操作が配列に書き込む場合、結果として得られる配列の要素は特定の CPU レジスタに格納され、実行時に定数時間でアクセスできるようになります。[ 102 ]
従来は関数型言語とはみなされていなかった言語でも、関数型プログラミングスタイルを使用することが可能です。[ 103 ]例えば、D [ 104 ]とFortran 95 [ 62 ]はどちらも純粋関数を明示的にサポートしています。
JavaScript、Lua、[ 105 ] Python、Go [ 106 ]は、当初から第一級関数を備えていました。 [ 107 ] Python は1994 年に「 lambda」、「map」、「reduce」、「filter 」をサポートし、Python 2.2 ではクロージャもサポートしていましたが、 [ 108 ] Python 3 では「reduce」がfunctools標準ライブラリモジュールに追いやられました。[ 109 ]第一級関数は、1994 年にPerl 5.0、PHP 5.3、Visual Basic 9、C# 3.0、C++11、Kotlinなどの他の主流言語にも導入されています。[ 29 ]
Perlでは、ラムダ式、マップ式、リデュース式、フィルタ式、クロージャ式が完全にサポートされており、頻繁に使用されています。2005年に出版された書籍『Higher-Order Perl』は、関数型プログラミングにおけるPerlの活用方法を包括的に解説するために書かれました。
PHPでは、匿名クラス、クロージャ、ラムダ式が完全にサポートされています。関数型プログラミングを支援するため、不変データ構造のためのライブラリや言語拡張機能が開発されています。
Javaでは、匿名クラスを使用してクロージャをシミュレートできる場合があります。[ 110 ]ただし、匿名クラスは機能が制限されているため、クロージャの適切な代替手段とは必ずしも言えません。 [ 111 ] Java 8では、一部の匿名クラスの代替としてラムダ式がサポートされています。[ 112 ]
C#では、クロージャとラムダ式が完全にサポートされているため、匿名クラスは不要です。C#における関数型プログラミングを支援するため、不変データ構造のためのライブラリや言語拡張機能が開発されています。
オブジェクト指向設計パターンの多くは、関数型プログラミングの用語で表現できます。たとえば、ストラテジーパターンは単に高階関数の使用を指示するものであり、ビジターパターンはおおよそカタモルフィズム、つまりフォールドに対応します。
同様に、関数型プログラミングの不変データの概念は、命令型プログラミング言語にもよく取り入れられています。[ 113 ]例えば、Python のタプルは不変配列であり、JavaScript の Object.freeze() などがあります。[ 114 ]
論理プログラミングは、関数プログラミングの一般化と見なすことができ、関数は関係の特殊なケースです。[ 115 ] 例えば、関数 mother(X) = Y (すべての X にはただ 1 つの母 Y がある) は、関係 mother(X, Y) で表すことができます。関数は引数の厳密な入出力パターンを持ちますが、関係は任意の入力と出力のパターンで照会できます。次の論理プログラムを考えてみましょう。
母(チャールズ、エリザベス)。母(ハリー、ダイアナ)。このプログラムは、関数型プログラムのようにクエリを実行することで、子供から母親を生成することができます。
?-母(ハリー、X )。X =ダイアナ。? -母(チャールズ、X ) 。X =エリザベス。しかし、子要素を生成するために、逆方向にクエリを実行することもできます。
?-母( X 、エリザベス). X =チャールズ。?-母( X 、ダイアナ). X =ハリー。親関係のすべてのインスタンスを生成するためにも使用できます。
?-母親( X 、Y )。X =チャールズ、Y =エリザベス。X =ハリー、Y =ダイアナ。関係構文と比較すると、関数構文は入れ子になった関数を表すためのより簡潔な表記法です。例えば、関数構文における母方の祖母の定義は、入れ子形式で次のように記述できます。
maternal_grandmother ( X ) = mother ( mother ( X ))。関係表記における同じ定義を、ネストされていない形式で記述する必要がある。
maternal_grandmother ( X , Y ) :- mother ( X , Z ), mother ( Z , Y ).ここで、 は:-を意味し、は を意味します。 ,
しかし、2 つの表現の違いは単に構文上のものです。Ciao Prolog では、関数型プログラミングの関数のように、関係をネストすることができます。[ 116 ]
grandparent ( X ) := parent ( parent ( X )). parent ( X ) := mother ( X ). parent ( X ) := father ( X ).母(チャールズ) :=エリザベス。父(チャールズ) :=フィリップ。母(ハリー) :=ダイアナ。父(ハリー) :=チャールズ。?-祖父母( X 、Y )。X =ハリー、Y =エリザベス。X =ハリー、Y =フィリップ。Ciaoは関数のような表記を関係式に変換し、結果として得られる論理プログラムを標準的なProlog実行戦略を用いて実行する。
拡張性の高いテキストエディタファミリーであるEmacsは、プラグインを作成するために独自のLisp方言を使用しています。最も人気のあるEmacs実装であるGNU EmacsとEmacs Lispのオリジナル作者であるリチャード・ストールマンは、Lispを自身のお気に入りのプログラミング言語の1つと考えています。[ 117 ]
スプレッドシートは、純粋なゼロ次厳密評価関数型プログラミングシステムの一形態とみなすことができます。 [ 118 ]しかし、スプレッドシートは一般的に高階関数やコードの再利用性に欠け、一部の実装では再帰も欠けています。スプレッドシートプログラムには、高階関数や再利用可能な関数を可能にするための拡張機能がいくつか開発されていますが、今のところ主に学術的な性質にとどまっています。[ 119 ]
関数型プログラミングパラダイムは、その構成可能性により、マイクロサービスベースのアーキテクチャに適している可能性がある。[ 120 ]
関数型プログラミングは、プログラミング言語理論の分野において活発な研究領域です。関数型プログラミングに焦点を当てた査読付きの出版媒体は複数あり、国際関数型プログラミング会議、関数型プログラミングジャーナル、関数型プログラミング動向シンポジウムなどが挙げられます。
関数型プログラミングは、幅広い産業用途で採用されています。たとえば、1980 年代後半にスウェーデンの企業Ericssonによって開発されたErlang は、もともとは耐障害性通信システムの実装に使用されていましたが[ 11 ] 、その後Nortel、Facebook、Électricité de France、WhatsAppなどの企業でさまざまなアプリケーションを構築するために人気になりました。[ 10 ] [ 12 ] [ 121 ] [ 122 ] [ 123 ] Lispの方言であるScheme は、初期のApple Macintoshコンピュータ上のいくつかのアプリケーションの基盤として使用され[ 3 ] [ 4 ] 、トレーニングシミュレーション ソフトウェア[ 5 ]や望遠鏡制御などの問題に適用されています。[ 6 ] 1990年代半ばに導入されたOCamlは、金融分析、 [ 14 ]ドライバ検証、産業用ロボットプログラミング、組み込みソフトウェアの静的解析などの分野で商用利用されています。[ 15 ] Haskellは、当初は研究言語として意図されていましたが、[ 17 ]航空宇宙システム、ハードウェア設計、Webプログラミングなどの分野にも応用されています。[ 16 ] [ 17 ]
業界で使用されている他の関数型プログラミング言語には、Scala [ 124 ] 、F# [ 18 ] [ 19 ] 、 Wolfram Language [ 7 ] 、Lisp [ 125 ] 、 Standard ML [ 126 ] [ 127 ] 、 Clojure [ 128 ]などがあります。Scalaはデータサイエンスで広く使用されています[ 129 ]。ClojureScript [ 130 ]、Elm [ 131 ]、PureScript [ 132 ]は、本番環境で使用されている関数型フロントエンドプログラミング言語の一部です。ElixirのPhoenix フレームワークは、 Font AwesomeやAllegro (ポーランド最大の e コマース プラットフォームの 1 つ) [ 133 ]の分類広告プラットフォームAllegro Lokalnie [ 134 ]など、比較的人気のある商用プロジェクトでも使用されています。
金融業界では、リスク分析(特に大手投資銀行)において、関数型「プラットフォーム」が広く利用されてきました。リスク要因は、相互依存グラフ(カテゴリ)を形成する関数としてコード化され、市場変動の相関関係を測定します。これは、グロブナー基底最適化と同様の手法ですが、包括的資本分析レビュー(CCAR)などの規制枠組みにも適用されます。金融業界ではOCamlやCamlの派生言語が広く使われているため、これらのシステムはカテゴリカル抽象マシンと関連付けられることもあります。関数型プログラミングは、カテゴリ理論の影響を強く受けています。
多くの大学が関数型プログラミングを教えている。[ 135 ] [ 136 ] [ 137 ] [ 138 ]入門的なプログラミング概念として扱う大学もあれば[ 138 ]、命令型プログラミング手法を最初に教える大学もある。[ 137 ] [ 139 ]
コンピュータサイエンス以外では、関数型プログラミングは問題解決、代数、幾何学の概念を教えるために使用されています。[ 140 ]また、 『古典力学の構造と解釈』という本のように、古典力学を教えるためにも使用されています。
特に、Schemeは長年にわたりプログラミング教育において比較的人気のある選択肢となっている。[ 141 ] [ 142 ]
は、航空宇宙や防衛から金融、ウェブスタートアップ、ハードウェア設計会社、芝刈り機メーカーまで、商業的に幅広い用途があります。
Effective Scala.
{{cite book}}: CS1メンテナンス: 場所の発行元が見つかりません (リンク)Object.freeze() メソッドはオブジェクトをフリーズします。フリーズされたオブジェクトは変更できなくなります。オブジェクトをフリーズすると、新しいプロパティの追加、既存のプロパティの削除、既存のプロパティの列挙性、構成性、書き込み性の変更、既存のプロパティの値の変更ができなくなります。さらに、オブジェクトをフリーズすると、プロトタイプの変更もできなくなります。freeze() は渡されたオブジェクトと同じオブジェクトを返します。