functor 0.3.1
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/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
|
+
|