OxL Reference

Generics and interfaces

A generic type#

Task. One stack for any element type.

oxl✓ checked in 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() ?? "-"}")
}
Output
3 top=3
3 2 1 -1
x
  • data Stack<T> and impl Stack<T> - the type parameter is declared once, in the header. Stack.new() infers T from the annotation Stack<i32>.
  • var fn push changes the value, fn peek only reads; pop() returns T? - an empty stack gives none, not a panic.
  • Constraints are written T: Ord, T: Show, for example fn largest<T: Ord>(xs: List<T>) -> T?.

Interfaces and dyn#

Task. Process shapes of different types with one piece of code.

oxl✓ checked in 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 }]  // 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: Circle and Rect declare nothing about Shape, it is enough for them to have the methods area() and name().
  • 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 generic fn 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.

oxl✓ checked in 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 }  // 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.
  • Money stores cents as an integer: the sum is exact, without f64 errors.
  • Overloading creates no implicit conversions: price * 3 is defined, price * 2.5 is not until you write such an operator.

Your own iterators#

Task. Infinite sequences and "slicing" a stream of values.

oxl✓ checked in CI
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 captured vars - this is how state is kept.
  • iterate(seed, f) gives seed, f(seed), f(f(seed)), …; infinite iterators are safe because they are evaluated lazily - take, takeWhile bound them.
  • zip, windows(n), chunks(n), enumerate, groupBy, sortBy are ready-made adapters of std.core.iter.
  • Pitfall: an infinite iterator must not be collected (toList()) without a limiter - the program will run until memory is exhausted.

Back: cookbook.

See also

OxL 1.0.0-beta · Documentation version 1.0.0-beta© OxwaldLink · generated from the Standard, the std sources and the guides