adt 0.0.1 → 0.0.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.
- 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: ""
|