Self tutorial

Self, a successor to Smalltalk, goes even further in pushing the generality of the object paradigm. Self implements almost all standard language constructs in terms of sending messages to objects, i.e. function calls. As a result, the syntax for conditionals, loops, etc. is somewhat strange. The situation is similar to Scheme's treatment of everything as a function call.

Summary

Concept Scheme Self
Arithmetic (+ (* 2 3) 2) (2 * 3) + 2
Variables
(let ((x 3))
  (set! x (+ x 1)))
| x <- 3 |
x: x + 1
Functions
(define (f) (set! x (+ x 1)))
(f)
| f = (x: x + 1) |
f
Multiple arguments
(define (timesPlus x a b) (+ (* a x) b))
(timesPlus 3 2 2)
| times: a Plus: b = ((a * self) + b) |
3 times: 2 Plus: 2
Conditional (if (< x 0) -1 1) (x < 0) ifTrue: -1 False: 1
Block (let ((x 3) (y 4)) (+ x y)) [| x <- 3. y <- 4 | x + y ]
Loop
(for-each (lambda (v) (set! x (+ x v))) 
          '(1 2 3))
(1 & 2 & 3) asList do: [|:v| x: x + v]
Object none (| x = 3. y <- 4. parent* = lobby |)
Reflection none (| x = 3 |) _Mirror at: 'x' Put: 4

Arithmetic

Comments a delimited by double quotes; strings are delimited by single quotes. Arithmetic is infix, but there is no operator precedence: you must use parentheses in compound expressions. Self overloads several operators; the lack of precedence prevents confusion. Try typing these into the interpreter:
(2 * 3) + 2                   "prints 8"
(2 @ 3) + (1 @ -2)            "addition of two points; prints 3 @ 1" 
'hello' , ' ' , 'there'       "comma concatenates strings"
(2 & 'foo' & 4.5) asSequence  "a sequence of integer, string, float"
(2 & 'foo' & 4.5) asList      "a list"
(2 & 'foo' & 4.5) asSet       "a set"
(('a' @ 2) & ('b' @ 3)) asDictionary  "a dictionary"
To create the various kinds of collections, we use '&' to make a generic collection and then "type-cast" it.

Variables

The interpreter is an object (called the shell) in which your expressions are evaluated. So to create a variable, you add a slot to the shell. You can do this with:
_AddSlots: (| x = 3 |)  "bind x to 3; x is read-only"
x + 1                   "prints 4"
_AddSlots: (| x <- 3 |) "bind x to 3; x is mutable"
x: 4                    "change x to 4"
x: x + 1                "increment x; prints 5"
When x is defined as a mutable slot, a method called "x:" is also defined. Calling this method with an argument changes the value in the slot.

Since the syntax for adding slots is cumbersome, we're sometimes going to be a bit sloppy and use the abbrevation | x = 3 | instead of _AddSlots: (| x = 3 |).

Functions

We can create a function f, which increments x:
_AddSlots: (| f = (x: x + 1) |)    "bind f to a function"
f                                  "call the function"
x                                  "prints 6"
There is no difference between reading a variable and calling a function (as in the case with f). This means that we can switch between storing a value to generating it on the fly, without making clients aware of it. In this sense, Self really doesn't have a special notion of "variable"; it only has slots and function calls which can modify slots.

Every function has an implicit argument called self, which is the value to the left of the function name at the point of call. Here is a function which uses self:

lobby _AddSlots: (| plus1 = (self + 1) |) "define the successor function"
4 plus1                                   "prints 5"
By adding the function to the lobby, not just the shell, we can apply it to almost any object, including numbers, since they all inherit from the lobby object.

In Self parlance, function calls are called message sends, where the first argument (the one bound to self inside the function body) is the receiver and the result is the answer. When the receiver is omitted it defaults to the self at the point of call, which in our case is the shell.

Multiple arguments

To call or create a function with multiple arguments, we use a colon-suffixed keyword before each argument. This makes Self read somewhat like English. For example,
"Define a function which multiplies the receiver, self, by 'a'
 and adds 'b' to the result."
lobby _AddSlots: (| times: a Plus: b = ((a * self) + b) |)
3 times: 2 Plus: 2        "prints 8"
By comparison, in Sather we would say something like 3.timesPlus(2, 2). Note that the first keyword is uncapitalized, while all successive keywords are capitalized. This is used to disambiguate nested calls.

Setting a variable, e.g. x: 4 is really just a procedure call with two arguments. The first argument is self (the shell) and the second is the number 4.

Conditional

Like Scheme, a conditional in Self is a function call. However, it looks different because Self uses a keyword calling convention. For example, the Scheme expression
(if (< x 0) -1 1)
translates into
(x < 0) ifTrue: -1 False: 1

Blocks

The Scheme let block is realized in Self using brackets. The Scheme expression
(let ((x 3) (y 4)) (+ x y))
translates into
[| x = 3. y = 4 | x + y ]
A few words of explanation are required here. A block has the form
[|slots| code ]
where both the slots and the code are optional. The value of a block is the result of executing the code in the environment defined by the slots. Slots are separated by periods, and have the form name = value (immutable slot) or name <- value (mutable slot). We saw this notation earlier with respect to objects. In fact, blocks are just a kind of short-lived object; their slots can be accessed from the outside and they can be passed to functions (but not returned from them).

Loops

Like closures in Scheme, Self blocks are used to implement control structures. Examples:
[x > 0] whileTrue: [ x: x - 1 ]    "evaluates the right block until
                                       the left block answers false"
(1 & 2 & 3) asSequence do: [ x: x + 1 ]  "increments x three times"
(1 & 2 & 3) asSequence do: [|:v| x: x + v]  "adds 6 to x"
The last example demonstrates a block which takes an argument v. The argument is designated by a slot name with a leading colon. The do: method for a sequence calls the block with every member of the sequence. In the second case, the block had no arguments so these values were ignored. We can create new objects which behave like sequences just by defining a do: function for them. Or we can define any new control structures we want.

Objects

An object is written just like a block, but with parentheses instead of brackets. Objects differ in that they can live longer than blocks. Now we can understand how _AddSlots: works: it copies the slots in the object on the right into the object on the left. There is no syntax for adding a slot to an existing object, so we have to create new objects and throw them away.

To print an object in a low-level format, use the _Print function:

((| x = 3 |) _AddSlots: (| y <- 4 |)) _Print
"prints ( | x = 3. y <- 4. | )"

A slot defined with an arrow is a normal data member. A slot defined with the equals sign is a read-only constant member. If the slot name ends with a star, then the slot is a parent slot. If a slot lookup fails on an object, then the parent slots are searched in order. This is similar to multiple inheritance in Python. The difference is that in Self a parent slot can be a mutable data member, allowing an object to change its behavior at runtime.

Reflection

Like Python, objects can be treated as collections. In Self, these collections are called mirrors. To treat an object as a dictionary of slots, call the function _Mirror on it. Dictionaries are accessed using the at: and at:Put: functions:
_AddSlots: (| d = (('a' @ 2) & ('b' @ 3)) asDictionary.
              o = (| a = 2. b = 3 |)
            |)
d at: 'b'                       "prints 3"
d at: 'b' Put: 4
d at: 'b'                       "prints 4"
o _Mirror at: 'b'               "prints b = 3"
o _Mirror at: 'b' Put: 4
o _Mirror at: 'b'               "prints b = 4"
(o _Mirror at: 'b') name: 'c'   "changes name of slot b to c"
o c                             "prints 4"

PLE Home page
Last modified: Tue Apr 25 10:00:57 GMT 2006