Project

mr_darcy

0.01
Repository is archived
No commit activity in last 3 years
No release in over 3 years
MrDarcy takes async promises from the javascript world, DCI from Jim Gay's brain and sprinkles some ruby on top for great justice!
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Project Readme

MrDarcy

A mashup of Async Promises and DCI in ruby.

Build Status

Installation

Add this line to your application's Gemfile:

gem 'mr_darcy'

And then execute:

bundle

Or install it yourself as:

gem install mr_darcy

Documentation

API Documentation is available on rdoc.info

Usage

WARNING

MrDarcy is definitely experimental, and was mostly built over the weekend of RailsCamp NZ 5 with the generous help of the amazing and sexy @breccan and @eoinkelly.

API Changes (0.4.0 and above)

As of version 0.4.0 the instance_exec of promise blocks has been removed (meaning that you can't call resolve or reject from directly inside your promise blocks). This is becuase instance_exec changes the binding of self inside the promise, and will cause calls to enclosed methods to fail.

For example:

class InstanceExecExample

  def example_method
    puts "I did a thing"
  end

  def make_promise
    MrDarcy.promise { resolve example_method }.raise
  end
end

Will raise:

NameError in `block in make_promise': undefined local variable or method `example_method' for #<MrDarcy::Promise::DSL:0x007fcc542d75d0>

As of 0.4.0 the DSL object wll be passed to the promise block as an argument, therefore:

  def make_promise
    MrDarcy.promise { |promise| promise.resolve example_method }.raise
  end

will not raise an exception.

Should I use MrDarcy in Production?

No.

How can I help make MrDarcy production ready?

Run it in production. Report bugs.

Such promise. Many then.

Promises are a way of structuring batches of (async) functionality into a pipeline, in such a way as to make them seem synchronous.

Here's an example:

# We're going to wrap an asynchronous web request using EventMachine
# in a promise:
data = MrDarcy.promise do |promise|
  EM.run do
    http = EM.HttpRequest.new('http://camp.ruby.org.nz/').get
    http.errback do
      promise.reject http.error
      EM.stop
    end
    http.callback do
      promise.resolve http.response
      EM.stop
    end
  end
end.then do |response|
  response.body
end.result

puts data

What's cool about MrDarcy is that we can switch between different methods of doing async ruby:

  • Naive threads, using MRI's thread implementation.
  • Reactor pattern, using EventMachine to schedule promises on the a reactor thread.
  • Actor pattern, using Celluloid to schedule promises using Celluloid futures.

Key points to know about Promises

  1. You create them with a block, which is scheduled asynchronously, and inside of which you can place your long-running executable. Inside this block you call either resolve <value> or reject <exception> to resolve or reject the promise.

    MrDarcy.promise do |promise|
      accelerate_the_delorean
      if speed >= 88
        promise.resolve :time_flux_initiated
      else
        promise.reject :engage_service_brake
      end
    end
  2. All promises have then and fail methods, to which you pass a block to be called when the promise resolves (then) or rejects (fail). These methods return new promises, upon which you can chain more then and fail calls.

    MrDarcy.promise do |promise|
      i = rand
      i > 0.5 ? promise.resolve i : promise.reject i
    end.then |value|
      # success
    end.fail |value|
      # failure
    end
  3. fail is used to catch errors asynchronously, and deal with them. Therefore the result of a fail block will be a resolved promise. If you wish to keep processing a failure then you can raise it within the fail block to pass it along to the next fail block.

    MrDarcy.promise do |promise|
      promise.reject 2
    end.fail |value|
      value * value
    end.then |value|
      # I am called with 4
    end
  4. Failures cascade until they're caught:

    MrDarcy.promise do |promise|
      promise.reject :fail
    end.then
      # I am skipped
    end.then
      # as am I
    end.fail
      # I am called
    end
  5. If your block returns a new promise, then then or fail will defer their resolution until the new promise is resolved:

    MrDarcy.promise do |promise|
      promise.resolve 1
    end.then do |value|
      MrDarcy.promise do |p|
        p.resolve value * 2
      end
    end.then |value|
      # I will be called with 2
    end

Promise collections

If you have a bunch of promises, and you'd like to know when they're all complete then you can use the MrDarcy.all_promises method:

MrDarcy.all_promises do
  10.times.map { |i| MrDarcy.promise { |p| sleep 1; p.resolve i } }
end.then do |values|
  puts "All done."
end

In the above example, all 10 promises will execute simultaneously, the message All done. will be printed to standard out, after approximately one second.

It will also reject as soon as the first promise rejects.

Sprinkle on some DCI goodness.

DCI is a method of specifying interactions between objects in a single location, by decorating or extending your data objects within a context, running the interaction and then (optionally) removing the extensions again.

Other takes on DCI in Ruby:

  • playhouse is an app framework for building entire apps with DCI from the lovable hippies at enspiral,

  • surrounded by Jim Gay is a gem for doing DCI in a simple, repeatable fashion.

MrDarcy is a little differnt to these approaches, as is builds an interesting DSL on top of promises to create contexts that are alive as long as they need to be to achieve their goal, even when code is being run asynchronously.

Here's how we define a classic bank trasfer example:

class BankTransfer < MrDarcy::Context
  role :money_source do
    def has_available_funds? amount
      available_balance >= amount
    end

    def subtract_funds amount
      self.available_balance = available_balance - amount
    end
  end

  role :money_destination do
    def receive_funds amount
      self.available_balance = available_balance + amount
    end
  end

  action :transfer do |amount|
    if money_source.has_available_funds? amount
      money_source.subtract_funds amount
      money_destination.receive_funds amount
    else
      raise "insufficient funds"
    end
    amount
  end
end
  • The role class method defines roles, which the context will expect to be passed on object creation. They also define the extra behaviour (methods) that we wish to see added to our role players at initialisation.

  • The action class method defines our interactions. In other words, this is where we define how the interaction will take place. These also define instance methods on the class of the same name, which return a promise when called.

  • You can have as many roles and actions as needed by your context.

Let's transfer some funds between two accounts:

Account = Struct.new(:available_balance)
marty     = Account.new(10)
doc_brown = Account.new(15)

context = BankTransfer.new money_source: marty, money_destination: doc_brown
context.transfer(5).then do |amount|
  puts "Successfully transferred #{amount} from #{money_source} to #{money_destination}"
end

context.transfer(50).fail do |exception|
  puts "Failed to transfer funds: #{exception.message}"
end

What's super cool, however is that because promises can return and chain other promises, we can come up with some pretty involved scenarios:

marty     = Account.new(10)
jenn      = Account.new(10)
doc_brown = Account.new(200)

context = BankTransfer.new money_source: marty, money_destination: jenn
context.transfer(20).fail do
  # Oh no, Marty doesn't have enough money, let's borrow some from Doc.
  BankTransfer.new(money_source: doc_brown, money_destination: marty) \
    .transfer(20).then
      # Try transferring again.
      context.transfer(20)
    end
end

I hope that's enough to get you started. Yup, it's a bit crazy, but it might just work.

Contributing

  1. Fork it ( http://github.com/jamesotron/mr_darcy/fork )
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create new Pull Request