カランのブログ

ソフトウェアエンジニア / 台湾人 / 福岡生活

今のモード ライト

前書き

ramdaは非常に便利なライブラリです。lodashやunderscoreを聞いたことがあるなら、ramdaは関数型プログラミングのlodashと考えることができます。彼らのAPIは多くの類似点がありますが、ramdaはFPの機能を持っており、パラメータを渡さない場合、自動的にカリー化されます。これにより、非常に柔軟性が向上します。

例えば、lodashではよく次のように使用されます:

_.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) // 関数を返す
times2([1, 2, 3]) // [2, 4, 6]

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

FPに関する詳細情報を知りたい場合は、次の記事をおすすめします:関数型プログラミングの重要性

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

  • 同じ入力には同じ出力が得られ、外部の状態に影響されません。
  • 副作用はありません。

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はすべて関数を返します。これは、他のAPIを組み立てるためにcomposeを使用できることを意味します。

compose

関数を組み合わせて、内側から外側に実行します。これは、高校や中学校でよく見かけるf(g(h(x)))のような関数の問題と関連付けることができます。したがって、まずh(x)の値を計算し、順番に進めます。

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

useWith

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

useWithをうまく活用すると、ポイントフリースタイルの効果を得ることができます。

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]) // 第1引数に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) // 9を返す

fn2

identity

非常に直感的ですが、説明する方法がわかりません。コードを使って説明する方が早いです。

const identify = arg => arg

なぜこのようにするのでしょうか?関数をチェーン可能にするために関数を組み立てる必要がある場合があります。この場合、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) // ['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) // これら3つのプロパティの値を返します。プロパティが見つからない場合は無視されます

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は、配列を受け取り、値を取得できなかった場合にデフォルト値を返します。

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

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

次の記事

還元法におけるPCAとT-Sneについての説明

前の記事

フロントエンド面接体験

この文章が役に立つと思うなら、下のリンクで応援してくれると大変嬉しいです✨

Buy me a coffee

作者

Kalan 頭像照片,在淡水拍攝,淺藍背景

愷開 | Kalan

Kalan です。台湾出身で、2019年に日本へ転職し、福岡に住んでいます。フロントエンド開発に精通しているだけでなく、IoT、アプリ開発、バックエンド、電子工作などの分野にも挑戦しています。 最近、エレキギターを始めました。ブログを通じて、より多くの人と交流できればと思っています。気軽に絡んでください