Type System
Metel is statically and strongly typed. Types are checked at compile time. There are no implicit conversions.
Primitive Types
| Type | Description | Example |
|---|---|---|
i64 | 64-bit signed integer | 42 |
f64 | 64-bit floating point | 3.14 |
boolean | Boolean | true |
String | UTF-8 string | "hello" |
Char | Unicode scalar value | 'a' |
() | Unit — represents no value | () |
The unit type () is only written explicitly when needed as a type parameter (e.g. Result<(), Error>). Functions that return nothing omit the -> annotation entirely.
Sized Numeric Types
Metel provides exact-width numeric types for low-level and systems programming. i64 and f64 are the default integer and floating-point types in ordinary code.
Signed integers:
| Type | Width |
|---|---|
i8 | 8-bit |
i16 | 16-bit |
i32 | 32-bit |
i64 | 64-bit |
Unsigned integers:
| Type | Width |
|---|---|
u8 | 8-bit |
u16 | 16-bit |
u32 | 32-bit |
u64 | 64-bit |
Floats:
| Type | Width |
|---|---|
f32 | 32-bit IEEE 754 |
f64 | 64-bit IEEE 754 |
Sized literals use a suffix: 42i32, 3.14f32, 255u8. All casts between sized numeric types are explicit (as). Array indices must be u64; indexing with an i64 requires an explicit as u64 cast.
Unsuffixed literals are polymorphic. When the expected type is known from context (annotation, function parameter, struct field, return type, or the other operand in arithmetic/comparison), an unsuffixed numeric literal adopts that type automatically. When no context is available, the literal defaults to i64 (integer) or f64 (float).
let a: i32 = 10; // 10 is i32
let b: u8 = 255; // 255 is u8
let c: f32 = 1.5; // 1.5 is f32
fun scale(x: f32, factor: f32) -> f32 { x * factor }
let r = scale(2.0, 3.0); // both literals are f32
let x: i32 = 10i32;
let y = x + 5; // 5 adopts i32 from x; y is i32
This also applies to var reassignment — the right-hand side of m = expr adopts m's declared type:
var count: i32 = 0;
count = 99; // 99 is i32
Char
Char represents a single Unicode scalar value. Character literals use single quotes: 'a', '\n', '\u{1F600}'.
fun main() {
let c: Char = 'a';
let code: u32 = u32::from(c);
let back: Char = Char::from(code);
}
Char is not u32 and not a string — no implicit coercions exist. Use u32::from(c) to get the Unicode scalar value and Char::from(n) to construct from a code point; Char::from raises a runtime error if n is not a valid Unicode scalar value.
Type Inference
Types are inferred using the Hindley-Milner algorithm with let-polymorphism. Annotations are optional for all bindings, including function parameters and return types. They may be written explicitly for documentation or to restrict a binding to a less general type.
Annotations are required only where there is no expression to infer from:
- Struct and enum field types
- Aspect method signatures
Every named type in an annotation must resolve in the annotation's declaring scope,
including names nested inside arrays, tuples, function types, and record fields. This is
checked when the declaration is type-checked, even if no value ever reaches the
annotation. A generic parameter in scope and Self where it is permitted resolve as
types; every other unknown name is error T0003.
fun add_annotated(a: i64, b: i64) -> i64 { a + b }
fun add_inferred(a, b) { a + b }
fun main() -> i64 {
let x = 42; // inferred: i64
let name = "Vlad"; // inferred: String
let y: f64 = 3.14; // explicit annotation (optional here)
let total = add_annotated(x, 1) + add_inferred(2, 3);
if (name == "Vlad") { total + (y as i64) } else { 0 }
}
Tuples
Tuples are lightweight anonymous product types.
fun main() -> i64 {
let coord: (i64, i64) = (10, 20);
let triple: (String, i64, boolean) = ("yes", 42, true);
return coord.0 + triple.1;
}
Positional field access uses .0, .1, etc.:
fun main() -> i64 {
let coord: (i64, i64) = (10, 20);
let x = coord.0;
let y = coord.1;
return x + y;
}
() is the zero-element tuple (unit type).
Tuples can be destructured in match:
fun main() -> i64 {
let coord: (i64, i64) = (10, 0);
match coord {
(0, y) => y,
(x, 0) => x,
(x, y) => x + y,
}
}
Anonymous Records
A record is a product type whose components are labelled, where a tuple's are positional. It is written in bare braces, with no keyword:
{ x: f64, y: f64 } // the type
{ x = 1.0, y = 2.0 } // a value of it
Field declarations classify and take :; field initializers define and take = — the same
distinction let x: i64 = 1 already draws.
A record type is exact. { x: f64 } is inhabited only by records with that row and
nothing else; a value of { x: f64, y: f64 } is not a value of { x: f64 }. Records are
not implicitly widened or narrowed.
Records are structurally typed. Two records with the same labels and field types are the
same type, wherever they were written. A record has no declaration site and no name.
Field order does not matter: { x: i64, y: i64 } and { y: i64, x: i64 } are the same
type, and { x = 1, y = 2 } and { y = 2, x = 1 } are indistinguishable — each is usable
wherever the other is. A record is a set of labelled fields, not an ordered one. Repeating a
label in one record ({ x: i64, x: f64 }) is an error.
(Indistinguishable is a statement about the type, not about ==, which no compound type —
record, struct, tuple, or array — supports.)
When a local variable has the same name as a field, the = value part may be omitted, as in
a struct literal:
fun main() {
let x = 1.0;
let y = 2.0;
let p = { x, y }; // { x: f64, y: f64 }
println("${p.x}");
}
Punning, and single-field record literals generally, are read as records only in positions
that expect an expression — a let/var or field initializer, a call argument, an array
element. In a position that also admits a block — an if/else or match arm, a
function, closure, or loop body — a bare { x } is a block whose result is x, and
{ x = 1 } is a block whose result is the assignment. Write the record in parentheses to
force it: ({ x }). A multi-field literal needs no parentheses, as { x = 1, y = 2 }
cannot be a block.
Where records may be used
Records are ordinary values: they may appear as parameters, returns, let bindings, and
struct or enum fields; they may be pattern-matched, used as generic arguments, and tagged or
borrowed (@a { x: f64 }, &r { x: f64 }) exactly as a struct is. Send and Sync extend
to them by the same field-composition rule used for structs.
Three things a record cannot do, all for the same underlying reason — it has no nominal owner:
- No inherent methods. Two unrelated modules could otherwise write conflicting methods for the same shape with no principled way to choose between them.
- No implementations of a non-local aspect, by the other direction of that rule. An aspect local to the current module may be implemented for a record — but see the note below: that is not available yet.
- No custom
Drop.Dropis a standard-library aspect and never local to ordinary user code, so teardown logic belongs to nominal types only.
Not available in v0.12.0: implementing a local aspect for a record.
extend { w: i64 }: MyAspect { … }does not work. This is not specific to records —extendon a tuple or an array target fails the same way; implementations for structural types are not built yet. Until they are, a record satisfies no aspect that requires an implementation, so a record cannot be printed, compared, or passed where any such bound is required. Auto-derived aspects are unaffected. Tracked as issue #296.
Projection
A nominal type's row may be projected to a named subset, written with a dot to distinguish it from a struct literal:
Handle.{ fd } // the type: Handle's row, narrowed to `fd`
A bare identifier inside projection braces is always a field label, never a type or a
row variable. Chained projection (S.{ a }.{ b }) and projection in pattern position are not
accepted.
Arrays
Array<T> is the built-in ordered sequence type. The shorthand T[] is preferred.
fun main() -> i64 {
let nums: i64[] = [1, 2, 3];
let names: Array<String> = ["alice", "bob"];
if (names.len() == 2) { nums[0] } else { 0 }
}
Index access uses [] with an i64 index. Out-of-bounds access causes a panic.
fun main() -> i64 {
let nums: i64[] = [1, 2, 3];
let first = nums[0];
return first;
}
Arrays are usable in for-in loops.
T[] is a borrowed view, not an owning buffer. T[] will be a non-owning, immutable, unconditionally-Copy view over a contiguous run — a pointer and a length, produced only by borrowing a List<T>, a [T; N], or another slice. a[0] = 9 through a T[] will stop compiling; mutation moves to List<T> or a [T; N]. Array literals produce [T; N] (below), not T[] — let nums: i64[] = [1, 2, 3]; above will keep working via [T; N]'s existing implicit coercion to T[] (RFC-0053), not because the literal itself is a T[]The three-way split between T[], [T; N], and List<T> below reflects the current
design. The exact boundary between them — in particular, how a growable list's storage is
allocated and grown — is not yet fully specified and may change in a future release.
Fixed-size arrays
[T; N] is an array type whose length N is a non-negative integer literal known at compile time.
[T; N] coerces to T[] (not the reverse). N must be a non-negative integer literal; variables are not permitted.
fun main() {
// Repeat construction: every element is the same value.
let zeros: [i64; 3] = [0; 3];
// Literal construction with an explicit sized type.
let ones: [i64; 3] = [1, 2, 3];
// Coerces to T[] when a T[] is expected.
fun first(xs: i64[]) -> i64 { xs[0] }
let v = first(ones); // [i64; 3] → i64[]
}
Indexing and for-in work identically to T[]. Array patterns match sized arrays:
fun sum(xs: [i64; 3]) -> i64 {
match xs {
[a, b, c] => a + b + c, // exact-count pattern on [T; 3]
}
}
[T; N], not T[]. [1, 2, 3] will have type [i64; 3]; a literal has a statically known length and owns its elements, which is what [T; N] already is. Slices arise only from borrowing, never from a literal. The [T; N] → T[] coercion above already applies wherever T[] is expected — a let/var target, a function argument, a generic instantiation — so this does not by itself require touching call sites that already pass a [T; N]-typed or explicitly T[]-annotated value; it only changes what an unannotated literal's own type isSee the note under "Arrays" above — this split is not considered final.
References
Reference types provide explicit aliasing for non-linear values.
fun main() -> i64 {
var value = 1;
let p: &i64 = &value;
let q: &var i64 = &var value;
*q = *p + 1;
return q;
}
Metel has two reference types:
&T— shared immutable reference toT&var T— exclusive mutable reference toT
&T borrows, or exactly one &var T, never both"Exclusive" means exactly that rule. It is not yet enforced: the current interpreter has
no borrow checker, so a program may hold two &var T to the same place and will not be
rejected.
&var T coerces to &T. The reverse coercion does not exist. Both are non-owning
aliases — a reference never owns the value it points to.
&T is Copy; &var T is not, so an exclusive reference is moved on use rather than
duplicated. Passing one as an argument reborrows instead of moving — see
§References and moves.
References are first-class values, but they are distinct from the referent type. Ordinary
access — field reads/writes, indexing, method dispatch, reading a plain value out — goes
through auto-deref and type-directed copy; an explicit dereference operator *p is also
available (v0.11.0) for reading through a reference and for writing through a
&var T (*p = v). See §References.
References are only for non-linear aliasing. They cannot target linear values.
&var accepts arbitrary addressable lvalue paths — struct fields, tuple elements, array elements, and chains thereof. Writes through the resulting &var T propagate back to the original storage location:
struct Counter { value: i64 }
fun main() -> i64 {
var c = Counter { value = 0 };
let p: &var i64 = &var c.value;
*p = 42;
return c.value; // 42
}
&var for lvalue pathsv0.10.0Reading a value out of a reference
No field, no method, no operator — just the plain value a reference points to. This cannot be a move (references never own their referent), only a copy, and only when the referent's type permits copying:
fun main() -> i64 {
let x = 42;
let r: &i64 = &x;
let y: i64 = r; // type-directed copy: y's declared type differs from r's
return y;
}
The copy fires at every position where a declared or expected type is already
known — not only let/var bindings and explicit ascription, but also a return
value against the enclosing function's declared return type, a break value against
the enclosing loop's inferred type, and any tail expression of a function/method/
closure body, an if/else branch, or a match arm (each of those resolves its
result against a declared or expected type the same way a let binding does):
fun bump(p: &var i64) -> i64 {
*p += 1;
p // tail expression, no explicit `return` — copies out of p
}
It never fires silently at a plain call site; fun f(v: i64) called as f(r) where
r: &i64 is a type error, not an implicit copy. Argument position has no declared type
of its own for the rule to compare against, the same reason type-directed extraction of
an allocated value never fires implicitly at a plain-parameter call site either
(internal/rfcs/2-accepted/rfc-0066-allocated-value-extraction.md §3a — not yet
integrated, cited here only for the parallel).
Chains through multiple reference layers the same way auto-deref does — reaching the declared type may require copying out of more than one layer:
fun main() -> i64 {
let x = 42;
let r: &i64 = &x;
let rr: &&i64 = &r;
let y: i64 = rr; // copies through both layers of the chain
return y;
}
Until affine ownership (Copy/Drop, not yet integrated) lands, this applies to
every type — the interpreter has no move semantics today (everything is deep-cloned on
bind), so there is no non-Copy type yet to exclude. Once ownership is integrated, a
non-Copy T cannot be produced this way.
List<T>
List<T> is the standard growable-sequence type. Use it when you need to append, pop, or otherwise mutate a sequence. Use T[] when the sequence is fixed after construction.
fun main() {
var xs: List<i64> = List::new();
xs.push(1);
xs.push(2);
xs.push(3);
println(xs.len().to_string()); // 3
let last = xs.pop(); // Perhaps::Some { value = 3 }
}
Construction:
| Form | Description |
|---|---|
List::new() | Empty list |
List::from(arr) | Construct from a T[] — copies elements |
Methods:
| Method | Signature | Description |
|---|---|---|
push | (&var self, value: T) | Append an element |
pop | (&var self) -> Perhaps<T> | Remove and return the last element, or None |
len | (&self) -> i64 | Number of elements |
get | (&self, index: i64) -> Perhaps<T> | Bounds-checked access |
as_slice | (&self) -> T[] | View as an immutable array (no copy) |
List<T> does not implicitly coerce to T[]. Call .as_slice() to get a read-only view.
as_slice is what its signature already says. Today as_slice returns the same underlying storage, but the result is deep-copied at whatever binding or return receives it, so "no copy" describes only the call itself, not the value's subsequent lifetime. Once T[] is a genuine borrowed view, the returned slice stays a live view for as long as it is used — still bounded by self's lifetime, not copied away from itType Ascription
The : operator asserts that an expression has a given type without performing any runtime conversion. It is a pure type-inference hint — no code is emitted at runtime.
Type ascription is mainly an ergonomics feature. Most code should type-check from
its surrounding context alone; : is for the cases where spelling out the intended
type inline is clearer than introducing a separate annotated binding.
fun main() -> i64 {
let xs = [] : i64[];
let x = 1 : i64;
if (xs.len() == 0) { x } else { 0 }
}
Ascription fails at compile time if the inferred type of the sub-expression cannot be unified with the ascribed type. For example, 1 : String is invalid. Use as to convert between types; use : only when the value already has the target type.
fun main() -> i64 {
let y = 1 : String;
return 0;
}
When ascription helps
Type inference uses surrounding expected types. That expected type can come from a let annotation, a function return type, a callee's parameter types, or the surrounding expression context.
Because of that, ambiguous literals like [] and None often type-check without explicit ascription when the context already determines their type:
fun zip_lengths(a: i64[], b: String[]) -> i64 {
return a.len() + b.len();
}
fun make_row(use_default: boolean, fallback: i64[]) -> i64[] {
return match use_default {
true => [],
false => fallback,
};
}
fun first_or_default(items: i64[], fallback: Perhaps<i64>) -> i64 {
return match fallback {
Perhaps::Some { value } => value,
None => if (items.len() > 0) { items[0] } else { 0 },
};
}
fun main() -> i64 {
let total = zip_lengths([], ["a", "b"]);
let row = make_row(true, [1, 2, 3]);
let first = first_or_default([1, 2, 3], None);
return total + row.len() + first;
}
Ascription is still useful when no surrounding context fixes the type:
fun main() -> i64 {
let arr = [] : i64[];
let value = None : Perhaps<i64>;
match value {
Perhaps::Some { value } => value + arr.len(),
Perhaps::None => arr.len(),
}
}
Without such context, ambiguous literals remain a type error. For example, let x = None; does not provide enough information to infer the element type.
fun main() -> i64 {
let x = None;
return 0;
}
Type Casting
The as operator casts between any two numeric primitive types. It desugars to a call to the From aspect and is infallible — the result is the target type directly.
fun main() {
let x: i32 = 1000i32;
let b: i8 = x as i8; // wraps: 1000 mod 256 → -24
let f: f32 = x as f32; // 1000.0f32
let u: u64 = x as u64; // 1000u64
let pi: f64 = 3.14;
let n: i32 = pi as i32; // truncates toward zero → 3
}
All pairwise casts among i8, i16, i32, i64, u8, u16, u32, u64, f32, f64 are supported. Narrowing integer casts wrap (two's-complement truncation). f64-to-integer casts truncate toward zero.
Because as desugars to From, user-defined types become castable by implementing From<SourceType> for the target type.
Generics
Perhaps<T>, Result<T, E>, T[])v0.1.0User-defined generic functions and typesv0.3.0Types and functions can be parameterized with <T> syntax.
struct Stack<T> {
items: T[],
}
fun first<T>(arr: T[]) -> Perhaps<T> {
if (arr.len() == 0) {
return None;
}
return Perhaps::Some { value = arr[0] };
}
fun main() -> i64 {
let stack = Stack { items = [1, 2, 3] };
match first(stack.items) {
Perhaps::Some { value } => value,
Perhaps::None => 0,
}
}
Row bounds
A bound written as a row accepts any type carrying at least the listed fields:
fun magnitude<record T: { x: f64, y: f64, .. }>(p: T) -> f64 {
(p.x * p.x + p.y * p.y).sqrt()
}
The trailing .. is load-bearing. It stands for "and a rest I am not naming," and its
presence is what makes the bound open:
fun g<record T: { x: f64 }>(p: T) // closed: T's row is exactly `x`
fun h<record T: { x: f64, .. }>(p: T) // open: T has at least `x`
A field may omit its type to constrain the label only — { x } means "carries an x,
whatever its type":
fun f<record T: { x, .. }>(p: T) // has an `x` of some type
fun g<record T: { x, y: f64, .. }>(p: T) // any-typed `x`, `f64` `y`
Negation reuses the ! that bounds already accept, and is the complement of the positive
bound — just as !Copy means "does not implement Copy". It takes no .., since absence
has no rest to quantify over:
fun send<record T: !{ token }>(t: T) -> i64 { … } // carries no `token` at all
fun tag<record T: !{ id: String }>(t: T) -> i64 { … } // no `String`-typed `id`
Note the second form is satisfied by a record whose id is an i64 — it does not have a
String id. Write !{ id } for "no id of any type".
A row bound is satisfied by a record, not by a nominal struct. The record marker on the
type parameter says so at the declaration; a bare <T: { … }> is an error.
The marker may be written at the parameter or in a where clause — the two are equivalent,
and a parameter is record-kinded if either one carries it:
fun f<record T: { x: f64, .. }>(p: T) -> f64
fun g<T>(p: T) -> f64 where record T: { x: f64, .. }
The row bound is optional. <record T> on its own means "any record, whatever its
fields" — the only way to write that, since a bound of { .. } alone is not accepted:
fun labels<record T>(x: T) -> Symbol[] // any record; no constraint on its fields
magnitude({ x = 3.0, y = 4.0 }); // a record — satisfies the bound
magnitude(some_point); // a struct — does not
To give a nominal type row behaviour, declare it as a record — that is the primary route, not conversion:
record Point { x: f64, y: f64 } // satisfies row bounds directly
struct Point { x: f64, y: f64 } // does not
Converting an existing struct (some_point.to_record()) is the escape hatch for types you
do not control, not the ordinary path.
Why row capability is opt-in
A nominal type's API is what it declares. A record's API is what it contains.
Once a type satisfies row bounds, its field names and types are part of its public interface,
whether the author intended that or not. Renaming a field breaks every caller who wrote a
bound mentioning it; adding one can make the type accidentally satisfy a bound its author
never heard of. On a struct, a field rename is an internal change.
That is why structural capability is opt-in rather than automatic, and why a struct is not
simply a less capable record. The two trade against each other:
| encapsulation | structural flexibility | |
|---|---|---|
struct | layout is private; the API is what you declare | none |
record X | layout is the API | full |
Most types want the first. A type whose shape is genuinely the contract — a coordinate
pair, a configuration fragment — wants the second, and says so by declaring record.
What satisfies which bound
Both bound kinds are opted into; they differ only in granularity. An aspect bound is
opted into per aspect, by writing an implementation. A row bound is opted into per type,
by choosing the record kind. Nothing is implicit in either direction.
non-local aspect (Display) | local aspect | row bound | |
|---|---|---|---|
struct | yes, with an impl | yes, with an impl | no |
enum | yes, with an impl | yes, with an impl | no — sums, not products |
| anonymous record | no — see below | yes, with an impl | yes |
record X (named) | yes, with an impl | yes, with an impl | yes |
An anonymous record has no owning module, so the orphan rule permits an implementation only
for an aspect local to the implementing module. Every standard-library aspect is non-local,
which means no anonymous record is Display and println("${r}") does not work on one.
Auto-derived aspects are unaffected — Send and Sync are computed from field composition
rather than declared. A named record has an owning module and does not have this limit.
Implementing an aspect for a record
Three forms, with different rules:
extend { x: f64, y: f64 }: MyAspect { … } // one concrete row
extend<row R: { x: f64, .. }> { ..R }: MyAspect { … } // every row of a given shape
extend<row R> { ..R }: MyAspect { … } // every row
The first applies to exactly one structural type and is permitted when the aspect is local. The second and third require row variables and are not available in v0.12.0. The second also needs overlap checking between row bounds — two shape-conditional implementations can be incomparable rather than one being more specific, so they must be disjoint. The third additionally needs a way to require an aspect of every field in the row, which does not yet exist.
Never Type
! (Never) is the uninhabited bottom type — no value of type ! can ever be
constructed. A loop with no reachable break has type !:
fun main() -> i64 {
let result: i64 = loop { break 42; };
return result;
}
return <expr>, panic(<message>), loop { } with no reachable break, and break/continue used as value expressions in loop context all have type !. If any sub-expression has type !, that sub-expression diverges before the outer expression can produce a value, so the outer expression's type is unconstrained and any type is accepted in that position.
Subtyping and coercion
! is a subtype of every type — ! <: T for all T — so an expression of type ! coerces implicitly, with no cast, to any context expecting T. This is what makes the rule above sound: code after a diverging expression is unreachable, but still typechecks against whatever its context requires.
Match exhaustiveness
A match whose scrutinee has type ! needs no arms — an empty match is vacuously exhaustive, since no value of type ! can ever reach it:
fun unreachable_code(x: !) -> i64 {
match x { } // exhaustive — no arms needed
}
More generally, an enum variant whose payload type is ! is uninhabited — no value of that variant can ever be constructed — and a match may omit the arm for an uninhabited variant while remaining exhaustive:
enum Foo {
A { x: i64 },
B { y: ! },
}
fun handle(f: Foo) -> i64 {
match f {
Foo::A { x } => x,
// Foo::B omitted — exhaustive; B is uninhabited
}
}
Inhabited-singleton coercion
If an enum has exactly one inhabited variant (every other variant's payload is !) and that variant has exactly one field, a value of the enum type coerces implicitly to the field's type — the compiler inserts the destructuring, no explicit match required:
enum Wrapper<T> {
Present { value: T },
Absent { _: ! },
}
fun infallible() -> Wrapper<i64> { Wrapper::Present { value = 42 } }
fun main() -> i64 {
let x: i64 = infallible(); // implicit coercion via the inhabited-singleton rule
return x;
}
Result<T, !> satisfies this: Ok { value: T } is the one inhabited variant with one field, so a Result<T, !>-returning function's caller can use the result as a plain T with no match. Perhaps<!> does not satisfy it — None is inhabited but has zero fields — so Perhaps<!> never coerces implicitly to anything, though nothing prevents it from arising through generic instantiation.
! as a return type
A function annotated -> ! promises never to return; every control-flow path must end in a diverging expression, checked by the compiler:
fun abort(msg: String) -> ! {
panic(msg);
}
A -> ! function containing a reachable return is a type error.
Perhaps<T>
Perhaps<T> is the built-in optional type. There is no null — all absence is expressed via Perhaps<T>.
The type of None is Perhaps<T> for some T that must be determinable from context. If no context constrains T — for example, a bare let x = None with no annotation and no subsequent use that pins the element type — the program is a type error. An explicit annotation is required in that case:
None and Some are ordinary variants of Perhaps<T>, not literalsNone and Some have no special status in the grammar or the type system. They resolve exactly as Red does for a user-declared enum Colour { Red, .. } — bare where the expected type determines the enum, qualified (Perhaps::None) anywhere. Everything said here about needing a determinable type follows from that general rule rather than from a rule about None specifically, and the same is true of Result<T, E>'s Ok/Err. See §Unqualified variant construct….
fun main() -> i64 {
let x: Perhaps<i64> = None;
match x {
Perhaps::Some { value } => value,
Perhaps::None => 0,
}
}
fun main() -> i64 {
let result: Perhaps<i64> = None;
let value: Perhaps<i64> = 42;
match value {
Perhaps::Some { value } => value,
Perhaps::None => match result {
Perhaps::Some { value } => value,
Perhaps::None => 0,
},
}
}
Use match to unwrap safely:
struct User {
id: i64,
}
fun find_user(id: i64) -> Perhaps<User> {
if (id == 1) {
return Perhaps::Some { value = User { id = 1 } };
}
return None;
}
fun main() -> i64 {
match find_user(1) {
Perhaps::Some { value } => value.id,
Perhaps::None => 0,
}
}
.yolo() unwraps, panicking if the value is None:
struct User {
id: i64,
}
fun find_user(id: i64) -> Perhaps<User> {
if (id == 1) {
return Perhaps::Some { value = User { id = 1 } };
}
return None;
}
fun main() -> i64 {
let user = find_user(1).yolo();
return user.id;
}
Result<T, E>
Result<T, E> represents the outcome of a fallible operation:
fun divide(a: f64, b: f64) -> Result<f64, String> {
if (b == 0.0) {
return Result::Err { error = "division by zero" };
}
return Result::Ok { value = a / b };
}
fun main() -> i64 {
match divide(8.0, 2.0) {
Result::Ok { value } => value as i64,
Result::Err { error } => 0,
}
}
Use match to handle both cases, or ? to propagate errors.
.yolo() also works on Result<T, E>, panicking on Err.