関数型プログラミングパターン:クックブック

UnsplashのJonas Jacobssonによる写真

この記事は、ramdaなどの機能ライブラリから代数データ型の使用に移行する対象者を対象としています。これらの概念は他のADTやヘルパーにも当てはまる場合がありますが、ADTとヘルパーには優れたcrocksライブラリを使用しています。多くの理論を掘り下げることなく、実用的なアプリケーションとパターンのデモンストレーションに焦点を当てます。

危険な機能を安全に実行する

サードパーティライブラリのdarkenという関数を使用したい状況があるとしましょう。 darkenは乗数と色を取り、その色のより濃い色合いを返します。

CSSのニーズには非常に便利です。しかし、関数は見た目ほど無邪気ではないことがわかりました。 darkenは、予期しない引数を受け取るとエラーをスローします!

もちろん、これはデバッグには非常に役立ちますが、色を導出できなかったからといって、アプリケーションが爆発することは望ましくありません。ここでtryCatchが役立ちます。

tryCatchは、try-catchブロック内で提供された関数を実行し、ResultというSum Typeを返します。本質的に、Sum Typeは基本的に「or」タイプです。つまり、結果は、操作が成功した場合はOk、失敗した場合はエラーになる可能性があります。合計タイプの他の例には、多分、いずれか、非同期などがあります。どちらかのポイントフリーヘルパーは、[結果]ボックスから値を取り出し、物事が南に行った場合はCSSのデフォルトの継承を返し、すべてがうまくいった場合は暗い色を返します。

たぶんヘルパーを使用して型を強制する

JavaScriptでは、特定のデータ型を期待しているために関数が爆発する場合がよくありますが、代わりに別のデータ型を受け取ります。 crocksは、Maybeタイプを使用してコードをより予測可能に実行できるsafe、safeAfter、safeLift関数を提供します。キャメルケースされた文字列をタイトルケースに変換する方法を見てみましょう。

safeAfterを使用して、一致しない場合に未定義を返すString.prototype.matchの動作を解決するヘルパー関数の一致を作成しました。 isArray述語は、一致が見つからない場合はNothingを受け取り、一致する場合はJust [String]を受け取ることを保証します。 safeAfterは、既存の機能またはサードパーティの機能を信頼できる安全な方法で実行するのに最適です。

(ヒント:safeAfterは、undefinedを返すramda関数と非常によく機能します。)

amel amel関数はsafeLift(isString)を使用して実行されます。つまり、isString述語の入力がtrueを返した場合にのみ実行されます。

これに加えて、crocksはpropおよびpropPathヘルパーも提供します。これにより、オブジェクトと配列からプロパティを選択できます。

これは特に、API応答などの制御下にない副作用のデータを処理する場合に便利です。しかし、API開発者が突然フォーマットを処理することに決めた場合はどうなりますか?

ランタイムエラー!実際には存在しないStringに対してtoFixedメソッドを呼び出そうとしました。 toFixedを呼び出す前に、bankBalanceが実際に数値であることを確認する必要があります。安全なヘルパーで解決してみましょう。

prop関数の結果をsafe(isNumber)関数にパイプします。これは、propの結果が述語を満たすかどうかに応じて、Maybeも返します。上記のパイプラインは、toFixedを含む最後のマップがbankBalanceがNumberの場合にのみ呼び出されることを保証します。

同様のケースを多数処理する場合は、このパターンをヘルパーとして抽出するのが理にかなっています。

Applicativesを使用して機能をクリーンに保つ

多くの場合、コンテナにラップされた値を持つ既存の関数を使用したい状況にあります。前のセクションの概念を使用して、数字のみを許可する安全な加算関数を設計してみましょう。これが最初の試みです。

これはまさに必要なことを行いますが、add関数は単純なa + bではなくなりました。最初に値をMaybesに持ち上げ、次に値にアクセスして値にアクセスし、結果を返す必要があります。 add関数のコア機能を保持しながら、ADTに含まれる値を操作できるようにする方法を見つける必要があります! Applicative Functorが役立つのはここです。

Applicative Functorは通常のFunctorに似ていますが、mapとともに、2つの追加メソッドも実装します。

of :: Applicative f => a-> f a

ofは完全に馬鹿なコンストラクターであり、ユーザーが指定した値をデータ型に変換します。他の言語では純粋とも呼ばれます。

そして、ここにすべてのお金があります— apメソッド:

ap ::適用f => f a〜> f(a-> b)-> f b

署名はmapと非常によく似ていますが、a-> b関数もfでラップされている点のみが異なります。これを実際に見てみましょう。

最初にカリー化されたadd関数をMaybeに持ち上げてから、Maybe aとMaybe bを適用します。これまでマップを使用してコンテナ内の値にアクセスしてきましたが、apも同じです。内部的には、safeNumber(a)にマッピングしてaにアクセスし、それを適用して追加します。これにより、部分的に適用された追加が含まれる場合があります。 safeNumber(b)で同じプロセスを繰り返してadd関数を実行し、aとbの両方が有効な場合はJust of the結果、それ以外の場合はNothingを返します。

Crocksはまた、liftA2およびliftNヘルパーを提供して、同じ概念を無意味に表現します。簡単な例を次に示します。

セクション「並列処理の表現」でこのヘルパーを広範囲に使用します。

ヒント:apはmapを使用して値にアクセスすることを確認したため、2つのリストが与えられたときにデカルト積を生成するなどのクールなことができます。

予測可能なエラー処理のための非同期の使用

crocksは非同期データ型を提供し、これにより遅延非同期計算を構築できます。詳細については、ここで広範な公式ドキュメントを参照できます。このセクションの目的は、Asyncを使用してエラー報告の品質を改善し、コードを復元する方法の例を提供することです。

多くの場合、相互に依存するAPI呼び出しを行いたい場合があります。ここで、getUserエンドポイントはGitHubからユーザーエンティティを返し、応答にはリポジトリ、スター、お気に入りなどの多くの埋め込みURLが含まれます。 Asyncを使用してこのユースケースを設計する方法を確認します。

maybeToAsync変換を使用すると、Maybeを使用することで得られるすべての安全機能を使用して、Asyncフローにそれらを取り込むことができます。非同期フローの一部として、入力およびその他のエラーにフラグを立てることができます。

モノイドの効果的な使用

ネイティブJavaScriptで文字列/配列の連結や数値の追加などの操作を実行するときは、すでにモノイドを使用しています。これは、次のメソッドを提供する単なるデータ型です。

concat ::モノイドm => m a-> m a-> m a

concatを使用すると、同じタイプの2つのモノイドを事前に指定された操作と組み合わせることができます。

空::モノイドm =>()=> m a

emptyメソッドはidentity要素を提供します。これは、同じタイプの他のMonoidと連結すると、同じ要素を返します。これが私が話していることです。

これ自体はあまり便利ではありませんが、crocksはヘルパーmconcat、mreduce、mconcatMap、mreduceMapとともにいくつかの追加のMonoidを提供します。

mconcatおよびmreduceメソッドは、Monoidおよび処理する要素のリストを受け取り、それらのすべての要素にconcatを適用します。それらの唯一の違いは、mrecatが生の値を返すのに対して、mconcatはMonoidのインスタンスを返すことです。 mconcatMapヘルパーとmreduceMapヘルパーは、concatを呼び出す前にすべての要素をマップするために使用される追加の関数を受け入れることを除いて、同じように機能します。

廃人からのモノイドの別の例、最初のモノイドを見てみましょう。連結する場合、Firstは常に最初の空でない値を返します。

Firstの力を使用して、オブジェクトで最初に使用可能なプロパティを取得しようとする関数を作成してみましょう。

きれいです!さまざまなタイプの値が提供されたときにベストエフォートフォーマッタを作成しようとする別の例を次に示します。

Pointfree方式で並列処理を表現する

単一のデータに対して複数の操作を実行し、何らかの方法で結果を結合したい場合があります。 crocksは、これを実現する2つの方法を提供します。最初のパターンは、製品タイプのペアとタプルを活用します。次のようなオブジェクトがある小さな例を見てみましょう。

{id:[11233、12351、16312]、拒否:[11233]}

このオブジェクトを受け入れ、拒否されたIDを除くIDの配列を返す関数を作成したいと思います。ネイティブJavaScriptでの最初の試みは次のようになります。

これはもちろん機能しますが、プロパティの1つが正しくないか定義されていない場合に爆発します。代わりにgetIdsにMaybeを返させてみましょう。 2つの関数を受け入れ、同じ入力で実行し、結果のペアを返すファンアウトヘルパーを使用します。

ポイントフリーアプローチを使用する主な利点の1つは、ロジックを小さな断片に分割することを奨励することです。ペアを半分にマージするために使用できる再利用可能なヘルパーの違いがあります(前述のliftA2を使用)。

2番目の方法は、収束コンビネーターを使用して同様の結果を達成することです。収束は3つの関数と1つの入力値を取ります。次に、入力を2番目と3番目の関数に適用し、両方の結果を最初の関数にパイプします。それを使用して、IDに基づいてオブジェクトの配列を正規化する関数を作成しましょう。オブジェクトを一緒に結合できるようにするAssign Monoidを使用します。

トラバースとシーケンスを使用してデータの健全性を確保する

Maybeと友人を使用して、常に期待どおりのタイプで作業できるようにする方法を見てきました。しかし、たとえば配列やリストなど、他の値を含む型を使用している場合はどうなりますか?配列に含まれるすべての文字列の合計長を取得する単純な関数を見てみましょう。

すばらしいです。配列を受け取らない場合、関数は常にNothingを返すようにしました。これで十分ですか?

あんまり。私たちの機能は、リストの内容に驚きがないことを保証するものではありません。これを解決できる方法の1つは、文字列でのみ機能するsafeLength関数を定義することです。

マッピング関数としてlengthの代わりにsafeLengthを使用すると、[Number]の代わりに[Maybe Number]を受け取り、sum関数を使用できなくなります。ここで、シーケンスが役立ちます。

シーケンスは、内側の型がApplicativeである場合、特定の効果を実行しながら内側の型を外側の型と交換するのに役立ちます。 Identityのシーケンスはかなり馬鹿げています。内部の型にマッピングされ、Identityコンテナにラップされたコンテンツを返します。 ListおよびArrayの場合、sequenceはapとconcatを使用してリストのコンテンツを結合するためにリストでreduceを使用します。リファクタリングされたtotalLength実装で実際にこれを見てみましょう。

すばらしいです!完全に防弾のtotalLengthを構築しました。 a-> m bから何かをマッピングしてからシーケンスを使用するこのパターンは非常に一般的であるため、両方の操作を一緒に実行するトラバースと呼ばれる別のヘルパーがあります。上記の例で、シーケンスの代わりにトラバースを使用する方法を見てみましょう。

そこ!まったく同じように機能します。考えてみると、シーケンス演算子は基本的にトラバースであり、マッピング関数としてのアイデンティティを持っています。

注:JavaScriptを使用して内部タイプを推測することはできないため、トラバースとシーケンスの最初の引数としてタイプコンストラクターを明示的に提供する必要があります。

シーケンスとトラバースがデータの検証にどのように役立つかを簡単に確認できます。スキーマを取得して入力オブジェクトを検証する汎用バリデーターを作成してみましょう。結果タイプを使用します。これは、エラーを収集できる左側のセミグループを受け入れます。セミグループはモノイドに似ており、concatメソッドを定義しますが、モノイドとは異なり、空のメソッドの存在を必要としません。また、変換関数maybeToResultを以下に紹介します。これは、MaybeとResultの間の相互運用に役立ちます。

makeValidator関数を反転してカレーに適したものにしたため、コンポーズチェーンは、最初に検証する必要があるスキーマを受け取ります。最初にスキーマをキーと値のペアに分割し、各プロパティの値を対応する検証関数に渡します。関数が失敗した場合、bimapを使用してエラーをマッピングし、さらに情報を追加して、シングルトン配列として返します。次に、traverseは、存在する場合はすべてのエラーを連結し、有効な場合は元のオブジェクトを返します。配列の代わりに文字列を返すこともできますが、配列の方がずっといい感じです。

この投稿に関する情報を提供してくれたIan Hofmann-Hicks、Sinisa Louc、およびDale Francisに感謝します。