Обобщения и интерфейсы
Обобщённый тип#
Задача. Один стек для любых типов элементов.
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() ?? "-"}")
}Вывод
3 top=3 3 2 1 -1 x
data Stack<T>иimpl Stack<T>— параметр типа объявляется один раз, в заголовке.Stack.new()выводитTиз аннотацииStack<i32>.var fn pushизменяет значение,fn peekтолько читает;pop()возвращаетT?— пустой стек даётnone, а не панику.- Ограничения выражаются как
T: Ord,T: Show: напримерfn largest<T: Ord>(xs: List<T>) -> T?.
Интерфейсы и dyn#
Задача. Обработать фигуры разных типов одним кодом.
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 }] // разные типы, один интерфейс
var total = 0.0
for s in shapes {
print(describe(&s))
total += s.area()
}
print("total {total}")
}Вывод
circle 3.14 rect 6.0 total 9.14
- Интерфейс структурный:
CircleиRectничего не объявляют оShape, им достаточно иметь методыarea()иname(). List<dyn Shape>хранит значения разных типов, вызовы идут через таблицу виртуальных методов. Если тип известен при компиляции, пишите обобщениеfn f<T: Shape>(x: &T)— вызов будет статическим и без накладных расходов.describe(&s)принимает&dyn Shape— заимствование любого значения, удовлетворяющего интерфейсу.- Если нужен явный контракт («тип обязан объявить, что реализует»), интерфейс помечается
@explicit.
Перегрузка операторов#
Задача. Работать с деньгами как с числами, но без возможности сложить рубли с метрами.
@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 } // перегрузка оператора +
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()}")
}Вывод
38.25 true true 25.75
static op +(a: Money, b: Money) -> Money— оператор определяется как статический метод типа; поддерживаются арифметические операторы и сравнения (через@derive(Ord)).Moneyхранит копейки целым числом: сумма точная, без ошибокf64.- Перегрузка не создаёт неявных преобразований:
price * 3определён,price * 2.5— нет, пока вы не напишете такой оператор.
Свои итераторы#
Задача. Бесконечные последовательности и «нарезка» потока значений.
fn fib() -> iter<i64> {
var a = 0
var b = 1
fromFn(() => { // fromFn: итератор из замыкания
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()}")
}Вывод
[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(() => …)строит итератор из замыкания, которое возвращает очередное значение; замыкание может менять захваченныеvar— так хранится состояние.iterate(seed, f)даётseed, f(seed), f(f(seed)), …; бесконечные итераторы безопасны, потому что вычисляются лениво —take,takeWhileограничивают их.zip,windows(n),chunks(n),enumerate,groupBy,sortBy— готовые адаптерыstd.core.iter.- Подводный камень: бесконечный итератор нельзя собирать (
toList()) без ограничителя — программа будет работать, пока не кончится память.
Вернуться: кулинарная книга.