functor 0.3.1
Sign up to get free protection for your applications and to get access to all the features.
- data/doc/HISTORY +3 -0
- data/doc/README +45 -0
- data/lib/functor.rb +64 -0
- data/lib/object.rb +17 -0
- data/test/fib.rb +16 -0
- data/test/functor.rb +29 -0
- data/test/guards.rb +15 -0
- data/test/helpers.rb +15 -0
- data/test/inheritance.rb +28 -0
- data/test/matchers.rb +24 -0
- data/test/reopening.rb +18 -0
- metadata +65 -0
data/doc/HISTORY
ADDED
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"
|
data/test/inheritance.rb
ADDED
@@ -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
|
+
|