filigree 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,499 @@
1
+ # Author: Chris Wailes <chris.wailes@gmail.com>
2
+ # Project: Filigree
3
+ # Date: 2013/05/04
4
+ # Description: Pattern matching for Ruby.
5
+
6
+ ############
7
+ # Requires #
8
+ ############
9
+
10
+ # Standard Library
11
+ require 'ostruct'
12
+ require 'singleton'
13
+
14
+ # Filigree
15
+ require 'filigree/abstract_class'
16
+
17
+ ##########
18
+ # Errors #
19
+ ##########
20
+
21
+ # An error that indicates that no pattern matched a given object.
22
+ class MatchError < RuntimeError; end
23
+
24
+ ###########
25
+ # Methods #
26
+ ###########
27
+
28
+ # This is an implementation of pattern matching. The objects passed to match
29
+ # are tested against the patterns defined inside the match block. The return
30
+ # value of `match` will be the result of evaluating the block given to `with`.
31
+
32
+ # The most basic pattern is the literal. Here, the object or objects being
33
+ # matched will be tested for equality with value passed to `with`. In the
34
+ # example below, the call to `match` will return `:one`. Similar to the
35
+ # literal pattern is the wildcard pattern `_`. This will match any object.
36
+
37
+ # You may also match against variables. This can sometimes conflict with the
38
+ # next kind of pattern, which is a binding pattern. Here, the pattern will
39
+ # match any object, and then make the object it matched available to the with
40
+ # block via an attribute reader. This is accomplished using the method_missing
41
+ # callback, so if there is a variable or function with that name you might
42
+ # accidentally compare against a variable. To bind to a name that is already
43
+ # in scope you can use the {Filigree::MatchEnvironment#Bind} method. In
44
+ # addition, class and destructuring pattern results (see bellow) can be bound
45
+ # to a variable by using the {Filigree::BasicPattern#as} method.
46
+
47
+ # If you wish to match string patterns you may use regular expressions. Any
48
+ # object that isn't a string will fail to match against a regular expression.
49
+ # If the object being matched is a string then the regular expressions `match?`
50
+ # method is used. The result of the regular expression match is available
51
+ # inside the with block via the match_data accessor.
52
+
53
+ # When a class is used in a pattern it will match any object that is an
54
+ # instance of that class. If you wish to compare one regular expression to
55
+ # another, or one class to another, you can force the comparison using the
56
+ # {Filigree::MatchEnvironment#Literal} method.
57
+ #
58
+ # Destructuring patterns allow you to match against an instance of a class,
59
+ # while simultaneously binding values stored inside the object to variables
60
+ # in the context of the with block. A class that is destructurable must
61
+ # include the {Filigree::Destructurable} module. You can then destructure an
62
+ # object as shown bellow.
63
+
64
+ # Both `match` and `with` can take multiple arguments. When this happens, each
65
+ # object is paired up with the corresponding pattern. If they all match, then
66
+ # the `with` clause matches. In this way you can match against tuples.
67
+
68
+ # Any with clause can be given a guard clause by passing a lambda as the last
69
+ # argument to `with`. These are evaluated after the pattern is matched, and
70
+ # any bindings made in the pattern are available to the guard clause.
71
+
72
+ # If you wish to evaluate the same body on matching any of several patterns you
73
+ # may list them in order and then specify the body for the last pattern in the
74
+ # group.
75
+
76
+ # Patterns are evaluated in the order in which they are defined and the first
77
+ # pattern to match is the one chosen. You may define helper methods inside the
78
+ # match block. They will be re-defined every time the match statement is
79
+ # evaluated, so you should move any definitions outside any match calls that
80
+ # are being evaluated often.
81
+
82
+ # @example The literal pattern
83
+ # def foo(n)
84
+ # match 1 do
85
+ # with(1) { :one }
86
+ # with(2) { :two }
87
+ # with(_) { :other }
88
+ # end
89
+ # end
90
+ #
91
+ # foo(1)
92
+
93
+ # @example Matching against variables
94
+ # var = 42
95
+ # match 42 do
96
+ # with(var) { :hoopy }
97
+ # with(0) { :zero }
98
+ # end
99
+
100
+ # @example Binding patterns
101
+ # # Returns 42
102
+ # match 42 do
103
+ # with(x) { x }
104
+ # end
105
+
106
+ # x = 3
107
+ # # Returns 42
108
+ # match 42 do
109
+ # with(Bind(:x)) { x }
110
+ # with(42) { :hoopy }
111
+ # end
112
+
113
+ # @example Regular expression and class instance pattern
114
+ # def matcher(object)
115
+ # match object do
116
+ # with(/hoopy/) { 42 }
117
+ # with(Integer) { 'hoopy' }
118
+ # end
119
+ # end
120
+
121
+ # # Returns 42
122
+ # matcher('hoopy')
123
+ # # Returns 'hoopy'
124
+ # matcher(42)
125
+
126
+ # @example Destructuring an object
127
+ # class Foo
128
+ # include Filigree::Destructurable
129
+ # def initialize(a, b)
130
+ # @a = a
131
+ # @b = b
132
+ # end
133
+ #
134
+ # def destructure(_)
135
+ # [@a, @b]
136
+ # end
137
+ # end
138
+
139
+ # # Returns true
140
+ # match Foo.new(4, 2) do
141
+ # with(Foo.(4, 2)) { true }
142
+ # with(_) { false }
143
+ # end
144
+
145
+ # @example Using guard clauses
146
+ # match o do
147
+ # with(n, -> { n < 0 }) { :NEG }
148
+ # with(0) { :ZERO }
149
+ # with(n, -> { n > 0 }) { :POS }
150
+ # end
151
+ #
152
+ # @param [Object] objects Objects to be matched
153
+ # @param [Proc] block Block containing with clauses.
154
+ #
155
+ # @return [Object] Result of evaluating the matched pattern's block
156
+ def match(*objects, &block)
157
+ me = Filigree::MatchEnvironment.new
158
+
159
+ me.instance_exec &block
160
+
161
+ me.find_match(objects)
162
+ end
163
+
164
+ #######################
165
+ # Classes and Modules #
166
+ #######################
167
+
168
+ module Filigree
169
+
170
+ # A module indicating that an object may be destructured. The including
171
+ # class must define the `destructure` instance method, which takes one
172
+ # argument specifying the number of pattern elements it is being matched
173
+ # against.
174
+ module Destructurable
175
+ # The instance method that generates a destructuring pattern.
176
+ #
177
+ # @param [Object] pattern Sub-patterns used to match the destructured elements of the object.
178
+ #
179
+ # @return [DestructuringPattern]
180
+ def call(*pattern)
181
+ DestructuringPattern.new(self, *pattern)
182
+ end
183
+ end
184
+
185
+ # Match blocks are evaluated inside an instance of MatchEnvironment.
186
+ class MatchEnvironment
187
+ # Force binding to the given name
188
+ #
189
+ # @param [Symbol] name Name to bind the value to
190
+ #
191
+ # @return [BindingPattern]
192
+ def Bind(name)
193
+ BindingPattern.new(name)
194
+ end
195
+
196
+ # Force a literal comparison
197
+ #
198
+ # @param [Object] obj Object to test equality with
199
+ #
200
+ # @return [LiteralPattern]
201
+ def Literal(obj)
202
+ LiteralPattern.new(obj)
203
+ end
204
+
205
+ def initialize
206
+ @patterns = Array.new
207
+ @deferred = Array.new
208
+ end
209
+
210
+ # Find a match for the given objects among the defined patterns.
211
+ #
212
+ # @param [Array<Object>] objects Objects to be matched
213
+ #
214
+ # @return [Object] Result of evaluating the matching pattern's block
215
+ #
216
+ # @raise [MatchError] Raised if no pattern matches the objects
217
+ def find_match(objects)
218
+ @patterns.each do |pattern|
219
+ env = OpenStruct.new
220
+
221
+ return pattern.(env, objects) if pattern.match?(objects, env)
222
+ end
223
+
224
+ # If we didn't find anything we raise a MatchError.
225
+ raise MatchError
226
+ end
227
+
228
+ # Define a pattern in this match call.
229
+ #
230
+ # @see match Documentation on pattern matching
231
+ #
232
+ # @param [Object] pattern Objects defining the pattern
233
+ # @param [Proc] block Block to be executed if the pattern matches
234
+ #
235
+ # @return [void]
236
+ def with(*pattern, &block)
237
+ guard = if pattern.last.is_a?(Proc) then pattern.pop end
238
+
239
+ @patterns << (mp = OuterPattern.new(pattern, guard, block))
240
+
241
+ if block
242
+ @deferred.each { |pattern| pattern.block = block }
243
+ @deferred.clear
244
+
245
+ else
246
+ @deferred << mp
247
+ end
248
+ end
249
+ alias :w :with
250
+
251
+ #############
252
+ # Callbacks #
253
+ #############
254
+
255
+ # Callback used to generate wildcard and binding patterns
256
+ def method_missing(name, *args)
257
+ if args.empty?
258
+ if name == :_ then WildcardPattern.instance else BindingPattern.new(name) end
259
+ else
260
+ super(name, *args)
261
+ end
262
+ end
263
+ end
264
+
265
+ # This class provides the basis for all match patterns.
266
+ class BasicPattern
267
+ extend AbstractClass
268
+
269
+ # Wraps this pattern in a {BindingPattern}, causing the object that
270
+ # this pattern matches to be bound to this name in the with block.
271
+ #
272
+ # @param [BindingPattern] binding_pattern Binding pattern containing the name
273
+ def as(binding_pattern)
274
+ binding_pattern.tap { |bp| bp.pattern_elem = self }
275
+ end
276
+
277
+ # Test to see if a single object matches a single pattern, using the
278
+ # given environment to store bindings.
279
+ #
280
+ # @param [Object] pattern_elem Object representing the pattern
281
+ # @param [Object] object Object to be matched
282
+ # @param [Object] env Environment in which to store bindings
283
+ #
284
+ # @return [Boolean] If the pattern element matched
285
+ def match_pattern_element(pattern_elem, object, env)
286
+ case pattern_elem
287
+ when Class
288
+ object.is_a?(pattern_elem)
289
+
290
+ when Regexp
291
+ (object.is_a?(String) and (md = pattern_elem.match(object))).tap do |match|
292
+ env.send("match_data=", md) if match
293
+ end
294
+
295
+ when BasicPattern
296
+ pattern_elem.match?(object, env)
297
+
298
+ else
299
+ object == pattern_elem
300
+ end
301
+ end
302
+ end
303
+
304
+ # A pattern that matches any object
305
+ class WildcardPattern < BasicPattern
306
+ include Singleton
307
+
308
+ # Return true for any object and don't create any bindings.
309
+ #
310
+ # @return [true]
311
+ def match?(_, _)
312
+ true
313
+ end
314
+ end
315
+
316
+ # An abstract class that matches only a single object to a single pattern.
317
+ class SingleObjectPattern < BasicPattern
318
+ extend AbstractClass
319
+
320
+ # Create a new pattern with a single element.
321
+ #
322
+ # @param [Object] pattern_elem Object representing the pattern
323
+ def initialize(pattern_elem)
324
+ @pattern_elem = pattern_elem
325
+ end
326
+
327
+ # Testing the pattern only involves testing the single pattern
328
+ # element.
329
+ #
330
+ # @param [Object] object Object to test pattern against
331
+ # @param [Object] env Binding environment
332
+ #
333
+ # @return [Boolean]
334
+ def match?(object, env)
335
+ match_pattern_element(@pattern_elem, object, env)
336
+ end
337
+ end
338
+
339
+ # A pattern that forces an equality comparison
340
+ class LiteralPattern < SingleObjectPattern
341
+ # Test the object for equality to the pattern element.
342
+ #
343
+ # @param [Object] object Object to test pattern against
344
+ #
345
+ # @return [Boolean]
346
+ def match?(object, _)
347
+ object == @pattern_elem
348
+ end
349
+ end
350
+
351
+ # A pattern that binds a sub-pattern's matching object to a name in the
352
+ # binding environment.
353
+ class BindingPattern < SingleObjectPattern
354
+ attr_accessor :pattern_elem
355
+
356
+ # Create a new binding pattern.
357
+ #
358
+ # @param [Symbol] name Name to bind to
359
+ # @param [Object] pattern_elem Sub-pattern
360
+ def initialize(name, pattern_elem = nil)
361
+ @name = name
362
+ super(pattern_elem)
363
+ end
364
+
365
+ # Test the object for equality to the pattern element. Binds the
366
+ # object to the binding pattern's name if it does match.
367
+ #
368
+ # @param [Object] object Object to test pattern against
369
+ # @param [Object] env Binding environment
370
+ #
371
+ # @return [Boolean]
372
+ def match?(object, env)
373
+ (@pattern_elem.nil? or super).tap do |match|
374
+ env.send("#{@name}=", object) if match
375
+ end
376
+ end
377
+ end
378
+
379
+ # An abstract class that matches multiple objects to multiple patterns.
380
+ class MultipleObjectPattern < BasicPattern
381
+ extend AbstractClass
382
+
383
+ # Create a new pattern with multiple elements.
384
+ #
385
+ # @param [Array<Object>] pattern Array of pattern elements
386
+ def initialize(pattern)
387
+ @pattern = pattern
388
+ end
389
+
390
+ # Test multiple objects against multiple pattern elements.
391
+ #
392
+ # @param [Object] objects Object to test pattern against
393
+ #
394
+ # @return [Boolean]
395
+ def match?(objects, env)
396
+ if objects.length == @pattern.length
397
+ @pattern.zip(objects).each do |pattern_elem, object|
398
+ return false unless match_pattern_element(pattern_elem, object, env)
399
+ end
400
+
401
+ true
402
+
403
+ else
404
+ (@pattern.length == 1 and @pattern.first == WildcardPattern.instance)
405
+ end
406
+ end
407
+ end
408
+
409
+ # The that contains all of the pattern elements passed to a with clause.
410
+ class OuterPattern < MultipleObjectPattern
411
+ attr_writer :block
412
+
413
+ # Create a new outer pattern with the given pattern elements, guard,
414
+ # and block.
415
+ #
416
+ # @param [Array<Object>] pattern Pattern elements
417
+ # @param [Proc] guard Guard clause that is tested if the pattern matches
418
+ # @param [Proc] block Block to be evaluated if the pattern matches
419
+ def initialize(pattern, guard, block)
420
+ super(pattern)
421
+ @guard = guard
422
+ @block = block
423
+ end
424
+
425
+ # Call the pattern's block, passing the given objects to the block.
426
+ #
427
+ # @param [Object] env Environment in which to evaluate the block
428
+ # @param [Array<Object>] objects Arguments to the block
429
+ def call(env, objects = [])
430
+ if @block then env.instance_exec(*objects, &@block) else nil end
431
+ end
432
+
433
+ # Test the objects for equality to the pattern elements.
434
+ #
435
+ # @param [Object] objects Objects to test pattern elements against
436
+ # @param [Object] env Binding environment
437
+ #
438
+ # @return [Boolean]
439
+ def match?(objects, env)
440
+ super && (@guard.nil? or env.instance_exec(&@guard))
441
+ end
442
+ end
443
+
444
+ # A pattern that matches an instance of a class and destructures it so
445
+ # that the values contained by the object may be matched upon.
446
+ class DestructuringPattern < MultipleObjectPattern
447
+ # Create a new destructuring pattern.
448
+ #
449
+ # @param [Class] klass Class to match instances of. It must be destructurable.
450
+ # @param [Object] pattern Pattern elements to use in matching the object's values
451
+ def initialize(klass, *pattern)
452
+ @klass = klass
453
+ super(pattern)
454
+ end
455
+
456
+ # Test to see if the object is an instance of the appropriate class,
457
+ # and if so destructure it and test it's values against the
458
+ # sub-pattern elements.
459
+ #
460
+ # @param [Object] object Object to test pattern against
461
+ # @param [Object] env Binding environment
462
+ #
463
+ # @return [Boolean]
464
+ def match?(object, env)
465
+ object.is_a?(@klass) and super(object.destructure(@pattern.length), env)
466
+ end
467
+ end
468
+ end
469
+
470
+ ###################################
471
+ # Standard Library Deconstructors #
472
+ ###################################
473
+
474
+ class Array
475
+ extend Filigree::Destructurable
476
+
477
+ # Destructuring for the array class. If the array is being matched
478
+ # against two patterns the destructuring of the array will be the first
479
+ # element and then an array containing the rest of the values. If there
480
+ # are three patterns the destructuring of the array will be the first and
481
+ # second elements, and then an array containing the remainder of the
482
+ # values.
483
+ #
484
+ # @param [Fixnum] num_elems Number of sub-pattern elements
485
+ #
486
+ # @return [Array<Object>]
487
+ def destructure(num_elems)
488
+ [*self.first(num_elems - 1), self[(num_elems - 1)..-1]]
489
+ end
490
+ end
491
+
492
+ class Class
493
+ # Causes an instance of a class to be bound the the given name.
494
+ #
495
+ # @param [BindingPattern] binding_pattern Name to bind the instance to
496
+ def as(binding_pattern)
497
+ binding_pattern.tap { |bp| bp.pattern_elem = self }
498
+ end
499
+ end
@@ -0,0 +1,40 @@
1
+ # Author: Chris Wailes <chris.wailes@gmail.com>
2
+ # Project: Filigree
3
+ # Date: 2013/05/04
4
+ # Description: Additional features for all objects.
5
+
6
+ ############
7
+ # Requires #
8
+ ############
9
+
10
+ # Standard Library
11
+
12
+ # Filigree
13
+
14
+ ###########
15
+ # Methods #
16
+ ###########
17
+
18
+ # Simple implementation of the Y combinator.
19
+ #
20
+ # @param [Object] value Value to be returned after executing the provided block.
21
+ #
22
+ # @return [Object] The object passed in parameter value.
23
+ def returning(value)
24
+ yield(value)
25
+ value
26
+ end
27
+
28
+ #######################
29
+ # Classes and Modules #
30
+ #######################
31
+
32
+ # Object class extras.
33
+ class Object
34
+ # A copy and modification helper.
35
+ #
36
+ # @return [Object] A copy of the object with the block evaluated in the context of the copy.
37
+ def clone_with(&block)
38
+ self.clone.tap { |clone| clone.instance_exec(&block) }
39
+ end
40
+ end
@@ -0,0 +1,33 @@
1
+ # Author: Chris Wailes <chris.wailes@gmail.com>
2
+ # Project: Filigree
3
+ # Date: 2014/1/20
4
+ # Description: A helper for require.
5
+
6
+ ############
7
+ # Requires #
8
+ ############
9
+
10
+ # Standard Library
11
+
12
+ # Filigree
13
+
14
+ ###########
15
+ # Methods #
16
+ ###########
17
+
18
+ # Require a file, but fail gracefully if it isn't found.
19
+ #
20
+ # @param [String] file File to be requested
21
+ # @param [Boolean] print_failure To print a message on failure or not
22
+ def request_file(file, print_failure = false)
23
+ begin
24
+ require file
25
+ yield if block_given?
26
+ rescue LoadError
27
+ if print_warning.is_a?(String)
28
+ puts print_failure
29
+ elsif print_failure
30
+ puts "Unable to require file: #{file}"
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,52 @@
1
+ # Author: Chris Wailes <chris.wailes@gmail.com>
2
+ # Project: Filigree
3
+ # Date: 2014/01/20
4
+ # Description: Class extensions for the String class.
5
+
6
+ ############
7
+ # Requires #
8
+ ############
9
+
10
+ # Standard Library
11
+
12
+ # Filigree
13
+
14
+ #######################
15
+ # Classes and Modules #
16
+ #######################
17
+
18
+ class String
19
+ # Chop up the string into multiple lines so that no line is longer than
20
+ # the specified number of characters.
21
+ #
22
+ # @param [Fixnum] indent Indentation to put before each line; it is
23
+ # assumed that this indentation is already present for the first line
24
+ # @param [Fixnum] max_length Maximum length per line
25
+ def segment(indent, max_length = 80)
26
+ lines = Array.new
27
+
28
+ words = self.split(/\s/)
29
+ line = words.shift
30
+
31
+ line_length = indent + line.length
32
+
33
+ words.each do |word|
34
+ new_length = line_length + 1 + word.length
35
+
36
+ if new_length < max_length
37
+ line += " #{word}"
38
+ line_length = new_length
39
+
40
+ else
41
+ lines << line
42
+
43
+ line = word
44
+ line_length = indent + word.length
45
+ end
46
+ end
47
+
48
+ lines << line unless line.empty?
49
+
50
+ lines.join("\n" + (' ' * indent))
51
+ end
52
+ end