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.
@@ -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
- Now:
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
- Later...
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
- mine.ok? # <= true
39
- mine.when_missing(proc { "I'm missing!" }, proc { "It's okay I'm around" })
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 aN to access arguments. The result should be
60
- # evalled at the call site
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.class.send(:define_method, name) do |*args|
83
- the_proc = eval(proc_create[num_cases, "a", "a#{index+1}.call(*args)"])
84
- self.new(&the_proc)
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? && begin
102
- fold(*cases.map { |(cn, args)|
103
- inner_check = proc_create[args.count, "o", (1..(args.count)).to_a.map { |idx| "s#{idx} == o#{idx}" }.<<("true").join(' && ')]
104
- eval(proc_create[args.count, "s", "other.when_#{cn}(#{inner_check}, proc { false })"])
105
- })
106
- end
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
-
@@ -1,4 +1,5 @@
1
1
  module ADT
2
+ # @private
2
3
  class CaseRecorder
3
4
  alias :__instance_eval :instance_eval
4
5
 
@@ -10,6 +11,7 @@ module ADT
10
11
  @_church_cases = []
11
12
  end
12
13
 
14
+ # Defines a case for an ADT.
13
15
  def define_case(sym, *args)
14
16
  @_church_cases << [sym, args]
15
17
  end
metadata CHANGED
@@ -2,7 +2,7 @@
2
2
  name: adt
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease:
5
- version: 0.0.1
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: ""