maroon 0.6.1 → 0.6.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Examples/Dijkstra/CalculateShortestDistance.rb +16 -12
- data/Examples/Dijkstra/calculate_shortest_path.rb +47 -27
- data/Examples/Dijkstra/data.rb +41 -14
- data/Examples/Dijkstra/dijkstra.rb +4 -3
- data/Examples/MoneyTransfer.rb +61 -60
- data/Examples/greeter.rb +8 -7
- data/Examples/meter.rb +35 -29
- data/Gemfile +9 -4
- data/LICENSE.txt +21 -21
- data/README.md +13 -0
- data/Rakefile +41 -1
- data/Test/Generate/method_info_test.rb +12 -0
- data/Test/{Greeter_test.rb → Greeter_test_disabled.rb} +24 -20
- data/Test/ImmutableQueue_test.rb +18 -0
- data/Test/MethodInfo_test.rb +65 -0
- data/Test/alltests.rb +1 -0
- data/Test/{source_assertions.rb → assertions.rb} +15 -7
- data/Test/bind_test.rb +13 -0
- data/Test/expression_test.rb +105 -0
- data/Test/method_call_test.rb +83 -0
- data/Test/self_test.rb +46 -0
- data/Test/stack_test.rb +17 -0
- data/Test/test_helper.rb +14 -0
- data/base/ImmutableStack.rb +32 -0
- data/base/MethodDefinition.rb +124 -0
- data/base/bind_rewriter.rb +58 -0
- data/base/immutable_queue.rb +50 -0
- data/base/maroon_base.rb +267 -0
- data/base/method_call.rb +78 -0
- data/base/method_info.rb +86 -0
- data/base/self.rb +60 -0
- data/generated/build.rb +13 -0
- data/generated/interpretation_context.rb +29 -0
- data/lib/Bind.rb +65 -0
- data/lib/Context.rb +187 -0
- data/lib/ImmutableQueue.rb +50 -0
- data/lib/ImmutableStack.rb +38 -0
- data/lib/MethodCall.rb +91 -0
- data/lib/MethodDefinition.rb +114 -0
- data/lib/MethodInfo.rb +78 -0
- data/lib/Self.rb +71 -0
- data/lib/build.rb +14 -0
- data/lib/interpretation_context.rb +30 -0
- data/lib/maroon/contracts.rb +43 -0
- data/lib/maroon/kernel.rb +2 -0
- data/lib/maroon/version.rb +3 -3
- data/maroon.gemspec +26 -26
- metadata +49 -31
- data/lib/Source_cleaner.rb +0 -34
- data/lib/maroon.rb +0 -165
- data/lib/rewriter.rb +0 -185
data/base/method_info.rb
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
context :MethodInfo do
|
2
|
+
initialize do |on_self, block_source, is_private|
|
3
|
+
raise 'Must be S-Expressions' unless block_source.instance_of? Sexp
|
4
|
+
|
5
|
+
if on_self.instance_of? Hash
|
6
|
+
@block = on_self[:block]
|
7
|
+
@on_self = on_self[:self]
|
8
|
+
else
|
9
|
+
@on_self = on_self
|
10
|
+
end
|
11
|
+
@block_source = block_source
|
12
|
+
@private = is_private
|
13
|
+
self.freeze
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
role :on_self do
|
18
|
+
end
|
19
|
+
|
20
|
+
role :block do
|
21
|
+
end
|
22
|
+
|
23
|
+
role :block_source do
|
24
|
+
get_arguments {
|
25
|
+
sexp = block_source[2]
|
26
|
+
return nil unless sexp
|
27
|
+
return sexp[1] if sexp[0] == :lasgn
|
28
|
+
return [] if sexp[1] == nil
|
29
|
+
sexp = sexp[1..-1]
|
30
|
+
args = []
|
31
|
+
sexp.each do |e|
|
32
|
+
args << if e.instance_of? Symbol
|
33
|
+
e
|
34
|
+
else
|
35
|
+
if e[0] == :splat
|
36
|
+
'*' + e[1][1].to_s
|
37
|
+
else
|
38
|
+
e[1]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
if block
|
44
|
+
b = '&' + block.to_s
|
45
|
+
if args
|
46
|
+
unless args.instance_of? Array
|
47
|
+
args = [args]
|
48
|
+
end
|
49
|
+
args << b
|
50
|
+
else
|
51
|
+
args = [b]
|
52
|
+
end
|
53
|
+
end
|
54
|
+
args
|
55
|
+
}
|
56
|
+
|
57
|
+
arguments {
|
58
|
+
args = block_source.get_arguments
|
59
|
+
args && args.length ? args.join(',') : nil
|
60
|
+
}
|
61
|
+
|
62
|
+
body {
|
63
|
+
block_source[3]
|
64
|
+
}
|
65
|
+
end
|
66
|
+
|
67
|
+
is_private do
|
68
|
+
@private
|
69
|
+
end
|
70
|
+
|
71
|
+
build_as_context_method do |context_method_name, interpretation_context|
|
72
|
+
MethodDefinition.new(block_source.body, interpretation_context).transform
|
73
|
+
body = Ruby2Ruby.new.process(block_source.body)
|
74
|
+
args = block_source.arguments ? '(' + block_source.arguments + ')' : ""
|
75
|
+
on = if on_self then
|
76
|
+
'self.'
|
77
|
+
else
|
78
|
+
''
|
79
|
+
end
|
80
|
+
'
|
81
|
+
def ' + on.to_s + context_method_name.to_s + args +'
|
82
|
+
' + body +'
|
83
|
+
end
|
84
|
+
'
|
85
|
+
end
|
86
|
+
end
|
data/base/self.rb
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
context :Self, :execute do
|
2
|
+
initialize do |abstract_syntax_tree, interpretationcontext|
|
3
|
+
raise 'Interpretation context missing' unless interpretationcontext
|
4
|
+
raise 'Must have a defining role' unless interpretationcontext.defining_role
|
5
|
+
|
6
|
+
@abstract_syntax_tree = abstract_syntax_tree
|
7
|
+
@interpretation_context = interpretationcontext
|
8
|
+
end
|
9
|
+
|
10
|
+
role :abstract_syntax_tree do
|
11
|
+
is_indexer_call_on_self do
|
12
|
+
abstract_syntax_tree.length == 4 &&
|
13
|
+
abstract_syntax_tree[0] == :call &&
|
14
|
+
abstract_syntax_tree[1] == nil &&
|
15
|
+
abstract_syntax_tree[2] == :[] &&
|
16
|
+
abstract_syntax_tree[3][0] == :argslist
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
role :interpretation_context do
|
21
|
+
defining_role do
|
22
|
+
interpretation_context.defining_role
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# rewrites a call to self in a role method to a call to the role player accessor
|
27
|
+
# which is subsequently rewritten to a call to the instance variable itself
|
28
|
+
# in the case where no role method is called on the role player
|
29
|
+
# It's rewritten to an instance call on the context object if a role method is called
|
30
|
+
execute do
|
31
|
+
if abstract_syntax_tree
|
32
|
+
if abstract_syntax_tree[0] == :self #if self is used in a role method, then rewrite to role getter
|
33
|
+
abstract_syntax_tree[0] = :call
|
34
|
+
abstract_syntax_tree[1] = nil
|
35
|
+
abstract_syntax_tree[2] = interpretation_context.defining_role
|
36
|
+
elsif abstract_syntax_tree[0] == :call and abstract_syntax_tree[1] == nil
|
37
|
+
method_name = abstract_syntax_tree[2]
|
38
|
+
#self is removed from S-expressions
|
39
|
+
if method_name == :[] or method_name == :[]=
|
40
|
+
get_role = Sexp.new
|
41
|
+
get_role[0] = :call
|
42
|
+
get_role[1] = nil
|
43
|
+
get_role[2] = interpretation_context.defining_role
|
44
|
+
abstract_syntax_tree[1] = get_role
|
45
|
+
end
|
46
|
+
elsif abstract_syntax_tree.instance_of? Sexp
|
47
|
+
if abstract_syntax_tree.is_indexer_call_on_self
|
48
|
+
getter = new Sexp.new
|
49
|
+
getter[0] = :call
|
50
|
+
getter[1] = nil
|
51
|
+
getter[2] = interpretation_context.defining_role
|
52
|
+
arglist = Sexp.new
|
53
|
+
getter[3] = arglist
|
54
|
+
arglist[0] = :arglist
|
55
|
+
abstract_syntax_tree[1] = getter
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
data/generated/build.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'sorcerer'
|
2
|
+
require 'sourcify'
|
3
|
+
|
4
|
+
require_relative './Self'
|
5
|
+
require_relative './Bind'
|
6
|
+
require_relative './ImmutableStack'
|
7
|
+
require_relative './ImmutableQueue'
|
8
|
+
require_relative './interpretation_context'
|
9
|
+
require_relative './MethodInfo'
|
10
|
+
require_relative './MethodCall'
|
11
|
+
require_relative './MethodDefinition'
|
12
|
+
require_relative './Context'
|
13
|
+
|
@@ -0,0 +1,29 @@
|
|
1
|
+
class InterpretationContext
|
2
|
+
def contracts
|
3
|
+
(@contracts ||= {})
|
4
|
+
end
|
5
|
+
|
6
|
+
def roles
|
7
|
+
(@roles ||= {})
|
8
|
+
end
|
9
|
+
|
10
|
+
def role_aliases
|
11
|
+
(@role_aliases ||= {})
|
12
|
+
end
|
13
|
+
|
14
|
+
def defining_role
|
15
|
+
@defining_role
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize(roles, contracts, role_aliases, defining_role)
|
19
|
+
raise "Aliases must be a hash" unless role_aliases.instance_of? Hash or role_aliases == nil
|
20
|
+
raise "Roles must be a hash" unless roles.instance_of? Hash or roles == nil
|
21
|
+
raise "Contracts must be a hash" unless contracts.instance_of? Hash or contracts == nil
|
22
|
+
|
23
|
+
@roles = roles
|
24
|
+
raise "Defining role is undefined" if defining_role && (!self.roles.has_key? defining_role)
|
25
|
+
@contracts = contracts
|
26
|
+
@role_aliases = role_aliases
|
27
|
+
@defining_role = defining_role
|
28
|
+
end
|
29
|
+
end
|
data/lib/Bind.rb
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
class Bind
|
2
|
+
|
3
|
+
def initialize(local,aliased_role,block)
|
4
|
+
@local = local
|
5
|
+
@aliased_role = aliased_role
|
6
|
+
@block = block
|
7
|
+
|
8
|
+
end
|
9
|
+
|
10
|
+
def execute()
|
11
|
+
must_b_sym = "aliased_role must be a Symbol".to_sym
|
12
|
+
local_must_b_sym = "local must be a Symbol".to_sym
|
13
|
+
raise(must_b_sym) unless aliased_role.instance_of?(Symbol)
|
14
|
+
raise(local_must_b_sym) unless local.instance_of?(Symbol)
|
15
|
+
aliased_field = ("@" + aliased_role.to_s).to_sym
|
16
|
+
temp_symbol = ("temp____" + aliased_role.to_s).to_sym
|
17
|
+
assignment = Sexp.new
|
18
|
+
assignment[0] = :iasgn
|
19
|
+
assignment[1] = aliased_field
|
20
|
+
load_arg = Sexp.new
|
21
|
+
load_arg[0] = :lvar
|
22
|
+
load_arg[1] = local
|
23
|
+
assignment[2] = load_arg
|
24
|
+
block.insert(1, assignment)
|
25
|
+
assignment = Sexp.new
|
26
|
+
assignment[0] = :lasgn
|
27
|
+
assignment[1] = temp_symbol
|
28
|
+
load_field = Sexp.new
|
29
|
+
load_field[0] = :ivar
|
30
|
+
load_field[1] = aliased_field
|
31
|
+
assignment[2] = load_field
|
32
|
+
block.insert(1, assignment)
|
33
|
+
assignment = Sexp.new
|
34
|
+
assignment[0] = :iasgn
|
35
|
+
assignment[1] = aliased_field
|
36
|
+
load_temp = Sexp.new
|
37
|
+
load_temp[0] = :lvar
|
38
|
+
load_temp[1] = temp_symbol
|
39
|
+
assignment[2] = load_temp
|
40
|
+
block[block.length] = assignment
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.call(*args)
|
45
|
+
arity = Bind.method(:new).arity
|
46
|
+
newArgs = args[0..arity-1]
|
47
|
+
obj = Bind.new *newArgs
|
48
|
+
if arity < args.length
|
49
|
+
methodArgs = args[arity..-1]
|
50
|
+
obj.execute *methodArgs
|
51
|
+
else
|
52
|
+
obj.execute
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def call(*args);execute *args; end
|
57
|
+
|
58
|
+
private
|
59
|
+
attr_reader :block
|
60
|
+
attr_reader :local
|
61
|
+
attr_reader :aliased_role
|
62
|
+
|
63
|
+
|
64
|
+
|
65
|
+
end
|
data/lib/Context.rb
ADDED
@@ -0,0 +1,187 @@
|
|
1
|
+
class Context
|
2
|
+
|
3
|
+
def self.define(*args,&block)
|
4
|
+
@@with_contracts ||= nil
|
5
|
+
@@generate_file_path ||= nil
|
6
|
+
(alias :method_missing :role_or_interaction_method)
|
7
|
+
base_class, ctx, default_interaction, name = self.send(:create_context_factory, args, block)
|
8
|
+
if (args.last.instance_of?(FalseClass) or args.last.instance_of?(TrueClass)) then
|
9
|
+
ctx.generate_files_in(args.last)
|
10
|
+
end
|
11
|
+
return ctx.send(:finalize, name, base_class, default_interaction)
|
12
|
+
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.generate_files_in(*args,&b)
|
16
|
+
if block_given? then
|
17
|
+
return role_or_interaction_method(:generate_files_in, *args, &b)
|
18
|
+
end
|
19
|
+
@@generate_file_path = args[0]
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def self.with_contracts(*args)
|
26
|
+
return @@with_contracts if (args.length == 0)
|
27
|
+
value = args[0]
|
28
|
+
if @@with_contracts and (not value) then
|
29
|
+
raise("make up your mind! disabling contracts during execution will result in undefined behavior")
|
30
|
+
end
|
31
|
+
@@with_contracts = value
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
def role(*args,&b)
|
36
|
+
role_name = args[0]
|
37
|
+
if (args.length.!=(1) or (not role_name.instance_of?(Symbol))) then
|
38
|
+
return role_or_interaction_method(:role, *args, &b)
|
39
|
+
end
|
40
|
+
@defining_role = role_name
|
41
|
+
@roles[role_name] = Hash.new
|
42
|
+
yield if block_given?
|
43
|
+
@defining_role = nil
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
def initialize(*args,&b)
|
48
|
+
if block_given? then
|
49
|
+
role_or_interaction_method(:initialize, *args, &b)
|
50
|
+
else
|
51
|
+
@roles = Hash.new
|
52
|
+
@interactions = Hash.new
|
53
|
+
@role_alias = Hash.new
|
54
|
+
@contracts = Hash.new
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def private()
|
59
|
+
@private = true
|
60
|
+
end
|
61
|
+
|
62
|
+
def current_interpretation_context(*args,&b)
|
63
|
+
if block_given? then
|
64
|
+
return role_or_interaction_method(:current_interpretation_context, *args, &b)
|
65
|
+
end
|
66
|
+
InterpretationContext.new(roles, contracts, role_alias, nil)
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
def get_methods(*args,&b)
|
71
|
+
return role_or_interaction_method(:get_methods, *args, &b) if block_given?
|
72
|
+
name = args[0]
|
73
|
+
sources = (@defining_role ? (@roles[@defining_role]) : (@interactions))[name]
|
74
|
+
if @defining_role and (not sources) then
|
75
|
+
@roles[@defining_role][name] = []
|
76
|
+
else
|
77
|
+
@interactions[name] = []
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
|
82
|
+
def add_method(*args,&b)
|
83
|
+
return role_or_interaction_method(:add_method, *args, &b) if block_given?
|
84
|
+
name, method = *args
|
85
|
+
sources = get_methods(name)
|
86
|
+
(sources << method)
|
87
|
+
|
88
|
+
end
|
89
|
+
|
90
|
+
def finalize(*args,&b)
|
91
|
+
return role_or_interaction_method(:finalize, *args, &b) if block_given?
|
92
|
+
name, base_class, default = *args
|
93
|
+
code = generate_context_code(default, name)
|
94
|
+
if @@generate_file_path then
|
95
|
+
name = name.to_s
|
96
|
+
complete = ((((("class " + name) + (base_class ? (("<< " + base_class.name)) : (""))) + "\n ") + code.to_s) + "\n end")
|
97
|
+
File.open((((("./" + @@generate_file_path.to_s) + "/") + name) + ".rb"), "w") do |f|
|
98
|
+
f.write(complete)
|
99
|
+
end
|
100
|
+
complete
|
101
|
+
else
|
102
|
+
c = base_class ? (Class.new(base_class)) : (Class.new)
|
103
|
+
if @@with_contracts then
|
104
|
+
c.class_eval("def self.assert_that(obj)\n ContextAsserter.new(self.contracts,obj)\n end\n def self.refute_that(obj)\n ContextAsserter.new(self.contracts,obj,false)\n end\n def self.contracts\n @@contracts\n end\n def self.contracts=(value)\n raise 'Contracts must be supplied' unless value\n @@contracts = value\n end")
|
105
|
+
c.contracts = contracts
|
106
|
+
end
|
107
|
+
Kernel.const_set(name, c)
|
108
|
+
temp = c.class_eval(code)
|
109
|
+
(temp or c)
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
113
|
+
|
114
|
+
def self.create_context_factory(args,block)
|
115
|
+
name, base_class, default_interaction = *args
|
116
|
+
if default_interaction and (not base_class.instance_of?(Class)) then
|
117
|
+
base_class = eval(base_class.to_s)
|
118
|
+
end
|
119
|
+
if base_class and ((not default_interaction) and (not base_class.instance_of?(Class))) then
|
120
|
+
base_class, default_interaction = default_interaction, base_class
|
121
|
+
end
|
122
|
+
ctx = Context.new
|
123
|
+
ctx.instance_eval(&block)
|
124
|
+
return [base_class, ctx, default_interaction, name]
|
125
|
+
|
126
|
+
end
|
127
|
+
|
128
|
+
def generate_context_code(*args,&b)
|
129
|
+
if block_given? then
|
130
|
+
return role_or_interaction_method(:generate_context_code, *args, &b)
|
131
|
+
end
|
132
|
+
default, name = args
|
133
|
+
getters = ""
|
134
|
+
impl = ""
|
135
|
+
interactions = ""
|
136
|
+
@interactions.each do |method_name, methods|
|
137
|
+
methods.each do |method|
|
138
|
+
@defining_role = nil
|
139
|
+
code = (" " + method.build_as_context_method(method_name, current_interpretation_context))
|
140
|
+
method.is_private ? ((getters << code)) : ((interactions << code))
|
141
|
+
end
|
142
|
+
end
|
143
|
+
if default then
|
144
|
+
(interactions << (((((((("\n def self.call(*args)\n arity = " + name.to_s) + ".method(:new).arity\n newArgs = args[0..arity-1]\n obj = ") + name.to_s) + ".new *newArgs\n if arity < args.length\n methodArgs = args[arity..-1]\n obj.") + default.to_s) + " *methodArgs\n else\n obj.") + default.to_s) + "\n end\n end\n "))
|
145
|
+
(interactions << (("\n def call(*args);" + default.to_s) + " *args; end\n"))
|
146
|
+
end
|
147
|
+
@roles.each do |role, methods|
|
148
|
+
(getters << (("attr_reader :" + role.to_s) + "\n "))
|
149
|
+
methods.each do |method_name, method_sources|
|
150
|
+
unless (method_sources.length < 2) then
|
151
|
+
raise(("Duplicate definition of " + method_name.to_s))
|
152
|
+
end
|
153
|
+
unless (method_sources.length > 0) then
|
154
|
+
raise(("No source for " + method_name.to_s))
|
155
|
+
end
|
156
|
+
method_source = method_sources[0]
|
157
|
+
@defining_role = role
|
158
|
+
rewritten_method_name = ((("self_" + role.to_s) + "_") + method_name.to_s)
|
159
|
+
definition = method_source.build_as_context_method(rewritten_method_name, current_interpretation_context)
|
160
|
+
(impl << (" " + definition.to_s)) if definition
|
161
|
+
end
|
162
|
+
end
|
163
|
+
(((((interactions + "\n private\n") + getters) + "\n ") + impl) + "\n ")
|
164
|
+
|
165
|
+
end
|
166
|
+
|
167
|
+
def role_or_interaction_method(*arguments,&b)
|
168
|
+
method_name, on_self = *arguments
|
169
|
+
unless method_name.instance_of?(Symbol) then
|
170
|
+
on_self = method_name
|
171
|
+
method_name = :role_or_interaction_method
|
172
|
+
end
|
173
|
+
raise(("Method with out block " + method_name.to_s)) unless block_given?
|
174
|
+
add_method(method_name, MethodInfo.new(on_self, b.to_sexp, @private))
|
175
|
+
|
176
|
+
end
|
177
|
+
attr_reader :roles
|
178
|
+
attr_reader :interactions
|
179
|
+
attr_reader :defining_role
|
180
|
+
attr_reader :role_alias
|
181
|
+
attr_reader :alias_list
|
182
|
+
attr_reader :cached_roles_and_alias_list
|
183
|
+
attr_reader :contracts
|
184
|
+
|
185
|
+
|
186
|
+
|
187
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
class ImmutableQueue
|
2
|
+
|
3
|
+
def push(element)
|
4
|
+
b = (back or ImmutableStack.empty)
|
5
|
+
ImmutableQueue.new(front, b.push(element))
|
6
|
+
|
7
|
+
end
|
8
|
+
|
9
|
+
def pop()
|
10
|
+
f, b = front, back
|
11
|
+
if (f == ImmutableStack.empty) then
|
12
|
+
until (b == ImmutableStack.empty) do
|
13
|
+
(e, b = b.pop
|
14
|
+
f = f.push(e))
|
15
|
+
end
|
16
|
+
end
|
17
|
+
head, f = f.pop
|
18
|
+
if (f == b) then
|
19
|
+
[head, ImmutableQueue.empty]
|
20
|
+
else
|
21
|
+
[head, ImmutableQueue.new(f, b)]
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.empty()
|
27
|
+
@@empty ||= ImmutableQueue.new(ImmutableStack.empty, ImmutableStack.empty)
|
28
|
+
end
|
29
|
+
|
30
|
+
def push_array(arr)
|
31
|
+
q = self
|
32
|
+
arr.each { |i| q = q.push(i) } if arr
|
33
|
+
q
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def initialize(front,back)
|
40
|
+
@front = (front or ImmutableStack.empty)
|
41
|
+
@back = (back or ImmutableStack.empty)
|
42
|
+
self.freeze
|
43
|
+
|
44
|
+
end
|
45
|
+
attr_reader :front
|
46
|
+
attr_reader :back
|
47
|
+
|
48
|
+
|
49
|
+
|
50
|
+
end
|