Functions and generics
Functions and methods, overloads, lambdas and function types, generics and constraints, operators and extension blocks in Antril.
On this page
Functions and methods
A function is declared with its name, its parameters and, after :, its result. Without a result it returns nothing. Functions can live in a namespace (free functions) or in a type (methods); both are written the same way.
import System;
into namespace Math2
{
Square(i32 x) : i32
{
return x * x;
}
// A body that is a single expression.
Cube(i32 x) : i32 => x * x * x;
Report(str label, i32 value)
{
Console.WriteLine(label + ": " + value);
}
// Overloads: same name, different parameters.
Describe(i32 x) : str => "the number " + x;
Describe(str s) : str => "the text " + s;
Main()
{
Report("square", Square(4));
Report("cube", Cube(3));
Console.WriteLine(Describe(7));
Console.WriteLine(Describe("seven"));
}
}
larva run
square: 16
cube: 27
the number 7
the text seven
An overload is chosen by the number of arguments, then by their types. Two overloads with the same parameters, or a call that fits two equally well, are errors.
Antril has no default values for parameters and no named arguments; write an overload instead:
Greet(str name, str greeting) : str => greeting + ", " + name;
Greet(str name) : str => Greet(name, "Hello");
Lambdas and function types
func<A, B> : R is the type of a function that takes an A and a B and returns an R. func : R takes nothing; without : R it returns nothing (func<str>).
A lambda writes a function in place. Its parameter types come from the func type it is assigned to, or are written out. Any function, static method, or object.Method is also a function value.
import System;
into namespace Funcs
{
Twice(i32 x) : i32 => x * 2;
Apply(func<i32> : i32 f, i32 x) : i32 => f(x);
Map<T, U>(List<T> items, func<T> : U f) : List<U>
{
List<U> result = new List<U>();
loop (T item in items)
result.Add(f(item));
return result;
}
Main()
{
Console.WriteLine(Apply(Twice, 21));
func<i32, i32> : i32 add = (a, b) => a + b;
Console.WriteLine(add(2, 3));
i32 offset = 10;
func<i32> : i32 shift = x => x + offset;
Console.WriteLine(shift(5));
func<str> show = (str text) => { Console.WriteLine("> " + text); };
show("hi");
List<i32> numbers = new List<i32>();
numbers.Add(1);
numbers.Add(2);
List<str> labels = Map(numbers, n => "#" + n);
Console.WriteLine(String.Join(", ", labels));
}
}
larva run
42
5
15
> hi
#1, #2
A lambda captures the variables it uses: it sees them as they are when it runs, and assigning one inside the lambda changes the variable. Inside an instance method, a lambda sees the object and its members as the method does.
Collections take lambdas too: List<T> has Where, Map, Any, All, FindIndex, ForEach, RemoveAll and Sort(compare).
Iterators
A function that returns Iterator<T> can give its values one at a time with yield. Each turn of a loop over it runs the function on to its next yield:
import System;
Squares(i32 count) : Iterator<i32>
{
loop (i32 i = 1..=count)
yield i * i;
}
Main()
{
loop (i32 x in Squares(4))
Console.Write(x + " ");
Console.WriteLine();
}
larva run
1 4 9 16
An iterator runs once; call the function again to start over.
Generics
Types and functions take type parameters in <…>. The arguments are usually inferred from the call.
import System;
into namespace Generic
{
public class Stack<T>
{
List<T> items = new List<T>();
public:
Push(T item) { items.Add(item); }
Pop() : T => items.Pop();
i32 Count { get => items.Count; }
}
Largest<T : IComparable<T>>(T a, T b) : T => a.CompareTo(b) >= 0 ? a : b;
Sum<T : number>(T[] values) : T
{
T total = T.Zero;
loop (T v in values)
total = total + v;
return total;
}
Main()
{
Stack<str> stack = new Stack<str>();
stack.Push("a");
stack.Push("b");
Console.WriteLine(stack.Pop() + " " + stack.Count);
Console.WriteLine(Largest("pear", "apple"));
Console.WriteLine(Largest(3, 7));
Console.WriteLine(Sum([1, 2, 3]));
Console.WriteLine(Sum([1.5, 2.25]));
}
}
larva run
b 1
pear
7
6
3.75
Constraints
A constraint after : says what a type argument must be or have. Several are separated by commas.
| Constraint | The type argument must… |
|---|---|
T : IShape, T : IComparable<T> |
implement the interface (or derive from the class). |
T : number |
be a number: + - * / % == <, T.Zero, T.One (System.INumber<T>). |
T : snumber |
be a signed number: also -x and T.NegativeOne (System.ISignedNumber<T>). |
T : [u8, u16, u32] |
be one of the listed types. |
T : int, uint, float, numeric |
belong to that group of primitive types. |
T : class, T : value |
be an object type, or a primitive or value. |
T : new(), T : new(i32, str) |
have a constructor with those parameters, so new T(…) works. |
T : copy, T : plain |
be copyable; be a type whose zero is a valid value (so default(T) works). |
T : op +(T, T) : T |
have that operator. |
T : (i64)T |
convert to i64 with a cast. |
T : exception, T : attribute |
be an exception or an attribute class. |
T : bitflag enum |
be a bitflag enum. |
import System;
public class Thing
{
public:
i32 Id = 7;
}
Twice<T : op +(T, T) : T>(T x) : T => x + x;
Make<T : new()>() : T => new T();
Main()
{
Console.WriteLine(Twice(21));
Console.WriteLine(Twice(1.25));
Console.WriteLine(Make<Thing>().Id);
}
larva run
42
2.5
7
Math is written with these constraints: Math.Max, Math.Abs, Math.Sqrt and the rest work on any number type, including your own value that implements INumber.
Variadic generics
The last type parameter can be a pack, T...: it takes any number of types. static loop (E in T) repeats a statement for each type of the pack, at compile time.
import System;
CountArguments<T...>(T values) : i32
{
i32 n = 0;
static loop (E in T)
n++;
return n;
}
Main()
{
Console.WriteLine(CountArguments(1, "two", 3.0));
}
larva run
3
This is how System.ValueTuple and System.Tuple support any number of elements.
Operators
Classes and values declare operators with op. A symbol can be one of the usual ones (+, ==…) or a new sequence of the characters + - * / % & | ^ ~ ! < > = ? @ # $ .. A new binary symbol says how strongly it binds with precedence(PrecedenceLevel.X) and, optionally, associativity(Associativity.Right).
import System;
public value Money
{
public:
i64 Cents;
new(i64 Cents);
op +(Money a, Money b) : Money => new Money(a.Cents + b.Cents);
op ==(Money a, Money b) : bool => a.Cents == b.Cents;
op <+>(Money a, Money b) : Money precedence(PrecedenceLevel.Additive) => new Money(a.Cents * 2 + b.Cents);
op (i64)(Money m) => m.Cents;
}
Main()
{
Money total = new Money(150) + new Money(250);
Console.WriteLine(total.Cents);
Console.WriteLine(total == new Money(400));
Console.WriteLine((new Money(1) <+> new Money(2)).Cents);
Console.WriteLine((i64)total);
}
larva run
400
true
4
400
left op ++(T x)is a prefix unary operator (++a),right op !(T x)a postfix one (a!); withoutleftorright, a unary operator works on both sides.op (T)(S s)declares a cast fromStoT.- The levels of
PrecedenceLevelgo fromAssignmenttoPower; a higher level binds more strongly.
Extension blocks
ext(T name) { … } adds methods to an existing type, including primitives and interfaces. Inside, name is the value the method is called on. static ext(T) adds static methods to the type itself, and ext<T : …>(T name) extends every type that meets a constraint.
import System;
into namespace Extras
{
ext(i32 n)
{
public:
IsEven() : bool => n % 2 == 0;
}
ext(str s)
{
public:
Shout() : str => s.ToUpper() + "!";
}
static ext(i32)
{
public:
Answer() : i32 => 42;
}
Main()
{
i32 n = 4;
Console.WriteLine(n.IsEven());
Console.WriteLine("hello".Shout());
Console.WriteLine(i32.Answer());
}
}
larva run
true
HELLO!
42
An extension block has no fields. The standard library uses one, ext<T : bitflag enum>(T flags), to give every bitflag enum HasFlag, With, Without and Toggle.
Next: Errors and exceptions.
Edit this page on GitHub Report an issue