maroon 0.5.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,84 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require '../../Lib/maroon.rb'
3
+ require './data.rb'
4
+ require './CalculateShortestDistance.rb'
5
+ require './Calculate_Shortest_Path.rb'
6
+ #!/usr/bin/env ruby
7
+ # Example in Ruby -- Dijkstra's algorithm in DCI
8
+ # Modified and simplified for a Manhattan geometry with 8 roles
9
+ #
10
+ #
11
+ #
12
+ # Demonstrates an example where:
13
+ #
14
+ # - objects of class Node play several roles simultaneously
15
+ # (albeit spread across Contexts: a Node can
16
+ # play the CurrentIntersection in one Context and an Eastern or
17
+ # Southern Neighbor in another)
18
+ # - stacked Contexts (to implement recursion)
19
+ # - mixed access of objects of Node through different
20
+ # paths of role elaboration (the root is just a node,
21
+ # whereas others play roles)
22
+ # - there is a significant pre-existing data structure called
23
+ # a Geometry (plays the Map role) which contains the objects
24
+ # of instance. Where DCI comes in is to ascribe roles to those
25
+ # objects and let them interact with each other to evaluate the
26
+ # minimal path through the network
27
+ # - true to core DCI we are almost always concerned about
28
+ # what happens between the objects (paths and distance)
29
+ # rather than in the objects themselves (which have
30
+ # relatively uninteresting properties like "name")
31
+ # - equality of nodes is not identity, and several
32
+ # nodes compare equal with each other by standard
33
+ # equality (eql?)
34
+ # - returns references to the original data objects
35
+ # in a vector, to describe the resulting path
36
+ #
37
+ # There are some curiosities
38
+ #
39
+ # - east_neighbor and south_neighbor were typographically equivalent,
40
+ # so I folded them into a single role: Neighbor. That type still
41
+ # serves the two original roles
42
+ # - Roles are truly scoped to the use case context
43
+ # - The Map and Distance_labeled_graph_node roles have to be
44
+ # duplicated in two Contexts. blah blah blah
45
+ # - Node inheritance is replaced by injecting two roles
46
+ # into the object
47
+ # - Injecting roles no longer adds new fields to existing
48
+ # data objects.
49
+ # - There is an intentional call to distance_between while the
50
+ # Context is still extant, but outside the scope of the
51
+ # Context itself. Should that be legal?
52
+ # - I have added a tentative_distance_values array to the Context
53
+ # to support the algorithm. Its data are shared across the
54
+ # roles of the CalculateShortestPath Context
55
+ # - nearest_unvisited_node_to_target is now a feature of Map,
56
+ # which seems to reflect better coupling than in the old
57
+ # design
58
+
59
+
60
+
61
+ # --- Main Program: test driver
62
+ #
63
+ geometries = Geometry_1.new
64
+ path = CalculateShortestPath.new(geometries.root, geometries.destination, geometries)
65
+ print 'Path is: '
66
+ path.each { |node| print "#{node.name} " }
67
+ print "\n"
68
+ puts "distance is #{CalculateShortestDistance.new(geometries.root, geometries).distance}"
69
+
70
+ puts ''
71
+
72
+ geometries = ManhattanGeometry2.new
73
+ path = CalculateShortestPath.new(geometries.root, geometries.destination, geometries)
74
+ print 'Path is: '
75
+ last_node = nil
76
+ path.each do |node|
77
+ if last_node != nil; print " - #{geometries.distances[Edge.new(node, last_node)]} - " end
78
+ print "#{node.name}"
79
+ last_node = node
80
+ end
81
+ print "\n"
82
+
83
+ geometries = ManhattanGeometry2.new
84
+ puts "distance is #{CalculateShortestDistance.new(geometries.root, geometries).distance }"
@@ -0,0 +1,61 @@
1
+ require '../lib/maroon.rb'
2
+
3
+ Context::define :MoneyTransfer do
4
+ role :source do
5
+ withdraw do |amount|
6
+ source.movement(amount)
7
+ source.log "withdrawal #{amount}"
8
+ end
9
+ log do |message|
10
+ p "#{@source} source #{message}"
11
+ end
12
+ end
13
+
14
+ role :destination do
15
+ deposit do |amount|
16
+ @destination.movement(amount)
17
+ @destination.log "deposit #{amount}"
18
+ end
19
+ logger do |message|
20
+ p "#{@source} destination #{message}"
21
+ end
22
+ end
23
+
24
+ role :amount do end
25
+
26
+ transfer do
27
+ source.withdraw -amount
28
+ destination.deposit amount
29
+ end
30
+ end
31
+
32
+ class MoneyTransfer
33
+ def initialize(source, destination, amount)
34
+ @source = source
35
+ @destination = destination
36
+ @amount = amount
37
+ end
38
+ end
39
+ class Account
40
+ def initialize (amount, id)
41
+ @balance = amount
42
+ @account_id = id
43
+ end
44
+
45
+ def movement(amount)
46
+ log "Amount #{amount}"
47
+ @balance+=amount
48
+ end
49
+
50
+ def log(message)
51
+ (p s = "instance #{message}")
52
+ end
53
+
54
+ def to_s
55
+ "balance of #{@account_id}: #{@balance}"
56
+ end
57
+ end
58
+
59
+ account = Account.new 1000, "source"
60
+ ctx = MoneyTransfer.new account, account, 100
61
+ ctx.transfer
@@ -0,0 +1,45 @@
1
+ #Thanks to Ted Milken for updating the original example
2
+
3
+ require '../lib/maroon.rb'
4
+ require '../lib/Moby/kernel.rb'
5
+
6
+ class Person
7
+ attr_accessor :name
8
+ attr_accessor :greeting
9
+ end
10
+
11
+ context :Greet_Someone, :greet do
12
+ role :greeter do
13
+ welcome do
14
+ self.greeting
15
+ end
16
+ end
17
+
18
+ role :greeted do
19
+ end
20
+
21
+ greet do
22
+ puts "#{greeter.name}: \"#{greeter.welcome}, #{greeted.name}!\""
23
+ end
24
+ end
25
+
26
+ class Greet_Someone
27
+ def initialize(greeter, greeted)
28
+ @greeter = greeter
29
+ @greeted = greeted
30
+ end
31
+ end
32
+
33
+ p1 = Person.new
34
+ p1.name = 'Bob'
35
+ p1.greeting = 'Hello'
36
+
37
+ p2 = Person.new
38
+ p2.name = 'World!'
39
+ p2.greeting = 'Greetings'
40
+
41
+ #Execute is automagically created for the default interaction (specified by the second argument in context :Greet_Someone, :greet do)
42
+ #Executes construc a context object and calls the default interaction on this object
43
+ Greet_Someone.execute p1, p2
44
+ #constructs a Greet_Someone context object and executes greet.
45
+ Greet_Someone.new(p2, p1).greet
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in maroon.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 TODO: Write your name
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,41 @@
1
+ # Maroon
2
+
3
+ A module to make pure DCI available in Ruby
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'maroon'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install maroon
18
+
19
+ ## Usage
20
+
21
+ See the examples for detailed information on how to use maroon.
22
+
23
+ Essentially you can define a context by using
24
+
25
+ Context::define :context_name do
26
+ role :role_name do
27
+ print_self do |x| #notice no symbol
28
+ p "#{role_name} #use role_name to refer to the role of said name
29
+ end
30
+ end
31
+ end
32
+
33
+
34
+ ## Contributing
35
+
36
+ 1. Fork it
37
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
38
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
39
+ 4. Push to the branch (`git push origin my-new-feature`)
40
+ 5. Create new Pull Request
41
+
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/lib/maroon.rb ADDED
@@ -0,0 +1,363 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require 'live_ast'
3
+ require 'live_ast/to_ruby'
4
+
5
+ ##
6
+ # The Context class is used to define a DCI context with roles and their role methods
7
+ # to define a context call define with the name of the context (this name will become the name of the class that defines the context)
8
+ # the name should be a symbol and since it's going to be used as a class name, use class naming conventions
9
+ # follow the name with a block. With in this block you can define roles and interactions
10
+ # and interaction is defined by write the name of the interaction (hello in the below example) followed by a block
11
+ # the block will become the method body
12
+ # a role can be defined much like a context. instead of calling the define method call the role method followed by the role name (as a symbol)
13
+ # the role will be used for a private instance variable and the naming convention should match this
14
+ # With in the block supplied to the role method you can define role methods the same way as you define interactions. See the method who
15
+ # in the below example
16
+ # = Example
17
+ # Context::define :Greeter do
18
+ # role :who do
19
+ # say do
20
+ # @who #could be self as well to refer to the current role player of the 'who' role
21
+ # end
22
+ # end
23
+ # greeting do
24
+ # p "Hello #{who.say}!"
25
+ # end
26
+ # end
27
+ #
28
+ # class Greeter
29
+ # def initialize(player)
30
+ # #@who = player
31
+ # end
32
+ # end
33
+ #
34
+ # Greeter.new('world').greeting #Will print "Hello world!"
35
+ #maroon is base on Marvin which was the first injectionless language for DCI
36
+ #being injectionless there's no runtime extend or anything else impacting the performance. There' only regular method invocation even when using role methods
37
+ #Author:: Rune Funch Søltoft (funchsoltoft@gmail.com)
38
+ #License:: Same as for Ruby
39
+ ##
40
+ class Context
41
+ @roles
42
+ @interactions
43
+ @defining_role
44
+ @role_alias
45
+ @alias_list
46
+ @cached_roles_and_alias_list
47
+
48
+ #define is the only exposed method and can be used to define a context (class)
49
+ #if maroon/kernel is required calling context of Context::define are equivalent
50
+ #params
51
+ #name:: the name of the context. Since this is used as the name of a class, class naming convetions should be used
52
+ #block:: the body of the context. Can include definitions of roles (through the role method) or definitions of interactions
53
+ #by simply calling a method with the name of the interaction and passing a block as the body of the interaction
54
+ def self.define(*args, &block)
55
+ name,base_class,default_interaction = *args
56
+ #if there's two arguments and the second is not a class it must be an interaction
57
+ base_class,default_interaction = default_interaction, base_class if base_class and !default_interaction and base_class.instance_of? Symbol
58
+ ctx = Context.new
59
+ ctx.instance_eval &block
60
+ return ctx.send(:finalize, name,base_class,default_interaction)
61
+ end
62
+
63
+ private
64
+ ##
65
+ #Defines a role with the given name
66
+ #role methods can be defined inside a block passed to this method
67
+ # = Example
68
+ # role :who do
69
+ # say do
70
+ # p @who
71
+ # end
72
+ # end
73
+ #The above code defines a role called 'who' with a role method called say
74
+ ##
75
+ def role(role_name)
76
+ raise 'Argument role_name must be a symbol' unless role_name.instance_of? Symbol
77
+
78
+ @defining_role = role_name
79
+ @roles[role_name] = Hash.new
80
+ yield if block_given?
81
+ @defining_role = nil
82
+ end
83
+
84
+ def initialize
85
+ @roles = Hash.new
86
+ @interactions = Hash.new
87
+ @role_alias = Array.new
88
+ end
89
+
90
+ def role_aliases
91
+ @alias_list if @alias_list
92
+ @alias_list = Hash.new
93
+ @role_alias.each {|aliases|
94
+ aliases.each {|k,v|
95
+ @alias_list[k] = v
96
+ }
97
+ }
98
+ @alias_list
99
+ end
100
+
101
+ def roles
102
+ @cached_roles_and_alias_list if @cached_roles_and_alias_list
103
+ @roles unless @role_alias and @role_alias.length
104
+ @cached_roles_and_alias_list = Hash.new
105
+ @roles.each {|k,v|
106
+ @cached_roles_and_alias_list[k] = v
107
+ }
108
+ role_aliases.each {|k,v|
109
+ @cached_roles_and_alias_list[k] = @roles[v]
110
+ }
111
+ @cached_roles_and_alias_list
112
+ end
113
+
114
+ def methods
115
+ (@defining_role ? @roles[@defining_role] : @interactions)
116
+ end
117
+
118
+ def add_alias (a,role_name)
119
+ @cached_roles_and_alias_list,@alias_list = nil
120
+ @role_alias.last()[a] = role_name
121
+ end
122
+
123
+ def finalize(name, base_class, default)
124
+ c = base_class ? (Class.new base_class) : Class.new
125
+ Kernel.const_set name, c
126
+ code = ''
127
+ fields = ''
128
+ getters = ''
129
+ impl = ''
130
+ interactions = ''
131
+ @interactions.each do |method_name, method_source|
132
+ @defining_role = nil
133
+ interactions << " #{lambda2method(method_name, method_source)}"
134
+ end
135
+ if default
136
+ interactions <<"\ndef self.execute(*args);#{name}.new(*args).#{default}; end\n"
137
+ end
138
+
139
+ @roles.each do |role, methods|
140
+ fields << "@#{role}\n"
141
+ getters << "def #{role};@#{role} end\n"
142
+
143
+ methods.each do |method_name, method_source|
144
+ @defining_role = role
145
+ rewritten_method_name = "self_#{role}_#{method_name}"
146
+ definition = lambda2method rewritten_method_name, method_source
147
+ impl << " #{definition}" if definition
148
+ end
149
+ end
150
+
151
+ code << "#{interactions}\n#{fields}\n private\n#{getters}\n#{impl}\n"
152
+
153
+ complete = "class #{name}\r\n#{code}\r\nend"
154
+ return c.class_eval(code),complete
155
+ end
156
+
157
+ def role_or_interaction_method(method_name,*args, &b)
158
+ raise "method with out block #{method_name}" unless b
159
+
160
+ args, block = block2source b.to_ruby, method_name
161
+ args = "|#{args}|" if args
162
+ source = "(proc do #{args}\n #{block}\nend)"
163
+ methods[method_name] = source
164
+ end
165
+
166
+ alias method_missing role_or_interaction_method
167
+
168
+ def role_method_call(ast, method)
169
+ is_call_expression = ast && ast[0] == :call
170
+ self_is_instance_expression = is_call_expression && (!ast[1]) #implicit self
171
+ is_in_block = ast && ast[0] == :lvar
172
+ role_name_index = self_is_instance_expression ? 2 : 1
173
+ role = (self_is_instance_expression || is_in_block) ? roles[ast[role_name_index]] : nil #is it a call to a role getter
174
+ is_role_method = role && role.has_key?(method)
175
+ role_name = is_in_block ? role_aliases[ast[1]] : (ast[2] if self_is_instance_expression)
176
+ role_name if is_role_method #return role name
177
+ end
178
+
179
+ def lambda2method (method_name, method_source)
180
+ evaluated = ast_eval method_source, binding
181
+ ast = evaluated.to_ast
182
+ transform_ast ast
183
+ args, block = block2source LiveAST.parser::Unparser.unparse(ast), method_name
184
+ args = "(#{args})" if args
185
+ "\ndef #{method_name} #{args}\n#{block} end\n"
186
+ end
187
+
188
+ ##
189
+ #Test if there's a block that needs to potentially be transformed
190
+ ##
191
+ def transform_block(exp)
192
+ if exp && exp[0] == :iter
193
+ (exp.length-1).times do |i|
194
+ expr = exp[i+1]
195
+ #find the block
196
+ if expr && expr.length && expr[0] == :block
197
+ transform_ast exp if rewrite_bind? expr,expr[1]
198
+ end
199
+ end
200
+ end
201
+ end
202
+
203
+ ##
204
+ #Calls rewrite_block if needed and will return true if the AST was changed otherwise false
205
+ ##
206
+ def rewrite_bind?(block, expr)
207
+ #check if the first call is a bind call
208
+ if expr && expr.length && (expr[0] == :call && expr[1] == nil && expr[2] == :bind)
209
+ arglist = expr[3]
210
+ if arglist && arglist[0] == :arglist
211
+ arguments = arglist[1]
212
+ if arguments && arguments[0] == :hash
213
+ block.delete_at 1
214
+ count = (arguments.length-1) / 2
215
+ (1..count).each do |j|
216
+ temp = j * 2
217
+ local = arguments[temp-1][1]
218
+ if local.instance_of? Sexp
219
+ local = local[1]
220
+ end
221
+ raise 'invalid value for role alias' unless local.instance_of? Symbol
222
+ #find the name of the role being bound to
223
+ aliased_role = arguments[temp][1]
224
+ if aliased_role.instance_of? Sexp
225
+ aliased_role = aliased_role[1]
226
+ end
227
+ raise "#{aliased_role} used in binding is an unknown role #{roles}" unless aliased_role.instance_of? Symbol and @roles.has_key? aliased_role
228
+ add_alias local, aliased_role
229
+ #replace bind call with assignment of iteration variable to role field
230
+ rewrite_bind(aliased_role, local, block)
231
+ return true
232
+ end
233
+ end
234
+ end
235
+ end
236
+ false
237
+ end
238
+
239
+ ##
240
+ #removes call to bind in a block
241
+ #and replaces it with assignment to the proper role player local variables
242
+ #in the end of the block the local variables have their original values reassigned
243
+ def rewrite_bind(aliased_role, local, block)
244
+ raise 'aliased_role must be a Symbol' unless aliased_role.instance_of? Symbol
245
+ raise 'local must be a Symbol' unless local.instance_of? Symbol
246
+ assignment = Sexp.new
247
+ assignment[0] = :iasgn
248
+ assignment[1] = aliased_role
249
+ load_arg = Sexp.new
250
+ load_arg[0] = :lvar
251
+ load_arg[1] = local
252
+ assignment[2] = load_arg
253
+ block.insert 1, assignment
254
+
255
+ # assign role player to temp
256
+ temp_symbol = "temp____#{aliased_role}".to_sym
257
+ assignment = Sexp.new
258
+ assignment[0] = :lasgn
259
+ assignment[1] = temp_symbol
260
+ load_field = Sexp.new
261
+ load_field[0] = :ivar
262
+ load_field[1] = aliased_role
263
+ assignment[2] = load_field
264
+ block.insert 1, assignment
265
+
266
+ # reassign original player
267
+ assignment = Sexp.new
268
+ assignment[0] = :iasgn
269
+ assignment[1] = aliased_role
270
+ load_temp = Sexp.new
271
+ load_temp[0] = :lvar
272
+ load_temp[1] = temp_symbol
273
+ assignment[2] = load_temp
274
+ block[block.length] = assignment
275
+ end
276
+
277
+ # rewrites a call to self in a role method to a call to the role player accessor
278
+ # which is subsequently rewritten to a call to the instance variable itself
279
+ # in the case where no role method is called on the role player
280
+ # It's rewritten to an instance call on the context object if a role method is called
281
+ def rewrite_self (ast)
282
+ ast.length.times do |i|
283
+ raise 'Invalid argument. must be an expression' unless ast.instance_of? Sexp
284
+ exp = ast[i]
285
+ if exp == :self
286
+ ast[0] = :call
287
+ ast[1] = nil
288
+ ast[2] = @defining_role
289
+ arglist = Sexp.new
290
+ ast[3] = arglist
291
+ arglist[0] = :arglist
292
+ elsif exp.instance_of? Sexp
293
+ rewrite_self exp
294
+ end
295
+ end
296
+ end
297
+
298
+ #rewrites the ast so that role method calls are rewritten to a method invocation on the context object rather than the role player
299
+ #also does rewriting of binds in blocks
300
+ def transform_ast(ast)
301
+ if ast
302
+ if @defining_role
303
+ rewrite_self ast
304
+ end
305
+ ast.length.times do |k|
306
+ exp = ast[k]
307
+ if exp
308
+ method_name = exp[2]
309
+ role = role_method_call exp[1], exp[2]
310
+ if exp[0] == :iter
311
+ @role_alias.push Hash.new
312
+ transform_block exp
313
+ @role_alias.pop()
314
+ end
315
+ if exp[0] == :call && role
316
+ exp[1] = nil #remove call to attribute
317
+ exp[2] = "self_#{role}_#{method_name}".to_sym
318
+ end
319
+ if exp.instance_of? Sexp
320
+ transform_ast exp
321
+ end
322
+ end
323
+ end
324
+ end
325
+ end
326
+
327
+ #cleans up the string for further processing and separates arguments from body
328
+ def block2source(b, method_name)
329
+ args = nil
330
+ block = b.strip
331
+ block = block[method_name.length..-1].strip if block.start_with? method_name.to_s
332
+ block = cleanup_head_and_tail(block)
333
+ if block.start_with? '|'
334
+ args = block.scan(/\|([\w\d,\s]*)\|/)
335
+ if args.length && args[0]
336
+ args = args[0][0]
337
+ else
338
+ args = nil
339
+ end
340
+ block = block[(2 + (block[1..-1].index '|'))..-1].strip
341
+ end
342
+ return args, block
343
+ end
344
+
345
+ # removes proc do/{ at start and } or end at the end of the string
346
+ def cleanup_head_and_tail(block)
347
+ if /^proc\s/.match(block)
348
+ block = block['proc'.length..-1].strip
349
+ end
350
+ if /^do\s/.match(block)
351
+ block = block[2..-1].strip
352
+ elsif block.start_with? '{'
353
+ block = block[1..-1].strip
354
+ end
355
+
356
+ if /end$/.match(block)
357
+ block = block[0..-4]
358
+ elsif /\}$/.match(block)
359
+ block = block[0..-2]
360
+ end
361
+ block
362
+ end
363
+ end