← Code Compare

Sum Types & Pattern Matching

Model a value that is one of several shapes and take it apart by structure, not by a chain of type tests. The task is the canonical one: a Shape is either a Circle (radius) or a Rect (width, height), and area matches on the variant to pull out its fields. Watch which languages have real sum types with exhaustiveness checking (guji, OCaml, Rust), which bolt structural match onto classes (Python 3.10+), and which have no native sum type at all and must fake it — Go with an interface, Perl with a tagged array and an if/elsif ladder.

Show: gujiGoOCamlPerlRakuRustPython
guji
enum Shape {
    Circle($radius: Float)
    Rect($width: Float, $height: Float)
}

sub area($s: Shape): Float {
    match $s {
        Circle($r)   { 3.14159 * $r * $r }
        Rect($w, $h) { $w * $h }
    }
}

sub main(): Int {
    @shapes = [Circle(2.0), Rect(3.0, 4.0)]
    for $s in @shapes {
        print(area($s))
    }
    0
}

An enum is guji's sum type: a Shape is one of Circle or Rect, and each variant carries its own typed fields. The only way to read those fields is to match, which binds them positionally — Circle($r) destructures the radius. The compiler checks the match is exhaustive, so adding a third variant without an arm is a compile-time error rather than a silent fall-through.

Go
package main

import "fmt"

type Shape interface{ isShape() }

type Circle struct{ Radius float64 }
type Rect struct{ Width, Height float64 }

func (Circle) isShape() {}
func (Rect) isShape()   {}

func area(s Shape) float64 {
	switch v := s.(type) {
	case Circle:
		return 3.14159 * v.Radius * v.Radius
	case Rect:
		return v.Width * v.Height
	}
	panic("unreachable")
}

func main() {
	for _, s := range []Shape{Circle{2}, Rect{3, 4}} {
		fmt.Println(area(s))
	}
}

Go has no sum type, so the idiom is a sealed interface: an unexported method isShape() that only Circle and Rect implement keeps the set of shapes closed to this package. A type switch (s.(type)) recovers the concrete type and its fields. Crucially the compiler does not check exhaustiveness — a missing case just falls through, which is why the trailing panic is needed to satisfy the return.

OCaml
type shape =
  | Circle of float
  | Rect of float * float

let area = function
  | Circle r -> 3.14159 *. r *. r
  | Rect (w, h) -> w *. h

let () =
  List.iter
    (fun s -> Printf.printf "%g\n" (area s))
    [ Circle 2.0; Rect (3.0, 4.0) ]

A type ... = with | alternatives is a variant (algebraic data type) — OCaml's original sum type, and the construct that popularized pattern matching. let area = function is sugar for a one-argument match, destructuring each constructor's payload directly. The compiler emits a non-exhaustive match warning if a constructor is unhandled, and *. is the distinct float multiply (OCaml never overloads *).

Perl
use strict;
use warnings;

sub area {
    my ($tag, @f) = @{ $_[0] };
    if    ($tag eq 'Circle') { return 3.14159 * $f[0] ** 2 }
    elsif ($tag eq 'Rect')   { return $f[0] * $f[1] }
    else  { die "unknown shape: $tag" }
}

my @shapes = ( ['Circle', 2.0], ['Rect', 3.0, 4.0] );
print area($_), "\n" for @shapes;

Perl has no sum types or structural match, so the closest equivalent is a tagged array: the first element names the variant and the rest are its fields. Dispatch is a hand-written if/elsif ladder on the tag, with die as the catch-all — there is no exhaustiveness check, so a forgotten variant is a runtime error. (Perl's real pattern-matching strength is regular expressions, a different kind of matching.)

Raku
role Shape {}
class Circle does Shape { has $.radius }
class Rect   does Shape { has ($.width, $.height) }

sub area(Shape $s) {
    given $s {
        when Circle { 3.14159 * .radius ** 2 }
        when Rect   { .width * .height }
    }
}

say area($_) for Circle.new(radius => 2), Rect.new(width => 3, height => 4);

Raku models the closed set with a role the variants do, then matches with given/when. when uses smartmatch (~~), so when Circle succeeds by type, and inside a when the topic $_ lets you write .radius with no explicit variable. It is matching by type and condition rather than by destructured fields, and unlike a true ADT there is no compile-time exhaustiveness guarantee.

Rust
enum Shape {
    Circle { radius: f64 },
    Rect { width: f64, height: f64 },
}

fn area(s: &Shape) -> f64 {
    match s {
        Shape::Circle { radius } => 3.14159 * radius * radius,
        Shape::Rect { width, height } => width * height,
    }
}

fn main() {
    let shapes = [
        Shape::Circle { radius: 2.0 },
        Shape::Rect { width: 3.0, height: 4.0 },
    ];
    for s in &shapes {
        println!("{}", area(s));
    }
}

Rust's enum is a true sum type whose variants may carry named fields, and match destructures them by name with { radius } binding shorthand. The compiler enforces exhaustiveness: omit a variant and the program will not compile, which makes adding a Triangle later a guided refactor rather than a lurking bug. Matching on &Shape borrows, so area reads each shape without taking ownership.

Python
from dataclasses import dataclass

@dataclass
class Circle:
    radius: float

@dataclass
class Rect:
    width: float
    height: float

def area(s):
    match s:
        case Circle(radius=r):
            return 3.14159 * r * r
        case Rect(width=w, height=h):
            return w * h

for s in [Circle(2.0), Rect(3.0, 4.0)]:
    print(area(s))

Python 3.10's structural pattern matching (match/case) destructures objects: case Circle(radius=r) matches instances of the Circle dataclass and binds the field to r. The @dataclass decorator generates the __init__ and gives the classes a tidy positional/keyword shape. There is no exhaustiveness check, though — an unmatched value simply falls through every case and area returns None.