Objects at the Command Line #003 — A Calculator With No Operator Dispatch
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?