Functions
A function declares the type of every parameter and of the result. The return type is required.
function add(int32 $a, int32 $b) : int32
{
return $a + $b;
}
echo add(1, 2); // 3Every parameter has a type, and so does the result. That's the whole shape of it.
void is not the default
A function that returns nothing still has to say so:
function log(string $message) : void
{
echo $message;
}Leave the : void off and it doesn't parse:
function log(string $message)
{
echo $message;
}
// error: Unexpected token 'open_brace '{'' found. Expected 'colon (:)'I know that's a few extra characters on every side-effecting function. I prefer it to the alternative, where the only way to find out whether something comes back is to read the body.
void is not a value. A function that can fail and has nothing to hand back returns status<E>.
Order does not matter
You can call a function before the file declares it:
echo fib(10); // 55
function fib(int32 $n) : int32
{
if ($n < 2) {
return $n;
}
return fib($n - 1) + fib($n - 2);
}Declarations are collected before any body is compiled, across every file of the module at once. There are no forward declarations and no include order to get right. Only top-level statements care about order, and those run in filename order.
Named arguments
Sometimes a function has two or three arguments of the same type and the call site is just a pile of numbers. $name: binds a parameter by name, in any order, after the positionals:
function pair(int32 $a, int32 $b) : int32
{
return $a * 10 + $b;
}
echo pair(1, 2); // 12
echo pair($b: 2, $a: 1); // 12
echo pair(1, $b: 2); // 12The name is the parameter, dollar sign included. pair(b: 2) does not parse.
Positionals fill in order first. After the first $name:, everything else has to be named too:
echo pair($a: 1, 2);
// error: positional argument after named argumentAn unknown $nope: is an error. A name at most once.
Defaults fill holes
A parameter may carry = expr. After matching, any hole still empty is filled from it:
function add(int32 $a, int32 $b = 2) : int32
{
return $a + $b;
}
echo add(3); // 5
echo add(3, 4); // 7
echo add($b: 4, $a: 1); // 5Defaults do not have to be trailing, because a later parameter can still be named:
function wrap(int32 $a = 1, int32 $b) : int32
{
return $a * 10 + $b;
}
echo wrap($b: 8); // 18wrap(8) would bind 8 to $a and leave $b empty, which is an error. Name the one you mean.
Labels are part of the signature
$name: is optional sugar. A label is not. Write it with a colon on the declaration, the same way the call site writes it, and the call has to use it. Not a positional, not $name::
function listen(forEvent: string $name, int32 $code) : int32
{
echo $name;
return $code;
}
echo listen(forEvent: 'click', 7);
echo listen(forEvent: 'tap', $code: 8);That prints click, then 7, then tap, then 8. listen('click', 7) is an error: the parameter is labelled forEvent, so the call must say forEvent:. listen($name: 'click', 7) is the same error. The label and the parameter are two different words, on purpose.
Why would you want that? Because the label is part of the overload identity and the $name is not. Two functions that both take int32 then string are one signature. Two functions that take different labels are two:
function print(int32 $level, string $message) : int32
{
echo $message;
return $level;
}
function print(fromDecimal: int32 $cents, currency: string $code) : int32
{
echo $code;
return $cents;
}
echo print(1, 'hi');
echo print(fromDecimal: 100, currency: 'EUR');print(100, 'EUR') is the first overload. The second required its labels. That is the whole reason labels exist: they let you have two call shapes that would otherwise be the same types in the same order.
There is no print statement in Echo, so declaring one is fine. Unlabelled parameters stay positional, with $name: as optional sugar. Constructors are functions too, so the same three spellings work on Point(...). Structs is that chapter.
Arguments are copies
By default a parameter is the function's own copy of the value:
struct Coordinate { float64 $x; float64 $y; }
function scaled(Coordinate $c) : Coordinate
{
$c->x = $c->x * 2.0;
$c->y = $c->y * 2.0;
return $c; // the caller's Coordinate was never touched
}Scribbling on $c is fine. It's yours.
For an int32 that copy is free. For a struct of two floats it's nearly free. For something that owns a heap buffer, like an array<T>, it's a real copy of the whole buffer, which is usually not what you meant. That's what borrows are for.
Borrowing instead of copying
Put & on the parameter type and the function gets access to the caller's value instead of a copy. The call site says nothing, because a borrow takes nothing: the compiler takes the address for you.
function total(const array<int32>& $xs) : int32
{
int32 $sum = 0;
foreach ($xs as $x) {
$sum = $sum + $x;
}
return $sum;
}
array<int32> $nums = [1, 2, 3];
echo total($nums); // 6
echo $nums->count(); // 3, still yoursconst T& is a read-only borrow. Drop the const and the function can write through it, which is how you get an out parameter:
function fill(array<int32>& $out) : void
{
$out->push(9);
}
array<int32> $nums = [1, 2, 3];
fill($nums);
echo $nums->count(); // 4Three rules up front:
- A borrow is never null. That's the difference between
T&andptr<T>. const T&andT&are different types, sof(const Foo&)andf(Foo&)are two different overloads and the compiler picks the more specific one for a mutable argument.- Writing
total(&$nums)explicitly is the same call. Aptr<T>parameter is the one that does not borrow for you, because it may be null and that should look different at the call.
Pointers and references has the rest.
Taking ownership
If a function needs to keep the value rather than borrow it, it asks for ownership with mv:
function consume(mv array<int32> $xs) : int32
{
return $xs->count();
}And the call site has to agree, out loud:
array<int32> $nums = [1, 2, 3];
echo consume($nums); // error: '$xs' takes ownership of this argument -
// write 'mv' in front of it
echo consume(mv $nums); // 3
echo $nums->count(); // error: '$nums' has been moved out ofA signature can't quietly eat something you thought you still had. Ownership and moving is the chapter.
Which one should a parameter be?
The short answer, in the order I'd try them:
| Want | Write | Cost |
|---|---|---|
| Read a value, do not keep it | const T& $x | nothing |
| Read and write the caller's value | T& $x | nothing |
| Your own copy to scribble on | T $x | a copy |
| Keep the value for good | mv T $x | nothing, but the caller loses it |
For small primitives (int32, bool, a pointer) just take them by value. A borrow of an int32 costs more than the int32 did.
Overloading
Several functions can share a name as long as their parameters differ:
function describe(int32 $v) : void { echo 1; }
function describe(string $v) : void { echo 2; }
function describe(float64 $v) : void { echo 3; }
describe(1); // 1
describe("x"); // 2
describe(1.5); // 3Resolution happens on the arguments, at compile time, with no runtime dispatch involved. When nothing fits, the compiler shows you what it had:
function f(int32 $a) : void { echo 1; }
f(1, 2);
// error: No overload of 'f' accepts these arguments. Candidates are:
// f(int32)Overloads are matched on arity first, and a set with exactly one candidate of the right arity wins without the types being consulted at all. That is why the message above lists the signature rather than complaining about the second argument specifically.
The return type is not part of the signature. Two functions differing only in what they return are a duplicate, not an overload.
Labels are part of that identity. Parameter names are not. f(int32 $a) and f(int32 $b) collide. f(from: int32 $a) and f(to: int32 $a) do not. Labels is the section.
Generic functions
A function can take type parameters:
function largest<T: numeric>(T $a, T $b) : T
{
if ($a > $b) {
return $a;
}
return $b;
}
echo largest(3, 7); // 7
echo largest(1.5, 0.5); // 1.500000T is inferred from the arguments, so you rarely write it at the call site. The constraint after the colon can be an interface you declared, or one of the built-in shorthands: numeric, integer, signed, unsigned, floating.
Functions as values
A function type is written function<R(P...)>, and a function literal in value position is a closure:
function<int32(int32)> $double = function(int32 $x) : int32 { return $x * 2; };
echo $double(21); // 42
function apply(function<int32(int32)> $f, int32 $v) : int32
{
return $f($v);
}
echo apply($double, 10); // 20Calling into C
An extern block declares functions that exist somewhere else. No body, no mangling, and as renames the symbol locally so it does not collide with an Echo name:
extern {
function abs as c_abs(int32 $v) : int32;
}
echo c_abs(-5); // 5C interop covers linking, strings across the boundary, shipping C sources beside your Echo, and passing a function to C as extern function<R(P...)> with &name.
What functions cannot do yet
A couple of things you might reach for that aren't there. Both are on the list. This is just so you don't spend twenty minutes on the syntax.
Variadics. There is no ..., which is why there is no printf and no string formatting.
One more, and this one is a real hole rather than a missing feature: a function that falls off the end without returning is not diagnosed. It compiles and hands back whatever was in the register.
function bad() : int32
{
echo 1;
} // no error, and bad() returns garbageMake sure every path returns. The compiler will not check for you yet.
Next
- Closures for the value form.
- Generics for type parameters.
- Structs for constructors, which are the same call machinery:
$name:, defaults, labels, and writing anyconstructordeleting the free one. - Ownership and moving for what
mvand&really mean.