An Introduction to Programming

A gentle, from-scratch look at the ideas behind the code.

Programming is the act of telling a computer exactly what to do, one step at a time. A computer is astonishingly fast and completely literal: it follows your instructions to the letter, which means it does exactly what you say and not what you meant. So learning to program is really learning to think clearly enough to say what you mean — a skill worth having whether or not you ever write code for a living.

This guide is for someone who has never programmed before. It goes slowly, and it explains the ideas — what a variable is, what a loop is, why any of it is shaped the way it is — instead of assuming you already know them. The examples are written in a small language called lux, chosen because it keeps things simple, but the ideas are universal, and the last few sections show how they carry into other languages.

It also has a companion. lux comes with a compact tutorial you read right in your terminal by typing lux learn. Those cards teach the language quickly and are what you will come back to for reference. This page is their slower half: if one of the cards uses a word or an idea you have not met yet — value, type, loop — this is where it is explained from the ground up. The two cover the same topics in the same order, so you can read them side by side.

There is nothing to memorize and no rush. Read from the top; every example shows its result right here, so you can follow along without installing a thing. And if you would like to run the examples yourself, the final section shows you how.

Your first program

A program is nothing more than a list of instructions for the computer to carry out, one after another, from the top of the file to the bottom. The computer does exactly what the instructions say, in exactly the order you wrote them — no more, no less. It will not skip ahead, and it will not fill in what you forgot. Most of learning to program is learning to write those instructions clearly enough that a machine, which cannot guess what you had in mind, does what you actually wanted.

The simplest useful instruction is to show something on the screen. In lux that instruction is print, and you give it something to show inside parentheses:

print("Hello, world!")

The words inside the quotation marks are the text to display; the quotation marks themselves just mark where the text starts and stops. When the computer runs this line, it prints:

Hello, world!

A program is usually many such instructions, and they run in order, top to bottom. You can also leave notes for yourself with a comment — anything after two slashes is ignored by the computer and is there purely for the humans reading the code:

print("Hello, world!")
print("Nice to meet you.")   // this is a comment; the computer skips it

Running that prints two lines, in the order they were written:

Hello, world!
Nice to meet you.

That is the whole shape of a program: instructions, in order, each doing one small thing. Everything else in this guide is about what those instructions can be and how to combine them.

The matching card in your terminal: lux learn hello

Statements and syntax

Two quick words for what you have been writing. Each instruction in a program — each line so far, like print("Hello, world!") — is called a statement. A program is simply a sequence of statements, run one at a time, top to bottom.

And the rules for how those statements must be written — where the quotation marks go, that every opening parenthesis needs a closing one, that a comment starts with two slashes — are the language's syntax. Syntax is to a programming language what grammar and spelling are to English: a fixed set of rules the computer cannot bend. Leave off a closing quote and it will not guess what you meant — it stops and says so, which is what a syntax error is. lux's syntax is small and regular on purpose, and you will absorb it by reading and writing, not by memorizing a list.

When something breaks

Here is something no one tells beginners plainly enough: most of programming is fixing things that do not work yet. Everyone who writes code — at every level of experience — writes code that fails, reads what went wrong, and tries again. It is not a sign that you are bad at this. It is the ordinary rhythm of the work. So it is worth meeting failure early, while the stakes are nothing but a printed line, and learning that it is completely normal.

When the computer cannot do what a line asks, it does not guess, and it does not fail in silence. It stops and tells you what confused it and where. Suppose you ask it to print something called favColor that you never created:

print(favColor)

lux answers with a message like this:

error: `favColor` is not defined
  --> hello.lux:1:7
 1 | print(favColor)
           ^^^^^^^^
note: declare it with let or var before using it
help: `lux learn scope` — a name lives only inside the { } where it's made

It reads like a little report, and it is worth reading slowly, from the top. The first line says what went wrong: there is no favColor. The arrow line points at the exact file, line, and column, and the small carets underneath underline the trouble spot. The note suggests a fix. The help line even points you to the idea behind the mistake — the same topics this page and lux learn explain — for when you would rather understand it than just patch it. An error is not the computer scolding you; it is the most helpful thing it can offer when it is stuck.

So do not let an error message rattle you. You will read a great many of them, and before long you will glance at one the way you read a road sign — a quick look, an easy fix, and on you go. Nothing is broken for good, no one is watching, and you can always change a line and run it again. Getting comfortable reading these, more than any clever trick, is what turns programming from frustrating into fun.

The matching card in your terminal: lux learn errors

Variables

Almost every program needs to hold on to a piece of information and use it later — someone's name, a running score, the price of an item. A variable is how you do that. You take a value and give it a name, and from then on you can refer to that value by its name.

The clearest way to picture a variable is as a labeled box: the label written on the outside is the name, and whatever you place inside is the value. To create one in lux, you write a line like this:

let favFruit = "apple"

There are three parts. On the right is the value, "apple" — a piece of text. On the left is the name you chose, favFruit. And the = in the middle is the key to it: despite looking like the equals sign from arithmetic, here it is an instruction, and it means put the value on the right into the box on the left.

Once the value has a name, the name stands in for it everywhere you use it. Ask the computer to print favFruit and it prints whatever is in the box:

let favFruit = "apple"
print("My favorite fruit is", favFruit)

which prints:

My favorite fruit is apple

Why give a value a name at all? Two reasons. The first is reuse: you can refer to the same value in many places without writing it out each time, and if it ever needs to change, you change it in one spot. The second is meaning. The name is a note to anyone reading the program — favFruit tells you what the value stands for, in a way that a bare "apple" sitting loose in your code never could.

There is one more distinction lux asks you to make. Some values are set once and should never change; others are meant to change as the program runs. lux uses two different words for them: let for a name that stays put, and var for one that can be updated later.

let name = "Ada"     // set once; this never changes
var score = 0        // this will change as the program runs
score = score + 10   // now the box named score holds 10
print(name, "has", score, "points")

Running it prints:

Ada has 10 points

That line score = score + 10 can look strange the first time, because the name appears on both sides. Remember that = means "store into," not "is equal to." So the computer first works out the right-hand side — take what is in score right now (0) and add 10 — and then stores the result (10) back into the box. Reach for let by default: a value that cannot change is one less thing to keep track of. Use var only when something genuinely needs to move.

The matching card in your terminal: lux learn variables

Keywords and names

You just did something worth pausing on: in let favFruit = "apple", you invented the word favFruit. Telling the words you invent apart from the ones built into the language is one of the first things that trips people up, so let us make it plain.

Every line of code is built from two kinds of words. Some belong to lux itself. let is one: it always means "make a name that will not change," it is always spelled exactly let, and you can never use it for anything else. Words like this are called keywords, and there are only a couple dozen of them. You have already met let and var; ahead are func, if, else, while, for, struct, enum, match, and return. That short, fixed list is the entire vocabulary of the language itself.

The other words are the ones you make up — the names you give your own things. favFruit is a name; so is every variable, every function, and every struct and its fields. lux does not care what you call them. You could just as well have written fruit, or snack, or x, as long as you use the same name each time you mean that value:

let snack = "apple"
print(snack)        // apple — the name is yours to choose; this works just the same

A name has to be a single unbroken word, with no spaces, and it cannot be one of the keywords — you cannot name a variable let, because lux already owns that word. (Misspell a keyword and lux simply will not recognize it, which is a common early slip and an easy one to spot once you know to look for it.) Past those rules, the craft is to choose names that say what the thing is: favFruit tells a reader far more than x ever could.

One thing you might wonder: a word like print is a name too, not a keyword — it is simply a name that lux has already defined for you, ready to use, and you will meet more such built-in names as you go. So from here on you can sort every word in a program into one of two piles: a keyword, part of lux and never changing, or a name, chosen by someone and free to be anything. It is a small habit, and it takes a lot of the mystery out of unfamiliar code.

Numbers

A great deal of programming is arithmetic, and this is the place to meet an idea that runs through everything: every value has a type — a kind. The type is what the value is, and it decides what you can do with it. Numbers, in lux, come in two types. A whole number, like 3 or -8, is called an int (short for integer). A number with a decimal point, like 3.5 or 0.1, is called a float.

You can do the usual arithmetic with +, -, * for multiply, and / for divide:

print(2 + 3)     // 5
print(10 - 4)    // 6
print(6 * 7)     // 42

Division holds a small surprise. When you divide two whole numbers, the answer stays a whole number, and any remainder is simply dropped:

print(7 / 2)     // 3, not 3.5 — the leftover half is thrown away
print(7 % 2)     // 1 — the % sign gives you that leftover, the remainder

That is not a bug; it is the type being honest. Two whole numbers go in, so a whole number comes out. If you want the fractional answer, use floats, which keep the decimal part:

print(7.0 / 2.0)   // 3.5

This split between the two types has a consequence worth understanding, and it is the first place a type will actually stop you. An int and a float are genuinely different kinds of number: one counts in exact whole steps, the other measures along a continuous scale with a fractional part. Because they are different kinds, lux refuses to quietly mix them. Add the int 7 to the float 2.5 directly, and it halts rather than guess which kind of answer you meant:

print(7 + 2.5)
error: cannot mix int and float — convert one first
  --> numbers.lux:1:7
 1 | print(7 + 2.5)
           ^^^^^^^
note: wrap a value in float(...) or int(...)
help: `lux learn numbers` — there's a reason lux makes you say when a whole number becomes a fraction

The fix is right there in the error: to combine them, first turn one into the other. Changing a value from one type into another is called converting it, or casting, and in lux you cast a value by handing it to a small tool named after the type you want. float(7) takes the whole number 7 and gives back the float 7.0, which then adds happily to another float:

print(float(7) + 2.5)   // 9.5 — both sides are floats now

That float(...) is worth a second glance, because it is your first look at a function: something you hand a value to and get a value back. There is a matching int(...) that casts the other way, dropping the fraction — int(3.9) is 3. Both functions and casting get a fuller section of their own later; for now the useful habit is just this — when lux tells you two kinds of number will not mix, convert one of them on purpose.

The matching card in your terminal: lux learn numbers

Strings

Not every value is a number. A piece of text — a word, a name, a whole sentence — is a value too, and its type is called a string (as in a string of characters, one after another). You have already been writing them: the quotation marks are what mark a value as text, telling the computer where it begins and ends.

You can join two strings together with +. Joining text this way is called concatenation:

let name = "Ada"
print("Hello, " + name + "!")    // Hello, Ada!

There is a catch, and it comes straight from the idea of types. Both sides of that + must already be strings. A number is not text, so you cannot glue a number directly onto a string. To put a number inside some text, you first convert it to a string with string(...):

let count = 3
print("You have " + string(count) + " messages")   // You have 3 messages

This feels strict at first, but it is the same honesty as the numbers rule: lux never silently turns one type into another behind your back. When you want a number to become text, you say so, and the code stays clear about what is happening. You can also ask a string how long it is with length, which counts its characters.

The matching card in your terminal: lux learn strings

Asking a question

Every program so far has talked at you — it prints, and you read. But the programs worth writing usually listen, too: they ask a question and do something with the answer. The instruction for that is input. You hand it a prompt to show, and it hands back whatever the person types, as a string:

let name = input("What is your name? ")
print("Hello, " + name + "!")

Run it and the program prints the question, then waits with the cursor sitting right after it. Type a name, press enter, and it carries on:

What is your name? Ada
Hello, Ada!

That is the whole idea. input pauses, lets a person type one line, and gives that line back to your program as a value — here caught in the variable name, then used like any other string. The prompt is optional: write input() with nothing between the parentheses and it simply waits for a line without asking anything first.

One detail matters, and it comes straight from types. What input gives back is always a string, even when the person types digits. Ask someone's age and let them type 30, and you get the text "30" back, not the number 30 — and text and numbers, as you have seen, do not mix. Turning typed text into a number you can actually do arithmetic with is a small step of its own, and it has a section later.

There is also a more careful, lower-level way to read input — one that can tell an empty line apart from no more input at all. You do not need it yet; input is the friendly front door, and it is plenty for now. When you reach The outside world, you will meet what it was quietly handling for you.

The matching card in your terminal: lux learn input

True and false

Some questions have only two possible answers: yes or no, on or off, true or false. A value that is only ever true or false has its own type, called a boolean (named after the mathematician George Boole). It is the smallest type there is, and the most important one for making a program decide anything, because every decision a program makes comes down to a true or a false.

You store a boolean in a variable just as you store a number or a string. And because a boolean answers a yes-or-no question, it is customary to name it like one — isRaining, isCold, hasPermission — so that later on, code like if isCold reads almost as plain English.

More often than not, though, you do not spell out true or false yourself — you produce a boolean by comparing two things. The comparisons are the ones you would expect — > greater than, < less than, >= and <= for "or equal," == for "is equal to," and != for "is not equal to":

print(3 > 2)      // true
print(5 == 4)     // false

And you can combine booleans with three little words: && means "and" (both must be true), || means "or" (either one is enough), and ! means "not" (it flips true and false):

let isRaining = true
let isCold = false
print(isRaining && isCold)   // false — it is not both raining and cold
print(isRaining || isCold)   // true  — it is at least one of them
print(!isRaining)            // false — "not raining" is false, since it is raining

Hold on to this idea, because the next section runs on it: a boolean is exactly what a decision looks at to choose its path.

The matching card in your terminal: lux learn booleans

Making decisions

Up to now our programs have run straight through, top to bottom, doing every line. But real programs need to choose — to do one thing in some situations and another thing in others. That is what if is for. It looks at a condition (a true or false, the boolean from the last section) and runs a block of instructions only when the condition is true.

let temperature = 45
if temperature < 60 {
    print("Bring a jacket.")
}

The condition is temperature < 60. Since 45 < 60 is true, the block runs and the program advises a jacket. The curly braces { } mark off the block — the group of instructions that belongs to this if — so you always know exactly what is and is not governed by the condition.

That condition is nothing new: temperature < 60 is just a boolean, the true-or-false value from a few sections back. An if always tests a boolean, and nothing else. Sometimes it helps to make that plain by lifting the condition out into a named variable, which can let the if read almost like a sentence:

let temperature = 45
let cold = temperature < 60
if cold {
    print("Bring a jacket.")
}

Here cold holds true, so the block runs, exactly as before. Whether you write the comparison directly inside the if or give it a name first, the if is doing the very same thing: looking at a boolean and choosing its path accordingly.

Often you want an alternative for when the condition is false. That is else: the computer takes one path or the other, never both.

let temperature = 72
if temperature < 60 {
    print("Bring a jacket.")
} else {
    print("You'll be fine.")
}

And when there are several possibilities, you can chain them with else if, and the computer walks down the ladder, taking the first branch whose condition is true:

let score = 75
if score >= 90 {
    print("A")
} else if score >= 60 {
    print("passing")
} else {
    print("try again")
}

This shape — test a condition, run a block, maybe run another instead — is one of the two great powers a program has. The other is repetition, which is next.

The matching card in your terminal: lux learn if

Repeating: the while loop

Computers are tireless in a way people are not: they will do the same thing ten times, or ten million times, without complaint or mistakes. To ask for that repetition, you write a loop. The simplest kind is the while loop, and it is a close cousin of the if you just met. Where an if runs its block once if a condition is true, a while runs its block over and over, as long as the condition stays true.

var n = 1
while n <= 3 {
    print("Step", n)
    n = n + 1
}

Running it prints:

Step 1
Step 2
Step 3

Follow it around the loop. The condition n <= 3 is checked first; while it holds, the block runs, printing the step and then increasing n by one. After n becomes 4 the condition is false, and the loop ends. That last detail — n = n + 1 — is the important one. Something inside the loop must make progress toward the condition becoming false, or the loop would run forever. A loop that never ends is one of the first mistakes every programmer makes, and now you know what to look for.

The matching card in your terminal: lux learn while

Arrays

So far each name has held a single value. But suppose you want to keep three test scores. You could make three separate variables — score1, score2, score3 — yet that turns clumsy fast, and it falls apart entirely once you do not know ahead of time how many scores there will be. An array is the answer: it holds many values of the same type in a single variable, kept in order. You write one with square brackets:

let scores = [90, 85, 100]

You reach an individual value by its position, which is called its index. Here is the part that surprises everyone at first: positions are counted starting from zero, not one. So the first value is at index 0, the second at index 1, and so on:

let scores = [90, 85, 100]
print(scores[0])   // 90  — the first value
print(scores[2])   // 100 — the third value
print(length(scores))   // 3 — how many values are in the array

Counting from zero looks odd until you get used to it, and then it feels natural; nearly every programming language does it this way. If you ask for a position that does not exist — scores[10] in an array of three — lux stops you rather than handing back nonsense. An array is the first tool you have for handling "many things at once," and it pairs naturally with a loop, which is exactly where we go next.

The matching card in your terminal: lux learn arrays

Repeating: the for loop

Now we can put the last two ideas together. Suppose you have an array and you want to do something with every value in it — print each score, say. You already have the tools: a while loop to repeat, and an index to reach each position. Start a counter at 0, keep going while it is still a valid position, and step it forward each time around:

let scores = [90, 85, 100]
var i = 0
while i < length(scores) {
    print(scores[i])
    i = i + 1
}

Read what that does. The counter i starts at the first position, 0. The loop runs while i is still less than the length, so it visits every position in turn. Each time around it prints the value at position i, then moves i forward by one. It prints all three scores, in order.

This pattern — start a counter, loop while it is in range, move it along each time — is so common, and so easy to get subtly wrong, that languages give you a shortcut for it: the for loop. Here is the exact same job written as a for loop:

let scores = [90, 85, 100]
for score in scores {
    print(score)
}

Read for score in scores as "for each score in scores." The loop walks through the array from start to finish and, each time around, hands you the next value under the name score — no counter to set up, no length to check, no chance of stepping off the end. It does precisely what the while loop above did; it simply keeps the bookkeeping out of your way. That is all a for loop is: a while loop tidied up for the common case of walking through a collection.

When you just want to count rather than walk an array, you can loop over a range of numbers. 0..3 means the numbers 0, 1, 2 — it starts at 0 and stops just before 3:

for i in 0..3 {
    print(i)     // 0, then 1, then 2
}

The matching card in your terminal: lux learn for

Functions

As a program grows, you find yourself doing the same piece of work in several places. Rather than copy those instructions each time, you can bundle them up, give the bundle a name, and use it by name whenever you need it. That bundle is a function — think of it as teaching the computer a new verb of your own.

In fact, you have been calling functions since your very first program. print is one; so are length and string — they simply come built into lux. What is new here is writing functions of your own.

There are two separate acts to keep straight: defining a function, which means writing it down once, and calling it, which means putting it to use. They happen at different moments, and it helps to picture them apart.

Defining a function is like writing down a recipe. You give it a name, say what it takes in and what it gives back, and write the steps in between. Here is one that takes a name and returns a greeting:

func greet(name: string) -> string {
    return "Hello, " + name + "!"
}

Read that first line — the function's signature — as a small contract. func begins a definition; greet is the name you chose; (name: string) says the function takes one input, a string, which the steps inside will refer to as name; and -> string promises that it hands back a string. That input named in the definition — name — is called a parameter: a placeholder that stands in for whatever value gets supplied later. The return line is the answer the function gives back.

Writing the definition does not run anything. The recipe just sits there, ready. Something happens only when you call the function — use its name and hand it a real value to work on:

print(greet("Ada"))     // Hello, Ada!

That is a call. greet("Ada") runs the function with "Ada" filling in for the parameter name, and the whole greet("Ada") becomes the value the function hands back — the finished greeting — which print then shows. The real value you pass in a call, "Ada" here, is called an argument. A simple way to keep the pair straight: the parameter is the placeholder in the definition, and the argument is the actual thing you hand over when you call.

The reason you define a function once is so you can call it as often as you like, with a different argument each time:

print(greet("Ada"))     // Hello, Ada!
print(greet("Grace"))   // Hello, Grace!
print(greet("Alan"))    // Hello, Alan!

That reuse is the first reason functions matter. The second is subtler and just as valuable: once greet exists, you can call it knowing only what it does, without holding in your head how it does it. A well-named function is a box you can trust and stop thinking about, and that is how programs stay manageable as they grow — you build bigger ideas out of smaller ones you have already solved and set aside.

Two quick variations round out the picture. A function can take more than one input — list the parameters with commas, and supply an argument for each, in order — and a function need not return anything at all, in which case you leave off the -> and the return type:

func add(a: int, b: int) -> int {
    return a + b
}
print(add(2, 3))    // 5

func announce(message: string) {   // no arrow, no return type: it just does something
    print(message)
}
announce("Dinner is ready!")

Finally, a function can even call itself — a technique called recursion, suited to a problem that breaks into a smaller version of itself. A factorial is the classic example (factorial(5) is 5 × 4 × 3 × 2 × 1): it returns 1 for the smallest case, and otherwise multiplies n by the factorial of the next smaller number, calling itself down until it reaches the bottom. Recursion takes a while to feel natural, so do not worry if it does not click today.

func factorial(n: int) -> int {
    if n <= 1 {
        return 1
    }
    return n * factorial(n - 1)
}
print(factorial(5))    // 120

The matching card in your terminal: lux learn functions

Scope

When you create a name inside a block — the { } of a function, an if, or a loop — that name lives only inside that block. Step outside it, and the name is gone, as if it never existed. The region of a program where a name is available is called its scope.

func double(n: int) -> int {
    let result = n * 2      // result exists only inside double
    return result
}
print(double(4))            // 8

Here result is created inside double, so it belongs to double alone. If you tried to print result from outside the function, lux would tell you it is not defined — the same "not defined" error from earlier, and now you know why it happens.

This might sound like a limitation, but it is a gift. Because the names inside a function cannot leak out and collide with names elsewhere, you can write and understand one small piece at a time without worrying about the rest of the program reaching in. Every function gets its own clean, private workspace. Scope is what keeps a large program from becoming a tangle where everything can touch everything else.

The matching card in your terminal: lux learn scope

Structs

You now have the everyday tools of programming — values, decisions, loops, and functions. The next few sections put them to work building something new: your own kinds of value. This first kind is also the most straightforward, a good place to start.

An array is a good way to hold many values of the same kind. But often what you want is to bundle a few different things that belong together into one value — a person has a name and an age; a book has a title, an author, and a page count. A struct lets you define your own kind of value, made of named parts called fields. A value like this is often called a record, because it records several facts about one thing.

You define a struct once, listing its fields and the type of each. Here is a Person, with a name (a string) and an age (an int):

struct Person {
    name: string
    age: int
}

Look at what those two fields are: a piece of text and a number, two different types, sitting together in a single value. That is exactly the thing an array cannot do, and it is the whole reason structs exist. Defining the struct does not create a person yet; it describes what any Person value will look like. To make an actual one, you fill in the fields, and you read a field back with a dot:

let ada = Person(name: "Ada", age: 36)
print(ada.name)    // Ada
print(ada.age)     // 36

Because Person is now a type of its own, you can make as many of them as you like, each carrying its own values:

let ada = Person(name: "Ada", age: 36)
let grace = Person(name: "Grace", age: 45)
print(ada.name, "and", grace.name)   // Ada and Grace

And because a struct is just a value, it travels through your program like any other. You can hand a whole Person to a function instead of passing its parts around one by one:

func describe(p: Person) -> string {
    return p.name + " is " + string(p.age) + " years old"
}
print(describe(ada))     // Ada is 36 years old

That is what makes structs so useful. Instead of juggling a loose name here and a loose age there, hoping you always keep the right ones together, you have a single Person that holds everything about one person as a unit — and code that reads the way you actually think about the thing itself.

The matching card in your terminal: lux learn structs

Enums

A word before this stretch. The next three ideas — enums, then match, then missing values — are best taken as a set, because they are really one idea arriving in three steps. An enum lets a value be exactly one of several shapes; match is how you take such a value back apart; and the two together are how lux handles a value that might not be there at all, designing out a whole category of crash. They run a little more advanced than the everyday tools, and more distinctly lux, so take them one at a time — and if a piece does not land on its own, keep going. It is built to come together by the third.

Sometimes a value should be exactly one choice out of a fixed, known set — nothing more and nothing less. The direction you are facing is north, south, east, or west. A traffic light is red, yellow, or green. A task is waiting, finished, or failed. An enum — short for enumeration, a listing-out of the options — lets you define such a set of named possibilities, each one called a case, so that the value can only ever be one of them:

enum Light {
    red
    yellow
    green
}
let signal = Light.green

Where a struct says a value is "this and that" — a point is its x and its y — an enum says a value is "this or that": a light is red or yellow or green. That is a powerful thing to be able to state, because it makes impossible values impossible to write: there is simply no way to make a Light that is "purple," and so no code down the line ever has to worry about that case.

You might reasonably ask why not just use a plain string like "green", or a number like 2, to stand for the light. You can — but then nothing stops you from writing "gren" by mistake, or 7, which means nothing at all, and a later reader is left guessing what 2 was supposed to signify. An enum removes both problems at once: its cases are the only values allowed, they cannot be misspelled or invented, and each one says in plain words exactly what it means.

Each choice in an enum can also carry its own extra information. A shape might be a circle with a radius, or a rectangle with a width and a height:

enum Shape {
    circle(radius: float)
    rectangle(width: float, height: float)
}
let c = Shape.circle(radius: 2.0)

Now c is a shape that is specifically a circle, carrying its radius of 2.0 along with it. To do anything useful with such a value, you need a way to ask which case it is and pull out what it carries — which is the next idea.

The matching card in your terminal: lux learn enums

Match

When a program has to choose among many possibilities, a long chain of if and else if gets hard to read. Match is a cleaner way to say it: look at this value, and depending on what it is, do the matching thing. You give it a value and list the possibilities, mapping each one to a result with =>:

func dayName(n: int) -> string {
    return match n {
        1 => "Monday"
        2 => "Tuesday"
        3 => "Wednesday"
        _ => "some other day"
    }
}
print(dayName(2))    // Tuesday
print(dayName(9))    // some other day

Here is exactly what happens when it runs. lux takes the value once — here, n — and compares it against each case from the top down. The moment it finds one that fits, it runs that arm and no other, and matching stops there. So if n is 1, the first arm runs; if 2, the second. The last line, the underscore _, is a catch-all that fits anything you did not list — the way "none of the above" covers every remaining option — so dayName(9) lands there. You need it here because there is no end to the numbers someone might pass in.

There is a second thing match does that a chain of ifs does not, and it is the reason match turns up so often in lux: a match does not merely choose an action, it produces a value. Whichever arm fires, its result becomes the value of the whole match. That is why dayName can write return match n { ... } directly — the match evaluates to the matching day name, and that is what gets returned. You could just as easily capture it in a variable first:

let name = match n {
    1 => "Monday"
    2 => "Tuesday"
    _ => "some other day"
}

Either way, the match hands back a value, the same way 3 + 4 hands back 7. That is a genuinely bigger idea than choosing which lines to run, and it is worth letting sink in.

So far match has looked at plain values. Its real home, though, is the enums from the last section — and the simplest kind needs no new machinery at all. Take the traffic light again: each arm just names a case, and the matching one fires.

enum Light {
    red
    yellow
    green
}
func advice(signal: Light) -> string {
    return match signal {
        red    => "stop"
        yellow => "slow down"
        green  => "go"
    }
}
print(advice(Light.red))     // stop
print(advice(Light.green))   // go

Notice there is no _ here, and no need for one: a light is only ever red, yellow, or green, so the three arms already cover every possibility. Hold on to that — it is the point we come back to at the end.

The other thing match does with an enum is reach inside a case and pull out the data it carries. Here it works out the area of a shape, lifting the values out of whichever case it finds:

enum Shape {
    circle(radius: float)
    rectangle(width: float, height: float)
}
func area(s: Shape) -> float {
    return match s {
        circle(let r)           => 3.14159 * r * r
        rectangle(let w, let h) => w * h
    }
}
print(area(Shape.circle(radius: 2.0)))                 // 12.56636
print(area(Shape.rectangle(width: 3.0, height: 4.0)))  // 12.0

When the shape is a circle, the first arm runs, and circle(let r) lifts the radius out into a name r you can use right there on the same line. A rectangle carries two values, so its arm names two, w and h — match unpacks exactly as many as the case holds.

Match has one rule that turns out to be its greatest strength: for an enum, you must cover every case. This is why the traffic light needed no _: its three arms already were every case. But if you delete the rectangle arm from the area function, lux refuses to run the program at all, because there is now a shape it would not know how to handle. (And it is exactly why the number example did need its _ — a way to cover "everything else" when the possibilities have no end.) The language itself, not your memory, guarantees that nothing slips through unhandled — which is why the next two ideas, for missing values and for failures, lean on match so heavily.

The matching card in your terminal: lux learn match

Missing values

Sometimes the honest answer to a question is that there isn't one. Ask for the first even number in a list, and the list might hold none. Look up a customer by name, and there might be no such customer. The program still has to hand something back — but what, when there is genuinely nothing to give?

The tempting move is to hand back a stand-in: a 0 for the missing number, an empty string for the missing name. But a stand-in quietly lies. 0 is itself a perfectly good even number, so now no one can tell "the first even number is 0" apart from "there wasn't one." You could stop the whole program instead — but coming up empty is a normal, expected outcome here, not a breakdown. What you actually want is an answer with two honest shapes: one that says "here it is, and here is the value," and one that says "there is nothing here," which can never be mistaken for each other.

That answer is an Option. You write the kind of value it would hold in angle brackets, so an Option<int> is a maybe-there int: either some(n) — there is a number, and here it is — or none, nothing at all. The type itself now carries the warning that the value might be absent, right where anyone reading the code can see it.

You have met this shape already. An Option is just an enum — the same "a value is exactly one of a fixed set of cases" from two sections back — with two cases: some, which carries a value, and none, which carries nothing, the way red carried nothing. The only new thing is that lux writes this particular enum for you, because "a value that might be missing" turns up in nearly every program ever written. And because it is an ordinary enum, you already know how to open it — with match, which here forces you to handle the empty case:

func firstEven(numbers: [int]) -> Option<int> {
    for n in numbers {
        if n % 2 == 0 {
            return some(n)
        }
    }
    return none
}

match firstEven([1, 3, 4, 7]) {
    some(let n) => print("first even number:", n)     // first even number: 4
    none        => print("there are no even numbers")
}

The "nothing" case cannot sneak past you, because the program will not run until you have said what to do about it — no forgotten check, no stand-in value pretending to be real. And you got there without having to remember anything: the type asked, and match made you answer.

The matching card in your terminal: lux learn option

Converting values

You have already done a little of this — float(...) back in the numbers section, string(...) with text. Turning a value of one type into another comes up constantly: a decimal into a whole number, a number into text so you can print it, or text a person typed into a number you can calculate with. lux splits these into two honest kinds, and the difference is worth understanding.

Some conversions always work. Turning a float into an int just drops the fraction; turning a number into a string always produces some text. There is no way for these to fail, so they simply give you the answer:

print(int(3.9))     // 3 — the fraction is dropped
print(string(42))   // 42 — the number, now as text

But reading a number out of text is a different matter, because the text might not be a number at all. What whole number should "hello" become? There is no answer. So an operation like parseInt cannot promise to succeed — and instead of crashing when it fails, it hands back the Option you just met: some(n) if the text really was a number, none if it was not:

match parseInt("17") {
    some(let n) => print("the number is", n)     // the number is 17
    none        => print("that wasn't a number")
}

Keeping these two kinds apart — the conversion that cannot fail, and the reading that can — is how lux keeps a hidden failure from lurking inside something that looked perfectly safe.

The matching card in your terminal: lux learn conversions

When something can fail

Option handles a value that might be absent. Its close cousin handles an operation that might fail, and needs to say why. That type is Result, and it carries two kinds in its angle brackets: the type of the answer on success, and the type of the reason on failure. So a Result<int, string> is either ok(value) — it worked, and here is the int answer — or err(reason) — it failed, and here is the string reason. As with Option, you handle both sides with match, so a failure can never be quietly ignored:

func half(n: int) -> Result<int, string> {
    if n % 2 == 0 {
        return ok(n / 2)
    }
    return err("that number can't be halved evenly")
}

match half(10) {
    ok(let h)  => print("half is", h)        // half is 5
    err(let e) => print("sorry:", e)
}
match half(7) {
    ok(let h)  => print("half is", h)
    err(let e) => print("sorry:", e)         // sorry: that number can't be halved evenly
}

The difference from Option is just the reason that rides along with a failure. none says "nothing here"; err(...) says "this went wrong, and here is the explanation." Treating a failure as an ordinary value you inspect — rather than a surprise that halts your program — keeps the thing that can go wrong right beside the thing that can go right, where you can see it.

The matching card in your terminal: lux learn result

The outside world

Every program so far has kept to itself. But useful programs reach out to the world: they read files, take input from whoever is using them, and write results back out. Input and output — reading and writing — is often shortened to I/O.

The world outside your program is unreliable in a way your own variables are not. A file you ask for might not exist. Input you wait for might run out. So lux treats these operations with the same honesty you have already seen: anything that can fail hands its failure back as a value. Reading a file gives you a Result — the text if it worked, or a reason if it did not — and match makes you face both:

match readFile("poem.txt") {
    ok(let text) => print("the file has", length(text), "characters")
    err(let e)   => print("couldn't read it:", e)
}

This is the same Result from the last section, now earning its keep on a real task. That is the quiet reward of these ideas: once you have met Option and Result, the entire messy outside world is just those two shapes again. Reading a line of input, for instance, gives back an Option — a line, or none when there is no more input to read. This is the honest machinery beneath the input you met early on: input folds that none into an empty string, so you could ask a question long before you knew what an Option was, while readLine keeps the two apart for the times the difference matters — reading a file line by line until it runs out, say.

The matching card in your terminal: lux learn io

Running other programs

A program can do one more thing worth knowing about: it can run other programs — the same commands you might type into a terminal — and read back what they produce. This is how small tools get combined into bigger ones. The run command takes the name of a program and a list of arguments to give it, and hands back a Result. Here it runs echo, a tiny program that simply prints back whatever you give it:

match run("echo", ["hello"]) {
    ok(let result) => print("it said:", result.stdout)
    err(let e)     => print("couldn't start it:", e)
}

There are really two questions here, and lux keeps them separate. The Result answers the first: did the program even start? (Maybe it is not installed.) And within a successful launch, the output carries a status that answers the second: once it ran, did it actually succeed? A program can start perfectly and still report that its work failed. Keeping "did it start" and "did it succeed" apart is a small habit that saves a lot of confusion later.

The matching card in your terminal: lux learn shell

Putting it together: a small world

Every idea in this guide is a building block, and the point of building blocks is that they combine. To see them working together, run lux crawl and lux writes you a tiny text adventure you can play — and the secret of it is that the whole game world is a lux file you can open and change.

Nothing in it is beyond what you have now read. The rooms are an enum. Where you stand and what you carry is a struct. A function uses match to describe whichever room you are in:

enum Room {
    cell
    yard
}
func describe(room: Room) -> string {
    return match room {
        cell => "A cold stone cell, with a door to the east."
        yard => "Open sky at last."
    }
}
print(describe(Room.cell))

Add a room to the enum, and match will insist you describe it before the program will run — the safety net from earlier, quietly keeping your world consistent. That is the whole idea behind learning these pieces one at a time: put a handful of them together and you have something real, something yours, that you can read from top to bottom and change however you like.

The matching card in your terminal: lux learn crawl

Getting lux: try it yourself

You have read the ideas — and if you would like, you can now make them run. Every example on this page is real lux, and the surest way to make a concept stick is to type one out, run it, and change it to see what happens. Here is how to get set up.

On macOS or Linux, one line installs lux, with nothing else required:

curl -LsSf https://anderix.com/lux/install | sh

Run that same line again any time to update to the newest lux — and once it is installed, lux update does the same from the tool itself.

A lux program is just a text file whose name ends in .lux. Put any example from this page into a file — say hello.lux — and run it by typing:

$ lux run hello.lux

From there it is the loop you have watched all along: write a little, run it, read what comes back, change something, run it again. Typing lux learn opens the compact terminal companion to this page — the same topics in the same order, always a keystroke away. And a fine first move is lux crawl, the little world from just above: open its file and start changing rooms.

And if you ever want it gone, lux leaves as cleanly as it arrived — one line removes it: curl -LsSf https://anderix.com/lux/uninstall | sh.

The same ideas in other languages

Everything in this guide has been a general idea wearing lux's particular spelling. Naming a value, choosing with if, repeating with a loop, bundling work into a function — these are not lux inventions. They are the shared bones of nearly every programming language, and once you can see them here you can find them anywhere. Open a page titled "learn some-other-language in ten minutes" and most of it will be new punctuation for things you already understand.

To make that concrete, here is a handful of the ideas from this guide, side by side with three widely used languages — Rust, Swift, and Go. Notice how similar the shapes are; what changes is mostly spelling.

The idealuxRustSwiftGo
a name that can't changeletletletconst
a name that canvarlet mutvarvar
walk a collectionfor x in xsfor x in xsfor x in xsfor _, x := range xs
repeat while truewhile cwhile cwhile cfor c
a value that might be missingOptionOptionOptionalvalue, ok

That last row is worth a word, because out in other languages you will meet the idea it replaced. It is called null: a single value meaning "nothing here" that any value is quietly allowed to be, with no Option around it and no match to make you check. It sounds convenient, and it has caused so many crashes — code reaches for something real, finds the "nothing," and falls over — that the man who invented it calls it his billion-dollar mistake. Option is the modern answer, and the reason lux has no null at all: "missing" is a shape you have to open, never a trapdoor hidden inside an ordinary value. Having learned it this way first, you will recognize null on sight for what it is — and know exactly what it forgot to make you do.

lux was built to be a good first language and then to be outgrown. As you move toward a language like Rust or Swift, you will meet a few genuinely hard new ideas that lux leaves out on purpose — but you will meet them standing on everything here, which does not change. That is the whole plan: learn the ideas once, in the gentlest form, and carry them wherever you go next.

Beyond

Step back from the syntax and every section here was really the same handful of moves: break a big problem into smaller ones until each is small enough to solve, say exactly what you mean so the machine cannot mistake it, and when something breaks, read what it tells you and try the next thing. Those moves are not truly about programming. They are how you take anything tangled — a plan, an argument, a stubborn form, a broken bike — and make it give way. Programming is just an unusually honest place to practice them, because the program either runs or it does not, and it never pretends.

There is a quieter reward, too. Once you can program, even a little, you can build the small tool you wish existed instead of only using the ones handed to you. That is worth more than any single language, and it is why lux is built to be left behind. When it starts to feel small, that means it worked. Go pick a bigger language, and bring these ideas with you.

Read the whole thing in your terminal any time: lux learn