Module: DeferrableGratification::Combinators

Defined in:
lib/deferrable_gratification/combinators.rb,
lib/deferrable_gratification/combinators/bind.rb,
lib/deferrable_gratification/combinators/join.rb

Overview

Combinators for building up higher-level asynchronous abstractions by composing simpler asynchronous operations, without having to manually wire callbacks together and remember to propagate errors correctly.

Examples:

Perform a sequence of database queries and transform the result.

# With DG::Combinators:
def product_names_for_username(username)
  DB.query('SELECT id FROM users WHERE username = ?', username).bind! do |user_id|
    DB.query('SELECT name FROM products WHERE user_id = ?', user_id)
  end.transform do |product_names|
    product_names.join(', ')
  end
end

status = product_names_for_username('bob')

status.callback {|product_names| ... }
# If both queries complete successfully, the callback receives the string
# "Car, Spoon, Coffee".  The caller doesn't have to know that two separate
# queries were made, or that the query result needed transforming into the
# desired format: he just gets the event he cares about.

status.errback {|error| puts "Oh no!  #{error}" }
# If either query went wrong, the errback receives the error that occurred.

# Without DG::Combinators:
def product_names_for_username(username)
  product_names_status = EM::DefaultDeferrable.new
  query1_status = DB.query('SELECT id FROM users WHERE username = ?', username)
  query1_status.callback do |user_id|
    query2_status = DB.query('SELECT name FROM products WHERE user_id = ?', user_id)
    query2_status.callback do |product_names|
      product_names = product_names.join(', ')
      # N.B. don't forget to report success to the caller!
      product_names_status.succeed(product_names)
    end
    query2_status.errback do |error|
      # N.B. don't forget to tell the caller we failed!
      product_names_status.fail(error)
    end
  end
  query1_status.errback do |error|
    # N.B. don't forget to tell the caller we failed!
    product_names_status.fail(error)
  end
  # oh yes, and don't forget to return this!
  product_names_status
end

Defined Under Namespace

Modules: ClassMethods Classes: Bind, Join

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.included(base) ⇒ Object

Boilerplate hook to extend ClassMethods.



221
222
223
# File 'lib/deferrable_gratification/combinators.rb', line 221

def self.included(base)
  base.send :extend, ClassMethods
end

Instance Method Details

#>>(prok) ⇒ Deferrable

Alias for #bind!.

Note that this takes a Proc (e.g. a lambda) while #bind! takes a block.

Examples:

Perform a database query that depends on the result of a previous query.

DB.query('first query') >> lambda {|result| DB.query("query with #{result}") }

Parameters:

  • prok (Proc)

    proc to call with the successful result of self. Assumed to return a Deferrable representing the status of its own operation.

Returns:

  • (Deferrable)

    status of the compound operation of passing the result of self into the proc.



71
72
73
# File 'lib/deferrable_gratification/combinators.rb', line 71

def >>(prok)
  Bind.setup!(self, &prok)
end

#bind!(&block) ⇒ Deferrable

Register callbacks so that when this Deferrable succeeds, its result will be passed to the block, which is assumed to return another Deferrable representing the status of a second operation.

If this operation fails, the block will not be run. If either operation fails, the compound Deferrable returned will fire its errbacks, meaning callers don’t have to know about the inner operations and can just subscribe to the result of #bind!.

If you find yourself writing lots of nested #bind! calls, you can equivalently rewrite them as a chain and remove the nesting: e.g.

a.bind! do |x|
  b(x).bind! do |y|
    c(y).bind! do |z|
      d(z)
    end
  end
end

has the same behaviour as

a.bind! do |x|
  b(x)
end.bind! do |y|
  c(y)
end.bind! do |z|
  d(y)
end

As well as being more readable due to avoiding left margin inflation, this prevents introducing bugs due to inadvertent local variable capture by the nested blocks.

Examples:

Perform a web request based on the result of a database query.

DB.query('url').bind! {|url| HTTP.get(url) }.
  callback {|response| puts "Got response!" }

Parameters:

  • &block

    block to call with the successful result of self. Assumed to return a Deferrable representing the status of its own operation.

Returns:

  • (Deferrable)

    status of the compound operation of passing the result of self into the block.

See Also:



123
124
125
# File 'lib/deferrable_gratification/combinators.rb', line 123

def bind!(&block)
  Bind.setup!(self, &block)
end

#guard(reason = nil) {|*args| ... } ⇒ Object

If this Deferrable succeeds, ensure that the arguments passed to Deferrable#succeed meet certain criteria (specified by passing a predicate as a block). If they do, subsequently defined callbacks will fire as normal, receiving the same arguments; if they do not, this Deferrable will fail instead, calling its errbacks with a GuardFailed exception.

This follows the usual Deferrable semantics of calling Deferrable#fail inside a callback: any callbacks defined before the call to #guard will still execute as normal, but those defined after the call to #guard will only execute if the predicate returns truthy.

Multiple successive calls to #guard will work as expected: the predicates will be evaluated in order, stopping as soon as any of them returns falsy, and subsequent callbacks will fire only if all the predicates pass.

If instead of returning a boolean, the predicate raises an exception, the Deferrable will fail, but errbacks will receive the exception raised instead of GuardFailed. You could use this to indicate the reason for failure in a complex guard expression; however the same intent might be more clearly expressed by multiple guard expressions with appropriate reason messages.

Parameters:

  • reason (String) (defaults to: nil)

    optional description of the reason for the guard: specifying this will both serve as code documentation, and be included in the GuardFailed exception for error handling purposes.

Yield Parameters:

  • *args

    the arguments passed to callbacks if this Deferrable succeeds.

Yield Returns:

  • (Boolean)

    true if subsequent callbacks should fire (with the same arguments); false if instead errbacks should fire.

Raises:

  • (ArgumentError)

    if called without a predicate



205
206
207
208
209
210
211
212
213
214
215
216
217
# File 'lib/deferrable_gratification/combinators.rb', line 205

def guard(reason = nil, &block)
  raise ArgumentError, 'must be called with a block' unless block_given?
  callback do |*callback_args|
    begin
      unless block.call(*callback_args)
        raise ::DeferrableGratification::GuardFailed.new(reason, callback_args)
      end
    rescue => exception
      fail(exception)
    end
  end
  self
end

#transform(&block) ⇒ Deferrable

Transform the result of this Deferrable by invoking block, returning a Deferrable which succeeds with the transformed result.

If this operation fails, the operation will not be run, and the returned Deferrable will also fail.

Examples:

Retrieve a web page and call back with its title.

HTTP.request(url).transform {|page| Hpricot(page).at(:title).inner_html }

Parameters:

  • &block

    block that transforms the expected result of this operation in some way.

Returns:

  • (Deferrable)

    Deferrable that will succeed if this operation did, after transforming its result.



141
142
143
# File 'lib/deferrable_gratification/combinators.rb', line 141

def transform(&block)
  Bind.setup!(self, :without_chaining => true, &block)
end

#transform_error(&block) ⇒ Deferrable

Transform the value passed to the errback of this Deferrable by invoking block. If this operation succeeds, the returned Deferrable will succeed with the same value. If this operation fails, the returned Deferrable will fail with the transformed error value.

Parameters:

  • &block

    block that transforms the expected error value of this operation in some way.

Returns:

  • (Deferrable)

    Deferrable that will succeed if this operation did, otherwise fail after transforming the error value with which this operation failed.



156
157
158
159
160
161
162
163
164
165
# File 'lib/deferrable_gratification/combinators.rb', line 156

def transform_error(&block)
  errback do |*err|
    self.fail(
      begin
        yield(*err)
      rescue => e
        e
      end)
  end
end