Modeling with enums¶
This guide shows practical ways to use enums to model real program structure: state machines, commands, error types, and expression trees.
If you haven’t read it yet, start with: Enums.
Coming from TS/JS?
If you’re used to discriminated unions ({ kind: "A", ... } | { kind: "B", ... }), Incan enums play the same role:
a closed set of variants with typed payloads, and match for case handling.
Example:
=== Typescript
```typescript
type Event =
| { kind: "click"; x: number; y: number }
| { kind: "key"; key: string };
function handle(e: Event) {
switch (e.kind) {
case "click": return `${e.x},${e.y}`;
case "key": return e.key;
}
}
```
===Incan
```incan
enum Event:
Click(x: int, y: int)
Key(key: str)
def handle(e: Event) -> str:
match e:
Click(x, y) => return f"{x},{y}"
Key(key) => return key
```
Pattern 1: State machines¶
Use this when a value progresses through a closed set of states and you want transitions to be explicit and checked.
enum ConnectionState:
Disconnected
Connecting(str) # URL being connected to
Connected(Connection)
Error(str)
def handle_state(state: ConnectionState) -> ConnectionState:
match state:
Disconnected =>
return ConnectionState.Connecting("https://api.example.com")
Connecting(url) =>
match try_connect(url):
Ok(conn) => return ConnectionState.Connected(conn)
Err(e) => return ConnectionState.Error(e)
Connected(_) =>
# Stay connected
return state
Error(msg) =>
println(f"Error: {msg}")
return ConnectionState.Disconnected
Tips:
- Prefer representing transitions as
state -> statefunctions (like above). - Avoid “boolean soup” (
is_connected,is_connecting,last_error, …) when the states are mutually exclusive.
Pattern 2: Commands / actions¶
Use this when your program receives a finite set of commands and each command has its own payload.
enum Command:
Create(str, str) # (name, content)
Update(int, str) # (id, new_content)
Delete(int) # id
List
def execute(cmd: Command) -> Result[str, str]:
match cmd:
Create(name, content) =>
return create_item(name, content)
Update(id, content) =>
return update_item(id, content)
Delete(id) =>
return delete_item(id)
List =>
return Ok(list_items())
Tips:
- Keep the payload minimal; prefer IDs over large embedded objects if you can look them up.
- If the set of commands is open-ended (plugins), consider a trait-based approach instead.
See: Traits as language hooks (open-ended “interfaces” via
trait).
Pattern 3: Error hierarchies¶
Use this when you want rich, typed errors but still keep exhaustiveness and structure.
enum DatabaseError:
ConnectionFailed(str)
QueryFailed(str, int) # (query, error_code)
NotFound(str) # table/record name
PermissionDenied
enum AppError:
Database(DatabaseError) # Nested enum
Validation(str)
NotAuthenticated
def handle_error(err: AppError) -> str:
match err:
Database(db_err) =>
match db_err:
ConnectionFailed(host) => return f"Can't reach {host}"
QueryFailed(q, code) => return f"Query error {code}: {q}"
NotFound(name) => return f"Not found: {name}"
PermissionDenied => return "Access denied"
Validation(msg) => return f"Invalid: {msg}"
NotAuthenticated => return "Please log in"
Tips:
- Keep “leaf” errors close to the layer that produces them (e.g. database layer).
- Wrap/translate into an app-level enum at boundaries so the rest of the app doesn’t depend on lower-level details.
Pattern 4: Expression trees (ASTs)¶
Use this when you want to represent recursive structure (expressions, queries, filters) and interpret/transform it.
enum Expr:
Number(int)
Add(Expr, Expr)
Mul(Expr, Expr)
Neg(Expr)
def eval(expr: Expr) -> int:
match expr:
Number(n) => return n
Add(a, b) => return eval(a) + eval(b)
Mul(a, b) => return eval(a) * eval(b)
Neg(e) => return -eval(e)
# (3 + 4) * -2 = -14
expr = Expr.Mul(
Expr.Add(Expr.Number(3), Expr.Number(4)),
Expr.Neg(Expr.Number(2)),
)
result = eval(expr) # -14
Tips:
- Prefer small, composable constructors.
- Use helper functions to build trees if you want a cleaner “builder” API.