functor 0.5.1 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
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