filigree 0.1.2

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,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