From the notes for a Self seminar at Brown university (cs196b):
Self is a derivative of Smalltalk. Its most striking difference is the lack of classes - instead everything is an object. Objects inherit from each other. Every thing is an object, and every action is a message send.Some of the things Self does not have includes variables, scope, or structured flow of control statements, yet its semantics is powerful enough to provide the equivalent with an equivalent syntax.
(|slots| code)where both the slots and the code are optional. For example, this is an object which evaluates to 3:
(1+2)However, the object is not 3, only its value is. There are several kinds of slots an object can have:
(| x = 3. y <- 4. z <- nil. p* = lobby |)The dot must be used to separate slots.
This object contains a keyword slot containing the doubling function:
(| sqr: = (| :arg | arg * 2) |)The doubling function is simply an object with an argument slot and some code. This kind of declaration is so common that there is synactic sugar for it (one of the few instances of sugar in Self). You just bring the colons together:
(| sqr: arg = (arg * 2) |)An object with a two argument method can likewise be declared with
(| add:To: = (| :x. :y | x + y) |)or
(| add: x To: y = (x+y) |)
Quirk: Slot initializers are not lexically scoped; they are evaluated in the context of a special object called the lobby. This code produces an error:
(| y = 3. f = (| x = y | x + 1) |)because the parser only searches for the initializer y in the lobby. The only way to get a pseudo-lexically scoped initialization is to assign to the slot at runtime:
(| y = 3. f = (| x <- nil | x: y. x + 1 ) |)Here f evaluates to 4. (See the exercise for why this is only "pseudo"-lexical scoping.)
(| value = ('hello world' printLine) |) [ 'hello world' printLine ]are both objects which print "hello world" when the value message is sent to them. This is a useful shorthand since many control structures, such as
if
, use this message. Blocks may have slots, including argument slots. If the block has no argument slots, the translation is
[ code ] ----> (| value = ( code ) |)If the block has one argument, the translation is
[|:arg| code] ----> (| value: = (|:arg| code) |)For two arguments:
[|:arg1. :arg2| code] ----> (| value:With: = (|:arg1. :arg2| code) |)
However, blocks also inherit from the block in which they are defined, i.e. they are lexically scoped, unlike heap-allocated objects. Blocks are stack-allocated, which means that they disappear after the enclosing block has returned. Therefore they cannot be used as return values.
object messagei.e. an expression evaluating to an object, followed by a space, followed by a message expression. (Other object-oriented languages use a dot instead of a space.) There are three kinds of message expressions:
(| x = 3 |) xevaluates to 3. This operation does not simply return the object in a slot; it evaluates that object. For example:
(| x = ('hello' printLine) |) x "prints hello"The philosophy behind this is behaviorism.
times:Plus:
message we use
times: 2 Plus: 3
. This makes Self read somewhat like
English, as opposed to timesPlus(2, 3)
. The answer, i.e. the
resulting value, depends on which object received the message. The
spaces and the capitalization are mandatory. The parser uses
capitalization to disambiguate nested expressions like
times: 2 Plus: [|:a| a] value: 3into
times: 2 Plus: ([|:a| a] value: 3)i.e. not the three-argument message
times:Plus:value:
(which
is an illegal message name, because the 'v' is lowercase). This
expression, by the way, is the same as times: 2 Plus: 3
.
+
and *
. So instead of
writing x +: y
we write x + y
. Thus we see
that Self interprets arithmetic as a message send to the left operand.
To modify a read-write slot, use a keyword message with the new value as argument. Thus there is no distinction between setting a variable and calling a method. For example,
(| x <- 3 |) x: 4writes 4 into the x slot. The answer to the message is the receiver of the message, so we can cascade assignments:
((| x <- 3. y <- 4 |) x: 4) y: 3writes 4 into the x slot and 3 into the y slot. If the parenthesis are dropped, we get an error, because the statement is interpreted as:
(| x <- 3. y <- 4 |) x: (4 y: 3) "error"but 4 does not have a y slot. In general, use parentheses liberally, to clarify who is receiving a message.
To add a slot to an object at runtime, use the _AddSlots:
message. The argument is a object whose slots will be copied into the
receiver. For example,
(| x = 3 |) _AddSlots: (| y = 4 |)returns the object
(| x = 3. y = 4 |)
.
The interpreter is a bona-fide object, called the
ifTrue:False:
. The arguments to this message are blocks,
exactly one of which is evaluated. For example, true
implements ifTrue:False:
as
ifTrue: b1 False: b2 = ( b1 value )i.e. it always evaluates the first block. The Boolean objects are returned by comparison operators, allowing the Scheme absolute-value expression
(if (< x 0) (negate x) x)to be written as
(x < 0) ifTrue: [ negate x ] False: [ x ]A more complex example is factorial:
| fact = ((self < 2) ifTrue: [ 1 ] False: [ self * ((self - 1) fact)]) | 5 fact "prints 120"(Remember, to define this function in the Self interpreter, you must surround it with
lobby _AddSlots: (| ... |)
.)
do:
and at:
.
(define (make-counter n) (lambda () (set! n (+ n 1)) n)) (define c (make-counter 4)) (c) "prints 5" (c) "prints 6"cannot be translated directly into Self. Instead, we must use an explicit prototype counter object, which is cloned, initialized, and returned:
lobby _AddSlots: (| proto_counter = (| n <- nil. value = (n: n + 1. n). parent* = traits clonable |). make_counter = (proto_counter clone n: self) |) _AddSlots: (| c = 4 make_counter |) c value "prints 5" c value "prints 6"Of course, this is really what is happening behind the scenes in Scheme. In Self we just have to make this mechanism explicit. A benefit is that we can now access n from outside:
c n "prints 6" c n "prints 6" c n: 1 c value "prints 2"We can also use polymorphism to treat c as a block:
(1 & 2 & 3) asSequence do: c "increment the counter 3 times"
_Mirror
, when sent to any object, will return
a collection of slots called a mirror which can be used to refer
to that object.
The mirror acts like a table of slots (cf. Python's __dict__
).
Examples:
_Mirror
printAll
.
_Mirror at: 'x'
_Mirror at: 'x' Put: 3
(shell _Mirror at: 'x') name: 'y'
Since functions in Self are just objects, we can modify them, too:
_AddSlots: (| f = (| x = 3 | x) |) "f is a function returning 3" (shell _Mirror at: 'f') contents at: 'x' Put: 4 f "prints 4"