maroon 0.5.2

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.
@@ -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