· 7分で読了

Ramda のいくつかの API について

この記事は中国語から自動翻訳されたものです。翻訳によりニュアンスが失われている場合があります。

はじめに

Ramda は非常に便利なライブラリだ。lodash や underscore を知っていれば、Ramda を 関数型プログラミング(functional programming) 版の lodash だと捉えることができる。それらの API には多くの類似点があるが、違いは Ramda 自体が FP の機能を持っている点だ。どんな API でも、引数をすべて渡さなければ Ramda が自動的にカリー化(curry)してくれるため、非常に高い柔軟性が得られる。

例えば lodash では、API の一般的な使い方は以下のようになる:

_.map([1, 2, 3], n => n * 2) // [2, 4, 6]

一方で Ramda では次のようになる:

R.map(n => n * 2, [1, 2, 3]) // [2, 4, 6]

あるいは、このように書くこともできる:

const times2 = R.map(n => n * 2) // return function
times2([1, 2, 3]) // [2, 4, 6]

この方法を使えばデータに縛られることがなくなり、再利用性を高めることができる。

FP についてもっと詳しく知りたいなら、僕はこの記事をおすすめする:なぜ関数型プログラミングが重要なのか

直感的な関数型プログラミング

  • 同じ input に対しては必ず同じ output を返し、外部の状態によって結果が変わることはない
  • 副作用がない

なぜ Ramda を選ぶのか

Ramda の API は非常に多く、その大半はとても直感的なので、一つひとつ詳しく説明する必要はないだろう。しかし、Ramda には開発の複雑さを軽減するのに役立つ優れた API がたくさんある。以下では、僕が見ておく価値があると思う API をいくつか紹介する。

propEq

プロパティ名として文字列を受け取り、渡されたオブジェクトのプロパティ値が等しいかどうかを比較する。

const obj = {
  name: "kalan",
}

propEq("name", "kalan")(obj) // true

// 等價於
const propEq = (name, value) => obj => {
  return obj[name] === value
}

zipObj

渡された引数を object に圧縮(結合)する。

R.zipObj(["id", "title"], ["2", "mytitle"])
/*
{
  id: '2',
  title: 'mytitle'
}
*/

ifElse

分岐のロジックを扱う際に非常に便利だ。「なぜ普通の if…else をそのまま使わないのか?」と思うかもしれないが、Ramda の API はすべて関数を返すため、compose を使って他の API と組み合わせることができる。

compose

複数の関数を合成し、内側から外側へと実行していく。中学や高校でよく見かける関数の問題 f(g(h(x))) を思い浮かべるといい。つまり、まず h(x) の値を計算し、順番に適用していくわけだ。

const a = compose(
  toInteger,
  toCurrency("TWD"),
  toUppercase
)("125000")

useWith

1つの関数と、関数の配列を受け取る。データを関数の配列に渡して計算された結果を、最初の関数に渡す。

useWith をうまく使えば、ポイントフリー(point-free)スタイルを実現するのに役立つ。

const currencies = [
  { name: "TWD", shape: "$" },
  { name: "USD", shape: "$" },
  { name: "JPY", shape: "¥" },
  { name: "CAD", shape: "$" },
]

// without useWith
const getCurrency = (name, dic) => R.find(R.propEq("name", name), dic)

getCurrency("TWD", currencies) // $

// with useWith
const getCurrency = R.useWith(R.find, [R.propEq("name"), R.identity]) // 將第一個參數傳入 R.propEq,第二個參數傳入 R.identity,運算後的結果分別丟給 R.find 的第一與第二個參數。

getCurrency("TWD", currencies)

useWith を使ったことで、name と dic という2つの引数を排除できた。

useWith の概念図:2つの入力を別々に変換してから結果を組み合わせる

converge

この関数は先ほどの useWith と少し似ているが、converge は1つの引数のみを受け取る。以下の図を見れば、この2つの違いが直感的にわかるだろう。

const numbers = [1, 2, 5, 8, 10]

const getRange = R.converge(substract, [getFirst, getLast])(numbers) // return 9

converge の概念図:同じ入力を2つの関数に渡して結果を組み合わせる

identity

直感的すぎて……逆にどう説明したらいいかわからない。直接コードで説明したほうが早い。

const identify = arg => arg

では、なぜこのようなことをするのだろうか?時には関数を組み立ててチェーン可能(chainable)にする必要がある場合があり、そんな時にこの identity 関数が役に立つ。

tap

指定した関数に引数を渡し、その後その値をそのまま返す。これはデバッグや他のサードパーティツールと連携する際に非常に便利だ。

tap(console.log)("hello world") // 傳入 hello world 給 console.log,並且回傳 hello world 這個值

上記の例だけでは tap の有用性がわかりにくいかもしれないので、compose と組み合わせて使ってみよう。

const uploadToMedium = article => API.postArticle(article)
const notifyAdmin = article => API.notify(article, subscribers)
const log = article => Logger.log(article)
const preprocessArticle = article => article.toLowerCase()

const publishPostFlow = article =>
  compose(
    preprocessArticle,
    R.tap(uploadToMedium),
    R.tap(notifyAdmin),
    R.tap(log)
  )

publishPostFlow(article)

こうすることで、わざわざ return article のようなコードを書くことなく他のサービスと簡単に連携できるようになり、ボイラープレートコードの手間を減らせるだけでなく、バグを生む可能性も減らせる。

pluck

文字どおりの意味で、指定されたキーの値を抽出して新しい配列にする。ネストされたオブジェクトから値を取り出すときにとても役立つ。例えば:

const data = [
  {
    id: "1",
    content: "content...",
  },
  {
    id: "2",
    content: "content...",
  },
  {
    id: "3",
    content: "content...",
  },
]

const getIds = R.pluck("id", data) // return ['1','2','3']

pick, pickBy, pickAll

実務ではオブジェクト内のすべてのプロパティを使うわけではなく、一部のプロパティだけを取り出して使いたいことが多い。この3つの pick 系の関数を使えば、それを簡単に行うことができる:

const data = {
  url: "https://api.github.com/repos/example/magazine/issues/1",
  repository_url: "https://api.github.com/repos/example/magazine",
  labels_url:
    "https://api.github.com/repos/example/magazine/issues/1/labels{/name}",
  comments_url:
    "https://api.github.com/repos/example/magazine/issues/1/comments",
  events_url: "https://api.github.com/repos/example/magazine/issues/1/events",
  html_url: "https://github.com/example/magazine/issues/1",
  id: 252372781,
  number: 1,
  title: "test issue",
}

R.pick(["url", "repository_url", "id", "name"], data) // 回傳這三個屬性的值,如果找不到此屬性直接忽略

R.pickAll(["url", "repository_url", "name"], data) // 回傳值如果屬性存在,沒有的話會回傳 undefined

const isURL = (value, key) => key.indexOf("_url") !== -1
R.pickBy(isURL, data) // 回傳任何屬性含有 _url

pathOr

フロントエンドでバックエンドの API を呼び出す際、返ってくる JSON がかなり深い構造を持っていることがある。通常のように a && a.b && a.b.c という形で値を取得しようとすると、コードがかなり煩雑になるだけでなく、階層が深くなるほど多くの条件分岐を書かなければならなくなる。

pathOr は値を取り出すパスの順序として配列を受け取り、値の取得結果が undefined だった場合にはデフォルト値を返す。

const article = {
  id: "116208916",
  author: {
    information: {
      birthday: "1994-11-11",
      name: "kalan",
    },
    subscribers_count: 1239,
  },
  content: {
    title: "title",
    body: "body",
  },
}

バックエンドから返ってくるフィールドが不足しているとエラーが発生する可能性が高いが、そのような場合に pathOr を使って処理できる。こうすれば、誕生日に値がない場合でも「未提供生日」という文字列で代替できる。

const getBirthday = R.pathOr(
  ["author", "information", "birthday"],
  "未提供生日"
)
getBirthday(article)

memoize

素数や階乗の計算など計算量が比較的多い場面で、値を取得するたびに再計算しないように、memoize 関数を使ってすでに計算した結果をキャッシュできる。

まとめ

Ramda は非常に便利なライブラリだ。この記事では通常あまり見かけないいくつかの API を紹介したが、Ramda 自体の API は非常に豊富だ。compose と組み合わせることで独自の関数を自由に組み立て、上記の方法を活用して関数をシンプルに保つことができる。

もし君もこのようなコードスタイルが好きなら、関数型プログラミングの世界へようこそ。

関連記事

他のトピックを探索