Advanced Ruby Tutorial 01

Hi all, I’m getting better at Ruby at the moment and thought I should share some insights. First off, reactive programming – a powerful paradigm that loosely couples events and responses. Conceptually it’s all about automatically propagating changes in input values. For example if a = b + 1 and b is later set to 1, the value of a will be reevaluated and changes from undefined to 2. The main advantage of this is that you no longer have to think about propagating changes across interrelated variables. Think spreadsheet cell interactions and you know what I mean. A complete and efficient implementation of this concept will take some time, but initially I’m going for a publisher/subscriber model, which may be improved upon at a later time. So let’s start with an initial setup:

module EventPublisher
  def add_listener(var, func)
    @callbacks ||= {}
    @callbacks[var] ||= []
    @callbacks[var] << func
  end #add_listener
	
  def publish(var, new_value)
    return unless @callbacks
    return unless @callbacks[var]
    
    @callbacks[var].each do |func|
        func.call
      end
    end
  end #publish
end #EventPublisher

Pretty nice and fairly unassuming. Now I'd like to add a DSL-like system that allows you to define class members as a 'hook' (some metaprogramming ahead):

class Module
  def hook(*members)
    members.each do |member|
      class_eval do
        define_method(member) do
          instance_variable_get("@#{member}")
        end
      end #class_eval
    end
  end #hook
end #Module

By defining the hook method for the Method object, it becomes available for all modules and classes. The EventPublisher module will still need to be included before listeners can be added however, and failure to do so may result in something like 'method not found: publish'. Still, this is pretty close to what I wanted, but I'd like any modifications to automagically be published. The idea here is to overload the '=' method to publish whenever any changes occur:

class Module
  def hook(*members)
    members.each do |member|
      class_eval do
        define_method(member) do
          instance_variable_get("@#{member}")
        end

        define_method("#{member}=") do |new_value|
          if (new_value != instance_variable_get("@#{member}"))
            publish(member.to_sym, new_value)
          end
          instance_variable_set("@#{member}", new_value)
        end              
      end #class_eval
    end
  end #hook
end #Module

Now we're getting somewhere. The next step is to communicate the changes to the listeners, which requires some changes to the publisher's publish function:

module EventPublisher
  def publish(var, new_value)
    return unless @callbacks
    return unless @callbacks[var]
    
    @callbacks[var].each do |func|
      if (func.arity == 0)
        func.call
      else
        func.call(new_value)
      end
    end
  end #publish
end #EventPublisher

By making use of arity, this remains compatible with listeners that do not take arguments. So now the hook code needs to include passing the new value to the publish function. While I'm at it, I'd like to only publish actually changed values, so let's check for that too:

class Module
  def hook(*members)
    members.each do |member|
      class_eval do
        define_method(member) do
          instance_variable_get("@#{member}")
        end

        define_method("#{member}=") do |new_value|
          if (new_value != instance_variable_get("@#{member}"))
            publish(member.to_sym, new_value)
          end
          instance_variable_set("@#{member}", new_value)
        end              
      end #class_eval
    end
  end #hook
end #Module

Here is an example of how this can be used as a simple callback system:

class Example
  include EventPublisher
  
  hook :test, :another_one
  
  def horrible_function(new_value)
    puts "horrible_function: #{new_value}"
  end
  
  def terrible_function
    puts "terrible_function"
  end
end #Example

ex = Example.new
ex.test = 'test_one'

ex.add_listener(:test, ex.method(:horrible_function))
ex.add_listener(:test, ex.method(:terrible_function))
ex.add_listener(:test, lambda { |arg| puts "lambda: " + arg })
ex.add_listener(:test, lambda { puts "lambda2" })

ex.test = 'boo!'

This yields the following result:

horrible_function: boo!
terrible_function
lambda: boo!
lambda2

The final code, including example can be downloaded [here].

I'll leave it at that for now, feel free to add comments and let me know what you think.

2 thoughts on “Advanced Ruby Tutorial 01

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.