Tagbangers Blog

純粋関数ライブラリ Ramda.js を使う(関数紹介編)

最近のプロジェクトではフロントエンドは TypeScript を用いたプロジェクトがデファクトスタンダードとなっています

いくつか定番のライブラリも入れていますが、その中で今回はユーティリティライブラリとして Ramda.js をご紹介します

Ramda.js

https://ramdajs.com

ユーティリティライブラリの一つで類似するものとして、弊社では以前 Lodash.js を使用していました

https://lodash.com

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 には多くの関数があります

https://ramdajs.com/docs/

この機会にぜひ Ramda.js をお試しください