functor 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
data/doc/HISTORY ADDED
@@ -0,0 +1,3 @@
1
+ 0.1 - Initial implemention of Functor class.
2
+ 0.2 - Added method dispatch, to_proc support, tests.
3
+ 0.3 - Added support for guards and redefinition.
data/doc/README ADDED
@@ -0,0 +1,45 @@
1
+ Functor provides pattern-based function and method dispatch for Ruby. To use it in a class:
2
+
3
+ class Repeater
4
+ attr_accessor :times
5
+ include Functor::Method
6
+ functor( :repeat, Integer ) { |x| x * @times }
7
+ functor( :repeat, String ) { |s| [].fill( s, 0..@times ).join(' ') }
8
+ end
9
+
10
+ r = Repeater.new
11
+ r.times = 5
12
+ r.repeat( 5 ) # => 25
13
+ r.repeat( "-" ) # => "- - - - -"
14
+ r.repeat( 7.3 ) # => RuntimeError!
15
+
16
+ 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:
17
+
18
+ A.functors[ :foo ].apply( self, 'bar' )
19
+
20
+ You can also define Functor objects directly:
21
+
22
+ fib = Functor.new do
23
+ given( 0 ) { 0 }
24
+ given( 1 ) { 1 }
25
+ given( 2..10000 ) { |n| self.call( n - 1 ) + self.call( n - 2 ) }
26
+ end
27
+
28
+ You can use functors directly with functions taking a block like this:
29
+
30
+ [ *0..10 ].map( &fib ) # => [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
31
+
32
+ You can explicitly bind self using #bind:
33
+
34
+ fun.bind( obj )
35
+
36
+ which is actually how the method dispatch is implemented.
37
+
38
+ 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:
39
+
40
+ stripe ||= Functor.new do
41
+ given( lambda { |x| x % 2 == 0 } ) { 'white' }
42
+ given( lambda { |x| x % 2 == 1 } ) { 'silver' }
43
+ end
44
+
45
+ which will return "white" and "silver" alternately for a sequence of numbers.
data/lib/functor.rb ADDED
@@ -0,0 +1,64 @@
1
+ require 'lib/object'
2
+
3
+ class Functor
4
+
5
+ module Method
6
+ def self.included( k )
7
+ def k.functors ; @__functors ||= {} ; end
8
+ def k.functor( name, *args, &block )
9
+ unless functors[ name ]
10
+ functors[ name ] = Functor.new
11
+ klass = self.name ; module_eval <<-CODE
12
+ def #{name}( *args, &block )
13
+ begin
14
+ #{klass}.functors[ :#{name} ].apply( self, *args, &block )
15
+ rescue ArgumentError => e
16
+ begin
17
+ super
18
+ rescue NoMethodError => f
19
+ raise e
20
+ end
21
+ end
22
+ end
23
+ CODE
24
+ end
25
+ functors[ name ].given( *args, &block )
26
+ end
27
+ end
28
+ end
29
+
30
+ def initialize( &block )
31
+ @rules = [] ; instance_eval( &block ) if block_given?
32
+ end
33
+
34
+ def given( *pattern, &action )
35
+ @rules.delete_if { |p,a| p == pattern }
36
+ @rules << [ pattern, action ]
37
+ end
38
+
39
+ def apply( object, *args, &block )
40
+ object.instance_exec( *args, &match( *args, &block ) )
41
+ end
42
+
43
+ def call( *args, &block )
44
+ args.push( block ) if block_given?
45
+ match( *args, &block ).call( *args )
46
+ end
47
+
48
+ def to_proc ; lambda { |*args| self.call( *args ) } ; end
49
+
50
+ private
51
+
52
+ def match( *args, &block )
53
+ args.push( block ) if block_given?
54
+ pattern, action = @rules.find { | pattern, action | match?( args, pattern ) }
55
+ raise ArgumentError.new( "argument mismatch for argument(s): #{args.inspect}." ) unless action
56
+ return action
57
+ end
58
+
59
+ def match?( args, pattern )
60
+ pattern.zip(args).all? { |x,y| x === y or x == y or
61
+ ( x.respond_to?(:call) && x.call( y ) ) } if pattern.length == args.length
62
+ end
63
+
64
+ end
data/lib/object.rb ADDED
@@ -0,0 +1,17 @@
1
+ class Object
2
+ # This is an extremely powerful little function that will be built-in to Ruby 1.9.
3
+ # This version is from Mauricio Fernandez via ruby-talk. Works like instance_eval
4
+ # except that you can pass parameters to the block. This means you can define a block
5
+ # intended for use with instance_eval, pass it to another method, which can then
6
+ # invoke with parameters. This is used quite a bit by the Waves::Mapping code.
7
+ def instance_exec(*args, &block)
8
+ mname = "__instance_exec_#{Thread.current.object_id.abs}"
9
+ class << self; self end.class_eval{ define_method(mname, &block) }
10
+ begin
11
+ ret = send(mname, *args)
12
+ ensure
13
+ class << self; self end.class_eval{ undef_method(mname) } rescue nil
14
+ end
15
+ ret
16
+ end
17
+ end
data/test/fib.rb ADDED
@@ -0,0 +1,16 @@
1
+ require "#{File.dirname(__FILE__)}/helpers"
2
+
3
+ fib ||= Functor.new do
4
+ given( 0 ) { 0 }
5
+ given( 1 ) { 1 }
6
+ given( Integer ) { | n | self.call( n - 1 ) + self.call( n - 2 ) }
7
+ end
8
+
9
+ describe "Dispatch on a functor object should" do
10
+
11
+ specify "be able to implement the Fibonacci function" do
12
+ [*0..10].map( &fib ).should == [ 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55 ]
13
+ end
14
+
15
+ end
16
+
data/test/functor.rb ADDED
@@ -0,0 +1,29 @@
1
+ require "#{File.dirname(__FILE__)}/helpers"
2
+
3
+ class Repeater
4
+ attr_accessor :times
5
+ include Functor::Method
6
+ functor( :repeat, Integer ) { |x| x * @times }
7
+ functor( :repeat, String ) { |s| [].fill( s, 0, @times ).join(' ') }
8
+ functor( :repeat ) { nil }
9
+ end
10
+
11
+ describe "Dispatch on instance method should" do
12
+
13
+ before do
14
+ @r = Repeater.new
15
+ @r.times = 5
16
+ end
17
+
18
+ specify "invoke different methods with object scope based on arguments" do
19
+ @r.repeat( 5 ).should == 25
20
+ @r.repeat( "-" ).should == '- - - - -'
21
+ @r.repeat.should == nil
22
+ end
23
+
24
+ specify "raise an exception if there is no matching value" do
25
+ lambda { @r.repeat( 7.3 ) }.should.raise(ArgumentError)
26
+ end
27
+ end
28
+
29
+
data/test/guards.rb ADDED
@@ -0,0 +1,15 @@
1
+ require 'test/helpers'
2
+
3
+ stripe ||= Functor.new do
4
+ given( lambda { |x| x % 2 == 0 } ) { 'white' }
5
+ given( lambda { |x| x % 2 == 1 } ) { 'silver' }
6
+ end
7
+
8
+ describe "Dipatch should support guards" do
9
+
10
+ specify "allowing you to use odd or even numbers as a dispatcher" do
11
+ [*0..9].map( &stripe ).should == %w( white silver ) * 5
12
+ end
13
+
14
+ end
15
+
data/test/helpers.rb ADDED
@@ -0,0 +1,15 @@
1
+ require 'rubygems'
2
+ %w{ bacon }.each { |dep| require dep }
3
+ Bacon.summary_on_exit
4
+
5
+ module Kernel
6
+ private
7
+ def specification(name, &block) Bacon::Context.new(name, &block) end
8
+ end
9
+
10
+ Bacon::Context.instance_eval do
11
+ alias_method :specify, :it
12
+ end
13
+
14
+ $:.unshift "#{File.dirname(__FILE__)}/../lib"
15
+ require "functor"
@@ -0,0 +1,28 @@
1
+ require "#{File.dirname(__FILE__)}/helpers"
2
+
3
+ class A
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 ] }
8
+ end
9
+
10
+ class B < A
11
+ functor( :foo, String ) { |s| [ B, String ] }
12
+ functor( :foo, Float ) { |f| [ B, *A.functors[:foo].apply( self, f ) ] }
13
+ end
14
+
15
+ describe "Functor methods should support inheritance" do
16
+
17
+ specify "by inheriting base class implementations" do
18
+ B.new.foo( 5 ).should == [ A, Integer ]
19
+ end
20
+
21
+ specify "by allowing derived classes to override an implementation" do
22
+ B.new.foo( "bar" ).should == [ B, String ]
23
+ end
24
+
25
+ specify "by allowing you to call base class functors using #functors" do
26
+ B.new.foo( 1.0 ).should == [ B, A, Float ]
27
+ end
28
+ end
data/test/matchers.rb ADDED
@@ -0,0 +1,24 @@
1
+ require "#{File.dirname(__FILE__)}/helpers"
2
+
3
+ class C
4
+ include Functor::Method
5
+ functor( :foo, 1 ) { |a| "==" }
6
+ functor( :foo, Integer ) { |a| "===" }
7
+ functor( :foo, lambda { |a| a == "boo" } ) { |v| "Lambda: #{v}" }
8
+ end
9
+
10
+ describe "Functors match" do
11
+
12
+ specify "using ==" do
13
+ C.new.foo( 1 ).should == "=="
14
+ end
15
+
16
+ specify "using ===" do
17
+ C.new.foo( 2 ).should == "==="
18
+ end
19
+
20
+ specify "using #call" do
21
+ C.new.foo( "boo" ).should == "Lambda: boo"
22
+ end
23
+
24
+ end
data/test/reopening.rb ADDED
@@ -0,0 +1,18 @@
1
+ require "#{File.dirname(__FILE__)}/helpers"
2
+
3
+ class A
4
+ include Functor::Method
5
+ functor( :foo, Integer ) { |x| 1 }
6
+ end
7
+
8
+ class A
9
+ functor( :foo, Integer ) { |x| 2 }
10
+ end
11
+
12
+ describe "Functor methods should support reopening" do
13
+
14
+ specify "by allowing reopening of a class to override an implementation" do
15
+ A.new.foo( 5 ).should == 2
16
+ end
17
+
18
+ end
metadata ADDED
@@ -0,0 +1,65 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: functor
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.1
5
+ platform: ruby
6
+ authors:
7
+ - Dan Yoder
8
+ - Matthew King
9
+ - Lawrence Pit
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+
14
+ date: 2008-06-15 00:00:00 -07:00
15
+ default_executable:
16
+ dependencies: []
17
+
18
+ description:
19
+ email: dan@zeraweb.com
20
+ executables: []
21
+
22
+ extensions: []
23
+
24
+ extra_rdoc_files: []
25
+
26
+ files:
27
+ - doc/HISTORY
28
+ - doc/README
29
+ - lib/functor.rb
30
+ - lib/object.rb
31
+ - test/fib.rb
32
+ - test/functor.rb
33
+ - test/guards.rb
34
+ - test/helpers.rb
35
+ - test/inheritance.rb
36
+ - test/matchers.rb
37
+ - test/reopening.rb
38
+ has_rdoc: true
39
+ homepage: http://dev.zeraweb.com/
40
+ post_install_message:
41
+ rdoc_options: []
42
+
43
+ require_paths:
44
+ - lib
45
+ required_ruby_version: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: 1.8.6
50
+ version:
51
+ required_rubygems_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: "0"
56
+ version:
57
+ requirements: []
58
+
59
+ rubyforge_project: functor
60
+ rubygems_version: 1.0.1
61
+ signing_key:
62
+ specification_version: 2
63
+ summary: Pattern-based dispatch for Ruby, inspired by Topher Cyll's multi.
64
+ test_files: []
65
+