関数型プログラミング(FP)を使用すると、コード作成のアプローチを改善できます。 しかし、AFを習得するのは簡単ではありません。 多くの記事やマニュアルでは、Monads、Applicativeなどの詳細に注意を払っていませんが、強力なFPテクニックを日常的に使用するのに役立つ実例としての実例を挙げていません。 この省略を修正することにしました。
強調したいのは、この記事では、機能X が必要な理由ではなく、機能X が必要な理由に焦点を当てています 。
関数型プログラミング
FPは、一連の機能を単純に組み合わせたプログラム作成スタイルです。 特に、FPには、ほぼすべての関数を関数でラップすることが含まれます。 (func1.func2.func3)のような結果またはfunc1(func2(func3()))のような結果を得るには、多くの小さな再利用可能な関数を記述し、それらを次々に呼び出す必要があります。
しかし、実際にこのスタイルでプログラムを作成するには、関数が特定のルールに従っていくつかの問題を解決する必要があります。
AFの問題
一連の機能を組み合わせることですべてを実行できる場合、...
- if-else条件を処理する方法は? (ヒント:モナドのどちらか)
- null例外を処理する方法は? (ヒント:たぶんモナド)
- 関数が本当に再利用可能であり、どこでも使用できることを確認する方法は? (ヒント:純粋な機能、参照透過性)
- 私たちから関数に渡されたデータが変更されず、他の場所で使用できることを確認する方法は? (ヒント:純粋な関数、不変性)
- 関数が複数の値を取るが、チェーンに結合された場合(チェーン)、一度に1つの関数しか渡すことができない場合、この関数をチェーンの一部にするにはどうすればよいですか? (ヒント:カリー化と高次関数)
- その他<質問をここに追加>。
FPソリューション
これらのすべての問題を解決するために、Haskellのような完全に機能する言語は、モナド、ファンクターなどのさまざまなツールと数学概念を提供します。JavaScriptは、豊富なツールを提供しませんが、幸いなことに、十分なFPプロパティのセットがあります。ライブラリを作成できます。
ファンタジーランドとFPライブラリの仕様
ライブラリがファンクター、モナドなどの機能を提供する場合、提供される機能がHaskellなどの言語と同じになるように、特定の仕様を満たす関数/クラスを実装する必要があります。
顕著な例は、各JS関数/クラスの動作方法を説明するFantasy Landの仕様です。
この図は、すべての仕様とその依存関係を示しています。 仕様は基本的に法律であり、Javaインターフェースに似ています。 JSの観点からは、仕様は、仕様に従っていくつかのメソッド( map、of、chainなど)を実装するクラスまたはコンストラクター関数と見なすことができます。
例:
JSクラスは、mapメソッドを実装する場合はFunctorです。 また、このメソッドは仕様で規定されているとおりに機能する必要があります(説明は簡略化され、実際にはより多くのルールがあります)。
JSクラスは、仕様に従ってmapおよびap関数を実装する場合、Apply(Apply Functor)ファンクターです。
JSクラスは、Functor、Apply、Applicative、Chain、およびMonad自体の要件を(依存関係チェーンに従って)実装する場合、モナド(Monad Functor)です。
注:依存関係は継承のように見えますが、必須ではありません。 たとえば、モナドは、(残りに加えて)ApplicativeとChainの両方の仕様を実装します。
Fantasy Land準拠のライブラリ
FL仕様を実装するライブラリがいくつかあります。 たとえば、 monet.js 、 ほとんど機能しない 、 folktalejs 、 ramda-fantasy ( Ramdaに基づく)、 immutable-ext (ImmutableJSに基づく)、 Flutureなどです。
どのライブラリを使用する必要がありますか?
lodash-fpやramdajsのようなライブラリでは、FPのスタイルでのみ記述できます。 しかし、実際の問題を解決できるモナド、ファンクター、Foldableなどの主要な数学概念を使用するための機能は提供していません。
そのため、FL仕様を使用するライブラリのいずれかを選択することもお勧めします: monet.js 、 ほとんど機能しない 、 folktalejs 、 ramda-fantasy ( Ramdaに基づく)、 immutable-ext (ImmutableJSに基づく)、 Fltureなど
注: ramdajsとramda -fantasyを使用します。
それで、基本のアイデアを得たので、実際の例に移り、FPのさまざまな可能性とテクニックを研究しましょう。
例1. Nullチェックの使用
このセクションでは、ファンクター、モナド、多分モナド、カリーについて説明します
アプリケーション: 優先言語のユーザー設定に応じて、異なる開始ページを表示します 。 ユーザー(joeUser)の優先言語(スペイン語)のURL (indexURL)のリストから対応するURLを返すgetUrlForUserを記述する必要があります。
問題:言語はnullにできません。 また、ユーザーをnullにすることはできません(ログインしていません)。 言語がindexURLのリストにない場合があります。 したがって、多数のヌルまたは未定義の世話をする必要があります。
//TODO const getUrlForUser = (user) => { //todo } // let joeUser = { name: 'joe', email: 'joe@example.com', prefs: { languages: { primary: 'sp', secondary: 'en' } } }; // indexURL' let indexURLs = { 'en': 'http://mysite.com/en', // 'sp': 'http://mysite.com/sp', // 'jp': 'http://mysite.com/jp' // } //apply url to window.location const showIndexPage = (url) => { window.location = url };
ソリューション(必須と機能):
FPバージョンを理解するのが難しいと思われる場合でも心配しないでください。 さらにこの記事では、ステップごとに分析します。
// : // if-else null; indexURL'; «» URL' const getUrlForUser = (user) => { if (user == null) { // return indexURLs['en']; // } if (user.prefs.languages.primary && user.prefs.languages.primary != 'undefined') { if (indexURLs[user.prefs.languages.primary]) {// , indexURLs[user.prefs.languages.primary]; } else { return indexURLs['en']; } } } // showIndexPage(getUrlForUser(joeUser)); // : //( , , ) // -: , Maybe Currying const R = require('ramda'); const prop = R.prop; const path = R.path; const curry = R.curry; const Maybe = require('ramda-fantasy').Maybe; const getURLForUser = (user) => { return Maybe(user)// Maybe .map(path(['prefs', 'languages', 'primary'])) // Ramda .chain(maybeGetUrl); // maybeGetUrl URL null } const maybeGetUrl = R.curry(function(allUrls, language) {// return Maybe(allUrls[language]);// (url | null) })(indexURLs);// indexURLs function boot(user, defaultURL) { showIndexPage(getURLForUser(user).getOrElse(defaultURL)); } boot(joeUser, 'http://site.com/en'); //'http://site.com/sp'
最初に、このソリューションで使用されるFPの概念と手法を分析しましょう。
ファンクター
値を格納し、mapメソッドを実装するクラス(またはコンストラクター関数)またはデータ型は、ファンクターと呼ばれます。
たとえば、配列は値を格納でき、格納された値に(map)関数を適用できるmapメソッドがあるため、配列はファンクターです。
const add1 = (a) => a+1; let myArray = new Array(1, 2, 3, 4); // myArray.map(add1) // -> [2,3,4,5] //
独自のファンクター-MyFunctorを作成しましょう。 これは、何らかの値を格納し、mapメソッドを実装するJSクラス(コンストラクター関数)にすぎません。 このメソッドは、保存された値に関数を適用し、結果から新しいMyfunctorを作成して返します。
const add1 = (a) => a + 1; class MyFunctor { // constructor(value) { this.val = value; } map(fn) { // this.val + Myfunctor return new Myfunctor(fn(this.val)); } } //temp — , 1 let temp = new MyFunctor(1); temp.map(add1) //-> temp (map) "add1"
PS Functorは、マップに加えて他の仕様( Fantasy-land )を実装する必要がありますが、ここでは触れません。
モナド
モナドもファンクターです。つまり、それらはmapメソッドを持っています 。 しかし、彼らは彼だけではありません。 依存関係図をもう一度見ると、モナドは異なる仕様の異なる関数、たとえば、 Apply ( apメソッド)、 Applicative ( ap and ofメソッド)、 Chain ( チェーンメソッド)を実装する必要があることがわかります。
簡単な説明 。 JSでは、モナドはクラスまたはコンストラクター関数であり、一部のデータを格納し、指定に従って格納されたデータを処理するmap、ap、of、およびchainメソッドを実装します。
モナドの内部構造を理解するためのサンプル実装です。
// — class Monad { constructor(val) { this.__value = val; } static of(val) {//Monad.of , new Monad(val) return new Monad(val); }; map(f) {// , ! return Monad.of(f(this.__value)); }; join() { // return this.__value; }; chain(f) {// , (map), return this.map(f).join(); }; ap(someOtherMonad) {// return someOtherMonad.map(this.__value); } }
通常、モナドは一般的な目的には使用されませんが、より具体的で有用です。 たとえば、「Maybe」または「Ether」。
たぶんモナド
たぶんモナドはモナド仕様を実装するクラスです。 しかし、モナドの特徴は、nullまたは未定義の値を正しく処理することです。
特に、格納されたデータがnullまたは未定義の場合、map関数はこの関数をまったく実行しないため、nullおよびundefinedの問題はありません。 このモナドは、null値を処理する必要がある状況で使用されます。
以下のコードは、Maybeモナドのramda-fantasy実装を示しています。 値に応じて、2つの異なるサブクラスのいずれかのインスタンスを作成します-JustまたはNothing (値は有用またはnull /未定義のいずれかです)。
JustメソッドとNothingメソッドは同じ(map、orElseなど)ですが、Justは何かを実行し、Nothingは何も実行しません。
このコードのmapおよびorElseメソッドに特に注意してください。
// Maybe ramda-fantasy // : https://github.com/ramda/ramda-fantasy/blob/master/src/Maybe.js function Maybe(x) { //<-- , Maybe Just Nothing return x == null ? _nothing : Maybe.Just(x); } function Just(x) { this.value = x; } util.extend(Just, Maybe); Just.prototype.isJust = true; Just.prototype.isNothing = false; function Nothing() {} util.extend(Nothing, Maybe); Nothing.prototype.isNothing = true; Nothing.prototype.isJust = false; var _nothing = new Nothing(); Maybe.Nothing = function() { return _nothing; }; Maybe.Just = function(x) { return new Just(x); }; Maybe.of = Maybe.Just; Maybe.prototype.of = Maybe.Just; // Just.prototype.map = function(f) { // map, Just , Just return this.of(f(this.value)); }; Nothing.prototype.map = util.returnThis; // <-- map, Nothing Just.prototype.getOrElse = function() { return this.value; }; Nothing.prototype.getOrElse = function(a) { return a; }; module.exports = Maybe;
Maybeモナドを使用してnullチェックを処理する方法を見てみましょう。
段階的に行きましょう:
- nullまたはnullプロパティを持つことができるオブジェクトがある場合、それからモナドオブジェクトを作成します。
- Maybeを使用して、モナドの内側と外側から値にアクセスするramdajsのようなライブラリを使用してみましょう。
- 実際の値がnullであることが判明した場合は、デフォルト値を提供します(つまり、nullエラーを事前に処理します)。
// 1. ... if (user == null) { // return indexURLs['en']; // } //: Maybe(user) // Maybe({userObj}) Maybe(null). Maybe // 2. ... if (user.prefs.languages.primary && user.prefs.languages.primary != 'undefined') { if (indexURLs[user.prefs.languages.primary]) {// , return indexURLs[user.prefs.languages.primary]; //: //, Maybe, map.path Ramda: <userMaybe>.map(path(['prefs', 'languages', 'primary'])) // 3. ... return indexURLs['en']; //hardcoded default values //: // Maybe- orElse getOrElse, , <userMayBe>.getOrElse('http://site.com/en')
カリー化(グローバルデータとマルチパラメーター関数の操作に役立ちます)
このセクションでは、純関数(純関数)と合成(合成)について説明します。
関数のチェーン-func1.func2.func3または(func1(func2(func3()))を作成したい場合、それらはそれぞれ1つのパラメーターのみを取ることができます。たとえば、func2が2つのパラメーター-func2(param1、param2)を取る場合、チェーンに含めることはできません!
しかし実際には、多くの関数はいくつかのパラメーターを取ります。 では、それらをどのように組み合わせるのでしょうか? 解決策:カリー化。
カリー化とは、一度に複数のパラメーターを受け取る関数を、1つのパラメーターのみを受け取る関数に変換することです。 この関数は、すべてのパラメーターが渡されるまで実行されません。
さらに、グローバル変数にアクセスするとき、つまり「クリーンに」実行するときにカリー化を使用できます。
もう一度ソリューションを見てみましょう。
// , // indexURLs let indexURLs = { 'en': 'http://mysite.com/en', //English 'sp': 'http://mysite.com/sp', //Spanish 'jp': 'http://mysite.com/jp' //Japanese } // const getUrl = (language) => allUrls[language]; //, ( ), // // : const getUrl = (allUrls, language) => { return Maybe(allUrls[language]); } // : const getUrl = R.curry(function(allUrls, language) {//curry return Maybe(allUrls[language]); }); const maybeGetUrl = getUrl(indexURLs) // curried . // maybeGetUrl (). maybe(user).chain(maybeGetUrl).bla.bla
例2.エラーをスローする関数を操作し、エラーが発生した直後に終了する
このセクションでは、次のことについて説明します。
Nullエラーを置き換える「デフォルト」値がある場合、おそらくモナドは非常に便利です。 しかし、実際にエラーをスローする必要がある関数はどうでしょうか? また、エラーをスローする複数の関数をチェーンにまとめると、どの関数がエラーをスローしたかをどのようにして知ることができますか? つまり、高速障害(高速障害)が必要です。
たとえば、チェーンfunc1.func2.func3 ...があり、 func2がエラーをスローする場合は、 func3と次の関数をスキップし、さらに処理するためにfunc2からエラーを正しく表示する必要があります。
モナドいずれか
いずれかのモナドは、エラーが発生してすぐに終了したい場合に、エラーが発生した場所を正確に特定できるように、複数の関数を操作するのに最適です。
アプリケーション:以下の命令コードでは、 アイテムの 税金と割引を計算し、 showTotalPriceを表示します 。
価格値が数値でない場合、税関数はエラーをスローすることに注意してください。 同じ理由で、割引機能もエラーをスローします。 ただし、さらに、アイテムの価格が10未満の場合、割引はエラーをスローします。
したがって、 showTotalPriceはエラーをチェックします。
// // , const tax = (tax, price) => { if (!_.isNumber(price)) return new Error("Price must be numeric"); return price + (tax * price); }; // , const discount = (dis, price) => { if (!_.isNumber(price)) return (new Error("Price must be numeric")); if (price < 10) return new Error("discount cant be applied for items priced below 10"); return price - (price * dis); }; const isError = (e) => e && e.name == 'Error'; const getItemPrice = (item) => item.price; // . . const showTotalPrice = (item, taxPerc, disount) => { let price = getItemPrice(item); let result = tax(taxPerc, price); if (isError(result)) { return console.log('Error: ' + result.message); } result = discount(discount, result); if (isError(result)) { return console.log('Error: ' + result.message); } // console.log('Total Price: ' + result); } let tShirt = { name: 't-shirt', price: 11 }; let pant = { name: 't-shirt', price: '10 dollars' }; let chips = { name: 't-shirt', price: 5 }; //less than 10 dollars error showTotalPrice(tShirt) // : 9,075 showTotalPrice(pant) // : showTotalPrice(chips) // : 10
いずれかのモナドでshowTotalPriceを改善し、FPスタイルですべてを書き換える方法を見てみましょう。
いずれかのモナドには、Ether.LeftとBelow.Rightの2つのコンストラクターがあります。 いずれかのサブクラスと見なすことができます。 左と右はモナドです! アイデアは、エラー/例外を左に、有用な値を右に保存することです。 つまり、値に応じて、Ether.LeftまたはBelow.Rightのインスタンスを作成します。 そうすることで、これらの値にマップ、チェーンなどを適用できます。
LeftとRightの両方がマップ、チェーンなどを提供しますが、 Leftコンストラクターはエラーのみを保存し、Rightコンストラクターは実際の結果を保存するため、すべての関数を実装します。
では、命令型コードを関数型に変換する方法を見てみましょう。
手順1.戻り値を左と右にラップします。
注:「ラップ」は「何らかのクラスのインスタンスを作成する」ことを意味します。 内部のこれらの関数はnewを呼び出すため、これを行う必要はありません。
var Either = require('ramda-fantasy').Either; var Left = Either.Left; var Right = Either.Right; const tax = R.curry((tax, price) => { if (!_.isNumber(price)) return Left(new Error("Price must be numeric")); //<-- Either.Left return Right(price + (tax * price)); //<-- Either.Right }); const discount = R.curry((dis, price) => { if (!_.isNumber(price)) return Left(new Error("Price must be numeric")); //<--Wrap Error in Either.Left if (price < 10) return Left(new Error("discount cant be applied for items priced below 10")); //<-- Either.Left return Right(price - (price * dis)); //<-- Either.Right });
ステップ2.元の値をRightでラップします。これは有効であり、結合(構成)できるためです。
const getItemPrice = (item) => Right(item.price);
ステージ3。2つの関数を作成します。1つはエラーを処理し、もう1つは結果を処理します。 ( ramda -fantasy.js apiから ) Either.eitherでラップします。
Both.eitherは、結果ハンドラー、エラーハンドラー、 Etherモナドの 3つのパラメーターを取ります。 どちらかがカリー化されているため、ハンドラーをすぐに渡すことができます。
どちらかが3つすべてのパラメーターを受け取るとすぐに、どちらかが右か左かによって、結果ハンドラーまたはエラーハンドラーのいずれかにどちらかを渡します。
const displayTotal = (total) => { console.log('Total Price: ' + total) }; const logError = (error) => { console.log('Error: ' + error.message); }; const eitherLogOrShow = Either.either(logError, displayTotal);
ステップ4. chainメソッドを使用して、エラーをスローする関数を組み合わせます。 結果をBoth.either(eitherLogOrShow)に渡し、結果またはエラーハンドラーにリレーします。
const showTotalPrice = (item) => eitherLogOrShow(getItemPrice(item).chain(apply25PercDisc).chain(addCaliTax));
すべてをまとめる:
const tax = R.curry((tax, price) => { if (!_.isNumber(price)) return Left(new Error("Price must be numeric")); return Right(price + (tax * price)); }); const discount = R.curry((dis, price) => { if (!_.isNumber(price)) return Left(new Error("Price must be numeric")); if (price < 10) return Left(new Error("discount cant be applied for items priced below 10")); return Right(price - (price * dis)); }); const addCaliTax = (tax(0.1));// 10 % const apply25PercDisc = (discount(0.25));// 25 % const getItemPrice = (item) => Right(item.price); const displayTotal = (total) => { console.log('Total Price: ' + total) }; const logError = (error) => { console.log('Error: ' + error.message); }; const eitherLogOrShow = Either.either(logError, displayTotal); //api const showTotalPrice = (item) => eitherLogOrShow(getItemPrice(item).chain(apply25PercDisc).chain(addCaliTax)); let tShirt = { name: 't-shirt', price: 11 }; let pant = { name: 't-shirt', price: '10 dollars' }; //error let chips = { name: 't-shirt', price: 5 }; //less than 10 dollars error showTotalPrice(tShirt) // : 9,075 showTotalPrice(pant) // : showTotalPrice(chips) // : 10
例3.潜在的なNULLオブジェクトへの値の割り当て
***使用されるFPコンセプト:applicative(Applicative)
アプリケーション:ユーザーがログインしており、プロモーションが有効な場合(つまり、割引がある場合)、ユーザーに割引を与える必要があるとします。
applyDiscountメソッドを使用します。 ユーザー(左側)または割引(右側)がnullの場合、nullエラーをスローできます。
// user, , . //Null- , null. const applyDiscount = (user, discount) => { let userClone = clone(user);// - userClone.discount = discount.code; return userClone; }
applicativeを使用してこれをどのように解決できるかを見てみましょう。
適用性
apメソッドを持ち、Applicative仕様を実装するクラスはすべてapplicativeと呼ばれます。 このようなクラスは、方程式の左側(ユーザー)と右側(割引)の両方でNULL値を処理する関数で使用できます。
(すべてのモナドと同様に)モナドもap仕様を実装している可能性があります。 したがって、機能レベルでは、Maybeモナドを使用してnullを操作できます。 適用可能なものとして使用されるMaybeモナドでapplyDiscountを機能させる方法を見てみましょう。
ステップ1.潜在的なヌル値を多分モナドでラップします。
const maybeUser = Maybe(user); const maybeDiscount = Maybe(discount);
ステップ2.関数を書き直し、一度に1つのパラメーターを渡すようにカリー化します。
// , // var applyDiscount = curry(function(user, discount) { user.discount = discount.code; return user; });
ステップ3.最初の引数(maybeUser)をmap経由でapplyDiscountに渡します。
// map (maybeUser) applyDiscount const maybeApplyDiscountFunc = maybeUser.map(applyDiscount); // , applyDiscount map , (maybeApplyDiscountFunc) Maybe applyDiscount, maybeUser( ).
つまり、モナドにラップされた関数ができました!
ステージ4。maybeApplyDiscountFuncを使用します。
この時点で、多分ApplyDiscountFuncは次のようになります。
1)多分ラップされた関数-ユーザーが存在する場合;
2)なし(Maybeのサブクラス)-ユーザーが存在しない場合。
ユーザーが存在しない場合、Nothingが返され、その後のユーザーとのすべてのアクションは完全に無視されます。 したがって、2番目の引数を渡すと、何も起こりません。 nullエラーもありません。
ユーザーが存在する場合、関数を実行するためにmapを介してmaybeApplyDiscountFuncに2番目の引数を渡そうとすることができます。
maybeDiscount.map(maybeApplyDiscountFunc)! // !
痛い:mapは、関数(maybeApplyDiscountFunc)がMaybe内にあるときに、その実行方法を知らない!
したがって、このシナリオで動作するには別のインターフェイスが必要です。 そして、このインターフェースはapです!
ステップ5. ap関数情報を更新します。 apメソッドは別のMaybeモナドを受け取り、現在保存している関数を渡します/適用します。
class Maybe { constructor(val) { this.val = val; } ... ... //ap maybe . //this.val Nothing ( ) ap(differentMayBe) { return differentMayBe.map(this.val); } }
以下に示すように、mapを使用する代わりに、maybeApplyDiscountFuncをmaybeDiscountに適用(ap)するだけです。 そして、それはうまくいきます!
maybeApplyDiscountFunc.ap(maybeDiscount) // applyDiscount this.val maybeApplyDiscountFunc: maybeDiscount.map(applyDiscount) //, maybeDiscount , . maybeDiscount Null, .
詳細については、明らかに、ファンタジーランドの仕様が変更されています。 古いバージョンでは、次のように記述する必要がありました。Just(f).ap(Just(x))、ここでfは関数、xは値です。 新しいバージョンでは、Just(x).ap(Just(f))を記述する必要があります。 しかし、売上の大部分はまだ変わっていません。 ケイタレクサンダーに感謝します 。
まとめると。 複数のパラメーターで機能する関数があり、それぞれがヌルの場合、最初にカレーし、次にMaybe内に配置します。 また、すべてのパラメーターをMaybeに入れ、apを使用して関数を実行します。
CurryN関数
私たちはすでにカレーに精通しています。 これは関数の単純な変換であるため、一度に複数の引数をとるのではなく、一度に1つの引数を取ります。
// : const add = (a, b) =>a+b; const curriedAdd = R.curry(add); const add10 = curriedAdd(10);// . , (b) . // . add10(2) // -> 12 // add 10 2.
しかし、2つの数字を追加する代わりに、 add関数が引数として渡されたすべての数字を合計できるとしたらどうでしょうか?
const add = (...args) => R.sum(args); //
curryNを使用して引数の数を制限することにより、それをカリー化できます。
// curryN const add = (...args) => R.sum(args); // CurryN: const add = (...args) => R.sum(args); const add3Numbers = R.curryN(3, add); const add5Numbers = R.curryN(5, add); const add10Numbers = R.curryN(10, add); add3Numbers(1,2,3) // 6 add3Numbers(1) // , . add3Numbers(1, 2) // , .
curryNを使用して関数呼び出しの数を待つ
ログを3回呼び出したときにのみログに書き込む関数が必要だとします(1回と2回の呼び出しは無視されます)。 例:
// let counter = 0; const logAfter3Calls = () => { if(++counter == 3) console.log('called me 3 times'); } logAfter3Calls() // logAfter3Calls() // logAfter3Calls() // 'called me 3 times'
curryNを使用してこの動作をエミュレートできます。
// const log = () => { console.log('called me 3 times'); } const logAfter3Calls = R.curryN(3, log); // logAfter3Calls('')('')('')//'called me 3 times' //: '', CurryN .
注:この手法は、適用可能な検証に使用します。
例4.エラーの収集と表示
このセクションでは、検証(ファンクター検証、アプリカティブ検証、モナド検証)について説明します。
Validation (Validation Applicative) , ap (apply).
Either , . Either, chain, Validation ap. , chain , ap, Validation, .
, .
: , , (isUsernameValid, isPwdLengthCorrect ieEmailValid). , .
Validation. , Validation.
folktalejs data.validation, ramda-fantasy . Either : Success Failure . , Either.
1. Success Failure (. . ).
const Validation = require('data.validation') // folktalejs const Success = Validation.Success const Failure = Validation.Failure const R = require('ramda'); //: function isUsernameValid(a) { return /^(0|[1-9][0-9]*)$/.test(a) ? ["Username can't be a number"] : a } //: function isUsernameValid(a) { return /^(0|[1-9][0-9]*)$/.test(a) ? Failure(["Username can't be a number"]) : Success(a) }
, .
2. (dummy function) .
const returnSuccess = () => 'success';// success
3. curryN ap
ap : ( ), .
, ap. , monad1 . monad1.ap(monad2), . . resultingMonad , , ap monad3.
let finalResult = monad1.ap(monad2).ap(monad3) // let resultingMonad = monad1.ap(monad2) let finalResult = resultingMonad.ap(monad3) // , monad1 , monad1.ap(monad2) (resultingMonad)
, ap, , .
, ap.
, - .
Success(returnSuccess) .ap(isUsernameValid(username)) // .ap(isPwdLengthCorrect(pwd))// .ap(ieEmailValid(email))//
, Success(returnSuccess).ap(isUsernameValid(username)) . ap .
curryN, , N .
//3 — ap . let success = R.curryN(3, returnSuccess);
success .
function validateForm(username, pwd, email) { //3 — ap . let success = R.curryN(3, returnSuccess); return Success(success)// ; ap .ap(isUsernameValid(username)) .ap(isPwdLengthCorrect(pwd)) .ap(ieEmailValid(email)) }
:
const Validation = require('data.validation') // folktalejs const Success = Validation.Success const Failure = Validation.Failure const R = require('ramda'); function isUsernameValid(a) { return /^(0|[1-9][0-9]*)$/.test(a) ? Failure(["Username can't be a number"]) : Success(a) } function isPwdLengthCorrect(a) { return a.length == 10 ? Success(a) : Failure(["Password must be 10 characters"]) } function ieEmailValid(a) { var re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; return re.test(a) ? Success(a) : Failure(["Email is not valid"]) } const returnSuccess = () => 'success';// success function validateForm(username, pwd, email) { let success = R.curryN(3, returnSuccess);// 3 — ap . return Success(success) .ap(isUsernameValid(username)) .ap(isPwdLengthCorrect(pwd)) .ap(ieEmailValid(email)) } validateForm('raja', 'pwd1234567890', 'r@r.com').value; //Output: success validateForm('raja', 'pwd', 'r@r.com').value; //Output: ['Password must be 10 characters' ] validateForm('raja', 'pwd', 'notAnEmail').value; //Output: ['Password must be 10 characters', 'Email is not valid'] validateForm('123', 'pwd', 'notAnEmail').value; //['Username can\'t be a number', 'Password must be 10 characters', 'Email is not valid']