GALA Examples

This page contains examples demonstrating various features of the GALA language.

Complete Example

The following example demonstrates many of GALA’s features, including structs, immutability, expression functions, and control flow.

package main


struct Point(X int, Y int)

func moveX(p Point, delta int) Point = p.Copy(X = p.X + delta)

func main() {
    val p1 = Point(10, 20)
    val p2 = moveX(p1, 5)

    val msg = if (p2.X > 10) "moved" else "static"
    Println(msg, p2)
}

Named Arguments

Function calls support named arguments in any order. The compiler reorders them to match the function signature.

package main

func describe(name string, age int, role string) string =
    s"$name ($role, age $age)"

func main() {
    Println(describe("Alice", 30, "engineer"))                   // positional
    Println(describe(role = "designer", name = "Bob", age = 25)) // named, any order
}

Default Parameter Values

Functions can have default parameter values. Callers can omit trailing defaults or use named arguments to skip specific parameters.

package main

func greet(name string, greeting string = "Hello", punctuation string = "!") string =
    s"$greeting, $name$punctuation"

func add(a int, b int = 10) int = a + b

func main() {
    Println(greet("World"))                     // Hello, World!
    Println(greet("World", "Hi"))               // Hi, World!
    Println(greet("World", "Hey", "..."))       // Hey, World...
    Println(greet("World", punctuation = "?"))  // Hello, World?
    Println(greet(name = "World", greeting = "Yo")) // Yo, World!
    Println(add(5))                             // 15
    Println(add(5, 20))                         // 25
}

Default expressions are evaluated at each call site:

package main

var counter = 0

func nextId() int {
    counter = counter + 1
    return counter
}

func createItem(name string, id int = nextId()) string =
    s"$name#$id"

func main() {
    Println(createItem("A"))      // A#1
    Println(createItem("B"))      // B#2
    Println(createItem("C", 99))  // C#99
}

Option Monad Example

package main


func main() {
    val x = Some(10)
    val y = x.Map((v) => v * 2)         // parameter type inferred as int
    val z = None[int]().GetOrElse(42)

    val res = y match {
        case Some(v) => v
        case _       => 0
    }

    Println(res, z)
}

See also: Language Reference - Option Monad

Default Case

A sealed type’s default case is its zero value: a field a struct literal leaves out holds it. Option’s default case is None().

package main

sealed type Mode {
    case On(Level int)
    default case Off()
}

struct Lamp(Name string, M Mode, Dim Option[int])

func describe(l Lamp) string = l.M match {
    case On(level) => s"${l.Name} on at $level"
    case Off()     => s"${l.Name} off, dim ${l.Dim.GetOrElse(0)}"
}

func main() {
    Println(describe(Lamp{Name: "desk"}))
    Println(describe(Lamp("hall", On(7), Some(3))))
}

Output:

desk off, dim 0
hall on at 7

Case Field Defaults

A case’s fields can declare defaults. A constructor leaves them out by position or by name.

package main

sealed type Light {
    case Off()
    default case On(Power int = 30, Room string = "Living")
}

func main() {
    Println(On(), On(Power = 50), On(10, "Kitchen"))
}

Output:

On(30, Living) On(50, Living) On(10, Kitchen)

Generic Methods Example

package main


type Box[T any] struct { Value T }

func (b Box[T]) Transform[U any](f func(T) U) Box[U] = Box[U](Value = f(b.Value))

func main() {
    val b = Box[int](Value = 10)
    val s = b.Transform((i) => s"Value is $i")
    Println(s.Value)
}

See also: Language Reference - Generics

Immutable Parameters, Lambda Parameters and Receivers

package main

struct Counter(var N int)

// Writing a `var` field through the receiver is allowed; rebinding it is not.
func (c *Counter) Bump() {
    c.N = c.N + 1
}

func main() {
    var c = Counter(0)
    c.Bump()
    // A lambda parameter is reassignable only when declared `var`.
    val clamp = (var n int) int => {
        if (n < 0) {
            n = 0
        }
        n
    }
    Println(c.N, clamp(-3))
}

Pattern Matching with Filters (Guards) Example

package main

import . "martianoff/gala/collection_immutable"

struct Person(Name string, Age int)

func main() {
    val people = ArrayOf(
        Person("Alice", 25),
        Person("Bob", 15),
        Person("Charlie", 70),
    )

    people.ForEach((p) => {
        val status = p match {
            case Person(name, age) if age < 18 => s"$name is a minor"
            case Person(name, age) if age > 65 => s"$name is a senior"
            case Person(name, _)               => s"$name is an adult"
            case _                             => "Unknown"
        }
        Println(status)
    })
}

See also: Language Reference - Pattern Matching Filters

Alternative Patterns Example

package main

sealed type Event {
    case Click(X int, Y int)
    case Tap(X int, Y int)
    case Key(Code int)
    case Quit()
}

func describe(e Event) string = e match {
    case Click(_, _) | Tap(_, _) => "pointer"
    case Key(9 | 13 | 32)        => "whitespace key"
    case Key(_)                  => "other key"
    case Quit()                  => "quit"
}

func main() {
    Println(describe(Tap(1, 2)))
    Println(describe(Key(13)))
    Println(describe(Key(65)))
    Println(describe(Quit()))
}

Named Sub-Patterns Example

package main

// Named sub-patterns: `Field = p` matches p against the field of that name.
// Fields left out match anything.

sealed type Shape {
    case Circle(Radius int)
    case Rect(Width int, Height int)
}

struct Point(X int, Y int)

func height(s Shape) int = s match {
    case Circle(Radius = r)          => 2 * r
    case Rect(Height = h, Width = _) => h
}

func flat(s Shape) bool = s match {
    case Rect(Height = 0) => true
    case _                => false
}

func square(s Shape) string = s match {
    case Rect(1, Height = 1)           => "unit square"
    case Rect(w, Height = h) if w == h => "square"
    case Rect(_, _)                    => "rect"
    case Circle(_)                     => "circle"
}

func axis(p Point) string = p match {
    case Point(Y = 0) => "on the x axis"
    case Point(X = 0) => "on the y axis"
    case _            => s"(${p.X}, ${p.Y})"
}

func main() {
    Println(height(Rect(1, 2)), height(Circle(3)))
    Println(flat(Rect(5, 0)), flat(Rect(0, 5)))
    Println(square(Rect(1, 1)), square(Rect(2, 2)), square(Rect(2, 3)), square(Circle(1)))
    Println(axis(Point(4, 0)), axis(Point(0, 4)), axis(Point(1, 2)))
}

Pattern Matching with Extractors Example

package main


type Even struct {}
func (e Even) Unapply(i int) Option[int] = if (i % 2 == 0) Some(i) else None[int]()

func main() {
    val number = 42

    val description = number match {
        case Even(n) => s"$n is an even number"
        case _       => s"$number is odd"
    }

    Println(description)

    // Nested patterns
    val opt = Some(10)
    opt match {
        case Some(Even(n)) => Println("Found some even number", n)
        case Some(n)       => Println("Found some odd number", n)
        case None()        => Println("Nothing found")
        case _             => Println("Other")
    }
}

See also: Language Reference - Extractors and Unapply

Sealed View Example

A type with MatchView() S, S sealed, is matched with S’s variants:

package main

sealed type Level {
    case Low()
    case Mid(N int)
    case High()
}

struct Reading(v int)

func (r Reading) MatchView() Level = if (r.v < 10) Low() else if (r.v < 100) Mid(r.v) else High()

func level(r Reading) string = r match {
    case Low() | High() => "edge"
    case Mid(n)         => s"mid $n"
}

func main() {
    Println(level(Reading(1)), level(Reading(42)))
}

See also: Language Reference - Sealed Views

Equality Compares by Value

a == b calls a.Equal(b) on structs, sealed types, tuples, Option and the collections, so values built separately with the same contents are ==. Types made of primitives only keep Go’s ==, which gives the same answer.

package main

import . "martianoff/gala/collection_immutable"

struct Route(name string, stops List[string])

val Home = ListOf("home")

func where(stops List[string]) string = stops match {
    case Home => "home"
    case _    => "away"
}

func main() {
    Println(ListOf(1, 2) == ListOf(2).Prepend(1))                  // true
    Println(Route("r", ListOf("a")) == Route("r", ListOf("a")))    // true
    Println(HashSetOf(ArrayOf(1, 2), ArrayOf(1).Append(2)).Size()) // 1
    Println(where(ListOf("home")))                                 // home
}

Matching Against Constants (Stable Identifiers)

A capitalized name that refers to a value in scope compares with that value instead of binding a new variable. A lowercase name never compares with a value: it binds (unless it names a variant of the matched sealed type or a zero-field extractor), so compare against a lowercase value with a guard.

package main

type Environment string

val Development Environment = "development"
val Production Environment = "production"
val Answer = 42

func describe(env Environment) string = env match {
    case Development => "dev"
    case Production  => "prod"
    case _           => s"other: $env"
}

func probe(o Option[int]) string = o match {
    case Some(Answer) => "the answer"
    case Some(n)      => s"some $n"
    case _            => "none"
}

func main() {
    Println(describe(Production))                  // prod
    Println(probe(Some(42)))                       // the answer
    val limit = 10
    Println(10 match {
        case n if n == limit => "at the limit"
        case n               => s"bound $n"
    })                                             // at the limit
}

See also: Language Reference - Stable Identifiers

Type-Based Pattern Matching Example

package main


func main() {
    val x any = "hello"

    val res = x match {
        case s: string => s"string: $s"
        case i: int    => s"int: $i"
        case _         => "unknown"
    }

    Println(res)
}

Generic Wildcard Pattern Matching Example

package main


type Wrap[T any] struct { Value T }
func (w Wrap[T]) GetValue() any = w.Value

func main() {
    val w = Wrap[string](Value = "hello")
    val res = w match {
        case w1: Wrap[_] => s"Matched Wrap[_]: ${w1.GetValue()}"
        case _ => "Other"
    }
    Println(res)
}

Generic Type Pattern Matching Example

package main


// Define a generic type
type Wrap[T any] struct {
    Value T
}

// Define an extractor object
type Wrapper struct {}
func (w Wrapper) Apply[T any](v T) Wrap[T] = Wrap[T](Value = v)
func (w Wrapper) Unapply(o any) Option[any] = o match {
    case wi: Wrap[_] => Some[any](wi.Value)
    case _           => None[any]()
}

func main() {
    // 1. Matching specific generic type
    val w1 = Wrap[int](Value = 42)
    val res1 = w1 match {
        case w: Wrap[int] => s"Matched Wrap[int]: ${w.Value}"
        case w: Wrap[string] => "Matched Wrap[string]: " + w.Value
        case _ => "Other"
    }
    Println(res1)

    // 2. Matching wildcard generic type
    val w2 = Wrap[string](Value = "hello")
    val res2 = w2 match {
        case w: Wrap[_] => s"Matched Wrap[_]: ${w.Value}"
        case _          => "Other"
    }
    Println(res2)

    // 3. Using the generic extractor
    val w3 = Wrapper("GALA")
    val res3 = w3 match {
        case Wrapper(s: string) => "Extracted string: " + s
        case Wrapper(i: int) => s"Extracted int: $i"
        case _ => "Other"
    }
    Println(res3)
}

Interface Example

package main


type Shaper interface {
    Area() float64
}

struct Rect(width float64, height float64)
func (r Rect) Area() float64 = r.width * r.height

struct Circle(radius float64)
func (c Circle) Area() float64 = 3.14159 * c.radius * c.radius

func main() {
    val r = Rect(10.0, 5.0)
    val c = Circle(10.0)

    val s1 Shaper = r
    val s2 Shaper = c

    Println("Area 1:", s1.Area())
    Println("Area 2:", s2.Area())
}

See also: Language Reference - Interfaces

Interface Literals

An interface literal builds an interface value from one function per method, without declaring a struct. Tests use it to fake dependencies.

package main

type Clock interface {
    Now() int
    Zone() string
}

struct Event(Name string, clock Clock)

func (e Event) Stamp() string = s"${e.Name} at ${e.clock.Now()} ${e.clock.Zone()}"

func main() {
    val fixed = Clock(Now = () => 12, Zone = () => "UTC")
    Println(Event("deploy", fixed).Stamp())   // deploy at 12 UTC
}

See also: Language Reference - Interface Literals and the Dependency Injection guide

Apply Method Example

package main


type Adder struct { Delta int }
func (a Adder) Apply(x int) int = x + a.Delta

type Multiply struct {}
func (m Multiply) Apply(x int, y int) int = x * y

func main() {
    val add5 = Adder(5)
    val res1 = add5(10) // 15

    val res2 = Multiply(3, 4) // 12

    Println(res1, res2)
}

Struct Fields Declared Immutable[T]

A shorthand field declared Immutable[T] is the same field as one declared T: it takes a T however it is supplied, and reads as a T.

package main

struct Counter(Label string, Hits Immutable[int64] = 1)

func main() {
    val named = Counter(Hits = 3, Label = "named")
    val copied = named.Copy(Hits = 4)
    Println(s"${named.Hits} ${copied.Hits} ${Counter("d").Hits}")   // 3 4 1
}

Non-null Pointers: Ptr and OptionPtr

&x is a Ptr[T], a pointer that is never nil; a pointer that may be absent is an OptionPtr[T]; a Go *T becomes one or the other once, at the boundary.

package main

struct Node(Value int, Next OptionPtr[Node])

func (n Ptr[Node]) Sum() int = n.Value + n.Next.Map((next) => next.Sum()).GetOrElse(0)

// A Go-style lookup: nil when nothing is found.
func find(found bool, n *Node) *Node = if (found) n else nil

func main() {
    val tail = &Node{Value: 3}
    val head = &Node(1, SomePtr(&Node(2, SomePtr(tail))))
    Println(head.Sum())                                  // 6
    Println(RequirePtr(find(true, head)).Value)          // 1
    Println(OptionPtrOf(find(false, head)).IsEmpty())    // true
}

Using External Libraries

GALA allows you to organize your code into multiple packages and import them as needed.

mathlib/math.gala

package mathlib

func Add(a int, b int) int = a + b

main.gala

package main

import "martianoff/gala/examples/mathlib"

func main() {
    val sum = mathlib.Add(10, 20)
    Println("Sum is", sum)
}

You can also use dot imports to bring symbols into the current namespace:

import . "martianoff/gala/examples/mathlib"

func main() {
    val sum = Add(10, 20)
    Println("Sum is", sum)
}

See also: Language Reference - GALA Packages, Dependency Management

Advanced Type Inference Example

GALA infers the type arguments of generic function calls from their arguments. This enables features like automatic unwrapping of Immutable values even when they are returned from generic functions.

package main


func identity[T any](x T) T = x
func getImm[T any](x T) Immutable[T] = NewImmutable(x)

func main() {
    // getImm(42) is an Immutable[int], which GALA then automatically unwraps to int
    var x = identity(getImm(42))
    Println(s"x: $x")
}

Partial Type Arguments

Writing only the leading type arguments of a generic constructor binds those and infers the rest:

package main

struct Pair[A any, B any](First A, Second B)

func main() {
    val p = Pair[int64](1, "one")             // Pair[int64, string]
    val q = Pair[float64](Second = true, First = 2)
    Println(s"${p.First} ${p.Second} ${q.First} ${q.Second}")
}

Type Arguments From the Expected Type

A generic struct construction takes the type arguments its fields leave open from the type it is expected to have:

package main

struct Tag[T any](Name string)    // T is a phantom: no field names it

func label() Tag[int] = Tag("count")

func main() {
    val t Tag[string] = Tag("name")
    Println(s"${label().Name} ${t.Name}")
}

Boolean Exhaustive Match

Boolean pattern matching is exhaustive when both true and false cases are covered:

val desc = flag match {
    case true  => "enabled"
    case false => "disabled"
    // No case _ needed — true/false is exhaustive
}

A tuple arm made only of wildcards and bindings (or nested such tuples) matches every value, so it closes a match too:

func divmod(a int, b int) Tuple3[int, int, string] =
    if (b == 0) (0, 0, "division by zero") else (a / b, a % b, "ok")

val result = divmod(17, 0) match {
    case (q, r, "ok") => s"$q remainder $r"
    case (_, _, err)  => s"Error: $err"   // covers every other Tuple3
}

Void Closures and Lambda Parameter Inference

Functions can accept void closures (no return value), and lambda parameter types can be inferred from context. Prefer omitting parameter types in lambdas — the compiler infers them from the method signature:

val opt = Some(42)

// ForEach takes func(T) — void, no return needed
opt.ForEach((x) => {
    Println(x)
})

// Parameter type inferred from Option[int].Filter's signature
val positive = opt.Filter((x) => x > 0)

// Method type param and lambda param both inferred
val doubled = opt.Map((x) => x * 2)

// FoldLeft accumulator type inferred from zero value
val list = ArrayOf(1, 2, 3)
val sum = list.FoldLeft(0, (acc, x) => acc + x)

// Non-generic wrapper methods also infer lambda param types
val s = S("hello")
val hasVowel = s.Exists((r) => r == 'a' || r == 'e' || r == 'i' || r == 'o' || r == 'u')

// A val of function type types the lambdas passed to it
val apply = (h func(string) string) => h("gala")
val shouted = apply((w) => w + "!")

Collect - Filter and Transform in One Pass

Collect combines Filter and Map into a single operation using partial functions:

package main

import . "martianoff/gala/collection_immutable"

func main() {
    val nums = ArrayOf(1, 2, 3, 4, 5, 6)

    // Collect: filter and transform in one pass
    val evenDoubled = nums.Collect({ case n if n % 2 == 0 => n * 2 })
    Println(evenDoubled)  // Array(4, 8, 12)

    // With sealed type extractors
    val options = ArrayOf(Some(1), None[int](), Some(2), None[int](), Some(3))
    val values = options.Collect({ case Some(v) => v * 10 })
    Println(values)  // Array(10, 20, 30)
}

See also: Immutable Collections

Option.OrElse - Fallback Options

package main

func main() {
    val primary = None[string]()
    val fallback = Some("default")

    val result = primary.OrElse(fallback)   // Some("default")
    Println(result.Get())                    // "default"

    val present = Some("actual")
    val result2 = present.OrElse(fallback)  // Some("actual")
    Println(result2.Get())                   // "actual"
}

Option.Exists and Option.ForAll - Predicates on an Optional Value

Exists is true when the Option holds a value that satisfies the predicate; ForAll is true when it holds no value or one that does.

package main

func main() {
    val age = Some(34)
    val unknown = None[int]()

    Println(age.Exists((a) => a >= 18))      // true
    Println(unknown.Exists((a) => a >= 18))  // false — None never satisfies Exists
    Println(unknown.ForAll((a) => a >= 18))  // true — None always satisfies ForAll
}

Try with Function References

When wrapping a zero-argument function, pass the function reference directly instead of wrapping in a lambda:

package main

import "os"

func main() {
    // Function reference (preferred for zero-arg functions)
    val dir = Try(os.TempDir)

    // Lambda form (use when args needed)
    Try(os.MkdirAll("/tmp/test", 0755))

    dir.OnSuccess((d) => Println(s"Temp dir: $d"))
}

Go Functions That Return Several Results

A Go call returning (T, error) is a Try[T]; (A, B, error) is a Try of a Tuple; (A, B, C) is a Tuple3. Match on them directly — no Try(...) needed:

package main

import (
    "net"
    "strconv"
    "strings"
)

func main() {
    // (int, error) -> Try[int]
    val intResult = strconv.Atoi("42") match {
        case Success(n) => s"Parsed: $n"
        case Failure(err) => s"Error: ${err.Error()}"
    }
    Println(intResult) // Parsed: 42

    // (string, string, error) -> Try[Tuple[string, string]]
    val hostPort = net.SplitHostPort("localhost:8080") match {
        case Success((host, port)) => s"host=$host port=$port"
        case Failure(err) => s"Error: ${err.Error()}"
    }
    Println(hostPort) // host=localhost port=8080

    // (string, string, bool) -> Tuple3[string, string, bool]
    val (key, value, found) = strings.Cut("mode=fast", "=")
    Println(s"$key -> $value ($found)") // mode -> fast (true)
}

See also: Language Reference - Go functions that return several results

Returning Several Results to Go

A function or method that Go calls with several results declares Go’s result list; its body gives their one GALA value, a Try for (T, error) or a Tuple for (A, B). A lambda passed where Go expects such a function works the same way:

package main

import (
    "encoding/json"
    "fmt"
    "strconv"
    "sync"
    . "martianoff/gala/go_interop"
)

struct LineCounter(var Lines int)

// io.Writer
func (c *LineCounter) Write(p []byte) (int, error) {
    c.Lines = c.Lines + 1
    Success(p.Size())
}

struct Celsius(Degrees int)

// json.Marshaler
func (c Celsius) MarshalJSON() ([]byte, error) = Success(ToBytes(s"\"${c.Degrees}C\""))

func divmod(a int, b int) (int, int) = (a / b, a % b)

func main() {
    var counter = LineCounter(0)
    fmt.Fprintln(&counter, "one")
    fmt.Fprintln(&counter, "two")
    Println(counter.Lines) // 2

    Println(json.Marshal(Celsius(21)).Map((b) => b.Size())) // Success(5)

    val (q, r) = divmod(17, 5) // a GALA call of it is a Tuple
    Println(s"$q $r") // 3 2

    // A lambda returning Go's (int, error)
    val port = sync.OnceValues(() => strconv.Atoi("8080"))
    Println(port()) // Success(8080)
}

Output:

2
Success(5)
3 2
Success(8080)

See also: Language Reference - Returning several results to Go

MkString - Joining Collection Elements

package main

import . "martianoff/gala/collection_immutable"

func main() {
    val nums = ArrayOf(1, 2, 3, 4, 5)
    Println(nums.MkString(", "))     // "1, 2, 3, 4, 5"
    Println(nums.MkString(" | "))    // "1 | 2 | 3 | 4 | 5"

    val words = ListOf("hello", "world")
    Println(words.MkString(" "))     // "hello world"

    val empty = EmptyArray[int]()
    Println(empty.MkString(", "))    // ""
}

Sorted API - Sorting Collections

All collection types support Sorted(), SortWith(), and SortBy() for flexible sorting. Sorted, SortWith and SortBy are stable: elements that compare equal keep their original relative order.

package main

import . "martianoff/gala/collection_immutable"

func main() {
    // Array.Sorted - natural ordering
    val arr = ArrayOf(3, 1, 4, 1, 5, 9)
    Println(arr.Sorted())                    // Array(1, 1, 3, 4, 5, 9)

    // Array.SortWith - custom comparator (descending)
    Println(arr.SortWith((a, b) => a > b))   // Array(9, 5, 4, 3, 1, 1)

    // Array.SortBy - sort by key function
    val arr2 = ArrayOf(1, 9, 3, 7, 5)
    Println(arr2.SortBy((x) => x))           // Array(1, 3, 5, 7, 9)

    // List.Sorted
    val list = ListOf("banana", "apple", "cherry")
    Println(list.Sorted())                   // List(apple, banana, cherry)

    // TreeSet.Sorted (already sorted)
    val tree = TreeSetOf(30, 10, 20)
    Println(tree.Sorted())                   // Array(10, 20, 30)

    // HashSet.Sorted
    val set = HashSetOf(5, 3, 1)
    Println(set.Sorted())                    // Array(1, 3, 5)

    // Sorting is stable: equal keys keep their input order
    val words = ArrayOf("bb", "a", "cc", "d")
    Println(words.SortBy((w) => w.Size()))   // Array(a, d, bb, cc)

    // Empty collection
    Println(EmptyArray[int]().Sorted())      // Array()
}

See also: Immutable Collections, Mutable Collections

Type Aliases

Type aliases create alternative names for existing types, useful for re-exporting or shortening names:

package main


type MyString string

func main() {
    val s MyString = "hello"
    Println(s)
}

Output:

hello

An alias can take type parameters; a lambda in its place is typed by the instantiation:

package main

type Conv[A any, B any] func(A) B

struct Step[A any, B any](In A, Run Conv[A, B])

func main() {
    val step = Step(In = 3, Run = (x) => s"<$x>")
    Println(step.Run(step.In))
}

Output:

<3>

A generic alias can fix some of its target’s arguments. Result[int] is Either[AppError, int], so Left and Right take their type arguments from it and a match on it sees Either’s variants:

package main

import . "martianoff/gala/collection_immutable"

struct AppError(Code int)

type Result[T any] Either[AppError, T]
type StrMap[V any] HashMap[string, V]

func parse(s string) Result[int] = if (s == "") Left(AppError(1)) else Right(s.Size())

func describe(r Result[int]) string = r match {
    case Right(n) => s"ok $n"
    case Left(e) => s"error ${e.Code}"
}

func main() {
    Println(describe(parse("four")))
    Println(describe(parse("")))
    val counts StrMap[int] = HashMapOf(("a", 1))
    Println(counts.Put("b", 2).Size())
}

Output:

ok 4
error 1
2

Opaque Types

An opaque type is a distinct type over a scalar: typed IDs and units that cannot be mixed up, with methods, operators, HashMap keys and sorting, and the bare value on the wire:

package main

import (
    . "martianoff/gala/collection_immutable"
    "martianoff/gala/json"
)

opaque type UserID int64
opaque type Millis int64

func (m Millis) Seconds() float64 = float64(m) / 1000.0

struct Visit(User UserID, Took Millis)

func main() {
    val id = UserID(7)
    val took = Millis(1200) + 300
    val names = EmptyHashMap[UserID, string]().Put(id, "ada")
    Println(names.Get(id), took.Seconds(), took > Millis(1000))
    Println(ArrayOf(UserID(3), UserID(1)).Sorted())
    Println(json.Codec[Visit](json.SnakeCase()).Encode(Visit(id, took)).Get())
}

Output:

Some(ada) 1.5 true
Array(1, 3)
{"user":7,"took":1500}

case UserID(n) unwraps an opaque value (n is the int64); on an any subject it first checks the value is a UserID, which a plain int64 is not. A phantom type parameter gives each entity its own ID type:

package main

struct Order(Total int)

opaque type UserID int64
opaque type Id[T any] int64

func describe(id UserID) string = id match {
    case UserID(0) => "nobody"
    case UserID(n) => s"user $n"
    case _ => "unreachable"
}

func kind(v any) string = v match {
    case UserID(n) => s"a UserID $n"
    case n: int64 => s"an int64 $n"
    case _ => "other"
}

func orderTotal(id Id[Order]) int = int(id) * 10

func main() {
    Println(describe(UserID(0)), describe(UserID(7)))
    Println(kind(UserID(5)), kind(int64(5)))
    Println(orderTotal(Id[Order](3)))
}

Output:

nobody user 7
a UserID 5 an int64 5
30

More Examples

You can find more examples in the examples/ directory of the project:


See also: Language Reference Why GALA? Immutable Collections Streams