覚えておいてください: 関数型プログラミングは、関数を使用したプログラミングではありません。 ! !
23.4 関数型プログラミング
23.4.1 関数型プログラミングとは何
ですか?率直に尋ねると、それが説明するのが簡単ではない概念であることがわかります。プログラミングの分野で長年の経験を持つベテランの多くは、関数型プログラミングが何を研究しているのかを明確に説明できません。関数型プログラミングは、手続き型プログラミングに慣れているプログラマにとってはまったく馴染みのない分野ですが、クロージャ、継続、カリー化という概念は、私たちにとって馴染みのないものに見えます。関数型プログラミングには手続き型プログラミングにはない美しい数学的プロトタイプがありますが、非常に神秘的であるため、博士号を取得した人だけがそれを習得できます。
ヒント: このセクションは少し難しいですが、Lisp で実行されるタスクを JavaScript を使用したくない場合、または JavaScript の難解なスキルを学びたくない場合には、JavaScript を習得するために必要なスキルではありません。関数型プログラミングについてはスキップして、次の章に進んでください。
さて、質問に戻りますが、関数型プログラミングとは何でしょうか?答えは長いです…
関数型プログラミングの第 1 法則: 関数は最初のタイプです。
この文自体はどう理解すればいいのでしょうか?本当のタイプ1とは何でしょうか?次の数学的概念を見てみましょう。
二項方程式 F(x, y) = 0、x、y は変数です。y = f(x) と書きます。x はパラメータ、y は戻り値、f は x からのものです。 to y マッピング関係は関数と呼ばれます。 G(x, y, z) = 0、または z = g(x, y) がある場合、g は x、y から z への写像関係であり、関数でもあります。 g のパラメータ x と y が前述の関係 y = f(x) を満たす場合、z = g(x, y) = g(x, f(x)) が得られます。これには 2 つの意味があります。 x) は x 上の関数であり、関数 g のパラメータです。 次に、g は f よりも高次の関数です。
このように、z = g(x, f(x)) を使用して、方程式 F(x, y) = 0 および G(x, y, z) = 0 の関連する解を表します。これは反復関数です。 。 g を別の形式で表現することもできます。z = g(x, y, f) を思い出して、関数 g を高次関数に一般化します。前のものと比較して、後者の表現の利点は、T(x,y) = 0 および G(x,y,z) = 0 の関連する解など、より一般的なモデルであることです。は同じ形式で表現できます (f=t とするだけ)。問題の解を高階関数に変換する反復をサポートするこの言語システムでは、この関数を「第 1 型」と呼びます。
JavaScript の関数は明らかに「最初のタイプ」です。典型的な例を次に示します。
Array.prototype.each = 関数(クロージャ)
{
this.length を返す ? [closure(this[0])].concat(this.slice(1).each(closure)) : [];
、
関数型スタイルの魅力を最大限に発揮する、まさに魔法のコードです。コード全体には関数とシンボルしかありません。シンプルな形でありながら、無限のパワーを持っています。
[1,2,3,4].each(function(x){return x * 2}) は [2,4,6,8] を取得しますが、[1,2,3,4].each(function(x) ){return x-1}) は [0,1,2,3] を取得します。
関数型とオブジェクト指向の本質は「道は自然に従う」ということです。オブジェクト指向が現実世界のシミュレーションであるとすれば、関数表現はある意味、オブジェクト指向よりも抽象度が高く、本質的に比較できない性質を持っています。抽象化の。
関数型プログラミングの第 2 法則: クロージャは関数型プログラミングの親友です。
前の章で説明したように、クロージャは関数型プログラミングにとって非常に重要です。最大の特徴は、変数 (シンボル) を渡さずに内部層から外部環境に直接アクセスできることです。これにより、複数のネスト下の関数型プログラムに大きな利便性がもたらされます。
(function innerFun(x))
{
戻り関数 innerFun(y)
{
x * y を返します。
}
})(2)(3);
関数型プログラミングの第 3 法則: 関数はカリー化できる。
カリー化とは何ですか? それは興味深い概念です。数学から始めましょう。3 次元空間方程式 F(x, y, z) = 0 を考えます。z = 0 に限定すると、F(x, y, 0) = 0 が得られ、F と表されます。 '(x, y)。ここで F' は明らかに新しい方程式であり、z = 0 平面上の 3 次元空間曲線 F(x, y, z) の 2 次元投影を表します。 y = f(x, z) と表し、z = 0 とすると、y = f(x, 0) が得られ、それを y = f'(x) と表します。関数 f' は f のカリー化解であると言います。 。
JavaScript カリー化の例を以下に示します。
関数 add(x, y)
{
if(x!=null && y!=null) は x + y を返します。
else if(x!=null && y==null) return function(y)
{
x + y を返します。
}
else if(x==null && y!=null) return function(x)
{
x + y を返します。
}
}
var a = add(3, 4);
var b = add(2);
var c = b(10);
上記の例では、b=add(2) は、x = 2 の場合のパラメーター y の関数である add() のカリー化関数になります。これは上記のプロパティでも使用されていることに注意してください。閉鎖の。
興味深いことに、次のように任意の関数に対してカリー化を一般化できます。
function Foo(x, y, z, w)
{
var args = 引数
if(Foo.length < args.length)
戻り関数()
{
戻る
args.callee.apply(Array.apply([], args).concat(Array.apply([], argument)));
}
それ以外
x + y – z * w を返します。
関数型プログラミングの第 4 法則: 評価と継続の遅延
。
//TODO: ここでもう一度考えてみましょう
単体テスト
の利点
厳密な関数型プログラミングのすべてのシンボルは直接の量または式の結果への参照であり、関数には副作用がありません。値がどこかで変更されることはなく、他の関数 (クラス メンバーやグローバル変数など) によって使用されるスコープ外の量を変更する関数も存在しないためです。これは、関数の評価の結果は戻り値のみであり、戻り値に影響を与えるのは関数のパラメーターだけであることを意味します。
これはユニットテスターの夢精です。テスト対象プログラムの各関数については、関数呼び出しの順序を考慮したり、外部状態を慎重に設定したりする必要がなく、そのパラメーターに注意するだけで済みます。必要なのは、エッジ ケースを表すパラメーターを渡すことだけです。プログラム内のすべての関数が単体テストに合格した場合、ソフトウェアの品質にかなりの自信が持てます。しかし、命令型プログラミングでは、関数の戻り値をチェックするだけでは十分ではなく、関数が変更した可能性のある外部状態も検証する必要があります。
デバッグ
関数型プログラムが期待どおりに動作しない場合、デバッグは簡単です。関数型プログラムのバグは、実行前に無関係なコード パスに依存しないため、発生した問題はいつでも再現できます。命令型プログラムでは、関数の機能が他の関数の副作用に依存しているため、バグが現れたり消えたりします。また、バグの発生とは関係のない方向に長時間検索しても結果が得られない場合があります。これは関数型プログラムには当てはまりません。関数の結果が間違っている場合、その前に何を実行しても、その関数は常に同じ間違った結果を返します。
問題を再現すれば、根本原因を簡単に見つけることができ、満足することさえあります。そのプログラムの実行を中断し、スタックを調べます。命令型プログラミングと同様に、スタック上の各関数呼び出しのパラメーターが表示されます。しかし、命令型プログラムでは、これらのパラメーターだけでは十分ではありません。関数は、メンバー変数、グローバル変数、クラスの状態 (これらの変数の多くに依存します) にも依存します。関数型プログラミングでは、関数はそのパラメーターにのみ依存し、その情報は目の前にあります。また、命令型プログラムでは、関数の戻り値をチェックするだけでは、その関数が正しく動作しているかどうかを確認できません。確認するには、その関数の範囲外にある数十のオブジェクトのステータスをチェックする必要があります。関数型プログラムでは、戻り値を確認するだけで済みます。
スタックに沿って関数のパラメータと戻り値を確認し、不当な結果を見つけたらすぐにその関数を入力し、バグが発生している箇所が見つかるまでこのプロセスを繰り返します。
並列関数プログラムは、変更を加えずに並列実行できます。ロックは決して使用しないため、デッドロックやクリティカル セクションについて心配する必要はありません。関数型プログラム内のデータは、2 つの異なるスレッドはもちろん、同じスレッドによって 2 回変更されることはありません。これは、並列アプリケーションを悩ませる従来の問題を引き起こすことなく、何も考えずにスレッドを簡単に追加できることを意味します。
そうだとしたら、なぜ誰もが高度な並列処理を必要とするアプリケーションで関数型プログラミングを使用しないのでしょうか?まあ、彼らはそれをやっているのです。エリクソンは Erlang と呼ばれる関数型言語を設計し、非常に高いフォールト トレランスとスケーラビリティを必要とする通信スイッチにそれを使用しました。多くの人が Erlang の利点を発見し、使い始めています。ここで話しているのは、ウォール街向けに設計された一般的なシステムよりもはるかに高い信頼性と拡張性を必要とする通信制御システムです。実際、Erlang システムは信頼性も拡張性もありませんが、JavaScript は信頼性と拡張性があります。 Erlang システムはまさに盤石です。
並列処理に関する話はこれで終わりではありません。プログラムがシングルスレッドであっても、関数型プログラム コンパイラーは複数の CPU で実行できるように最適化できます。次のコードを見てください。
String s1 = someLongOperation1();
文字列 s2 = someLongOperation2();
String s3 = concatenate(s1, s2);
関数型プログラミング言語では、コンパイラーはコードを分析して、文字列 s1 と s2 を作成する潜在的に時間のかかる関数を特定し、それらを並列実行します。これは、各関数が関数の範囲外で状態を変更する可能性があり、後続の関数がこれらの変更に依存する可能性がある命令型言語では不可能です。関数型言語では、関数を自動的に分析し、並列実行に適した候補を特定することは、関数を自動インライン展開するのと同じくらい簡単です。この意味で、関数型プログラミングは「将来性がある」ものです (業界用語を使いたくありませんが、今回は例外とします)。ハードウェア メーカーは CPU を高速に実行できなくなったため、プロセッサ コアの速度を向上させ、並列処理により 4 倍の速度向上を達成しました。もちろん、私たちが費やした余分なお金は、並行問題を解決するためのソフトウェアにのみ使用されたことも忘れていました。ごく一部の命令型ソフトウェアと 100% 機能するソフトウェアは、これらのマシン上で直接並行して実行できます。
コードのホット デプロイメント
では、Windows に更新プログラムをインストールする必要があり、新しいバージョンのメディア プレーヤーがインストールされている場合でも、コンピューターを複数回再起動することが避けられませんでした。 Windows XP ではこの状況が大幅に改善されましたが、まだ理想的とは言えません (今日職場で Windows Update を実行しましたが、マシンを再起動しない限り、煩わしいアイコンが常にトレイに表示されるようになりました)。 Unix システムは常に、更新プログラムをインストールするときに、オペレーティング システム全体ではなく、システム関連コンポーネントのみを停止する必要があります。それでも、これは大規模なサーバー アプリケーションにとってはまだ不十分です。システムの更新中に緊急ダイヤルに失敗すると人命が失われる可能性があるため、通信システムは常に 100% 稼働していなければなりません。ウォール街の企業がアップデートをインストールするために週末にサービスを停止しなければならない理由はない。
理想的な状況は、システムのコンポーネントをまったく停止せずに、関連するコードを更新することです。これは命令型の世界では不可能です。ランタイムが Java クラスをアップロードして新しい定義をオーバーライドすると、保存された状態が失われるため、このクラスのすべてのインスタンスが使用できなくなることを考慮してください。この問題を解決するために、面倒なバージョン管理コードを書き始め、次にこのクラスのすべてのインスタンスをシリアル化し、これらのインスタンスを破棄し、次にこのクラスの新しい定義を使用してこれらのインスタンスを再作成し、その後、以前にシリアル化されたデータをロードして、ロードが完了することを期待します。コードはそのデータを新しいインスタンスに適切に移植します。これに加えて、移植コードは更新ごとに手動で書き直す必要があり、オブジェクト間の相互関係が壊れないように細心の注意を払う必要があります。理論は簡単ですが、実践するのは簡単ではありません。
関数型プログラムの場合、すべての状態、つまり関数に渡されるパラメーターがスタックに保存されるため、ホット デプロイメントが簡単になります。実際、作業するコードと新しいバージョンの差分を確認し、新しいコードをデプロイするだけです。残りは言語ツールによって自動的に行われます。これが SF の物語だと思うなら、もう一度考えてください。 Erlang エンジニアは何年もの間、実行中のシステムを中断することなく更新してきました。
機械支援による推論と最適化
関数型言語の興味深い特性は、数学的に推論できることです。関数型言語は正式なシステムの単なる実装であるため、紙の上で行われるすべての操作は、この言語で書かれたプログラムに適用できます。コンパイラは数学理論を使用して、コードの一部を同等だがより効率的なコードに変換できます [7]。リレーショナル データベースでは、この種の最適化が長年にわたって行われてきました。このテクニックを通常のソフトウェアに適用できない理由はありません。
さらに、これらのテクニックを使用してプログラムの一部が正しいことを証明したり、コードを分析して単体テスト用のエッジ ケースを自動的に生成するツールを作成することもできます。この機能は堅牢なシステムにとっては何の価値もありませんが、ペースメーカーや航空交通管制システムを設計している場合には、このツールは不可欠です。あなたが作成するアプリケーションが業界の中核的なタスクではない場合、このタイプのツールは競合他社に対する切り札となることもあります。
23.4.3 関数型プログラミングの欠点
クロージャの副作用
非厳密関数型プログラミングでは、クロージャが外部環境をオーバーライドする可能性があり (前の章ですでに説明しました)、これにより副作用が発生します。また、そのような副作用が頻繁に発生する場合、およびプログラムが実行される環境が頻繁に変更されると、エラーの追跡が困難になります。
//TODO:
再帰形式
再帰は多くの場合、最も簡潔な表現形式ですが、非再帰ループほど直感的ではありません。
//TODO:
遅延値の弱点
//TODO: