functional-ruby 0.7.7 → 1.0.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 +92 -152
- data/doc/memo.txt +192 -0
- data/doc/pattern_matching.txt +485 -0
- data/doc/protocol.txt +221 -0
- data/doc/record.txt +144 -0
- data/doc/thread_safety.txt +8 -0
- data/lib/functional.rb +48 -18
- data/lib/functional/abstract_struct.rb +161 -0
- data/lib/functional/delay.rb +117 -0
- data/lib/functional/either.rb +222 -0
- data/lib/functional/memo.rb +93 -0
- data/lib/functional/method_signature.rb +72 -0
- data/lib/functional/option.rb +209 -0
- data/lib/functional/pattern_matching.rb +117 -100
- data/lib/functional/protocol.rb +157 -0
- data/lib/functional/protocol_info.rb +193 -0
- data/lib/functional/record.rb +155 -0
- data/lib/functional/type_check.rb +112 -0
- data/lib/functional/union.rb +152 -0
- data/lib/functional/version.rb +3 -1
- data/spec/functional/abstract_struct_shared.rb +154 -0
- data/spec/functional/complex_pattern_matching_spec.rb +205 -0
- data/spec/functional/configuration_spec.rb +17 -0
- data/spec/functional/delay_spec.rb +147 -0
- data/spec/functional/either_spec.rb +237 -0
- data/spec/functional/memo_spec.rb +207 -0
- data/spec/functional/option_spec.rb +292 -0
- data/spec/functional/pattern_matching_spec.rb +279 -276
- data/spec/functional/protocol_info_spec.rb +444 -0
- data/spec/functional/protocol_spec.rb +274 -0
- data/spec/functional/record_spec.rb +175 -0
- data/spec/functional/type_check_spec.rb +103 -0
- data/spec/functional/union_spec.rb +110 -0
- data/spec/spec_helper.rb +6 -4
- metadata +55 -45
- data/lib/functional/behavior.rb +0 -138
- data/lib/functional/behaviour.rb +0 -2
- data/lib/functional/catalog.rb +0 -487
- data/lib/functional/collection.rb +0 -403
- data/lib/functional/inflect.rb +0 -127
- data/lib/functional/platform.rb +0 -120
- data/lib/functional/search.rb +0 -132
- data/lib/functional/sort.rb +0 -41
- data/lib/functional/utilities.rb +0 -189
- data/md/behavior.md +0 -188
- data/md/catalog.md +0 -32
- data/md/collection.md +0 -32
- data/md/inflect.md +0 -32
- data/md/pattern_matching.md +0 -512
- data/md/platform.md +0 -32
- data/md/search.md +0 -32
- data/md/sort.md +0 -32
- data/md/utilities.md +0 -55
- data/spec/functional/behavior_spec.rb +0 -528
- data/spec/functional/catalog_spec.rb +0 -1206
- data/spec/functional/collection_spec.rb +0 -752
- data/spec/functional/inflect_spec.rb +0 -85
- data/spec/functional/integration_spec.rb +0 -205
- data/spec/functional/platform_spec.rb +0 -501
- data/spec/functional/search_spec.rb +0 -187
- data/spec/functional/sort_spec.rb +0 -61
- data/spec/functional/utilities_spec.rb +0 -277
@@ -0,0 +1,93 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
3
|
+
module Functional
|
4
|
+
|
5
|
+
# Memoization is a technique for optimizing functions that are time-consuming
|
6
|
+
# and/or involve expensive calculations. Every time a memoized function is
|
7
|
+
# called the result is caches with reference to the given parameters.
|
8
|
+
# Subsequent calls to the function that use the same parameters will return
|
9
|
+
# the cached result. As a result the response time for frequently called
|
10
|
+
# functions is vastly incresed (after the first call with any given set of)
|
11
|
+
# arguments, at the cost of increased memory usage (the cache).
|
12
|
+
#
|
13
|
+
# @!macro memoize
|
14
|
+
#
|
15
|
+
# @note Memoized method calls are thread safe and can safely be used in concurrent systems.
|
16
|
+
# Declaring memoization on a function is *not* thread safe and should only be done during
|
17
|
+
# application initialization.
|
18
|
+
module Memo
|
19
|
+
|
20
|
+
def self.extended(base)
|
21
|
+
base.extend(ClassMethods)
|
22
|
+
base.send(:__method_memos__=, {})
|
23
|
+
super(base)
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.included(base)
|
27
|
+
base.extend(ClassMethods)
|
28
|
+
base.send(:__method_memos__=, {})
|
29
|
+
super(base)
|
30
|
+
end
|
31
|
+
|
32
|
+
# @!visibility private
|
33
|
+
module ClassMethods
|
34
|
+
|
35
|
+
# @!visibility private
|
36
|
+
Memo = Struct.new(:function, :mutex, :cache, :max_cache) do
|
37
|
+
def max_cache?
|
38
|
+
max_cache > 0 && cache.size >= max_cache
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# @!visibility private
|
43
|
+
attr_accessor :__method_memos__
|
44
|
+
|
45
|
+
# Returns a memoized version of a referentially transparent function. The
|
46
|
+
# memoized version of the function keeps a cache of the mapping from arguments
|
47
|
+
# to results and, when calls with the same arguments are repeated often, has
|
48
|
+
# higher performance at the expense of higher memory use.
|
49
|
+
#
|
50
|
+
# @param [Symbol] func the class/module function to memoize
|
51
|
+
# @param [Hash] opts the options controlling memoization
|
52
|
+
# @option opts [Fixnum] :at_most the maximum number of memos to store in the
|
53
|
+
# cache; a value of zero (the default) or `nil` indicates no limit
|
54
|
+
#
|
55
|
+
# @raise [ArgumentError] when the method has already been memoized
|
56
|
+
# @raise [ArgumentError] when :at_most option is a negative number
|
57
|
+
def memoize(func, opts = {})
|
58
|
+
func = func.to_sym
|
59
|
+
max_cache = opts[:at_most].to_i
|
60
|
+
raise ArgumentError.new("method :#{func} has already been memoized") if __method_memos__.has_key?(func)
|
61
|
+
raise ArgumentError.new(':max_cache must be > 0') if max_cache < 0
|
62
|
+
__method_memos__[func] = Memo.new(method(func), Mutex.new, {}, max_cache.to_i)
|
63
|
+
__define_memo_proxy__(func)
|
64
|
+
nil
|
65
|
+
end
|
66
|
+
|
67
|
+
# @!visibility private
|
68
|
+
def __define_memo_proxy__(func)
|
69
|
+
self.class_eval <<-RUBY
|
70
|
+
def self.#{func}(*args, &block)
|
71
|
+
self.__proxy_memoized_method__(:#{func}, *args, &block)
|
72
|
+
end
|
73
|
+
RUBY
|
74
|
+
end
|
75
|
+
|
76
|
+
# @!visibility private
|
77
|
+
def __proxy_memoized_method__(func, *args, &block)
|
78
|
+
memo = self.__method_memos__[func]
|
79
|
+
memo.mutex.lock
|
80
|
+
if block_given?
|
81
|
+
memo.function.call(*args, &block)
|
82
|
+
elsif memo.cache.has_key?(args)
|
83
|
+
memo.cache[args]
|
84
|
+
else
|
85
|
+
result = memo.function.call(*args)
|
86
|
+
memo.cache[args] = result unless memo.max_cache?
|
87
|
+
end
|
88
|
+
ensure
|
89
|
+
memo.mutex.unlock
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module Functional
|
2
|
+
|
3
|
+
module PatternMatching
|
4
|
+
|
5
|
+
# @!visibility private
|
6
|
+
#
|
7
|
+
# Helper functions used when pattern matching runtime arguments against
|
8
|
+
# a method defined with the `defn` function of Functional::PatternMatching.
|
9
|
+
module MethodSignature
|
10
|
+
extend self
|
11
|
+
|
12
|
+
# Do the given arguments match the given function pattern?
|
13
|
+
#
|
14
|
+
# @return [Boolean] true when there is a match else false
|
15
|
+
def match?(pattern, args)
|
16
|
+
return false unless valid_pattern?(args, pattern)
|
17
|
+
|
18
|
+
pattern.length.times.all? do |index|
|
19
|
+
param = pattern[index]
|
20
|
+
arg = args[index]
|
21
|
+
|
22
|
+
all_param_and_last_arg?(pattern, param, index) ||
|
23
|
+
arg_is_type_of_param?(param, arg) ||
|
24
|
+
hash_param_with_matching_arg?(param, arg) ||
|
25
|
+
param_matches_arg?(param, arg)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Is the given pattern a valid pattern with respect to the given
|
30
|
+
# runtime arguments?
|
31
|
+
#
|
32
|
+
# @return [Boolean] true when the pattern is valid else false
|
33
|
+
def valid_pattern?(args, pattern)
|
34
|
+
(pattern.last == PatternMatching::ALL && args.length >= pattern.length) \
|
35
|
+
|| (args.length == pattern.length)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Is this the last parameter and is it `ALL`?
|
39
|
+
#
|
40
|
+
# @return [Boolean] true when matching else false
|
41
|
+
def all_param_and_last_arg?(pattern, param, index)
|
42
|
+
param == PatternMatching::ALL && index+1 == pattern.length
|
43
|
+
end
|
44
|
+
|
45
|
+
# Is the parameter a class and is the provided argument an instance
|
46
|
+
# of that class?
|
47
|
+
#
|
48
|
+
# @return [Boolean] true when matching else false
|
49
|
+
def arg_is_type_of_param?(param, arg)
|
50
|
+
param.is_a?(Class) && arg.is_a?(param)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Is the given parameter a Hash and does it match the given
|
54
|
+
# runtime argument?
|
55
|
+
#
|
56
|
+
# @return [Boolean] true when matching else false
|
57
|
+
def hash_param_with_matching_arg?(param, arg)
|
58
|
+
param.is_a?(Hash) && arg.is_a?(Hash) && ! param.empty? && param.all? do |key, value|
|
59
|
+
arg.has_key?(key) && (value == PatternMatching::UNBOUND || arg[key] == value)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Does the given parameter exactly match the given runtime
|
64
|
+
# argument or is the parameter `UNBOUND`?
|
65
|
+
#
|
66
|
+
# @return [Boolean] true when matching else false
|
67
|
+
def param_matches_arg?(param, arg)
|
68
|
+
param == PatternMatching::UNBOUND || param == arg
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,209 @@
|
|
1
|
+
require_relative 'abstract_struct'
|
2
|
+
require_relative 'either'
|
3
|
+
require_relative 'protocol'
|
4
|
+
|
5
|
+
Functional::SpecifyProtocol(:Option) do
|
6
|
+
instance_method :some?, 0
|
7
|
+
instance_method :none?, 0
|
8
|
+
instance_method :some, 0
|
9
|
+
end
|
10
|
+
|
11
|
+
module Functional
|
12
|
+
|
13
|
+
# An optional value that may be none (no value) or some (a value).
|
14
|
+
# This type is a replacement for the use of nil with better type checks.
|
15
|
+
# It is an immutable data structure that extends `AbstractStruct`.
|
16
|
+
#
|
17
|
+
# @see Functional::AbstractStruct
|
18
|
+
# @see http://functionaljava.googlecode.com/svn/artifacts/3.0/javadoc/index.html Functional Java
|
19
|
+
#
|
20
|
+
# @!macro thread_safe_immutable_object
|
21
|
+
class Option
|
22
|
+
include AbstractStruct
|
23
|
+
|
24
|
+
# @!visibility private
|
25
|
+
NO_OPTION = Object.new.freeze
|
26
|
+
|
27
|
+
self.datatype = :option
|
28
|
+
self.fields = [:some].freeze
|
29
|
+
|
30
|
+
private_class_method :new
|
31
|
+
|
32
|
+
# The reason for the absence of a value when none,
|
33
|
+
# defaults to nil
|
34
|
+
attr_reader :reason
|
35
|
+
|
36
|
+
class << self
|
37
|
+
|
38
|
+
# Construct an `Option` with no value.
|
39
|
+
#
|
40
|
+
# @return [Option] the new option
|
41
|
+
def none(reason = nil)
|
42
|
+
new(nil, true, reason).freeze
|
43
|
+
end
|
44
|
+
|
45
|
+
# Construct an `Option` with the given value.
|
46
|
+
#
|
47
|
+
# @param [Object] value the value of the option
|
48
|
+
# @return [Option] the new option
|
49
|
+
def some(value)
|
50
|
+
new(value, false).freeze
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Does the option have a value?
|
55
|
+
#
|
56
|
+
# @return [Boolean] true if some else false
|
57
|
+
def some?
|
58
|
+
! none?
|
59
|
+
end
|
60
|
+
alias_method :value?, :some?
|
61
|
+
alias_method :fulfilled?, :some?
|
62
|
+
|
63
|
+
# Is the option absent a value?
|
64
|
+
#
|
65
|
+
# @return [Boolean] true if none else false
|
66
|
+
def none?
|
67
|
+
@none
|
68
|
+
end
|
69
|
+
alias_method :reason?, :none?
|
70
|
+
alias_method :rejected?, :none?
|
71
|
+
|
72
|
+
# The value of this option.
|
73
|
+
#
|
74
|
+
# @return [Object] the value when some else nil
|
75
|
+
def some
|
76
|
+
to_h[:some]
|
77
|
+
end
|
78
|
+
alias_method :value, :some
|
79
|
+
|
80
|
+
# Returns the length of this optional value;
|
81
|
+
# 1 if there is a value, 0 otherwise.
|
82
|
+
#
|
83
|
+
# @return [Fixnum] The length of this optional value;
|
84
|
+
# 1 if there is a value, 0 otherwise.
|
85
|
+
def length
|
86
|
+
none? ? 0 : 1
|
87
|
+
end
|
88
|
+
alias_method :size, :length
|
89
|
+
|
90
|
+
# Perform a logical `and` operation against this option and the
|
91
|
+
# provided option or block. Returns true if this option is some and:
|
92
|
+
#
|
93
|
+
# * other is an `Option` with some value
|
94
|
+
# * other is a truthy value (not nil or false)
|
95
|
+
# * the result of the block is a truthy value
|
96
|
+
#
|
97
|
+
# If a block is given the value of the current option is passed to the
|
98
|
+
# block and the result of block processing will be evaluated for its
|
99
|
+
# truthiness. An exception will be raised if an other value and a
|
100
|
+
# block are both provided.
|
101
|
+
#
|
102
|
+
# @param [Object] other the value to be evaluated against this option
|
103
|
+
# @yieldparam [Object] value the value of this option when some
|
104
|
+
# @return [Boolean] true when the union succeeds else false
|
105
|
+
# @raise [ArgumentError] when given both other and a block
|
106
|
+
def and(other = NO_OPTION)
|
107
|
+
raise ArgumentError.new('cannot give both an option and a block') if other != NO_OPTION && block_given?
|
108
|
+
return false if none?
|
109
|
+
|
110
|
+
if block_given?
|
111
|
+
!! yield(some)
|
112
|
+
elsif Protocol::Satisfy? other, :Option
|
113
|
+
other.some?
|
114
|
+
else
|
115
|
+
!! other
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Perform a logical `or` operation against this option and the
|
120
|
+
# provided option or block. Returns true if this option is some.
|
121
|
+
# If this option is none it returns true if:
|
122
|
+
#
|
123
|
+
# * other is an `Option` with some value
|
124
|
+
# * other is a truthy value (not nil or false)
|
125
|
+
# * the result of the block is a truthy value
|
126
|
+
#
|
127
|
+
# If a block is given the value of the result of block processing
|
128
|
+
# will be evaluated for its truthiness. An exception will be raised
|
129
|
+
# if an other value and a block are both provided.
|
130
|
+
#
|
131
|
+
# @param [Object] other the value to be evaluated against this option
|
132
|
+
# @return [Boolean] true when the intersection succeeds else false
|
133
|
+
# @raise [ArgumentError] when given both other and a block
|
134
|
+
def or(other = NO_OPTION)
|
135
|
+
raise ArgumentError.new('cannot give both an option and a block') if other != NO_OPTION && block_given?
|
136
|
+
return true if some?
|
137
|
+
|
138
|
+
if block_given?
|
139
|
+
!! yield
|
140
|
+
elsif Protocol::Satisfy? other, :Option
|
141
|
+
other.some?
|
142
|
+
else
|
143
|
+
!! other
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
# Returns the value of this option when some else returns the
|
148
|
+
# value of the other option or block. When the other is also an
|
149
|
+
# option its some value is returned. When the other is any other
|
150
|
+
# value it is simply passed through. When a block is provided the
|
151
|
+
# block is processed and the return value of the block is returned.
|
152
|
+
# An exception will be raised if an other value and a block are
|
153
|
+
# both provided.
|
154
|
+
#
|
155
|
+
# @param [Object] other the value to be evaluated when this is none
|
156
|
+
# @return [Object] this value when some else the value of other
|
157
|
+
# @raise [ArgumentError] when given both other and a block
|
158
|
+
def else(other = NO_OPTION)
|
159
|
+
raise ArgumentError.new('cannot give both an option and a block') if other != NO_OPTION && block_given?
|
160
|
+
return some if some?
|
161
|
+
|
162
|
+
if block_given?
|
163
|
+
yield
|
164
|
+
elsif Protocol::Satisfy? other, :Option
|
165
|
+
other.some
|
166
|
+
else
|
167
|
+
other
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
# If the condition satisfies, return the given A in some, otherwise, none.
|
172
|
+
#
|
173
|
+
# @param [Object] value The some value to use if the condition satisfies.
|
174
|
+
# @param [Boolean] condition The condition to test (when no block given).
|
175
|
+
# @yield The condition to test (when no condition given).
|
176
|
+
#
|
177
|
+
# @return [Option] A constructed option based on the given condition.
|
178
|
+
#
|
179
|
+
# @raise [ArgumentError] When both a condition and a block are given.
|
180
|
+
def self.iff(value, condition = NO_OPTION)
|
181
|
+
raise ArgumentError.new('requires either a condition or a block, not both') if condition != NO_OPTION && block_given?
|
182
|
+
condition = block_given? ? yield : !! condition
|
183
|
+
condition ? some(value) : none
|
184
|
+
end
|
185
|
+
|
186
|
+
# @!macro inspect_method
|
187
|
+
def inspect
|
188
|
+
super.gsub(/ :some/, " (#{some? ? 'some' : 'none'}) :some")
|
189
|
+
end
|
190
|
+
alias_method :to_s, :inspect
|
191
|
+
|
192
|
+
private
|
193
|
+
|
194
|
+
# Create a new Option with the given value and disposition.
|
195
|
+
#
|
196
|
+
# @param [Object] value the value of this option
|
197
|
+
# @param [Boolean] none is this option absent a value?
|
198
|
+
# @param [Object] reason the reason for the absense of a value
|
199
|
+
#
|
200
|
+
# @!visibility private
|
201
|
+
def initialize(value, none, reason = nil)
|
202
|
+
@none = none
|
203
|
+
@reason = none ? reason : nil
|
204
|
+
hsh = none ? {some: nil} : {some: value}
|
205
|
+
set_data_hash(hsh)
|
206
|
+
set_values_array(hsh.values)
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
@@ -1,132 +1,149 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
1
|
+
require_relative 'method_signature'
|
2
|
+
|
3
|
+
module Functional
|
4
|
+
|
5
|
+
# As much as I love Ruby I've always been a little disappointed that Ruby doesn't
|
6
|
+
# support function overloading. Function overloading tends to reduce branching
|
7
|
+
# and keep function signatures simpler. No sweat, I learned to do without. Then
|
8
|
+
# I started programming in Erlang. My favorite Erlang feature is, without
|
9
|
+
# question, pattern matching. Pattern matching is like function overloading
|
10
|
+
# cranked to 11. So one day I was musing on Twitter that I'd like to see
|
11
|
+
# Erlang-stype pattern matching in Ruby and one of my friends responded
|
12
|
+
# "Build it!" So I did. And here it is.
|
13
|
+
#
|
14
|
+
# @!macro pattern_matching
|
15
|
+
module PatternMatching
|
16
|
+
|
17
|
+
# A parameter that is required but that can take any value.
|
18
|
+
# @!visibility private
|
19
|
+
UNBOUND = Object.new.freeze
|
20
|
+
|
21
|
+
# A match for one or more parameters in the last position of the match.
|
22
|
+
# @!visibility private
|
23
|
+
ALL = Object.new.freeze
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
# A guard clause on a pattern match.
|
28
|
+
# @!visibility private
|
29
|
+
GuardClause = Class.new do
|
30
|
+
def initialize(function, clazz, pattern)
|
31
|
+
@function = function
|
32
|
+
@clazz = clazz
|
33
|
+
@pattern = pattern
|
17
34
|
end
|
18
|
-
|
19
|
-
|
20
|
-
|
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
|
+
def when(&block)
|
36
|
+
unless block_given?
|
37
|
+
raise ArgumentError.new("block missing for `when` guard on function `#{@function}` of class #{@clazz}")
|
35
38
|
end
|
36
|
-
|
39
|
+
@pattern.guard = block
|
40
|
+
self
|
37
41
|
end
|
38
|
-
return false unless p == UNBOUND || p == arg
|
39
42
|
end
|
40
|
-
return true
|
41
|
-
end
|
42
43
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
44
|
+
# @!visibility private
|
45
|
+
FunctionPattern = Struct.new(:function, :args, :body, :guard)
|
46
|
+
|
47
|
+
# @!visibility private
|
48
|
+
def __unbound_args__(match, args)
|
49
|
+
argv = []
|
50
|
+
match.args.each_with_index do |p, i|
|
51
|
+
if p == ALL && i == match.args.length-1
|
52
|
+
argv << args[(i..args.length)].reduce([]){|memo, arg| memo << arg }
|
53
|
+
elsif p.is_a?(Hash) && p.values.include?(UNBOUND)
|
54
|
+
p.each do |key, value|
|
55
|
+
argv << args[i][key] if value == UNBOUND
|
56
|
+
end
|
57
|
+
elsif p.is_a?(Hash) || p == UNBOUND || p.is_a?(Class)
|
58
|
+
argv << args[i]
|
51
59
|
end
|
52
|
-
elsif p.is_a?(Hash) || p == UNBOUND || p.is_a?(Class)
|
53
|
-
argv << args[i]
|
54
60
|
end
|
61
|
+
argv
|
55
62
|
end
|
56
|
-
return argv
|
57
|
-
end
|
58
|
-
|
59
|
-
def self.__pattern_match__(clazz, func, *args, &block) # :nodoc:
|
60
|
-
args = args.first
|
61
63
|
|
62
|
-
|
63
|
-
|
64
|
+
def __pass_guard__?(matcher, args)
|
65
|
+
matcher.guard.nil? ||
|
66
|
+
self.instance_exec(*__unbound_args__(matcher, args), &matcher.guard)
|
67
|
+
end
|
64
68
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
end
|
69
|
+
# @!visibility private
|
70
|
+
def __pattern_match__(clazz, function, *args, &block)
|
71
|
+
args = args.first
|
72
|
+
matchers = clazz.__function_pattern_matches__.fetch(function, [])
|
73
|
+
matchers.detect do |matcher|
|
74
|
+
MethodSignature.match?(matcher.args, args) && __pass_guard__?(matcher, args)
|
72
75
|
end
|
73
76
|
end
|
74
77
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
def self.included(base)
|
81
|
-
|
82
|
-
class << base
|
78
|
+
def self.included(base)
|
79
|
+
base.extend(ClassMethods)
|
80
|
+
super(base)
|
81
|
+
end
|
83
82
|
|
84
|
-
|
83
|
+
# Class methods added to a class that includes {Functional::PatternMatching}
|
84
|
+
# @!visibility private
|
85
|
+
module ClassMethods
|
85
86
|
|
86
|
-
|
87
|
-
|
87
|
+
# @!visibility private
|
88
|
+
def _()
|
89
|
+
UNBOUND
|
88
90
|
end
|
89
91
|
|
90
|
-
|
92
|
+
# @!visibility private
|
93
|
+
def defn(function, *args, &block)
|
91
94
|
unless block_given?
|
92
|
-
raise ArgumentError.new("block missing for definition of function `#{
|
95
|
+
raise ArgumentError.new("block missing for definition of function `#{function}` on class #{self}")
|
93
96
|
end
|
94
97
|
|
95
|
-
pattern
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
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
|
98
|
+
# add a new pattern for this function
|
99
|
+
pattern = __register_pattern__(function, *args, &block)
|
100
|
+
|
101
|
+
# define the delegator function if it doesn't exist yet
|
102
|
+
unless self.instance_methods(false).include?(function)
|
103
|
+
__define_method_with_matching__(function)
|
113
104
|
end
|
114
105
|
|
115
|
-
return
|
106
|
+
# return a guard clause to be added to the pattern
|
107
|
+
GuardClause.new(function, self, pattern)
|
116
108
|
end
|
117
109
|
|
118
|
-
|
110
|
+
# @!visibility private
|
111
|
+
# define an arity -1 function that dispatches to the appropriate
|
112
|
+
# pattern match variant or raises an exception
|
113
|
+
def __define_method_with_matching__(function)
|
114
|
+
define_method(function) do |*args, &block|
|
115
|
+
begin
|
116
|
+
# get the collection of matched patterns for this function
|
117
|
+
# use owner to ensure we climb the inheritance tree
|
118
|
+
match = __pattern_match__(self.method(function).owner, function, args, block)
|
119
|
+
if match
|
120
|
+
# call the matched function
|
121
|
+
argv = __unbound_args__(match, args)
|
122
|
+
self.instance_exec(*argv, &match.body)
|
123
|
+
else
|
124
|
+
# delegate to the superclass
|
125
|
+
super(*args, &block)
|
126
|
+
end
|
127
|
+
rescue NoMethodError, ArgumentError
|
128
|
+
# raise a custom error
|
129
|
+
raise NoMethodError.new("no method `#{function}` matching #{args} found for class #{self.class}")
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
119
133
|
|
120
|
-
|
134
|
+
# @!visibility private
|
135
|
+
def __function_pattern_matches__
|
121
136
|
@__function_pattern_matches__ ||= Hash.new
|
122
137
|
end
|
123
138
|
|
124
|
-
|
139
|
+
# @!visibility private
|
140
|
+
def __register_pattern__(function, *args, &block)
|
125
141
|
block = Proc.new{} unless block_given?
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
142
|
+
pattern = FunctionPattern.new(function, args, block)
|
143
|
+
patterns = self.__function_pattern_matches__.fetch(function, [])
|
144
|
+
patterns << pattern
|
145
|
+
self.__function_pattern_matches__[function] = patterns
|
146
|
+
pattern
|
130
147
|
end
|
131
148
|
end
|
132
149
|
end
|