Welcome to Slug: A Practical Developer Course
Welcome. This guide teaches Slug through short, runnable lessons with beginner-friendly build-ups.
By the end, you will be able to:
- Read and write idiomatic Slug.
- Build scripts, modules, and data pipelines.
- Use pattern matching, structs, and tagged functions confidently.
- Handle errors and concurrency with
defer,spawn, andawait.
How to use this guide
Each module includes:
- A quick mental model.
- Small runnable examples.
- Expected outputs for key snippets.
- Common mistakes to avoid.
- A
Try itexercise.
Keep a terminal open while reading and run each snippet.
Course map
- Getting Started
- Core Building Blocks
- Functional Programming
- Data Structures
- Flow Control
- Mini Project
- Testing
- Concurrency
- Reference
Let’s build with Slug.
Module 1: Getting Started
In this module, you will run Slug code, understand module resolution, and inspect CLI inputs.
Lesson 1.1: Your first program
Mental model
A Slug file is a sequence of statements. The CLI loads one entry module and executes it.
Create hello.slug:
println("Hello, Slug!")
Run it:
slug hello.slug
Expected output:
Hello, Slug!
Common mistakes:
- Running from a different folder than the file path you pass.
Try it
Print two values with println("hello", 123) and verify both appear.
Lesson 1.2: How Slug resolves imports
Mental model
import("x") is resolved relative to the entry module first, then library paths.
If you run:
slug ./tests/bytes.slug
then import("slug.std") is searched in:
./tests/slug/std.slug$SLUG_HOME/lib/slug/std.slug
If the CLI target is not a local file, Slug treats it as a module name and searches local then library paths.
Common mistakes:
- Assuming imports are always resolved from current working directory only.
Lesson 1.3: Program arguments
Mental model
Everything after the entry target is user input.
-
argv()returns raw argument list. -
argm()returns parsed options + positional args.
println(argv())
println(argm())
Run:
slug script.slug --user knuckles input.txt
Typical shape:
argv: ["--user", "knuckles", "input.txt"]
argm: { options: {user: "knuckles"}, positional: ["input.txt"] }
Try it
Print just argm().options and argm().positional separately.
Module 2: Core Building Blocks
This module introduces the syntax you will use constantly.
Lesson 2.1: Core values and types
Mental model
Slug is expression-oriented: most constructs produce values.
Common value shapes:
-
nil,true,false - numbers (DEC64-inspired)
- strings and bytes (
0x"ff00") - lists (
[1, 2, 3]) - maps (
{name: "Slug"}) - functions (
fn(...) { ... }) - task handles (from
spawn) - symbols (
:ok,:"Content-Type")
Lesson 2.2: Comments
# line comment
// also a line comment
/* block comment */
/** doc comment */
Common mistakes:
- Forgetting doc comments are a distinct form used for docs/metadata.
Lesson 2.3: Strings and interpolation
Build up in steps:
val name = "Slug"
val msg = "Hello "
val raw = 'C:\temp\file.txt'
println(msg, raw)
Expected output:
Hello Slug C:\temp\file.txt
Lesson 2.4: Numeric and bytes literals
val users = 1_000_000
val hex = 0x10_ff
val bytes = 0x"414243"
println(users, hex, bytes)
Expected output (shape):
1000000 4351 <bytes value>
Lesson 2.5: var vs val
Mental model
-
val: bind once. -
var: reassignable binding.
var counter = 0
val label = "requests"
counter = counter + 1
println(counter, label)
Common mistakes:
- Reassigning a
val.
Lesson 2.6: Semicolons and newlines
val a = 1
val b = 2
println(a + b)
Line continuation example:
val sql =
"select *"
+ " from users"
Common mistakes:
- Breaking lines where parser expects expression completion.
Lesson 2.7: Trailing commas
val m = {
user: "slug",
}
println(
m,
)
Trailing commas are allowed in maps/lists/call args/tags.
Lesson 2.8: Everyday builtins
val {*} = import("slug.std")
println(len([1, 2, 3]))
print("hello")
println(" world")
Lesson 2.9: Modules and exports
// math.slug
@export
val add = fn(a, b) { a + b }
// app.slug
val math = import("math")
println(math.add(2, 3))
Expected output:
5
Lesson 2.10: Functions, defaults, and named args
val greet = fn(name, title = "Mx") { "Hello " }
println(greet("Slug"))
println(greet(name: "Slug", title: "Dr"))
Lesson 2.11: Pipelines (/>)
Mental model
x /> f(y) rewrites to f(x, y).
val double = fn(n) { n * 2 }
println(10 /> double)
Pipeline into match:
10 /> match {
10 => "ten"
_ => "other"
} /> println()
Common mistakes:
- Assuming
/>is method dispatch only; it is general call piping.
Lesson 2.12: Tagged dispatch
fn add(a:num, b:num) { a + b }
fn add(a:str, b:str) { a + b }
println(add(1, 2))
println(add("a", "b"))
Common type names: num, str, bool, list, map, bytes, fn, task, sym, chan.
Try it
Add a list overload for add that concatenates two lists, then print add([1], [2]).
Module 3: Functional Programming
This module focuses on transforming data with small composable functions.
Lesson 3.1: Map, filter, reduce
Mental model
-
map: transform each item -
filter: keep some items -
reduce: combine into one result
val {*} = import("slug.std")
val xs = [1, 2, 3, 4, 5]
val squares = xs /> map(fn(v) { v * v })
val evens = xs /> filter(fn(v) { v % 2 == 0 })
val sum = xs /> reduce(0, fn(acc, v) { acc + v })
println(squares)
println(evens)
println(sum)
Expected output:
[1, 4, 9, 16, 25]
[2, 4]
15
Lesson 3.2: Match expressions
val classify = fn(v) {
match v {
0 => "zero"
1 => "one"
_ => "other"
}
}
println(classify(1))
println(classify(10))
Common mistakes:
- Forgetting
_fallback case.
Lesson 3.3: Destructuring patterns
val headOrZero = fn(xs) {
match xs {
[h, ..._] => h
[] => 0
}
}
println(headOrZero([10, 20]))
println(headOrZero([]))
Pattern tools:
-
_wildcard -
...restspread capture -
^namepin -
{| ... |}exact-map pattern
val expected = 42
match 42 {
^expected => println("matched")
_ => println("nope")
}
Lesson 3.4: Higher-order functions
val applyTwice = fn(f, v) { f(f(v)) }
val inc = fn(x) { x + 1 }
println(applyTwice(inc, 10))
Try it
Write times(n, f, x) using recur.
Module 4: Data Structures
You will use lists, maps, symbols, and structs together.
Lesson 4.1: Lists
val xs = [10, 20, 30, 40]
println(xs[1])
println(xs[-1])
println(xs[1:3])
println(xs[0:2])
println(xs[2:])
Common mistakes:
- Confusing slice bounds (
start:end) with single index access.
Lesson 4.2: Maps and symbol keys
Mental model
Bare map keys are symbols.
val m = {name: "Slug", age: 2}
println(m[:name])
println(m.name)
Lesson 4.3: Symbols
println(:ok)
println(:"Content-Type")
println(sym("user id"))
println(label(:ok))
Lesson 4.4: Struct schemas and values
Step-by-step:
val User = struct {
name:str,
age:num,
active = true,
}
val u1 = User { name: "Slug", age: 2 }
val u2 = u1 copy { age: 3 }
println(type(u1))
println(keys(u1))
println(u2.age)
Lesson 4.5: Matching maps and structs
match u2 {
User { name, age } => println(name, age)
_ => println("unknown")
}
match m {
{| :name: n, :age: a |} => println(n, a)
_ => println("missing fields")
}
Common mistakes:
- Using non-exact map patterns when exact field set is required.
Try it
Create Account with id, email, and default active = true, then copy it with a new email.
Module 5: Flow Control
This module covers branching, loops via recursion, and robust error handling.
Lesson 5.1: Conditionals
Mental model
if is an expression, so both branches should produce a value.
val max = fn(a, b) {
if (a > b) { a } else { b }
}
println(max(3, 5))
Expected output:
5
Lesson 5.2: Looping with tail recursion and recur
recur must be in tail position.
val sumTo = fn(n, acc = 0) {
if (n == 0) {
acc
} else {
recur(n - 1, acc + n)
}
}
println(sumTo(5))
Expected output:
15
Common mistakes:
- Calling
recur(...)and then doing more work after it.
Lesson 5.3: Throwing errors
val Error = struct {
type:str = "Error",
msg:str,
code = nil,
data = nil,
cause = nil,
}
val divide = fn(a, b) {
if (b == 0) {
throw Error { type: "ValidationError", msg: "divisor cannot be zero" }
}
a / b
}
Lesson 5.4: defer, defer onsuccess, defer onerror(err)
Mental model
-
defer: always runs when scope exits. -
defer onsuccess: only on success path. -
defer onerror(err): only on thrown error path.
val run = fn(x) {
defer { println("always") }
defer onsuccess { println("success") }
defer onerror(err) { println("failed:", err.msg) }
if (x < 0) {
throw Error { type: "ValidationError", msg: "x must be >= 0" }
}
x * 2
}
println(run(2))
Common mistakes:
- Treating
onsuccessoronerroras global keywords outsidedefer.
Try it
Write parsePositive(n) that throws on n <= 0 and logs failures with defer onerror(err).
Module 6: Mini Project - Data Pipeline
Build a complete mini project in small steps.
Goal
Given a list of numbers:
- Keep only odd numbers.
- Square each value.
- Sum the result.
- Return a structured report.
Step 1: Transform and sum
val {*} = import("slug.std")
val run = fn(xs) {
xs
/> filter(fn(v) { v % 2 != 0 })
/> map(fn(v) { v * v })
/> reduce(0, fn(acc, v) { acc + v })
}
println(run([1, 2, 3, 4, 5]))
Expected output:
35
Step 2: Add input validation
val Error = struct {
type:str = "Error",
msg:str,
}
val runSafe = fn(xs) {
if (len(xs) == 0) {
throw Error { type: "InputError", msg: "expected at least one value" }
}
run(xs)
}
Step 3: Return a report map
val runReport = fn(xs) {
val odds = xs /> filter(fn(v) { v % 2 != 0 })
val squares = odds /> map(fn(v) { v * v })
val total = squares /> reduce(0, fn(acc, v) { acc + v })
{
inputCount: len(xs),
processedCount: len(odds),
total: total,
}
}
println(runReport([1, 2, 3, 4, 5]))
Common mistakes:
- Mutating intermediate structures instead of recomputing immutable values.
Challenge
Add maxSquare to the report.
Module 7: Testing
This module walks through practical test-writing patterns.
Lesson 7.1: A basic behaviour test
val {*} = import("slug.test")
val add = fn(a, b) { a + b }
@test
val addWorks = fn() {
assertEqual(add(2, 3), 5)
}
Why this matters
A tiny, focused behaviour test is easier to debug than one large test.
Lesson 7.2: Parameterized tests with @testWith
val {*} = import("slug.test")
@testWith(
[3, 5], 8,
[10, -5], 5,
[0, 0], 0,
)
val addCases = fn(a, b) {
a + b
}
Mental model
- each input tuple is executed,
- return value is compared to expected value.
Lesson 7.3: Testing errors
val Error = struct { type:str = "Error", msg:str }
val mustPositive = fn(n) {
if (n <= 0) { throw Error { type: "ValidationError", msg: "n must be positive" } }
n
}
@test
val mustPositiveThrows = fn() {
fn() { mustPositive(0) } /> assertThrows()
}
Lesson 7.4: Running tests
slug test path_to_source.slug
Common mistakes:
- Asserting too many behaviours in one test.
- Using unclear test names.
Try it
Add one new @testWith case and one new error-path test.
Module 8: Concurrency
Slug concurrency is structured: tasks belong to a logical scope and are awaited explicitly.
Lesson 8.1: Core ideas
-
spawnstarts child work. -
await(handle)waits for result. -
nurserydefines ownership boundary. - errors/cancellation flow through the structure.
Lesson 8.2: Start with one spawned task
val work = nursery fn() {
val t = spawn { 20 + 22 }
await(t)
}
println(work())
Expected output:
42
Lesson 8.3: Add timeout-aware await
val getSlow = nursery fn() {
val t = spawn { slowTask() }
await(t, 500)
}
Mental model
-
await(handle)blocks until completion. -
await(handle, timeoutMs)throws on timeout.
Lesson 8.4: Fan-out and fan-in
val fetchBoth = nursery fn(id) {
val userT = spawn { fetchUser(id) }
val postsT = spawn { fetchPosts(id) }
val user = await(userT, 500)
val posts = await(postsT, 1000)
{ user: user, posts: posts }
}
Lesson 8.5: Limits with nursery limit
val handler = nursery limit 10 fn(ids) {
ids /> map(fn(id) { spawn { fetchUser(id) } })
}
Use limits when fan-out can grow large.
Lesson 8.6: Error handling around async flows
val run = nursery fn() {
defer onerror(err) { println("task flow failed:", err) }
val t = spawn { riskyWork() }
await(t)
}
Common mistakes:
- Forgetting to await handles you care about.
- Treating cancellation as immediate at every line (it is observed at suspension points).
Try it
Implement awaitAll(handles) that returns results in order and rethrows first failure.
Module 9: Reference
Use this as a quick syntax and precedence lookup.
Operator precedence (lowest to highest)
| Level | Operators | Associativity |
|---|---|---|
| 1 | = | Right |
| 2 | \|\| | Left |
| 3 | && | Left |
| 4 |
== !=
| Left |
| 5 |
< <= > >=
| Left |
| 6 | \| | Left |
| 7 | ^ | Left |
| 8 | & | Left |
| 9 |
<< >>
| Left |
| 10 |
+ -
| Left |
| 11 |
* / %
| Left |
| 12 |
:+ +:
| Left |
| 13 | prefix ! - ~
| Right |
| 14 | /> | Left |
| 15 | calls, indexing, dot access, struct init/copy | Left |
Quick syntax reminders
- Symbols:
:ok,:"content-type" - Exact map patterns:
{| :id: id |} - Map literals vs blocks are structurally disambiguated in expression position.
- Defer modes are contextual after
defer:onsuccess,onerror(err).
Minimal examples
val x = 10 /> fn(v) { v * 2 }
val y = {name: "slug"}.name
val z = [1, 2, 3][1]