Functions and generics

Functions and methods, overloads, lambdas and function types, generics and constraints, operators and extension blocks in Antril.

Last updated 10 min read Edit on GitHub

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.

AntrilRun
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"));
	}
}
Terminal
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:

Antril
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.

AntrilRun
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));
	}
}
Terminal
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:

AntrilRun
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();
}
Terminal
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.

AntrilRun
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]));
	}
}
Terminal
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.
AntrilRun
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);
}
Terminal
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.

AntrilRun
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));
}
Terminal
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).

AntrilRun
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);
}
Terminal
larva run
400
true
4
400
  • left op ++(T x) is a prefix unary operator (++a), right op !(T x) a postfix one (a!); without left or right, a unary operator works on both sides.
  • op (T)(S s) declares a cast from S to T.
  • The levels of PrecedenceLevel go from Assignment to Power; 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.

AntrilRun
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());
	}
}
Terminal
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.

Was this page helpful?

No address is stored; your user name is, if you are signed in.

Esc