OxL Справочник OxL
РусскийEnglish

Обобщения и интерфейсы

Обобщённый тип#

Задача. Один стек для любых типов элементов.

oxl✓ проверяется в CI
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#

Задача. Обработать фигуры разных типов одним кодом.

oxl✓ проверяется в CI
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.

Перегрузка операторов#

Задача. Работать с деньгами как с числами, но без возможности сложить рубли с метрами.

oxl✓ проверяется в CI
@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 — нет, пока вы не напишете такой оператор.

Свои итераторы#

Задача. Бесконечные последовательности и «нарезка» потока значений.

oxl✓ проверяется в CI
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()) без ограничителя — программа будет работать, пока не кончится память.

Вернуться: кулинарная книга.

См. также

OxL 1.0.0-beta · Версия документации 1.0.0-beta© OxwaldLink · сгенерировано из Стандарта, исходников std и руководства