At the heart of Io lies a simple but powerful idea: all computation happens through message passing. Objects communicate by sending messages to each other, and objects respond to messages by looking up slots. This chapter explores this fundamental mechanism in depth.
When you write this in Io:
person setName("Alice")What actually happens? Let's break it down:
personis the receiver - the object receiving the messagesetNameis the message name (or selector)"Alice"is the argument to the message- The entire expression is a message send
But here's where it gets interesting. Messages are objects too:
// Create a message object
msg := message(person setName("Alice"))
// Inspect it
msg name println // setName
msg arguments println // list(Message_0x...)
msg arguments first code println // "Alice"
// Execute it
msg doInContext(Lobby) // Actually calls person setName("Alice")Slots are named storage locations in objects. They can hold any value:
obj := Object clone
// Create slots with different values
obj number := 42 // Number
obj text := "hello" // String
obj method := method(x, x * 2) // Method
obj child := Object clone // Another object
obj flag := true // Boolean
// List all slots
obj slotNames println
// list(number, text, method, child, flag)
// Check for slots
obj hasSlot("number") println // true
obj hasSlot("missing") println // false
// Get slot values
obj getSlot("number") println // 42
obj getSlot("method") println // method(x, ...)When an object receives a message, Io follows a specific algorithm to find the corresponding slot:
Animal := Object clone
Animal speak := method("generic sound" println)
Dog := Animal clone
Dog speak := method("woof" println)
Dog wagTail := method("wagging..." println)
rover := Dog clone
rover name := "Rover"
// When rover receives 'speak':
rover speak
// 1. Look for 'speak' in rover - not found
// 2. Look for 'speak' in rover's proto (Dog) - found!
// 3. Execute Dog's speak method with rover as self
// When rover receives 'name':
rover name
// 1. Look for 'name' in rover - found!
// 2. Return the value
// Visual representation:
/*
Object
↑
Animal (speak: "generic sound")
↑
Dog (speak: "woof", wagTail)
↑
rover (name: "Rover")
*/Io distinguishes between creating new slots and updating existing ones:
obj := Object clone
// Create a new slot with :=
obj x := 10
obj hasSlot("x") println // true
// Update existing slot with =
obj x = 20
obj x println // 20
// Trying to update non-existent slot fails
obj y = 30 // Exception: Slot y not found
// But you can use setSlot to create or update
obj setSlot("y", 30) // Creates if doesn't exist
obj y println // 30
// Remove slots
obj removeSlot("y")
obj hasSlot("y") println // falseThis distinction helps catch typos:
counter := 0
countr = 1 // Error! Probably meant 'counter'In Io, methods aren't special—they're just slots that hold executable blocks:
Calculator := Object clone
// Method is just a slot containing a method object
Calculator add := method(a, b, a + b)
// You can manipulate methods like any other value
addMethod := Calculator getSlot("add")
addMethod type println // Block
// You can copy methods between objects
ScientificCalc := Object clone
ScientificCalc addition := Calculator getSlot("add")
ScientificCalc addition(5, 3) println // 8
// You can even store methods in variables
operation := method(x, x * 2)
Calculator double := operation
Calculator double(21) println // 42Every method has access to special variables:
Printer := Object clone
Printer name := "HP"
Printer print := method(doc,
("Printer: " .. self name) println // self = receiver
("Sender: " .. sender type) println // sender = who sent the message
("Document: " .. doc) println
)
Computer := Object clone
Computer sendJob := method(
Printer print("report.pdf")
)
Computer sendJob
// Printer: HP
// Sender: Computer
// Document: report.pdfWhen an object doesn't have a slot for a received message, it calls forward:
Proxy := Object clone
Proxy target := nil
Proxy forward := method(
("Forwarding " .. call message name .. " to target") println
call evalArgAt(0) // This would forward to target
)
p := Proxy clone
p doSomething("arg")
// Forwarding doSomething to targetThis enables powerful patterns like delegation and method missing:
// Ruby-style method_missing
DynamicObject := Object clone
DynamicObject forward := method(
methodName := call message name
if(methodName beginsWithSeq("get"),
# Handle getters
property := methodName afterSeq("get") lowercase
self getSlot(property),
# Handle setters
if(methodName beginsWithSeq("set"),
property := methodName afterSeq("set") lowercase
value := call evalArgAt(0)
self setSlot(property, value)
)
)
)
obj := DynamicObject clone
obj setName("Alice") // Creates 'name' slot
obj getName println // "Alice"Messages don't evaluate immediately—they're data structures you can manipulate:
// Messages as data
expr := message(2 + 3 * 4)
expr println // 2 +(3 *(4))
// Evaluate when ready
result := expr doInContext(Lobby)
result println // 14
// Modify messages before evaluation
expr := message(x + y)
context := Object clone
context x := 10
context y := 20
expr doInContext(context) println // 30This enables macro-like capabilities:
// Create a timing macro
Object time := method(
code := call argAt(0) // Get the message, not its value
start := Date now
result := code doInContext(call sender)
elapsed := Date now - start
("Elapsed: " .. elapsed) println
result
)
// Use it
time(
sum := 0
for(i, 1, 1000000, sum = sum + i)
sum
)
// Elapsed: 0.234
// Returns: 500000500000The call object provides detailed information about the current method invocation:
Object debug := method(
"=== Call Debug ===" println
("Sender: " .. call sender type) println
("Target: " .. call target type) println
("Message: " .. call message name) println
("Args: " .. call message arguments) println
("Activated: " .. call activated type) println
"================" println
)
TestObject := Object clone
TestObject test := method(
debug
)
TestObject test
// === Call Debug ===
// Sender: Lobby
// Target: TestObject
// Message: debug
// Args: list()
// Activated: Block
// ================Operators are messages with special precedence rules. Unlike many languages where operators are built-in syntax, in Io they're regular messages that follow configurable precedence levels. This means you can treat operators like any other method - you can override them, create new ones, or call them using regular message passing syntax.
The OperatorTable manages operator precedence, with levels from 0 (highest) to 13 (lowest). Standard arithmetic follows familiar rules: multiplication and division (level 3) bind tighter than addition and subtraction (level 4). Assignment operators like := and = have the lowest precedence (level 13), ensuring they capture everything to their right.
// These are equivalent
2 + 3 * 4
2 +(3 *(4))
// You can see the precedence
OperatorTable println
// You can add custom operators
OperatorTable addOperator("@@", 5)
Number @@ := method(n,
self pow(n) + n pow(self)
)
2 @@ 3 println // 17 (2^3 + 3^2 = 8 + 9)
// Operators are just messages
5 send("+", 3) println // 8
"hello" send("at", 1) println // eThis unified treatment of operators as messages enables powerful metaprogramming techniques. You can intercept arithmetic operations for logging, create domain-specific operators for mathematical or business logic, or even implement operator overloading for custom types - all using the same message passing mechanism that underlies the entire language.
Even assignment is message passing:
// These are equivalent
x := 10
setSlot("x", 10)
// And these
x = 20
updateSlot("x", 20)
// You can override assignment behavior
Object setSlot := method(name, value,
("Setting " .. name .. " to " .. value) println
resend // Call original setSlot
)
y := 42
// Setting y to 42Io distinguishes between activatable and non-activatable values:
obj := Object clone
// Methods are activatable - they run when accessed
obj greet := method("Hello!" println)
obj greet // Prints "Hello!"
// Other values are just returned
obj name := "Alice"
obj name // Returns "Alice"
// You can get a method without activating it
m := obj getSlot("greet")
m println // method(...)
// And activate it later
m call // Prints "Hello!"
// Check if something is activatable
obj getSlot("greet") isActivatable println // true
obj getSlot("name") isActivatable println // falseLet's build a simple HTML DSL using messages:
HTML := Object clone
HTML forward := method(
tagName := call message name
args := call message arguments
// Build opening tag
result := "<" .. tagName
// Handle attributes (first arg if it's a Map)
if(args size > 0 and args at(0) name == "curlyBrackets",
attrs := call evalArgAt(0)
attrs foreach(key, value,
result = result .. " " .. key .. "=\"" .. value .. "\""
)
args removeFirst
)
result = result .. ">"
// Handle content
args foreach(arg,
content := call sender doMessage(arg)
if(content, result = result .. content)
)
// Closing tag
result = result .. "</" .. tagName .. ">"
result
)
// Usage
html := HTML clone
page := html div({ "class": "container" },
html h1("Welcome"),
html p("This is a paragraph"),
html ul(
html li("Item 1"),
html li("Item 2")
)
)
page println
// <div class="container"><h1>Welcome</h1><p>This is a paragraph</p><ul><li>Item 1</li><li>Item 2</li></ul></div>Message passing has overhead compared to direct function calls:
// Traditional method call
obj := Object clone
obj directMethod := method(x, x * 2)
// Message construction and sending
msg := Message clone setName("directMethod") setArguments(list(Message clone setName("5")))
// Benchmark
time(
100000 times(obj directMethod(5))
)
// 0.015000 seconds
time(
100000 times(obj doMessage(msg))
)
// 0.048000 seconds
// Direct calls are ~3x faster, but message objects enable metaprogrammingMessage passing and slot manipulation enable elegant implementations of common design patterns. These patterns leverage Io's dynamic nature to create flexible, maintainable code structures that would require significant boilerplate in more static languages.
This pattern demonstrates how to dynamically generate getters and setters using message construction. Instead of manually writing accessor methods for each property, we use Io's metaprogramming capabilities to create them on demand. The generated methods follow a naming convention where properties are stored with an underscore prefix internally, while the public interface uses clean method names.
Person := Object clone
Person init := method(
self name := nil
self age := nil
self
)
// Generate getters/setters with messages
Person addAccessors := method(slotName,
// Getter
self setSlot(slotName,
method(self getSlot("_" .. slotName))
)
// Setter
self setSlot("set" .. slotName asCapitalized,
method(value, self setSlot("_" .. slotName, value))
)
)
Person addAccessors("name")
Person addAccessors("age")
p := Person clone
p setName("Alice")
p name println // "Alice"The beauty of this approach is that it scales effortlessly - adding new properties requires just one line of code per property, and the accessor methods are created with consistent behavior and naming. This pattern is particularly useful when building data models or domain objects where property access needs to be controlled or monitored.
The Chain of Responsibility pattern creates a pipeline of handlers where each handler decides whether to process a request or pass it to the next handler. In Io, this pattern is particularly clean because message passing naturally supports delegation. Each handler in the chain examines the request and either processes it or forwards it using simple message passing.
Handler := Object clone
Handler next := nil
Handler handle := method(request,
if(self canHandle(request),
self process(request),
if(next, next handle(request))
)
)
AuthHandler := Handler clone
AuthHandler canHandle := method(request,
request hasSlot("needsAuth")
)
AuthHandler process := method(request,
"Authenticating..." println
)
LogHandler := Handler clone
LogHandler canHandle := method(request, true)
LogHandler process := method(request,
("Logging: " .. request type) println
)
// Build chain
auth := AuthHandler clone
log := LogHandler clone
auth next := log
// Process requests
request := Object clone
request type := "GET"
request needsAuth := true
auth handle(request)
// Authenticating...
// Logging: GETThis pattern is invaluable for building middleware systems, request processors, or event handling pipelines. The chain can be dynamically reconfigured at runtime by simply changing the next references, and new handler types can be added without modifying existing code. The pattern also naturally supports optional processing - handlers can choose to stop the chain or allow it to continue based on the request's characteristics.
Understanding message flow is crucial for debugging:
Object trace := method(
self setSlot("forward",
method(
("Missing: " .. call message name) println
("Arguments: " .. call message arguments) println
("Sender: " .. sender type) println
)
)
self
)
buggy := Object clone trace
buggy doSomethingWrong(1, 2, 3)
// Missing: doSomethingWrong
// Arguments: list(1, 2, 3)
// Sender: Lobby-
Message Logger: Create a wrapper that logs all messages sent to an object, including arguments and return values.
-
Lazy Properties: Implement properties that are only computed when first accessed, then cached.
-
Message Queue: Build an object that queues messages and executes them later in order.
-
Method Decorators: Create a system for wrapping methods with before/after behavior using messages.
-
Message Router: Build a router that directs messages to different handlers based on patterns.
Rewriter := Object clone
Rewriter forward := method(
msg := call message
// Rewrite add to multiply
if(msg name == "add",
msg setName("multiply")
)
// Continue with modified message
resend
)
calc := Rewriter clone
calc multiply := method(a, b, a * b)
calc add(3, 4) println // 12 (rewritten to multiply!)Object sendIf := method(condition, messageName,
if(condition,
self doMessage(Message clone setName(messageName))
)
)
Object sendUnless := method(condition, messageName,
if(condition not,
self doMessage(Message clone setName(messageName))
)
)
obj := Object clone
obj greet := method("Hello!" println)
obj sendIf(true, "greet") // Hello!
obj sendUnless(false, "greet") // Hello!Messages and slots form the foundation of Io's object model. Every computation—from simple arithmetic to complex method calls—is accomplished through message passing. Objects store their state and behavior in slots, and respond to messages by looking up the corresponding slots.
This uniform model provides incredible flexibility. You can intercept messages, forward them, rewrite them, or queue them. You can introspect the entire message-passing process. You can build DSLs that feel native to the language. And you can debug by tracing the flow of messages through your system.
Understanding messages and slots deeply is essential to mastering Io. They're not just an implementation detail—they're the conceptual core that makes Io's radical simplicity possible.