Edited at

javascript での関数部分適用

More than 5 years have passed since last update.

javascript での関数の部分適用(カリー化の説明を参考)は比較的カンタンにできます。

サンプルコードは CoffeeScript で書いています。

partial = (f) ->

require_length = f.length
partialized = (args...) ->
if require_length <= args.length # 関数に必要な引数がすでに足りている
g = f.apply(null, args.slice(0, require_length)) # 適用して結果を取り出す
if typeof g is "function" # 結果、関数が返ってきた
# args の余りから、次の関数を更に部分適用して返す
partial(g).apply(null, args.slice(require_length)))
else
g # 関数以外のものがきたら、適用しきった事にしてオブジェクトを返す
else
# 引数が足りてない場合、足りない引数を待ち受ける関数を返す
partial_apply = (adds...) -> partialized.apply(null, args.concat(adds))

この partial は先にβ簡約を行い、正規形になるまで部分適用した関数を返す仕組みです。正確には s-m-n定理 と部分適用の組み合わせじゃないかとは思います。

また、一見 末尾再帰のように見えますが、関数かオブジェクトをすぐに返すのでコールスタックが深くなるような事はありません。


部分適用を利用したライブラリの実装例


cps.js

# 継続渡し Array forEach

cps_each = partial (iterator, callback, array) ->
limit = array.length
index = 0
count = 0
finished = no
while index < limit
next = (error) ->
count += 1
if not finished and (error or count >= limit)
finished = yes
callback error
iterator next, array[index], index, array
index += 1

# 継続渡し Array map
cps_map = partial (iterator, callback/*, array */) ->
results = []
mapping = null
cps_each (next, value, index, iterable) ->
mapping = (error, x) ->
unless error
results[index] = x
next error
iterator mapping, value, index, iterable
, (error) -> callback error, results


partial 以下は普通の関数の書き方で宣言が書けます。この時、partial に渡す関数の仮引数の数は必要最小限で宣言するようにします。cps_map の例は「最終的に必要となる仮引数」を /* */ で伏せ、ソースの可読性を損なわないようにしています。


利用例


use_cps.js

# {number} を二乗して次の処理に渡す

# value が {number} でない場合、エラーオブジェクトを次の処理に渡す
cps_square = (next, value, index) ->
unless typeof value is "number"
next new TypeError "require number , but typeof value is {#{typeof value}: #{value}} at [#{index}]"
else
next null, x * x

# Array.<number> の各要素を二乗して次の処理に渡す
cps_square_map = cps_map cps_square/*, callback, array*/
# callback, array 共に現時点では指定されていない
# cps_square_map の後処理となる callback 、
# 具体的にイテレータを回したい array は
# 後から決める事ができる

cps_log = (error, result) ->
console.log if error then "!!!ERROR!!! #{error}" else ":::SUCCESS::: #{result}"

square_map = cps_square_map cps_log

square_map [1, 2, 3] # 正常系
#=> :::SUCCESS::: 1, 4, 9

square_map [10, "dummy", 30] # 異常系
#=> !!!ERROR!!! TypeError: require number, but type of value is {string: dummy} at [1]

# 必要な引数を一気に渡しても当然よい。
cps_map cps_square, cps_log, [1, 2, 3]
#=> :::SUCCESS::: 1, 4, 9


以上のように、汎用性と応用に富む強力なユーティリティーを提供することができます。

CoffeeScript より後発で、より関数指向プログラミングを目的とした LiveScript(x, y, z ...) --> という書式で、カリー化された関数にコンパイルしますが、冒頭のような「引数を余らせたら更に適用できるか試す」ような実装にはなっていません。あと、あれもカリー化と部分適用の誤用な気がする…。

また、この実装には オプショナルな可変長引数に対して弱い面がありますが、それは別のエントリーで解説します。