functional-ruby 0.5.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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +712 -0
- data/lib/functional/all.rb +5 -0
- data/lib/functional/behavior.rb +59 -0
- data/lib/functional/behaviour.rb +2 -0
- data/lib/functional/concurrency.rb +27 -0
- data/lib/functional/core.rb +62 -0
- data/lib/functional/pattern_matching.rb +133 -0
- data/lib/functional/version.rb +3 -0
- data/spec/functional/behavior_spec.rb +237 -0
- data/spec/functional/integration_spec.rb +205 -0
- data/spec/functional/pattern_matching_spec.rb +416 -0
- data/spec/spec_helper.rb +17 -0
- metadata +78 -0
@@ -0,0 +1,59 @@
|
|
1
|
+
def behavior_info(name, callbacks = {})
|
2
|
+
$__behavior_info__ ||= {}
|
3
|
+
$__behavior_info__[name.to_sym] = callbacks.inject({}){|memo,(k,v)| memo[k.to_sym] = v; memo}
|
4
|
+
end
|
5
|
+
|
6
|
+
alias :behaviour_info :behavior_info
|
7
|
+
alias :interface :behavior_info
|
8
|
+
|
9
|
+
class Object
|
10
|
+
def behaves_as?(name)
|
11
|
+
|
12
|
+
name = name.to_sym
|
13
|
+
bi = $__behavior_info__[name]
|
14
|
+
return false if bi.nil?
|
15
|
+
|
16
|
+
bi.each do |method, arity|
|
17
|
+
begin
|
18
|
+
return false unless arity == :any || self.method(method).arity == arity
|
19
|
+
rescue NameError
|
20
|
+
return false
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
return true
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def behavior(name)
|
29
|
+
|
30
|
+
name = name.to_sym
|
31
|
+
raise ArgumentError.new("undefined behavior '#{name}'") if $__behavior_info__[name].nil?
|
32
|
+
|
33
|
+
clazz = self.method(:behavior).receiver
|
34
|
+
|
35
|
+
unless clazz.instance_methods(false).include?(:behaviors)
|
36
|
+
class << clazz
|
37
|
+
def behaviors
|
38
|
+
@behaviors ||= []
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
clazz.behaviors << name
|
44
|
+
|
45
|
+
class << clazz
|
46
|
+
def new(*args, &block)
|
47
|
+
name = self.behaviors.first
|
48
|
+
obj = super
|
49
|
+
unless obj.behaves_as?(name)
|
50
|
+
raise ArgumentError.new("undefined callback functions in #{self} (behavior '#{name}')")
|
51
|
+
else
|
52
|
+
return obj
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
alias :behaviour :behavior
|
59
|
+
alias :behaves_as :behavior
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# http://docs.oracle.com/javase/tutorial/java/javaOO/enum.html
|
2
|
+
# http://www.lesismore.co.za/rubyenums.html
|
3
|
+
# http://gistflow.com/posts/682-ruby-enums-approaches
|
4
|
+
|
5
|
+
# http://richhickey.github.io/clojure/clojure.core-api.html
|
6
|
+
# * agent
|
7
|
+
# * add-watch
|
8
|
+
# * apply
|
9
|
+
# * assert
|
10
|
+
# * await
|
11
|
+
# * future
|
12
|
+
# * memoize
|
13
|
+
# * promise
|
14
|
+
# * send
|
15
|
+
# * slurp
|
16
|
+
|
17
|
+
# Other stuff
|
18
|
+
# * pure - creates an object, freezes it, and removes the unfreeze method
|
19
|
+
# * retry - retries something x times if it fails
|
20
|
+
# * promise = ala JavaScript http://blog.parse.com/2013/01/29/whats-so-great-about-javascript-promises/
|
21
|
+
# * ada range type - http://en.wikibooks.org/wiki/Ada_Programming/Types/range
|
22
|
+
# * slurpee - slurp + erb parsing
|
23
|
+
# * spawn/send/receive - http://www.erlang.org/doc/reference_manual/processes.html
|
24
|
+
|
25
|
+
# Using Ruby's queue for sending messages between threads
|
26
|
+
# * http://www.subelsky.com/2010/02/using-rubys-queue-class-to-manage-inter.html
|
27
|
+
# * http://www.ruby-doc.org/stdlib-1.9.3/libdoc/thread/rdoc/Queue.html#method-i-pop
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'pp'
|
2
|
+
require 'stringio'
|
3
|
+
|
4
|
+
Infinity = 1/0.0 unless defined?(Infinity)
|
5
|
+
NaN = 0/0.0 unless defined?(NaN)
|
6
|
+
|
7
|
+
module Kernel
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
def repl?
|
12
|
+
return ($0 == 'irb' || $0 == 'pry' || $0 == 'script/rails' || !!($0 =~ /bin\/bundle$/))
|
13
|
+
end
|
14
|
+
module_function :repl?
|
15
|
+
|
16
|
+
def safe(*args, &block)
|
17
|
+
raise ArgumentError.new('no block given') unless block_given?
|
18
|
+
result = nil
|
19
|
+
t = Thread.new do
|
20
|
+
$SAFE = 3
|
21
|
+
result = self.instance_exec(*args, &block)
|
22
|
+
end
|
23
|
+
t.join
|
24
|
+
return result
|
25
|
+
end
|
26
|
+
module_function :safe
|
27
|
+
|
28
|
+
# http://rhaseventh.blogspot.com/2008/07/ruby-and-rails-how-to-get-pp-pretty.html
|
29
|
+
def pp_s(*objs)
|
30
|
+
s = StringIO.new
|
31
|
+
objs.each {|obj|
|
32
|
+
PP.pp(obj, s)
|
33
|
+
}
|
34
|
+
s.rewind
|
35
|
+
s.read
|
36
|
+
end
|
37
|
+
module_function :pp_s
|
38
|
+
|
39
|
+
# Compute the difference (delta) between two values.
|
40
|
+
#
|
41
|
+
# When a block is given the block will be applied to both arguments.
|
42
|
+
# Using a block in this way allows computation against a specific field
|
43
|
+
# in a data set of hashes or objects.
|
44
|
+
#
|
45
|
+
# @yield iterates over each element in the data set
|
46
|
+
# @yieldparam item each element in the data set
|
47
|
+
#
|
48
|
+
# @param [Object] v1 the first value
|
49
|
+
# @param [Object] v2 the second value
|
50
|
+
#
|
51
|
+
# @return [Float] positive value representing the difference
|
52
|
+
# between the two parameters
|
53
|
+
def delta(v1, v2)
|
54
|
+
if block_given?
|
55
|
+
v1 = yield(v1)
|
56
|
+
v2 = yield(v2)
|
57
|
+
end
|
58
|
+
return (v1 - v2).abs
|
59
|
+
end
|
60
|
+
module_function :delta
|
61
|
+
|
62
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
module PatternMatching
|
2
|
+
|
3
|
+
UNBOUND = Class.new
|
4
|
+
ALL = Class.new
|
5
|
+
|
6
|
+
private
|
7
|
+
|
8
|
+
GUARD_CLAUSE = Class.new do # :nodoc:
|
9
|
+
def initialize(func, clazz, matcher) # :nodoc:
|
10
|
+
@func = func
|
11
|
+
@clazz = clazz
|
12
|
+
@matcher = matcher
|
13
|
+
end
|
14
|
+
def when(&block) # :nodoc:
|
15
|
+
unless block_given?
|
16
|
+
raise ArgumentError.new("block missing for `when` guard on function `#{@func}` of class #{@clazz}")
|
17
|
+
end
|
18
|
+
@matcher[@matcher.length-1] = block
|
19
|
+
return nil
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.__match_pattern__(args, pattern) # :nodoc:
|
24
|
+
return unless (pattern.last == ALL && args.length >= pattern.length) \
|
25
|
+
|| (args.length == pattern.length)
|
26
|
+
pattern.each_with_index do |p, i|
|
27
|
+
break if p == ALL && i+1 == pattern.length
|
28
|
+
arg = args[i]
|
29
|
+
next if p.is_a?(Class) && arg.is_a?(p)
|
30
|
+
if p.is_a?(Hash) && arg.is_a?(Hash) && ! p.empty?
|
31
|
+
p.each do |key, value|
|
32
|
+
return false unless arg.has_key?(key)
|
33
|
+
next if value == UNBOUND
|
34
|
+
return false unless arg[key] == value
|
35
|
+
end
|
36
|
+
next
|
37
|
+
end
|
38
|
+
return false unless p == UNBOUND || p == arg
|
39
|
+
end
|
40
|
+
return true
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.__unbound_args__(match, args) # :nodoc:
|
44
|
+
argv = []
|
45
|
+
match.first.each_with_index do |p, i|
|
46
|
+
if p == ALL && i == match.first.length-1
|
47
|
+
argv << args[(i..args.length)].reduce([]){|memo, arg| memo << arg }
|
48
|
+
elsif p.is_a?(Hash) && p.values.include?(UNBOUND)
|
49
|
+
p.each do |key, value|
|
50
|
+
argv << args[i][key] if value == UNBOUND
|
51
|
+
end
|
52
|
+
elsif p.is_a?(Hash) || p == UNBOUND || p.is_a?(Class)
|
53
|
+
argv << args[i]
|
54
|
+
end
|
55
|
+
end
|
56
|
+
return argv
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.__pattern_match__(clazz, func, *args, &block) # :nodoc:
|
60
|
+
args = args.first
|
61
|
+
|
62
|
+
matchers = clazz.__function_pattern_matches__[func]
|
63
|
+
return [:nodef, nil] if matchers.nil?
|
64
|
+
|
65
|
+
match = matchers.detect do |matcher|
|
66
|
+
if PatternMatching.__match_pattern__(args, matcher.first)
|
67
|
+
if matcher.last.nil?
|
68
|
+
true # no guard clause
|
69
|
+
else
|
70
|
+
self.instance_exec(*PatternMatching.__unbound_args__(matcher, args), &matcher.last)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
return (match ? [:ok, match] : [:nomatch, nil])
|
76
|
+
end
|
77
|
+
|
78
|
+
protected
|
79
|
+
|
80
|
+
def self.included(base)
|
81
|
+
|
82
|
+
class << base
|
83
|
+
|
84
|
+
public
|
85
|
+
|
86
|
+
def _() # :nodoc:
|
87
|
+
return UNBOUND
|
88
|
+
end
|
89
|
+
|
90
|
+
def defn(func, *args, &block)
|
91
|
+
unless block_given?
|
92
|
+
raise ArgumentError.new("block missing for definition of function `#{func}` on class #{self}")
|
93
|
+
end
|
94
|
+
|
95
|
+
pattern = __add_pattern_for__(func, *args, &block)
|
96
|
+
|
97
|
+
unless self.instance_methods(false).include?(func)
|
98
|
+
|
99
|
+
define_method(func) do |*args, &block|
|
100
|
+
result, match = PatternMatching.__pattern_match__(self.method(func).owner, func, args, block)
|
101
|
+
if result == :ok
|
102
|
+
# if a match is found call the block
|
103
|
+
argv = PatternMatching.__unbound_args__(match, args)
|
104
|
+
return self.instance_exec(*argv, &match[1])
|
105
|
+
else # if result == :nodef || result == :nomatch
|
106
|
+
begin
|
107
|
+
super(*args, &block)
|
108
|
+
rescue NoMethodError, ArgumentError
|
109
|
+
raise NoMethodError.new("no method `#{func}` matching #{args} found for class #{self.class}")
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
return GUARD_CLAUSE.new(func, self, pattern)
|
116
|
+
end
|
117
|
+
|
118
|
+
public
|
119
|
+
|
120
|
+
def __function_pattern_matches__ # :nodoc:
|
121
|
+
@__function_pattern_matches__ ||= Hash.new
|
122
|
+
end
|
123
|
+
|
124
|
+
def __add_pattern_for__(func, *args, &block) # :nodoc:
|
125
|
+
block = Proc.new{} unless block_given?
|
126
|
+
matchers = self.__function_pattern_matches__
|
127
|
+
matchers[func] = [] unless matchers.has_key?(func)
|
128
|
+
matchers[func] << [args, block, nil]
|
129
|
+
return matchers[func].last
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,237 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
#behaviour_info(:gen_foo, foo: 0, bar: 1, baz: 2, boom: -1, bam: :any)
|
4
|
+
|
5
|
+
#class Foo
|
6
|
+
#behavior(:gen_foo)
|
7
|
+
|
8
|
+
#def foo
|
9
|
+
#return 'foo/0'
|
10
|
+
#end
|
11
|
+
|
12
|
+
#def bar(one, &block)
|
13
|
+
#return 'bar/1'
|
14
|
+
#end
|
15
|
+
|
16
|
+
#def baz(one, two)
|
17
|
+
#return 'baz/2'
|
18
|
+
#end
|
19
|
+
|
20
|
+
#def boom(*args)
|
21
|
+
#return 'boom/-1'
|
22
|
+
#end
|
23
|
+
|
24
|
+
#def bam
|
25
|
+
#return 'bam!'
|
26
|
+
#end
|
27
|
+
#end
|
28
|
+
|
29
|
+
describe 'behavior/interface definitions' do
|
30
|
+
|
31
|
+
before(:each) do
|
32
|
+
$__behavior_info__ = {}
|
33
|
+
end
|
34
|
+
|
35
|
+
context 'behavior_info/2' do
|
36
|
+
|
37
|
+
it 'accepts a symbol name' do
|
38
|
+
behavior_info(:gen_foo, foo: 0)
|
39
|
+
$__behavior_info__.keys.first.should eq :gen_foo
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'accepts a string name' do
|
43
|
+
behavior_info('gen_foo', foo: 0)
|
44
|
+
$__behavior_info__.keys.first.should eq :gen_foo
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'accepts zero function names' do
|
48
|
+
behavior_info(:gen_foo)
|
49
|
+
$__behavior_info__.keys.first.should eq :gen_foo
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'accepts symbols for function names' do
|
53
|
+
behavior_info(:gen_foo, foo: 0)
|
54
|
+
$__behavior_info__.values.first.should == {foo: 0}
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'accepts strings as function names' do
|
58
|
+
behavior_info(:gen_foo, 'foo' => 0)
|
59
|
+
$__behavior_info__.values.first.should == {foo: 0}
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'accepts numeric arity values' do
|
63
|
+
behavior_info(:gen_foo, foo: 0)
|
64
|
+
$__behavior_info__.values.first.should == {foo: 0}
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'accepts :any as an arity value' do
|
68
|
+
behavior_info(:gen_foo, foo: :any)
|
69
|
+
$__behavior_info__.values.first.should == {foo: :any}
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
context 'behavior/1' do
|
74
|
+
|
75
|
+
it 'raises an exception if the behavior has not been defined' do
|
76
|
+
lambda {
|
77
|
+
Class.new{
|
78
|
+
behavior(:gen_foo)
|
79
|
+
}
|
80
|
+
}.should raise_error(ArgumentError)
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'can be called multiple times for one class' do
|
84
|
+
behavior_info(:gen_foo, foo: 0)
|
85
|
+
behavior_info(:gen_bar, bar: 0)
|
86
|
+
|
87
|
+
lambda {
|
88
|
+
Class.new{
|
89
|
+
behavior(:gen_foo)
|
90
|
+
behavior(:gen_bar)
|
91
|
+
}
|
92
|
+
}.should_not raise_error
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
context 'behavior check on object creation' do
|
97
|
+
|
98
|
+
it 'raises an exception when one or more function definitions are missing' do
|
99
|
+
behavior_info(:gen_foo, foo: 0, bar: 1)
|
100
|
+
clazz = Class.new {
|
101
|
+
behavior(:gen_foo)
|
102
|
+
def foo() nil; end
|
103
|
+
}
|
104
|
+
|
105
|
+
lambda {
|
106
|
+
clazz.new
|
107
|
+
}.should raise_error(ArgumentError)
|
108
|
+
end
|
109
|
+
|
110
|
+
it 'raises an exception when one or more functions do not have proper arity' do
|
111
|
+
behavior_info(:gen_foo, foo: 0)
|
112
|
+
clazz = Class.new {
|
113
|
+
behavior(:gen_foo)
|
114
|
+
def foo(broken) nil; end
|
115
|
+
}
|
116
|
+
|
117
|
+
lambda {
|
118
|
+
clazz.new
|
119
|
+
}.should raise_error(ArgumentError)
|
120
|
+
end
|
121
|
+
|
122
|
+
it 'accepts any arity when function arity is set to :any' do
|
123
|
+
behavior_info(:gen_foo, foo: :any)
|
124
|
+
clazz = Class.new {
|
125
|
+
behavior(:gen_foo)
|
126
|
+
def foo(first) nil; end
|
127
|
+
}
|
128
|
+
|
129
|
+
lambda {
|
130
|
+
clazz.new
|
131
|
+
}.should_not raise_error(ArgumentError)
|
132
|
+
end
|
133
|
+
|
134
|
+
it 'creates the object when function definitions match' do
|
135
|
+
behavior_info(:gen_foo, foo: 0, bar: 1)
|
136
|
+
clazz = Class.new {
|
137
|
+
behavior(:gen_foo)
|
138
|
+
def foo() nil; end
|
139
|
+
def bar(first) nil; end
|
140
|
+
}
|
141
|
+
|
142
|
+
lambda {
|
143
|
+
clazz.new
|
144
|
+
}.should_not raise_error(ArgumentError)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
context '#behaves_as?' do
|
149
|
+
|
150
|
+
it 'returns true when the behavior is fully suported' do
|
151
|
+
behavior_info(:gen_foo, foo: 0, bar: 1, baz: 2)
|
152
|
+
clazz = Class.new {
|
153
|
+
def foo() nil; end
|
154
|
+
def bar(first) nil; end
|
155
|
+
def baz(first, second) nil; end
|
156
|
+
}
|
157
|
+
|
158
|
+
clazz.new.behaves_as?(:gen_foo).should be_true
|
159
|
+
end
|
160
|
+
|
161
|
+
it 'accepts any arity when function arity is set to :any' do
|
162
|
+
behavior_info(:gen_foo, foo: :any)
|
163
|
+
clazz = Class.new {
|
164
|
+
def foo(*args, &block) nil; end
|
165
|
+
}
|
166
|
+
|
167
|
+
clazz.new.behaves_as?(:gen_foo).should be_true
|
168
|
+
end
|
169
|
+
|
170
|
+
it 'returns false when the behavior is partially supported' do
|
171
|
+
behavior_info(:gen_foo, foo: 0, bar: 1, baz: 2)
|
172
|
+
clazz = Class.new {
|
173
|
+
def foo() nil; end
|
174
|
+
def bar(first) nil; end
|
175
|
+
}
|
176
|
+
|
177
|
+
clazz.new.behaves_as?(:gen_foo).should be_false
|
178
|
+
end
|
179
|
+
|
180
|
+
it 'returns false when the behavior is not supported at all' do
|
181
|
+
behavior_info(:gen_foo, foo: 0, bar: 1, baz: 2)
|
182
|
+
clazz = Class.new { }
|
183
|
+
clazz.new.behaves_as?(:gen_foo).should be_false
|
184
|
+
end
|
185
|
+
|
186
|
+
it 'returns false when the behavior does not exist' do
|
187
|
+
clazz = Class.new { }
|
188
|
+
clazz.new.behaves_as?(:gen_foo).should be_false
|
189
|
+
end
|
190
|
+
|
191
|
+
it 'accepts behavior name as a symbol' do
|
192
|
+
behavior_info(:gen_foo)
|
193
|
+
clazz = Class.new { }
|
194
|
+
clazz.new.behaves_as?(:gen_foo).should be_true
|
195
|
+
end
|
196
|
+
|
197
|
+
it 'accepts behavior name as a string' do
|
198
|
+
behavior_info(:gen_foo)
|
199
|
+
clazz = Class.new { }
|
200
|
+
clazz.new.behaves_as?('gen_foo').should be_true
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
context 'aliases' do
|
205
|
+
|
206
|
+
it 'aliases behaviour_info for behavior_info' do
|
207
|
+
behaviour_info(:gen_foo)
|
208
|
+
clazz = Class.new { }
|
209
|
+
clazz.new.behaves_as?(:gen_foo).should be_true
|
210
|
+
end
|
211
|
+
|
212
|
+
it 'aliases interface for behavior_info' do
|
213
|
+
interface(:gen_foo)
|
214
|
+
clazz = Class.new { }
|
215
|
+
clazz.new.behaves_as?(:gen_foo).should be_true
|
216
|
+
end
|
217
|
+
|
218
|
+
it 'aliases behaviour for behavior' do
|
219
|
+
behavior_info(:gen_foo, foo: 0)
|
220
|
+
clazz = Class.new {
|
221
|
+
behaviour(:gen_foo)
|
222
|
+
def foo() nil; end
|
223
|
+
}
|
224
|
+
clazz.new.behaves_as?(:gen_foo).should be_true
|
225
|
+
end
|
226
|
+
|
227
|
+
it 'aliases behaves_as for behavior' do
|
228
|
+
behavior_info(:gen_foo, foo: 0)
|
229
|
+
clazz = Class.new {
|
230
|
+
behaves_as :gen_foo
|
231
|
+
def foo() nil; end
|
232
|
+
}
|
233
|
+
clazz.new.behaves_as?(:gen_foo).should be_true
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
end
|