はじめに
「関数合成」と聞くとなんだか難しいイメージがありましたが、
調べてみると初歩ならば私でも理解できる部分もあったので記事を書いてみます。
よろしくお願いします。
文字だけの場合、高階関数は型がある方が読みやすい1と思ったので JS でなく TS を選びました。
関数を合成してみる
以下の「1を足す関数」と「2を掛ける」を合成します。
const add1 = (n: number) => n + 1
const times2 = (n: number) => n * 2
console.log(add1(5)) // 6
console.log(times2(5)) // 10
合成と言うとなんだか難しく聞こえますが、「関数を順に適用する」だけです。
// イメージ
console.log(add1(times2(5))) // 11
次に『関数を合成して合成した関数を返す』関数を定義します。
add1
とtimes2
は数値を受け取って数値を返すので以下の関数を定義すればよさそうです。
const compose = (
a: (n: number) => number,
b: (n: number) => number
) => (x: number) => a(b(x))
//js だと const compose = (a, b) => x => a(b(x))
ではadd1
とtimes2
を合成した関数を使ってみます。
// add1OfTimes2 の型は関数 (x: number) => number
const add1OfTimes2 = compose(add1, times2)
console.log(add1OfTimes2(5)) //=> 11 (5 * 2 + 1)
パイプ処理
先ほどのcompose
は引数の数が2つに固定されているので任意の引数を取れるようにします。
ついでに、引数は前から順に適用される方が直感的なので、そこも直すと以下のようになります。
const pipe = (...fns: Array<(n: number) => number>) =>
(n: number) => fns.reduce((v, f) => f(v), n)
...
はスプレッド構文です。可変長引数を定義できます。
arguments
オブジェクトでなくArray
として扱えます。
reduce
は配列の各要素に対して(左から右へ)関数を適用し、単一の値にします。
MDN の解説がわかりやすいです-> Array.prototype.reduce
さっそくpipe
を使って見ます。
const add1 = (n: number) => n + 1
const times2 = (n: number) => n * 2
const enhancer = pipe(add1, times2)
console.log(enhancer(5)) // 12
// ↑ (5 + 1) * 2
引数を増やしたり減らしたりしてみます。
// 「3を引く関数」を追加
const minus3 = (n: number) => n - 3
const enhancer2 = pipe(add1, times2, minus3)
console.log(enhancer2(5)) // 9
// ↑ (5 + 1) * 2 - 3
// 引数なし
const identity = pipe()
console.log(identity(5)) // 5
Generics
を使って数値以外もパイプ処理できようにする
上記のpipe
は「数値を受け取り数値を返す関数」しか引数に取れませんでした。
Generics
を使って文字列やオブジェクトなどに対してもパイプ処理ができるようにします。
const pipe = <T>(...fns: Array<(t: T) => T>) =>
(t: T) => fns.reduce((v, f) => f(v), t)
T
は型引数です。使うときはコンパイラが推論可能であれば、型引数は省略できます。(今回も省略します。)
これで補完などエディタの支援を受けつつも動的言語のように柔軟に処理ができるようになりました。
ではジェネリックなpipe
を文字列で使ってみます。
const addTitle = (s: string) => `Mr. ${s}`
const hello = (s: string) => `Hello ${s}!`
const greeting = (s:string) => `${s} Nice to meet you.`
const enhancer = pipe(addTitle, hello, greeting)
console.log(enhancer('Nossa')) // Hello Mr. Nossa! Nice to meet you.
console.log(enhancer('Smith')) // Hello Mr. Smith! Nice to meet you.
// もちろん今までのコードも動作します。
const enhancer2 = pipe(add1, times2, minus3)
console.log(enhancer2(5)) // 9
最後に
この記事ではpipe
やcompose
などの高階関数に渡す引数の関数を、一度ローカル変数として定義しました。
ラムダ式を直接渡すことももちろん可能です。
const greet = pipe(
(s: string) => `Mr. ${s}`,
(s: string) => `Hello ${s}!`,
(s: string) => `${s} Nice to meet you.`
)
console.log(greet('Nossa')) //=> Hello Mr. Nossa! Nice to meet you.
世間では React の HoC や redux、recompose といったライブラリで広く使われている関数の合成ですが、
私個人としては実務での使いどころを見いだせていません。
使いこなせるようになって副作用が少なくバグのないコードや、スコープが小さく脳内メモリに優しいコーディングがしたいです。
ここまで読んでいただきありがとうございました!!
-
エディタで入力補完が効いて楽という点もあります。 ↩