Search code examples
coffeescriptlist-comprehensionfor-comprehension

Is it possible to collect results from callbacks through coffeescript comprehensions?


Suppose I have array of objects, each have some asynchronous readValue function, which accepts callback as its parameter, which will be fired when value is available for this object.

My goal is to calculate all values of each object and return array with those values.

First that came into my head is this

calculateValues = (sources, callback) ->
    counter = 0
    length = sources.length
    result = []

    for source in sources
        source.readValue (value) ->
            result.push value
            counter++
            callback result if counter is length

Since readValue method is asynchronous then function which calls it is also asynchronous. When all values will be in result array, callback function will be executed.

But all this seems messy to me. This is the area where coffeescript is really powerful. Is it possible to write this function even with less code with comprehensions? That's OK if it will be synchronous.


Solution

  • Unfortunately, CoffeeScript won't help you that much with these kinds of problems involving asynchronous code (apart from having a more terse function syntax, which is quite nice actually).

    Your code works, but with one small caveat: the order of the values in the results array does not match with the order of their sources in the sources. Here's a small working snippet that uses setTimeout and a random number to get the desired unpredictable asynchronous behaviour:

    calculateValues = (sources, callback) ->
      counter = 0
      length = sources.length
      result = []
    
      for source in sources
        source.readValue (value) ->
          result.push value
          counter++
          callback result if counter is length
    
    sources = for i in [1..5]
      do (i) ->
        readValue: (cb) -> 
          setTimeout (-> cb "Value #{i}"), Math.random() * 1000
    
    calculateValues sources, (results) -> 
      console.log "results", results
    

    jsFiddle

    Sample output:

    results ["Value 2", "Value 5", "Value 1", "Value 3", "Value 4"]
    

    If you wanted to preserve the ordering, it's a pretty simple fix, but this showcases how tricky it can be to get async code right.

    Fortunately, there are better ways to express this kind of construct with the help of other tools. I'd recommend using a simple library like Async.js to help with synchronizing things. Here's the same snippet rewritten to use async.parallel and Node's convention of using two-argument callbacks of the form function(err, value) (and also a bit of Underscore.js little help, which you can translate to native CS very easily if you don't want it):

    calculateValues = (sources, callback) ->
      funcs = _.pluck sources, 'readValue'
      async.parallel funcs, callback
    
    sources = for i in [1..5]
      do (i) ->
        readValue: (cb) -> 
          setTimeout (-> cb null, "Value #{i}"), Math.random() * 1000
    
    calculateValues sources, (err, results) -> 
      console.log "results", results
    

    jsFiddle

    Output:

    results ["Value 1", "Value 2", "Value 3", "Value 4", "Value 5"]