Match Syntax Proposal
Overview
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.
Mental Model
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
...
}
Syntax Examples
Basic Value Matching
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"
}
}
Union Type Matching (Discriminated Unions)
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
}
}
Literal Matching in Unions
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
}
Destructuring
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
}
}
Design Rationale
Primary Benefits
- Type Safety: The compiler ensures all cases are handled (exhaustiveness checking).
- Expressiveness: Concisely handle complex data structures without nested
ifstatements. - LLM Output Handling: Perfect for processing polymorphic outputs from LLMs (e.g., "Extract this, or return an error").
Syntax Decisions
1. Type Patterns: var: Type
Decision: 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.
* Discarded Alternative: Type(var) (Rust style) - Rejected because it looks like a constructor/function call, and primitives like string are not wrappers in BAML.
2. Literal Matching
Decision: Use direct literals ("abc", 123).
Rationale: Simple and intuitive. No need to wrap them (e.g., string("abc") is redundant).
3. Destructuring
Decision: Use Type { field: pattern }.
Rationale: Consistent with object construction syntax.
Key Features
1. Exhaustiveness Checking
The compiler will error if not all possible cases are covered.
// Error: Missing case for Status.Pending
match (status) {
Status.Active => ...
Status.Inactive => ...
}
2. Guards
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"
}
3. Wildcards
Use _ or a named variable (without type) to catch "everything else".
Advanced Matching
1. Subset Matching
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"
}
}
2. Wildcard Binding
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"
}
Semantics: Value vs Type Patterns
The syntax unifies two kinds of matching:
- Value Patterns: Matching against a specific runtime value (Literals, Enum Members).
- Example:
"abc",123,Status.Active
- Example:
- Type Patterns: Matching against a type variant in a Union.
- Example:
s: string,u: User
- Example:
Mixed Matching Example
type 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
}