algebrick 0.1.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 +7 -0
- data/MIT-LICENSE +19 -0
- data/README.md +7 -0
- data/README_FULL.md +102 -0
- data/lib/algebrick.rb +1003 -0
- metadata +135 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: e6e3ca091c97766d3eb3524ce82f129558386865
|
4
|
+
data.tar.gz: b4c5547dd1dd6604f57db2b2360e11136f883226
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e1bdc83c04dde504c7d9360bea9f3599c2b8e8bd251bd42087c4c1eab574482258f762e1c0444f1b97ffefe49ac7f35beb0eec85ad69cdb443039733a225f7ce
|
7
|
+
data.tar.gz: 71d61898fdcf3e4b29ada2552ae90527a609355b52841e7236983adc2158c46085db2dfaba155f7eababadfcc268a0c327a8f3ff496738ae60c0ffc7628b29f4
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (c) 2013 Petr Chalupa <git@pitr.ch>
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
data/README.md
ADDED
data/README_FULL.md
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
# Algebrick
|
2
|
+
|
3
|
+
Is a small gem providing algebraic types and pattern matching on them for Ruby.
|
4
|
+
|
5
|
+
- Documentation: {http://blog.pitr.ch/algebrick}
|
6
|
+
- Source: {https://github.com/pitr-ch/algebrick}
|
7
|
+
- Blog: {http://blog.pitr.ch}
|
8
|
+
|
9
|
+
## Quick example with Maybe type
|
10
|
+
|
11
|
+
{include:file:doc/maybe.out.rb}
|
12
|
+
|
13
|
+
## Algebraic types: what is it?
|
14
|
+
|
15
|
+
If you ever read something about Haskell that you probably know:
|
16
|
+
|
17
|
+
data Tree = Empty
|
18
|
+
| Leaf Int
|
19
|
+
| Node Tree Tree
|
20
|
+
|
21
|
+
which is an algebraic type. This snippet defines type `Tree` which has 3 possible values:
|
22
|
+
`Empty`, `Leaf` with and extra value of type `Int`, `Node` with two values of type `Tree`.
|
23
|
+
See {https://en.wikipedia.org/wiki/Algebraic_data_type}.
|
24
|
+
|
25
|
+
Same thing can be defined with this gem:
|
26
|
+
|
27
|
+
{include:file:doc/tree1.out.rb}
|
28
|
+
|
29
|
+
There are 4 kinds of algebraic types in Algebrick gem:
|
30
|
+
|
31
|
+
- **Atom** a type that has only one value e.g. `Empty`.
|
32
|
+
- **Product** a type that has a set nuber of fields with given type e.g. `Leaf(Integer)`
|
33
|
+
- **Variant** a type that does have set number of variants e.g. `Tree(Empty | Leaf(Integer) | Node(Tree, Tree)`.
|
34
|
+
It means that values of `Empty`, `Leaf[1]`, `Node[Empty, Empry]` have all type `Tree`.
|
35
|
+
- **ProductVariant** will be created when a recursive type like `list === empty | list(Object, list)` is defined.
|
36
|
+
`List` has two variants `Empty` and itself simultaneously it has fields as product type.
|
37
|
+
|
38
|
+
### Type definition
|
39
|
+
|
40
|
+
{include:file:doc/type_def.out.rb}
|
41
|
+
|
42
|
+
### Value creation
|
43
|
+
|
44
|
+
{include:file:doc/values.out.rb}
|
45
|
+
|
46
|
+
### Extending behavior
|
47
|
+
|
48
|
+
{include:file:doc/extending_behavior.out.rb}
|
49
|
+
|
50
|
+
### Pattern matching
|
51
|
+
|
52
|
+
Algebraic matchers are helper objects to match algebraic objects and others with
|
53
|
+
`#===` method based on theirs initialization values.
|
54
|
+
|
55
|
+
{include:file:doc/pattern_matching.out.rb}
|
56
|
+
|
57
|
+
## What is it good for?
|
58
|
+
|
59
|
+
### Defining data with a given structure
|
60
|
+
|
61
|
+
{include:file:doc/data.out.rb}
|
62
|
+
|
63
|
+
### Serialization
|
64
|
+
|
65
|
+
Algebraic types also play nice with JSON serialization. So it is ideal for defining messegas
|
66
|
+
for cross-process comunication.
|
67
|
+
|
68
|
+
{include:file:doc/json.out.rb}
|
69
|
+
|
70
|
+
### Null Object Pattern
|
71
|
+
|
72
|
+
see {http://en.wikipedia.org/wiki/Null_Object_pattern#Ruby}.
|
73
|
+
|
74
|
+
{include:file:doc/null.out.rb}
|
75
|
+
|
76
|
+
This has advantage over a classical approach that the methods are defined
|
77
|
+
on one place, no need to track methods in two separate classes `User` and `NullUser`.
|
78
|
+
|
79
|
+
### Message matching in Actor pattern
|
80
|
+
|
81
|
+
Just small snippet from a gem I am still working on.
|
82
|
+
|
83
|
+
class Worker < AbstractActor
|
84
|
+
def initialize(executor)
|
85
|
+
super()
|
86
|
+
@executor = executor
|
87
|
+
end
|
88
|
+
|
89
|
+
def on_message(message)
|
90
|
+
match message,
|
91
|
+
Work.(~any, ~any) --> actor, work do
|
92
|
+
@executor.tell Finished[actor, work.call, self.reference]
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
### TODO
|
98
|
+
|
99
|
+
- Menu model, TypedArray
|
100
|
+
- Pretty print example, see {http://homepages.inf.ed.ac.uk/wadler/papers/prettier/prettier.pdf}
|
101
|
+
- update actor pattern when gem is done
|
102
|
+
|
data/lib/algebrick.rb
ADDED
@@ -0,0 +1,1003 @@
|
|
1
|
+
# TODO method definition in variant type defines methods on variants based on match
|
2
|
+
|
3
|
+
require 'set'
|
4
|
+
|
5
|
+
class Module
|
6
|
+
# Return any modules we +extend+
|
7
|
+
def extended_modules
|
8
|
+
class << self
|
9
|
+
self
|
10
|
+
end.included_modules
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
module Algebrick
|
15
|
+
|
16
|
+
module TypeCheck
|
17
|
+
#def is_kind_of?(value, *types)
|
18
|
+
# a_type_check :kind_of?, false, value, *types
|
19
|
+
#end
|
20
|
+
|
21
|
+
def is_kind_of!(value, *types)
|
22
|
+
a_type_check :kind_of?, true, value, *types
|
23
|
+
end
|
24
|
+
|
25
|
+
#def is_matching?(value, *types)
|
26
|
+
# a_type_check :===, false, value, *types
|
27
|
+
#end
|
28
|
+
|
29
|
+
def is_matching!(value, *types)
|
30
|
+
a_type_check :===, true, value, *types
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def a_type_check(which, bang, value, *types)
|
36
|
+
ok = types.any? do |t|
|
37
|
+
case which
|
38
|
+
when :===
|
39
|
+
t === value
|
40
|
+
when :kind_of?
|
41
|
+
value.kind_of? t
|
42
|
+
else
|
43
|
+
raise ArgumentError
|
44
|
+
end
|
45
|
+
end
|
46
|
+
raise TypeError, "value (#{value.class}) '#{value}' is not ##{which} any of #{types.join(', ')}" if bang && !ok
|
47
|
+
value
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
module Matching
|
52
|
+
def any
|
53
|
+
Matchers::Any.new
|
54
|
+
end
|
55
|
+
|
56
|
+
alias_method :_, :any # TODO make it optional
|
57
|
+
|
58
|
+
#match Empty,
|
59
|
+
# Empty -->() {},
|
60
|
+
# Leaf.(~any) >>-> value do
|
61
|
+
# value
|
62
|
+
# end
|
63
|
+
#match Empty,
|
64
|
+
# Node => ->() {},
|
65
|
+
# Empty => ->() {},
|
66
|
+
# Leaf.(~any) => ->(value) { value }
|
67
|
+
#match Empty,
|
68
|
+
# [Node, lambda {}],
|
69
|
+
# [Empty, lambda {}],
|
70
|
+
# [Leaf.(~any), lambda { |value| value }]
|
71
|
+
#match(Empty,
|
72
|
+
# Node.case {},
|
73
|
+
# Empty.case {},
|
74
|
+
# Leaf.(~any).case { |value| value })
|
75
|
+
|
76
|
+
def match(value, *cases)
|
77
|
+
cases = if cases.size == 1 && cases.first.is_a?(Hash)
|
78
|
+
cases.first
|
79
|
+
else
|
80
|
+
cases
|
81
|
+
end
|
82
|
+
|
83
|
+
cases.each do |matcher, block|
|
84
|
+
return match_value matcher, block if matcher === value
|
85
|
+
end
|
86
|
+
raise "no match for #{value} by any of #{cases.map(&:first).join ', '}"
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
def match_value(matcher, block)
|
92
|
+
if block.kind_of? Proc
|
93
|
+
if matcher.kind_of? Matchers::Abstract
|
94
|
+
matcher.assigns &block
|
95
|
+
else
|
96
|
+
block.call
|
97
|
+
end
|
98
|
+
else
|
99
|
+
block
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
include Matching
|
105
|
+
extend Matching
|
106
|
+
|
107
|
+
module MatcherDelegations
|
108
|
+
def ~
|
109
|
+
~to_m
|
110
|
+
end
|
111
|
+
|
112
|
+
def &(other)
|
113
|
+
to_m & other
|
114
|
+
end
|
115
|
+
|
116
|
+
def |(other)
|
117
|
+
to_m | other
|
118
|
+
end
|
119
|
+
|
120
|
+
def !
|
121
|
+
!to_m
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
class Type < Module
|
126
|
+
include TypeCheck
|
127
|
+
include Matching
|
128
|
+
include MatcherDelegations
|
129
|
+
|
130
|
+
def to_m(*args)
|
131
|
+
raise NotImplementedError
|
132
|
+
end
|
133
|
+
|
134
|
+
def ==(other)
|
135
|
+
raise NotImplementedError
|
136
|
+
end
|
137
|
+
|
138
|
+
def be_kind_of(type)
|
139
|
+
raise NotImplementedError
|
140
|
+
end
|
141
|
+
|
142
|
+
def to_s
|
143
|
+
raise NotImplementedError
|
144
|
+
end
|
145
|
+
|
146
|
+
def inspect
|
147
|
+
to_s
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
module Value
|
152
|
+
include TypeCheck
|
153
|
+
include Matching
|
154
|
+
|
155
|
+
def ==(other)
|
156
|
+
raise NotImplementedError
|
157
|
+
end
|
158
|
+
|
159
|
+
def type
|
160
|
+
raise NotImplementedError
|
161
|
+
end
|
162
|
+
|
163
|
+
def to_hash
|
164
|
+
raise NotImplementedError
|
165
|
+
end
|
166
|
+
|
167
|
+
def to_s
|
168
|
+
raise NotImplementedError
|
169
|
+
end
|
170
|
+
|
171
|
+
def inspect
|
172
|
+
to_s
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
class Atom < Type
|
177
|
+
include Value
|
178
|
+
|
179
|
+
def initialize(&block)
|
180
|
+
super &block
|
181
|
+
extend self
|
182
|
+
end
|
183
|
+
|
184
|
+
def to_m
|
185
|
+
Matchers::Atom.new self
|
186
|
+
end
|
187
|
+
|
188
|
+
def be_kind_of(type)
|
189
|
+
extend type
|
190
|
+
end
|
191
|
+
|
192
|
+
def ==(other)
|
193
|
+
self.equal? other
|
194
|
+
end
|
195
|
+
|
196
|
+
def type
|
197
|
+
self
|
198
|
+
end
|
199
|
+
|
200
|
+
def to_s
|
201
|
+
name
|
202
|
+
end
|
203
|
+
|
204
|
+
def to_hash
|
205
|
+
{ name => name }
|
206
|
+
end
|
207
|
+
|
208
|
+
def from_hash(hash)
|
209
|
+
if hash == to_hash
|
210
|
+
self
|
211
|
+
else
|
212
|
+
raise ArgumentError
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
class ProductConstructor
|
218
|
+
include Value
|
219
|
+
attr_reader :fields
|
220
|
+
|
221
|
+
def initialize(*fields)
|
222
|
+
if fields.size == 1 && fields.first.is_a?(Hash)
|
223
|
+
fields = type.field_names.map { |k| fields.first[k] }
|
224
|
+
end
|
225
|
+
@fields = fields.zip(self.class.type.fields).map do |field, type|
|
226
|
+
is_kind_of! field, type
|
227
|
+
end.freeze
|
228
|
+
end
|
229
|
+
|
230
|
+
def to_s
|
231
|
+
"#{self.class.type.name}[" +
|
232
|
+
if type.field_names
|
233
|
+
type.field_names.map { |name| "#{name}: #{self[name].to_s}" }.join(', ')
|
234
|
+
else
|
235
|
+
fields.map(&:to_s).join(',')
|
236
|
+
end + ']'
|
237
|
+
end
|
238
|
+
|
239
|
+
def to_ary
|
240
|
+
@fields
|
241
|
+
end
|
242
|
+
|
243
|
+
def to_a
|
244
|
+
@fields
|
245
|
+
end
|
246
|
+
|
247
|
+
def to_hash
|
248
|
+
{ self.class.type.name =>
|
249
|
+
if type.field_names
|
250
|
+
type.field_names.inject({}) { |h, name| h.update name => hashize(self[name]) }
|
251
|
+
else
|
252
|
+
fields.map { |v| hashize v }
|
253
|
+
end }
|
254
|
+
end
|
255
|
+
|
256
|
+
def ==(other)
|
257
|
+
return false unless other.kind_of? self.class
|
258
|
+
@fields == other.fields
|
259
|
+
end
|
260
|
+
|
261
|
+
def self.type
|
262
|
+
@type || raise
|
263
|
+
end
|
264
|
+
|
265
|
+
def type
|
266
|
+
self.class.type
|
267
|
+
end
|
268
|
+
|
269
|
+
def self.type=(type)
|
270
|
+
raise if @type
|
271
|
+
@type = type
|
272
|
+
include type
|
273
|
+
end
|
274
|
+
|
275
|
+
private
|
276
|
+
|
277
|
+
def hashize(value)
|
278
|
+
(value.respond_to? :to_hash) ? value.to_hash : value
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
class AbstractProductVariant < Type
|
283
|
+
def be_kind_of(type = nil)
|
284
|
+
if initialized?
|
285
|
+
be_kind_of! type if type
|
286
|
+
if @to_be_kind_of
|
287
|
+
while (type = @to_be_kind_of.shift)
|
288
|
+
be_kind_of! type
|
289
|
+
end
|
290
|
+
end
|
291
|
+
else
|
292
|
+
@to_be_kind_of ||= []
|
293
|
+
@to_be_kind_of << type if type
|
294
|
+
end
|
295
|
+
self
|
296
|
+
end
|
297
|
+
|
298
|
+
protected
|
299
|
+
|
300
|
+
def be_kind_of!(type)
|
301
|
+
raise NotImplementedError
|
302
|
+
end
|
303
|
+
|
304
|
+
private
|
305
|
+
|
306
|
+
def initialized?
|
307
|
+
!!@initialized
|
308
|
+
end
|
309
|
+
|
310
|
+
def initialize(&block)
|
311
|
+
super &block
|
312
|
+
@initialized = true
|
313
|
+
be_kind_of
|
314
|
+
end
|
315
|
+
|
316
|
+
def set_fields(fields_or_hash)
|
317
|
+
fields = if fields_or_hash.size == 1 && fields_or_hash.first.is_a?(Hash)
|
318
|
+
keys = fields_or_hash.first.keys
|
319
|
+
fields_or_hash.first.values
|
320
|
+
else
|
321
|
+
fields_or_hash
|
322
|
+
end
|
323
|
+
|
324
|
+
if keys
|
325
|
+
@field_names = keys
|
326
|
+
keys.all? { |k| is_kind_of! k, Symbol }
|
327
|
+
dict = @field_indexes = keys.each_with_index.inject({}) { |h, (k, i)| h.update k => i }
|
328
|
+
define_method(:[]) { |key| @fields[dict[key]] }
|
329
|
+
end
|
330
|
+
|
331
|
+
fields.all? { |f| is_kind_of! f, Type, Class }
|
332
|
+
raise TypeError, 'there is no product with zero fields' unless fields.size > 0
|
333
|
+
define_method(:value) { @fields.first } if fields.size == 1
|
334
|
+
@fields = fields
|
335
|
+
@constructor = Class.new(ProductConstructor).tap { |c| c.type = self }
|
336
|
+
end
|
337
|
+
|
338
|
+
def set_variants(variants)
|
339
|
+
variants.all? { |v| is_kind_of! v, Type, Class }
|
340
|
+
@variants = variants
|
341
|
+
variants.each do |v|
|
342
|
+
if v.respond_to? :be_kind_of
|
343
|
+
v.be_kind_of self
|
344
|
+
else
|
345
|
+
v.send :include, self
|
346
|
+
end
|
347
|
+
end
|
348
|
+
end
|
349
|
+
|
350
|
+
def product_be_kind_of(type)
|
351
|
+
@constructor.send :include, type
|
352
|
+
end
|
353
|
+
|
354
|
+
def construct_product(*fields)
|
355
|
+
@constructor.new *fields
|
356
|
+
end
|
357
|
+
|
358
|
+
def product_to_s
|
359
|
+
fields_str = if field_names
|
360
|
+
field_names.zip(fields).map { |name, field| "#{name}: #{field.name}" }
|
361
|
+
else
|
362
|
+
fields.map(&:name)
|
363
|
+
end
|
364
|
+
"#{name}(#{fields_str.join ', '})"
|
365
|
+
end
|
366
|
+
|
367
|
+
def product_from_hash(hash)
|
368
|
+
raise ArgumentError, 'hash does not have size 1' unless hash.size == 1
|
369
|
+
type_name, fields = hash.first
|
370
|
+
raise ArgumentError, "#{type_name} is not #{name}" unless type_name == name
|
371
|
+
is_kind_of! fields, Hash, Array
|
372
|
+
case fields
|
373
|
+
when Array
|
374
|
+
self[*fields.map { |value| field_from_hash value }]
|
375
|
+
when Hash
|
376
|
+
self[fields.inject({}) do |h, (name, value)|
|
377
|
+
raise ArgumentError unless @field_names.map(&:to_s).include? name
|
378
|
+
h.update name.to_sym => field_from_hash(value)
|
379
|
+
end]
|
380
|
+
end
|
381
|
+
end
|
382
|
+
|
383
|
+
def field_from_hash(hash)
|
384
|
+
return hash unless Hash === hash
|
385
|
+
return hash unless hash.size == 1
|
386
|
+
type_name, value = hash.first
|
387
|
+
type = constantize type_name
|
388
|
+
if type.respond_to? :from_hash
|
389
|
+
type.from_hash hash
|
390
|
+
else
|
391
|
+
value
|
392
|
+
end
|
393
|
+
end
|
394
|
+
|
395
|
+
def constantize(camel_cased_word)
|
396
|
+
names = camel_cased_word.split('::')
|
397
|
+
names.shift if names.empty? || names.first.empty?
|
398
|
+
|
399
|
+
constant = Object
|
400
|
+
names.each do |name|
|
401
|
+
constant = constant.const_defined?(name) ? constant.const_get(name) : constant.const_missing(name)
|
402
|
+
end
|
403
|
+
constant
|
404
|
+
end
|
405
|
+
end
|
406
|
+
|
407
|
+
class Product < AbstractProductVariant
|
408
|
+
attr_reader :fields, :field_names, :field_indexes
|
409
|
+
|
410
|
+
def initialize(*fields, &block)
|
411
|
+
set_fields fields
|
412
|
+
super(&block)
|
413
|
+
end
|
414
|
+
|
415
|
+
def [](*fields)
|
416
|
+
construct_product(*fields)
|
417
|
+
end
|
418
|
+
|
419
|
+
def be_kind_of!(type)
|
420
|
+
product_be_kind_of type
|
421
|
+
end
|
422
|
+
|
423
|
+
def call(*field_matchers)
|
424
|
+
Matchers::Product.new self, *field_matchers
|
425
|
+
end
|
426
|
+
|
427
|
+
def to_m
|
428
|
+
call *::Array.new(fields.size) { Algebrick.any }
|
429
|
+
end
|
430
|
+
|
431
|
+
def ==(other)
|
432
|
+
other.kind_of? Product and fields == other.fields
|
433
|
+
end
|
434
|
+
|
435
|
+
def to_s
|
436
|
+
product_to_s
|
437
|
+
end
|
438
|
+
|
439
|
+
def from_hash(hash)
|
440
|
+
product_from_hash hash
|
441
|
+
end
|
442
|
+
end
|
443
|
+
|
444
|
+
class Variant < AbstractProductVariant
|
445
|
+
attr_reader :variants
|
446
|
+
|
447
|
+
def initialize(*variants, &block)
|
448
|
+
set_variants(variants)
|
449
|
+
super &block
|
450
|
+
end
|
451
|
+
|
452
|
+
def be_kind_of!(type)
|
453
|
+
variants.each { |v| v.be_kind_of type }
|
454
|
+
end
|
455
|
+
|
456
|
+
def to_m
|
457
|
+
Matchers::Variant.new self
|
458
|
+
end
|
459
|
+
|
460
|
+
def ==(other)
|
461
|
+
other.kind_of? Variant and variants == other.variants
|
462
|
+
end
|
463
|
+
|
464
|
+
def to_s
|
465
|
+
"#{name}(#{variants.map(&:name).join ' | '})"
|
466
|
+
end
|
467
|
+
|
468
|
+
def from_hash(hash)
|
469
|
+
field_from_hash hash
|
470
|
+
end
|
471
|
+
end
|
472
|
+
|
473
|
+
class ProductVariant < AbstractProductVariant
|
474
|
+
attr_reader :fields, :field_names, :field_indexes, :variants
|
475
|
+
|
476
|
+
def initialize(fields, variants, &block)
|
477
|
+
set_fields fields
|
478
|
+
raise unless variants.include? self
|
479
|
+
set_variants variants
|
480
|
+
super &block
|
481
|
+
end
|
482
|
+
|
483
|
+
def be_kind_of!(type)
|
484
|
+
variants.each { |v| v.be_kind_of type unless v == self }
|
485
|
+
product_be_kind_of type
|
486
|
+
end
|
487
|
+
|
488
|
+
def call(*field_matchers)
|
489
|
+
Matchers::Product.new self, *field_matchers
|
490
|
+
end
|
491
|
+
|
492
|
+
def to_m
|
493
|
+
Matchers::Variant.new self
|
494
|
+
end
|
495
|
+
|
496
|
+
def [](*fields)
|
497
|
+
construct_product(*fields)
|
498
|
+
end
|
499
|
+
|
500
|
+
def ==(other)
|
501
|
+
other.kind_of? ProductVariant and
|
502
|
+
variants == other.variants and fields == other.fields
|
503
|
+
end
|
504
|
+
|
505
|
+
def to_s
|
506
|
+
name + '(' +
|
507
|
+
variants.map do |variant|
|
508
|
+
if variant == self
|
509
|
+
product_to_s
|
510
|
+
else
|
511
|
+
variant.name
|
512
|
+
end
|
513
|
+
end.join(' | ') +
|
514
|
+
')'
|
515
|
+
end
|
516
|
+
|
517
|
+
def from_hash(hash)
|
518
|
+
product_from_hash hash
|
519
|
+
end
|
520
|
+
end
|
521
|
+
|
522
|
+
module Matchers
|
523
|
+
|
524
|
+
class Abstract
|
525
|
+
include TypeCheck
|
526
|
+
attr_reader :value
|
527
|
+
|
528
|
+
def initialize
|
529
|
+
@assign, @value = nil
|
530
|
+
end
|
531
|
+
|
532
|
+
def case(&block)
|
533
|
+
return self, block
|
534
|
+
end
|
535
|
+
|
536
|
+
def +(block)
|
537
|
+
return self, block
|
538
|
+
end
|
539
|
+
|
540
|
+
alias_method :-, :+
|
541
|
+
alias_method :>>, :+
|
542
|
+
|
543
|
+
def ~
|
544
|
+
@assign = true
|
545
|
+
self
|
546
|
+
end
|
547
|
+
|
548
|
+
def &(matcher)
|
549
|
+
And.new self, matcher
|
550
|
+
end
|
551
|
+
|
552
|
+
def |(matcher)
|
553
|
+
Or.new self, matcher
|
554
|
+
end
|
555
|
+
|
556
|
+
def !
|
557
|
+
Not.new self
|
558
|
+
end
|
559
|
+
|
560
|
+
def assign?
|
561
|
+
@assign
|
562
|
+
end
|
563
|
+
|
564
|
+
def children_including_self
|
565
|
+
children.unshift self
|
566
|
+
end
|
567
|
+
|
568
|
+
def assigns
|
569
|
+
mine = @assign && @value ? [@value] : []
|
570
|
+
mine = @assign ? [@value] : []
|
571
|
+
children.inject(mine) { |assigns, child| assigns + child.assigns }.tap do
|
572
|
+
return yield *assigns if block_given?
|
573
|
+
end
|
574
|
+
end
|
575
|
+
|
576
|
+
def ===(other)
|
577
|
+
matching?(other).tap { |matched| @value = other if matched }
|
578
|
+
end
|
579
|
+
|
580
|
+
def assign_to_s
|
581
|
+
assign? ? '~' : ''
|
582
|
+
end
|
583
|
+
|
584
|
+
def inspect
|
585
|
+
to_s
|
586
|
+
end
|
587
|
+
|
588
|
+
def children
|
589
|
+
raise NotImplementedError
|
590
|
+
end
|
591
|
+
|
592
|
+
def to_s
|
593
|
+
raise NotImplementedError
|
594
|
+
end
|
595
|
+
|
596
|
+
def ==(other)
|
597
|
+
raise NotImplementedError
|
598
|
+
end
|
599
|
+
|
600
|
+
protected
|
601
|
+
|
602
|
+
def matching?(other)
|
603
|
+
raise NotImplementedError
|
604
|
+
end
|
605
|
+
|
606
|
+
private
|
607
|
+
|
608
|
+
def matchable!(obj)
|
609
|
+
raise ArgumentError, 'object does not respond to :===' unless obj.respond_to? :===
|
610
|
+
obj
|
611
|
+
end
|
612
|
+
|
613
|
+
def find_children(collection)
|
614
|
+
collection.map do |matcher|
|
615
|
+
matcher if matcher.kind_of? Abstract
|
616
|
+
end.compact
|
617
|
+
end
|
618
|
+
end
|
619
|
+
|
620
|
+
class AbstractLogic < Abstract
|
621
|
+
def self.call(*matchers)
|
622
|
+
new *matchers
|
623
|
+
end
|
624
|
+
|
625
|
+
attr_reader :matchers
|
626
|
+
|
627
|
+
def initialize(*matchers)
|
628
|
+
@matchers = matchers.each { |m| matchable! m }
|
629
|
+
end
|
630
|
+
|
631
|
+
def children
|
632
|
+
find_children matchers
|
633
|
+
end
|
634
|
+
|
635
|
+
def ==(other)
|
636
|
+
other.kind_of? self.class and
|
637
|
+
self.matchers == other.matchers
|
638
|
+
end
|
639
|
+
end
|
640
|
+
|
641
|
+
class And < AbstractLogic
|
642
|
+
def to_s
|
643
|
+
matchers.join ' & '
|
644
|
+
end
|
645
|
+
|
646
|
+
protected
|
647
|
+
|
648
|
+
def matching?(other)
|
649
|
+
matchers.all? { |m| m === other }
|
650
|
+
end
|
651
|
+
end
|
652
|
+
|
653
|
+
class Or < AbstractLogic
|
654
|
+
def to_s
|
655
|
+
matchers.join ' | '
|
656
|
+
end
|
657
|
+
|
658
|
+
protected
|
659
|
+
|
660
|
+
def matching?(other)
|
661
|
+
matchers.any? { |m| m === other }
|
662
|
+
end
|
663
|
+
end
|
664
|
+
|
665
|
+
class Not < Abstract # TODO
|
666
|
+
attr_reader :matcher
|
667
|
+
|
668
|
+
def initialize(matcher)
|
669
|
+
@matcher = matcher
|
670
|
+
end
|
671
|
+
|
672
|
+
def children
|
673
|
+
[]
|
674
|
+
end
|
675
|
+
|
676
|
+
def to_s
|
677
|
+
'!' + matcher.to_s
|
678
|
+
end
|
679
|
+
|
680
|
+
def ==(other)
|
681
|
+
other.kind_of? self.class and
|
682
|
+
self.matcher == other.matcher
|
683
|
+
end
|
684
|
+
|
685
|
+
protected
|
686
|
+
|
687
|
+
def matching?(other)
|
688
|
+
not matcher === other
|
689
|
+
end
|
690
|
+
end
|
691
|
+
|
692
|
+
class Any < Abstract
|
693
|
+
def children
|
694
|
+
[]
|
695
|
+
end
|
696
|
+
|
697
|
+
def to_s
|
698
|
+
assign_to_s + 'any'
|
699
|
+
end
|
700
|
+
|
701
|
+
def ==(other)
|
702
|
+
other.kind_of? self.class
|
703
|
+
end
|
704
|
+
|
705
|
+
protected
|
706
|
+
|
707
|
+
def matching?(other)
|
708
|
+
true
|
709
|
+
end
|
710
|
+
end
|
711
|
+
|
712
|
+
class Wrapper < Abstract
|
713
|
+
def self.call(something)
|
714
|
+
new something
|
715
|
+
end
|
716
|
+
|
717
|
+
attr_reader :something
|
718
|
+
|
719
|
+
def initialize(something)
|
720
|
+
super()
|
721
|
+
@something = matchable! something
|
722
|
+
end
|
723
|
+
|
724
|
+
def children
|
725
|
+
find_children [@something]
|
726
|
+
end
|
727
|
+
|
728
|
+
def to_s
|
729
|
+
assign_to_s + "Wrapper.(#{@something})"
|
730
|
+
end
|
731
|
+
|
732
|
+
def ==(other)
|
733
|
+
other.kind_of? self.class and
|
734
|
+
self.something == other.something
|
735
|
+
end
|
736
|
+
|
737
|
+
protected
|
738
|
+
|
739
|
+
def matching?(other)
|
740
|
+
@something === other
|
741
|
+
end
|
742
|
+
end
|
743
|
+
|
744
|
+
class ::Object
|
745
|
+
def to_m
|
746
|
+
Wrapper.new(self)
|
747
|
+
end
|
748
|
+
end
|
749
|
+
|
750
|
+
class Array < Abstract
|
751
|
+
def self.call(*matchers)
|
752
|
+
new *matchers
|
753
|
+
end
|
754
|
+
|
755
|
+
attr_reader :matchers
|
756
|
+
|
757
|
+
def initialize(*matchers)
|
758
|
+
super()
|
759
|
+
@matchers = matchers
|
760
|
+
end
|
761
|
+
|
762
|
+
def children
|
763
|
+
find_children @matchers
|
764
|
+
end
|
765
|
+
|
766
|
+
def to_s
|
767
|
+
"#{assign_to_s}#{"Array.(#{matchers.join(',')})" if matchers}"
|
768
|
+
end
|
769
|
+
|
770
|
+
def ==(other)
|
771
|
+
other.kind_of? self.class and
|
772
|
+
self.matchers == other.matchers
|
773
|
+
end
|
774
|
+
|
775
|
+
protected
|
776
|
+
|
777
|
+
def matching?(other)
|
778
|
+
other.kind_of? ::Array and
|
779
|
+
matchers.size == other.size and
|
780
|
+
matchers.each_with_index.all? { |m, i| m === other[i] }
|
781
|
+
end
|
782
|
+
end
|
783
|
+
|
784
|
+
class ::Array
|
785
|
+
def self.call(*matchers)
|
786
|
+
Matchers::Array.new *matchers
|
787
|
+
end
|
788
|
+
end
|
789
|
+
|
790
|
+
# TODO Hash matcher
|
791
|
+
# TODO Method matcher (:size, matcher)
|
792
|
+
|
793
|
+
class Product < Abstract
|
794
|
+
attr_reader :algebraic_type, :field_matchers
|
795
|
+
|
796
|
+
def initialize(algebraic_type, *field_matchers)
|
797
|
+
super()
|
798
|
+
is_kind_of! algebraic_type, Algebrick::Product, Algebrick::ProductVariant
|
799
|
+
@algebraic_type = algebraic_type
|
800
|
+
field_matchers += ::Array.new(algebraic_type.fields.size) { Algebrick.any } if field_matchers.empty?
|
801
|
+
@field_matchers = field_matchers
|
802
|
+
raise ArgumentError unless algebraic_type.fields.size == field_matchers.size
|
803
|
+
end
|
804
|
+
|
805
|
+
def children
|
806
|
+
find_children @field_matchers
|
807
|
+
end
|
808
|
+
|
809
|
+
def to_s
|
810
|
+
assign_to_s + "#{@algebraic_type.name}.(#{@field_matchers.join(',')})"
|
811
|
+
end
|
812
|
+
|
813
|
+
def ==(other)
|
814
|
+
other.kind_of? self.class and
|
815
|
+
self.algebraic_type == other.algebraic_type and
|
816
|
+
self.field_matchers == other.field_matchers
|
817
|
+
end
|
818
|
+
|
819
|
+
protected
|
820
|
+
|
821
|
+
def matching?(other)
|
822
|
+
other.kind_of?(@algebraic_type) and other.kind_of?(ProductConstructor) and
|
823
|
+
@field_matchers.zip(other.fields).all? do |matcher, field|
|
824
|
+
matcher === field
|
825
|
+
end
|
826
|
+
end
|
827
|
+
end
|
828
|
+
|
829
|
+
class Variant < Wrapper
|
830
|
+
def initialize(something)
|
831
|
+
is_kind_of! something, Algebrick::Variant, Algebrick::ProductVariant
|
832
|
+
super something
|
833
|
+
end
|
834
|
+
|
835
|
+
def to_s
|
836
|
+
assign_to_s + "#{@something.name}.to_m"
|
837
|
+
end
|
838
|
+
end
|
839
|
+
|
840
|
+
class Atom < Wrapper
|
841
|
+
def initialize(something)
|
842
|
+
is_kind_of! something, Algebrick::Atom
|
843
|
+
super something
|
844
|
+
end
|
845
|
+
|
846
|
+
def to_s
|
847
|
+
assign_to_s + "#{@something.name}.to_m"
|
848
|
+
end
|
849
|
+
end
|
850
|
+
end
|
851
|
+
|
852
|
+
module DSL
|
853
|
+
class PreType
|
854
|
+
attr_reader :environment, :name, :fields, :variants, :definition
|
855
|
+
|
856
|
+
def initialize(environment, name)
|
857
|
+
@environment = environment
|
858
|
+
@name = name
|
859
|
+
@fields = []
|
860
|
+
@variants = nil
|
861
|
+
@definition = nil
|
862
|
+
end
|
863
|
+
|
864
|
+
def |(other)
|
865
|
+
[self, other]
|
866
|
+
end
|
867
|
+
|
868
|
+
def to_ary
|
869
|
+
[self]
|
870
|
+
end
|
871
|
+
|
872
|
+
def fields=(fields)
|
873
|
+
raise unless @fields.empty?
|
874
|
+
@fields += fields
|
875
|
+
end
|
876
|
+
|
877
|
+
def definition=(block)
|
878
|
+
raise if @definition
|
879
|
+
@definition = block
|
880
|
+
end
|
881
|
+
|
882
|
+
def is(variants)
|
883
|
+
raise if @variants
|
884
|
+
@variants = variants
|
885
|
+
self
|
886
|
+
end
|
887
|
+
|
888
|
+
alias_method :===, :is
|
889
|
+
|
890
|
+
def kind
|
891
|
+
if @variants
|
892
|
+
if @fields.empty?
|
893
|
+
Variant
|
894
|
+
else
|
895
|
+
ProductVariant
|
896
|
+
end
|
897
|
+
else
|
898
|
+
if @fields.empty?
|
899
|
+
Atom
|
900
|
+
else
|
901
|
+
Product
|
902
|
+
end
|
903
|
+
end
|
904
|
+
end
|
905
|
+
end
|
906
|
+
|
907
|
+
class Environment
|
908
|
+
attr_reader :pre_types
|
909
|
+
def initialize(base, &definition)
|
910
|
+
@base = if base.is_a?(Object) && base.to_s == 'main'
|
911
|
+
Object
|
912
|
+
else
|
913
|
+
base
|
914
|
+
end
|
915
|
+
@pre_types = {}
|
916
|
+
instance_eval &definition
|
917
|
+
end
|
918
|
+
|
919
|
+
def method_missing(method, *fields, &definition)
|
920
|
+
const_name = method.to_s.split('_').map { |s| s[0] = s[0].upcase; s }.join
|
921
|
+
|
922
|
+
@pre_types[const_name] ||= PreType.new(self, const_name)
|
923
|
+
@pre_types[const_name].fields = fields unless fields.empty?
|
924
|
+
@pre_types[const_name].definition = definition if definition
|
925
|
+
@pre_types[const_name]
|
926
|
+
end
|
927
|
+
|
928
|
+
def run
|
929
|
+
define_constants
|
930
|
+
define_fields_and_variants
|
931
|
+
eval_definitions
|
932
|
+
@pre_types.map { |name, _| get_class name }
|
933
|
+
end
|
934
|
+
|
935
|
+
private
|
936
|
+
|
937
|
+
def define_constants
|
938
|
+
@pre_types.each do |name, pre_type|
|
939
|
+
type = pre_type.kind.allocate
|
940
|
+
if @base.const_defined? name
|
941
|
+
defined = @base.const_get(name)
|
942
|
+
# #unless defined == type
|
943
|
+
raise "#{name} already defined as #{defined}"
|
944
|
+
# #end
|
945
|
+
else
|
946
|
+
#puts "defining #{name.to_sym.inspect} in #{@base}"
|
947
|
+
@base.const_set name.to_sym, type
|
948
|
+
end
|
949
|
+
end
|
950
|
+
end
|
951
|
+
|
952
|
+
def define_fields_and_variants
|
953
|
+
select = ->(klass, &block) do
|
954
|
+
@pre_types.select { |_, pre_type| pre_type.kind == klass }.
|
955
|
+
map { |name, pre_type| [name, get_class(name), pre_type] }.
|
956
|
+
each &block
|
957
|
+
end
|
958
|
+
|
959
|
+
select.(Atom) do |name, type, pre_type|
|
960
|
+
type.send :initialize
|
961
|
+
end
|
962
|
+
|
963
|
+
select.(Product) do |name, type, pre_type|
|
964
|
+
type.send :initialize, *pre_type.fields.map { |f| get_class f }
|
965
|
+
end
|
966
|
+
|
967
|
+
select.(Variant) do |name, type, pre_type|
|
968
|
+
type.send :initialize, *pre_type.variants.map { |v| get_class v }
|
969
|
+
end
|
970
|
+
|
971
|
+
select.(ProductVariant) do |name, type, pre_type|
|
972
|
+
type.send :initialize,
|
973
|
+
pre_type.fields.map { |f| get_class f },
|
974
|
+
pre_type.variants.map { |v| get_class v }
|
975
|
+
end
|
976
|
+
end
|
977
|
+
|
978
|
+
def eval_definitions
|
979
|
+
@pre_types.each do |name, pre_type|
|
980
|
+
next unless pre_type.definition
|
981
|
+
type = get_class name
|
982
|
+
type.module_eval &pre_type.definition
|
983
|
+
end
|
984
|
+
end
|
985
|
+
|
986
|
+
def get_class(key)
|
987
|
+
if key.kind_of? String
|
988
|
+
@base.const_get key
|
989
|
+
elsif key.kind_of? PreType
|
990
|
+
@base.const_get key.name
|
991
|
+
elsif key.kind_of? Hash
|
992
|
+
key.each { |k, v| key[k] = get_class v }
|
993
|
+
else
|
994
|
+
key
|
995
|
+
end
|
996
|
+
end
|
997
|
+
end
|
998
|
+
|
999
|
+
def type_def(&definition)
|
1000
|
+
Environment.new(self, &definition).run
|
1001
|
+
end
|
1002
|
+
end
|
1003
|
+
end
|
metadata
ADDED
@@ -0,0 +1,135 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: algebrick
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Petr Chalupa
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-05-14 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: minitest
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: turn
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: pry
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: yard
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - '>='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: kramdown
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - '>='
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - '>='
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: multi_json
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - '>='
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - '>='
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
description: Provides algebraic type definitions and pattern matching
|
98
|
+
email: git@pitr.ch
|
99
|
+
executables: []
|
100
|
+
extensions: []
|
101
|
+
extra_rdoc_files:
|
102
|
+
- MIT-LICENSE
|
103
|
+
- README.md
|
104
|
+
- README_FULL.md
|
105
|
+
files:
|
106
|
+
- lib/algebrick.rb
|
107
|
+
- MIT-LICENSE
|
108
|
+
- README.md
|
109
|
+
- README_FULL.md
|
110
|
+
homepage: https://github.com/pitr-ch/algebrick
|
111
|
+
licenses:
|
112
|
+
- MIT
|
113
|
+
metadata: {}
|
114
|
+
post_install_message:
|
115
|
+
rdoc_options: []
|
116
|
+
require_paths:
|
117
|
+
- lib
|
118
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
119
|
+
requirements:
|
120
|
+
- - '>='
|
121
|
+
- !ruby/object:Gem::Version
|
122
|
+
version: '0'
|
123
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
124
|
+
requirements:
|
125
|
+
- - '>='
|
126
|
+
- !ruby/object:Gem::Version
|
127
|
+
version: '0'
|
128
|
+
requirements: []
|
129
|
+
rubyforge_project:
|
130
|
+
rubygems_version: 2.0.0
|
131
|
+
signing_key:
|
132
|
+
specification_version: 4
|
133
|
+
summary: Algebraic types and pattern matching for Ruby
|
134
|
+
test_files: []
|
135
|
+
has_rdoc:
|