JavaScriptインタビューをマスターする:関数型プログラミングとは?

ストラクチャーシンセ— Orihaus(CC BY 2.0)
「JavaScriptのインタビューをマスターする」は、中級から上級レベルのJavaScriptポジションに応募する際に遭遇する可能性のある一般的な質問の候補者を準備するための一連の投稿です。これらは、実際のインタビューでよく使用する質問です。

関数型プログラミングは、JavaScriptの世界で非常にホットなトピックになっています。ほんの数年前、関数型プログラミングとは何かを知っているJavaScriptプログラマーはほとんどいませんでしたが、過去3年で見た大規模なアプリケーションコードベースはすべて、関数型プログラミングのアイデアを多用しています。

関数型プログラミング(FPと略されることが多い)は、純粋な関数を構成し、共有状態、可変データ、および副作用を回避することにより、ソフトウェアを構築するプロセスです。関数型プログラミングは命令型ではなく宣言型であり、アプリケーションの状態は純粋な関数を流れます。通常、アプリケーションの状態はオブジェクト内のメソッドと共有され、同じ場所に配置されるオブジェクト指向プログラミングとは対照的です。

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

関数型コードは、命令型コードやオブジェクト指向型コードよりも簡潔で予測しやすく、テストしやすい傾向があります。関連する文献は、新規参入者には理解できません。

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

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

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

  • 純粋な機能
  • 機能構成
  • 共有状態を避ける
  • 状態の変化を避ける
  • 副作用を避ける

言い換えると、実際に関数型プログラミングが何を意味するのかを知りたい場合は、それらの中核概念を理解することから始めなければなりません。

純関数とは、次の関数です。

  • 同じ入力が与えられると、常に同じ出力を返します。
  • 副作用がありません

純粋な関数には、参照の透過性など、関数型プログラミングで重要な多くのプロパティがあります(プログラムの意味を変更することなく、関数呼び出しを結果の値に置き換えることができます)。詳細については、「純粋関数とは」をお読みください。

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

共有状態

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

たとえば、コンピューターゲームにはマスターゲームオブジェクトがあり、そのオブジェクトが所有するプロパティとしてキャラクターとゲームアイテムが保存されている場合があります。関数型プログラミングは共有状態を回避します。代わりに、不変のデータ構造と純粋な計算に依存して、既存のデータから新しいデータを導き出します。機能ソフトウェアがアプリケーションの状態を処理する方法の詳細については、「Reduxアーキテクチャを改善するための10のヒント」を参照してください。

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

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

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

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

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

上記の例では、Object.assign()を使用して、空のオブジェクトを最初のパラメーターとして渡し、xのプロパティをその場で変更する代わりにコピーします。この場合、Object.assign()を使用せずに、最初から新しいオブジェクトを作成するのと同等でしたが、これは、最初に説明したように、突然変異を使用する代わりに既存の状態のコピーを作成するJavaScriptの一般的なパターンです例。

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

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

関数呼び出しのタイミング依存性を削除し、潜在的なバグのクラス全体を排除します。

不変性

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

不変性は関数型プログラミングの中心的な概念です。不変性がなければ、プログラム内のデータフローは損失を伴うためです。状態の履歴は破棄され、奇妙なバグがソフトウェアに忍び寄ることがあります。不変性の重要性の詳細については、「不変性のダオ」を参照してください。

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

不変オブジェクトはまったく変更できません。オブジェクトを深く凍結することで、値を本当に不変にすることができます。 JavaScriptには、オブジェクトを1レベル深い凍結するメソッドがあります。

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

ご覧のように、フリーズしたオブジェクトのトップレベルのプリミティブプロパティは変更できませんが、オブジェクト(配列などを含む)でもプロパティを変更できます。したがって、フリーズしたオブジェクトでも、オブジェクトツリー全体とすべてのオブジェクトプロパティを凍結します。

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

構造共有を使用して、オブジェクトのすべての部分の参照メモリ位置を共有し、オブジェクトがオペレータによってコピーされた後に変更されないようにします。

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

JavaScriptには、Immutable.jsやMoriなど、試行を活用するライブラリがいくつかあります。

私は両方で実験しましたが、かなりの量の不変状態を必要とする大規模なプロジェクトでImmutable.jsを使用する傾向があります。詳細については、「Reduxアーキテクチャを改善するための10のヒント」を参照してください。

副作用

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

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

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

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

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

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

高階関数による再利用性

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

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

JavaScriptにはファーストクラスの関数があり、関数をデータとして扱うことができます。変数に割り当てたり、他の関数に渡したり、関数から返したりすることができます。

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

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

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

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

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

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

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

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

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

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

宣言的対命令的

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

命令型プログラムは、目的の結果を達成するために使用される特定のステップを記述するコード行を費やします—フロー制御:物事を行う方法。

宣言型プログラムは、フロー制御プロセスを抽象化し、代わりにデータフローを説明するコード行を使用します。方法は抽象化されます。

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

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

命令コードはステートメントを頻繁に利用します。ステートメントは、何らかのアクションを実行するコードです。一般的に使用されるステートメントの例には、for、if、switch、throwなどが含まれます…

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

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

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

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

結論

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

  • 共有状態と副作用の代わりに純粋な機能
  • 可変データに対する不変性
  • 命令型フロー制御を超える関数構成
  • コロケートされたデータのみを操作するメソッドの代わりに、高次関数を使用して多くのデータ型に作用する汎用の再利用可能なユーティリティがたくさん
  • 命令型コードではなく宣言型コード(実行方法ではなく実行方法)
  • ステートメントに対する式
  • アドホックポリモーフィズム上のコンテナと高次関数

宿題

機能配列エキストラのこのコアグループを学び、実践します。

  • 。地図()
  • 。フィルタ()
  • .reduce()

mapを使用して、次の値の配列をアイテム名の配列に変換します。

フィルターを使用して、ポイントが3以上のアイテムを選択します。

reduceを使用してポイントを合計します。

シリーズを見る

  • クロージャーとは何ですか?
  • クラス継承とプロトタイプ継承の違いは何ですか?
  • 純粋な機能とは何ですか?
  • 関数構成とは何ですか?
  • 関数型プログラミングとは何ですか?
  • 約束とは何ですか?
  • ソフトスキル
この投稿は「Composing Software」という本に含まれていました。
本を購入する|インデックス| <前へ|次へ>
EricElliottJS.comで無料レッスンを開始してください

エリック・エリオットは、「Composing Software」および「Programming JavaScript Applications」という本の著者です。 EricElliottJS.comとDevAnywhere.ioの共同設立者として、開発者に不可欠なソフトウェア開発スキルを教えています。彼は暗号化プロジェクトの開発チームを構築して助言し、Adobe Systems、Zumba Fitness、The Wall StreetJournal、ESPN、BBC、およびUsher、Frank Ocean、Metallicaなどのトップレコーディングアーティストのソフトウェアエクスペリエンスに貢献しています。

彼は世界で最も美しい女性との遠隔生活を楽しんでいます。