Basics

Maggie is a message-passing language: all computation happens by sending messages to objects. Understanding how messages work is the key to reading and writing Maggie code. This chapter covers the three message types, evaluation order, cascades, variables, comments, the special objects nil, true, and false, and the difference between identity and equality.

Test
3 + 4 >>> 7
'hello' size >>> 5
true not >>> false
nil isNil >>> true

Every piece of Maggie code is an expression that produces a value. Expressions are separated by periods. The last expression in a sequence is the result.

There are no statements in Maggie. Even assignment returns the assigned value, and control flow is done by sending messages to booleans and blocks.

Test
3 + 4 >>> 7
'hello' , ' world' >>> 'hello world'
42 even >>> true

Messages are the fundamental operation in Maggie. When you write 3 + 4, you are sending the message + with argument 4 to the object 3. There are three kinds of messages, distinguished by their syntax.

Unary messages take no arguments. They are a single word after the receiver. Examples include size, not, abs, negated, even, and factorial.

Test
'hello' size >>> 5
true not >>> false
-5 abs >>> 5
7 odd >>> true
5 factorial >>> 120

Binary messages take exactly one argument and use an operator symbol as the selector. Common binary messages include arithmetic (+, -, *, /, \\), comparison (<, >, <=, >=, =, ~=), string concatenation (,), and logical operators (&, |).

Test
3 + 4 >>> 7
10 - 3 >>> 7
6 * 7 >>> 42
10 / 3 >>> 3
10 \\ 3 >>> 1
5 < 10 >>> true
'hello' , ' world' >>> 'hello world'
true & false >>> false
true | false >>> true

Keyword messages have one or more colon-terminated keywords, each followed by an argument. The full selector is formed by joining all the keywords. For example, at: 0 has the selector at:, and at: 1 put: 42 has the selector at:put:.

Test
#(10 20 30) at: 0 >>> 10
3 max: 5 >>> 5
5 between: 1 and: 10 >>> true
'hello' copyFrom: 1 to: 4 >>> 'ell'

Message precedence determines evaluation order when multiple messages appear in a single expression. The rule is simple:

1. Unary messages are sent first (highest precedence). 2. Binary messages are sent next. 3. Keyword messages are sent last (lowest precedence).

Within the same precedence level, messages are evaluated left to right. This means arithmetic does NOT follow standard math precedence -- 2 + 3 * 4 evaluates as (2 + 3) * 4, not 2 + (3 * 4).

Use parentheses to override the default order.

Test
2 + 3 * 4 >>> 20
2 + (3 * 4) >>> 14
-5 abs + 3 >>> 8
1 + 2 max: 0 >>> 3
(1 + 2) max: 0 >>> 3

Unary messages chain naturally because each one returns a new value that becomes the receiver of the next message. They evaluate left to right.

Test
-5 abs >>> 5
-5 abs negated >>> -5
-5 abs negated abs >>> 5
7 factorial even >>> true

Cascades let you send multiple messages to the same receiver without repeating it. Use a semicolon (;) to separate cascaded messages. The result of a cascade is the result of the last message sent.

To get the receiver itself back from a cascade, end with yourself -- a method defined on Object that simply returns self.

Test
a := Array new: 3.
a at: 0 put: 10; at: 1 put: 20; at: 2 put: 30.
a at: 0 >>> 10
a at: 1 >>> 20
a at: 2 >>> 30

Variables are created with the := assignment operator. Inside method bodies, local variables can be declared in a | var1 var2 | block at the top, or simply assigned with :=. Assignment returns the assigned value.

Variable names start with a lowercase letter and use camelCase by convention.

Test
x := 42.
x >>> 42
x := x + 1.
x >>> 43
name := 'Maggie'.
greeting := 'Hello, ' , name.
greeting >>> 'Hello, Maggie'

Maggie has two comment styles. Line comments start with # and extend to the end of the line. They can appear at the start of a line or after code. Inside method bodies, traditional Smalltalk comments enclosed in double quotes are also supported. These are useful for inline annotations.

Docstrings are a separate feature -- triple-quoted strings that attach documentation to classes, traits, and methods. They are preserved in the compiled image for runtime access, not discarded like comments.

nil is the sole instance of UndefinedObject. It represents the absence of a value. Uninitialized variables default to nil. Test for nil with isNil and notNil.

true is the sole instance of True, and false is the sole instance of False. Both inherit from Boolean. Booleans support not, & (and), | (or), and conditional messages like ifTrue:, ifFalse:, and ifTrue:ifFalse:.

Test
nil isNil >>> true
nil notNil >>> false
42 isNil >>> false
42 notNil >>> true
nil class name >>> #UndefinedObject
true class name >>> #True
false class name >>> #False
true not >>> false
false not >>> true
true & false >>> false
true | false >>> true

Booleans drive control flow through messages. ifTrue: evaluates a block only when the receiver is true. ifFalse: evaluates its block only when the receiver is false. ifTrue:ifFalse: chooses between two blocks.

Test
(3 > 2) ifTrue: ['yes'] ifFalse: ['no'] >>> 'yes'
(1 > 5) ifTrue: ['yes'] ifFalse: ['no'] >>> 'no'
true ifTrue: ['hello'] >>> 'hello'
false ifTrue: ['hello'] >>> nil
false ifFalse: ['nope'] >>> 'nope'

Identity (==) tests whether two references point to the exact same object. Equality (=) tests whether two objects have the same value. The default implementation of = on Object delegates to ==, but subclasses like String override it to compare contents.

For small integers and symbols, identity and equality behave the same way because these values are interned (each distinct value exists only once in memory). For strings, two separately created strings with the same characters are equal (=) but may not be identical (==).

The negated forms are ~~ (not identical) and ~= (not equal).

Test
3 == 3 >>> true
3 = 3 >>> true
nil == nil >>> true
#hello == #hello >>> true
#hello = #hello >>> true
'abc' = 'abc' >>> true
3 ~= 4 >>> true
3 ~~ 4 >>> true

self refers to the current receiver -- the object that received the message that invoked the current method. It is used to send messages to the same object, access instance variables, and return the receiver.

Test
3 yourself >>> 3
'hello' yourself >>> 'hello'
-5 abs >>> 5

super also refers to the current receiver, but message lookups start from the superclass of the class where the method is defined, rather than from the receiver's actual class. This is how subclasses can extend inherited behavior without replacing it entirely.

You will see super used most often in initialization methods and in printString overrides, where a subclass wants to include the superclass behavior and then add to it.

super cannot be demonstrated with simple literals because it requires a class hierarchy. You will see it in action in the Classes chapter. The key point is: self always means the current receiver, super means the current receiver but dispatched through the parent class.

Every object knows its class. Send class to get the class object, and name to that to get a symbol identifying the class.

Test
42 class name >>> #SmallInteger
3.14 class name >>> #Float
'hello' class name >>> #String
#hello class name >>> #Symbol
true class name >>> #True
false class name >>> #False
nil class name >>> #UndefinedObject
#(1 2 3) class name >>> #Array

Here is a summary of the key points from this chapter.

Messages come in three types: unary (no arguments), binary (one argument with an operator), and keyword (named arguments with colons). Unary binds tightest, then binary, then keyword. Within the same level, evaluation is left to right. Use parentheses to override.

Cascades (;) send multiple messages to the same receiver. Variables use := for assignment. The special objects nil, true, and false are singletons. Identity (==) checks same-object; equality (=) checks same-value. self is the current receiver; super dispatches through the parent class.

Test
2 + 3 * 4 >>> 20
(2 + 3) * 4 >>> 20
2 + (3 * 4) >>> 14
nil isNil >>> true
3 == 3 >>> true
'abc' = 'abc' >>> true
3 yourself >>> 3