Generics and interfaces
A generic type#
Task. One stack for any element type.
data Stack<T> { items: List<T> }
impl Stack<T> {
static fn new() -> Stack<T> => Stack{ items: [] }
var fn push(x: T) { items.push(x) }
var fn pop() -> T? => items.pop()
fn peek() -> T? => items.last()
fn size() -> usize => items.len()
}
fn main() io {
var s: Stack<i32> = Stack.new()
s.push(1)
s.push(2)
s.push(3)
print("{s.size()} top={s.peek() ?? 0}")
print("{s.pop() ?? 0} {s.pop() ?? 0} {s.pop() ?? 0} {s.pop() ?? -1}")
var names: Stack<str> = Stack.new()
names.push("x")
print("{names.peek() ?? "-"}")
}Output
3 top=3 3 2 1 -1 x
data Stack<T>andimpl Stack<T>- the type parameter is declared once, in the header.Stack.new()infersTfrom the annotationStack<i32>.var fn pushchanges the value,fn peekonly reads;pop()returnsT?- an empty stack givesnone, not a panic.- Constraints are written
T: Ord,T: Show, for examplefn largest<T: Ord>(xs: List<T>) -> T?.
Interfaces and dyn#
Task. Process shapes of different types with one piece of code.
interface Shape {
fn area() -> f64
fn name() -> str
}
data Circle { r: f64 }
data Rect { w: f64; h: f64 }
impl Circle {
fn area() -> f64 => 3.14 * r * r
fn name() -> str => "circle"
}
impl Rect {
fn area() -> f64 => w * h
fn name() -> str => "rect"
}
fn describe(s: &dyn Shape) -> str => "{s.name()} {s.area()}"
fn main() io {
let shapes: List<dyn Shape> = [Circle{ r: 1.0 }, Rect{ w: 2.0, h: 3.0 }] // different types, one interface
var total = 0.0
for s in shapes {
print(describe(&s))
total += s.area()
}
print("total {total}")
}Output
circle 3.14 rect 6.0 total 9.14
- An interface is structural:
CircleandRectdeclare nothing aboutShape, it is enough for them to have the methodsarea()andname(). List<dyn Shape>holds values of different types, calls go through a virtual method table. If the type is known at compile time, write a genericfn f<T: Shape>(x: &T)- the call will be static with no overhead.describe(&s)takes&dyn Shape- a borrow of any value that satisfies the interface.- If you need an explicit contract ("a type must declare that it implements it"), mark the interface
@explicit.
Operator overloading#
Task. Work with money like numbers but without being able to add roubles to metres.
@derive(Copy, Eq, Ord, Show)
data Money { cents: i64 }
impl Money {
static fn of(units: i64, cents: i64) -> Money => Money{ cents: units * 100 + cents }
static op +(a: Money, b: Money) -> Money => Money{ cents: a.cents + b.cents } // overloading the + operator
static op -(a: Money, b: Money) -> Money => Money{ cents: a.cents - b.cents }
static op *(a: Money, k: i64) -> Money => Money{ cents: a.cents * k }
fn show() -> str => "{cents / 100}.{(cents % 100).abs().toString().padStart(2, '0')}"
}
fn main() io {
let price = Money.of(12, 50)
let total = price * 3 + Money.of(0, 75)
print("{total.show()}")
print("{total > price} {price == Money.of(12, 50)} {(total - price).show()}")
}Output
38.25 true true 25.75
static op +(a: Money, b: Money) -> Money- an operator is defined as a static method of the type; arithmetic operators and comparisons (via@derive(Ord)) are supported.Moneystores cents as an integer: the sum is exact, withoutf64errors.- Overloading creates no implicit conversions:
price * 3is defined,price * 2.5is not until you write such an operator.
Your own iterators#
Task. Infinite sequences and "slicing" a stream of values.
fn fib() -> iter<i64> {
var a = 0
var b = 1
fromFn(() => { // fromFn: an iterator from a closure
let r = a
a = b
b = r + b
r
})
}
fn main() io {
print("{fib().take(10).toList()}")
print("{fib().filter(n => n % 2 == 0).take(5).toList()}")
let powers = iterate(1, n => n * 2).takeWhile(n => n < 100).toList() // iterate: x, f(x), f(f(x)), …
print("{powers}")
print("{(1..=5).iter().zip(["a", "b", "c"].iter()).toList()}")
print("{[1, 2, 3, 4, 5].iter().windows(2).map(w => w[1] - w[0]).toList()}")
print("{(1..=7).iter().chunks(3).toList()}")
}Output
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34] [0, 2, 8, 34, 144] [1, 2, 4, 8, 16, 32, 64] [(1, "a"), (2, "b"), (3, "c")] [1, 1, 1, 1] [[1, 2, 3], [4, 5, 6], [7]]
fromFn(() => …)builds an iterator from a closure that returns the next value; the closure may change capturedvars - this is how state is kept.iterate(seed, f)givesseed, f(seed), f(f(seed)), …; infinite iterators are safe because they are evaluated lazily -take,takeWhilebound them.zip,windows(n),chunks(n),enumerate,groupBy,sortByare ready-made adapters ofstd.core.iter.- Pitfall: an infinite iterator must not be collected (
toList()) without a limiter - the program will run until memory is exhausted.
Back: cookbook.