functor 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/functor.rb CHANGED
@@ -1,72 +1,102 @@
1
- require "#{File.dirname(__FILE__)}/object"
2
-
3
1
  class Functor
4
2
 
3
+ class NoMatch < ArgumentError; end
4
+
5
+ def self.cache_config(options={})
6
+ (@cache_config ||= { :size => 4_096, :base => 8 }).merge!(options)
7
+ end
8
+
5
9
  module Method
6
- def self.copy_functors( functors )
7
- r = {} ; functors.each do | name, functor |
8
- r[ name ] = functor.clone
9
- end
10
- return r
11
- end
10
+
12
11
  def self.included( k )
13
- def k.functors
14
- @__functors ||= superclass.respond_to?( :functors ) ?
15
- Functor::Method.copy_functors( superclass.functors ) : {}
12
+
13
+ def k.functor_cache
14
+ @functor_cache ||= Hash.new { |hash, key| hash[key] = [ {},{},{},{} ] }
15
+ end
16
+
17
+ def k.functor_cache_config(options={})
18
+ @functor_cache_config = ( @functor_cache_config || Functor.cache_config ).merge(options)
19
+ end
20
+
21
+ def k.functor( name, *pattern, &action )
22
+ _functor( name, false, *pattern, &action)
16
23
  end
17
- def k.functor( name, *args, &block )
18
- name = name.to_sym
19
- ( f = ( functors[ name ] or
20
- ( functors[ name ] = Functor.new ) ) ).given( *args, &block )
21
- define_method( name ) { | *args | instance_exec( *args, &f.match( *args ) ) }
24
+
25
+ def k.functor_with_self( name, *pattern, &action )
26
+ _functor( name, true, *pattern, &action)
22
27
  end
23
- def k.functor_with_self( name, *args, &block )
24
- name = name.to_sym
25
- ( f = ( functors[ name ] or
26
- ( functors[ name ] = Functor.new ) ) ).given( *args, &block )
27
- define_method( name ) { | *args | instance_exec( *args, &f.match( self, *args ) ) }
28
+
29
+ # undefined methods beginning with '_' can be used as wildcards in Functor patterns
30
+ def k.method_missing(name, *args)
31
+ args.empty? && name.to_s =~ /^_/ ? lambda { |args| true } : super
28
32
  end
33
+
34
+ private
35
+
36
+ def k._functor( name, with_self=false, *pattern, &action)
37
+ name = name.to_s
38
+ mc = functor_cache[name] # grab the cache tiers for The Method
39
+ cache_size, cache_base = functor_cache_config[:size], functor_cache_config[:base]
40
+ c0_size, c1_size, c2_size, c3_size = cache_size * 4, cache_size * 3, cache_size * 2, cache_size
41
+ c1_thresh,c2_thresh,c3_thresh = cache_base.to_i, (cache_base ** 2).to_i, (cache_base ** 3).to_i
42
+ old_method = instance_method(name) if method_defined?( name ) # grab The Method's current incarnation
43
+ define_method( name, action ) # redefine The Method
44
+ newest = instance_method(name) # grab newly redefined The Method
45
+
46
+ # Recursively redefine The Method using the newest and previous incarnations
47
+ define_method( name ) do | *args |
48
+ match_args = with_self ? [self] + args : args
49
+ sig = match_args.hash
50
+ if meth = mc[3][sig] # check caches from top down
51
+ meth[0].bind(self).call(*args)
52
+ elsif meth = mc[2][sig]
53
+ meth[1] += 1 # increment hit count
54
+ mc[3][sig] = mc[2].delete(sig) if meth[1] > c3_thresh # promote sig if it has enough hits
55
+ (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
56
+ meth[0].bind(self).call(*args)
57
+ elsif meth = mc[1][sig]
58
+ meth[1] += 1
59
+ mc[2][sig] = mc[1].delete(sig) if meth[1] > c2_thresh
60
+ mc[0], mc[1], mc[2] = mc[1], mc[2], {} if mc[2].size >= c2_size
61
+ meth[0].bind(self).call(*args)
62
+ elsif meth = mc[0][sig]
63
+ meth[1] += 1
64
+ mc[1][sig] = mc[0].delete(sig) if meth[1] > c1_thresh
65
+ mc[0], mc[1] = mc[1], {} if mc[1].size >= c1_size
66
+ meth[0].bind(self).call(*args)
67
+ elsif Functor.match?(match_args, pattern) # not cached? Try newest meth/pat.
68
+ (mc[0], mc[1], mc[2], mc[3] = mc[1], mc[2], mc[3], {}) if mc[3].size >= c3_size
69
+ mc[3][sig] = [newest, 0] # methods are cached as [ method, counter ]
70
+ newest.bind(self).call(*args)
71
+ elsif old_method # or call the previous incarnation of The Method
72
+ old_method.bind(self).call(*args)
73
+ else # and if there are no older incarnations, whine about it
74
+ raise NoMatch.new( "No functor matches the given arguments for method :#{name}." )
75
+ end
76
+ end
77
+ end
78
+
29
79
  end
30
80
  end
31
81
 
32
-
33
82
  def initialize( &block )
34
- @rules = [] ; yield( self ) if block_given?
35
- end
36
-
37
- def initialize_copy( from )
38
- @rules = from.instance_eval { @rules.clone }
83
+ class << self; include Functor::Method; end
84
+ yield( self ) if block_given?
39
85
  end
40
86
 
41
87
  def given( *pattern, &action )
42
- @rules << [ pattern, action ]
88
+ (class << self; self; end)._functor( "call", false, *pattern, &action)
43
89
  end
44
90
 
45
- def call( *args, &block )
46
- match( *args, &block ).call( *args )
47
- end
48
-
49
- def []( *args, &block )
50
- call( *args, &block )
51
- end
91
+ def []( *args, &block ); call( *args, &block ); end
52
92
 
53
- def to_proc ; lambda { |*args| self.call( *args ) } ; end
93
+ def to_proc ; lambda { |*args| call( *args ) } ; end
54
94
 
55
- def match( *args, &block )
56
- args << block if block_given?
57
- pattern, action = @rules.reverse.find { | p, a | match?( args, p ) }
58
- action or
59
- raise ArgumentError.new( "Argument error: no functor matches the given arguments." )
60
- end
61
-
62
- private
63
-
64
- def match?( args, pattern )
65
- args.zip( pattern ).all? { | arg, rule | pair?( arg, rule ) } if args.length == pattern.length
66
- end
67
-
68
- def pair?( arg, rule )
69
- ( rule.respond_to? :call and rule.call( arg ) ) or rule === arg
95
+ def self.match?( args, pattern )
96
+ args.all? do |arg|
97
+ pat = pattern[args.index(arg)]
98
+ pat === arg || ( pat.respond_to?(:call) && pat.call(arg))
99
+ end if args.length == pattern.length
70
100
  end
71
101
 
72
102
  end
@@ -0,0 +1,39 @@
1
+ require 'rubygems'
2
+
3
+ $:.unshift "#{here = File.dirname(__FILE__)}/stevedore/lib"
4
+ $:.unshift "#{here}/../lib"
5
+ require 'stevedore'
6
+ require 'functor'
7
+
8
+
9
+ class FuncFib < Steve
10
+
11
+ subject "Fibonacci using Functor"
12
+
13
+ # power 0.8
14
+ # sig_level 0.05
15
+ delta 0.01
16
+
17
+ before do
18
+ @fib ||= Functor.new do |f|
19
+ f.given( Integer ) { | n | f.call( n - 1 ) + f.call( n - 2 ) }
20
+ f.given( 0 ) { 0 }
21
+ f.given( 1 ) { 1 }
22
+ end
23
+ end
24
+ end
25
+
26
+ fib_8 = FuncFib.new "f(8)" do
27
+ measure do
28
+ 64.times { @fib.call(8) }
29
+ end
30
+ end
31
+
32
+ fib_16 = FuncFib.new "f(16)" do
33
+ measure do
34
+ 2.times { @fib.call(16) }
35
+ end
36
+ end
37
+
38
+ # FuncFib.recommend_test_size( 8, 16)
39
+ FuncFib.compare_instances( 8, 128)
@@ -0,0 +1,6 @@
1
+ require 'rubygems'
2
+
3
+ $:.unshift "#{here = File.dirname(__FILE__)}/stevedore/lib"
4
+ $:.unshift "#{here}/../lib"
5
+ require 'stevedore'
6
+ require 'functor'
@@ -0,0 +1,71 @@
1
+ require "#{here = File.dirname(__FILE__)}/helpers"
2
+
3
+ class A
4
+ include Functor::Method
5
+ functor( :foo, 1, 2, 3, 4, 5, 6, 7 ) { |*x| "ints" }
6
+ functor( :foo, :a, :b, :c, :d, :e, :f, :g ) { |*x| "symbols" }
7
+ functor( :foo, *%w{ a b c d e f g } ) { |*x| "strings" }
8
+ functor( :foo, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0 ) { |*x| "floats" }
9
+ functor( :foo, *Array.new(7, "one") ) { |*x| "ones" }
10
+ end
11
+
12
+ class Native
13
+ def foo(*args)
14
+ case args
15
+ when [1, 2, 3, 4, 5, 6, 7]
16
+ "ints"
17
+ when [:a, :b, :c, :d, :e, :f, :g]
18
+ "symbols"
19
+ when %w{ a b c d e f g }
20
+ "strings"
21
+ when [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0]
22
+ "floats"
23
+ when Array.new(7, "one")
24
+ "ones"
25
+ else
26
+ raise ArgumentError
27
+ end
28
+ end
29
+ end
30
+
31
+ class ManyArgs < Steve
32
+ end
33
+
34
+ ManyArgs.new "native method" do
35
+ before do
36
+ @args = [
37
+ [1, 2, 3, 4, 5, 6, 7],
38
+ [:a, :b, :c, :d, :e, :f, :g],
39
+ %w{ a b c d e f g },
40
+ [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0],
41
+ Array.new(7, "one")
42
+ ]
43
+ @n = Native.new
44
+ end
45
+ measure do
46
+ 400.times do
47
+ @args.each { |args| @n.foo *args }
48
+ end
49
+ end
50
+ end
51
+
52
+
53
+ ManyArgs.new "functor method" do
54
+ before do
55
+ @args = [
56
+ [1, 2, 3, 4, 5, 6, 7],
57
+ [:a, :b, :c, :d, :e, :f, :g],
58
+ %w{ a b c d e f g },
59
+ [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0],
60
+ Array.new(7, "one")
61
+ ]
62
+ @a = A.new
63
+ end
64
+ measure do
65
+ 400.times do
66
+ @args.each { |args| @a.foo *args }
67
+ end
68
+ end
69
+ end
70
+
71
+ ManyArgs.compare_instances( 4, 64)
@@ -0,0 +1,61 @@
1
+ require "#{here = File.dirname(__FILE__)}/helpers"
2
+
3
+ class A
4
+ include Functor::Method
5
+ functor_cache_config :size => 700, :base => 6
6
+
7
+ functor( :foo, Integer ) { |x| :integer }
8
+ functor( :foo, String ) { |x| :string }
9
+ functor( :foo, Float ) { |x| :float }
10
+ functor( :foo, Symbol ) { |x| :symbol }
11
+ functor( :foo, "one" ) { |x| :one }
12
+ end
13
+
14
+ class Native
15
+ def foo(x)
16
+ case x
17
+ when Integer then :integer
18
+ when String then :string
19
+ when Float then :float
20
+ when Symbol then :symbol
21
+ when "one" then :one
22
+ else
23
+ raise ArgumentError
24
+ end
25
+ end
26
+ end
27
+
28
+ class OneArg < Steve
29
+ before do
30
+ nums = (1..200).to_a
31
+ alphas = ("a".."gr").to_a
32
+ @args_set = nums + nums.map { |i| i.to_f } + alphas + alphas.map { |i| i.to_sym } + Array.new(200, "one")
33
+ @args = []
34
+ srand(46)
35
+ 9000.times { @args << @args_set[rand(@args_set.size)] }
36
+ end
37
+ end
38
+
39
+ OneArg.new "functor method" do
40
+ before_sample do
41
+ @a = A.new
42
+ end
43
+ measure do
44
+ @args.each { |item| @a.foo item }
45
+ end
46
+
47
+ after_sample do
48
+ puts A.functor_cache["foo"].map{ |c| c.size }.inspect
49
+ end
50
+ end
51
+
52
+ OneArg.new "native method" do
53
+ before_sample do
54
+ @n = Native.new
55
+ end
56
+ measure do
57
+ @args.each { |item| @n.foo item }
58
+ end
59
+ end
60
+
61
+ OneArg.compare_instances( 4, 96)
data/test/fib.rb CHANGED
@@ -2,8 +2,8 @@ require "#{File.dirname(__FILE__)}/helpers"
2
2
 
3
3
  fib ||= Functor.new do |f|
4
4
  f.given( Integer ) { | n | f.call( n - 1 ) + f.call( n - 2 ) }
5
- f.given( 0 ) { 0 }
6
- f.given( 1 ) { 1 }
5
+ f.given( 0 ) { |x| 0 }
6
+ f.given( 1 ) { |x| 1 }
7
7
  end
8
8
 
9
9
  describe "Dispatch on a functor object should" do
data/test/functor.rb CHANGED
@@ -6,23 +6,25 @@ class Repeater
6
6
  functor( :repeat, Integer ) { |x| x * @times }
7
7
  functor( :repeat, String ) { |s| [].fill( s, 0, @times ).join(' ') }
8
8
  functor( :repeat ) { nil }
9
+ functor( :distraction, Integer ) { |x| "Boo!" }
9
10
  end
10
11
 
11
12
  describe "Dispatch on instance method should" do
12
13
 
13
14
  before do
14
15
  @r = Repeater.new
15
- @r.times = 5
16
+ @r.times = 3
16
17
  end
17
18
 
18
19
  specify "invoke different methods with object scope based on arguments" do
19
- @r.repeat( 5 ).should == 25
20
- @r.repeat( "-" ).should == '- - - - -'
20
+ @r.distraction( 5 )
21
+ @r.repeat( 5 ).should == 15
22
+ @r.repeat( "-" ).should == '- - -'
21
23
  @r.repeat.should == nil
22
24
  end
23
25
 
24
26
  specify "raise an exception if there is no matching value" do
25
- lambda { @r.repeat( 7.3 ) }.should.raise(ArgumentError)
27
+ lambda { @r.repeat( 7.3 ) }.should.raise(Functor::NoMatch)
26
28
  end
27
29
  end
28
30
 
data/test/guards.rb CHANGED
@@ -4,8 +4,8 @@ describe "Dispatch should support guards" do
4
4
 
5
5
  before do
6
6
  @stripe = Functor.new do |f|
7
- f.given( lambda { |x| x % 2 == 1 } ) { 'silver' }
8
- f.given( lambda { |x| x % 2 == 0 } ) { 'white' }
7
+ f.given( lambda { |x| x % 2 == 1 } ) { |x| 'silver' }
8
+ f.given( lambda { |x| x % 2 == 0 } ) { |x| 'white' }
9
9
  end
10
10
 
11
11
  @safe_divide = Functor.new do |f|
data/test/inheritance.rb CHANGED
@@ -1,24 +1,29 @@
1
1
  require "#{File.dirname(__FILE__)}/helpers"
2
2
 
3
- class A
3
+ class Parent
4
4
  include Functor::Method
5
- functor( :foo, Integer ) { |x| [ A, Integer ] }
6
- functor( :foo, String ) { |s| [ A, String ] }
7
- functor( :foo, Float ) { |h| [ A, Float ] }
5
+ functor( :foo, Integer ) { |x| [ Parent, Integer ] }
6
+ functor( :foo, String ) { |s| [ Parent, String ] }
7
+ functor( :foo, Float ) { |h| [ Parent, Float ] }
8
8
  end
9
9
 
10
- class B < A
11
- functor( :foo, String ) { |s| [ B, String ] }
10
+ class Child < Parent
11
+ functor( :foo, String ) { |s| [ Child, String ] }
12
+ functor( :foo, Float ) { |x| super(x).reverse }
12
13
  end
13
14
 
14
15
  describe "Functor methods should support inheritance" do
15
16
 
16
17
  specify "by inheriting base class implementations" do
17
- B.new.foo( 5 ).should == [ A, Integer ]
18
+ Child.new.foo( 5 ).should == [ Parent, Integer ]
18
19
  end
19
20
 
20
21
  specify "by allowing derived classes to override an implementation" do
21
- B.new.foo( "bar" ).should == [ B, String ]
22
+ Child.new.foo( "bar" ).should == [ Child, String ]
23
+ end
24
+
25
+ specify "by allowing #super" do
26
+ Child.new.foo(3.0).should == [ Float, Parent]
22
27
  end
23
28
 
24
29
  end
data/test/matchers.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  require "#{File.dirname(__FILE__)}/helpers"
2
2
 
3
- class C
3
+ class Matchers
4
4
  include Functor::Method
5
5
  functor( :foo, Integer ) { |a| "===" }
6
6
  functor( :foo, 1 ) { |a| "==" }
@@ -10,15 +10,15 @@ end
10
10
  describe "Functors match" do
11
11
 
12
12
  specify "using ==" do
13
- C.new.foo( 1 ).should == "=="
13
+ Matchers.new.foo( 1 ).should == "=="
14
14
  end
15
15
 
16
16
  specify "using ===" do
17
- C.new.foo( 2 ).should == "==="
17
+ Matchers.new.foo( 2 ).should == "==="
18
18
  end
19
19
 
20
20
  specify "using #call" do
21
- C.new.foo( "boo" ).should == "Lambda: boo"
21
+ Matchers.new.foo( "boo" ).should == "Lambda: boo"
22
22
  end
23
23
 
24
24
  end