Cone Language Reference

Blocks and conditionals: if and match

On this page

What day of the week did Apollo 11 land on the Moon? There is a formula that turns any date into its weekday with nothing but whole-number arithmetic, and working it out takes a few scratch values along the way. That is a job for a block. By the end of this page the same program names the day, for any date, using if and match.

Blocks#

mod moonday;

import stdio use *;

fn main() {
  // Apollo 11 landed on the Moon on 20 July 1969
  let day = 20;
  let month = 7;
  let year = 1969;

  // A block's value is the value of its last statement.
  // Its scratch variables, century and inCentury, exist only inside it.
  let weekday = {
    let century = year / 100;
    let inCentury = year % 100;
    (day + 13 * (month + 1) / 5 + inCentury + inCentury / 4
      + century / 4 + 5 * century) % 7;
  };
  print <- "Weekday number ", weekday, ", counting Saturday as 0\n";
}

It prints:

Weekday number 1, counting Saturday as 0

Braces group statements into a block, and the statements run in order. A block is also a value: the value of its last statement. So a calculation that needs a few steps can be written right where its answer is wanted, and weekday is given that answer.

A variable declared inside a block belongs to it. century and inCentury exist only between its braces, and nothing outside can use them by mistake. A block can see the variables around it, as this one uses year and day, but not the other way round. When a block ends, its variables go, and anything they own is released then.

Day 1 is a Sunday. But the program can only say "1", and the formula has a catch: it is right only from March to December.

Choosing with if#

The formula counts January and February as months 13 and 14 of the year before. An if chooses between two blocks, and since a block is a value, so is the if: it gives the value of whichever block ran. Put these two lines in place of month and year in the formula, and it works for every date:

  let m = if month < 3 { month + 12; } else { month; };
  let y = if month < 3 { year - 1; } else { year; };

An if used as a value needs its else, since every path must give a value, and both blocks give the same type. Used as a plain statement, run for what it does, the else is optional, and elif ("else if") tests further conditions in turn. else if, written as two words, means the same:

if day == 1 and month == 1 {
  print <- "Happy new year!\n";
} elif day == 25 and month == 12 {
  print <- "Merry Christmas!\n";
}

Conditions are written without parentheses around them. They compare with ==, !=, <, <=, > and >=, and combine with the words and, or and not, which stop as soon as the answer is known. not (also written !) binds tightest, then and, then or, and all three come after the comparisons, so a < 3 or not done and b == 0 needs no parentheses. A condition is a value of type bool, true or false, and can be kept in a variable: let leap = year % 4 == 0;.

A comparison takes two values of the same type: 2 <= 2.0 is refused, since an i32 is not an f32, until one of them is converted (Numbers). A number may also stand alone as a condition, true when it is not zero, so if count { ... } means if count != 0 { ... } (Coercion to bool). The types you make yourself are compared once they declare == (Method patterns, in Recipes).

Choosing with match#

Now to turn the number into a name. A chain of seven elifs would work, but match names the value once and lists the cases, like a table. Here is the whole program, trying it on the first day of 2000, the date so many feared computers would get wrong:

mod weekday;

import stdio use *;

fn main() {
  let day = 1;
  let month = 1;
  let year = 2000;

  // January and February count as months 13 and 14 of the year before
  let m = if month < 3 { month + 12; } else { month; };
  let y = if month < 3 { year - 1; } else { year; };

  let weekday = {
    let century = y / 100;
    let inCentury = y % 100;
    (day + 13 * (m + 1) / 5 + inCentury + inCentury / 4
      + century / 4 + 5 * century) % 7;
  };

  // A match gives the value of the first case that fits
  let name = match weekday {
    case 0 { "Saturday"; }
    case 1 { "Sunday"; }
    case 2 { "Monday"; }
    case 3 { "Tuesday"; }
    case 4 { "Wednesday"; }
    case 5 { "Thursday"; }
    case 6 { "Friday"; }
    else { "a day that cannot be"; }   // a number could be anything
  };
  print <- day, "/", month, "/", year, " was a ", name, "\n";
}

It prints:

1/1/2000 was a Saturday

Try this: put in your own birthday, or 20 July 1969 again, now that it can say "Sunday".

The cases are tried from the top, and the block of the first one that fits runs; its value is the match's value. A match must say what happens to every value it can be given. A number could be anything, so a match on one ends with else, for whatever is left; leave it out and the compiler stops you.

A case can do more than name one value. It can join several with or, take a range, where 2 .. 6 runs through 6 and 2 ..< 6 stops before it, or add a further condition with if, called a guard:

let kind = match weekday {
  case 0 or 1 { "the weekend"; }
  case 6 if day == 13 { "Friday the 13th"; }
  case 2 .. 6 { "a working day"; }
  else { "a day that cannot be"; }
};

Order matters, because the first case that fits wins: a Friday the 13th never reaches the working days.

A pattern is a value to compare with, and the value can be more than a literal: a named constant, case LIMIT, or any expression written after ==, case ==limit * 2. A comparison leaves out the matched value, as case >3; a negative number after < takes a space, case < -1, since <- is the send operator. A whole condition is not a pattern, and the compiler says so:

case n > 3 { ... }     // Error 1117: A condition is not a pattern: a value alone is compared with the
                       // matched value. Write the comparison with the matched value left out, as '>3',
                       // or put the condition in an 'if' guard.

Text matches text, so case "ball" or "bat" holds when the text is exactly one of those, and anything that can be ordered matches a range, characters among them: case 'a' .. 'z'. A case joins several patterns with or.

The compiler checks that a match says what happens to every value. It can count only an enum's variants and a bool's two values, so a match on a bool with case true and case false needs no else, while a match on a number, a character or text always ends with one. Where no other value can happen, else {} says so. A case with a guard may fail, so it never counts toward covering its value. The opposite mistake is caught too: a case that the cases above it already match, or an else after cases that cover everything, can never run, and is refused (Error 1418). Enums shows covering at its most useful, where every variant must have its case.

Two values at once#

A match can look at several values at once, separated by commas, with one pattern for each in every case. _ takes any value. A time such as 1745 splits into its hour and its minute:

match t / 100, t % 100 {
  case 0, 0 { print <- "midnight"; }
  case 12, 0 { print <- "noon"; }
  case _, 0 { print <- t / 100, " o'clock"; }
  case _, < 30 { print <- "just after ", t / 100; }
  else { print <- "nearly ", t / 100 + 1; }
}

It reads like a small decision table, which is exactly what it is. A case can also compare, as < 30 does here.

The values after match are worked out once each, left to right, before any case is tried; no tuple is made, the commas are all it takes. _ belongs to this form alone, and a column's pattern does not bind a variable. Where every column is an enum or a bool, the compiler counts the combinations, and a match that has a case for each needs no else.

Patterns of your own: using#

A literal, a range and a comparison cover most cases, but not a test such as "is it odd?" or "does the line start with GET?". For those, any ordinary function can be a pattern, with nothing to declare. using calls it with the matched value as its first argument, followed by the arguments written. What the function gives back says what the case does: a bool only tests, and an Option tests and binds, the case being taken when it gives Some, and the name before using getting what is inside. Two things here arrive ahead of their own pages: an Option is a value that may hold something (Some) or nothing (None), which Dict teaches properly, and &str is a view of a piece of text, as String explains:

mod router;

import stdio use *;

// The text after a prefix, if the line starts with it
fn prefix(line &str, p &str) Option[&str] {
  if line.startsWith(p) { Some(&line[p.len() ..]); } else { None[&str]; }
}

fn isOdd(n i32) bool { n % 2 == 1; }

fn between(n i32, lo i32, hi i32) bool { n >= lo and n <= hi; }

fn route(line &str) {
  match line {
    case let path using prefix("GET ") { print <- "fetch ", path, "\n"; }   // prefix(line, "GET ")
    case let path using prefix("PUT ") { print <- "store ", path, "\n"; }
    case "HEAD" { print <- "headers only\n"; }
    else { print <- "not understood\n"; }
  }
}

fn kind(n i32) &str {
  match n {
    case 0 { "zero"; }
    case using isOdd() { "odd"; }             // isOdd(n)
    case using between(10, 20) { "teen"; }    // between(n, 10, 20)
    else { "other"; }
  }
}

fn main() {
  route("GET /index.html");
  route("PUT /notes.txt");
  route("DELETE /");
  print <- kind(7), " ", kind(12), " ", kind(40), "\n";
}

It prints:

fetch /index.html
store /notes.txt
not understood
odd teen other

Cases are still tried from the top, and a function runs only when its case is reached, at most once. Which values a function matches only running it can say, so a match with one ends with else. A pattern never consumes anything: prefix hands back what is left of the line, and the case carries on with that. Patterns of your own, in Recipes, has the full rules (what a binding gets, joining and guards, the same patterns in an if and a declaration, a parser written this way), and how a match is compiled to a jump table.

You can now group work into blocks, and choose between them, with conditions, cases and patterns of your own. Next, loops: running a block again and again, which is how one short program can close in on the digits of pi.