Cone Language Reference

Functions, closures and generators

On this page

A function gives a piece of work a name, so it is written once and used wherever it is needed. Here is pi, at last with a name of its own, worked out to as many terms as the caller asks for:

mod pi;

import stdio use *;

// calcpi takes the number of terms, an i32, and gives back an f64.
// '= 10' is a default: a call may leave the number of terms off.
fn calcpi(terms i32 = 10) f64 {
  var left = terms;
  var result = 0.5d;
  var seed = 1d;
  var top = 1d;
  var bot = 1d;
  var twos = 2d;
  while left > 0 {
    top *= seed;
    bot *= seed + 1d;
    twos *= 2d * 2d;
    result += top / (bot * (seed + 2d) * twos);
    seed += 2d;
    left -= 1;
  }
  result * 6d;    // the last expression is what the function gives back
}

fn main() {
  print <- "with 2 terms:  ", calcpi(2), "\n";
  print <- "with 10 terms: ", calcpi(), "\n";
  print <- "with 20 terms: ", calcpi(20), "\n";
}

It prints:

with 2 terms:  3.13906
with 10 terms: 3.14159
with 20 terms: 3.14159

Try this: call calcpi with 1, 3 and 5 terms, and watch the digits arrive.

A name for a piece of work#

A function's first line says everything a caller needs to know. calcpi takes one parameter, with a name and a type, and after the parentheses comes the type of what it gives back. Inside, the parameters are variables, set to the values the caller passed, in order. As two variables of one block may not share a name, neither may two parameters, self included: fn f(a i32, a i32) is an error. The types are written out on the parameters and the result, so a reader, and the compiler, can check every call without looking inside.

The function gives back the value of its last expression, as a block does. return is for leaving before the end. The arguments of a call are worked out in order, left to right, before the function runs, and its variables, parameters included, belong to that one call: two calls, even of the same function, never share them.

A parameter may have a default, as terms i32 = 10 does, and then a call may leave it off: calcpi() is calcpi(10). A default is a value fixed when the program is compiled: a literal, a named constant, an expression of them such as step * 2, or an enum's variant that holds nothing. Once one parameter has a default, every parameter after it needs one too, and a call may leave off any run of them from the end: for fn span(start i32 = 0, len i32 = 10), span(), span(5) and span(5, 2) all work.

A parameter cannot be given a new value inside the function unless it is declared var, which is why calcpi counts down a variable of its own, left. A parameter with no word before it is a let one. A var parameter is the function's own copy: in fn halve(var n i32), changing n leaves the caller's variable as it was. A function can also give back several values at once, written as a list of types, which is a tuple.

A function may call itself. Euclid's greatest common divisor, the loop from the last page, becomes three lines: the answer for a and b is the answer for b and the remainder, until the remainder is 0:

fn gcd(a i32, b i32) i32 {
  if b == 0 { a; } else { gcd(b, a % b); }
}

gcd(84, 36)       // 12

A call that is the last thing a function does, as gcd's is, may be turned into a jump by the compiler, so deep recursion of this shape need not use up the stack; nothing promises it, though. A function marked inline, written after its return type, has its body generated within each caller's (Inline functions).

Functions that can fail: Result#

Some work can fail for reasons that are not a bug: a file that is not there, text that is not a number, an average of nothing at all. A function like that says so in its return type, and gives back a Result: Ok holding the answer, or Error holding what went wrong. It is a good habit from the start, because it is how a program stays honest about everything that can go wrong:

fn average(total i32, count i32) Result[i32, &str] {
  if count == 0 {
    return Error("there is nothing to average");
  }
  Ok(total / count);
}

The caller cannot use a Result as though it were the answer. It checks which one it got, and a match does that, with a case for each; a case names the variable that receives what is inside. The match must cover both, so a failure cannot be overlooked by accident:

match average(30, 0) {
  case let avg Ok { print <- "average: ", avg, "\n"; }
  case let why Error { print <- "no average: ", why, "\n"; }   // this one runs
}

When only the failure matters, is asks which it is: if r is Error { ... }. And a function that cannot deal with a failure itself passes it on to its own caller:

fn doubledAverage(total i32, count i32) Result[i32, &str] {
  match average(total, count) {
    case let avg Ok { Ok(avg * 2); }
    case let why Error { Error(why); }
  }
}

Those are the patterns in use today. Shorter ways to pass an error on, such as a ? operator, are being designed and are not built yet. The &str here is a view of a piece of text, which String explains, and Result itself is an ordinary enum, as Enums shows. The standard library's functions that can fail answer one too: the fs package's (Failure: Result and FsError), and the I/O an actor awaits (Awaiting I/O). What must never happen at all is not a Result but a panic.

Closures: functions with state#

A closure is a function written right where it is used, with no name of its own. It is a value like any other, so it is kept in a variable and called through it. In its simplest form it uses the variables around it:

let rate = 3;
let triple = fn(x i32) i32 { x * rate; };
triple(14)        // 42

What makes a closure more than a function is that it can carry state of its own. Its state goes in square brackets after its parameters, and lives on from one call to the next. This one hands out the Fibonacci numbers, one per call, each the sum of the two before:

var nextFib = fn() [var a = 0i64, var b = 1i64] i64 {
  let now = a;
  a, b = b, a + b;      // both change at once
  now;
};

each i in 1 .. 10 {
  print <- " ", nextFib();       // 0 1 1 2 3 5 8 13 21 34
}

a and b belong to the closure, and a second closure written the same way would have its own. In full, a closure is written fn (parameters) [state] returnType { body }: a function's definition without the name, with the state in square brackets after the parameters, where a bracket cannot be taken for the return type. Each state entry is a field of the closure, a name with an initial value whose type is the value's, and the entries are separated by commas; a closure with no state leaves the list out, as triple does. An entry is let unless written var, and the variable that holds a closure that changes its state is var too. An entry's initial value is read in the code around the closure, not from the entries before it: in [a = 1, b = a], b starts from the a outside.

An entry is how a closure gets a value of its own, by the ordinary rules for values:

let ribs = 3;
let shape = fn(u i32) [ribs, base = 2 * 5 + 1] i32 { u * ribs + base; };
shape(2)          // 2 * 3 + 11: 17

An entry with no =, [ribs], starts from the variable of that name around the closure: a value that copies is copied, and one that moves is moved, as it would be passed to a function. [buf = xs] moves a value in, and xs can no longer be used. Listing a counted handle, an Rc or an Arc, gives the closure a count of its own, and the handle's permission says whether the closure may change what it points at. [var n = 0] is new state, and [r = &p] is a borrow written out, which gives the closure the borrow's lifetime. Listing also tells the reader which values are the closure's own. A closure with only its own state and copies can be kept after the code around it has finished, or handed to an actor; one that uses the variables around it cannot.

A closure that uses a variable around it, without listing it, borrows it: it reads it where it stands, or changes it if the body does, and the compiler works out which from the body. Only a var variable can be changed that way; changing any other is the ordinary "no permission to modify" error, reported inside the body. While the closure is still to be used, nothing else may change the variable (nor read it, if the closure changes it), and once the closure's last use is past, it can. Borrowing is the default because a copy made silently would duplicate state: a closure that added to its own copy of a total would leave the total unchanged, with no sign that it had. A closure written in a method that names a member of the object, hits, reaches it through the method's self, which it borrows as it borrows any variable; a closure sees exactly what the code around it sees, private members included, wherever it is later called from.

A closure is a value, and its body is its one method, so a few rules follow from that. return inside it leaves the closure, never the function around it. It cannot call itself, since it has no name inside its own body ("Error 1393: A closure cannot call itself"), so recursion is a named function's work. It is written in a function, never at the module level as a global's value or a field's default, where there is nothing for it to borrow. Its return type is the one written; or else the one the signature it is given to says; or else it is read off the body: the first return or final expression fixes it, and the others must fit. A body whose last statement changes something (total += x;) returns nothing. Paths that give different types are refused, with Error 1392 asking for the return type to be written. And a function that takes a closure names the signature it will call, behind &<:

// f is any closure, or function, that takes an i32 and gives one back
fn apply(f &<fn(x i32) i32, x i32) i32 { f(x); }

let off = 10;
apply(x => x * 2, 21)                        // 42
apply(fn(x i32) i32 { x + off; }, 1)         // 11
apply(fn(x) { x + off; }, 1)                 // 11: the type comes from the signature

A closure that changes what it holds is passed as &<mut fn(x i32) i32 instead. This &< is a reference that stands for anything that can be called that way; the Closure references section of Modularity has every form of it, and what a closure may hold and where it may go. A parameter's type may be left out, as in the last call, only where the closure is given to something that says what the signature is: a parameter such as f, a generic bound F fn(sig), or an owner of a closure (An owner of a closure).

For a small closure handed to something that already knows its types, there is a short form, the parameters, an arrow and the body: x => x * 2, (a, b) => a.len() < b.len(), () => 5, n => total += n. It takes parameter names only, and a body that is one expression, an assignment or a block; it has no state list and no written return type, so a closure that needs either is written in full. A comma after the body belongs to the list the closure is in, so f(x => x + 1, 3) is a call with two arguments, and the arrow is =>, not ->, which would read as "returns". That is how closures are handed to a collection to keep, change or add up its items, on Collections.

Note: Not built yet: a closure that is called once and consumed, whose body moves one of its state entries out.

Generators: functions that hand out values#

The Fibonacci closure has to be called once for every number. A generator turns that around: it is a function written as the loop that makes the sequence, and each walks the values it hands out. It says yields and a type where a return type would go, and each yield hands out one value:

fn fibonacci(n i32) yields i64 {
  var a = 0i64;
  var b = 1i64;
  each i in 1 .. n {
    yield a;
    a, b = b, a + b;
  }
}

each f in fibonacci(10) {
  print <- " ", f;              // 0 1 1 2 3 5 8 13 21 34
}

After each yield, the generator waits, its variables just as they were, until each asks for the next value; then it carries on from where it stopped. When its body ends, so does the sequence. Calling fibonacci(10) runs none of it yet: each runs it a step at a time, as it needs each value, and a loop that leaves early with break simply never asks for the rest. Walking a list or a dictionary works the same way, and Collections comes back to it.

yield each hands on every value of another generator in turn, which joins sequences, and makes a recursive walk, such as through a tree, as short as the recursion. A method can be a generator too, walking what its value holds:

fn count(n i32) yields i32 {
  var i = 0;
  while i < n { yield i; i += 1; }
}

fn joined(a i32, b i32) yields i32 {
  yield each count(a);
  yield 99;
  yield each count(b);
}

struct Span {
  lo i32;
  hi i32;
  fn evens(self &) yields i32 {
    each x in lo .. hi if x % 2 == 0 { yield x; }
  }
}

each v in joined(2, 3) { print <- " ", v; }               //  0 1 99 0 1 2
let span = new Span(3, 10);
each v in span.evens() { print <- " ", v; }              //  4 6 8 10

A yield is a statement of its own, never part of a larger expression. A generator is an ordinary value with a next method, which is all each needs, and the iterator adapters take one as they take any cursor: count(5).map(x => x * 2).sum() is 20 (Being walked: next). Generators: writing your own, in Recipes, has the rest: generator methods, generic generators, what may be borrowed across a yield, and when a generator dies.

You can now name a piece of work, give it state, and have it hand out a sequence. Next come the types themselves, starting with the numbers that every example so far has quietly relied on.