Last active
December 11, 2015 15:29
-
-
Save runefs/4621229 to your computer and use it in GitHub Desktop.
This is an example of how DCI can be done in Ruby without injecting methods into objects. It's inspired by how the Marvin compiler works, and is basically rewritting method calls on a RolePlayer into a method call on the context object when the method being cllaed is a role method.
It does not work completely like marvin E.g. any access to a rol…
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require 'live_ast' | |
require 'live_ast/to_ruby' | |
module Player | |
alias metho_missing_org method_missing | |
def method_missing (name, *args) | |
current = Context.current | |
raise "no current context" unless current | |
role_name = current.role_name | |
method_name = "self_#{role_name}_#{name}".to_sym | |
if current.class.method_defined? method_name | |
current.send(method_name,*args) | |
else | |
metho_missing_org name, *args | |
end | |
end | |
end | |
class Context | |
@@defining_role | |
@@current_block | |
@@stack = Hash.new | |
@role_name | |
@index | |
def initialize | |
@role_name ||= Array.new | |
end | |
def bind(player, role_name) | |
player.class.send(:include, Player) unless player.is_a? Player | |
create_instance_aliases player, role_name | |
player | |
end | |
def Context.stack | |
@@stack[Thread.current.object_id] ||= Array.new | |
end | |
def push | |
Context.stack.push(self) | |
end | |
def pop(pop_role_name) | |
Context.stack.pop() | |
pop_role() if pop_role_name == self.role_name | |
end | |
def Context.current | |
Context.stack.last() | |
end | |
def role_name | |
@role_name[@index][0] | |
end | |
def role_id | |
@role_name[@index][-1] | |
end | |
def push_role(name, player) | |
@index = if @index then (@index + 1) % 256 else 1 end | |
(@role_name ||= Array.new)[@index] = [name,player.object_id] | |
player | |
end | |
def pop_role | |
name = @role_name[@index] | |
@index = if @index != 0 then @index - 1 else 255 end | |
name | |
end | |
def create_instance_aliases (player,role_name) | |
raise StandardException "player must have a value" unless player | |
prefix = "self_#{role_name}_" | |
alias_prefix = "#{self.class.name}_aliased_" | |
role_methods = self.methods.select do |m| | |
name = m.to_s | |
if name.start_with? prefix | |
method_name = name[(prefix.length)..-1].to_sym | |
has_instance_method = player.class.method_defined? method_name | |
has_context_override_already = player.class.method_defined? "#{alias_prefix}#{name}".to_sym | |
has_instance_method && !has_context_override_already | |
end | |
end | |
role_methods = role_methods.map do |m| | |
m.to_s[(prefix.length)..-1] | |
end | |
role_methods.each do |method_name| | |
alias_name = "#{alias_prefix}#{method_name}" | |
role_method_name = "self_#{role_name}_#{method_name}" | |
role_method_override = "\r\ndef #{method_name}(*args)\r\n"+ | |
"current = Context.current || self\r\n"+ | |
" is_playing = current.role_id == self.object_id\r\n" + | |
" is_current_role = current.role_name == :#{role_name}\r\n"+ | |
" role_method_exists = current.class.method_defined? :#{role_method_name}\r\n" + | |
#" p \"playing: \#{is_playing} current role: \#{current.role_name} == :#{role_name} -> \#{is_current_role} role method (#{role_method_name})exists: \#{role_method_exists}\"\r\n" + | |
"if is_playing && is_current_role && role_method_exists\r\n"+ | |
#" p 'calling role method'\r\n"+ | |
" current.#{role_method_name} *args\r\n"+ | |
"else\r\n"+ | |
#" p 'calling instance method'\r\n"+ | |
" #{alias_name} *args\r\n"+ | |
"end\r\n"+ | |
"end\r\n" | |
player.class.class_eval("alias :#{alias_name} :#{method_name}\r\n" +role_method_override) | |
end | |
end | |
def Context.role(role_name) | |
name = role_name.to_s | |
@@defining_role = name | |
@@current_block = "" | |
#creates code similar to | |
#@name | |
#def name | |
# @name | |
#end | |
#def name=(player) | |
# @name = bind(player) | |
#end | |
#def self_name args | |
# content of block | |
#end | |
pre = "@#{name}\r\n"+ | |
"def #{name}\r\n" + | |
"push_role :#{name},@#{name}\r\n" + | |
"end\r\n" + | |
"def #{name}=(player)\r\n"+ | |
" @#{name} = bind player, :#{name}\r\n"+ | |
"end\r\n" + | |
"private :#{name}\r\n" | |
yield if block_given? | |
code = pre + (@@current_block || "") | |
class_eval(code) | |
end | |
def Context.block2method(method_name,pop_role,&b) | |
name = method_name.to_s | |
code = "\r\n" | |
args = "" | |
if block_given? | |
block = b.to_ruby | |
block.strip! | |
index = (block.index '|') + 1 | |
index = 0 if index < 0 | |
block = block[index..-1] | |
index = (block.index '|') - 1 | |
index = -1 if index < 0 | |
args = block[0..index] | |
if /end$/.match(block) | |
block = block[(index+2)..-4] | |
elsif /\}$/.match(block) | |
block = block[(index+2)..-2] | |
end | |
else | |
block = "" | |
end | |
code += "\r\ndef #{name}_inner #{args} \r\n#{block}\r\n end\r\n" | |
code += "\r\ndef #{name} #{args} \r\nself.push()\r\nretval = #{name}_inner #{args}\r\nself.pop(#{pop_role})\r\nretval\r\n end\r\n" | |
code | |
end | |
def Context.role_method(method_name, &b) | |
role_method_name = "self_#{@@defining_role}_#{method_name}" | |
@@current_block += block2method role_method_name,true,&b | |
end | |
def Context.interaction(method_name, &b) | |
code = block2method method_name,false,&b | |
class_eval(code) | |
end | |
end | |
class MoneyTransfer < Context | |
role :source do | |
role_method :withdraw do |amount| | |
source.movement(amount) | |
p (source.log "withdrawal #{amount}") | |
end | |
role_method :log do |message| | |
p "role #{message}" | |
"return value" | |
end | |
end | |
role :destination do | |
role_method :deposit do |amount| | |
destination.movement(amount) | |
destination.log "deposit #{amount}" | |
end | |
end | |
interaction :transfer do |amount| | |
source.withdraw -amount | |
destination.deposit amount | |
end | |
def initialize(s,d) | |
self.source = s | |
self.destination = d | |
end | |
end | |
class Account | |
def initialize (amount,id) | |
@balance = amount | |
@account_id = id | |
end | |
def movement(amount) | |
p "Amount #{amount}" | |
@balance+=amount | |
end | |
def log(message) | |
p "instance #{message}" | |
end | |
def to_s | |
"balance of #{@account_id}: #{@balance}" | |
end | |
end | |
account = Account.new 1000, "source" | |
ctx = MoneyTransfer.new account, account | |
ctx.transfer 100 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment