qo 0.5.0 → 0.99.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,52 @@
1
+ module Qo
2
+ module PatternMatchers
3
+ # A module to allow the registration of braches to a pattern matcher.
4
+ #
5
+ # @author baweaver
6
+ # @since 1.0.0
7
+ #
8
+ module Branching
9
+ # On inclusion, extend the class including this module with branch
10
+ # registration methods.
11
+ #
12
+ # @param base [Class]
13
+ # Class including this module, passed from `include`
14
+ def self.included(base)
15
+ base.extend(ClassMethods)
16
+ end
17
+
18
+ # Class methods to extend the including class with
19
+ #
20
+ # @author baweaver
21
+ # @since 1.0.0
22
+ module ClassMethods
23
+ # Registers a branch to a pattern matcher.
24
+ #
25
+ # This defines a method on the pattern matcher matching the `name` of
26
+ # the branch. If the name is `where`, the pattern matcher will now be
27
+ # given a method called `where` with which to match with.
28
+ #
29
+ # When called, this will either ammend a matcher to the list of matchers
30
+ # or set a default matcher if the branch happens to be a default.
31
+ #
32
+ # @param branch [Branch]
33
+ # Branch object to register with a pattern matcher
34
+ def register_branch(branch)
35
+ define_method(branch.name) do |*conditions, **keyword_conditions, &function|
36
+ qo_matcher = Qo::Matchers::Matcher.new('and', conditions, keyword_conditions)
37
+
38
+ branch_matcher = branch.create_matcher(
39
+ qo_matcher, destructure: @destructure, &function
40
+ )
41
+
42
+ if branch.default?
43
+ @default = branch_matcher
44
+ else
45
+ @matchers << branch_matcher
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,170 @@
1
+ module Qo
2
+ module PatternMatchers
3
+ # Classic Pattern Match. This matcher uses `when` and `else` branches and
4
+ # is meant to be a more powerful variant of the case statement
5
+ #
6
+ # @author baweaver
7
+ # @since 0.2.0
8
+ class PatternMatch
9
+ include Branching
10
+
11
+ # The regular pattern matcher from classic Qo uses `when` and `else`
12
+ # branches, like a `case` statement
13
+ register_branch Qo::Branches::WhenBranch.new
14
+ register_branch Qo::Branches::ElseBranch.new
15
+
16
+ # Creates a new instance of a pattern matcher
17
+ #
18
+ # @param destructure: false [Boolean]
19
+ # Whether or not to destructure values before yielding to a block
20
+ #
21
+ # @param &fn [Proc]
22
+ # Function to be used to construct the pattern matcher's branches
23
+ #
24
+ # @return [Qo::PatternMatchers::PatternMatch]
25
+ def initialize(destructure: false, &fn)
26
+ @matchers = []
27
+ @default = nil
28
+ @destructure = destructure
29
+
30
+ yield(self) if block_given?
31
+ end
32
+
33
+ # Allows for the creation of an anonymous PatternMatcher based on this
34
+ # parent class. To be used by people wishing to make their own pattern
35
+ # matchers with variant branches and other features not included in the
36
+ # defaultly provided ones
37
+ #
38
+ # @param branches: [] [Array[Branch]]
39
+ # Branches to be used with this new pattern matcher
40
+ #
41
+ # @return [Class]
42
+ # Anonymous pattern matcher class to be bound to a constant or used
43
+ # anonymously.
44
+ def self.create(branches: [])
45
+ Class.new(Qo::PatternMatchers::PatternMatch) do
46
+ branches.each { |branch| register_branch(branch.new) }
47
+ end
48
+ end
49
+
50
+ # Allows for the injection of a pattern matching function into a type class
51
+ # for direct access, rather than yielding an instance of that class to a
52
+ # pattern matcher.
53
+ #
54
+ # This is typically done for monadic types that need to `match`. When
55
+ # combined with extractor type branches it can be very handy for dealing
56
+ # with container types.
57
+ #
58
+ # @example
59
+ #
60
+ # ```ruby
61
+ # # Technically Some and None don't exist yet, so we have to "cheat" instead
62
+ # # of just saying `Some` for the precondition
63
+ # SomeBranch = Qo.create_branch(
64
+ # name: 'some',
65
+ # precondition: -> v { v.is_a?(Some) },
66
+ # extractor: :value
67
+ # )
68
+ #
69
+ # NoneBranch = Qo.create_branch(
70
+ # name: 'none',
71
+ # precondition: -> v { v.is_a?(None) },
72
+ # extractor: :value
73
+ # )
74
+ #
75
+ # SomePatternMatch = Qo.create_pattern_match(branches: [SomeBranch, NoneBranch])
76
+ #
77
+ # class Some
78
+ # include SomePatternMatch.mixin
79
+ #
80
+ # attr_reader :value
81
+ #
82
+ # def initialize(value) @value = value end
83
+ #
84
+ # def fmap(&fn)
85
+ # new_value = fn.call(value)
86
+ # new_value ? Some.new(new_value) : None(value)
87
+ # end
88
+ # end
89
+ #
90
+ # class None
91
+ # include SomePatternMatch.mixin
92
+ #
93
+ # attr_reader :value
94
+ #
95
+ # def initialize(value) @value = value end
96
+ # def fmap(&fn) None.new(value) end
97
+ # end
98
+ #
99
+ # Some.new(1)
100
+ # .fmap { |v| v * 2 }
101
+ # .match { |m|
102
+ # m.some { |v| v + 100 }
103
+ # m.none { "OHNO!" }
104
+ # }
105
+ # => 102
106
+ #
107
+ # Some.new(1)
108
+ # .fmap { |v| nil }
109
+ # .match { |m|
110
+ # m.some { |v| v + 100 }
111
+ # m.none { "OHNO!" }
112
+ # }
113
+ # => "OHNO!"
114
+ # ```
115
+ #
116
+ # @param destructure: false [Boolean]
117
+ # Whether or not to destructure values before yielding to a block
118
+ #
119
+ # @param as: :match [Symbol]
120
+ # Name to use as a method name bound to the including class
121
+ #
122
+ # @return [Module]
123
+ # Module to be mixed into a class
124
+ def self.mixin(destructure: false, as: :match)
125
+ create_self = -> &function { new(destructure: destructure, &function) }
126
+
127
+ Module.new do
128
+ define_method(as) do |&function|
129
+ create_self.call(&function).call(self)
130
+ end
131
+ end
132
+ end
133
+
134
+ # Calls the pattern matcher, yielding the target value to the first
135
+ # matching branch it encounters.
136
+ #
137
+ # @param value [Any]
138
+ # Value to match against
139
+ #
140
+ # @return [Any]
141
+ # Result of the called branch
142
+ #
143
+ # @return [nil]
144
+ # Returns nil if no branch is matched
145
+ def call(value)
146
+ @matchers.each do |matcher|
147
+ status, return_value = matcher.call(value)
148
+ return return_value if status
149
+ end
150
+
151
+ if @default
152
+ _, return_value = @default.call(value)
153
+ return_value
154
+ else
155
+ nil
156
+ end
157
+ end
158
+
159
+ alias === call
160
+ alias [] call
161
+
162
+ # Procified version of `call`
163
+ #
164
+ # @return [Proc[Any] => Any]
165
+ def to_proc
166
+ -> target { self.call(target) }
167
+ end
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,13 @@
1
+ module Qo
2
+ # Classes for constructing pattern matchers, or defining your own
3
+ # should you want to.
4
+ #
5
+ # @author baweaver
6
+ # @since 1.0.0
7
+ module PatternMatchers
8
+ end
9
+ end
10
+
11
+ require 'qo/pattern_matchers/branching'
12
+ require 'qo/pattern_matchers/pattern_match'
13
+ require 'qo/pattern_matchers/result_pattern_match'
@@ -0,0 +1,45 @@
1
+ module Qo
2
+ module PatternMatchers
3
+ # Unlike the normal pattern matcher, this one works on tuple arrays containing
4
+ # a status and a result like `[:ok, result]` and `[:err, message]`.
5
+ #
6
+ # Note that each of these can still take conditionals much like a `where`
7
+ # branch for more fine grained control over what we're looking for.
8
+ #
9
+ # @example
10
+ # ```ruby
11
+ # matcher = Qo.result_match { |m|
12
+ # m.success(String) { |v| "Hello #{v}"}
13
+ # m.success { |v| v + 10 }
14
+ # m.failure { |v| "Error: #{v}" }
15
+ # }
16
+ #
17
+ # matcher.call([:ok, 'there'])
18
+ # # => "Hello there"
19
+ #
20
+ # matcher.call([:ok, 4])
21
+ # # => 14
22
+ #
23
+ # matcher.call([:err, 'OH NO'])
24
+ # # => "Error: OH NO"
25
+ # ```
26
+ #
27
+ # @author baweaver
28
+ # @since 1.0.0
29
+ class ResultPatternMatch < PatternMatch
30
+ register_branch Qo::Branches::SuccessBranch.new
31
+ register_branch Qo::Branches::FailureBranch.new
32
+
33
+ # Creates a new result matcher
34
+ #
35
+ # @param destructure: false [Boolean]
36
+ # Whether or not to destructure the value before yielding to
37
+ # the first matched block
38
+ #
39
+ # @return [type] [description]
40
+ def initialize(destructure: false)
41
+ super(destructure: destructure)
42
+ end
43
+ end
44
+ end
45
+ end
@@ -73,15 +73,19 @@ module Qo
73
73
  # })
74
74
  # => [0, 6, 2]
75
75
  #
76
+ # @param destructure: [Boolean]
77
+ # Whether or not to destructure an object before yielding it to the
78
+ # given function.
79
+ #
76
80
  # @param fn [Proc]
77
81
  # Body of the matcher, as shown in examples
78
82
  #
79
83
  # @return [Qo::PatternMatch]
80
84
  # A function awaiting a value to match against
81
- def match(&fn)
85
+ def match(destructure: false, &fn)
82
86
  return proc { false } unless block_given?
83
87
 
84
- Qo::Matchers::PatternMatch.new(&fn)
88
+ Qo::PatternMatchers::PatternMatch.new(destructure: destructure, &fn)
85
89
  end
86
90
 
87
91
  # Similar to the common case statement of Ruby, except in that it behaves
@@ -105,16 +109,136 @@ module Qo
105
109
  # @param value [Any]
106
110
  # Value to match against
107
111
  #
112
+ # @param destructure: [Boolean]
113
+ # Whether or not to destructure an object before yielding it to the
114
+ # given function.
115
+ #
108
116
  # @param &fn [Proc]
109
117
  # Body of the matcher, as shown above
110
118
  #
111
119
  # @return [Any]
112
120
  # The result of calling a pattern match with a provided value
113
- def case(value, &fn)
114
- Qo::Matchers::PatternMatch.new(&fn).call(value)
121
+ def case(value, destructure: false, &fn)
122
+ Qo::PatternMatchers::PatternMatch.new(destructure: destructure, &fn).call(value)
123
+ end
124
+
125
+ # Similar to `match`, except it uses a `ResultPatternMatch` which instead
126
+ # responds to tuple types:
127
+ #
128
+ # @example
129
+ #
130
+ # ```ruby
131
+ # pm = Qo.result_match { |m|
132
+ # m.success { |v| v + 10 }
133
+ # m.failure { |v| "Error: #{v}" }
134
+ # }
135
+ #
136
+ # pm.call([:ok, 3])
137
+ # # => 13
138
+ #
139
+ # pm.call([:err, "No Good"])
140
+ # # => "Error: No Good"
141
+ # ```
142
+ #
143
+ # @param destructure: [Boolean]
144
+ # Whether or not to destructure an object before yielding it to the
145
+ # given function.
146
+ #
147
+ # @param &fn [Proc]
148
+ # Body of the matcher, as shown above
149
+ #
150
+ # @see match
151
+ #
152
+ # @return [Proc[Any] => Any]
153
+ # Proc awaiting a value to match against.
154
+ def result_match(destructure: false, &fn)
155
+ return proc { false } unless block_given?
156
+
157
+ Qo::PatternMatchers::ResultPatternMatch.new(destructure: destructure, &fn)
115
158
  end
116
159
 
117
- # Abstraction for creating a matcher, allowing for common error handling scenarios.
160
+ # Similar to `case`, except it uses a `ResultPatternMatch` instead.
161
+ #
162
+ # @see match
163
+ # @see result_match
164
+ # @see case
165
+ #
166
+ # @param target [Any]
167
+ # Target to match against
168
+ #
169
+ # @param destructure: [Boolean]
170
+ # Whether or not to destructure an object before yielding it to the
171
+ # given function.
172
+ #
173
+ # @param &fn [Proc]
174
+ # Body of the matcher, as shown above
175
+ #
176
+ # @return [Any]
177
+ # Result of the match
178
+ def result_case(target, destructure: false, &fn)
179
+ Qo::PatternMatchers::ResultPatternMatch
180
+ .new(destructure: destructure, &fn)
181
+ .call(target)
182
+ end
183
+
184
+ # Dynamically creates a new branch to be used with custom pattern matchers.
185
+ #
186
+ # @param name: [String]
187
+ # Name of the branch. This is what binds to the pattern match as a method,
188
+ # meaning a name of `where` will result in calling it as `m.where`.
189
+ #
190
+ # @param precondition: Any [Symbol, #===]
191
+ # A precondition to the branch being considered true. This is done for
192
+ # static conditions like a certain type or perhaps checking a tuple type
193
+ # like `[:ok, value]`.
194
+ #
195
+ # If a `Symbol` is given, Qo will coerce it into a proc. This is done to
196
+ # make a nicer shorthand for creating a branch.
197
+ #
198
+ # @param extractor: IDENTITY [Proc, Symbol]
199
+ # How to pull the value out of a target object when a branch matches before
200
+ # calling the associated function. For a monadic type this might be something
201
+ # like extracting the value before yielding to the given block.
202
+ #
203
+ # If a `Symbol` is given, Qo will coerce it into a proc. This is done to
204
+ # make a nicer shorthand for creating a branch.
205
+ #
206
+ # @param destructure: false
207
+ # Whether or not to destructure the given object before yielding to the
208
+ # associated block. This means that the given block now places great
209
+ # importance on the argument names, as they'll be used to extract values
210
+ # from the associated object by that same method name, or key name in the
211
+ # case of hashes.
212
+ #
213
+ # @param default: false [Boolean]
214
+ # Whether this branch is considered to be a default condition. This is
215
+ # done to ensure that a branch runs last after all other conditions have
216
+ # failed. An example of this would be an `else` branch.
217
+ #
218
+ # @return [Class]
219
+ # Anonymous branch class to be bound to a constant or used directly
220
+ def create_branch(name:, precondition: Any, extractor: IDENTITY, destructure: false, default: false)
221
+ Qo::Branches::Branch.create(
222
+ name: name,
223
+ precondition: precondition,
224
+ extractor: extractor,
225
+ destructure: destructure,
226
+ default: default
227
+ )
228
+ end
229
+
230
+ # Creates a new type of pattern matcher from a set of branches
231
+ #
232
+ # @param branches: [Array[Branch]]
233
+ # An array of branches that this pattern matcher will respond to
234
+ #
235
+ # @return [Class]
236
+ # Anonymous pattern matcher to be bound to a constant or used directly
237
+ def create_pattern_match(branches:)
238
+ Qo::PatternMatchers::PatternMatch.create(branches: branches)
239
+ end
240
+
241
+ # Abstraction for creating a matcher.
118
242
  #
119
243
  # @param type [String]
120
244
  # Type of matcher
@@ -127,9 +251,7 @@ module Qo
127
251
  #
128
252
  # @return [Qo::Matcher]
129
253
  private def create_matcher(type, array_matchers, keyword_matchers)
130
- raise MultipleMatchersProvided if !array_matchers.empty? && !keyword_matchers.empty?
131
-
132
- Qo::Matchers::BaseMatcher.new(type, array_matchers, keyword_matchers)
254
+ Qo::Matchers::Matcher.new(type, array_matchers, keyword_matchers)
133
255
  end
134
256
  end
135
257
  end