Chapter 2: Enough Odin to be dangerous
This chapter:
- Variables, constants, and how to convert between types.
- The four ways to use Odin's loop keyword,
for. switch, with no fallthrough by default.- Procedures: multiple return values and default arguments.
- A working example that uses all of it: a small "what kind of number is this" checker.
Where we left off: a working toolchain and one line of printed output. This chapter teaches the vocabulary the rest of our journey depends on.
2.1 Variables and constants
Two ways to introduce a name in Odin:
message := "hello" // type inferred from the value: string
count: int = 0 // type stated explicitly
pi :: 3.14159 // a constant, computed at compile time
:= declares a variable and infers its type from the right-hand side. name: Type = value states the type explicitly: useful when you want a specific type (f32 rather than the inferred f64, say) or when there's no initial value yet. :: declares a constant: a name bound once, at compile time, that can never be reassigned.
Odin note: why bother stating a type at all, if
:=can infer it? Mostly for the numeric types. Odin infersintfor a whole-number literal andf64for anything with a decimal point; if you actually wantf32or a smaller integer, you say so explicitly.
2.2 Basic types
The most common ones we'll use:
int: a whole number, the default for anything counted or indexed.f64: a floating-point number (my default;f32shows up rarely).bool:trueorfalse.string: an immutable, read-only view over text (properly explained in Chapter 4).rune: a single Unicode codepoint (more in Chapter 4).
2.3 Odin doesn't coerce
This is the single biggest adjustment if you're coming from Python, JavaScript or even C: Odin never silently converts one numeric type into another. Try to add an int and an f64 and the compiler stops you:
a: int = 3
b: f64 = 2.0
c := a + b // compile error
Error: Mismatched types in binary expression 'a + b' : 'int' vs 'f64'
You have to say what you mean, with an explicit conversion:
c := f64(a) + b // 5.0, fine, both operands are now f64
Why does Odin insist on this? It's a deliberate design feature. A silent
int+floatmix hides a real decision: which number is getting reinterpreted, and do you lose precision doing it? Odin makes you write the conversion, so the decision is visible in the code rather than buried in a language rule you have to memorise.
2.4 if and else
No parentheses required around the condition; braces are required around the body, even for one statement:
if age >= 18 {
fmt.println("adult")
} else if age >= 13 {
fmt.println("teenager")
} else {
fmt.println("child")
}
2.5 for, in four shapes
Odin has exactly one loop keyword, for, used four ways. If you're coming from a C-family language, this is the other big adjustment: there's no separate while.
// 1. Three-part, C-style: init; condition; post
for i := 0; i < 3; i += 1 {
fmt.println("three-part:", i)
}
// 2. Condition only: this is Odin's "while"
j := 0
for j < 3 {
fmt.println("condition-only:", j)
j += 1
}
// 3. No condition at all: loops forever until a `break`
count := 0
for {
if count >= 3 {
break
}
fmt.println("infinite (broken):", count)
count += 1
}
// 4. Range-based: the one you'll write most often
for i in 0 ..< 3 {
fmt.println("range:", i)
}
Odin note: reading
0 ..< 3...<is a half-open range:0, 1, 2; it excludes the top end, exactly like indexing a slice of length 3...=is the closed variant, which includes the top end (0 ..= 3is0, 1, 2, 3). You'll see me use both; reach for..<whenever you're counting up to but not including a length, which is most of the time.
2.6 switch
switch compares a value against several cases, and, unlike C, does not fall through to the next case by default:
switch day {
case 1, 7:
fmt.println("weekend")
case 2, 3, 4, 5, 6:
fmt.println("weekday")
case:
fmt.println("not a valid day")
}
A bare switch with no value to compare, and boolean conditions in each case, is a clean way to write a chain of if/else if that reads a little tidier once there are more than two branches; you'll see it in the example further down.
2.7 Procedures
A procedure can return more than one value, and parameters can have default values:
divmod :: proc(a, b: int) -> (q: int, r: int) {
return a / b, a % b
}
greet :: proc(name: string, excited := false) {
if excited {
fmt.printfln("Hello, %s!!", name)
} else {
fmt.printfln("Hello, %s.", name)
}
}
q, r := divmod(17, 5)
fmt.printfln("17 / 5 = %d remainder %d", q, r) // 17 / 5 = 3 remainder 2
greet("Billy") // Hello, Billy.
greet("Billy", true) // Hello, Billy!!
2.8 Chapter Example
Put it together: a procedure that classifies a number, using everything above. Create the file, enter the code and try to run it yourself. The answer is in the checkpoint if you get stuck.
// numbers.odin
package main
import "core:fmt"
is_prime :: proc(n: int) -> bool {
if n < 2 {
return false
}
for i := 2; i*i <= n; i += 1 {
if n % i == 0 {
return false
}
}
return true
}
describe :: proc(n: int) -> string {
switch {
case n < 0:
return "negative"
case n == 0:
return "zero"
case is_prime(n):
return "prime"
case:
return "composite"
}
}
main :: proc() {
for n in 0 ..< 12 {
fmt.printfln("%d is %s", n, describe(n))
}
}
0 is zero
1 is composite
2 is prime
3 is prime
4 is composite
5 is prime
6 is composite
7 is prime
8 is composite
9 is composite
10 is composite
11 is prime
Consider is_prime a format you'll use often: an early return false for the easy failing cases, then a loop that only needs to find one counterexample to disprove primality, falling through to return true if it never does. And describe is the bare switch from §2.6 doing real work: each case is a condition, checked top to bottom, first match wins, including calling another procedure (is_prime) right inside a case.
Checkpoint
odin run numbers.odin -file prints twelve lines classifying 0 through 11. You've now written and run variables, constants, all four shapes of for, switch, and a two-procedure program with a multi-branch switch doing the real work. And this is only Chapter Two!
Exercises
- Add a case to
describefor negative even numbers, printed as"negative even"instead of plain"negative". (Solution sketch: acaseis just a boolean condition here, addcase n < 0 && n % 2 == 0:above the existingcase n < 0:, since the first matching case wins.) - Rewrite the range-based loop in §2.8 as a three-part
forloop instead, and confirm the output is identical. Which version reads more clearly to you, and why? is_primeloopsfor i := 2; i*i <= n; i += 1. Change the condition toi < ninstead (check every number belown, not just up to its square root) and confirm the output doesn't change. Now time both versions onn = 10_000_019(a prime) using your shell'stimecommand. What do you observe, and why does checking only up toi*i <= nstill catch every possible factor?- Write a
fizzbuzzprocedure: fornfrom 1 to 30, print"Fizz"ifnis divisible by 3,"Buzz"if divisible by 5,"FizzBuzz"if both, and the number itself otherwise. Use a bareswitchlikedescribe's. (Solution sketch: order matters again, check "both" before either single case, or usecase n % 15 == 0:first.) - What happens if you remove the
case:(default) arm fromdescribeentirely, leaving only the three specific cases? (Solution sketch: try it: the compiler refuses to build at all, with "Missing return statement at the end of the procedure".describepromises to return astringno matter what, and without a default case the compiler can't prove every path does; it won't wait to find out at runtime whether some input falls through the gap.)
Further reading
- Odin language overview: https://odin-lang.org/docs/overview/ (see the sections on variables, constants,
for,switchand procedures) - Odin FAQ: https://odin-lang.org/docs/faq/