前書き
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
を使用すると、name
とdic
という2つのパラメータがなくなります。
converge
この関数はuseWith
と少し似ていますが、convergeは1つの引数しか受け取りません。次の図を見ると、これら2つの違いが直感的にわかります。
const numbers = [1, 2, 5, 8, 10]
const getRange = R.converge(substract, [getFirst, getLast])(numbers) // 9を返す
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
を使用して独自の関数を自由に組み立て、上記の方法を使用して関数を簡素化することができます。
もし、このようなコードスタイルが好きなら、関数型プログラミングの世界へようこそ。