adt 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +12 -0
- data/README.md +54 -8
- data/lib/adt.rb +73 -31
- data/lib/adt/case_recorder.rb +2 -0
- metadata +2 -1
data/CHANGELOG.md
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
|
2
|
+
0.0.2
|
3
|
+
=====
|
4
|
+
|
5
|
+
* Added alias to #fold, named the same as the class in underscore form. ie. for CertStatus, value.fold <=> value.cert_status
|
6
|
+
* Added special methods for enumerations (defined as ADTs with only nullary constructors): #all_values, #to_i/::from_i
|
7
|
+
* Added caching of the values for nullary constructors, ie. Maybe.nothing.object_id == Maybe.nothing.object_id
|
8
|
+
* Added #to_a: simplifies #==
|
9
|
+
* Added #<=>
|
10
|
+
* Added case information methods: #case_name, #case_index (1-based), #case_arity
|
11
|
+
* BUG: Constructors were being defined on every class that extended ADT.
|
12
|
+
|
data/README.md
CHANGED
@@ -8,25 +8,48 @@ Usage
|
|
8
8
|
|
9
9
|
gem install adt
|
10
10
|
|
11
|
-
|
11
|
+
ADT provides a DSL for specifying the cases in an algebraic data type.
|
12
12
|
|
13
13
|
require 'adt'
|
14
14
|
class ValidatedValue
|
15
15
|
extend ADT
|
16
16
|
cases do
|
17
|
-
ok(:value)
|
18
17
|
missing
|
19
18
|
invalid(:reason)
|
19
|
+
ok(:value)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# An Enumeration (nullary constructors only)
|
24
|
+
class State
|
25
|
+
extend ADT
|
26
|
+
cases do
|
27
|
+
snafu
|
28
|
+
smoking # 'Nullary contructor' means it takes no arguments
|
20
29
|
end
|
21
30
|
end
|
22
31
|
|
23
|
-
|
32
|
+
What you now have:
|
33
|
+
|
34
|
+
* Constructors for each of the cases: Type._case_(arg)
|
35
|
+
* A `fold` method, for matching on all the cases.
|
36
|
+
* A good #== and #inspect implementation
|
37
|
+
* \#_case_? and #when__case_(handle_case_proc, default_proc) for dealing with a single case
|
38
|
+
|
39
|
+
Check the [documentation](http://rubydoc.info/gems/adt/0.0.2/ADT:cases) for more information.
|
40
|
+
|
41
|
+
Usage examples
|
42
|
+
--------------
|
43
|
+
|
44
|
+
Construction:
|
24
45
|
|
25
46
|
# Create values
|
26
47
|
mine = ValidatedValue.ok(5)
|
27
48
|
missing = ValidatedValue.missing
|
28
49
|
invalid = ValidatedValue.invalid("Wrong number!")
|
29
|
-
|
50
|
+
|
51
|
+
Folding:
|
52
|
+
|
30
53
|
# Define operations on a value, only the proc matching the value's case will be
|
31
54
|
# executed
|
32
55
|
mine.fold(
|
@@ -35,22 +58,45 @@ Later...
|
|
35
58
|
proc { |reason| raise "gah. Invalid is terrible" }
|
36
59
|
)
|
37
60
|
|
38
|
-
|
39
|
-
mine.
|
40
|
-
|
41
|
-
mine.fold(
|
61
|
+
# Use an alias to #fold, named after the type:
|
62
|
+
mine.validated_value(
|
42
63
|
:ok => proc { |value| value },
|
43
64
|
:missing => proc { "missing default " },
|
44
65
|
:invalid => proc { |reason| raise "gah. Invalid is terrible!" }
|
45
66
|
)
|
46
67
|
|
68
|
+
Support methods:
|
69
|
+
|
70
|
+
mine.ok? # <= true
|
71
|
+
mine.when_missing(proc { "I'm missing!" }, proc { "It's okay I'm around" })
|
72
|
+
|
47
73
|
# == does what you expect.
|
48
74
|
mine == ValidatedValue.missing # <= false
|
49
75
|
mine == ValidatedValue.ok(5) # <= true
|
50
76
|
|
77
|
+
# <=>
|
78
|
+
ValidatedValue.ok(5) <=> ValidatedValue.ok(3) # <= 1 # Ordering is by the inner value(s), if the cases match
|
79
|
+
ValidatedValue.ok(5) <=> ValidatedValue.missing # <= 1 # Otherwise it is by increasing order in which the cases are defined
|
80
|
+
|
81
|
+
# to_a
|
82
|
+
ValidatedValue.ok(5).to_a == [5]
|
83
|
+
ValidatedValue.missing.to_a = []
|
84
|
+
|
51
85
|
# Inspect looks good.
|
52
86
|
mine.inspect # <= "#<ValidatedValue ok value:5>"
|
53
87
|
|
88
|
+
For the enumeration only:
|
89
|
+
|
90
|
+
State.all_values # <= [State.snafu, State.smoking]
|
91
|
+
State.snafu.to_i # <= 1
|
92
|
+
State.from_i(2) # <= State.smoking
|
93
|
+
|
94
|
+
Case info:
|
95
|
+
|
96
|
+
State.snafu.case_name == "snafu"
|
97
|
+
ValidatedValue.ok(3).case_arity == 1
|
98
|
+
State.snafu.case_index = 2
|
99
|
+
|
54
100
|
More Information on ADTs
|
55
101
|
------------------------
|
56
102
|
|
data/lib/adt.rb
CHANGED
@@ -1,5 +1,17 @@
|
|
1
1
|
require 'adt/case_recorder'
|
2
2
|
|
3
|
+
module StringHelp
|
4
|
+
def self.underscore(camel_cased_word)
|
5
|
+
word = camel_cased_word.to_s.dup
|
6
|
+
word.gsub!(/::/, '/')
|
7
|
+
word.gsub!(/([A-Z]+)([A-Z][a-z])/,'\1_\2')
|
8
|
+
word.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
|
9
|
+
word.tr!("-", "_")
|
10
|
+
word.downcase!
|
11
|
+
word
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
3
15
|
module ADT
|
4
16
|
module_function
|
5
17
|
|
@@ -39,25 +51,40 @@ module ADT
|
|
39
51
|
# In addition, a number of helper methods are defined:
|
40
52
|
#
|
41
53
|
# * Standard object methods: #==, #inspect
|
54
|
+
# * Conversion to an array of the arguments: #to_a (nullary constructors return empty arrays)
|
55
|
+
# * #<=> and Comparable: cases are compared by index, and then by their parameters as an array
|
42
56
|
# * Case checking predicates:
|
43
57
|
# some_validation.success?
|
44
58
|
# some_validation.failure?
|
45
59
|
# * Functions for handling specific cases:
|
46
60
|
# some_validation.when_success(proc { |values| values }, proc { [] })
|
61
|
+
# * Case information
|
62
|
+
# some_validation.case_name # <= "success"
|
63
|
+
# some_validation.case_index # <= 1 # Indexing is 1-based.
|
64
|
+
# some_validation.case_arity # <= 1 # Number of arguments required by the case
|
65
|
+
# * #fold is aliased to an underscored name of the type. ie. ValidatedValue gets #validated_value
|
66
|
+
#
|
67
|
+
# If the type is an enumeration (it only has nullary constructors), then a few extra methods are available:
|
68
|
+
#
|
69
|
+
# * 1-based conversion to and from integers: #to_i, ::from_i
|
70
|
+
# * Accessor for all values: ::all_values
|
71
|
+
#
|
47
72
|
#
|
48
73
|
# @param [Proc] &definitions block which defines the constructors. This will be evaluated using
|
49
74
|
# #instance_eval to record the cases.
|
50
75
|
#
|
51
76
|
def cases(&definitions)
|
77
|
+
singleton_class = class <<self; self; end
|
52
78
|
dsl = CaseRecorder.new
|
53
79
|
dsl.__instance_eval(&definitions)
|
54
80
|
|
55
81
|
cases = dsl._church_cases
|
56
82
|
num_cases = dsl._church_cases.length
|
57
83
|
case_names = dsl._church_cases.map { |x| x[0] }
|
84
|
+
is_enumeration = dsl._church_cases.all?{ |(_, args)| args.count == 0 }
|
58
85
|
|
59
|
-
# creates procs with a certain arg count. body should use
|
60
|
-
#
|
86
|
+
# creates procs with a certain arg count. body should use #{prefix}N to access arguments. The result should be
|
87
|
+
# eval'ed at the call site
|
61
88
|
proc_create = proc { |argc, prefix, body|
|
62
89
|
args = argc > 0 ? "|#{(1..argc).to_a.map { |a| "#{prefix}#{a}" }.join(',')}|" : ""
|
63
90
|
"proc { #{args} #{body} }"
|
@@ -77,14 +104,41 @@ module ADT
|
|
77
104
|
end
|
78
105
|
end
|
79
106
|
|
107
|
+
# If we're inside a named class, then set up an alias to fold
|
108
|
+
define_method(StringHelp.underscore(name.split('::').last)) do |*args| fold(*args) end
|
109
|
+
|
80
110
|
# The Constructors
|
81
111
|
dsl._church_cases.each_with_index do |(name, case_args), index|
|
82
|
-
self.
|
83
|
-
|
84
|
-
|
112
|
+
constructor = proc { |*args| self.new(&eval(proc_create[num_cases, "a", "a#{index+1}.call(*args)"])) }
|
113
|
+
if case_args.size > 0 then
|
114
|
+
singleton_class.send(:define_method, name, &constructor)
|
115
|
+
else
|
116
|
+
# Cache the constructed value if it is unary
|
117
|
+
singleton_class.send(:define_method, name) do
|
118
|
+
instance_variable_get("@#{name}") || begin
|
119
|
+
instance_variable_set("@#{name}", constructor.call)
|
120
|
+
end
|
121
|
+
end
|
85
122
|
end
|
86
123
|
end
|
87
124
|
|
125
|
+
# Case info methods
|
126
|
+
# Indexing is 1-based
|
127
|
+
define_method(:case_index) do fold(*(1..case_names.length).to_a.map { |i| proc { i } }) end
|
128
|
+
define_method(:case_name) do fold(*case_names.map { |i| proc { i.to_s } }) end
|
129
|
+
define_method(:case_arity) do fold(*dsl._church_cases.map { |(_, args)| proc { args.count } }) end
|
130
|
+
|
131
|
+
# Enumerations are defined as classes with cases that don't take arguments. A number of useful
|
132
|
+
# functions can be defined for these.
|
133
|
+
if is_enumeration
|
134
|
+
singleton_class.send(:define_method, :all_values) do
|
135
|
+
@all_values ||= case_names.map { |x| send(x) }
|
136
|
+
end
|
137
|
+
|
138
|
+
define_method(:to_i) { case_index }
|
139
|
+
singleton_class.send(:define_method, :from_i) do |idx| send(case_names[idx - 1]) end
|
140
|
+
end
|
141
|
+
|
88
142
|
# The usual object helpers
|
89
143
|
define_method(:inspect) do
|
90
144
|
"#<" + self.class.name + fold(*dsl._church_cases.map { |(cn, case_args)|
|
@@ -98,14 +152,22 @@ module ADT
|
|
98
152
|
end
|
99
153
|
|
100
154
|
define_method(:==) do |other|
|
101
|
-
!other.nil? &&
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
155
|
+
!other.nil? && case_index == other.case_index && to_a == other.to_a
|
156
|
+
end
|
157
|
+
|
158
|
+
define_method(:to_a) do
|
159
|
+
fold(*cases.map { |(cn, args)|
|
160
|
+
eval(proc_create[args.count, "a", "[" + (1..args.count).to_a.map { |idx| "a#{idx}" }.join(',') + "]"])
|
161
|
+
})
|
107
162
|
end
|
108
163
|
|
164
|
+
# Comparisons are done by index, then by the values within the case (if any) via #to_a
|
165
|
+
define_method(:<=>) do |other|
|
166
|
+
comp = case_index <=> other.case_index
|
167
|
+
comp == 0 ? to_a <=> other.to_a : comp
|
168
|
+
end
|
169
|
+
include Comparable
|
170
|
+
|
109
171
|
# Case specific methods
|
110
172
|
# eg.
|
111
173
|
# cases do foo(:a); bar(:b); end
|
@@ -133,23 +195,3 @@ module ADT
|
|
133
195
|
end
|
134
196
|
end
|
135
197
|
|
136
|
-
module Kernel
|
137
|
-
# Returns a class configured with cases as specified in the block. See `ADT::cases` for details.
|
138
|
-
#
|
139
|
-
# Maybe = ADT do
|
140
|
-
# just(:value)
|
141
|
-
# nothing
|
142
|
-
# end
|
143
|
-
#
|
144
|
-
# v = Maybe.just(5)
|
145
|
-
#
|
146
|
-
def ADT(&blk)
|
147
|
-
c = Class.new
|
148
|
-
c.instance_eval do
|
149
|
-
extend ADT
|
150
|
-
cases(&blk)
|
151
|
-
end
|
152
|
-
c
|
153
|
-
end
|
154
|
-
end
|
155
|
-
|
data/lib/adt/case_recorder.rb
CHANGED
metadata
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
name: adt
|
3
3
|
version: !ruby/object:Gem::Version
|
4
4
|
prerelease:
|
5
|
-
version: 0.0.
|
5
|
+
version: 0.0.2
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- Nick Partridge
|
@@ -25,6 +25,7 @@ extra_rdoc_files: []
|
|
25
25
|
files:
|
26
26
|
- lib/adt/case_recorder.rb
|
27
27
|
- lib/adt.rb
|
28
|
+
- CHANGELOG.md
|
28
29
|
- README.md
|
29
30
|
has_rdoc: true
|
30
31
|
homepage: ""
|