DEV Community

Tomasz Wegrzanowski
Tomasz Wegrzanowski

Posted on

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

Hello, World!

Let's start by creating a Hello, World!, as a proper Unix script:

#!/usr/bin/env gst

'Hello, World!' displayNl.
Enter fullscreen mode Exit fullscreen mode

We can then run it from command line:

$ ./hello.st
Hello World!
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode
$ ./hello2.st
Hello World!
Enter fullscreen mode Exit fullscreen mode

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

Math

Take a guess what this does:

#!/usr/bin/env gst

a := 2.
b := 3.
c := 4.

Transcript
  display: a + b * c;
  cr.
Enter fullscreen mode Exit fullscreen mode

Is this what you expected?

$ ./math.st
20
Enter fullscreen mode Exit fullscreen mode

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 argument b
  • to whatever object that returns, send message * with argument c

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.
].
Enter fullscreen mode Exit fullscreen mode
  • 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 Number 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 database
  • isMultipleOf: n [ ] defines a method isMultipleOf:.
  • self is current object (this in most other languages, but self 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 return self not nil!
  • 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| ... 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 have if/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
Enter fullscreen mode Exit fullscreen mode

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.
].
Enter fullscreen mode Exit fullscreen mode

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
].
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode
$ ./vector.st
<60,230>
<9,190>
<nil,nil>
<69,420>
Enter fullscreen mode Exit fullscreen mode

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 x:y: in 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 stream is 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, self x + other x means (self.x()).+(other.x()) in more conventional syntax
  • Vector new would initialize all values to nil - 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.
Enter fullscreen mode Exit fullscreen mode
$ ./collections.st
(1 2 3 4 5 )
(10 20 30 40 50 )
(2 4 6 8 10 )
(1 3 5 )
15
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode
$ ./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
Enter fullscreen mode Exit fullscreen mode

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.

Code for the Smalltalk episode is available here.

Top comments (0)