T-CREATOR

Lodash vs Ramda vs Rambda:パイプライン記法・カリー化・DX を徹底比較

Lodash vs Ramda vs Rambda:パイプライン記法・カリー化・DX を徹底比較

JavaScript のユーティリティライブラリは、配列操作やオブジェクト変換など、日常的な処理を効率化する強力なツールです。中でも Lodash、Ramda、Rambda の 3 つは広く使われていますが、それぞれの設計思想や開発体験(DX)には大きな違いがあります。

本記事では、パイプライン記法・カリー化・開発者体験という 3 つの観点から、これらのライブラリを徹底比較します。関数型プログラミングに興味がある方や、プロジェクトに最適なライブラリを選びたい方に、実践的な判断材料をお届けいたします。

背景

JavaScript ユーティリティライブラリの役割

JavaScript には配列や文字列を操作するネイティブメソッドが備わっていますが、複雑な処理や繰り返し使われるパターンには、専用のユーティリティライブラリが便利です。

以下の図は、3 つのライブラリの開発時期と設計思想の違いを示しています。

mermaidflowchart TD
  era1["2012年:Lodash 登場"] --> concept1["命令的スタイル<br/>パフォーマンス重視"]
  era2["2013年:Ramda 登場"] --> concept2["関数型スタイル<br/>カリー化・不変性"]
  era3["2019年:Rambda 登場"] --> concept3["Ramda 互換<br/>軽量・高速化"]

  concept1 --> use1["チェーン記法<br/>幅広いユーティリティ"]
  concept2 --> use2["パイプライン記法<br/>ポイントフリースタイル"]
  concept3 --> use3["Ramda のサブセット<br/>バンドルサイズ削減"]

各ライブラリの基本特性

Lodash は 2012 年にリリースされ、Underscore.js の後継として広く普及しました。豊富な機能と高いパフォーマンスが特徴で、命令的スタイルの処理に向いています。

一方 Ramda は 2013 年に登場し、関数型プログラミングを第一に設計されました。すべての関数が自動カリー化され、データを最後の引数に取る設計により、パイプライン処理が自然に書けます。

Rambda は 2019 年にリリースされた比較的新しいライブラリで、Ramda の API を踏襲しつつ、バンドルサイズとパフォーマンスを大幅に改善しています。

#ライブラリリリース年設計思想バンドルサイズ(gzip)
1Lodash2012命令的・実用的約 24KB
2Ramda2013関数型・純粋約 12KB
3Rambda2019Ramda 互換・軽量約 4KB

これらのライブラリは、それぞれ異なる開発スタイルと目的に最適化されているため、選択には慎重な比較が必要です。

課題

開発者が直面する選択の難しさ

JavaScript エコシステムには多数のユーティリティライブラリが存在し、どれを選ぶべきか判断が難しいという課題があります。

以下の図は、ライブラリ選択時に開発者が考慮すべき主な要素を示しています。

mermaidflowchart LR
  decision["ライブラリ選択"] --> factor1["パフォーマンス"]
  decision --> factor2["バンドルサイズ"]
  decision --> factor3["開発体験(DX)"]
  decision --> factor4["学習コスト"]
  decision --> factor5["チームの慣習"]

  factor1 --> result["最適な選択"]
  factor2 --> result
  factor3 --> result
  factor4 --> result
  factor5 --> result

パイプライン処理の書き方の違い

データ変換処理を複数のステップに分けて記述するパイプライン処理は、コードの可読性を大きく左右します。しかし、ライブラリごとに記法が異なるため、混乱を招くことがあります。

Lodash はチェーン記法を推奨し、Ramda と Rambda はパイプ関数を使ったポイントフリースタイルを採用しています。これらの違いは、コードの構造だけでなく、型推論や再利用性にも影響を与えます。

カリー化の有無による設計の違い

関数型プログラミングにおいて、カリー化は部分適用を可能にし、関数の再利用性を高める重要な機能です。

Ramda と Rambda はすべての関数が自動的にカリー化されているのに対し、Lodash ではカリー化は明示的に行う必要があります。この違いは、関数を組み合わせる際の記述スタイルに大きな影響を及ぼします。

型定義とエディタ補完の精度

TypeScript を使用する場合、型定義の品質はエディタの補完精度やバグの早期発見に直結します。

Lodash は DefinitelyTyped で型定義が提供されていますが、チェーン記法における型推論には限界があります。Ramda も型定義が提供されていますが、カリー化された関数の型は複雑になりがちです。Rambda は TypeScript で書かれており、型定義が組み込まれています。

#課題LodashRamdaRambda
1パイプライン記法チェーンパイプ関数パイプ関数
2カリー化明示的自動自動
3型定義DT 提供DT 提供組み込み
4学習コスト

これらの違いを理解せずにライブラリを選択すると、開発効率や保守性に悪影響を与える可能性があります。

解決策

パイプライン記法の実装方法

それぞれのライブラリでデータ変換のパイプラインを実装する際の記法を比較します。

以下は、配列から偶数のみを抽出し、2 倍にして、合計を求める処理の例です。

Lodash のチェーン記法

typescriptimport _ from 'lodash';

// チェーン記法で処理を連結
const result = _([1, 2, 3, 4, 5, 6])
  .filter((n) => n % 2 === 0) // 偶数のみ
  .map((n) => n * 2) // 2倍
  .sum(); // 合計

console.log(result); // 24

Lodash では _() でラッパーオブジェクトを作成し、メソッドチェーンで処理を記述します。

Ramda のパイプ関数

typescriptimport * as R from 'ramda';

// 各処理を関数として定義
const isEven = (n: number) => n % 2 === 0;
const double = (n: number) => n * 2;

// パイプで処理を組み立て
const calculate = R.pipe(
  R.filter(isEven),
  R.map(double),
  R.sum
);

const result = calculate([1, 2, 3, 4, 5, 6]);
console.log(result); // 24

Ramda では pipe 関数を使い、関数を左から右へ合成します。データは最後に渡すことで、再利用可能な処理フローを作成できます。

Rambda のパイプ関数

typescriptimport * as R from 'rambda';

// Ramda と同じ API で軽量
const isEven = (n: number) => n % 2 === 0;
const double = (n: number) => n * 2;

const calculate = R.pipe(
  R.filter(isEven),
  R.map(double),
  R.sum
);

const result = calculate([1, 2, 3, 4, 5, 6]);
console.log(result); // 24

Rambda は Ramda とほぼ同じ記法ですが、バンドルサイズが小さく、実行速度も高速です。

カリー化による関数合成の利点

カリー化により、引数を部分適用した関数を簡単に作成できます。これは複雑な処理を小さな部品に分解する際に非常に便利です。

Lodash でのカリー化(明示的)

typescriptimport _ from 'lodash';

// カリー化を明示的に適用
const multiply = _.curry((a: number, b: number) => a * b);

// 部分適用で新しい関数を作成
const double = multiply(2);
const triple = multiply(3);

console.log(double(5)); // 10
console.log(triple(5)); // 15

Lodash では curry 関数を使って明示的にカリー化する必要があります。

Ramda での自動カリー化

typescriptimport * as R from 'ramda';

// すべての関数が自動的にカリー化される
const multiply = (a: number, b: number) => a * b;
const curriedMultiply = R.multiply; // Ramda の multiply は自動カリー化済み

// 部分適用が自然に書ける
const double = R.multiply(2);
const triple = R.multiply(3);

console.log(double(5)); // 10
console.log(triple(5)); // 15

Ramda では関数がデフォルトでカリー化されているため、部分適用が非常にスムーズに行えます。

Rambda での自動カリー化

typescriptimport * as R from 'rambda';

// Ramda と同じく自動カリー化
const double = R.multiply(2);
const triple = R.multiply(3);

console.log(double(5)); // 10
console.log(triple(5)); // 15

Rambda も Ramda と同様の自動カリー化をサポートしており、同じコードスタイルが使えます。

以下の図は、カリー化による関数合成のフローを示しています。

mermaidflowchart LR
  original["元の関数<br/>multiply(a, b)"] --> curry["カリー化"]
  curry --> partial1["部分適用<br/>multiply(2)"]
  curry --> partial2["部分適用<br/>multiply(3)"]

  partial1 --> func1["double 関数<br/>(b) => 2 * b"]
  partial2 --> func2["triple 関数<br/>(b) => 3 * b"]

  func1 --> result1["double(5) = 10"]
  func2 --> result2["triple(5) = 15"]

DX(開発者体験)の比較

開発者体験は、コードの書きやすさ、エディタの補完、デバッグのしやすさなど、多岐にわたります。

エディタ補完の比較

typescript// Lodash:チェーンメソッドの補完が優れている
import _ from 'lodash';

const users = [
  { name: 'Alice', age: 25 },
  { name: 'Bob', age: 30 },
];

// . を押すと利用可能なメソッドが補完される
_(users)
  .filter((u) => u.age > 20)
  .map((u) => u.name)
  .value(); // ["Alice", "Bob"]

Lodash のチェーン記法は、エディタの補完が効きやすく、初心者でも利用可能なメソッドを発見しやすいメリットがあります。

typescript// Ramda:パイプ関数の型推論
import * as R from 'ramda';

const users = [
  { name: 'Alice', age: 25 },
  { name: 'Bob', age: 30 },
];

// 各ステップでの型が推論される
const getAdultNames = R.pipe(
  R.filter((u: { age: number }) => u.age > 20),
  R.map((u) => u.name) // u の型が推論される
);

getAdultNames(users); // ["Alice", "Bob"]

Ramda のパイプでは、各ステップでの型推論が働きますが、初回の型アノテーションが必要な場合があります。

typescript// Rambda:軽量で高速な型推論
import * as R from 'rambda';

const users = [
  { name: 'Alice', age: 25 },
  { name: 'Bob', age: 30 },
];

// TypeScript ネイティブで型定義が組み込まれている
const getAdultNames = R.pipe(
  R.filter((u: { age: number }) => u.age > 20),
  R.map((u) => u.name)
);

getAdultNames(users); // ["Alice", "Bob"]

Rambda は TypeScript で書かれているため、型定義の精度が高く、エディタの補完もスムーズです。

デバッグのしやすさ

typescript// Lodash:中間結果を確認しやすい
const result = _([1, 2, 3, 4, 5])
  .filter((n) => {
    console.log('filter:', n);
    return n % 2 === 0;
  })
  .map((n) => {
    console.log('map:', n);
    return n * 2;
  })
  .value();

Lodash のチェーン記法では、各ステップで console.log を挿入してデバッグするのが簡単です。

typescript// Ramda:タップ関数でデバッグ
import * as R from 'ramda';

const logAndReturn = R.tap(console.log);

const result = R.pipe(
  R.filter((n) => n % 2 === 0),
  logAndReturn, // 中間結果をログ出力
  R.map((n) => n * 2),
  logAndReturn // 再度ログ出力
)([1, 2, 3, 4, 5]);

Ramda では tap 関数を使って中間結果を確認できます。関数型スタイルを維持しながらデバッグが可能です。

以下の表は、DX の観点から各ライブラリを比較したものです。

#観点LodashRamdaRambda
1エディタ補完★★★★★☆★★★
2型推論の精度★★☆★★☆★★★
3デバッグの容易さ★★★★★☆★★☆
4学習曲線なだらか中程度
5コミュニティ

具体例

ユースケース 1:配列のグループ化と集計

ユーザーデータから年代別の平均年齢を計算する処理を、3 つのライブラリで実装します。

サンプルデータ

typescript// 処理対象のユーザーデータ
const users = [
  { name: 'Alice', age: 25 },
  { name: 'Bob', age: 30 },
  { name: 'Charlie', age: 35 },
  { name: 'David', age: 28 },
  { name: 'Eve', age: 42 },
];

Lodash による実装

typescriptimport _ from 'lodash';

// 年代を計算する補助関数
const getAgeGroup = (age: number) =>
  Math.floor(age / 10) * 10;

// チェーン記法で処理を記述
const result = _(users)
  .groupBy((u) => getAgeGroup(u.age)) // 年代でグループ化
  .mapValues((group) => _.meanBy(group, 'age')) // 各グループの平均年齢
  .value();

console.log(result);
// { '20': 27.666..., '30': 32.5, '40': 42 }

Lodash では groupBymapValues を組み合わせて、直感的にグループ化と集計ができます。

Ramda による実装

typescriptimport * as R from 'ramda';

// 年代を計算
const getAgeGroup = (age: number) =>
  Math.floor(age / 10) * 10;

// 年齢の平均を計算
const averageAge = (users: typeof users) =>
  R.mean(R.map((u) => u.age, users));

// パイプで処理を組み立て
const calculateAverageAgeByGroup = R.pipe(
  R.groupBy((u: (typeof users)[0]) =>
    String(getAgeGroup(u.age))
  ),
  R.map(averageAge)
);

const result = calculateAverageAgeByGroup(users);
console.log(result);
// { '20': 27.666..., '30': 32.5, '40': 42 }

Ramda では関数合成により、処理フローを再利用可能な形で定義できます。

Rambda による実装

typescriptimport * as R from 'rambda';

// Ramda と同じ記法で軽量実装
const getAgeGroup = (age: number) =>
  Math.floor(age / 10) * 10;

const averageAge = (users: typeof users) => {
  const ages = R.map((u) => u.age, users);
  return ages.reduce((a, b) => a + b, 0) / ages.length;
};

const calculateAverageAgeByGroup = R.pipe(
  R.groupBy((u: (typeof users)[0]) =>
    String(getAgeGroup(u.age))
  ),
  R.map(averageAge)
);

const result = calculateAverageAgeByGroup(users);
console.log(result);
// { '20': 27.666..., '30': 32.5, '40': 42 }

Rambda は Ramda のサブセットですが、基本的な処理は同じ記法で実現できます。

以下の図は、グループ化と集計の処理フローを示しています。

mermaidflowchart TD
  input["ユーザーデータ配列"] --> group["年代でグループ化"]
  group --> g20["20代グループ"]
  group --> g30["30代グループ"]
  group --> g40["40代グループ"]

  g20 --> avg20["平均年齢 27.67"]
  g30 --> avg30["平均年齢 32.5"]
  g40 --> avg40["平均年齢 42"]

  avg20 --> output["結果オブジェクト"]
  avg30 --> output
  avg40 --> output

ユースケース 2:オブジェクトの深いマージ

複数の設定オブジェクトを深くマージして、最終的な設定を作成する処理を比較します。

マージ対象のオブジェクト

typescript// デフォルト設定
const defaultConfig = {
  server: { port: 3000, host: 'localhost' },
  database: { host: 'localhost', port: 5432 },
};

// ユーザー設定
const userConfig = {
  server: { port: 8080 },
  database: { ssl: true },
};

Lodash による深いマージ

typescriptimport _ from 'lodash';

// merge で深いマージを実行
const config = _.merge({}, defaultConfig, userConfig);

console.log(config);
/*
{
  server: { port: 8080, host: 'localhost' },
  database: { host: 'localhost', port: 5432, ssl: true }
}
*/

Lodash の merge は、ネストされたオブジェクトを再帰的にマージします。

Ramda による深いマージ

typescriptimport * as R from 'ramda';

// mergeDeepRight で右側を優先してマージ
const config = R.mergeDeepRight(defaultConfig, userConfig);

console.log(config);
/*
{
  server: { port: 8080, host: 'localhost' },
  database: { host: 'localhost', port: 5432, ssl: true }
}
*/

Ramda の mergeDeepRight は、右側の値を優先して深いマージを行います。

Rambda による深いマージ

typescriptimport * as R from 'rambda';

// Ramda と同じ API
const config = R.mergeDeepRight(defaultConfig, userConfig);

console.log(config);
/*
{
  server: { port: 8080, host: 'localhost' },
  database: { host: 'localhost', port: 5432, ssl: true }
}
*/

Rambda も mergeDeepRight をサポートしており、Ramda と同じ記法で使えます。

ユースケース 3:複雑な条件分岐のリファクタリング

複数の条件に応じて処理を分岐する場合、Ramda の cond 関数を使うと宣言的に記述できます。

条件分岐の要件

typescript// 数値を受け取り、以下のルールで分類
// - 0 より小さい → "negative"
// - 0 → "zero"
// - 100 より大きい → "large"
// - それ以外 → "positive"

Lodash による実装(if-else)

typescriptimport _ from 'lodash';

function classify(num: number): string {
  if (num < 0) {
    return 'negative';
  } else if (num === 0) {
    return 'zero';
  } else if (num > 100) {
    return 'large';
  } else {
    return 'positive';
  }
}

console.log(classify(-5)); // "negative"
console.log(classify(0)); // "zero"
console.log(classify(150)); // "large"
console.log(classify(50)); // "positive"

Lodash には条件分岐を簡潔に記述する専用関数がないため、通常の if-else を使います。

Ramda による実装(cond 関数)

typescriptimport * as R from 'ramda';

// 条件と処理のペアを配列で定義
const classify = R.cond([
  [R.lt(R.__, 0), R.always('negative')], // num < 0
  [R.equals(0), R.always('zero')], // num === 0
  [R.gt(R.__, 100), R.always('large')], // num > 100
  [R.T, R.always('positive')], // デフォルト
]);

console.log(classify(-5)); // "negative"
console.log(classify(0)); // "zero"
console.log(classify(150)); // "large"
console.log(classify(50)); // "positive"

Ramda の cond を使うと、条件と結果のペアを宣言的に記述でき、可読性が向上します。

Rambda による実装

Rambda は cond 関数を提供していないため、if-else または switch 文を使う必要があります。

typescript// Rambda では cond がサポートされていない
function classify(num: number): string {
  if (num < 0) return 'negative';
  if (num === 0) return 'zero';
  if (num > 100) return 'large';
  return 'positive';
}

console.log(classify(-5)); // "negative"
console.log(classify(0)); // "zero"
console.log(classify(150)); // "large"
console.log(classify(50)); // "positive"

以下の表は、各ユースケースにおける実装の特徴をまとめたものです。

#ユースケースLodashRamdaRambda
1グループ化と集計チェーンで直感的パイプで再利用可能Ramda とほぼ同じ
2深いマージmerge 関数mergeDeepRightmergeDeepRight
3条件分岐if-elsecond 関数if-else(cond 未対応)

まとめ

Lodash、Ramda、Rambda の 3 つのライブラリは、それぞれ異なる強みと適用場面を持っています。

Lodash は、豊富な機能とチェーン記法により、直感的で書きやすいコードを実現します。学習コストが低く、チームでの採用も容易ですが、関数型プログラミングのスタイルには向いていません。バンドルサイズが大きい点にも注意が必要です。

Ramda は、自動カリー化とデータラスト設計により、関数合成を中心とした宣言的なコードを書けます。関数型プログラミングに慣れているチームや、複雑なデータ変換処理を扱うプロジェクトに最適です。一方で、学習曲線が急であり、初心者には敷居が高いかもしれません。

Rambda は、Ramda の軽量版として登場し、バンドルサイズとパフォーマンスで優れた結果を出しています。Ramda の主要な機能をサポートしつつ、フロントエンドでのバンドルサイズを重視する場合に理想的です。ただし、一部の関数が未実装であるため、事前に必要な機能が揃っているか確認が必要です。

選択のポイントをまとめると、以下のようになります。

#重視する点推奨ライブラリ
1学習コストの低さ、チームでの採用しやすさLodash
2関数型プログラミング、宣言的なコードRamda
3バンドルサイズ、パフォーマンスRambda
4TypeScript の型推論精度Rambda / Lodash

プロジェクトの要件や開発チームのスキルセットに応じて、最適なライブラリを選択することが、長期的な保守性と開発効率の向上につながります。

関連リンク