ラムダを選ぶ理由

投稿には、「 なぜラムダなのかという記事の翻訳が含まれています。 」、 Scott Siteへの貢献者の1人によって準備されました。 この記事は2014年6月10日にWebサイトで公開され、ラムダライブラリと関数型プログラミング全般に注意を払う必要がある理由について説明しています。







翻訳者のメモ



この記事は2014年に作成されたため、一部の例は時代遅れであり、ライブラリの最新バージョンでは動作しませんでした。 したがって、それらはRamda@0.25.0の最新バージョンに適合しました。


ラムダを選ぶ理由



むかしむかしbuzzdecafeはRamdaを世界に紹介し、同時にコミュニティは2つのキャンプに分かれました。







最初のキャンプでは、JavaScriptの機能スタイルに慣れている人々を集めました。 彼らは、それが何のためにあるかを明確に理解していたため、新しい図書館を温かく受け入れました。







そして、2番目のキャンプでは、何の反応もしなかった人々が集まりました。







写真







関数型プログラミングに慣れていない人にとって、ラムダは無関心です。 そのコア機能のほとんどは、 UnderscoreLodashなどのライブラリですでにカバーされているためです







あなたが自分のコードを命令型またはオブジェクト指向スタイルに保ちたい人の一人であるなら、ラムダはあなたを助けません。







ただし、Ramdaは、純粋に機能的なプログラミング言語から借用したスタイルとは異なるコーディングスタイルを提供します。 ここでは、構成を使用して複雑な機能ロジックを作成するメカニズムが実装されています。 ただし、どのライブラリも機能構成を実装する機能を提供します。 しかし、他とは異なり、ここでは「簡単に実行できます」。







Ramdaの使用方法を見てみましょう。







「TODOリスト」は、Webフレームワークやライブラリなどの一般的な比較方法であるため、テストの主題として取り上げます。 まず、完了したタスクのリストを取得する必要があります。







組み込みのArray



プロトタイプメソッドを使用して、フィルタリングは次のように行われます。







 // Plain JS const incompleteTasks = tasks.filter(task => !task.complete);
      
      





Lodashでは、これは少し簡単に見えます。







 // Lo-Dash const incompleteTasks = _.filter(tasks, {complete: false});
      
      





いずれの場合でも、フィルタリングされたタスクのリストが取得されます。







Ramdaでは、フィルタリングは次のように実行できます。







 const incomplete = R.filter(R.whereEq({complete: false}));
      
      





何かが欠落していることに気づきましたか? タスクリストが不足しています。 Ramdaコードは、データではなく関数を返します。







データを取得するには、タスクのリストでこの関数を呼び出す必要があります。その後、フィルターされたリストを返します。







関数があるだけなので、任意のデータセットまたは他の関数と組み合わせることができます。 ユーザーごとにタスクのリストをグループ化するgroupByUser



関数があるとします。 その後、未完了のタスクを取得してユーザーごとにグループ化する新しい関数を簡単に作成できます。







 const activeByUser = R.compose(groupByUser, incomplete);
      
      





データバインドせずに関数を組み合わせて、新しい関数を取得することがわかりました。 上記の例を記述したい場合、次のようになります。







 const activeByUser = tasks => groupByUser(incomplete(tasks));
      
      





構成のおかげで、データバインディングなしで他の関数に基づいて新しい関数を構築できます。 したがって、合成は関数型プログラミングの重要な手法であると結論付けることができます。







先に進み、この例で他に何ができるか見てみましょう。 突然、ユーザーグループ化されたリストを日付で並べ替えることになりましたか? その後、決定は長くかかりません:







 const sortUserTasks = R.compose(R.map(R.sortBy(R.prop('dueDate'))), activeByUser);
      
      





すべてが1つの機能ですか?



compose



関数を使用すると3つ以上のパラメーターを使用できるため、上記の例を組み合わせることができることに注意してください。







 const sortUserTasks = R.compose( R.mapObj(R.sortBy(R.prop('dueDate'))), groupByUser, R.filter(R.where({complete: false}) );
      
      





ただし、 activeByUser



incomplete



中間関数があるため、これは意味がありません。 そして、他のすべてにとって、デバッグは非常に難しくなり、コードは読めなくなります。







したがって、私は他の方法を提案します。 すべてのロジックを再利用可能な小さな関数に分割します。







 const sortByDate = R.sortBy(R.prop('dueDate')); const sortUserTasks = R.compose(R.mapObj(sortByDate), activeByUser);
      
      





これで、 sortByDate



を使用して、タスクのコレクションを日付でソートできます。 実際、これはより柔軟なオプションであり、並べ替えられたdueDate



プロパティを含むオブジェクトのコレクションを並べdueDate



ます。







待って、日付を降順に並べ替える必要がある場合はどうしますか?







 const sortByDateDescend = R.compose(R.reverse, sortByDate); const sortUserTasks = R.compose(R.mapObj(sortByDateDescend), activeByUser);
      
      





日付の降順でのみソートすることがわかっている場合は、このソートを1つの定義sortByDateDescend



結合できます。 しかし、個人的には、データを昇順または降順に並べ替える場合に備えて、両方のオプションを保持することを好みます。 しかし、それはあなた次第です。







データはどこにありますか?



これまでのところデータはありませんが、ここで何が起こりますか? データなしのデータ処理、または何ですか? もう少し耐えて、すべてが明らかになります。 関数型のスタイルで記述すると、パイプラインを構成する関数のみが出力されます。 1つの関数がデータを次の関数に転送し、次にデータが次の関数に転送され、目的の結果が得られるまで続きます。







現時点では、次の機能セットがあります。







 incomplete: [Task] -> [Task] sortByDate: [Task] -> [Task] sortByDateDescend: [Task] -> [Task] activeByUser: [Task] -> {String: [Task]} sortUserTasks: {String: [Task]} -> {String: [Task]}
      
      





sortUserTasks



を実装するために、上記の関数を作成しましたが、それにもかかわらず、これらは個別に役立ちます。 前に、 groupByUser



関数があることを想像してほしいと頼みましたが、それを実装する方法を示していません。







1つの方法を次に示します。







 const groupByUser = R.groupBy(R.prop('username'));
      
      





groupBy



関数はgroupBy



reduce



使用しArray.prototype.reduce



。これはArray.prototype.reduce



に非常にていArray.prototype.reduce



。 したがって、 groupBy



関数はreduce



を使用して、 username



フィールドでリストをグループ化します。つまり、キーがusername



で、値がユーザータスクのリストであるオブジェクトを取得します。







さて、ラムダの柔軟性に感心しましたか? 注意してください、私はまだデータに言及していません。 失礼しますが、さらにこのライブラリの可能性をいくつか示します。







もう少し待って



リストから最初の5つのアイテムを取得したいと想像してください。 これは、 take



関数を使用して実行take



ます。 TODOシートから各ユーザーの最初の5つのタスクを取得するには、次のように記述すれば十分です。







 const topFiveUserTasks = R.compose(R.map(R.take(5)), sortUserTasks);
      
      





次に、余分なフィールドを削除して、返されるオブジェクトのサイズを小さくする価値がありdueDate



。たとえば、 title



dueDate



のみを残すことができます。 このデータ構造では、ユーザー情報は冗長であり、必要のないオーバーヘッドのみを作成します。







このような選択は、Ramda project



関数を使用して実装できます。これは、SQLからのselect



類似select



ています。







 const importantFields = R.project(['title', 'dueDate']); const topDataAllUsers = R.compose(R.mapObj(importantFields), topFiveUserTasks);
      
      





前に作成した機能の一部は本当に便利に思え、TODOアプリケーション内で他の目的に使用できます。 残りは単なるプレースホルダーであるため、組み合わせることができます。 すべてのコードを確認すると、次のようにリファクタリングできます。







 const incomplete = R.filter(R.where({complete: false})); const sortByDate = R.sortBy(R.prop('dueDate')); const sortByDateDescend = R.compose(R.reverse, sortByDate); const importantFields = R.project(['title', 'dueDate']); const groupByUser = R.partition(R.prop('username')); const activeByUser = R.compose(groupByUser, incomplete); const topDataAllUsers = R.compose(R.mapObj(R.compose(importantFields, R.take(5), sortByDateDescend)), activeByUser);
      
      





いいね! データを表示できますか?



はい、データ自体を表示します。







それらを関数に転送する時が来ました。 しかし、実際には、これらの関数はすべて同じデータを受け入れます。これはTODO要素の配列です。 これらの要素の構造については特に説明しませんでしたが、コードには、少なくとも次のプロパティが必要であることが示されています。









したがって、タスクのリストがある場合、それをどのように使用する必要がありますか? はい、とても簡単です:







 const results = topDataAllUsers(tasks);
      
      





それだけですか? 上記のすべての機能が機能し、必要な結果が得られますか?







怖いです。 結果はオブジェクトになります:







 { Michael: [ {dueDate: '2014-06-22', title: 'Integrate types with main code'}, {dueDate: '2014-06-15', title: 'Finish algebraic types'}, {dueDate: '2014-06-06', title: 'Types infrastucture'}, {dueDate: '2014-05-24', title: 'Separating generators'}, {dueDate: '2014-05-17', title: 'Add modulo function'} ], Richard: [ {dueDate: '2014-06-22', title: 'API documentation'}, {dueDate: '2014-06-15', title: 'Overview documentation'} ], Scott: [ {dueDate: '2014-06-22', title: 'Complete build system'}, {dueDate: '2014-06-15', title: 'Determine versioning scheme'}, {dueDate: '2014-06-09', title: 'Add `mapObj`'}, {dueDate: '2014-06-05', title: 'Fix `and`/`or`/`not`'}, {dueDate: '2014-06-01', title: 'Fold algebra branch back in'} ] }
      
      





しかし、ここに興味深い機能があります。 タスクの同じリストをincompleteTasks



関数に渡すと、リストがフィルター処理されます。







 const incompleteTasks = incomplete(tasks);
      
      





 [ { username: 'Scott', title: 'Add `mapObj`', dueDate: '2014-06-09', complete: false, effort: 'low', priority: 'medium' }, { username: 'Michael', title: 'Finish algebraic types', dueDate: '2014-06-15', complete: true, effort: 'high', priority: 'high' } /*, ... */ ]
      
      





そしてもちろん、タスクのリストをsortBydate



sortByDateDescend



importantFields



toUser



またはactiveByUser



渡すこともできます。 それらはすべて同じタイプのTODOタスクリストで動作するためです。 この方法で、簡単な組み合わせでツールの大規模なコレクションを作成できます。







新しい要件



すべてが完了した後、別の機能を実装する必要があることを学びました。 特定の1人のユーザーのみのタスクのリストをフィルターする必要があります。 これを行うには、1人のユーザーのみのタスクのサブセットを選択し、ユーザーのグループ全体で以前に使用したのと同じ並べ替えとフィルター処理を実行する必要があります。







このロジックは現在topDataAllUsers



組み込まれてtopDataAllUsers



ます...これは実際にはかなり積極的なソリューションです。 ただし、再編成は非常に簡単です。 よくあることですが、最も難しいのは良い名前を思いつくことです。 「グロス」はおそらく機能に最適な名前ではありませんが、深夜に思いつくのはそれだけです。







 const gloss = R.compose(importantFields, R.take(5), sortByDateDescend); const topData = R.compose(gloss, incomplete); const topDataAllUsers = R.compose(R.mapObj(gloss), activeByUser); const byUser = R.useWith(R.filter, [R.propEq('username'), R.identity])
      
      





これを使用する必要がある場合、次の関数を呼び出すだけで十分です。







 const results = topData(byUser('Scott', tasks));
      
      





いいけど、ただデータを取得したい



「わかりました」と言いますが、「いいかもしれませんが、今のところはデータを取得したいだけです。いつかデータを返す関数は必要ありません。この場合、Ramdaを使用できますか?」







もちろんできます。







最初の関数に戻りましょう。







 const incomplete = R.filter(R.whereEq({ complete: false }));
      
      





この関数をデータを返すものに変える方法は? 非常にシンプル:







 const incompleteTasks = R.filter(R.whereEq({ complete: false }), tasks);
      
      





同じことが残りの関数にも当てはまりますtasks



パラメーターを追加するだけで、データを取得できます。







どうした



これは、ラムダのもう1つの重要なポイントです。 すべての機能は自動的にカリー化されます。 期待されるすべての引数がそのような関数に渡されない場合、残りの引数を待つ新しい関数を返します。 incomplete



使用されるR.filter



関数は、値の配列と、それらをフィルタリングするための述語関数を取ります。 元のバージョンでは、値の配列を渡さなかったため、フィルターは単にこの配列を期待する新しい関数を返しました。 2番目のバージョンでは、予想される配列がすぐに転送され、応答を計算するために述語と一緒に使用されました。







Ramda Auto-Curryの機能は、「最初に機能、次にデータ」という原則と組み合わされています。 最後に、データ転送により、Ramdaは機能的構成のスタイルで作業するための非常にシンプルなライブラリになります。







Ramdaでのカレーの詳細については、「 カレーを好む」を参照してください。 同時に、ヒュー・ジャクソンの優れた記事を読む価値は確かにありますなぜカレーが役立つのか







このことは本当に機能しますか?



こちらが記事で説明されているコードです







このコードは、ラムダを使用する理由を明確に示しています。







Ramdaを使用する



Ramdaには非常に優れたドキュメントがあります







説明したコードは非常に適用可能であり、初めて役立つはずです。







RamdaソースコードはGitHubリポジトリから取得するか、npm経由でインストールできます。







Node.JSで使用するには、次のようにします。







 npm install ramda const R = require('ramda')
      
      





ブラウザで使用するには、次を追加します。







 <script src="path/to/yourCopyOf/ramda.js"></script>
      
      





または:







 <script src="path/to/yourCopyOf/ramda.min.js"></script>
      
      





CDNを使用することもできます。







 <script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.25.0/ramda.min.js"></script>
      
      






All Articles