最近のプロジェクトではフロントエンドは 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) // => 100
equals
===
と同じと思いきや、ネストするオブジェクトが完全一致するかまで見てくれる便利関数です(いわゆる deepEquals
)
equals( { a: 'A', b: { c: 'C', }, }, { a: 'A', b: { c: 'C', }, }, ) // => true
prop
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) // => true
isNil
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 をお試しください