JavaScript の関数型ライブラリ Ramda.js を使ってみる


こんにちは。フロントエンドエンジニアの辻です。
今回は JavaScript の関数型ライブラリ「Ramda.js」を紹介します!

Ramda.js とは?

Ramda.js とは、関数型プログラミングの考え方を取り入れたユーティリティライブラリです。

JavaScript のユーティリティライブラリと言えば Lodash が有名ですね。
Ramda.js とは、いうなれば「関数型プログラミングを取り入れた Lodash」です。

導入

一般的な npm パッケージと同じで、npm install でインストールできます。

npm install ramda

TypeScriptを利用している場合は、型定義もインストールします。

npm install @types/ramda --save-dev

インストールが正常に完了すれば、Ramda.js が扱えるようになっているはずです。
下記のように import か require 文で読み込めれば準備完了です。

import * as R from "ramda"
// or
// const R = require('ramda')

Install | Ramda.js
Usage | Ramda.js

Ramda.js の便利な関数

ここからはよく使う Ramda.js の便利な関数を紹介していきます!

clone()

clone() は、引数に入れた要素のディープコピーを返却します。

const obj = {
  a: 1,
  b: 10,
  c: 100
}
const cloned = R.clone(obj) // { a: 1, b: 10, c: 100 }

mergeAll()

mergeAll() は、引数に入れた配列内のオブジェクトをすべてマージして返却します。

const objA = {
  a: 1
}
const objB = {
  b: 10
}
const objC = {
  c: 100
}
const merged = R.mergeAll([objA, objB, objC]) // { a: 1, b: 10, c: 100 }

equals()

equals() は、引数に入れた要素同士が一致するかを判定します。

const obj = {
  a: 1,
  b: 10,
  c: 100
}
const anotherObj = {
  a: 1,
  b: 10,
  c: 100
}
R.equals(obj, anotherObj) // true

pick()

pick()は、第二引数のオブジェクトから、第一引数で指定したプロパティを抽出したオブジェクトを返却します。

const obj = {
  a: 1,
  b: 10,
  c: 100
}
const picked = R.pick(["a"], obj) // { a: 1 }

カリー化

ちなみに、Ramda.js の多くの関数はカリー化されています。
例えば、下記のように pick() に第一引数のみ指定した場合、pickA には「引数のオブジェクトから、プロパティ a を抽出する関数」が代入されます。
pickA はただの関数ですので、様々なオブジェクトに利用できます。

const obj = {
  a: 1,
  b: 10,
  c: 100
}
const anotherObj = {
  a: 2,
  b: 20,
  c: 200
}
const pickA = R.pick(["a"])
pickA(obj) // { a: 1 }
pickA(anotherObj) // { a: 2 }
pickA({}) // { }

今回は pick() を使って「aを抽出する関数」を作成しましたが、同じように「bを抽出する関数」「cを抽出する関数」も作れます。

このようにカリー化された Ramda.js の関数は「引数の設定」と「関数の実行」を分離できるため、少ないコードから再利用可能な関数をカンタンに作成できます。

omit()

omit()は、第二引数のオブジェクトから、第一引数で指定したプロパティを除外したオブジェクトを返却します。

const obj = {
  a: 1,
  b: 10,
  c: 100
}
const omitted = R.omit(["a"], obj) // { b: 10, c: 100 }

compose() と pipe()

compose() と pipe() は、いずれも引数の関数を合成して1つの関数にまとめます。

compose() と pipe() の違いは、関数の実行順(合成順)にあります。
compose() は、引数の関数のうち、最後に入れた関数から順次実行します。
pipe() は、引数の関数のうち、最初に入れた関数から順次実行します。

また、compose() と pipe() の注意点として、合成する関数の入出力値の型を合わせておく必要があります。

const composed = R.compose(
  (i) => i + 20, // 3番目に実行される
  (i) => i * 3, // 2番目に実行される
  (i) => i * i // はじめに実行される
)
composed(10) // 320 = ((10 * 10) * 3) + 20

const piped = R.pipe(
  (i) => i + 20, // はじめに実行される
  (i) => i * 3, // 2番目に実行される
  (i) => i * i // 3番目に実行される
)
piped(10) // 8100 = ((10 + 20) * 3) ** 2

この compose() と pipe() は、Ramda.js を代表する強力な機能の1つで、複雑なプログラムを簡潔にまとめる事ができます。
特に重厚長大な手続き的プログラムや、コールバック地獄には効果バツグンです。

例えば、下記のような大量の関数を手続き的に実行する場合、 pipe() を利用して関数をまとめると良いでしょう。

// 改善前
// なが〜い手続き的プログラム
doA()
doB()
doC()
doD()
…

if( … ) {
  doE()
  doF()
  doG()
  doH()
  doI()
  …
} else {
  doJ()
  doK()
  doL()
  doM()
  doN()
  …
}
// 改善後
const pipedA = R.pipe(doA, doB, doC, doD)
const pipedE = R.pipe(doE, doF, doG, doH, doI)
const pipedJ = R.pipe(doJ, doK, doL, doM, doN)

pipedA()
if( … ) {
  pipedE()
} else {
  pipedJ()
}

1つ1つの関数を小さく作り、 compose() ・ pipe() でまとめあげれば、メンテナンス性・可読性の向上に寄与します。

関数1つ分が小さい、改修時に修正範囲を最小限に抑える事が出来ますし、関数の実行順を変更したい場合は compose() ・ pipe() の引数の順番を変更するだけで済みます。

新規機能開発時に、compose() ・ pipe() で作成された関数をコピーして、一部を差し替えるだけで完了しました!…ってケースもあります。

おわりに

今回は JavaScript の関数型ライブラリ「Ramda.js」の導入と、その強力な機能の一端を紹介しました。
本記事で紹介した機能以外にも、実に様々な便利機能が Ramda.js に備わっています。

まだまだ紹介し足りないので、折を見て Ramda.js に触れられればと思っています!

Gaji-Labo は Next.js, React, TypeScript 開発の実績と知見があります

フロントエンド開発の専門家である私たちが御社の開発チームに入ることで、バックエンドも含めた全体の開発効率が上がります。

「既存のサイトを Next.js に移行したい」
「人手が足りず信頼できるエンジニアを探している」
「自分たちで手を付けてみたがいまいち上手くいかない」

フロントエンド開発に関わるお困りごとがあれば、まずは一度お気軽に Gaji-Labo にご相談ください。

オンラインでのヒアリングとフルリモートでのプロセス支援にも対応しています。

Next.js, React, TypeScript の相談をする!

投稿者 Tsuji Atsuhiro

フロントエンドエンジニア。 DTP・Webデザイナーを経験した後、フロントエンドエンジニアに転向。HTML/CSS/JavaScriptを中心にWeb開発を担当してきました。 UI・UXに興味があり、デザイン・コーディング両面から考えられるデザインエンジニアを目指しています。 普段はマラソンやボクシングなどで体を動かしてます。