functional-ruby 0.5.0 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +154 -562
- data/lib/functional/agent.rb +130 -0
- data/lib/functional/all.rb +9 -1
- data/lib/functional/behavior.rb +72 -39
- data/lib/functional/cached_thread_pool.rb +122 -0
- data/lib/functional/concurrency.rb +32 -24
- data/lib/functional/core.rb +2 -62
- data/lib/functional/event.rb +53 -0
- data/lib/functional/event_machine_defer_proxy.rb +23 -0
- data/lib/functional/fixed_thread_pool.rb +89 -0
- data/lib/functional/future.rb +42 -0
- data/lib/functional/global_thread_pool.rb +3 -0
- data/lib/functional/obligation.rb +121 -0
- data/lib/functional/promise.rb +194 -0
- data/lib/functional/thread_pool.rb +61 -0
- data/lib/functional/utilities.rb +114 -0
- data/lib/functional/version.rb +1 -1
- data/lib/functional.rb +1 -0
- data/lib/functional_ruby.rb +1 -0
- data/md/behavior.md +147 -0
- data/md/concurrency.md +465 -0
- data/md/future.md +32 -0
- data/md/obligation.md +32 -0
- data/md/pattern_matching.md +512 -0
- data/md/promise.md +220 -0
- data/md/utilities.md +53 -0
- data/spec/functional/agent_spec.rb +405 -0
- data/spec/functional/behavior_spec.rb +12 -33
- data/spec/functional/cached_thread_pool_spec.rb +112 -0
- data/spec/functional/concurrency_spec.rb +55 -0
- data/spec/functional/event_machine_defer_proxy_spec.rb +246 -0
- data/spec/functional/event_spec.rb +114 -0
- data/spec/functional/fixed_thread_pool_spec.rb +84 -0
- data/spec/functional/future_spec.rb +115 -0
- data/spec/functional/obligation_shared.rb +121 -0
- data/spec/functional/pattern_matching_spec.rb +10 -8
- data/spec/functional/promise_spec.rb +310 -0
- data/spec/functional/thread_pool_shared.rb +209 -0
- data/spec/functional/utilities_spec.rb +149 -0
- data/spec/spec_helper.rb +2 -0
- metadata +55 -5
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
3
|
+
require 'functional/obligation'
|
4
|
+
require 'functional/global_thread_pool'
|
5
|
+
|
6
|
+
module Functional
|
7
|
+
|
8
|
+
class Future
|
9
|
+
include Obligation
|
10
|
+
behavior(:future)
|
11
|
+
|
12
|
+
def initialize(*args)
|
13
|
+
|
14
|
+
unless block_given?
|
15
|
+
@state = :fulfilled
|
16
|
+
else
|
17
|
+
@value = nil
|
18
|
+
@state = :pending
|
19
|
+
$GLOBAL_THREAD_POOL.post do
|
20
|
+
semaphore.synchronize do
|
21
|
+
Thread.pass
|
22
|
+
begin
|
23
|
+
@value = yield(*args)
|
24
|
+
@state = :fulfilled
|
25
|
+
rescue Exception => ex
|
26
|
+
@state = :rejected
|
27
|
+
@reason = ex
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
module Kernel
|
37
|
+
|
38
|
+
def future(*args, &block)
|
39
|
+
return Functional::Future.new(*args, &block)
|
40
|
+
end
|
41
|
+
module_function :future
|
42
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require 'timeout'
|
3
|
+
|
4
|
+
require 'functional/behavior'
|
5
|
+
|
6
|
+
behavior_info(:future,
|
7
|
+
state: 0,
|
8
|
+
value: -1,
|
9
|
+
reason: 0,
|
10
|
+
pending?: 0,
|
11
|
+
fulfilled?: 0,
|
12
|
+
rejected?: 0)
|
13
|
+
|
14
|
+
behavior_info(:promise,
|
15
|
+
state: 0,
|
16
|
+
value: -1,
|
17
|
+
reason: 0,
|
18
|
+
pending?: 0,
|
19
|
+
fulfilled?: 0,
|
20
|
+
rejected?: 0,
|
21
|
+
then: 0,
|
22
|
+
rescue: -1)
|
23
|
+
|
24
|
+
module Functional
|
25
|
+
|
26
|
+
module Obligation
|
27
|
+
|
28
|
+
attr_reader :state
|
29
|
+
attr_reader :reason
|
30
|
+
|
31
|
+
# Has the obligation been fulfilled?
|
32
|
+
# @return [Boolean]
|
33
|
+
def fulfilled?() return(@state == :fulfilled); end
|
34
|
+
alias_method :realized?, :fulfilled?
|
35
|
+
|
36
|
+
# Is obligation completion still pending?
|
37
|
+
# @return [Boolean]
|
38
|
+
def pending?() return(!(fulfilled? || rejected?)); end
|
39
|
+
|
40
|
+
def value(timeout = nil)
|
41
|
+
if !pending? || timeout == 0
|
42
|
+
return @value
|
43
|
+
elsif timeout.nil?
|
44
|
+
return semaphore.synchronize { @value }
|
45
|
+
else
|
46
|
+
begin
|
47
|
+
return Timeout::timeout(timeout.to_f) {
|
48
|
+
semaphore.synchronize { @value }
|
49
|
+
}
|
50
|
+
rescue Timeout::Error => ex
|
51
|
+
return nil
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
alias_method :deref, :value
|
56
|
+
|
57
|
+
# Has the promise been rejected?
|
58
|
+
# @return [Boolean]
|
59
|
+
def rejected?() return(@state == :rejected); end
|
60
|
+
|
61
|
+
protected
|
62
|
+
|
63
|
+
def semaphore
|
64
|
+
@semaphore ||= Mutex.new
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
module Kernel
|
70
|
+
|
71
|
+
def deref(obligation, timeout = nil)
|
72
|
+
if obligation.respond_to?(:deref)
|
73
|
+
return obligation.deref(timeout)
|
74
|
+
elsif obligation.respond_to?(:value)
|
75
|
+
return obligation.deref(timeout)
|
76
|
+
else
|
77
|
+
return nil
|
78
|
+
end
|
79
|
+
end
|
80
|
+
module_function :deref
|
81
|
+
|
82
|
+
def pending?(obligation)
|
83
|
+
if obligation.respond_to?(:pending?)
|
84
|
+
return obligation.pending?
|
85
|
+
else
|
86
|
+
return false
|
87
|
+
end
|
88
|
+
end
|
89
|
+
module_function :pending?
|
90
|
+
|
91
|
+
def fulfilled?(obligation)
|
92
|
+
if obligation.respond_to?(:fulfilled?)
|
93
|
+
return obligation.fulfilled?
|
94
|
+
elsif obligation.respond_to?(:realized?)
|
95
|
+
return obligation.realized?
|
96
|
+
else
|
97
|
+
return false
|
98
|
+
end
|
99
|
+
end
|
100
|
+
module_function :fulfilled?
|
101
|
+
|
102
|
+
def realized?(obligation)
|
103
|
+
if obligation.respond_to?(:realized?)
|
104
|
+
return obligation.realized?
|
105
|
+
elsif obligation.respond_to?(:fulfilled?)
|
106
|
+
return obligation.fulfilled?
|
107
|
+
else
|
108
|
+
return false
|
109
|
+
end
|
110
|
+
end
|
111
|
+
module_function :realized?
|
112
|
+
|
113
|
+
def rejected?(obligation)
|
114
|
+
if obligation.respond_to?(:rejected?)
|
115
|
+
return obligation.rejected?
|
116
|
+
else
|
117
|
+
return false
|
118
|
+
end
|
119
|
+
end
|
120
|
+
module_function :rejected?
|
121
|
+
end
|
@@ -0,0 +1,194 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
3
|
+
require 'functional/obligation'
|
4
|
+
require 'functional/global_thread_pool'
|
5
|
+
|
6
|
+
module Functional
|
7
|
+
|
8
|
+
class Promise
|
9
|
+
include Obligation
|
10
|
+
behavior(:future)
|
11
|
+
behavior(:promise)
|
12
|
+
|
13
|
+
# Creates a new promise object. "A promise represents the eventual
|
14
|
+
# value returned from the single completion of an operation."
|
15
|
+
# Promises can be chained in a tree structure where each promise
|
16
|
+
# has zero or more children. Promises are resolved asynchronously
|
17
|
+
# in the order they are added to the tree. Parents are guaranteed
|
18
|
+
# to be resolved before their children. The result of each promise
|
19
|
+
# is passed to each of its children upon resolution. When
|
20
|
+
# a promise is rejected all its children will be summarily rejected.
|
21
|
+
# A promise that is neither resolved or rejected is pending.
|
22
|
+
#
|
23
|
+
# @param args [Array] zero or more arguments for the block
|
24
|
+
# @param block [Proc] the block to call when attempting fulfillment
|
25
|
+
#
|
26
|
+
# @see http://wiki.commonjs.org/wiki/Promises/A
|
27
|
+
# @see http://promises-aplus.github.io/promises-spec/
|
28
|
+
def initialize(*args, &block)
|
29
|
+
if args.first.is_a?(Promise)
|
30
|
+
@parent = args.first
|
31
|
+
else
|
32
|
+
@parent = nil
|
33
|
+
@chain = [self]
|
34
|
+
end
|
35
|
+
|
36
|
+
@mutex = Mutex.new
|
37
|
+
@handler = block || Proc.new{|result| result }
|
38
|
+
@state = :pending
|
39
|
+
@value = nil
|
40
|
+
@reason = nil
|
41
|
+
@children = []
|
42
|
+
@rescuers = []
|
43
|
+
|
44
|
+
realize(*args) if root?
|
45
|
+
end
|
46
|
+
|
47
|
+
# Create a new child Promise. The block argument for the child will
|
48
|
+
# be the result of fulfilling its parent. If the child will
|
49
|
+
# immediately be rejected if the parent has already been rejected.
|
50
|
+
#
|
51
|
+
# @param block [Proc] the block to call when attempting fulfillment
|
52
|
+
#
|
53
|
+
# @return [Promise] the new promise
|
54
|
+
def then(&block)
|
55
|
+
child = @mutex.synchronize do
|
56
|
+
block = Proc.new{|result| result } unless block_given?
|
57
|
+
@children << Promise.new(self, &block)
|
58
|
+
@children.last.on_reject(@reason) if rejected?
|
59
|
+
push(@children.last)
|
60
|
+
@children.last
|
61
|
+
end
|
62
|
+
return child
|
63
|
+
end
|
64
|
+
|
65
|
+
# Add a rescue handler to be run if the promise is rejected (via raised
|
66
|
+
# exception). Multiple rescue handlers may be added to a Promise.
|
67
|
+
# Rescue blocks will be checked in order and the first one with a
|
68
|
+
# matching Exception class will be processed. The block argument
|
69
|
+
# will be the exception that caused the rejection.
|
70
|
+
#
|
71
|
+
# @param clazz [Class] The class of exception to rescue
|
72
|
+
# @param block [Proc] the block to call if the rescue is matched
|
73
|
+
#
|
74
|
+
# @return [self] so that additional chaining can occur
|
75
|
+
def rescue(clazz = Exception, &block)
|
76
|
+
@mutex.synchronize do
|
77
|
+
@rescuers << Rescuer.new(clazz, block) if block_given?
|
78
|
+
end
|
79
|
+
return self
|
80
|
+
end
|
81
|
+
alias_method :catch, :rescue
|
82
|
+
alias_method :on_error, :rescue
|
83
|
+
|
84
|
+
protected
|
85
|
+
|
86
|
+
attr_reader :parent
|
87
|
+
attr_reader :handler
|
88
|
+
attr_reader :rescuers
|
89
|
+
|
90
|
+
# @private
|
91
|
+
Rescuer = Struct.new(:clazz, :block)
|
92
|
+
|
93
|
+
# @private
|
94
|
+
def root # :nodoc:
|
95
|
+
current = self
|
96
|
+
current = current.parent until current.root?
|
97
|
+
return current
|
98
|
+
end
|
99
|
+
|
100
|
+
# @private
|
101
|
+
def root? # :nodoc:
|
102
|
+
@parent.nil?
|
103
|
+
end
|
104
|
+
|
105
|
+
# @private
|
106
|
+
def push(promise) # :nodoc:
|
107
|
+
if root?
|
108
|
+
@chain << promise
|
109
|
+
else
|
110
|
+
@parent.push(promise)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# @private
|
115
|
+
def on_fulfill(value) # :nodoc:
|
116
|
+
@mutex.synchronize do
|
117
|
+
if pending?
|
118
|
+
@value = @handler.call(value)
|
119
|
+
@state = :fulfilled
|
120
|
+
@reason = nil
|
121
|
+
end
|
122
|
+
end
|
123
|
+
return @value
|
124
|
+
end
|
125
|
+
|
126
|
+
# @private
|
127
|
+
def on_reject(reason) # :nodoc:
|
128
|
+
@mutex.synchronize do
|
129
|
+
if pending?
|
130
|
+
@state = :rejected
|
131
|
+
@reason = reason
|
132
|
+
self.try_rescue(reason)
|
133
|
+
@value = nil
|
134
|
+
end
|
135
|
+
@children.each{|child| child.on_reject(reason) }
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
# @private
|
140
|
+
def try_rescue(ex) # :nodoc:
|
141
|
+
rescuer = @rescuers.find{|r| ex.is_a?(r.clazz) }
|
142
|
+
rescuer.block.call(ex) if rescuer
|
143
|
+
rescue Exception => e
|
144
|
+
# supress
|
145
|
+
end
|
146
|
+
|
147
|
+
# @private
|
148
|
+
def realize(*args) # :nodoc:
|
149
|
+
$GLOBAL_THREAD_POOL.post(@chain, @mutex, args) do |chain, mutex, args|
|
150
|
+
result = args.length == 1 ? args.first : args
|
151
|
+
index = 0
|
152
|
+
loop do
|
153
|
+
Thread.pass
|
154
|
+
current = mutex.synchronize{ chain[index] }
|
155
|
+
unless current.rejected?
|
156
|
+
current.semaphore.synchronize do
|
157
|
+
begin
|
158
|
+
result = current.on_fulfill(result)
|
159
|
+
rescue Exception => ex
|
160
|
+
current.on_reject(ex)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
index += 1
|
165
|
+
sleep while index >= chain.length
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
module Kernel
|
173
|
+
|
174
|
+
# Creates a new promise object. "A promise represents the eventual
|
175
|
+
# value returned from the single completion of an operation."
|
176
|
+
# Promises can be chained in a tree structure where each promise
|
177
|
+
# has zero or more children. Promises are resolved asynchronously
|
178
|
+
# in the order they are added to the tree. Parents are guaranteed
|
179
|
+
# to be resolved before their children. The result of each promise
|
180
|
+
# is passes to each of its children when the child resolves. When
|
181
|
+
# a promise is rejected all its children will be summarily rejected.
|
182
|
+
# A promise added to a rejected promise will immediately be rejected.
|
183
|
+
# A promise that is neither resolved or rejected is pending.
|
184
|
+
#
|
185
|
+
# @param args [Array] zero or more arguments for the block
|
186
|
+
# @param block [Proc] the block to call when attempting fulfillment
|
187
|
+
#
|
188
|
+
# @see Promise
|
189
|
+
# @see http://wiki.commonjs.org/wiki/Promises/A
|
190
|
+
def promise(*args, &block)
|
191
|
+
return Functional::Promise.new(*args, &block)
|
192
|
+
end
|
193
|
+
module_function :promise
|
194
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'functional/behavior'
|
2
|
+
require 'functional/event'
|
3
|
+
|
4
|
+
behavior_info(:thread_pool,
|
5
|
+
running?: 0,
|
6
|
+
shutdown?: 0,
|
7
|
+
killed?: 0,
|
8
|
+
shutdown: 0,
|
9
|
+
kill: 0,
|
10
|
+
size: 0,
|
11
|
+
wait_for_termination: -1,
|
12
|
+
post: -1,
|
13
|
+
:<< => 1,
|
14
|
+
status: 0)
|
15
|
+
|
16
|
+
behavior_info(:global_thread_pool,
|
17
|
+
post: -1,
|
18
|
+
:<< => 1)
|
19
|
+
|
20
|
+
module Functional
|
21
|
+
|
22
|
+
class ThreadPool
|
23
|
+
|
24
|
+
def initialize
|
25
|
+
@status = :running
|
26
|
+
@queue = Queue.new
|
27
|
+
@termination = Event.new
|
28
|
+
@pool = []
|
29
|
+
end
|
30
|
+
|
31
|
+
def running?
|
32
|
+
return @status == :running
|
33
|
+
end
|
34
|
+
|
35
|
+
def shutdown?
|
36
|
+
return ! running?
|
37
|
+
end
|
38
|
+
|
39
|
+
def killed?
|
40
|
+
return @status == :killed
|
41
|
+
end
|
42
|
+
|
43
|
+
def shutdown
|
44
|
+
@pool.size.times{ @queue << :stop }
|
45
|
+
@status = :shuttingdown
|
46
|
+
end
|
47
|
+
|
48
|
+
def wait_for_termination(timeout = nil)
|
49
|
+
if shutdown? || killed?
|
50
|
+
return true
|
51
|
+
else
|
52
|
+
return @termination.wait(timeout)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def <<(block)
|
57
|
+
self.post(&block)
|
58
|
+
return self
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
require 'pp'
|
2
|
+
require 'stringio'
|
3
|
+
require 'erb'
|
4
|
+
|
5
|
+
Infinity = 1/0.0 unless defined?(Infinity)
|
6
|
+
NaN = 0/0.0 unless defined?(NaN)
|
7
|
+
|
8
|
+
module Kernel
|
9
|
+
|
10
|
+
# Compute the difference (delta) between two values.
|
11
|
+
#
|
12
|
+
# When a block is given the block will be applied to both arguments.
|
13
|
+
# Using a block in this way allows computation against a specific field
|
14
|
+
# in a data set of hashes or objects.
|
15
|
+
#
|
16
|
+
# @yield iterates over each element in the data set
|
17
|
+
# @yieldparam item each element in the data set
|
18
|
+
#
|
19
|
+
# @param [Object] v1 the first value
|
20
|
+
# @param [Object] v2 the second value
|
21
|
+
#
|
22
|
+
# @return [Float] positive value representing the difference
|
23
|
+
# between the two parameters
|
24
|
+
def delta(v1, v2)
|
25
|
+
if block_given?
|
26
|
+
v1 = yield(v1)
|
27
|
+
v2 = yield(v2)
|
28
|
+
end
|
29
|
+
return (v1 - v2).abs
|
30
|
+
end
|
31
|
+
module_function :delta
|
32
|
+
|
33
|
+
# Sandbox the given operation at a high $SAFE level.
|
34
|
+
#
|
35
|
+
# @param args [Array] zero or more arguments to pass to the block
|
36
|
+
# @param block [Proc] the block to isolate
|
37
|
+
#
|
38
|
+
# @return [Object] the result of the block operation
|
39
|
+
def safe(*args)
|
40
|
+
raise ArgumentError.new('no block given') unless block_given?
|
41
|
+
result = nil
|
42
|
+
t = Thread.new do
|
43
|
+
$SAFE = 3
|
44
|
+
result = yield(*args)
|
45
|
+
end
|
46
|
+
t.join
|
47
|
+
return result
|
48
|
+
end
|
49
|
+
module_function :safe
|
50
|
+
|
51
|
+
# Open a file, read it, close the file, and return its contents.
|
52
|
+
#
|
53
|
+
# @param file [String] path to and name of the file to open
|
54
|
+
# @return [String] file contents
|
55
|
+
#
|
56
|
+
# @see slurpee
|
57
|
+
def slurp(file)
|
58
|
+
File.open(file, 'rb') {|f| f.read }
|
59
|
+
end
|
60
|
+
module_function :slurp
|
61
|
+
|
62
|
+
# Open a file, read it, close the file, run the contents through the
|
63
|
+
# ERB parser, and return updated contents.
|
64
|
+
#
|
65
|
+
# @param file [String] path to and name of the file to open
|
66
|
+
# @param safe_level [Integer] when not nil, ERB will $SAFE set to this
|
67
|
+
# @return [String] file contents
|
68
|
+
#
|
69
|
+
# @see slurpee
|
70
|
+
def slurpee(file, safe_level = nil)
|
71
|
+
ERB.new(slurp(file), safe_level).result
|
72
|
+
end
|
73
|
+
module_function :slurpee
|
74
|
+
|
75
|
+
#############################################################################
|
76
|
+
|
77
|
+
# @private
|
78
|
+
def repl? # :nodoc:
|
79
|
+
return ($0 == 'irb' || $0 == 'pry' || $0 == 'script/rails' || !!($0 =~ /bin\/bundle$/))
|
80
|
+
end
|
81
|
+
module_function :repl?
|
82
|
+
|
83
|
+
# @private
|
84
|
+
def timestamp # :nodoc:
|
85
|
+
return Time.now.getutc.to_i
|
86
|
+
end
|
87
|
+
|
88
|
+
# @private
|
89
|
+
def timer(*args) # :nodoc:
|
90
|
+
t1 = Time.now
|
91
|
+
result = yield(*args)
|
92
|
+
t2 = Time.now
|
93
|
+
return (t2 - t1)
|
94
|
+
end
|
95
|
+
module_function :timer
|
96
|
+
|
97
|
+
# @private
|
98
|
+
def strftimer(seconds) # :nodoc:
|
99
|
+
Time.at(seconds).gmtime.strftime('%R:%S.%L')
|
100
|
+
end
|
101
|
+
module_function :strftimer
|
102
|
+
|
103
|
+
# @private
|
104
|
+
# @see http://rhaseventh.blogspot.com/2008/07/ruby-and-rails-how-to-get-pp-pretty.html
|
105
|
+
def pp_s(*objs) # :nodoc:
|
106
|
+
s = StringIO.new
|
107
|
+
objs.each {|obj|
|
108
|
+
PP.pp(obj, s)
|
109
|
+
}
|
110
|
+
s.rewind
|
111
|
+
s.read
|
112
|
+
end
|
113
|
+
module_function :pp_s
|
114
|
+
end
|
data/lib/functional/version.rb
CHANGED
data/lib/functional.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'functional/all'
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'functional/all'
|
data/md/behavior.md
ADDED
@@ -0,0 +1,147 @@
|
|
1
|
+
# For good -behavior(timeoff).
|
2
|
+
|
3
|
+
One of Ruby's greatest strengths is [duck typing](http://rubylearning.com/satishtalim/duck_typing.html).
|
4
|
+
Usually this is awesome and I'm happy to not have to deal with static typing and the compiler. Usually.
|
5
|
+
The problem with duck typing is that is is impossible in Ruby to enforce an interface definition.
|
6
|
+
I would never advocate turning Ruby into the cesspool complex object creation that Java has
|
7
|
+
unfortunately become, but occasionally it would be nice to make sure a class implements a set of
|
8
|
+
required methods. Enter Erlang's [-behavior](http://metajack.im/2008/10/29/custom-behaviors-in-erlang/)
|
9
|
+
keyword. Basically, you define a `behavior_info` then drop a `behavior` call within a class.
|
10
|
+
Forget to implement a required method and Ruby will let you know. See the examples below for details.
|
11
|
+
|
12
|
+
## Usage
|
13
|
+
|
14
|
+
The `behavior` functionality is not imported by default. It needs a separate `require` statement:
|
15
|
+
|
16
|
+
```ruby
|
17
|
+
require 'functional/behavior'
|
18
|
+
|
19
|
+
# -or-
|
20
|
+
|
21
|
+
require 'functional/behaviour'
|
22
|
+
```
|
23
|
+
|
24
|
+
### behavior_info
|
25
|
+
|
26
|
+
Next, declare a behavior using the `behavior_info` function (this function should sit outside
|
27
|
+
of any module/class definition, but will probably work regardless). The first parameter to
|
28
|
+
`behavior_info` (or `behaviour_info`) is a symbol name for the behavior. The remaining parameter
|
29
|
+
is a hash of function names and their arity:
|
30
|
+
|
31
|
+
```ruby
|
32
|
+
behaviour_info(:gen_foo, foo: 0, bar: 1, baz: 2)
|
33
|
+
|
34
|
+
# -or (for the Java/C# crowd)
|
35
|
+
|
36
|
+
interface(:gen_foo, foo: 0, bar: 1, baz: 2)
|
37
|
+
|
38
|
+
```
|
39
|
+
|
40
|
+
Each function name can be listed only once and the arity must follow the rules of the
|
41
|
+
[Method#arity](http://ruby-doc.org/core-1.9.3/Method.html#method-i-arity) function.
|
42
|
+
Though not explicitly documented, block arguments do not count toward a method's arity.
|
43
|
+
methods defined using this gem's `defn` function will always have an arity of -1,
|
44
|
+
regardless of how many overloads are defined.
|
45
|
+
|
46
|
+
### behavior
|
47
|
+
|
48
|
+
To enforce a behavior on a class simply call the `behavior` function within the class,
|
49
|
+
passing the name of the desired behavior:
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
class Foo
|
53
|
+
behavior(:gen_foo)
|
54
|
+
...
|
55
|
+
end
|
56
|
+
|
57
|
+
# or use the idiomatic Erlang spelling
|
58
|
+
class Bar
|
59
|
+
behaviour(:gen_foo)
|
60
|
+
...
|
61
|
+
end
|
62
|
+
|
63
|
+
# or use the idiomatic Rails syntax
|
64
|
+
class Baz
|
65
|
+
behaves_as :gen_foo
|
66
|
+
...
|
67
|
+
end
|
68
|
+
```
|
69
|
+
|
70
|
+
Make sure you the implement the required methods in your class. If you don't, Ruby will
|
71
|
+
raise an exception when you try to create an object from the class:
|
72
|
+
|
73
|
+
```ruby
|
74
|
+
Baz.new #=> ArgumentError: undefined callback functions in Baz (behavior 'gen_foo')
|
75
|
+
```
|
76
|
+
|
77
|
+
### behaves_as?
|
78
|
+
|
79
|
+
As an added bonus, Ruby [Object](http://ruby-doc.org/core-1.9.3/Object.html) will be
|
80
|
+
monkey-patched with a `behaves_as?` predicate method.
|
81
|
+
|
82
|
+
## Example
|
83
|
+
|
84
|
+
A complete example:
|
85
|
+
|
86
|
+
```ruby
|
87
|
+
behaviour_info(:gen_foo, foo: 0, bar: 1, baz: 2, boom: -1, bam: :any)
|
88
|
+
|
89
|
+
class Foo
|
90
|
+
behavior(:gen_foo)
|
91
|
+
|
92
|
+
def foo
|
93
|
+
return 'foo/0'
|
94
|
+
end
|
95
|
+
|
96
|
+
def bar(one, &block)
|
97
|
+
return 'bar/1'
|
98
|
+
end
|
99
|
+
|
100
|
+
def baz(one, two)
|
101
|
+
return 'baz/2'
|
102
|
+
end
|
103
|
+
|
104
|
+
def boom(*args)
|
105
|
+
return 'boom/-1'
|
106
|
+
end
|
107
|
+
|
108
|
+
def bam
|
109
|
+
return 'bam!'
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
foo = Foo.new
|
114
|
+
|
115
|
+
foo.behaves_as? :gen_foo #=> true
|
116
|
+
foo.behaves_as?(:bogus) #=> false
|
117
|
+
'foo'.behaves_as? :gen_foo #=> false
|
118
|
+
```
|
119
|
+
|
120
|
+
## Copyright
|
121
|
+
|
122
|
+
*Functional Ruby* is Copyright © 2013 [Jerry D'Antonio](https://twitter.com/jerrydantonio).
|
123
|
+
It is free software and may be redistributed under the terms specified in the LICENSE file.
|
124
|
+
|
125
|
+
## License
|
126
|
+
|
127
|
+
Released under the MIT license.
|
128
|
+
|
129
|
+
http://www.opensource.org/licenses/mit-license.php
|
130
|
+
|
131
|
+
> Permission is hereby granted, free of charge, to any person obtaining a copy
|
132
|
+
> of this software and associated documentation files (the "Software"), to deal
|
133
|
+
> in the Software without restriction, including without limitation the rights
|
134
|
+
> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
135
|
+
> copies of the Software, and to permit persons to whom the Software is
|
136
|
+
> furnished to do so, subject to the following conditions:
|
137
|
+
>
|
138
|
+
> The above copyright notice and this permission notice shall be included in
|
139
|
+
> all copies or substantial portions of the Software.
|
140
|
+
>
|
141
|
+
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
142
|
+
> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
143
|
+
> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
144
|
+
> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
145
|
+
> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
146
|
+
> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
147
|
+
> THE SOFTWARE.
|