Tour: Turtle Graphics

Drive a turtle around a 2D plane. Each step adds one new idea, and by step 5 whole shapes execute as data. Showpieces: enums as data, pattern matching, and executing data as a program.

Step 1 — Values & functions

module Main import std.io.stdio.println func report(at x: Int, y: Int, facing heading: Int) { println("turtle at (\(x), \(y)) facing \(heading)°"); } @main func main() { report(at: 0, 0, facing: 0); }

at x and facing heading are labeled (at and facing are the labels; x and heading are the bind names). y: Int between them is positional — bare name, no label — so the call site is report(at: 0, 0, facing: 0).

Step 2 — Structs & methods

<!-- sample: continue --> struct Turtle { var x: Int var y: Int var heading: Int } extend Turtle { mutating func forward(distance: Int) { match self.heading { 0 => self.x = self.x + distance, 90 => self.y = self.y + distance, 180 => self.x = self.x - distance, 270 => self.y = self.y - distance, _ => {} } } mutating func turn(degrees: Int) { self.heading = (self.heading + degrees) % 360; } }

Both methods take a positional Int. Calls look like t.forward(10) and t.turn(90). var fields plus mutating func and the turtle has a body — note that match works on integers here, not just enums.

Replace main to try it:

<!-- sample: continue --> @main func main() { var t = Turtle(x: 0, y: 0, heading: 0); t.forward(10); t.turn(90); t.forward(5); report(at: t.x, t.y, facing: t.heading); }

Step 3 — Enums & pattern matching

A program for the turtle is a list of commands — one enum variant per instruction.

<!-- sample: continue --> enum Command { case Move(distance: Int) case Turn(degrees: Int) case PenUp case PenDown }

Each command is a simple value. A program is a [Command] — a list of instructions the turtle executes in order.

Step 4 — Collections

Now an interpreter. Walk the array, execute each Command.

<!-- sample: continue --> func run(program: [Command], mutating on turtle: Turtle) { for command in program { match command { .Move(distance) => turtle.forward(distance), .Turn(degrees) => turtle.turn(degrees), .PenUp => {}, .PenDown => {} } } }

run takes program: [Command] positionally and mutating on turtle: Turtle with a label — mutating comes before the label. The turtle accumulates all the moves as the loop runs.

Add to main:

<!-- sample: continue --> let program: [Command] = [.Move(distance: 10), .Turn(degrees: 90), .Move(distance: 5)]; run(program, on: t); report(at: t.x, t.y, facing: t.heading);

Step 5 — Protocols

A Shape is anything that produces a [Command]. With a protocol, you can define Square, Triangle, and Spiral as types — and feed them to run interchangeably.

<!-- sample: continue --> protocol Shape { func commands() -> [Command] } struct Square { let size: Int } extend Square: Shape { public func commands() -> [Command] { [ .Move(distance: self.size), .Turn(degrees: 90), .Move(distance: self.size), .Turn(degrees: 90), .Move(distance: self.size), .Turn(degrees: 90), .Move(distance: self.size), .Turn(degrees: 90) ] } } struct Spiral { let turns: Int } extend Spiral: Shape { public func commands() -> [Command] { var result: [Command] = []; var i = 1; while i <= self.turns { result.append(.Move(distance: i * 5)); result.append(.Turn(degrees: 90)); i = i + 1; } result } }

A Shape produces commands; run consumes commands. The two halves never meet directly — the protocol decouples them. That's the same pattern you'll see throughout Kestrel libraries.

Add to main:

<!-- sample: continue --> run(Square(size: 10).commands(), on: t); report(at: t.x, t.y, facing: t.heading); // a square returns the turtle to where it started run(Spiral(turns: 3).commands(), on: t); report(at: t.x, t.y, facing: t.heading);

What you saw

StepFeature
1Functions, positional vs labeled parameters
2var fields, mutating func, match on integers
3Enums with payloads
4for loop, interpreter, mutating parameters
5Protocols, multiple conforming types

The takeaway: programs are data. A [Command] is just a list, but the moment you walk it with match, it's executable. That same trick — represent behavior as data, interpret it later — is how Kestrel handles parsers, query builders, animation timelines, and most things that look like "small languages embedded in code."