adt 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: ""