ラムダのいくつかのAPIについてお話します

作成者:カランカラン
💡

質問やフィードバックがありましたら、フォームからお願いします

本文は台湾華語で、ChatGPT で翻訳している記事なので、不確かな部分や間違いがあるかもしれません。ご了承ください

前言

ramdaは非常に便利なライブラリです。lodashやunderscoreを知っているなら、ramdaはfunctional programmingのlodashと考えてもらえれば良いでしょう。彼らのAPIには多くの類似点がありますが、ramda自体にはFPの機能が備わっており、APIに引数を渡さない限り、ramdaが自動的にカリー化を行います。これにより、かなりの柔軟性が提供されます。

例えば、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は非常に多く、ほとんどの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

関数と関数の配列を受け取ります。データを関数の配列に投げ込んで得られた結果を、最初の関数に投げ込む仕組みです。

useWithを活用することで、point freeの効果を得ることができます。

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

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

getCurrency("TWD", currencies) // $

// useWithあり
const getCurrency = R.useWith(R.find, [R.propEq("name"), R.identity]) // 最初の引数をR.propEqに、2番目の引数をR.identityに渡し、計算結果をそれぞれR.findの1番目と2番目の引数に投げます。

getCurrency("TWD", currencies)

useWithを使うことでnamedicの2つの引数を排除しました。

fn1

converge

この関数は上記のuseWithと似ていますが、convergeは1つの引数のみを受け取ります。以下の図で、これら2つの違いを直感的に見ることができます。

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

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

fn2

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を組み合わせることで自分のfunctionを自由に組み立てることができ、上記の方法を利用して関数を簡素化することができます。

もしこのようなコードスタイルが好きなら、関数型プログラミングの世界に足を踏み入れることをお勧めします。

この記事が役に立ったと思ったら、下のリンクからコーヒーを奢ってくれると嬉しいです ☕ 私の普通の一日が輝かしいものになります ✨

Buy me a coffee