adt 0.0.1

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.
Files changed (4) hide show
  1. data/README.md +59 -0
  2. data/lib/adt.rb +155 -0
  3. data/lib/adt/case_recorder.rb +21 -0
  4. metadata +58 -0
@@ -0,0 +1,59 @@
1
+ ADT
2
+ ===
3
+
4
+ A library for declaring algebraic data types in Ruby.
5
+
6
+ Usage
7
+ -----
8
+
9
+ gem install adt
10
+
11
+ Now:
12
+
13
+ require 'adt'
14
+ class ValidatedValue
15
+ extend ADT
16
+ cases do
17
+ ok(:value)
18
+ missing
19
+ invalid(:reason)
20
+ end
21
+ end
22
+
23
+ Later...
24
+
25
+ # Create values
26
+ mine = ValidatedValue.ok(5)
27
+ missing = ValidatedValue.missing
28
+ invalid = ValidatedValue.invalid("Wrong number!")
29
+
30
+ # Define operations on a value, only the proc matching the value's case will be
31
+ # executed
32
+ mine.fold(
33
+ proc { |value| value },
34
+ proc { "missing default" }
35
+ proc { |reason| raise "gah. Invalid is terrible" }
36
+ )
37
+
38
+ mine.ok? # <= true
39
+ mine.when_missing(proc { "I'm missing!" }, proc { "It's okay I'm around" })
40
+
41
+ mine.fold(
42
+ :ok => proc { |value| value },
43
+ :missing => proc { "missing default " },
44
+ :invalid => proc { |reason| raise "gah. Invalid is terrible!" }
45
+ )
46
+
47
+ # == does what you expect.
48
+ mine == ValidatedValue.missing # <= false
49
+ mine == ValidatedValue.ok(5) # <= true
50
+
51
+ # Inspect looks good.
52
+ mine.inspect # <= "#<ValidatedValue ok value:5>"
53
+
54
+ More Information on ADTs
55
+ ------------------------
56
+
57
+ * http://blog.tmorris.net/algebraic-data-types-again/
58
+ * http://en.wikibooks.org/wiki/Haskell/Type_declarations#data_and_constructor_functions
59
+ * http://en.wikipedia.org/wiki/Algebraic_data_type
@@ -0,0 +1,155 @@
1
+ require 'adt/case_recorder'
2
+
3
+ module ADT
4
+ module_function
5
+
6
+ # Configures the class to be an ADT. Cases are defined by calling
7
+ # methods named for the case, and providing symbol arguments for
8
+ # the parameters to the case.
9
+ #
10
+ # eg.
11
+ #
12
+ # class Validation
13
+ # extend ADT
14
+ # cases do
15
+ # success(:values)
16
+ # failure(:errors, :position)
17
+ # end
18
+ # end
19
+ #
20
+ # This will provde 2 core pieces of functionality.
21
+ #
22
+ # 1. Constructors, as class methods, named the same as the case, and expecting
23
+ # parameters as per the symbol arguments provided in the `cases` block.
24
+ # @failure = Validation.failure(["error1"], 5)
25
+ # @success = Validation.success([1,2])
26
+ # 2. #fold. This method takes a proc for every case. If the case has parameters, those
27
+ # will be passed to the proc. The proc matching the particular value of the case will
28
+ # be called. Using this method, every instance method for the ADT can be defined.
29
+ # @failure.fold(
30
+ # proc { |values| "We are a success! #{values} "},
31
+ # proc { |errors, position| "Failed :(, at position #{position}" }
32
+ # )
33
+ # It can also be passed a hash of procs, keyed by case name:
34
+ # @failure.fold(
35
+ # :success => proc { |values| values },
36
+ # :failures => proc { |errors, position| [] }
37
+ # )
38
+ #
39
+ # In addition, a number of helper methods are defined:
40
+ #
41
+ # * Standard object methods: #==, #inspect
42
+ # * Case checking predicates:
43
+ # some_validation.success?
44
+ # some_validation.failure?
45
+ # * Functions for handling specific cases:
46
+ # some_validation.when_success(proc { |values| values }, proc { [] })
47
+ #
48
+ # @param [Proc] &definitions block which defines the constructors. This will be evaluated using
49
+ # #instance_eval to record the cases.
50
+ #
51
+ def cases(&definitions)
52
+ dsl = CaseRecorder.new
53
+ dsl.__instance_eval(&definitions)
54
+
55
+ cases = dsl._church_cases
56
+ num_cases = dsl._church_cases.length
57
+ case_names = dsl._church_cases.map { |x| x[0] }
58
+
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
61
+ proc_create = proc { |argc, prefix, body|
62
+ args = argc > 0 ? "|#{(1..argc).to_a.map { |a| "#{prefix}#{a}" }.join(',')}|" : ""
63
+ "proc { #{args} #{body} }"
64
+ }
65
+
66
+ # Initializer. Should not be used directly.
67
+ define_method(:initialize) do |&fold|
68
+ @fold = fold
69
+ end
70
+
71
+ # The Fold.
72
+ define_method(:fold) do |*args|
73
+ if args.first && args.first.is_a?(Hash) then
74
+ @fold.call(*case_names.map { |cn| args.first.fetch(cn) })
75
+ else
76
+ @fold.call(*args)
77
+ end
78
+ end
79
+
80
+ # The Constructors
81
+ 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)
85
+ end
86
+ end
87
+
88
+ # The usual object helpers
89
+ define_method(:inspect) do
90
+ "#<" + self.class.name + fold(*dsl._church_cases.map { |(cn, case_args)|
91
+ index = 0
92
+ bit = case_args.map { |ca|
93
+ index += 1
94
+ " #{ca}:#\{a#{index}\}"
95
+ }.join('')
96
+ eval(proc_create[case_args.count, "a", " \" #{cn}#{bit}\""])
97
+ }) + ">"
98
+ end
99
+
100
+ 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
107
+ end
108
+
109
+ # Case specific methods
110
+ # eg.
111
+ # cases do foo(:a); bar(:b); end
112
+ cases.each_with_index do |(name, args), idx|
113
+ # Thing.foo(5).foo? # <= true
114
+ # Thing.foo(5).bar? # <= false
115
+ define_method("#{name}?") do
116
+ fold(*case_names.map { |cn|
117
+ eval(proc_create[0, "a", cn == name ? "true" : "false"])
118
+ })
119
+ end
120
+
121
+ # Thing.foo(5).when_foo(proc {|v| v }, proc { 0 }) # <= 5
122
+ # Thing.bar(5).when_foo(proc {|v| v }, proc { 0 }) # <= 0
123
+ define_method("when_#{name}") do |handle, default|
124
+ fold(*case_names.map { |cn|
125
+ if (cn == name)
126
+ proc { |*args| handle.call(*args) }
127
+ else
128
+ default
129
+ end
130
+ })
131
+ end
132
+ end
133
+ end
134
+ end
135
+
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
+
@@ -0,0 +1,21 @@
1
+ module ADT
2
+ class CaseRecorder
3
+ alias :__instance_eval :instance_eval
4
+
5
+ instance_methods.each { |m| undef_method m unless m =~ /(^__|object_id)/ }
6
+
7
+ attr_reader :_church_cases
8
+
9
+ def initialize
10
+ @_church_cases = []
11
+ end
12
+
13
+ def define_case(sym, *args)
14
+ @_church_cases << [sym, args]
15
+ end
16
+
17
+ def method_missing(sym, *args)
18
+ define_case(sym, *args)
19
+ end
20
+ end
21
+ end
metadata ADDED
@@ -0,0 +1,58 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: adt
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.0.1
6
+ platform: ruby
7
+ authors:
8
+ - Nick Partridge
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2011-08-10 00:00:00 +10:00
14
+ default_executable:
15
+ dependencies: []
16
+
17
+ description: Define multiple constructors for a type, then match on them! Get all catamorphic.
18
+ email: nkpart@gmail.com
19
+ executables: []
20
+
21
+ extensions: []
22
+
23
+ extra_rdoc_files: []
24
+
25
+ files:
26
+ - lib/adt/case_recorder.rb
27
+ - lib/adt.rb
28
+ - README.md
29
+ has_rdoc: true
30
+ homepage: ""
31
+ licenses: []
32
+
33
+ post_install_message:
34
+ rdoc_options: []
35
+
36
+ require_paths:
37
+ - lib
38
+ required_ruby_version: !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: "0"
44
+ required_rubygems_version: !ruby/object:Gem::Requirement
45
+ none: false
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: "0"
50
+ requirements: []
51
+
52
+ rubyforge_project:
53
+ rubygems_version: 1.6.1
54
+ signing_key:
55
+ specification_version: 3
56
+ summary: Algebraic data type DSL
57
+ test_files: []
58
+