adt 0.0.1

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