beps/docs/proposals/BEP-002-match/ideas/match-syntax.md
This proposal introduces a match expression for BAML, enabling structural pattern matching on data types.
The core idea is to provide a type-safe, expressive way to handle different data shapes, particularly useful for working with union types and structured LLM outputs.
match is an expression that takes a value and compares it against a series of patterns. The first matching pattern determines the result.
It is helpful to think of the variable: Type syntax as syntactic sugar for if statements with instanceof checks.
// This match expression:
match (val) {
s: string => ...
u: User => ...
}
// Is logically equivalent to:
if (val instanceof string) {
let s = val; // narrowed to string
...
} else if (val instanceof User) {
let u = val; // narrowed to User
...
}
let result = match (value) {
Pattern1 => Result1
Pattern2 => Result2
}
enum Status {
Active
Inactive
Pending
}
function StatusMessage(s: Status) -> string {
return match (s) {
Status.Active => "User is active"
Status.Inactive => "User is inactive"
Status.Pending => "User is pending"
}
}
BAML often deals with union types (e.g., string | Image). The syntax uses variable: Type for type patterns, consistent with variable declarations.
class Image {
url string
}
function GetContent(input: string | Image) -> string {
return match (input) {
// Variable binding with Type Assertion
s: string => s
img: Image => img.url
}
}
You can match exact values within a union without wrapping them.
type Result = "success" | int
match (res) {
// Exact match (Literal)
"success" => "Operation succeeded"
// Type match (Catch-all for int)
code: int => "Error code: " + code
}
Match can destructure classes and objects using the existing object syntax.
class User {
name string
age int
}
function Greet(u: User) -> string {
return match (u) {
// Structural match with constant pattern
User { name: "Admin" } => "Welcome, Administrator"
// Structural match with guard
User { name, age } if age < 18 => "Hello, young " + name
// Structural match binding 'name'
User { name } => "Hello, " + name
}
}
if statements.var: TypeDecision: Use variable: Type (e.g., s: string).
Rationale: This aligns with BAML's variable declaration syntax (let s: string = ...) and function arguments (arg: Type). It treats the pattern match as a "conditional declaration" of a variable with a specific type.
Type(var) (Rust style) - Rejected because it looks like a constructor/function call, and primitives like string are not wrappers in BAML.Decision: Use direct literals ("abc", 123).
Rationale: Simple and intuitive. No need to wrap them (e.g., string("abc") is redundant).
Decision: Use Type { field: pattern }.
Rationale: Consistent with object construction syntax.
The compiler will error if not all possible cases are covered.
// Error: Missing case for Status.Pending
match (status) {
Status.Active => ...
Status.Inactive => ...
}
Add conditions to patterns using if.
match (response) {
// Pattern + Guard
s: Success if s.score > 0.9 => "High confidence"
s: Success => "Low confidence"
Failure => "Failed"
}
Use _ or a named variable (without type) to catch "everything else".
match (x) {
1 => "One"
other => "Something else: " + other
}
Since variable: Type is just sugar for if (variable instanceof Type), the type T doesn't have to be a single variant. It can be a subset of the union.
type Primitive = string | int | bool
type Complex = User | Image
type Any = Primitive | Complex
function Handle(val: Any) -> string {
return match (val) {
// Matches if val is string, int, or bool
p: Primitive => "Got a primitive value"
// Matches if val is User or Image
c: Complex => "Got a complex object"
}
}
You can use _ to match without binding a name, or _: Type to match a type without a name.
match (val) {
// Ignore the value, just match the type
_: int => "It's an integer"
// Catch-all wildcard
_ => "Everything else"
}
The syntax unifies two kinds of matching:
"abc", 123, Status.Actives: string, u: Usertype Mixed = "Special" | string | Status
match (val) {
// Value Patterns (Specific)
"Special" => "Got the special string"
Status.Active => "Got active status"
// Type Patterns (General)
// 's' is bound as 'Status' here (narrowed from Status.Active)
s: Status => "Got some other status"
// 'str' is bound as 'string'
str: string => "Got some other string: " + str
}