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
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.
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.sends, 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.
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
. 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
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 Ruby
instance_evaland such are used to similar effect.
- no-argument methods ("unary methods") are called with just their names like
- 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 argument
show:. Or from a different perspective, it's a method with name
show:that takes a single argument.
That's a lot to take from such a tiny bit of code.
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:
- to whatever object that returns, send message
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.
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 reopen
Numberclass 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 database
isMultipleOf: n [ ]defines a method
selfis current object (
thisin most other languages, but
selfin Ruby too)
rem:is remainder (
%in other languages)
=is equality, as
^is a return statement - but we're in for a small surprise here too, as by default methods return
- to construct a Range there's no special syntax, we just send
to:method with appropriate argument to number
1, and it will give us a Range
- we then send
do: [:i | ]to Range, which is equivalent of
(1..100).each do |i| ... endin 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 have
if/elsestatements 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.
#!/usr/bin/env gst Number extend [ fib [ ^ (self <= 2) ifTrue:  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 method
Vector'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:, or
Vector 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 with
yourself. 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 streamis sort of equivalent of
to_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 x + other xmeans
(self.x()).+(other.x())in more conventional syntax
Vector newwould initialize all values to
nil- we can create custom initializer if we want to override that.
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 (
inject:into:). Nowadays these pretty much standardized on different names (
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 (
filter is the only one where Ruby consensus is still on the Smalltalk name).
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
Delegator comes with a bunch of methods predefined already (like
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?
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.
All code examples for the series will be in this repository.
Top comments (0)