関数型プログラミングとは何ですか?

ブログ

関数型プログラミングとは何ですか?

関数型プログラミングは 宣言型 それよりも 命令 、およびアプリケーションの状態は純粋関数を介して流れます。アプリケーションの状態が通常オブジェクト内のメソッドと共有および同じ場所に配置されるオブジェクト指向プログラミングとは対照的です。

関数型プログラミングは プログラミングパラダイム 、これは、いくつかの基本的な定義原則(上記にリストされている)に基づいたソフトウェア構築についての考え方であることを意味します。プログラミングパラダイムの他の例には、オブジェクト指向プログラミングや手続き型プログラミングが含まれます。

関数型コードは、命令型コードやオブジェクト指向コードよりも簡潔で、予測可能で、テストが簡単な傾向があります。ただし、関数型コードとそれに関連する一般的なパターンに慣れていない場合は、関数型コードの方がはるかに密度が高く、関連する文献は、新参者には浸透できない可能性があります。

関数型プログラミング用語をグーグルで検索し始めると、初心者にとって非常に恐ろしい可能性のある学術用語のレンガの壁にすぐにぶつかることになります。それが学習曲線を持っていると言うことは深刻な控えめな表現です。ただし、JavaScriptでプログラミングをしばらく行っている場合は、実際のソフトウェアで関数型プログラミングの概念とユーティリティを数多く使用している可能性があります。

すべての新しい言葉があなたを怖がらせないでください。思ったよりずっと簡単です。

最も難しいのは、なじみのない語彙すべてに頭を包むことです。関数型プログラミングの意味を理解し始める前に、上記の無邪気な見た目の定義には多くのアイデアがあり、それらすべてを理解する必要があります。

  • 純粋関数
  • 関数の合成
  • 共有状態を避ける
  • 状態の変化を避ける
  • 副作用を避ける

言い換えれば、関数型プログラミングが実際に何を意味するのかを知りたい場合は、それらのコアコンセプトを理解することから始める必要があります。

純粋関数 次のような関数です。

反応ネイティブ引き出し
  • 同じ入力が与えられると、常に同じ出力を返し、
  • 副作用はありません

純粋関数には、関数型プログラミングで重要な多くのプロパティがあります。 参照透過性 (プログラムの意味を変更せずに、関数呼び出しをその結果の値に置き換えることができます)。

関数の合成 新しい関数を生成したり、何らかの計算を実行したりするために、2つ以上の関数を組み合わせるプロセスです。たとえば、構成f . g (ドットはで構成されることを意味します)はf(g(x))と同等ですJavaScriptで。関数の合成を理解することは、関数型プログラミングを使用してソフトウェアがどのように構築されるかを理解するための重要なステップです。

共有状態

共有状態 共有スコープ内に存在する、またはスコープ間で渡されるオブジェクトのプロパティとして存在する変数、オブジェクト、またはメモリ空間です。共有スコープには、グローバルスコープまたはクロージャスコープを含めることができます。多くの場合、オブジェクト指向プログラミングでは、他のオブジェクトにプロパティを追加することにより、オブジェクトがスコープ間で共有されます。

たとえば、コンピュータゲームには、マスターゲームオブジェクトがあり、そのオブジェクトが所有するプロパティとしてキャラクターとゲームアイテムが保存されている場合があります。関数型プログラミングは、共有状態を回避します。代わりに、不変のデータ構造と純粋な計算に依存して、既存のデータから新しいデータを導き出します。

共有状態の問題は、関数の効果を理解するために、関数が使用または影響するすべての共有変数の履歴全体を知る必要があることです。

保存が必要なユーザーオブジェクトがあるとします。あなたのsaveUser()関数はサーバー上のAPIにリクエストを送信します。その間、ユーザーはプロフィール写真をupdateAvatar()で変更しますそして別のsaveUser()をトリガーしますリクエスト。保存時に、サーバーは、サーバー上で、または他のAPI呼び出しに応答して発生する変更と同期するために、メモリ内にあるものをすべて置き換える必要がある正規のユーザーオブジェクトを送り返します。

残念ながら、2番目の応答は最初の応答の前に受信されるため、最初の(現在は古くなっている)応答が返されると、新しいプロファイル写真がメモリ内で消去され、古いプロファイル写真に置き換えられます。これは競合状態の例です—共有状態に関連する非常に一般的なバグです。

共有状態に関連するもう1つの一般的な問題は、共有状態に作用する関数はタイミングに依存するため、関数が呼び出される順序を変更すると、一連の障害が発生する可能性があることです。

// With shared state, the order in which function calls are made // changes the result of the function calls. const x = { val: 2 }; const x1 = () => x.val += 1; const x2 = () => x.val *= 2; x1(); x2(); console.log(x.val); // 6 // This example is exactly equivalent to the above, except... const y = { val: 2 }; const y1 = () => y.val += 1; const y2 = () => y.val *= 2; // ...the order of the function calls is reversed... y2(); y1(); // ... which changes the resulting value: console.log(y.val); // 5

タイミング依存の例

共有状態を回避する場合、関数呼び出しのタイミングと順序は、関数を呼び出した結果を変更しません。純粋関数では、同じ入力が与えられると、常に同じ出力が得られます。これにより、関数呼び出しは他の関数呼び出しから完全に独立し、変更とリファクタリングを大幅に簡素化できます。 1つの関数を変更したり、関数呼び出しのタイミングが波及したり、プログラムの他の部分を壊したりすることはありません。

const x = { val: 2 }; const x1 = x => Object.assign({}, x, { val: x.val + 1}); const x2 = x => Object.assign({}, x, { val: x.val * 2}); console.log(x1(x2(x)).val); // 5 const y = { val: 2 }; // Since there are no dependencies on outside variables, // we don't need different functions to operate on different // variables. // this space intentionally left blank // Because the functions don't mutate, you can call these // functions as many times as you want, in any order, // without changing the result of other function calls. x2(y); x1(y); console.log(x1(x2(y)).val); // 5

上記の例では、Object.assign()を使用しますxのプロパティをコピーする最初のパラメータとして空のオブジェクトを渡します所定の位置に変更する代わりに。この場合、Object.assign()を使用せずに、新しいオブジェクトを最初から作成するのと同じですが、これは、最初に示したミューテーションを使用する代わりに、既存の状態のコピーを作成するJavaScriptの一般的なパターンです。例。

console.log()をよく見るとこの例のステートメントでは、すでに述べた関数の合成に気付くはずです。以前のことを思い出してください。関数の合成は次のようになります:f(g(x))。この場合、f()を置き換えますおよびg() x1()でおよびx2()構成の場合:x1 . x2

もちろん、コンポジションの順番を変えると出力も変わります。操作の順序は依然として重要です。 f(g(x))は常にg(f(x))と等しいとは限りませんが、もはや重要ではないのは、関数の外部の変数に何が起こるかです。これは大きな問題です。不純な関数では、関数が使用または影響するすべての変数の履歴全体を知らない限り、関数が何をするのかを完全に理解することは不可能です。

関数呼び出しのタイミング依存関係を削除すると、潜在的なバグのクラス全体が削除されます。

不変性

NS 不変 オブジェクトは、作成後に変更できないオブジェクトです。逆に、 可変 オブジェクトは、作成後に変更できる任意のオブジェクトです。

不変性は関数型プログラミングの中心的な概念です。不変性がないと、プログラムのデータフローが失われるためです。状態履歴は破棄され、奇妙なバグがソフトウェアに忍び寄る可能性があります。

JavaScriptでは、constを不変性と混同しないことが重要です。 const作成後に再割り当てできない変数名バインディングを作成します。 const不変のオブジェクトを作成しません。バインディングが参照するオブジェクトを変更することはできませんが、オブジェクトのプロパティを変更することはできます。つまり、バインディングはconstで作成されます。不変ではなく、可変です。

不変オブジェクトはまったく変更できません。オブジェクトをディープフリーズすることで、値を真に不変にすることができます。 JavaScriptには、オブジェクトを1レベル深くフリーズするメソッドがあります。

const a = Object.freeze({ foo: 'Hello', bar: 'world', baz: '!' }); a.foo = 'Goodbye'; // Error: Cannot assign to read only property 'foo' of object Object

しかし、凍結されたオブジェクトは表面的に不変です。たとえば、次のオブジェクトは変更可能です。

const a = Object.freeze({ foo: { greeting: 'Hello' }, bar: 'world', baz: '!' }); a.foo.greeting = 'Goodbye'; console.log(`${ a.foo.greeting }, ${ a.bar }${a.baz}`);

ご覧のとおり、フリーズされたオブジェクトの最上位のプリミティブプロパティは変更できませんが、オブジェクトでもあるプロパティ(配列などを含む)は変更できます。したがって、フリーズされたオブジェクトでさえ、歩かない限り不変ではありません。オブジェクトツリー全体をフリーズし、すべてのオブジェクトプロパティをフリーズします。

多くの関数型プログラミング言語には、と呼ばれる特別な不変のデータ構造があります。 トライデータ構造 (ツリーと発音)効果的に凍結されます。つまり、オブジェクト階層内のプロパティのレベルに関係なく、プロパティを変更することはできません。

使用してみます 構造共有 オブジェクトがオペレータによってコピーされた後に変更されないオブジェクトのすべての部分の参照メモリ位置を共有します。これにより、メモリの使用量が少なくなり、特定の種類の操作のパフォーマンスが大幅に向上します。

たとえば、オブジェクトツリーのルートでID比較を使用して比較できます。 IDが同じである場合は、ツリー全体を歩いて違いを確認する必要はありません。

JavaScriptには、試行を利用するライブラリがいくつかあります。 Immutable.js取った

私は両方を試しましたが、大量の不変状態を必要とする大規模なプロジェクトでImmutable.jsを使用する傾向があります。

副作用

副作用は、戻り値以外に、呼び出された関数の外部で観察可能なアプリケーションの状態変化です。副作用は次のとおりです。

  • 外部変数またはオブジェクトプロパティ(グローバル変数、親関数スコープチェーン内の変数など)の変更
  • コンソールへのロギング
  • 画面への書き込み
  • ファイルへの書き込み
  • ネットワークへの書き込み
  • 外部プロセスのトリガー
  • 副作用のある他の関数の呼び出し

関数型プログラミングでは副作用がほとんど回避されるため、プログラムの効果がはるかに理解しやすくなり、テストもはるかに簡単になります。

Haskellやその他の関数型言語は、以下を使用して純粋関数から副作用を分離してカプセル化することがよくあります。 モナド 。モナドのトピックは本を書くのに十分深いので、後で使用するために保存しておきます。

今知っておく必要があるのは、副作用のアクションをソフトウェアの他の部分から分離する必要があるということです。副作用を他のプログラムロジックから分離しておくと、ソフトウェアの拡張、リファクタリング、デバッグ、テスト、および保守がはるかに簡単になります。

これが、ほとんどのフロントエンドフレームワークが、ユーザーが状態とコンポーネントのレンダリングを別々の疎結合モジュールで管理することを推奨している理由です。

高階関数による再利用性

関数型プログラミングは、データを処理するために関数型ユーティリティの共通セットを再利用する傾向があります。オブジェクト指向プログラミングは、オブジェクト内のメソッドとデータを同じ場所に配置する傾向があります。これらの同じ場所に配置されたメソッドは、操作するように設計されたタイプのデータのみを操作でき、多くの場合、その特定のオブジェクトインスタンスに含まれるデータのみを操作できます。

関数型プログラミングでは、あらゆるタイプのデータが公正なゲームです。同じmap()ユーティリティは、指定されたデータ型を適切に処理する引数として関数を受け取るため、オブジェクト、文字列、数値、またはその他のデータ型にマップできます。 FPは、 高階関数

JavaScriptには ファーストクラス関数 、これにより、関数をデータとして扱うことができます—変数への割り当て、他の関数への受け渡し、関数からの返送など…

高階関数 関数を引数として取るか、関数を返すか、またはその両方を行う関数です。高階関数は、次の目的でよく使用されます。

  • コールバック関数、promise、モナドなどを使用して、アクション、エフェクト、または非同期フロー制御を抽象化または分離します…
  • さまざまなデータ型に対応できるユーティリティを作成する
  • 関数を引数に部分的に適用するか、再利用または関数合成の目的でカリー化された関数を作成します
  • 関数のリストを取得し、それらの入力関数の構成を返します

コンテナ、ファンクタ、リスト、およびストリーム

ファンクターは、マッピングできるものです。つまり、内部の値に関数を適用するために使用できるインターフェイスを備えたコンテナです。ファンクターという言葉を目にしたときは、マッピング可能だと考える必要があります。

以前、同じmap()であることを学びましたユーティリティは、さまざまなデータ型に対応できます。これは、マッピング操作を解除してファンクターAPIで動作するようにすることで実現します。 map()で使用される重要なフロー制御操作そのインターフェースを利用してください。 Array.prototype.map()の場合、コンテナーは配列ですが、マッピングAPIを提供する限り、他のデータ構造もファンクターにすることができます。

どのようにArray.prototype.map()を見てみましょうマッピングユーティリティからデータ型を抽象化して、map()を作成できます。任意のデータ型で使用できます。簡単なdouble()を作成します渡された値に2を掛けるだけのマッピング。

const double = n => n * 2; const doubleMap = numbers => numbers.map(double); console.log(doubleMap([2, 3, 4])); // [ 4, 6, 8 ]

ゲーム内のターゲットを操作して、獲得するポイント数を2倍にしたい場合はどうなりますか? double()に微妙な変更を加えるだけです。 map()に渡す関数で、すべてが引き続き機能します。

const double = n => n.points * 2; const doubleMap = numbers => numbers.map(double); console.log(doubleMap([ { name: 'ball', points: 2 }, { name: 'coin', points: 3 }, { name: 'candy', points: 4} ])); // [ 4, 6, 8 ]

関数型プログラミングでは、汎用ユーティリティ関数を使用して任意の数の異なるデータ型を操作するために、関数や高階関数などの抽象化を使用するという概念が重要です。同様の概念が あらゆる種類の異なる方法

時間の経過とともに表現されるリストはストリームです。

今のところ理解する必要があるのは、配列とファンクターだけがこのコンテナーの概念とコンテナー内の値を適用する方法ではないということです。たとえば、配列は単なるリストです。時間の経過とともに表現されるリストはストリームです。したがって、同じ種類のユーティリティを適用して、着信イベントのストリームを処理できます。これは、FPを使用して実際のソフトウェアの構築を開始するときによく見られます。

宣言型と命令型

関数型プログラミングは宣言型パラダイムです。つまり、プログラムロジックは、フロー制御を明示的に記述せずに表現されます。

命令 プログラムは、目的の結果を達成するために使用される特定の手順を説明するコード行を費やします。 フロー制御:どのように 物事をするために。

宣言型 プログラムはフロー制御プロセスを抽象化し、代わりに データフロー:何 やること。 NS どうやって 抽象化されます。

たとえば、これ 命令 マッピングは数値の配列を受け取り、各数値に2を掛けた新しい配列を返します。

const doubleMap = numbers => { const doubled = []; for (let i = 0; i

命令型データマッピング

この 宣言型 マッピングは同じことを行いますが、関数Array.prototype.map()を使用してフロー制御を抽象化します。ユーティリティ。データの流れをより明確に表現できます。

const doubleMap = numbers => numbers.map(n => n * 2); console.log(doubleMap([2, 3, 4])); // [4, 6, 8]

命令 コードは頻繁にステートメントを利用します。 NS 声明 は、何らかのアクションを実行するコードの一部です。一般的に使用されるステートメントの例には、forifswitchthrowなどがあります。

宣言型 コードは式に依存しています。 NS 表現 ある値に評価されるコードの一部です。式は通常、結果の値を生成するために評価される関数呼び出し、値、および演算子の組み合わせです。

これらはすべて式の例です。

2 * 2 doubleMap([2, 3, 4]) Math.max(4, 3, 2)

通常、コードでは、式が識別子に割り当てられているか、関数から返されるか、関数に渡されます。割り当て、返送、または渡される前に、式が最初に評価され、結果の値が使用されます。

結論

関数型プログラミングの利点:

  • 共有状態と副作用の代わりに純粋関数
  • 可変データに対する不変性
  • 命令型フロー制御に対する関数構成
  • 同じ場所に配置されたデータのみを操作するメソッドではなく、高階関数を使用して多くのデータ型を操作する、多くの汎用的で再利用可能なユーティリティ
  • 命令型コードではなく宣言型コード(方法ではなく、何をすべきか)
  • ステートメントに対する表現
  • アドホック多相性に対するコンテナと高階関数

#function#javascript#web-development