最近のプロジェクトではフロントエンドは TypeScript を用いたプロジェクトがデファクトスタンダードとなっています
いくつか定番のライブラリも入れていますが、その中で今回はユーティリティライブラリとして Ramda.js をご紹介します
Ramda.js
ユーティリティライブラリの一つで類似するものとして、弊社では以前 Lodash.js を使用していました
Ramda.js の特徴としては、全ての関数が純粋関数型でカリー化が行われているという点です
純粋関数型
全ての関数は副作用を持たず、引数として渡した値自体を壊すことなく結果を新しい値として生成します
例えば下記の関数は引数として渡したオブジェクトの中身を直接編集してしまう破壊的な処理を行なっているため、純粋関数ではありません
function editProp(object, propName, propValue) {
object[propName] = propValue
return object
}純粋関数とするためには Object.assign やスプレッド演算子を用いて非破壊な処理にする必要があります
function editProp(object, propName, propValue) {
return {
...object,
[propName]: propValue
}
}Ramda.js では全ての関数が純粋関数であるため、意図しない不具合を回避できます
カリー化
引数が複数ある場合にまとめて引数に値を渡すこともできれば、1つずつ渡すこともできます
例えば先ほどの関数を例にとると、Ramda.js では assoc という関数が用意されており最大3つの引数を取りますが、バラバラに引数を与えることができま
assoc(propName, propValue, object) assoc(propName, propValue)(object) assoc(propName)(propValue)(object)
これに何のメリットがあるかというと、特定の処理を行う関数を定義することができます
const makeNameToJohn = assoc('name', 'John')
const result = makeNameToJohn({ name: 'Michael' , age: 20 })
console.log(result) // { name: 'John', age: 20 }これにより表現の幅が大きく広がります
よく使う関数
identity always
単純に値を返す関数です
identity は引数をそのまま返却します、always は引数に関係なく固定の値を返します
identity(100) // => 100
always('Jone')(100) // => 100equals
=== と同じと思いきや、ネストするオブジェクトが完全一致するかまで見てくれる便利関数です(いわゆる deepEquals)
equals(
{
a: 'A',
b: {
c: 'C',
},
},
{
a: 'A',
b: {
c: 'C',
},
},
) // => trueprop props propEq path paths pathEq
オブジェクトのプロパティにアクセスします、オブジェクトが null undefined の場合にエラーではなくundefined を返すため、安全にプロパティのアクセスができます
複数形の場合は複数のプロパティを取ってこれます
xxxEq は比較ができます
ネストするオブジェクトへは path 系関数を用います
const object = {
a: 'A',
b: {
c: 'C',
},
}
prop('a', object) // => 'A'
props(['a', 'b'], object) // => ['A', { c: 'C' }]
propEq('a', 'A', object) // => true
path(['b', 'c'], object) // 'C'
paths([['a'], ['b', 'c']], object) // => ['A', 'C']
pathEq(['b', 'c'], 'C', object) // => trueisNil isEmpty
条件分岐時に値の存在判定としてよく使いますね
判定方法として
if (!object) {
return
}のように記述する人もいるかと思いますが、この判定方法は falsy な値かを判定するため下記に引っかかりやすいです
0''NaN→false{}[]→true
一方で isNil isEmpty は判定がはっきりしているのでわかりやすさがあると感じます
(Ramda.js のドキュメントから転載)
R.isNil(null); //=> true
R.isNil(undefined); //=> true
R.isNil(0); //=> false
R.isNil([]); //=> false
R.isEmpty([1, 2, 3]); //=> false
R.isEmpty([]); //=> true
R.isEmpty(''); //=> true
R.isEmpty(null); //=> false
R.isEmpty({}); //=> true
R.isEmpty({length: 0}); //=> false
R.isEmpty(Uint8Array.from('')); //=> true逆値を使いたい時も多いと思うので下記の関数を定義しているプロジェクトも多いです
const isNotNil = complement(isNil) const isNotEmpty = complement(isEmpty)
complement は返り値が Boolean な関数の結果を反転させる関数です
anyPass allPass
条件分岐時に、特定のオブジェクトに対して複数の判定を行いたい時があるかと思います
if (isNotNil(object) && isNotEmpty(object)) {
// ...
}この場合に anyPass は &&、anyPass は|| の代替として利用することができます
const objectFulfilled = allPass([isNotNil, isNotEmpty])
if (objectFufilled(object)) {
// ...
}ifElse when unless cond
条件分岐そのものの関数もあります
ifElse はよく三項演算子として記述する使い方に近いです
const detectType = ifElse(
propEq('enabled', true), // 条件
always('ENABLED'), // true の場合
always('DISABLED'), // false の場合
)
detectType({ enabled: true }) // => 'ENABLED'
// object.enabled === 'true' ? 'ENABLED' : 'DISABLED' と同じwhen unless は条件を「満たした場合」「満たさなかった場合」に処理を行い、それ以外は引数の値をそのまま返す関数を作成できます
cond は switch 文が記述できます
const detectEnvirontment = cond([
[equals('production'), always('PRODUCTION')],
[equals('staging'), always('STAGING')],
[T, always('DEVELOPMENT')]
])
detectEnvirontment('production') // => 'PRODUCTION'
detectEnvirontment('hoge') // => 'DEVELOPMENT'T は引数に関わらず true を返す関数で always(true) と同義です
assoc assocPath dissoc dissocPath
オブジェクトからプロパティを追加(編集)、削除する関数です
オブジェクトを非破壊的に編集するときはスプレッド演算子などを使う必要があり、場合によってはプロパティが null でないかの考慮も必要になり、冗長な書き方になりがちです
return {
...object,
a: {
...object.a,
[b]: 'B',
},
}そういったケースでシンプルに記述することができます
return assocPath(['a', 'b'], 'B', object)
dissoc 系は冗長な記述になりがちなプロパティの削除を非破壊的に行え便利です
sort sortWith
配列ソートを簡単に記述できます、条件が複数ある場合は sortWith を使います
const sortProducts = sortWith([
descend(prop('rate')),
assend(prop('id')),
])
sortProducts([
{ id: 0, rate: 3 },
{ id: 1, rate: 5 },
{ id: 2, rate: 5 },
]) // => [{ id: 1, rate: 5 }, { id: 2, rate: 5 }, { id: 0, rate: 3 }]applySpec
オブジェクトのマッピング関数を書くのに重宝します
const applyAccountSpec = applySpec({
id: prop('accountId'),
name: prop('accountName')
enabled: propSatisfies(isNil, 'closedAt')
})
applyAccountSpec({ accountId: 10, accountName: 'John' }) // => { id: 10, name: 'John', enabled: true }propSatisfies は特定のプロパティの検証を行なっています
pipe compose
複数の関数を合成することができます、これによりより複雑な処理を行う関数を定義できるようになります
pipe は書いた順に処理が行われますが、compose は逆に処理が行われます
結果から書いていくかどうかによって使い分けを行います
const pickFirstUrl = pipe(
propOr([], 'urls'),
head,
)
pickFirstUrl({
urls: ['https://google.com', 'https://yahoo.co.jp']
}) // => 'https://google.com'head は配列の先頭を取ってくる関数です
総括
紹介した以外にも Ramda.js には多くの関数があります
この機会にぜひ Ramda.js をお試しください
