はじめに
「同じようなfor文とif文を、また別の関数の中に書いている」——そんな既視感を覚えたことはないでしょうか。データを取り出して、加工して、条件で絞り込んで、また加工する。処理そのものは単純なのに、コードは毎回微妙に違う形になり、テストも書きにくく、どこかで元のオブジェクトを書き換えてしまってバグを生む。
Ramdaは、こうした「手続き的に書くほど複雑になる」問題を、関数型プログラミングのアプローチで解決するJavaScriptライブラリです。すべての関数が自動的にカリー化され、部品として組み合わせられる形で提供されているため、小さな関数をパイプでつなぐだけでデータ変換ロジックを組み立てられます。破壊的な操作は一切なく、渡したオブジェクトや配列がRamdaの関数によって書き換えられることはありません。
とはいえ、説明を読むより実際に動かした方が早いと思います。カリー化と関数合成がどう動くのか、先に挙動を見たい方はこちらからどうぞ。
Ramdaとは
Ramda(ラムダ)は「Practical functional Javascript」を掲げる、純粋関数型スタイルのユーティリティライブラリです。Lodashやunderscore.jsと同じく配列・オブジェクト操作の関数を数多く提供していますが、設計思想が大きく異なります。すべての関数がイミュータブル(不変)かつサイドエフェクトフリーで、しかも自動的にカリー化されているのが最大の特徴です。
主な特徴
- 自動カリー化 -
R.map(fn)のように一部の引数だけ渡すと、残りの引数を待つ関数が返ってきます。ループのたびに無名関数を書く必要がありません - データ最後(data-last)の設計 - 関数の最後の引数がデータになるよう統一されているため、
R.pipeやR.composeで処理をつなげやすくなっています - イミュータブル -
R.assocやR.evolveなど、更新系の関数はすべて新しいオブジェクト・配列を返し、元のデータを書き換えません - ポイントフリースタイル -
x => f(g(x))のような明示的な引数を書かず、R.pipe(g, f)のように関数だけを合成してロジックを表現できます
インストール
npmまたはyarnでインストールできます。
npm install ramda
yarn add ramda
Ramdaのサンプルを動かす
R.curryで自作関数を自動カリー化し、R.pipeで複数の関数を左から右へつなげる例です。フォームに入力した数値を、Ramdaの関数だけでフィルタ・変換・合計まで一気に処理します。カリー化されたR.filter・R.map・R.sumが、部分適用によってどう組み合わさるかに注目してください。
import * as R from 'ramda'
// カリー化: 一部の引数だけ渡すと残りを待つ関数になる
const isEven = (n) => n % 2 === 0
const double = R.multiply(2)
// pipeで「絞り込み→変換→合計」を左から右へ合成
const process = R.pipe(
R.filter(isEven), // 偶数だけ残す
R.map(double), // 2倍にする
R.sum, // 合計する
)
process([1, 2, 3, 4, 5, 6]) // => 24 (2+4+6 を2倍して合計)
実際に入力を変えて試せるサンプルが下にあります。
R.filter(isEven)だけでもR.map(double)だけでも、それ単体で「配列を受け取る関数」として動きます。processはそれらをR.pipeでつないだだけの合成関数です。入力欄の数値を書き換えると、フィルタ→変換→合計の結果がリアルタイムで変わります。奇数だけの文字列(例: 1,3,5)を入れると合計が0になる点も確認してみてください。
基本的な使い方
配列・オブジェクトに対する基本操作は、素のJavaScriptとほぼ同じ感覚で書けますが、すべてカリー化されている点が異なります。
import * as R from 'ramda'
const users = [
{ name: 'Alice', age: 28 },
{ name: 'Bob', age: 34 },
{ name: 'Carol', age: 22 },
]
// R.propで特定のプロパティだけ取り出す
R.map(R.prop('name'), users)
// => ['Alice', 'Bob', 'Carol']
// R.propsで複数プロパティ、R.sortByで並び替え
R.sortBy(R.prop('age'), users)
// => age昇順の配列
// R.whereでオブジェクトの条件フィルタ
R.filter(R.where({ age: R.gt(R.__, 25) }), users)
// => ageが25より大きい要素だけ
R.prop('name')は「オブジェクトを受け取ってnameプロパティを返す関数」そのものです。R.mapに渡すだけでコールバックとして機能するため、users.map(u => u.name)と書くよりも意図が明確になります。
実践的なユースケース
カリー化による部分適用
Ramdaの関数はすべて自動カリー化されているため、汎用的な関数から特化した関数を「その場で作る」ことができます。バリデーションや変換のルールを事前に部品化しておき、必要な箇所で組み合わせる、という使い方に向いています。
R.curryでくるんだapplyDiscountは、applyDiscount(0.1, 1200)のようにまとめて呼んでも、applyDiscount(0.1)(1200)のように分けて呼んでも同じ結果になります。tenPercentOffやhalfOffは割引率だけを先に固定した専用関数で、rateを0.3や0.7に変えると別の割引ルールを簡単に増やせます。
関数合成によるデータ変換パイプライン
配列やオブジェクトに対して「絞り込み→変換→集計」のような複数ステップの処理を行う場面では、R.pipe(左から右)やR.compose(右から左)で処理を1本のパイプラインとして表現できます。途中の関数を差し替えたり並べ替えたりするだけで、処理全体の挙動を変更できるのが利点です。
R.composeはR.pipeと処理順が逆で、リストの一番下の関数から実行されます。ここでは「完了済みを除外→キーワードで絞り込み→タイトルだけ取り出す」という3ステップを1つのpickTitles関数にまとめています。入力欄を空にすると未完了タスクがすべて表示され、「資料」と入力すると2件に絞られる様子を確認できます。
レンズ(lens)によるイミュータブルな更新
ネストしたオブジェクトの一部だけを更新したいとき、スプレッド構文を何段にも重ねるのは読みづらくなりがちです。RamdaのR.lensPathを使うと、更新したい場所を「レンズ」として定義しておき、R.setやR.overで元のオブジェクトを書き換えずに新しいオブジェクトを作れます。
R.set(nicknameLens, 値, user)を呼んでも、上の出力で「元のオブジェクト」のnicknameはtakuのまま変わりません。R.lensPathに渡す配列を['profile', 'age']に変えれば年齢を更新するレンズになりますし、R.over(nicknameLens, R.toUpper, user)のようにR.overと変換関数を組み合わせれば「大文字化して更新」といった加工も1行で書けます。
まとめ
Ramdaは、自動カリー化・データ最後の引数設計・イミュータブルな更新関数という3つの特徴によって、小さな関数を部品のように組み合わせてデータ変換ロジックを書けるライブラリです。R.pipeやR.composeで処理をつなぐスタイルに慣れると、for文やif文を都度書くよりも見通しの良いコードになり、テストもしやすくなります。
すべての関数を使いこなす必要はありません。まずはR.map・R.filter・R.pipe・R.curryあたりから普段のコードに取り入れて、慣れてきたらR.lensPathのようなレンズ操作へと広げていくのがおすすめです。