LogoChemical Docs
Ctrl+K

Variants

Variants (also known as Tagged Unions) allow a variable to hold one of several different types of data.

Definition and Layout

A variant consists of a Tag (automatically managed integer) and a Union of the data associated with each case.

variant Shape {
    Circle(radius : float)
    Rect(w : float, h : float)
    None()
}

Pattern Matching

Pattern matching is strictly reserved for Variants.

if (var ...) Syntax

Extraction and condition in one step.

if (var Shape.Circle(r) = my_shape) {
    printf("Radius is %f\n", r)
}

match / switch Syntax

switch (my_shape) {
    Shape.Circle(r) => return 3.14 * r * r
    Shape.Rect(w, h) => return w * h
    default => return 0.0
}

The else Unwrapping

Used for quick data extraction. If the variant doesn't match the case, the else block is executed (must return, panic, or skip).

var Shape.Circle(r) = my_shape else unreachable
printf("Radius: %f\n", r);

Type Testing with is

Check the state without extracting data.

if (my_shape is Shape.Circle) {
    // ...

}
IMPORTANT
Because Variants use a tag and union layout, they are highly memory-efficient, consuming only as much space as the largest case + the tag.

Methods in Variants

Variants can have methods that operate on their internal data:

variant FuncInVariant {
    First(value : int)
    Second(value : int)
    
    func get(&self) : int {
        switch(self) {
            First(value) => return value;
            Second(value) => return value;
        }
    }
}

var v = FuncInVariant.First(232)
v.get()  // Returns 232

Variant Inheritance from Structs

Variants can inherit from structs to share common data across all cases:

struct Point {
    var a : int = 10
    var b : int = 20
    
    func multiply(&self) : int {
        return a * b
    }
}

variant Shape : Point {
    Circle(radius : int)
    Rect(width : int, height : int)
}

var s = Shape.Circle(5)
s.a  // 10 (inherited, default initialized)

s.b  // 20 (inherited, default initialized)

s.multiply()  // 200

Variant Interface Implementation

Variants can implement interfaces with impl:

interface Giver {
    func give(&self) : int
}

variant OptionalInt : Giver {
    None()
    Some(value : int)
}
impl Giver for OptionalInt {
    func give(&self) : int {
        switch(self) {
            None() => return -1;
            Some(value) => return value;
        }
    }
}

Static Dispatch with Variants

Use generic constraints for zero-cost abstraction:

func <T : Giver> get_value(v : &T) : int {
    return v.give()
}

var opt = OptionalInt.Some(42)
get_value(opt)  // 42

Dynamic Dispatch with Variants

Use dyn for runtime polymorphism:

func process_giver(g : dyn Giver) : int {
    return g.give()
}

var opt = OptionalInt.Some(93)
process_giver(dyn<Giver>(opt))  // 93