Objects at the Command Line #003 — A Calculator With No Operator Dispatch

August 6, 2026

An RPN (reverse Polish) calculator is a classic small exercise, and it almost always grows the same organ: a place that maps "+" to addition, "-" to subtraction, and so on. Here’s the honest, idiomatic Ruby version:

def rpn(tokens)
  stack = []
  tokens.each do |tok|
    if tok =~ /\A\d+\z/
      stack.push(tok.to_i)
    else
      b = stack.pop
      a = stack.pop
      case tok
      when "+" then stack.push(a + b)
      when "-" then stack.push(a - b)
      when "*" then stack.push(a * b)
      when "/" then stack.push(a / b)
      end
    end
  end
  stack.last
end

puts "3 4 + 5 *  = #{rpn(%w[3 4 + 5 *])}"

Now, Ruby of all languages gets close to the trick that’s coming — because Ruby inherited its instincts from Smalltalk. You could write a.send(tok.to_sym, b) and turn the operator string straight into a message. That’s the same idea. But the version most Rubyists reach for first is the case/when above: still a table you own, mapping operator strings to the code that implements them.

Here’s the GNU Smalltalk version:

| stack tokens |
stack := OrderedCollection new.
tokens := #('3' '4' '+' '5' '*').

tokens do: [:token |
    token first isDigit
        ifTrue:  [ stack addLast: token asNumber ]
        ifFalse: [ | b a |
                   b := stack removeLast.
                   a := stack removeLast.
                   stack addLast: (a perform: token asSymbol with: b) ] ].

Transcript showCr: '3 4 + 5 *  =  ', stack last printString.

Run it (gst after.st): 3 4 + 5 * = 35.

Where did the operator table go?

There isn’t one. Look at the single line that does the arithmetic:

a perform: token asSymbol with: b

'+' asSymbol is the symbol #+. In Smalltalk, + isn’t operator syntax baked into the grammar — it’s an ordinary message that the number a already knows how to answer. So a perform: #+ with: b is exactly a + b. The same is true for -, *, /. The parser reads the operator token and hands it straight to the number as a message. The mapping from “+” to addition never had to be written, because it was never separate from the number in the first place.

The transferable lesson

You don’t need Smalltalk to steal this instinct. The next time you’re about to build a string-to-behavior lookup — a dict of handlers, a switch of commands — pause and ask whether the behavior already lives on an object you’re already holding. Dispatch you don’t write is dispatch that can’t drift out of sync with the thing it’s dispatching to.

Honest caveat — and this one matters: a real calculator needs error handling, precedence, and input validation, and perform: on unchecked user input is a genuine footgun (you’d whitelist the allowed selectors). The lesson isn’t “replace all your dispatch with reflection.” It’s narrower and more useful: notice when the objects you already have can do the work you were about to write a table for.


Every sample is executed against GNU Smalltalk 3.2.5 before it ships — 3 4 + 5 * really does print 35. I write these in VS Code using my open-source GNU Smalltalk extension. Which of the first three ideas surprised you most — and what should Episode #004 tackle?