functional-ruby 0.7.7 → 1.0.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 +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
|