Help us understand the problem. What is going on with this article?

TypeScript で関数合成やPipe処理をする

はじめに

「関数合成」と聞くとなんだか難しいイメージがありましたが、
調べてみると初歩ならば私でも理解できる部分もあったので記事を書いてみます。
よろしくお願いします。

文字だけの場合、高階関数は型がある方が読みやすい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

次に『関数を合成して合成した関数を返す』関数を定義します。
add1times2は数値を受け取って数値を返すので以下の関数を定義すればよさそうです。

2つ関数を受け取って合成した関数を返す関数
const compose = (
  a: (n: number) => number,
  b: (n: number) => number
) => (x: number) => a(b(x))

//js だと const compose = (a, b) => x => a(b(x))

ではadd1times2を合成した関数を使ってみます。

// 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<(n: 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

最後に

この記事ではpipecomposeなどの高階関数に渡す引数の関数を、一度ローカル変数として定義しました。
ラムダ式を直接渡すことももちろん可能です。

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 といったライブラリで広く使われている関数の合成ですが、
私個人としては実務での使いどころを見いだせていません。
使いこなせるようになって副作用が少なくバグのないコードや、スコープが小さく脳内メモリに優しいコーディングがしたいです。
ここまで読んでいただきありがとうございました!!


  1. エディタで入力補完が効いて楽という点もあります。 

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした