qo 0.5.0 → 0.99.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.
@@ -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