100 Languages Speedrun: Episode 59: Smalltalk
Smalltalk was a revolutionary language which introduced Object Oriented Programming - programming through objects sending messages to other objects. Most languages created since then tried to incorporate Object Oriented Programming in some way. Some like Ruby did it earnestly. Most were a lot more lukewarm about it, but even a weak hybrid-OOP like Java's is still quite useful.
Something very similar happened to functional programming as well. Lisp introduced functional programming, and by now most language have some degree of support for it, and you can define closures, and do some map
and filter
, but very few go as far as Lisp did.
All this makes sense. A new idea needs some commitment to be explored properly, and then the lessons learned can be incorporated, to appropriate degree, in other contexts.
Smalltalk itself is pretty much dead now, but its main spiritual successor Ruby is doing great. And both had enormous influence on most modern programming languages.
Image-based programming
Some of the Smalltalk's ideas didn't last. One of them was Smalltalk's extremely minimalist syntax, which even Ruby abandoned. In principle you can code Ruby Smalltalk-style with just a lot of obj.send
s, but nobody does.
The other idea that lasted even less is image-based programming. In Smalltalk you wouldn't write code as text files - you loaded live Smalltalk image, interacted with it by creating some new classes and methods or such inside the image, and saved the whole thing. There are some advantages to it, but it really didn't mesh well with how programmers prefer to write code, so even newer many Smalltalk systems switched back to regular files, including GNU Smalltalk I'll be using for this episode.
By the way, there's one place where image-based programming is still alive and well, and that's relational databases! Databases don't load their stored procedures from some git repository subject to version control. To add or change a stored procedure, or a trigger, or any part of the schema, you need to talk to the database server directly, and it becomes part of the running system, with no text files representing database schema.
Most programmers tend to find this extremely frustrating, and there are complicated migration systems that try to force databases to behave more like the usual text file based programming. But in the end, they're still image-based, and you cannot just declaratively define the end state you want, the way it works with application code. Instead you need to write migrations, that is actions performed on live system to get it to the desired state.
Hello, World!
Let's start by creating a Hello, World!, as a proper Unix script:
#!/usr/bin/env gst
'Hello, World!' displayNl.
We can then run it from command line:
$ ./hello.st
Hello World!
As I already said, this is not how Smalltalk was supposed to be used originally.
Here we take object 'Hello World!'
and call its method displayNl
. The .
ends the sentence, sort of but not exactly like ;
in a lot of other languages.
This method however, is a GNU Smalltalk extension.
More traditional Hello, World!
Strings knowing how to print themselves, including handling newlines, is a bit weird, and that's not how Smalltalk traditionally worked. Instead, you had Transcript
object, which was sort of like a JavaScript console
.
So let's do a more traditional version:
#!/usr/bin/env gst
Transcript
show: 'Hello, World!';
cr.
$ ./hello2.st
Hello World!
This does a lot of interesting things:
- if we're sending a lot of methods to the same object, we can list them with a
;
- here we're sending two methods to the same object. This pattern is not really available in other languages, but in Rubyinstance_eval
and such are used to similar effect. - no-argument methods ("unary methods") are called with just their names like
Transcript cr
. - methods with arguments ("keyword methods") don't have "names" in traditional sense - they only have named keyword arguments.
Transcript show:
method is a nameless method with keyword argumentshow:
. Or from a different perspective, it's a method with nameshow:
that takes a single argument.
That's a lot to take from such a tiny bit of code.
Math
Take a guess what this does:
#!/usr/bin/env gst
a := 2.
b := 3.
c := 4.
Transcript
display: a + b * c;
cr.
Is this what you expected?
$ ./math.st
20
Smalltalk has third type of method ("binary methods"), which are used for mathematical operations. But it doesn't bother with any such complexities as precedence, associativity, etc. The whole precedence table is "unary > binary > keyword", so if you say a + b * c
it will be interpreted as (a + b) * c
. Or:
- send
a
a message+
with argumentb
- to whatever object that returns, send message
*
with argumentc
As you can probably imagine this was not a popular choice, and other languages did not copy it, but there's certain nice minimalism it achieves.
Smalltalk is not the only language without operator precedence. Lisp doesn't have them, as everything is a parenthesized (* (+ 2 3) 4)
. Stack languages like Forth or Postscript don't have them, as everything is postscript (2 3 4 + *
). Some languages like assembly don't have any formula support.
Smalltalk is fairly unusual in that its syntax otherwise looks "normal" enough that you'd expect operator precedence, but alas, it doesn't support it. That's one of the things Ruby fixed.
Interestingly Self, which is basically a Smalltalk dialect, decided to just outright ban such expressions. In Self 2 + 3 + 4
is (2 + 3) + 4
, but 2 + 3 * 4
just plain won't run without parenthesis one way or the other.
FizzBuzz
Let's write a FizzBuzz. There's a lot to unpack here:
#!/usr/bin/env gst
"FizzBuzz in Smalltalk"
Number extend [
isMultipleOf: n [
^ (self rem: n) = 0
]
]
(1 to: 100) do: [:i |
(i isMultipleOf: 3) ifTrue: [
(i isMultipleOf: 5) ifTrue: [
Transcript display: 'FizzBuzz'
] ifFalse: [
Transcript display: 'Fizz'
]
] ifFalse: [
(i isMultipleOf: 5) ifTrue: [
Transcript display: 'Buzz'
] ifFalse: [
Transcript display: i
]
].
Transcript cr.
].
- just in case it's not obvious so far, "Smalltalk" is about as much of a language as "SQL" - every version is based on similar principles, but it's wildly incompatible, none of the code is even close to portable to other implementations
- comments go into double quotes
Number extend [ ... ]
is how we can reopenNumber
class and add some methods - this is GNU Smalltalk extension - in original Smalltalk you were supposed to open Number class in "class browser", type method definition in the right box, then "accept" it - a process about as ridiculous as modifying stored procedures on a live production databaseisMultipleOf: n [ ]
defines a methodisMultipleOf:
.self
is current object (this
in most other languages, butself
in Ruby too)rem:
is remainder (mod
or%
in other languages)=
is equality, as:=
is assignment^
is a return statement - but we're in for a small surprise here too, as by default methods returnself
notnil
!- to construct a Range there's no special syntax, we just send
to:
method with appropriate argument to number1
, and it will give us a Range - we then send
do: [:i | ]
to Range, which is equivalent of(1..100).each do |i| ... end
in Ruby. Notice how in Ruby ranges have special syntax (but you could also use some method on Integer if for that if you really wanted). ifTrue:ifFalse:
is one method of boolean that takes two blocks - notice that we didn't use the;
trick to run two methods. Smalltalk just doesn't haveif/else
statements or anything like that.
And now we can run our program, and it almost works except...
$ ./fizzbuzz.st
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
...
FizzBuzz
91
92
"Global garbage collection... done"
Fizz
94
Buzz
Fizz
97
98
Fizz
Buzz
Oh WTF is this? For some insane reason, GNU Smalltalk by default prints on STDOUT (not even STDERR) such completely pointless debug messages. I have no idea how anyone ever thought that would be reasonable.
FizzBuzz take two
OK, let's get rid of this silly message with -g
flag. I'm still totally baffled by why it's here. We can also try some rearrangements of the code:
#!/usr/bin/env gst -g
"FizzBuzz in Smalltalk"
Number extend [
isMultipleOf: n [
^ (self rem: n) = 0
]
]
(1 to: 100) do: [:i |
Transcript
display: (
(i isMultipleOf: 3) ifTrue: [
(i isMultipleOf: 5)
ifTrue: ['FizzBuzz']
ifFalse: ['Fizz']
] ifFalse: [
(i isMultipleOf: 5)
ifTrue: [ 'Buzz' ]
ifFalse: [ i ]
]
);
cr.
].
This looks a lot cleaner.
Fibonacci
#!/usr/bin/env gst
Number extend [
fib [
^ (self <= 2)
ifTrue: [1]
ifFalse: [(self - 1) fib + (self - 2) fib]
]
]
(1 to: 20) do: [:i |
Transcript
display: 'fib(';
display: i;
display: ') = ';
display: i fib;
cr
].
There's no syntax for anything, so obviously there's no syntax for string interpolation either. There are some ways to hack some kind of string interpolation together with metaprogramming, but it won't work too well. Sometimes you need a bit of syntax.
$ ./fib.st
fib(1) = 1
fib(2) = 1
fib(3) = 2
fib(4) = 3
fib(5) = 5
fib(6) = 8
fib(7) = 13
fib(8) = 21
fib(9) = 34
fib(10) = 55
fib(11) = 89
fib(12) = 144
fib(13) = 233
fib(14) = 377
fib(15) = 610
fib(16) = 987
fib(17) = 1597
fib(18) = 2584
fib(19) = 4181
fib(20) = 6765
Defining a new class
Smalltalk is all about objects, so let's define a new class!
#!/usr/bin/env gst
Object subclass: Vector [
| x y |
x: xVal [ x := xVal ]
y: yVal [ y := yVal ]
x [ ^x ]
y [ ^y ]
Vector class >> x: xVal y: yVal [
^(self new)
x: xVal;
y: yVal;
yourself.
]
printOn: stream [
stream nextPutAll: '<'.
x printOn: stream.
stream nextPutAll: ','.
y printOn: stream.
stream nextPutAll: '>'.
]
+ other [
^ Vector
x: self x + other x
y: self y + other y
]
].
a := Vector x: 60 y: 230.
b := Vector x: 9 y: 190.
c := Vector new.
Transcript
display: a; cr;
display: b; cr;
display: c; cr;
display: a + b; cr.
$ ./vector.st
<60,230>
<9,190>
<nil,nil>
<69,420>
There's so much going on here!
- we're really relying on GNU Smalltalk convenience features here - in the original Smalltalks it would be a tedious multistep GUI operation to do all that, let's not get there
- unlike Ruby, instance of same class in Smalltalk need the same instance variables - we could of course change it at runtime, but it would affect every instance of that class
- we still need to define public getters and setters - these public setters are needed for our constructor to work properly
Vector class >> x: y:
defines methodx:y:
inVector
's metaclass. This isn't a method of an instance, but a method of the class itself. Ruby has same distinction, just nicer syntax.- it might look like it, but Smalltalk doesn't have keyword arguments
Vector y: x:
, orVector x:
would be entirely different methods Vector class >> x: y:
first creates a new instance (self new
), then uses public setters to set the instance variables, then it returns what it just constructed withyourself
. This is nice use of message chaining. Without message chaining we'd need a local variable, like[ r := self new. r x: xVal. r y: yVal. ^ r. ]
printOn stream
is sort of equivalent ofto_s
. It looks really bad without Ruby style string interpolation, and we have different direction for strings and for objects.+
is just a method.- we can send messages to
self
,self x + other x
means(self.x()).+(other.x())
in more conventional syntax Vector new
would initialize all values tonil
- we can create custom initializer if we want to override that.
Collections
Smalltalk had a tiny bit of syntax here and there, one of them being array literal syntax, which you didn't absolutely need, but it was definitely helpful. Oh and indexing started at 1 for some insane reason.
#!/usr/bin/env gst
"Array literal syntax"
a := #(1 2 3 4 5).
"Arrays are fixed size"
b := Array new: 5.
b
at: 1 put: 10;
at: 2 put: 20;
at: 3 put: 30;
at: 4 put: 40;
at: 5 put: 50.
Transcript
display: a; cr;
display: b; cr;
display: (a collect: [:x | x * 2]); cr;
display: (a select: [:x | (x rem: 2) = 1]); cr;
display: (a inject: 0 into: [:x :y | x + y]); cr.
$ ./collections.st
(1 2 3 4 5 )
(10 20 30 40 50 )
(2 4 6 8 10 )
(1 3 5 )
15
There are basic functional programming methods (collect:
/ select:
/ inject:into:
). Nowadays these pretty much standardized on different names (map
/ filter
/ reduce
). Ruby decided to support both sets of names (and also find_all
), to make programmers feel at home no matter which names they were used to. Nowadays it feels like a weird duplication, as there aren't really any Smalltalk programmers around. This is one place where after decades of confusion, there's now pretty much consensus on which names to use. By the way if any Ruby style guide tells you to use map
in one context, and collect
in some other context, it's dumb. Just pick one and stick to it - and now there's an obvious winner (select
vs filter
is the only one where Ruby consensus is still on the Smalltalk name).
doesNotUnderstand
SmallTalk also supports doesNotUnderstand
which is equivalent of Ruby's method_missing
. Unlike in Ruby, the method and all arguments are packaged into a single message object, not splatted into separate arguments.
There's no standard equivalent of BasicObject
, so Delegator
comes with a bunch of methods predefined already (like printOn
saying a Delegator
), but some Smalltalk dialects have something like that.
#!/usr/bin/env gst
Object subclass: Person [
| firstName lastName |
firstName: firstNameVal [ firstName := firstNameVal ]
lastName: lastNameVal [ lastName := lastNameVal ]
firstName [ ^firstName ]
lastName [ ^lastName ]
Person class >> firstName: firstNameVal lastName: lastNameVal [
^(self new)
firstName: firstNameVal;
lastName: lastNameVal;
yourself.
]
"x printOn: stream vs stream nextPutAll: x is like inspect vs to_s"
printOn: stream [
stream
nextPutAll: firstName;
nextPutAll: ' ';
nextPutAll: lastName.
]
].
Object subclass: Delegator [
| object |
object: objectVal [ object := objectVal ]
object [ ^object ]
doesNotUnderstand: aMessage [
Transcript
display: 'Forwarding message: ';
display: aMessage;
display: ' to object: ';
display: object;
cr.
^object perform: aMessage
]
printOn: stream [
stream nextPutAll: 'Delegator for '.
object printOn: stream.
]
]
a := Person firstName: 'Alice' lastName: 'Wonderland'.
b := (Delegator new) object: a.
Transcript
display: a; cr;
display: (a firstName); cr;
display: (a lastName); cr;
display: b; cr;
display: (b firstName); cr;
display: (b lastName); cr.
$ ./delegate.st
Alice Wonderland
Alice
Wonderland
Delegator for Alice Wonderland
Forwarding message: firstName to object: Alice Wonderland
Alice
Forwarding message: lastName to object: Alice Wonderland
Wonderland
Should you use Smalltalk?
Not anymore.
Ruby took all the best parts of Smalltalk, dropped all the bad parts, then not only really refined the best parts of Smalltalk, but also added so much more beyond that. Smalltalk is a language of enormous historical significance, but there's no reason to use it today.
On the other hand, Smalltalk might still be a good way to experience object-orientation in its purest form. Smalltalk had one idea for OOP, and its dialects like Self had their own unique and very different takes. Ruby has very similar OOP system, but parts of it are covered by syntactic sugar for things like math, conditionals, string interpolation, and so on. Aspiring programming language designers would be about the only group of people to whom I'd recommend giving Smalltalk a try.
Code
All code examples for the series will be in this repository.