Ramda.js を使って合成関数を作成してみる
こんにちは、フロントエンドエンジニアの辻です。
前回の記事では「Ramda.js」を通してカリー化に触れてみました。
今回は Ramda.js の pipe()
を使って、関数を合成していきます!
Ramda.js の pipe()
以前の記事にて pipe()
を紹介しましたが、改めて説明します。
Ramda.js の pipe()
は、引数の関数を合成して 1 つの関数(合成関数)にまとめる関数です。pipe()
は実行時に関数を引数にとります。その引数のうち、第一引数の関数を起点にして、順次 関数を実行する関数を生成します。
const piped = R.pipe(
(i) => i + 20, // はじめに実行される
(i) => i * 3, // 2番目に実行される
(i) => i * i // 3番目に実行される
)
piped(10) // 8100 = ((10 + 20) * 3) ** 2
pipe() は連続した処理に効果的
pipe()
によって生成される関数が威力を発揮する場面は多々あります。
特に「特定のデータに連続した処理を実行する」場面では、非常に心強いです。
例えば、TypeScript コードにおいて以下のような Student
型があるとします。
type Student = {
name: string
age: number
score: {
math: number
english: number
chemistry: number
physics: number
biology: number
}
}
Student
型のデータに次の 5 ステップを実行するプログラムを書くとしましょう。
score
のmath
に 10 加算score
のenglish
から 10 減算age
が 20 以下ならscore
のphysics
に 5 加算score
のmath
が 50 以下ならchemistry
に 20 加算age
が 20 以下 かつscore
のchemistry
が 70 以上ならbiology
に 10 加算
まずは手続き的な関数を書いてみます。
const handleStudent = (student: Student) => {
// 1.
student.score.math += 10
// 2.
student.score.english -= 10
// 3.
if (student.age <= 20) student.score.physics += 5
// 4.
if (student.score.math <= 50) student.score.chemistry += 20
// 5.
if (student.age <= 20 && student.score.chemistry >= 70) student.score.biology += 10
return student
}
さて、ステップ 1 〜 5 の処理を実行できる handleStudent
関数は作成できたものの、どうも使いづらさが残ります。
ここで「handleStudent
関数のステップ 1 〜 5 のうち、3 のみを変更した関数がほしい」との要望があったとします。
手っ取り早い方法は、handleStudent
関数をコピーして新しい関数を作成し、ステップ 3 の処理だけ調整する方法ですね。
もしくは handleStudent
関数に真偽値の引数を 1 つ設けて、ステップ 3 の実行を切り分けても良いでしょう。
…では、もしステップ数が 10・20 あるとしたら、どうでしょう?
一部のステップだけでなく、複数のステップを変更する必要があるとしたら?
ステップの順番を入れ替える必要があるとしたら?
単純に handleStudent
関数を改変し続けていけば、重厚長大なプログラムが出来てしまうでしょう。どこかで戦略を練る必要があります。
ここで pipe()
の出番です!handleStudent
関数を Ramda.js の pipe()
を使って改善してみます。
まずは ステップ 1 〜 5 を、1 つの関数に分解していきいます。
// 1.
const addMath = (student: Student) => {
student.score.math += 10
return student
}
// 2.
const subEnglidh = (student: Student) => {
student.score.english -= 10
return student
}
// 3.
const addPhysics = (student: Student) => {
if (student.age <= 20) student.score.physics += 5
return student
}
// 4.
const addChemistry = (student: Student) => {
if (student.score.math <= 50) student.score.chemistry += 20
return student
}
// 5.
const addBiology = (student: Student) => {
if (student.age <= 20 && student.score.chemistry >= 70) student.score.biology += 10
return student
}
次に分解した 5 つの関数を pipe()
の引数にいれて関数を合成します。
import * as R from "ramda"
// pipe で合成した関数
const pipedHandleStudent = R.pipe(
addMath,
subEnglidh,
addPhysics,
addChemistry,
addBiology
)
// Equal? true
console.log(
"Equal? ",
R.equals(
handleStudent(R.clone(studentA)),
pipedHandleStudent(R.clone(studentA))
)
)
これではじめに作成した handleStudent
関数と同じ処理を実行できる pipedHandleStudent
関数を合成できました。
さて、ここで「ステップを追加した処理を新たに作成したい」との要望があったとします。
対応方法はシンプルです。
これまでと同様、小さな関数を必要な分だけ作成して、pipe()
の引数に追加するだけで OK です。
pipe()
により生成される関数は、引数の設定時に実行内容が決定づけられます。
したがって、pipedHandleStudent
関数を残したままに、新しい関数 pipedNewHandleStudent
を作成できます。
import * as R from "ramda"
// pipe で合成した関数
const pipedHandleStudent = R.pipe(
addMath,
subEnglidh,
addPhysics,
addChemistry,
addBiology
)
// pipe で新しい関数をもう 1 つ作成
const pipedNewHandleStudent = R.pipe(
addMath,
subEnglidh,
addPhysics,
addChemistry,
addBiology,
newStepFunc1, // 追加
newStepFunc2, // 追加
newStepFunc3, // 追加
newStepFunc4, // 追加
newStepFunc5, // 追加
// …
)
また、別の要望があったとしましょう。
要望の内容は「はじめに作成した pipedHandleStudent()
関数の実行順を逆転させた処理が新たに欲しい」とします。
どうすれば対応できるか…は明白ですね。pipe()
の引数の順番だけ変えれば OK です。
(引数の順番をそのままで compose()
を使っても、同じ関数が得られます。)
import * as R from "ramda"
// pipe で合成した関数
const pipedHandleStudent = R.pipe(
addMath,
subEnglidh,
addPhysics,
addChemistry,
addBiology
)
const pipedNewHandleStudent = R.pipe(
// … 略
)
// pipedHandleStudent() の実行順を逆転させた関数
const pipedReverseHandleStudent = R.pipe(
addBiology,
addChemistry,
addPhysics,
subEnglidh,
addMath
)
このようにpipe()
関数を使うと、小さな関数を合成して、大きな処理をこなす関数を作成できます。
ステップの 1 つ 1 つを小さな関数で定義できるので、単体テストも行いやすく、保守性・可読性もあがります。
また、pipe()
の引数の順序を組み替えるだけで、簡単に実行順序を変更できるので、改修しやすくもなります。
まとめ
今回は Ramda.js の pipe()
を使って、関数を合成してみました!pipe()
は Ramda.js を代表する強力な機能ですので、ぜひ機会があれば使ってみてください。
Gaji-Labo は Next.js, React, TypeScript 開発の実績と知見があります
フロントエンド開発の専門家である私たちが御社の開発チームに入ることで、バックエンドも含めた全体の開発効率が上がります。
「既存のサイトを Next.js に移行したい」
「人手が足りず信頼できるエンジニアを探している」
「自分たちで手を付けてみたがいまいち上手くいかない」
フロントエンド開発に関わるお困りごとがあれば、まずは一度お気軽に Gaji-Labo にご相談ください。
オンラインでのヒアリングとフルリモートでのプロセス支援にも対応しています。
Next.js, React, TypeScript の相談をする!