diff --git a/README b/README
new file mode 100644
index 0000000..08c27a4
--- /dev/null
+++ b/README
@@ -0,0 +1,81 @@
+Functor provides pattern-based function and method dispatch for Ruby, originally inspired by Topher Cyll's multi gem.
+
+= Method Functors
+
+To use it in a class:
+
+ class Repeater
+ attr_accessor :times
+ include Functor::Method
+ functor( :repeat, Integer ) { |x| x * @times }
+ functor( :repeat, String ) { |s| [].fill( s, 0, @times ).join(' ') }
+ end
+
+ r = Repeater.new
+ r.times = 5
+ r.repeat( 5 ) # => 25
+ r.repeat( "-" ) # => "- - - - -"
+ r.repeat( 7.3 ) # => Functor::NoMatch!
+
+= Stand-Alone Functors
+
+You can also define Functor objects directly:
+
+ fib ||= Functor.new do |f|
+ f.given( Integer ) { | n | f.call( n - 1 ) + f.call( n - 2 ) }
+ f.given( 0 ) { |x| 0 }
+ f.given( 1 ) { |x| 1 }
+ end
+
+You can use functors directly with functions taking a block like this:
+
+ [ *0..10 ].map( &fib ) # => [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
+
+You can call a functor as a method using #call:
+
+ fun.call( obj, 7 )
+
+= Pattern Matching
+
+Arguments are matched first using #===, so anything that supports these methods can be matched against. In addition, you may pass as a "guard" any object that responds to #call and which takes an object (the argument) and return true or false. This allows you to do things like this:
+
+ stripe ||= Functor.new do
+ given( lambda { |x| x % 2 == 0 } ) { |x| 'white' }
+ given( lambda { |x| x % 2 == 1 } ) { |x| 'silver' }
+ end
+
+ stripe.call( 3 ) # => 'silver'
+ stripe.call( 4 ) # => 'white'
+
+= Precedence
+
+Precedence works similarly to Ruby method definition, i.e. Last In, First Out. Thus, you need to be careful in how you define your functor. The Fibonacci example above would not work properly if the Integer pattern was given last.
+
+= Caching Options
+
+Methods defined with functors are substantially slower than methods defined natively with "def". To (partially) alleviate the performance hit, Functor keeps track of which functor block matches each particular *args set, allowing it to short circuit the matching process when an *args set is seen again. If the *args cache were unlimited, this would represent a serious memory leak. Naturally, we have not implemented an unlimited cache.
+
+Rather, the *args cache is subjected to flexible and scalable limits that also have the effect of promoting frequently encountered *args sets and allowing those less frequently encountered to languish, yea even unto death. The caching system uses two configuration parameters, which may be set for Functor or for any class which has included Functor::Method. When a class does not set these parameters, it will inherit the options set at the Functor level. Functor comes with (what we think are) sensible defaults.
+
+The :size parameter controls, albeit indirectly, the number of items the caching system may store. If your :size is greater than the number of *args sets the functor will encounter, you do not need to worry (or even know) about the other parameter. You can set the :size param thusly:
+
+ Functor.cache_config :size => 10_000
+
+ class A
+ include Functor::Method
+ functor_cache_config :size => 700
+ end
+
+The other parameter, :base, determines the thresholds for promotion, i.e. how many times an *args set must be seen before it "becomes more important". While the use of the :size parameter is somewhat straightforward (albeit indirect), you need some idea of how the caching system works to understand the :base param. Functor classes maintain cached *args sets in tiers. When attempting to short-circuit the matching process, Functor checks each of these caches, starting at the top and working downward. Each tier has a promotion threshold, which represents the number of hits an item must receive before it can jump to the next tier. The promotion thresholds are determined using exponents of the :base parameter. The threshold for exiting the lowest tier is (base ** 1); for the next tier, it is (base ** 2); for the next, (base ** 3).
+
+Assume the following configuration:
+
+ Functor.cache_config :base => 10
+
+Functor currently uses 4 tiers. Let us call them c0, c1, c2, and c3. To pass from c0 to c1, an item must receive 10 (i.e. 10**1) hits. To pass from c1 to c2, it must receive 100 (10**2) hits. For promotion to c3, it must receive 1,000 hits. A lower :base setting results in lower thresholds across all tiers. The thresholds for a base of 8 would be 8, 64, and 512. Again, if your cache :size is large enough, the promotion thresholds are irrelevant. If, however, the number of distinct *args sets you expect to encounter is larger than your desired cache size, the :base parameter allows you to tune the promotion thresholds for a better fit with the frequency distribution of the *args sets. A high :base setting means that only very high-frequency items will make it into the top cache tier.
+
+Tier size limits are enforced in a useful way. Each tier has a size limit. When that size is reached, all of its items are dropped into the next lower tier, which drops all of its items into the next lower tier, etc. Tier size limits increase from top to bottom, so that when c3 dumps its items into c2, ample space remains in c2. After a tier cascade, all of the items in a particular tier have a hit count higher than the promotion threshold, guaranteeing that the next hit will promote the item. The effect of this is to allow "languishing" items at a certain level to eventually drop out the bottom, while preserving the valuable, active, high-frequency items.
+
+= Credits And Support
+
+Functor was written by Dan Yoder, Matthew King, and Lawrence Pit. Send email to dan at zeraweb.com for support or questions.
\ No newline at end of file
diff --git a/doc/README b/doc/README
deleted file mode 100644
index e5e5ef9..0000000
--- a/doc/README
+++ /dev/null
@@ -1,71 +0,0 @@
-Functor provides pattern-based function and method dispatch for Ruby, originally inspired by Topher Cyll's multi gem.
-
-= Method Functors
-
-To use it in a class:
-
- class Repeater
- attr_accessor :times
- include Functor::Method
- functor( :repeat, Integer ) { |x| x * @times }
- functor( :repeat, String ) { |s| [].fill( s, 0..@times ).join(' ') }
- end
-
- r = Repeater.new
- r.times = 5
- r.repeat( 5 ) # => 25
- r.repeat( "-" ) # => "- - - - -"
- r.repeat( 7.3 ) # => ArgumentError!
-
-Warning: This defines a class instance variable @__functors behind the scenes as a side-effect. Also, although inheritance works within a functor method, super does not. To call the parent method, you need to call it explicitly using the #functors class method, like this:
-
- A.functors[ :foo ].apply( self, 'bar' )
-
-= Stand-Alone Functors
-
-You can also define Functor objects directly:
-
- fib = Functor.new do
- given( 0 ) { 0 }
- given( 1 ) { 1 }
- given( Integer ) { |n| self.call( n - 1 ) + self.call( n - 2 ) }
- end
-
-You can use functors directly with functions taking a block like this:
-
- [ *0..10 ].map( &fib ) # => [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
-
-You can call a functor as a method using #apply:
-
- fun.apply( obj, 7 )
-
-which is actually how the method functors are implemented.
-
-= Pattern Matching
-
-Arguments are matched first using === and then ==, so anything that supports these methods can be matched against. In addition, you may pass "guards," any object that responds to #call and which take and object (the argument) and return true or false. This allows you to do things like this:
-
- stripe ||= Functor.new do
- given( lambda { |x| x % 2 == 0 } ) { 'white' }
- given( lambda { |x| x % 2 == 1 } ) { 'silver' }
- end
-
-which will return "white" and "silver" alternately for a sequence of numbers.
-
-= Precedence
-
-Precedence is defined in order of declaration: first-come, first-serve, aka FIFO. Thus, you need to be careful in how you define your functor. The Fibonacci example above would not work properly if the Integer pattern was given first. That said, it is possible to redefine earlier cases, which, in effect, "demotes" it, as if it had not been declared before. So the following will work properly:
-
- fib = Functor.new do
- given( Integer ) { |n| raise "this would start an infinite loop ..." }
- given( 0 ) { 0 }
- given( 1 ) { 1 }
- # but this will "demote" the Integer pattern and now it will work ...
- given( Integer ) { |n| self.call( n - 1 ) + self.call( n - 2 ) }
- end
-
-This isn't perfect, but it is very easy to predict, simple to implement, and reasonably fast, which other approaches (such as implementing a precedence scheme) are not.
-
-= Credits And Support
-
-Functor was written by Dan Yoder, Matthew King, and Lawrence Pit. Send email to dan at zeraweb.com for support or questions.
\ No newline at end of file
diff --git a/lib/functor.rb b/lib/functor.rb
index 893178b..f770c4b 100644
--- a/lib/functor.rb
+++ b/lib/functor.rb
@@ -1,72 +1,105 @@
require "#{File.dirname(__FILE__)}/object"
+require 'rubygems'
class Functor
+ class NoMatch < ArgumentError; end
+
+ def self.cache_config(options={})
+ (@cache_config ||= { :size => 4_096, :base => 8 }).merge!(options)
+ end
+
module Method
- def self.copy_functors( functors )
- r = {} ; functors.each do | name, functor |
- r[ name ] = functor.clone
- end
- return r
- end
+
def self.included( k )
- def k.functors
- @__functors ||= superclass.respond_to?( :functors ) ?
- Functor::Method.copy_functors( superclass.functors ) : {}
+
+ def k.functor_cache
+ @functor_cache ||= Hash.new { |hash, key| hash[key] = [ {},{},{},{} ] }
+ end
+
+ def k.functor_cache_config(options={})
+ @functor_cache_config = ( @functor_cache_config || Functor.cache_config ).merge(options)
+ end
+
+ def k.functor( name, *pattern, &action )
+ _functor( name, false, *pattern, &action)
end
- def k.functor( name, *args, &block )
- name = name.to_sym
- ( f = ( functors[ name ] or
- ( functors[ name ] = Functor.new ) ) ).given( *args, &block )
- define_method( name ) { | *args | instance_exec( *args, &f.match( *args ) ) }
+
+ def k.functor_with_self( name, *pattern, &action )
+ _functor( name, true, *pattern, &action)
end
- def k.functor_with_self( name, *args, &block )
- name = name.to_sym
- ( f = ( functors[ name ] or
- ( functors[ name ] = Functor.new ) ) ).given( *args, &block )
- define_method( name ) { | *args | instance_exec( *args, &f.match( self, *args ) ) }
+
+ # undefined methods beginning with '_' can be used as wildcards in Functor patterns
+ def k.method_missing(name, *args)
+ args.empty? && name.to_s =~ /^_/ ? lambda { |args| true } : super
end
+
+ private
+
+ def k._functor( name, with_self=false, *pattern, &action)
+ name = name.to_s
+ mc = functor_cache[name] # grab the cache tiers for The Method
+ cache_size, cache_base = functor_cache_config[:size], functor_cache_config[:base]
+ c0_size, c1_size, c2_size, c3_size = cache_size * 4, cache_size * 3, cache_size * 2, cache_size
+ c1_thresh,c2_thresh,c3_thresh = cache_base.to_i, (cache_base ** 2).to_i, (cache_base ** 3).to_i
+ old_method = instance_method(name) if method_defined?( name ) # grab The Method's current incarnation
+ define_method( name, action ) # redefine The Method
+ newest = instance_method(name) # grab newly redefined The Method
+
+ # Recursively redefine The Method using the newest and previous incarnations
+ define_method( name ) do | *args |
+ match_args = with_self ? [self] + args : args
+ sig = match_args.hash
+ if meth = mc[3][sig] # check caches from top down
+ meth[0].bind(self).call(*args)
+ elsif meth = mc[2][sig]
+ meth[1] += 1 # increment hit count
+ mc[3][sig] = mc[2].delete(sig) if meth[1] > c3_thresh # promote sig if it has enough hits
+ (mc[0], mc[1], mc[2], mc[3] = mc[1], mc[2], mc[3], {}) if mc[3].size >= c3_size # cascade if c3 is full
+ meth[0].bind(self).call(*args)
+ elsif meth = mc[1][sig]
+ meth[1] += 1
+ mc[2][sig] = mc[1].delete(sig) if meth[1] > c2_thresh
+ mc[0], mc[1], mc[2] = mc[1], mc[2], {} if mc[2].size >= c2_size
+ meth[0].bind(self).call(*args)
+ elsif meth = mc[0][sig]
+ meth[1] += 1
+ mc[1][sig] = mc[0].delete(sig) if meth[1] > c1_thresh
+ mc[0], mc[1] = mc[1], {} if mc[1].size >= c1_size
+ meth[0].bind(self).call(*args)
+ elsif Functor.match?(match_args, pattern) # not cached? Try newest meth/pat.
+ (mc[0], mc[1], mc[2], mc[3] = mc[1], mc[2], mc[3], {}) if mc[3].size >= c3_size
+ mc[3][sig] = [newest, 0] # methods are cached as [ method, counter ]
+ newest.bind(self).call(*args)
+ elsif old_method # or call the previous incarnation of The Method
+ old_method.bind(self).call(*args)
+ else # and if there are no older incarnations, whine about it
+ raise NoMatch.new( "No functor matches the given arguments for method :#{name}." )
+ end
+ end
+ end
+
end
end
-
def initialize( &block )
- @rules = [] ; yield( self ) if block_given?
- end
-
- def initialize_copy( from )
- @rules = from.instance_eval { @rules.clone }
+ class << self; include Functor::Method; end
+ yield( self ) if block_given?
end
def given( *pattern, &action )
- @rules << [ pattern, action ]
+ (class << self; self; end)._functor( "call", false, *pattern, &action)
end
- def call( *args, &block )
- match( *args, &block ).call( *args )
- end
-
- def []( *args, &block )
- call( *args, &block )
- end
+ def []( *args, &block ); call( *args, &block ); end
- def to_proc ; lambda { |*args| self.call( *args ) } ; end
+ def to_proc ; lambda { |*args| call( *args ) } ; end
- def match( *args, &block )
- args << block if block_given?
- pattern, action = @rules.reverse.find { | p, a | match?( args, p ) }
- action or
- raise ArgumentError.new( "Argument error: no functor matches the given arguments." )
- end
-
- private
-
- def match?( args, pattern )
- args.zip( pattern ).all? { | arg, rule | pair?( arg, rule ) } if args.length == pattern.length
- end
-
- def pair?( arg, rule )
- ( rule.respond_to? :call and rule.call( arg ) ) or rule === arg
+ def self.match?( args, pattern )
+ args.all? do |arg|
+ pat = pattern[args.index(arg)]
+ pat === arg || ( pat.respond_to?(:call) && pat.call(arg))
+ end if args.length == pattern.length
end
end
\ No newline at end of file
diff --git a/metrics/.gitignore b/metrics/.gitignore
new file mode 100644
index 0000000..19c8276
--- /dev/null
+++ b/metrics/.gitignore
@@ -0,0 +1 @@
+stevedore
diff --git a/metrics/benchmark.rb b/metrics/benchmark.rb
new file mode 100644
index 0000000..2660b0b
--- /dev/null
+++ b/metrics/benchmark.rb
@@ -0,0 +1,39 @@
+require 'rubygems'
+
+$:.unshift "#{here = File.dirname(__FILE__)}/stevedore/lib"
+$:.unshift "#{here}/../lib"
+require 'stevedore'
+require 'functor'
+
+
+class FuncFib < Steve
+
+ subject "Fibonacci using Functor"
+
+ # power 0.8
+ # sig_level 0.05
+ delta 0.01
+
+ before do
+ @fib ||= Functor.new do |f|
+ f.given( Integer ) { | n | f.call( n - 1 ) + f.call( n - 2 ) }
+ f.given( 0 ) { 0 }
+ f.given( 1 ) { 1 }
+ end
+ end
+end
+
+fib_8 = FuncFib.new "f(8)" do
+ measure do
+ 64.times { @fib.call(8) }
+ end
+end
+
+fib_16 = FuncFib.new "f(16)" do
+ measure do
+ 2.times { @fib.call(16) }
+ end
+end
+
+# FuncFib.recommend_test_size( 8, 16)
+FuncFib.compare_instances( 8, 128)
\ No newline at end of file
diff --git a/metrics/fib.rb b/metrics/fib.rb
new file mode 100644
index 0000000..398a2c0
--- /dev/null
+++ b/metrics/fib.rb
@@ -0,0 +1,33 @@
+require "#{here = File.dirname(__FILE__)}/helpers"
+
+class FuncFib < Steve
+
+ subject "Fibonacci using Functor"
+
+ # power 0.8
+ # sig_level 0.05
+ delta 0.01
+
+ before do
+ @fib ||= Functor.new do |f|
+ f.given( Integer ) { | n | f.call( n - 1 ) + f.call( n - 2 ) }
+ f.given( 0 ) { 0 }
+ f.given( 1 ) { 1 }
+ end
+ end
+end
+
+fib_8 = FuncFib.new "f(8)" do
+ measure do
+ 64.times { @fib.call(8) }
+ end
+end
+
+fib_16 = FuncFib.new "f(16)" do
+ measure do
+ 2.times { @fib.call(16) }
+ end
+end
+
+# FuncFib.recommend_test_size( 8, 16)
+FuncFib.compare_instances( 8, 128)
\ No newline at end of file
diff --git a/metrics/helpers.rb b/metrics/helpers.rb
new file mode 100644
index 0000000..fd8c4bc
--- /dev/null
+++ b/metrics/helpers.rb
@@ -0,0 +1,6 @@
+require 'rubygems'
+
+$:.unshift "#{here = File.dirname(__FILE__)}/stevedore/lib"
+$:.unshift "#{here}/../lib"
+require 'stevedore'
+require 'functor'
\ No newline at end of file
diff --git a/metrics/many_args.rb b/metrics/many_args.rb
new file mode 100644
index 0000000..32a70e3
--- /dev/null
+++ b/metrics/many_args.rb
@@ -0,0 +1,71 @@
+require "#{here = File.dirname(__FILE__)}/helpers"
+
+class A
+ include Functor::Method
+ functor( :foo, 1, 2, 3, 4, 5, 6, 7 ) { |*x| "ints" }
+ functor( :foo, :a, :b, :c, :d, :e, :f, :g ) { |*x| "symbols" }
+ functor( :foo, *%w{ a b c d e f g } ) { |*x| "strings" }
+ functor( :foo, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0 ) { |*x| "floats" }
+ functor( :foo, *Array.new(7, "one") ) { |*x| "ones" }
+end
+
+class Native
+ def foo(*args)
+ case args
+ when [1, 2, 3, 4, 5, 6, 7]
+ "ints"
+ when [:a, :b, :c, :d, :e, :f, :g]
+ "symbols"
+ when %w{ a b c d e f g }
+ "strings"
+ when [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0]
+ "floats"
+ when Array.new(7, "one")
+ "ones"
+ else
+ raise ArgumentError
+ end
+ end
+end
+
+class ManyArgs < Steve
+end
+
+ManyArgs.new "native method" do
+ before do
+ @args = [
+ [1, 2, 3, 4, 5, 6, 7],
+ [:a, :b, :c, :d, :e, :f, :g],
+ %w{ a b c d e f g },
+ [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0],
+ Array.new(7, "one")
+ ]
+ @n = Native.new
+ end
+ measure do
+ 400.times do
+ @args.each { |args| @n.foo *args }
+ end
+ end
+end
+
+
+ManyArgs.new "functor method" do
+ before do
+ @args = [
+ [1, 2, 3, 4, 5, 6, 7],
+ [:a, :b, :c, :d, :e, :f, :g],
+ %w{ a b c d e f g },
+ [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0],
+ Array.new(7, "one")
+ ]
+ @a = A.new
+ end
+ measure do
+ 400.times do
+ @args.each { |args| @a.foo *args }
+ end
+ end
+end
+
+ManyArgs.compare_instances( 4, 64)
\ No newline at end of file
diff --git a/metrics/one_arg.rb b/metrics/one_arg.rb
new file mode 100644
index 0000000..9275406
--- /dev/null
+++ b/metrics/one_arg.rb
@@ -0,0 +1,61 @@
+require "#{here = File.dirname(__FILE__)}/helpers"
+
+class A
+ include Functor::Method
+ functor_cache_config :size => 700, :base => 6
+
+ functor( :foo, Integer ) { |x| :integer }
+ functor( :foo, String ) { |x| :string }
+ functor( :foo, Float ) { |x| :float }
+ functor( :foo, Symbol ) { |x| :symbol }
+ functor( :foo, "one" ) { |x| :one }
+end
+
+class Native
+ def foo(x)
+ case x
+ when Integer then :integer
+ when String then :string
+ when Float then :float
+ when Symbol then :symbol
+ when "one" then :one
+ else
+ raise ArgumentError
+ end
+ end
+end
+
+class OneArg < Steve
+ before do
+ nums = (1..200).to_a
+ alphas = ("a".."gr").to_a
+ @args_set = nums + nums.map { |i| i.to_f } + alphas + alphas.map { |i| i.to_sym } + Array.new(200, "one")
+ @args = []
+ srand(46)
+ 9000.times { @args << @args_set[rand(@args_set.size)] }
+ end
+end
+
+OneArg.new "functor method" do
+ before_sample do
+ @a = A.new
+ end
+ measure do
+ @args.each { |item| @a.foo item }
+ end
+
+ after_sample do
+ puts A.functor_cache["foo"].map{ |c| c.size }.inspect
+ end
+end
+
+OneArg.new "native method" do
+ before_sample do
+ @n = Native.new
+ end
+ measure do
+ @args.each { |item| @n.foo item }
+ end
+end
+
+OneArg.compare_instances( 4, 96)
\ No newline at end of file
diff --git a/metrics/two_arg.rb b/metrics/two_arg.rb
new file mode 100644
index 0000000..d33cf2d
--- /dev/null
+++ b/metrics/two_arg.rb
@@ -0,0 +1,39 @@
+require "#{here = File.dirname(__FILE__)}/helpers"
+
+class A
+ include Functor::Method
+ functor( :foo, Integer, String ) { |x| :int_string }
+ functor( :foo, Integer, Float ) { |x| :int_float }
+ functor( :foo, String, Float ) { |x| :string_float }
+ functor( :foo, String, Symbol ) { |x| :string_symbol }
+ functor( :foo, Float, Symbol ) { |x| :float_symbol }
+ functor( :foo, Float, String ) { |x| :float_string }
+ functor( :foo, Symbol, "one" ) { |x| :symbol_one }
+ functor( :foo, "one", false ) { |x| :one_false }
+end
+
+class B
+ def foo(x,y)
+ case x
+ when Integer
+ case y
+ when String
+ :int_string
+ when Float
+ :int_float
+ end
+ when String
+ case y
+ when Float
+ :string_float
+ when Symbol
+ :string_symbol
+ end
+ when Float
+ when Symbol
+ when "one"
+ else
+ raise ArgumentError
+ end
+ end
+end
\ No newline at end of file
diff --git a/test.rb b/test.rb
deleted file mode 100644
index 9c2630b..0000000
--- a/test.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-load 'lib/functor.rb'
-class A
- include Functor::Method
- functor( :foo, Integer ) { |x| [ A, Integer ] }
- functor( :foo, String ) { |s| [ A, String ] }
- functor( :foo, Float ) { |h| [ A, Float ] }
-end
-
-class B < A
- functor( :foo, String ) { |s| [ B, String ] }
- functor( :foo, Float ) { |f| [ B, *A.functors[:foo].apply( self, f ) ] }
-end
-
-a = A.new ; b = B.new
-puts a.foo( 7 ).inspect
-puts b.foo( 'tentacles' ).inspect
-
-fib ||= Functor.new do
- given( 0 ) { 0 }
- given( 1 ) { 1 }
- given( Integer ) { | n | self.call( n - 1 ) + self.call( n - 2 ) }
-end
-
-puts fib[ 7 ]
\ No newline at end of file
diff --git a/test/fib.rb b/test/fib.rb
index 696c662..f3d7824 100644
--- a/test/fib.rb
+++ b/test/fib.rb
@@ -2,8 +2,8 @@
fib ||= Functor.new do |f|
f.given( Integer ) { | n | f.call( n - 1 ) + f.call( n - 2 ) }
- f.given( 0 ) { 0 }
- f.given( 1 ) { 1 }
+ f.given( 0 ) { |x| 0 }
+ f.given( 1 ) { |x| 1 }
end
describe "Dispatch on a functor object should" do
diff --git a/test/functor.rb b/test/functor.rb
index a5d78a2..47a78e7 100644
--- a/test/functor.rb
+++ b/test/functor.rb
@@ -6,23 +6,25 @@ class Repeater
functor( :repeat, Integer ) { |x| x * @times }
functor( :repeat, String ) { |s| [].fill( s, 0, @times ).join(' ') }
functor( :repeat ) { nil }
+ functor( :distraction, Integer ) { |x| "Boo!" }
end
describe "Dispatch on instance method should" do
before do
@r = Repeater.new
- @r.times = 5
+ @r.times = 3
end
specify "invoke different methods with object scope based on arguments" do
- @r.repeat( 5 ).should == 25
- @r.repeat( "-" ).should == '- - - - -'
+ @r.distraction( 5 )
+ @r.repeat( 5 ).should == 15
+ @r.repeat( "-" ).should == '- - -'
@r.repeat.should == nil
end
specify "raise an exception if there is no matching value" do
- lambda { @r.repeat( 7.3 ) }.should.raise(ArgumentError)
+ lambda { @r.repeat( 7.3 ) }.should.raise(Functor::NoMatch)
end
end
diff --git a/test/guards.rb b/test/guards.rb
index 89ec9c6..d8e398f 100644
--- a/test/guards.rb
+++ b/test/guards.rb
@@ -4,8 +4,8 @@
before do
@stripe = Functor.new do |f|
- f.given( lambda { |x| x % 2 == 1 } ) { 'silver' }
- f.given( lambda { |x| x % 2 == 0 } ) { 'white' }
+ f.given( lambda { |x| x % 2 == 1 } ) { |x| 'silver' }
+ f.given( lambda { |x| x % 2 == 0 } ) { |x| 'white' }
end
@safe_divide = Functor.new do |f|
diff --git a/test/inheritance.rb b/test/inheritance.rb
index 1ad2188..ad1d814 100644
--- a/test/inheritance.rb
+++ b/test/inheritance.rb
@@ -1,24 +1,29 @@
require "#{File.dirname(__FILE__)}/helpers"
-class A
+class Parent
include Functor::Method
- functor( :foo, Integer ) { |x| [ A, Integer ] }
- functor( :foo, String ) { |s| [ A, String ] }
- functor( :foo, Float ) { |h| [ A, Float ] }
+ functor( :foo, Integer ) { |x| [ Parent, Integer ] }
+ functor( :foo, String ) { |s| [ Parent, String ] }
+ functor( :foo, Float ) { |h| [ Parent, Float ] }
end
-class B < A
- functor( :foo, String ) { |s| [ B, String ] }
+class Child < Parent
+ functor( :foo, String ) { |s| [ Child, String ] }
+ functor( :foo, Float ) { |x| super(x).reverse }
end
describe "Functor methods should support inheritance" do
specify "by inheriting base class implementations" do
- B.new.foo( 5 ).should == [ A, Integer ]
+ Child.new.foo( 5 ).should == [ Parent, Integer ]
end
specify "by allowing derived classes to override an implementation" do
- B.new.foo( "bar" ).should == [ B, String ]
+ Child.new.foo( "bar" ).should == [ Child, String ]
+ end
+
+ specify "by allowing #super" do
+ Child.new.foo(3.0).should == [ Float, Parent]
end
end
diff --git a/test/matchers.rb b/test/matchers.rb
index 0f92b9a..70bdf38 100644
--- a/test/matchers.rb
+++ b/test/matchers.rb
@@ -1,6 +1,6 @@
require "#{File.dirname(__FILE__)}/helpers"
-class C
+class Matchers
include Functor::Method
functor( :foo, Integer ) { |a| "===" }
functor( :foo, 1 ) { |a| "==" }
@@ -10,15 +10,15 @@ class C
describe "Functors match" do
specify "using ==" do
- C.new.foo( 1 ).should == "=="
+ Matchers.new.foo( 1 ).should == "=="
end
specify "using ===" do
- C.new.foo( 2 ).should == "==="
+ Matchers.new.foo( 2 ).should == "==="
end
specify "using #call" do
- C.new.foo( "boo" ).should == "Lambda: boo"
+ Matchers.new.foo( "boo" ).should == "Lambda: boo"
end
end
diff --git a/test/reopening.rb b/test/reopening.rb
index f26688b..7a2094f 100644
--- a/test/reopening.rb
+++ b/test/reopening.rb
@@ -1,18 +1,18 @@
require "#{File.dirname(__FILE__)}/helpers"
-class A
+class Reopening
include Functor::Method
functor( :foo, Integer ) { |x| 1 }
end
-class A
+class Reopening
functor( :foo, Integer ) { |x| 2 }
end
describe "Functor methods should support reopening" do
specify "by allowing reopening of a class to override an implementation" do
- A.new.foo( 5 ).should == 2
+ Reopening.new.foo( 5 ).should == 2
end
end
diff --git a/test/supplement.rb b/test/supplement.rb
new file mode 100644
index 0000000..b2465d9
--- /dev/null
+++ b/test/supplement.rb
@@ -0,0 +1,20 @@
+require "#{File.dirname(__FILE__)}/helpers"
+
+class Additive
+ include Functor::Method
+
+ def foo(*args)
+ args.reverse
+ end
+
+ functor( :foo, Integer ) { |x| 1 }
+end
+
+describe "A Functor method" do
+
+ specify "supplements, rather than obliterating, an existing method" do
+ Additive.new.foo( 5 ).should == 1
+ Additive.new.foo( :a, :b).should == [ :b, :a ]
+ end
+
+end
\ No newline at end of file
diff --git a/test/wildcard.rb b/test/wildcard.rb
new file mode 100644
index 0000000..7c91093
--- /dev/null
+++ b/test/wildcard.rb
@@ -0,0 +1,15 @@
+require "#{File.dirname(__FILE__)}/helpers"
+
+class Wildcard
+ include Functor::Method
+ functor( :foo, Integer, _whatever ) { |int, whatever| "#{int}: #{whatever}" }
+end
+
+describe "A functor" do
+
+ it "can use a method beginning with '_' to match anything" do
+ c = Wildcard.new
+ c.foo( 7, "Smurf").should == "7: Smurf"
+ end
+
+end
\ No newline at end of file
diff --git a/test/with_self.rb b/test/with_self.rb
index f978818..2e06cf7 100644
--- a/test/with_self.rb
+++ b/test/with_self.rb
@@ -1,21 +1,26 @@
require "#{File.dirname(__FILE__)}/helpers"
-class A
+class WithSelf
attr_accessor :bar
include Functor::Method
def initialize( x ) ; @bar = x ; end
functor_with_self( :foo, self, Integer ) { |x| x }
functor_with_self( :foo, lambda{ |x| x.bar == true }, Integer ) { |s| 'bar' }
+ functor_with_self( :foo, lambda{ |x| x.bar.is_a? String }, Integer ) { |s| 'I be string' }
end
describe "Functor methods should support allow matching on self" do
specify "by allowing functor_with_self to provide a guard on self" do
- A.new( true ).foo( 5 ).should == 'bar'
+ WithSelf.new( true ).foo( 5 ).should == 'bar'
end
specify "or by simply providing self as an argument" do
- A.new( false ).foo( 5 ).should == 5
+ WithSelf.new( false ).foo( 5 ).should == 5
+ end
+
+ specify "another guard example, for those who need it" do
+ WithSelf.new( "me" ).foo( 87 ).should == "I be string"
end
end