Interfaces
An interface names methods. A type declares which interfaces it satisfies with ::.
import std.io.console { println };
interface Drawable {
fn draw(): string;
}
class Circle :: [Drawable] {
radius: float,
fn draw(): string { return "circle"; }
}
class Square :: [Drawable] {
side: float,
fn draw(): string { return "square"; }
}
fn show(d: Drawable): void {
println(d.draw());
}
fn main(): int {
show(Circle { radius = 1.0 });
show(Square { side = 2.0 });
return 0;
}
A value used through its interface dispatches dynamically. That is the only dynamic dispatch in the language.
There is no inheritance and no interface inheritance. A type that needs two contracts lists both:
interface Named { fn name(): string; }
interface Sized { fn size(): int; }
class Box :: [Named, Sized] {
fn name(): string { return "box"; }
fn size(): int { return 1; }
}
fn main(): int { return Box { }.size(); }
Default methods
An interface method with a body is a default. A type may use it or override it; its own member wins.
import std.io.console { println };
interface Greeter {
fn name(): string;
fn greet(): string { return "Hello, " + this.name(); }
}
class Formal :: [Greeter] {
fn name(): string { return "Ada"; }
fn greet(): string { return "Good evening, " + this.name(); }
}
class Casual :: [Greeter] {
fn name(): string { return "Grace"; }
}
fn main(): int {
println(Formal { }.greet());
println(Casual { }.greet());
return 0;
}
Extending a type
extend adds methods to a type you did not declare, including a built-in one.
import std.io.console { println };
extend int {
fn doubled(): int { return this * 2; }
}
interface Drawable { fn draw(): string; }
class Plain { }
extend Plain :: [Drawable] {
fn draw(): string { return "plain"; }
}
fn main(): int {
println(Plain { }.draw());
return 21.doubled();
}
An extend block may also declare that the type satisfies an interface.
Operators through interfaces
== and != work on any type that conforms to Equatable from std.core. There is no separate
operator declaration: the operator is the interface method, written as mathematics. a == b calls
a.equals(b), and a != b negates it.
import std.core { Equatable };
import std.io.console { println };
struct Point :: [Equatable<Point>] {
x: int,
y: int,
fn equals(other: Point): bool {
return this.x == other.x && this.y == other.y;
}
}
fn main(): int {
let a = Point { x = 1, y = 2 };
let b = Point { x = 1, y = 2 };
if (a == b) {
println("same place");
}
return 0;
}
The conformance is what enables the operator, not the method alone. A type with an equals method
that never declares :: [Equatable<Point>] keeps its method — but == stays an error. That is
deliberate: the conformance names the contract, and without it any method that happens to be called
equals would silently become an operator.
Inside generic code the constraint is enough:
import std.core { Equatable };
fn contains<T :: [Equatable<T>]>(xs: T[], wanted: T): bool {
for (x in xs) {
if (x == wanted) {
return true;
}
}
return false;
}
fn main(): int {
return if (contains([1, 2, 3], 2)) 0 else 1;
}
Because the built-in types conform through extend blocks in std.core, the same generic function
serves an int, a string, and any type of yours that declares the conformance. Monomorphization
turns each use into a direct call — the operator costs no more than the method it stands for.
Optionals stay outside this rule: a ?T compares against null, and the value inside compares
after narrowing.
Ordering works the same way, through Ordered and its single compare method — negative, zero or
positive, as strcmp. All four comparison operators derive from it:
import std.core { Ordered };
import std.io.console { println };
struct Version :: [Ordered<Version>] {
major: int,
minor: int,
fn compare(other: Version): int {
if (this.major != other.major) {
return if (this.major < other.major) -1 else 1;
}
if (this.minor != other.minor) {
return if (this.minor < other.minor) -1 else 1;
}
return 0;
}
}
fn main(): int {
let old = Version { major = 1, minor = 4 };
let new = Version { major = 1, minor = 5 };
if (old < new) {
println("upgrade available");
}
return 0;
}
string conforms to Ordered<string> in the standard library, so "apple" < "banana" works out of
the box — lexicographic over code points, the same order compare defines.
Arithmetic follows the same rule, through one interface per operator: Add, Sub, Mul and Div
from std.core, each with a single method of the same name. The operands are homogeneous — T with
T, giving T:
import std.core { Add, Sub };
import std.io.console { println };
import std.string { fromInt };
struct Vec2 :: [Add<Vec2>, Sub<Vec2>] {
x: int,
y: int,
fn add(other: Vec2): Vec2 {
return Vec2 { x = this.x + other.x, y = this.y + other.y };
},
fn sub(other: Vec2): Vec2 {
return Vec2 { x = this.x - other.x, y = this.y - other.y };
}
}
fn main(): int {
let position = Vec2 { x = 10, y = 20 };
let velocity = Vec2 { x = 1, y = -2 };
let next = position + velocity;
println(fromInt(next.x));
println(fromInt(next.y));
return 0;
}
The built-in numerics and string conform in std.core, so a generic function constrained on Add
serves them and your types alike:
import std.core { Add };
fn total<T :: [Add<T>]>(a: T, b: T): T {
return a + b;
}
fn main(): int {
return total(40, 2) - 42;
}
A mixed form such as Vec2 * float does not exist yet: the interfaces are homogeneous by design,
and % stays numeric-only. Compound assignment (v += w) does not reach through the interfaces
either — write v = v + w.
The last operator is as. Beyond the numeric casts, which keep their built-in meaning, a cast is a
conversion the operand's type declared through Into:
import std.core { Into };
import std.io.console { println };
import std.string { fromInt };
struct Fahrenheit { degrees: int, }
struct Celsius :: [Into<Fahrenheit>] {
degrees: int,
fn into(): Fahrenheit {
return Fahrenheit { degrees = this.degrees * 9 / 5 + 32 };
}
}
fn main(): int {
let boiling = Celsius { degrees = 100 };
let f = boiling as Fahrenheit;
println(fromInt(f.degrees));
return 0;
}
Three boundaries, each deliberate. Conversions are explicit — nothing converts on its own, and
1 as float never goes through Into. A type has one conversion target, because into is a
member name and a type has one member of a name; the second conversion is an ordinary named method.
And a conversion that can fail does not belong here: Into returns T, not ?T — parsing a
string into a number is a named function returning an optional.