Dispatch Tables

Posted over 6 years ago in Higher-Order Ruby.

Note: This is the second article in my Higher-Order Ruby series. The previous article can be found at the following link:

  1. Recursion and Callbacks

I think in Ruby we tend to do a lot of this kind of work with method_missing(). I told you, Functional OO Programming.

Here's my attempt at something close to a direct translation of the RPN calculator example:

#!/usr/local/bin/ruby -w

$stack = Array.new

def rpn( expression, operations_table )
  tokens = expression.split(" ")
  tokens.each do |token|
    type = token =~ /\A\d+\Z/ ? :number : nil

    operations_table[type || token][token]
  end

  $stack.pop
end

if ARGV.size == 2 and ARGV.first == "-i" and ARGV.last =~ /\A[-+*\/0-9 ]+\Z/
  require "pp"

  def ast_to_infix( ast )
    if ast.is_a? Array
      op, left, right = ast
      "(#{ast_to_infix(left)} #{op} #{ast_to_infix(right)})"
    else
      ast.to_s
    end
  end

  ast_table = Hash.new do |table, token|
    lambda { |op| s = $stack.pop; $stack << [op, $stack.pop, s] }
  end.merge(:number => lambda { |num| $stack << num.to_i })

  puts "AST:"
  pp(ast = rpn(ARGV.last, ast_table))
  puts "Infix:"
  pp ast_to_infix(ast)
elsif ARGV.size == 1 and ARGV.first =~ /\A[-+*\/0-9 ]+\Z/
  calculation_table = Hash.new do |table, token|
    raise "Unknown token:  #{token}."
  end.merge(
    :number => lambda { |num| $stack << num.to_i },
    "+"     => lambda { $stack << $stack.pop + $stack.pop },
    "-"     => lambda { s = $stack.pop; $stack << $stack.pop - s },
    "*"     => lambda { $stack << $stack.pop * $stack.pop },
    "/"     => lambda { d = $stack.pop; $stack << $stack.pop / d }
  )

  puts rpn(ARGV.first, calculation_table)
else
  puts "Usage:  #{File.basename($PROGRAM_NAME)} [-i] RPN_EXPRESSION"
end

I originally thought I was being clever using the default block of a Hash, but the lambda() trick in the AST translator feels bumpy. I also don't like the global $stack. And types are under used. Let me try to fix those issues and convert the API to something a bit more in Ruby's style:

#!/usr/local/bin/ruby -w

class RPN
  def initialize
    @operations_table = Hash.new do |table, token|
      if table.include?(:default)
        table[:default]
      else
        raise "Unknow token:  #{token}."
      end
    end

    @types = [[:number, /\A\d+\Z/]]
  end

  def type( name, pattern )
    @types << [name, pattern]
  end

  def method_missing( meth, *args, &block )
    @operations_table[meth.to_sym] = block
  end

  def parse( expression, stack = Array.new )
    expression.split(" ").each do |token|
      case (type = find_type(token))
      when :binary_op
        call_binary_op(token, stack, &@operations_table[:binary_op])
      else
        @operations_table[type || token][token, stack]
      end
    end
    stack.pop
  end

  private

  def find_type( token )
    (type = @types.find { |t| t.last === token }) ? type.first : nil
  end

  def call_binary_op( operator, stack, &operation )
    right = stack.pop
    operation[operator, stack.pop, right, stack]
  end
end

if ARGV.size == 2 and ARGV.first == "-i" and ARGV.last =~ /\A[-+*\/0-9 ]+\Z/
  require "pp"

  def ast_to_infix( ast )
    if ast.is_a? Array
      op, left, right = ast
      "(#{ast_to_infix(left)} #{op} #{ast_to_infix(right)})"
    else
      ast.to_s
    end
  end

  calc = RPN.new
  calc.type(:binary_op, /\A[-+*\/]\Z/)
  calc.number { |num, stack| stack << num.to_i }
  calc.binary_op { |op, left, right, stack| stack << [op, left, right] }

  puts "AST:"
  pp(ast = calc.parse(ARGV.last))
  puts "Infix:"
  pp ast_to_infix(ast)
elsif ARGV.size == 1 and ARGV.first =~ /\A[-+*\/0-9 ]+\Z/
  calc = RPN.new
  calc.type(:binary_op, /\A[-+*\/]\Z/)
  calc.number { |num, stack| stack << num.to_i }
  calc.binary_op { |op, left, right, stack| stack << left.send(op, right) }

  puts calc.parse(ARGV.first)
else
  puts "Usage:  #{File.basename($PROGRAM_NAME)} [-i] RPN_EXPRESSION"
end

I'm not sure if I got everything perfect, but hopefully there's a good Ruby idiom or two hiding in there. It certainly feels more Rubyish. Of course, it is almost 30 lines longer...

Go to Next Article

Arlen added about 1 year later:

I'd like to point out that "Rubish" seems to say "Rubbish" to me. :( Maybe Rubyish?

James Edward Gray II added about 1 year later:

You're right. That was just a typo. It's fixed now. Thanks.

Nathan added over 3 years later:

Everyone knows the old adage about how the average OO program is about twice as long as a non-OO counterpart (Smalltalk and Ruby at it's best reverse this, but only if you write the code in a lispy way).

Add Your Thoughts

You can use Markdown in the body of your comment to format text and make links.

Note that I reserve the right to edit any content you post here. I typically exercise this right to fix formatting issues. All posts must be approved so spam will never be seen on these pages.

Author:
URL or Email (optional):
Body: