Node.js の非同期処理を関数合成で繋げてみる - 連続コールバック対策

Node.js でプログラミングする際の課題は、非同期処理のコールバックが多段になって分かり難くなってしまう点だと思います。(下記は CoffeeScript の例)

func1 引数・・・, (err, 処理結果1) ->
    if err?
        エラー処理
    else
        ・・・
        func2 引数・・・, (err, 処理結果2) ->
            if err?
                エラー処理
            else
                ・・・
                func3 引数・・・, (err, 処理結果3) ->
                    if err?
                        エラー処理
                    else
                        ・・・

対策はいろいろあると思いますが(id:fits:20120415 で使った TameJS もその一つ)、今回は id:fits:20101213 で扱ったような関数合成を適用する方法を模索してみました。

サンプルソースは http://github.com/fits/try_samples/tree/master/blog/20120617/

コールバックを繋げて関数合成

なんとなく、コールバック結果を繋げて関数合成すれば使い道ありそうな気がしました。

つまり、"関数1" のコールバック結果を引数にして "関数2" を呼び出し、そのコールバック結果を引数にして "関数3" を呼び出し・・・というような処理を関数合成で作り出せればと。

そこで、試しに CoffeeScript を使って関数合成する処理を実装してみました。

compose.coffee
# 関数をコールバックで繋げて処理する無名関数を返す
exports.compose = (funcs...) ->
    (args, callback) ->
        cb = genCallback callback

        if funcs?.length > 0
            for i in [(funcs.length - 1)..0]
                cb = genCallback cb, funcs[i]

        cb null, args

genCallback = (callback, func) ->
    (err, res) ->
        if err?
            callback err
        else if func?
            func res, callback
        else
            callback null, res

compose 関数は、渡された関数を入れ子で繋げて処理する無名関数(args と callback が引数)を返します。
ただし、compose に渡す関数の引数も (args, callback) で統一しておく必要があります。


処理としては、compose f1, f2 で以下のような無名関数が作られるイメージです。(cb の部分は関数実行時に毎回構築される点に注意)

compose f1, f2 で作成される無名関数のイメージ
(args, callback) ->
    cb = (err, res0) ->
        if err?
            callback err
        else
            f1 res0, (err, res1) ->
                if err?
                    callback err
                else
                    f2 res1, (err, res2) ->
                        if err?
                            callback err
                        else
                            callback null, res2

    cb null, args

動作確認

動作確認のため、上記で作成した compose 関数を使ったサンプルを作成・実行してみます。

sample.coffee
c = require './compose'

# 関数1
f1 = (args, cb) ->
    console.log "f1 : #{args}"
    cb null, [args, "aaa"]

# 関数2
f2 = (args, cb) ->
    console.log "f2 : #{args}"
    cb null, 10

# 関数3
f3 = (args, cb) ->
    console.log "f3 : #{args}"
    if args is 10
        cb null, "done"
    else
        cb 'error3'

# 関数4
f4 = (args, cb) ->
    console.log "f4 : #{args}"
    cb 'error4'

# 関数1, 2, 3 を合成
cf1 = c.compose f1, f2, f3
# 関数1, 2, 3 の合成関数を実行
cf1 'test', (err, res) ->
    console.log "result1 : #{err}, #{res}"

console.log '-----'

cf2 = c.compose f1, f4, f2, f3
cf2 'test', (err, res) ->
    console.log "result2 : #{err}, #{res}"

console.log '-----'

cf3 = c.compose f2, f1, f3
cf3 'test', (err, res) ->
    console.log "result3 : #{err}, #{res}"

console.log '-----'

cf4 = c.compose f4, f2
cf4 'test', (err, res) ->
    console.log "result4 : #{err}, #{res}"

console.log '-----'

cf5 = c.compose f2, f1
cf5 'test', (err, res) ->
    console.log "result5 : #{err}, #{res}"

console.log '-----'

cf6 = c.compose()
cf6 'test', (err, res) ->
    console.log "result6 : #{err}, #{res}"

console.log '-----'

cf7 = c.compose f1
cf7 'test', (err, res) ->
    console.log "result7 : #{err}, #{res}"

console.log '-----'

# 合成関数 cf1, cf5, cf7 を合成
cf8 = c.compose cf1, cf5, cf7
cf8 'test8', (err, res) ->
    console.log "result8 : #{err}, #{res}"

実行結果は以下の通り。一応、動作しているようです。

実行結果
> coffee sample

f1 : test
f2 : test,aaa
f3 : 10
result1 : null, done
-----
f1 : test
f4 : test,aaa
result2 : error4, undefined
-----
f2 : test
f1 : 10
f3 : 10,aaa
result3 : error3, undefined
-----
f4 : test
result4 : error4, undefined
-----
f2 : test
f1 : 10
result5 : null, 10,aaa
-----
result6 : null, test
-----
f1 : test
result7 : null, test,aaa
-----
f1 : test8
f2 : test8,aaa
f3 : 10
f2 : done
f1 : 10
f1 : 10,aaa
result8 : null, 10,aaa,aaa