Sum types are the #1 most-requested feature in the Go Developer Survey, yet Go 1.25 still doesn’t have them. GALA delivers sum types today through sealed types — algebraic data types (ADTs) that define a fixed set of variants, with the compiler enforcing that every match expression handles all of them. If you’ve been looking for golang sum types, discriminated unions, or closed type hierarchies, this is the answer.
An algebraic data type (ADT) is a composite type formed by combining other types. The most common form is the sum type — a type that can be one of several named variants, each carrying different data. Languages like Rust (enum), Scala (sealed trait), and Haskell (data) have had these for years. Go has no native equivalent. GALA fills that gap.
With sealed types you get:
Apply methodsUnapply methods for pattern matchingIsXxx() boolean checks on every instanceA sealed type declaration lists every variant inside a single block. Each case defines a variant with its own fields:
sealed type Shape {
case Circle(Radius float64)
case Rectangle(Width float64, Height float64)
case Point()
}
Variants can carry zero or more fields. Point() above has no fields — it represents a singleton-like value.
When you write a sealed type, the transpiler produces several things automatically.
Parent struct — A single Shape struct that merges all variant fields plus a _variant discriminator. This keeps memory layout flat and allocation-free.
Companion objects — Empty structs Circle{}, Rectangle{}, Point{} that act as factory namespaces.
Apply methods — Each companion gets an Apply method so you can construct variants with function-call syntax:
val c = Circle(3.14)
val r = Rectangle(10.0, 20.0)
val p = Point()
Unapply methods — Each companion gets an Unapply method for use in pattern matching. You never need to call these directly; the match expression uses them behind the scenes.
IsXxx discriminators — Methods on the parent type to test which variant an instance is:
val c = Circle(3.14)
Println(c.IsCircle()) // true
Println(c.IsRectangle()) // false
Copy and Equal — Like all GALA structs, sealed types get Copy() and Equal() for free.
Variants are constructed by calling the companion name as a function. Positional and named arguments both work:
// Positional arguments
val c = Circle(3.14)
val r = Rectangle(10.0, 20.0)
// Named arguments
val r2 = Rectangle(Width = 15.0, Height = 25.0)
Sealed types support type parameters, enabling you to define reusable algebraic structures:
sealed type Result[T any] {
case Ok(Value T)
case Err(Error error)
}
val success = Ok(42)
val failure = Err[int](fmt.Errorf("oops"))
Generic sealed types work exactly like non-generic ones — you get companion objects, Apply/Unapply, IsXxx(), and exhaustive matching, all parameterized by the type argument.
The compiler knows every variant of a sealed type. When you match on all of them, no case _ default is needed:
func area(s Shape) string = s match {
case Circle(r) => f"circle area: ${3.14159 * r * r}%.2f"
case Rectangle(w, h) => f"rect area: ${w * h}%.2f"
case Point() => "point"
}
If you forget a variant, the compiler tells you. This eliminates an entire class of bugs where new variants are added but forgotten in downstream logic.
You can add if conditions to sealed type match branches:
val desc = shape match {
case Circle(r) if r > 100.0 => "large circle"
case Circle(r) => s"circle with radius $r"
case Rectangle(w, h) => f"$w%.0f x $h%.0f"
case Point() => "point"
}
When you only care about a subset of variants, use case _ as a catch-all. The compiler accepts this as a valid exhaustive match:
sealed type Animal {
case Dog(Name string)
case Cat(Name string)
case Bird(Name string)
}
val msg = animal match {
case Dog(name) => "Woof! I'm " + name
case _ => "I'm not a dog"
}
GALA’s standard library defines three core sealed types that you will use constantly:
| Type | Variants | Purpose |
|---|---|---|
Option[T] |
Some(Value T), None() |
Optional values — eliminates nil |
Either[A, B] |
Left(LeftValue A), Right(RightValue B) |
Disjoint union — typed errors |
Try[T] |
Success(Value T), Failure(Err error) |
Failable computations |
All three support Map, FlatMap, GetOrElse, pattern matching, and more. See the Error Handling page for details.
val opt = Some(42)
val msg = opt match {
case Some(v) => s"got $v"
case None() => "empty"
}
| Use sealed types when… | Use interfaces when… |
|---|---|
| You know all variants at compile time | Third parties need to add implementations |
| You want exhaustive matching | You want open extension |
| Variants carry different data shapes | Implementations share a common method set |
| You are modeling state machines, ASTs, or messages | You are defining contracts between packages |
Sealed types and interfaces can coexist. You can define methods on a sealed type that satisfy an interface, giving you both closed variants and polymorphism.