adt 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +59 -0
- data/lib/adt.rb +155 -0
- data/lib/adt/case_recorder.rb +21 -0
- metadata +58 -0
data/README.md
ADDED
@@ -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
|
data/lib/adt.rb
ADDED
@@ -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
|
+
|