This page is an attempt in Ruby to duplicate the snippet of Scheme code given in SinisterSchemeSamplePerplexesPythonPorter. Check out that page for the original scheme code (which I don't repeat here).
The first three functions are easy ...
def make_pointer(getter, setter) [getter, setter] end def pointer_ref(ptr) ptr[0].call end def pointer_set!(ptr, value) ptr[1].call(value) endWe will skip the address function for now. Scheme uses macros to implement address. We will come back to it in a moment.
So, let's try it out our 3 basic functions ...
puts "Version 1" x = 1 px = make_pointer(lambda { x }, lambda { |value| x = value }) puts pointer_ref(px) # => 1 (good) pointer_set!(px, 2) puts pointer_ref(px) # => 2 (good) puts x # => 2 (good)So the basic approach works. We use two lambdas (anonymous functions) to capture the getting and setting of the variable x, just as the scheme version did. Note that each lambda function remembers the variable binding available at the point the lambda was created.
Now let's see if we can duplicate the address function. Our first attempt fails ...
def address(var) make_pointer(lambda { var }, lambda { |value| var = value }) endThe reason it fails is that the lambda are created inside the scope of the address function. "var" in the lambdas refers to the parameter of the address function, not to the original "x" variable bound in the main program.
Somehow we need to create the lambdas in the enclosing scope. One way to do it is to pass the outer bindings explicitly to the address function as a parameter, and then create the lambdas using eval. (Note, the function "binding" returns a Binding object representing the current variable bindings that can be passed to eval)
def address(symbol, outer_scope) make_pointer(eval("lambda { #{symbol.id2name} }", outer_scope), eval("lambda { |value| #{symbol.id2name} = value }", outer_scope)) endThis version of address works fine ...
x = 1 px = address(:x, binding) puts pointer_ref(px) # => 1 (good) pointer_set!(px, 2) puts pointer_ref(px) # => 2 (good) puts x # => 2 (good)Although workable, it is a bit ungainly. Here's a variation. Instead of passing the value of the binding method, we could pass a proc instead. Eval can use the bindings remembered in a proc as well. What's more, we can have the proc do double duty by returning the symbol to be returned. This makes the usage of address slightly more palatable. Here's the new version of address and how it is used...
def address(&block) make_pointer(eval("lambda { #{yield.id2name} }", block), eval("lambda { |value| #{yield.id2name} = value }", block)) end px = address {:x}
class Pointer def initialize(&block) @getter = eval("lambda { #{yield.id2name} }", block) @setter = eval("lambda { |value| #{yield.id2name} = value }", block) end def value @getter.call end def value=(new_value) @setter.call(new_value) end end x = 1 px = Pointer.new {:x} puts px.value # => 1 (good) px.value = 2 puts px.value # => 2 (good) puts x # => 2 (good)-- JimWeirich
Extra Credit
The version of Pointer given above works great. However, if I had not seen the scheme version first, I probably would have written it slightly differently. Compare the following BrokenPointer class to the working version. Why is BrokenPointer inferior to the Scheme inspired version? (hint: think about non-integer values)
class BrokenPointer def initialize(&block) @block = block @name = yield.id2name end def value eval "#{@name}", @block end def value=(new_value) eval "#{@name} = #{new_value}", @block end end
Ok, wise guy. What's the difference? The fact that you could possibly eval "x = the quick brown fox"?
Here is another one. It works with any lvalue, not just variables.
class Pointer attr_reader :lvalue attr_reader :binding attr_reader :getter attr_reader :setter def initialize( &block ) ref( &block) end def ref( &block ) @block = block || proc {:@value} @binding = @block.binding() @lvalue = @block.call() @getter = eval( "proc { #{@lvalue} }", @block) @setter = eval( "proc { |v| #{@lvalue} = v }", @block) end def []() @getter.call() end def []=( new_value ) @setter.call( new_value); end def to_s() self[].to_s() end def inspect() "<Pointer #{lvalue()}>#{self[].inspect()}" end end a = [1,2,3] ptr = Pointer.new{ "a[2]"} ptr[] = 4 p a # => [1,2,4]-- JeanHuguesRobert
This is the original Ruby version modified so that one doesn't need to pass a block to Pointer.new.
# Begin of inlined binding_of_caller.rb begin require 'simplecc' rescue LoadError? def Continuation.create(*args, &block) cc = nil; result = callcc {|c| cc = c; block.call(cc) if block and args.empty?} result ||= args return *[cc, *result] end end # This method returns the binding of the method that called your # method. Don't use it when you're not inside a method. # # It's used like this: # def inc_counter # Binding.of_caller do |binding| # eval("counter += 1", binding) # end # end # counter = 0 # 2.times { inc_counter } # counter # => 2 # # You will have to put the whole rest of your method into the # block that you pass into this method. If you don't do this # an Exception will be raised. Because of the way that this is # implemented it has to be done this way. def Binding.of_caller(&block) old_critical = Thread.critical Thread.critical = true count = 0 cc, result, error = Continuation.create(nil, nil) error.call if error tracer = lambda do |*args| type, context = args[0], args[4] if type == "return" count += 1 # First this method and then calling one will return -- # the trace event of the second event gets the context # of the method which called the method that called this # method. if count == 2 # It would be nice if we could restore the trace_func # that was set before we swapped in our own one, but # this is impossible without overloading set_trace_func # in current Ruby. set_trace_func(nil) cc.call(eval("binding", context), nil) end elsif type != "line" set_trace_func(nil) error_msg = "Binding.of_caller used in non-method context or " + "trailing statements of method using it aren't in the block." cc.call(nil, lambda { raise(ArgumentError?, error_msg ) }) end end unless result set_trace_func(tracer) return nil else Thread.critical = old_critical yield result end end # End of inlined binding_of_caller.rb class Pointer def initialize(target) Binding.of_caller do |context| @getter = eval("lambda { #{target} }", context) @setter = eval("lambda { |value| #{target} = value }", context) end end def value @getter.call end def value=(new_value) @setter.call(new_value) end end x = 1 px = Pointer.new :x puts px.value # => 1 (good) px.value = 2 puts px.value # => 2 (good) puts x # => 2 (good)