OxL Reference

4.3 Construction and constructors

Literal. T{ f1: e1, f2, ..base }:

  • fields without a default value MUST be given, otherwise it is a compile error;
  • f2 is shorthand for f2: f2;
  • ..base takes the remaining fields from base (moving or copying them);
  • a literal is allowed only if all fields of T are visible at the point of use and T has no @sealed attribute.

Positional literal. T(e1, e2, …) — fields in declaration order; allowed under the same conditions and if T has at most 4 fields without a default value.

Named constructors are declared in impl with the word init:

oxl
@sealed data Account {
  id: AccountId
  owner: UserId
  balance: Money
  invariant balance >= Money.zero()
}

impl Account {
  init new(owner: UserId) -> Self =>
    Self{ id: AccountId(cluster.ids.next()), owner, balance: Money.zero() }

  init open(owner: UserId, deposit: Money) -> Self | TooSmall {
    if deposit < MIN_DEPOSIT { fail TooSmall(deposit) }
    Self{ ..Self.new(owner), balance: deposit }
  }
}

let a = Account.new(me)
let b = Account.open(me, Money.rub(500))?
  • init name(params) -> Self | E — an associated function with access to all fields; the return type MUST be Self or a union containing Self.
  • @sealed forbids a literal outside the impl of the type; construction only through init.
  • T.default() exists if all fields have default values, or with @derive(Default).
  • Values of refined types in a literal obey §3.6: User{ name: "Ann" } — checked at compile time; User{ name: Name.from(s)? } — at run time.
  • Embedded fields (§4.7) are given flat (Dog{ name: "Rex", breed }) or as a whole by the type name (Dog{ Animal: a, breed }).

Type call. T(args) is resolved as follows: if a positional literal of T is allowed at the point of the call — it is a literal; otherwise, if T has init new — the call T.new(args); otherwise — a compile error. For a distinct type T(x) is a constructor, for a sharded actor — an ActorRef reference.

Destruction. A value is destroyed when its owner's scope is left, in the reverse order of declaration. A user destructor is impl Drop for T { var fn drop() { … } }; Drop is implemented only explicitly.

Destructors are synchronous. The body of drop MUST have no effects and no suspension points; only non-blocking calls that release local OS resources, marked @nonblocking, are allowed (close a descriptor, free memory). There are no asynchronous destructors.

Asynchronous cleanup — an explicit method and defer:

oxl
@must_close
data Conn { sock: Socket; open: bool = true }

impl Conn {
  move fn close() -> () | NetError  net {
    shield { sock.sendGoAway()? }           // cancellation does not interrupt the shutdown
    open = false
  }
}

impl Drop for Conn {
  var fn drop() {
    if open {                                // close() was forgotten: a safety net without the network
      sock.closeLocal()
      log.warn("Conn dropped without close()")
    }
  }
}

fn talk(addr: Addr) -> () | NetError  net {
  let c = Conn.dial(addr)?
  defer c.close()?                           // the error of close() is returned from the function
  c.send(HELLO)?
}
  • @must_close on a type: if a value can leave its scope without a call of the move method marked as closing (close by default, otherwise @must_close(by: name)), or without transfer of ownership, it is the compile error must-close.
  • defer e? inside a function with errors: the error of e is returned from the function if the function does not already return another error; otherwise it is written to the log.
  • A synchronous Drop remains a safety net for panic and cancellation: it releases local resources and writes a warning, but does not touch the network.

bad-init, missing-field, must-close, positional-literal, sealed-literal

See also

Source: docs/spec/oxl-standard.en.md