Higher-Order Ruby

A Rubyification of the Perl book about functional programming.

17

JAN
2006

Dispatch Tables

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 && ARGV.first == "-i" && 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 && 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 && ARGV.first == "-i" && 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 && 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…

Comments (3)
  1. Arlen
    Arlen October 5th, 2007 Reply Link

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

    1. Reply (using GitHub Flavored Markdown)

      Comments on this blog are moderated. Spam is removed, formatting is fixed, and there's a zero tolerance policy on intolerance.

      Ajax loader
    2. James Edward Gray II
      James Edward Gray II October 5th, 2007 Reply Link

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

      1. Reply (using GitHub Flavored Markdown)

        Comments on this blog are moderated. Spam is removed, formatting is fixed, and there's a zero tolerance policy on intolerance.

        Ajax loader
  2. Nathan
    Nathan July 22nd, 2009 Reply Link

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

    1. Reply (using GitHub Flavored Markdown)

      Comments on this blog are moderated. Spam is removed, formatting is fixed, and there's a zero tolerance policy on intolerance.

      Ajax loader
Leave a Comment (using GitHub Flavored Markdown)

Comments on this blog are moderated. Spam is removed, formatting is fixed, and there's a zero tolerance policy on intolerance.

Ajax loader