Permalink
Browse files

Added the promise class

  • Loading branch information...
mathieuravaux committed Apr 20, 2012
1 parent 50cd360 commit e91ff8d8dc2a898dbfccffcf014bbbd7492f4100
View
@@ -9,4 +9,8 @@ require "rake"
require "rspec"
require "rspec/core/rake_task"
+RSpec::Core::RakeTask.new('spec') do |spec|
+ spec.pattern = "spec/**/*_spec.rb"
+end
+
task :default => :spec
@@ -1,9 +1,5 @@
+require "redis-auto-batches/promise"
require "redis-auto-batches/version"
-module Redis
- module Auto
- module Batches
- # Your code goes here...
- end
- end
+module RedisAutoBatches
end
@@ -0,0 +1,141 @@
+# TODO: contribute back to the promising-future gem
+# https://github.com/bhuga/promising-future
+
+##
+# A delayed-execution promise. Promises are only executed once.
+#
+# @example
+# x = promise { factorial 20 }
+# y = promise { fibonacci 10**6 }
+# a = x + 1 # => factorial 20 + 1 after factorial calculates
+# result = promise { a += y }
+# abort "" # whew, we never needed to calculate y
+#
+# @example
+# y = 5
+# x = promise { y = y + 5 }
+# x + 5 # => 15
+# x + 5 # => 15
+#
+
+module RedisAutoBatches
+ class Promise < BasicObject
+ NOT_SET = ::Object.new.freeze
+
+ instance_methods.each { |m| undef_method m unless m.to_s =~ /__/ }
+
+ ##
+ # Creates a new promise.
+ #
+ # @example Lazily evaluate a database call
+ # result = promise { @db.query("SELECT * FROM TABLE") }
+ #
+ # @yield [] The block to evaluate lazily.
+ # @see Kernel#promise
+ def initialize(&block)
+ if block.arity > 0
+ raise ArgumentError, "Cannot store a promise that requires an argument"
+ end
+ @block = block
+ @mutex = ::Mutex.new
+ @result = NOT_SET
+ @error = NOT_SET
+ end
+
+ ##
+ # Force the evaluation of this promise immediately
+ #
+ # @return [Object]
+ def __force__
+ # ::Kernel.puts "__force__ called !"
+ @mutex.synchronize do
+ if pending?
+ begin
+ fulfill @block.call
+ rescue ::Exception => error
+ fail error
+ end
+ end
+ end if pending?
+ # BasicObject won't send raise to Kernel
+ ::Kernel.raise(@error) if failed?
+ @result
+ end
+ alias_method :force, :__force__
+
+ def __fulfill__ result
+ @result = result
+ end
+ alias_method :fulfill, :__fulfill__
+
+ def __fail__ error
+ @error = error
+ end
+ alias_method :fail, :__fail__
+
+ def __fulfilled__?
+ !@result.equal?(NOT_SET)
+ end
+ alias_method :fulfilled?, :__fulfilled__?
+
+ def __failed__?
+ !@error.equal?(NOT_SET)
+ end
+ alias_method :failed?, :__failed__?
+
+ def __pending__?
+ !(__fulfilled__? || __failed__?)
+ end
+ alias_method :pending?, :__pending__?
+
+ ##
+ # Does this promise support the given method?
+ #
+ # @param [Symbol]
+ # @return [Boolean]
+ def respond_to?(method)
+ [ :fulfill, :fail, :force, :chain, :fulfilled?, :failed?, :pending?,
+ :__fulfill__, :__fail__, :__force__, :__chain__, :__fulfilled__?, :__failed__?, :__pending__?
+ ].include?(method) || begin
+ # ::Kernel.puts "__forcing__ due to respond_to?(#{method}) !"
+ __force__.respond_to?(method)
+ end
+ end
+
+ def __chain__ &block
+ parent = self
+ Promise.new {
+ value = parent.__force__
+ block.call(value)
+ }
+ end
+ alias_method :chain, :__chain__
+
+ def inspect
+ if @result.equal?(NOT_SET)
+ if @error.equal?(NOT_SET)
+ "<RedisAutoBatches::Promise:pending:#{@block}>"
+ else
+ "<RedisAutoBatches::Promise:error:#{@error}>"
+ end
+ else
+ "<RedisAutoBatches::Promise:fulfilled:#{@result}>"
+ end
+ end
+
+ def self.ratio(part, total)
+ self.new { if total.zero? then 0.0 else part.to_f / total.to_f end }
+ end
+
+ private
+
+ def method_missing(method, *args, &block)
+ # ::Kernel.puts "Forcing promise due to call of #{method.inspect} on it."
+ __force__.__send__(method, *args, &block)
+ end
+ end
+
+ def self.promise &block
+ Promise.new(&block)
+ end
+end
@@ -1,7 +1,3 @@
-module Redis
- module Auto
- module Batches
- VERSION = "0.0.1"
- end
- end
+module RedisAutoBatches
+ VERSION = "0.0.1"
end
@@ -13,10 +13,12 @@ Gem::Specification.new do |gem|
gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
gem.name = "redis-auto-batches"
gem.require_paths = ["lib"]
- gem.version = Redis::Auto::Batches::VERSION
+ gem.version = RedisAutoBatches::VERSION
gem.add_dependency("redis")
gem.add_development_dependency("rake")
gem.add_development_dependency("rspec")
+ gem.add_development_dependency("guard-rspec")
+ gem.add_development_dependency("json")
end
View
@@ -0,0 +1,107 @@
+require "spec_helper"
+
+describe RedisAutoBatches::Promise do
+
+ it "can be instantiated" do
+ RedisAutoBatches::Promise.new { }
+ end
+
+ it "can be instantiated as a method call, like the Integer class" do
+ RedisAutoBatches.promise {}
+ end
+
+ it "doesn't call the passed proc when created" do
+ RedisAutoBatches.promise { fail "I shouldn't have been evaluated " }
+ end
+
+ it "calls the passed proc when it's evaluation is forced" do
+ task = mock(:task).tap { |mock| mock.should_receive(:work) }
+ p = RedisAutoBatches.promise { task.work }
+ p.force
+ end
+
+ it "presents the wrapped value transparently" do
+ (RedisAutoBatches.promise { 5 } + 3).should == 8
+ (3 + RedisAutoBatches.promise { 5 }).should == 8
+ RedisAutoBatches.promise { 5 }.to_s.should == "5"
+ end
+
+ it "lets exceptions bubble up naturally" do
+ p = RedisAutoBatches.promise { 1/0 }
+ expect { p.force }.to raise_error(ZeroDivisionError)
+ end
+
+ describe "#inspect" do
+ context "with a new RedisAutoBatches::Promise" do
+ it "doesn't force it to evaluate" do
+ task = mock(:task).tap { |mock| mock.should_not_receive(:work) }
+ p = RedisAutoBatches.promise { task.work }
+ p.inspect
+ end
+
+ it "prints a helpful message" do
+ message = RedisAutoBatches.promise { 5 }.inspect
+ message.should include("<RedisAutoBatches::Promise:pending:#<Proc:")
+ message.should include("spec/promise_spec.rb:")
+
+ end
+ end
+
+ context "with a fulfilled promise" do
+ it "presents itself as a fulfilled promise, with the result" do
+ p = RedisAutoBatches.promise { 5 }
+ p.force
+ p.inspect.should == "<RedisAutoBatches::Promise:fulfilled:5>"
+ end
+ end
+
+ context "with a failed promise" do
+ it "presents itself as a failed promise, with the error" do
+ p = RedisAutoBatches.promise {1/0}
+ p.force rescue nil
+ p.inspect.should == "<RedisAutoBatches::Promise:error:divided by 0>"
+ end
+ end
+ end
+
+ describe "#chain" do
+ it "returns a promise" do
+ RedisAutoBatches.promise { 5 }.chain(&:to_s).inspect.should include("<RedisAutoBatches::Promise:pending")
+ end
+
+ it "doesn't force the evaluation" do
+ task = mock(:task).tap { |mock| mock.should_not_receive(:work) }
+ p = RedisAutoBatches.promise { task.work }
+ p.chain { |work_result| work_result.use }
+ end
+
+ it "applies both evaluations when the result of the second promise is forced" do
+ task = mock(:task).tap { |mock| mock.should_receive(:work) }
+ p = RedisAutoBatches.promise { task.work }
+ q = p.chain { }
+ q.force
+ end
+ end
+
+ describe "Promise.ratio" do
+ let(:numerator) { RedisAutoBatches.promise { 2 } }
+ let(:denominator) { RedisAutoBatches.promise { 10 } }
+
+ it "evaluates lazily both operands" do
+ numerator, denominator = 2.times.map { RedisAutoBatches.promise { fail "U Can't Touch this !"} }
+ RedisAutoBatches::Promise.ratio(numerator, denominator)
+ end
+
+ it "evaluates both operands when evaluated itself" do
+ ratio = RedisAutoBatches::Promise.ratio(numerator, denominator)
+ ratio.should == 0.2
+ end
+
+ it "serializes as JSON as the result would" do
+ require "json"
+ ratio = RedisAutoBatches::Promise.ratio(numerator, denominator)
+ {:accuracy => ratio}.to_json.should == %Q({"accuracy":0.2})
+ end
+
+ end
+end
View
@@ -1,3 +1,5 @@
+require "redis-auto-batches"
+
RSpec.configure do |config|
config.treat_symbols_as_metadata_keys_with_true_values = true
config.run_all_when_everything_filtered = true

0 comments on commit e91ff8d

Please sign in to comment.