MrDarcy
A mashup of Async Promises and DCI in ruby.
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
-
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>
orreject <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
-
All promises have
then
andfail
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 morethen
andfail
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
-
fail
is used to catch errors asynchronously, and deal with them. Therefore the result of afail
block will be a resolved promise. If you wish to keep processing a failure then you canraise
it within thefail
block to pass it along to the nextfail
block.MrDarcy.promise do |promise| promise.reject 2 end.fail |value| value * value end.then |value| # I am called with 4 end
-
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
-
If your block returns a new promise, then
then
orfail
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
- Fork it ( http://github.com/jamesotron/mr_darcy/fork )
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create new Pull Request