Tagbangers Blog

CurryingとFunctional Composition in JavaScript

Currying

Currying(カリー化)のコンセプト自体はとてもシンプルで、複数の引数を一度に一つずつとる関数です。

例えば、2つの実引数の和を返す関数を考えた場合、まず一つの実引数を受け取る関数を返します。返り値となる関数は次の実引数を受け取る関数を返します。

const add = x => y => x + y

add関数は一つの引数をとって、closureスコープに固定された部分適用された関数を返すという形になります。カリー化は常に一つの実引数を取る関数(unary function)を返しますが、部分適用の実引数の数は任意です。

const add =
    x =>
    ↑    y =>
    ↑        x + y
    +------ // 最初のxを参照した y => x + yを返す

addは以下のように使うことができます。

add(40)(2) // 42

// or

const add40 = add(40)
add40(2) // 42

この関数の面白いところは、add関数を一定の目的を持つ関数を返す関数として使うことができる点です。add関数をベースに数値に50を足す関数、100を足す関数というのが作れます。


Functional Composition

Functional Composition(関数合成)は複数の関数を組み合わせてより複雑な処理をさせるテクニックです。以下のような単純な関数を組み合わせることで、より高度なことができる関数を作ります。

基本的には以下のようなアイデアで、関数と関数を組み合わせて新しい関数を返します。

const compose = (f, g) => x => f(g(x))


const { compose } = require('ramda')

const split = str => str.split(/\s+/)
const count = arr => arr.length

const wc = compose(count, split)

const sentence = 'The quick brown fox jumps over the lazy dog'

wc(sentence) // 9

文字列を空白で区切った配列を返す、配列の要素数を返すという一つのことをやる関数を組み合わせて、文章の単語の数を返すという関数を作ることができました。この例ではRamdaのcomposeを使っていますが、以下のように定義可能です。

const compose = (...fns) => x => fns.reduceRight((y, f) => f(y), x)

wc関数のもととなる2つの関数はcomposeの時点では評価されず、wcが引数とともに実行されるまで待機する状態になります。Functional Compositionの面白い点は「関数の記述と評価を分離する」ことができる点です。

composeは右から左へ関数が実行されますが、合成する関数の数が増えてくると、読みにくくなるかもしれません。その場合は左から右へ実行されるpipeを使うこともできます。

const pipe = (...fns) => x => fns.reduce((y, f) => f(y), x)
const wc = pipe(split, count)

Currying + Functional Composition

Curryingは汎用的な関数から特定の目的を持った関数を作ることができるという面で便利ですが、Functional Compositionを行う際に最も威力を発揮します。関数は任意の数の引数を取ることができますが、一つの値しか返せません。関数の合成を行うためには、ある関数の出力の型が次の関数の入力の型に揃っている必要があります。

f: a => b
g:        b => c
compose(f, g): a => c

この関数の流れが以下のようになっていると、関数gは関数fの出力を正しく受け取ることができません。

f: a => b
g:       (x, b) => c
compose(f, g): a => c

このような場合に関数gをcurryingします。Curryingすることで複数の引数を利用する関数を、引数を一つずつとる関数にすることができるからです。Ramdaが提供している関数はすべてCurringされているので、このような場合に便利です。

例題

特定の決まりを持つ文字列から、それが意味する内容のオブジェクトに変換したい。

// label format: 'YYYY/MM/DD'
const label = '2019/06/07'
const format = ['year', 'month', 'day']

label
  .split('/')
  .reduce((acc, unit, idx) => ({
    ...acc,
    [format[idx]]: parseInt(unit)
  }), {})

これをCurrying + Composeで書いてみると以下のようになります。

const R = require('ramda')

const extractLabel = R.compose(
  R.fromPairs,
  R.zip(format),
  R.map(parseInt),
  R.split('/')
)

extractLabel(label) // {day: 7, month: 6, year: 2019}


上と比べると、「関数の記述と評価を分離する」というのがわかりやすいかと思います。

※ちなみにこの例題はRamdaのzipObjで解決できます


おわりに

Functional Compositionを使うことで、関数の評価を、その実装と分離することができるのでコードの見通しがよくなりました。

また、小さな関数を組み合わせて処理を記述することになるので、小さな関数の入出力をテストすることで複雑な問題にも対応できます。

[参考]