関数型プログラミングは、依存性注入パターンの実行可能な代替手段ですか?


21

私は最近、C#での関数型プログラミングというタイトルの本を読んでいます。関数型プログラミングの不変でステートレスな性質は、依存性注入パターンと同様の結果を達成し、特にユニットテストに関してはより良いアプローチである可能性があります。

両方のアプローチの経験がある人なら誰でも、主な質問に答えるために自分の考えや経験を共有できれば感謝しています。


10
これは私にはあまり意味がなく、不変性は依存関係を削除しません。
Telastyn

依存関係を削除しないことに同意します。おそらく間違っているのは私の理解ですが、元のオブジェクトを変更できない場合は、それを使用するすべての関数に渡す(注入する)必要があるため、その推論をしました。
マットカシャット


5
また、OOプログラマーを愛情のこもった関数型プログラミングにTす方法もあります。これは、OOとFPの両方の観点からのDIの詳細な分析です。
ロバートハーベイ

1
この質問、リンク先の記事、および受け入れられた回答も役立つ場合があります。stackoverflow.com/ questions / 11276319 / 怖いMonadの単語は無視してください。Runarが彼の答えで指摘しているように、この場合、それは複雑な概念ではありません(単なる関数)。
-itsbruce

回答:


27

依存関係管理は、次の2つの理由でOOPの大きな問題です。

  • データとコードの密結合。
  • 副作用のユビキタス使用。

ほとんどのオブジェクト指向プログラマーは、データとコードの密結合は完全に有益であると考えていますが、それにはコストがかかります。レイヤーを通るデータの流れを管理することは、どのようなパラダイムでもプログラミングの避けられない部分です。データとコードを結合すると、特定のポイントで関数を使用する場合に、そのオブジェクトをそのポイントに到達させる方法を見つける必要があるという追加の問題が追加されます。

副作用を使用すると、同様の問題が生じます。一部の機能に副作用を使用しているが、その実装を交換できるようにしたい場合は、その依存関係を注入する以外に選択肢はほとんどありません。

例として、電子メールアドレスのWebページをスクレイピングしてから電子メールで送信するスパマープログラムを考えます。DIの考え方をお持ちの場合、現在、インターフェイスの背後にカプセル化するサービスと、どのサービスがどこに注入されるかを考えています。このデザインは読者の演習として残しておきます。FPの考え方をお持ちの場合、現時点では、次のような機能の最下層の入力と出力を考えています。

  • Webページのアドレスを入力し、そのページのテキストを出力します。
  • ページのテキストを入力し、そのページからリンクのリストを出力します。
  • ページのテキストを入力し、そのページのメールアドレスのリストを出力します。
  • メールアドレスのリストを入力し、重複が削除されたメールアドレスのリストを出力します。
  • メールアドレスを入力し、そのアドレスのスパムメールを出力します。
  • スパムメールを入力し、SMTPコマンドを出力してそのメールを送信します。

入力と出力の観点から考えると、関数の依存関係はなく、データの依存関係のみがあります。それがユニットテストをとても簡単にするものです。次のレイヤーは、ある関数の出力が次の関数の入力に供給されるように調整し、必要に応じてさまざまな実装を簡単に交換できます。

非常に現実的な意味で、関数型プログラミングは自然に、常に関数の依存関係を反転するように促します。そのため、通常、事後に特別な対策を講じる必要はありません。その場合、高階関数、クロージャ、部分適用などのツールを使用すると、少ない定型文で簡単に実行できます。

問題があるのは依存関係そのものではないことに注意してください。間違った方向指しているのは依存関係です次の層には次のような機能があります。

processText = spamToSMTP . emailAddressToSpam . removeEmailDups . textToEmailAddresses

このレイヤーがこのようにハードコードされた依存関係を持つことは完全に大丈夫です、なぜならその唯一の目的は下位レイヤーの機能を一緒に接着することだからです。実装の交換は、異なる構成を作成するのと同じくらい簡単です。

processTextFancy = spamToSMTP . emailAddressToFancySpam . removeEmailDups . textToEmailAddresses

この簡単な再構成は、副作用がないために可能になります。下位層の機能は互いに完全に独立しています。次の層でprocessTextは、ユーザー設定に基づいて実際に使用されるものを選択できます。

actuallyUsedProcessText = if (config == "Fancy") then processTextFancy else processText

繰り返しますが、すべての依存関係が一方向を指しているため、問題ではありません。すべての依存関係を同じようにするために、いくつかの依存関係を逆にする必要はありません。純粋な関数がすでにそれを強制しているからです。

config一番上でチェックするのではなく、最下層にパススルーすることで、これをさらに結合させることができます。FPはこれを行うことを妨げるものではありませんが、試してみるとはるかに迷惑になる傾向があります。


3
「副作用を使用すると同様の問題が発生します。一部の機能に副作用を使用し、その実装を交換したい場合は、その依存関係を注入する以外に選択肢はほとんどありません。」副作用はこれとは関係ないと思います。Haskellの実装を交換したい場合、依存性注入を行う必要があります。型クラスをデシュガーすると、すべての関数の最初の引数としてインターフェイスを渡します。
ドーバル

2
問題の核心は、ほとんどすべての言語が他のコードモジュールへの参照をハードコードすることを余儀なくするため、実装をスワップする唯一の方法は、どこでも動的ディスパッチを使用することであり、実行時に依存関係を解決しなければならないということです。モジュールシステムを使用すると、型チェック時に依存関係グラフを表現できます。
ドーバル

@Doval-興味深く、示唆に富むコメントをありがとう。私はあなたを誤解したかもしれませんが、DIスタイルよりも機能的なスタイルのプログラミングを使用すると(従来のC#の意味で)、ランタイムに関連する可能性のあるデバッグのフラストレーションを避けることができるというあなたのコメントから推測するのは正しいですか依存関係の解決?
マットカシャット

@MatthewPatrickCashattスタイルやパラダイムの問題ではなく、言語の機能の問題です。言語が第一級のものとしてモジュールをサポートしていない場合、依存関係を静的に表現する方法がないため、スワップ実装のために何らかの形式の動的ディスパッチと依存関係注入を行う必要があります。少し違って言えば、C#プログラムが文字列を使用する場合、依存関係はハードコードされていSystem.Stringます。モジュールシステムではSystem.String、文字列実装の選択がハードコーディングされないように変数で置き換えることができますが、コンパイル時に解決されます。
ドーバル

8

関数型プログラミングは依存性注入パターンの実行可能な代替手段ですか?

これは奇妙な質問です。関数型プログラミングのアプローチは、主に依存性注入の正接です。

確かに、不変の状態があると、副作用があるか、クラス状態を関数間の暗黙のコントラクトとして使用することで、「チート」しないようにプッシュできます。これにより、データの受け渡しがより明確になります。これは、依存性注入の最も基本的な形式だと思います。また、関数を渡すという関数型プログラミングの概念により、それがはるかに簡単になります。

ただし、依存関係は削除されません。状態が変更可能だったときに必要だったすべてのデータ/操作が操作に必要です。そして、それらの依存関係を何らかの形でそこに取得する必要があります。したがって、関数型プログラミングのアプローチ DIに取って代わるとは言いませんので、代替手段はありません。

どちらかといえば、プログラマーがめったに考えないよりも、オブジェクト指向コードが暗黙的な依存関係を作成する方法を示しています。


会話に貢献してくれてありがとう、テラスティン。あなたが指摘したように、私の質問はあまりうまく構築されていません(私の言葉)が、ここでのフィードバックのおかげで、これについて私の脳に火がついていることを少しよく理解し始めています:私たちはすべて同意します(私は思う)単体テストは、DIがなければ悪夢のようです。残念ながら、特にIoCコンテナでのDIの使用は、実行時に依存関係を解決するという事実のおかげで、デバッグの悪夢の新しい形式を作成できます。DIと同様に、FPはユニットテストを簡単にしますが、実行時の依存関係の問題はありません。
マットカシャット

(上記から続く)。。とにかくこれは私の現在の理解です。マークがない場合はお知らせください。私はここの巨人の中でただの人間だと認めても構わない!
マットカシャット

@MatthewPatrickCashatt-DIは、実行時の依存関係の問題を必ずしも意味するわけではありませんが、これは恐ろしいことです。
Telastyn

7

あなたの質問への簡単な答えは、いいえです。

しかし、他の人が主張しているように、この質問は2つのやや無関係な概念と結婚しています。

これを段階的にやってみましょう。

DIは非機能スタイルになります

関数プログラミングの中核には純粋な関数があります。これは、入力を出力にマップする関数であるため、特定の入力に対して常に同じ出力を取得します。

DI は、通常、出力が注入によって異なるため、ユニットがもはや純粋ではないことを意味します。たとえば、次の関数では:

const bookSeats = ( seatCount, getBookedSeatCount ) => { ... }

getBookedSeatCount(関数)は、同じ入力に対して異なる結果をもたらす可能性があります。これbookSeatsも不純になります。

これには例外があります-異なるアルゴリズムを使用していても、同じ入出力マッピングを実装する2つのソートアルゴリズムのいずれかを挿入できます。しかし、これらは例外です。

システムを純粋にすることはできません

システムは純粋ではありえないという事実は、関数型プログラミングソースで主張されているため、同様に無視されます。

システムには次のような明らかな例がある副作用がなければなりません。

  • UI
  • データベース
  • API(クライアントサーバーアーキテクチャ)

したがって、システムの一部には副作用が含まれる必要があり、その一部には命令型スタイルまたはオブジェクト指向スタイルも含まれる場合があります。

シェルコアのパラダイム

Gary Bernhardtの境界に関する素晴らしい講演から用語を借用すると、優れたシステム(またはモジュール)アーキテクチャには次の2つの層が含まれます。

  • コア
    • 純粋な機能
    • 分岐
    • 依存関係なし
  • シェル
    • 不純(副作用)
    • 分岐なし
    • 依存関係
    • 必須であり、オブジェクト指向スタイルなどが含まれる場合があります。

重要なポイントは、システムを純粋な部分(コア)と不純な部分(シェル)に「分割」することです。

少し欠陥のあるソリューション(および結論)を提供していますが、このMark Seemannの記事はまったく同じ概念を提案しています。Haskellの実装は、FPを使用してすべて実行できることを示しているため、特に洞察力に富んでいます。

DIおよびFP

アプリケーションの大部分が純粋であっても、DIを使用することは完全に合理的です。重要なのは、DIを不純なシェル内に閉じ込めることです。

例としてAPIスタブがあります-本番環境では実際のAPIが必要ですが、テストではスタブを使用します。ここでは、シェルコアモデルに準拠することが非常に役立ちます。

結論

したがって、FPとDIは正確な選択肢ではありません。システムに両方がある可能性が高いため、FPとDIがそれぞれ存在するシステムの純粋な部分と不純な部分を確実に分離することをお勧めします。


シェルコアパラダイムを参照する場合、シェルで分岐をどのように実現しないのでしょうか。アプリケーションは、値に基づいて不純なことを行う必要がある多くの例を考えることができます。この非分岐規則は、Javaなどの言語に適用できますか?
ジャラハリ

@jrahhali詳細については、ゲーリー・ベルンハルトの講演をご覧ください(回答にリンクされています)。
イザキ

別のrelaventシーマンシリーズblog.ploeh.dk/2017/01/27/...
JK。

1

OOPの観点からは、関数は単一メソッドのインターフェイスと見なすことができます。

インターフェイスは関数よりも強力なコントラクトです。

機能的アプローチを使用しており、多くのDIを実行している場合、OOPアプローチを使用する場合と比較して、各依存関係に対してより多くの候補が得られます。

void DoStuff(Func<DateTime> getDateTime) {}; //Anything that satisfies the signature can be injected.

void DoStuff(IDateTimeProvider dateTimeProvider) {}; //Only types implementing the interface can be injected.

3
インターフェイスを実装するために任意のクラスをラップできるため、「より強力なコントラクト」はそれほど強力ではありません。さらに重要なことは、各関数に異なる型を与えることにより、関数の合成を行うことができなくなります。
ドーバル

関数型プログラミングは「高階関数を使用したプログラミング」を意味するものではなく、はるかに広い概念を指します。高階関数は、パラダイムで役立つ1つの手法にすぎません。
ジミー・ホッファ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.