Skip to content

Commit fc9c957

Browse files
committed
Update docs for unwrap operators (! ?? ?) and From removal
1 parent a29b6b0 commit fc9c957

6 files changed

Lines changed: 139 additions & 55 deletions

File tree

src/content/docs/getting-started/introduction.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ fn area(s: Shape) -> Float =
6060
6161
// Effect function — can fail, can do I/O
6262
effect fn read_config(path: String) -> Result[String, String] = {
63-
let content = fs.read_text(path)
63+
let content = fs.read_text(path)!
6464
ok(content)
6565
}
6666
```

src/content/docs/guide/error-handling.md

Lines changed: 82 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
---
22
title: Error Handling
3-
description: Result, Option, effect fn auto-propagation, guard, and deriving From for error conversion.
3+
description: Result, Option, unwrap operators (!, ??, ?), effect fn, and guard for safe error handling.
44
---
55

6-
Almide has no exceptions. All errors are values, represented by `Result[T, E]` and `Option[T]`. The `effect fn` system automates error propagation, making error handling both safe and concise.
6+
Almide has no exceptions. All errors are values, represented by `Result[T, E]` and `Option[T]`. Three postfix operators -- `!`, `??`, and `?` -- provide concise, explicit control over unwrapping.
77

88
## Result
99

@@ -50,21 +50,90 @@ let upper = option.map(map.get(config, "name"), (s) => string.to_upper(s))
5050
let result = option.to_result(map.get(config, "name"), "name is required")
5151
```
5252

53-
## effect fn and auto-propagation
53+
## Unwrap operators: `!`, `??`, `?`
5454

55-
The key to ergonomic error handling in Almide is `effect fn`. Inside an `effect fn`, expressions returning `Result[T, E]` are automatically unwrapped:
55+
Almide provides three postfix operators for unwrapping `Result` and `Option` values. These are the primary way to work with fallible values.
56+
57+
### `!` — propagate error
58+
59+
`expr!` unwraps a `Result` or `Option`. If the value is `err(e)` or `none`, the enclosing `effect fn` immediately returns the error. Only valid inside `effect fn`.
5660

5761
```almide
5862
effect fn load_config(path: String) -> Result[String, String] = {
59-
let text = fs.read_text(path) // Result auto-unwrapped; error propagates
60-
let trimmed = string.trim(text) // text is String, not Result
63+
let text = fs.read_text(path)! // unwrap or propagate error
64+
let trimmed = string.trim(text)
6165
ok(trimmed)
6266
}
6367
```
6468

65-
Without `effect fn`, you would need to manually match on every `Result`. The compiler inserts error propagation automatically.
69+
On `Option`:
70+
71+
```almide
72+
effect fn first_name(users: List[User]) -> Result[String, String] = {
73+
let user = list.first(users)! // none becomes err, propagated
74+
ok(user.name)
75+
}
76+
```
77+
78+
### `??` — fallback value
79+
80+
`expr ?? fallback` unwraps with a default. If the value is `err(_)` or `none`, the fallback is used instead. Valid anywhere (not limited to `effect fn`).
81+
82+
```almide
83+
let port = int.parse(port_str) ?? 8080
84+
let name = map.get(env, "USER") ?? "anonymous"
85+
```
86+
87+
### `?` — convert to Option
88+
89+
`expr?` converts a `Result[T, E]` to `Option[T]`, discarding the error. On `Option`, it is a passthrough. Valid anywhere.
90+
91+
```almide
92+
let parsed = int.parse(input)? // Result[Int, String] -> Option[Int]
93+
let found = map.get(config, "key")? // Option[String] -> Option[String] (passthrough)
94+
```
95+
96+
### Summary table
97+
98+
| Operator | On Result | On Option | Valid in |
99+
|----------|-----------|-----------|----------|
100+
| `expr!` | `ok(v)` -> `v`, `err(e)` -> propagate | `some(v)` -> `v`, `none` -> propagate | `effect fn` only |
101+
| `expr ?? fallback` | `ok(v)` -> `v`, `err(_)` -> `fallback` | `some(v)` -> `v`, `none` -> `fallback` | Anywhere |
102+
| `expr?` | `ok(v)` -> `some(v)`, `err(_)` -> `none` | Passthrough | Anywhere |
103+
104+
## effect fn
105+
106+
Functions with side effects use `effect fn`. The `!` operator is the standard way to propagate errors inside an `effect fn`:
107+
108+
```almide
109+
effect fn process(path: String) -> Result[String, String] = {
110+
let text = fs.read_text(path)! // propagate on error
111+
let data = json.parse(text)! // propagate on error
112+
ok(data)
113+
}
114+
```
115+
116+
Without `!`, you would need to manually match on every `Result`.
66117

67-
The rule is simple: if any expression in an `effect fn` body evaluates to `err(e)`, the entire function immediately returns that error.
118+
## Error type conversion with map_err
119+
120+
When different functions return different error types, use `result.map_err` combined with `!` to convert errors:
121+
122+
```almide
123+
type AppError =
124+
| Io(String)
125+
| Parse(String)
126+
127+
effect fn load(path: String) -> Result[Config, AppError] = {
128+
let text = fs.read_text(path)
129+
|> result.map_err(_, (e) => Io(e))!
130+
let raw = json.parse(text)
131+
|> result.map_err(_, (e) => Parse(e))!
132+
ok(parse_config(raw))
133+
}
134+
```
135+
136+
This pattern replaces the former `From` convention with explicit, visible error conversion.
68137

69138
## guard
70139

@@ -88,35 +157,12 @@ effect fn process(path: String) -> Result[Unit, String] = {
88157
println("file not found, skipping")
89158
ok(())
90159
}
91-
let content = fs.read_text(path)
160+
let content = fs.read_text(path)!
92161
println(content)
93162
ok(())
94163
}
95164
```
96165

97-
## Error types with deriving From
98-
99-
For applications with multiple error sources, define a variant error type with `deriving From`:
100-
101-
```almide
102-
type AppError =
103-
| Io(IoError)
104-
| Parse(ParseError)
105-
deriving From
106-
```
107-
108-
`deriving From` generates automatic conversions from each inner error type to the variant. This enables seamless error propagation across different error types:
109-
110-
```almide
111-
effect fn load(path: String) -> Result[Config, AppError] = {
112-
let text = fs.read_text(path) // IoError -> AppError::Io (automatic)
113-
let raw = json.parse(text) // ParseError -> AppError::Parse (automatic)
114-
ok(parse_config(raw))
115-
}
116-
```
117-
118-
Without `deriving From`, you would need to manually wrap each error.
119-
120166
## Three-layer error strategy
121167

122168
| Layer | Mechanism | Use case |
@@ -153,16 +199,16 @@ effect fn create_user(name: String, age: Int) -> Result[User, String] = {
153199
}
154200
```
155201

156-
### unwrap_or for defaults
202+
### `??` for defaults
157203

158204
When a missing value has a sensible default:
159205

160206
```almide
161-
let port = result.unwrap_or(int.parse(port_str), 8080)
162-
let name = option.unwrap_or(map.get(env, "USER"), "anonymous")
207+
let port = int.parse(port_str) ?? 8080
208+
let name = map.get(env, "USER") ?? "anonymous"
163209
```
164210

165-
### and_then for chaining
211+
### flat_map for chaining
166212

167213
When each step can fail and depends on the previous:
168214

src/content/docs/guide/functions.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ Functions with side effects (I/O, network, randomness) must be marked `effect fn
3838

3939
```almide
4040
effect fn save(path: String, content: String) -> Result[Unit, String] = {
41-
fs.write(path, content)
41+
fs.write(path, content)!
4242
ok(())
4343
}
4444
```
@@ -47,7 +47,7 @@ The `effect` modifier enforces a strict boundary:
4747

4848
- Calling an `effect fn` from a non-effect function is a **compile error**
4949
- `effect fn` typically returns `Result[T, E]`
50-
- Inside `effect fn`, `Result` values are automatically unwrapped and errors propagate
50+
- Use the `!` operator to unwrap `Result` values and propagate errors explicitly
5151

5252
```almide
5353
fn pure() -> String =
@@ -57,7 +57,7 @@ effect fn safe() -> Result[String, String] =
5757
fs.read_text("file.txt") // OK: effect fn calling effect fn
5858
```
5959

60-
See [Error Handling](/docs/guide/error-handling/) for auto-propagation details.
60+
See [Error Handling](/docs/guide/error-handling/) for the `!`, `??`, and `?` operators.
6161

6262
## Parameters
6363

@@ -227,5 +227,5 @@ list.get(args, 1) |> match {
227227
## Next steps
228228

229229
- [Control Flow](/docs/guide/control-flow/) — if, for, while, guard
230-
- [Error Handling](/docs/guide/error-handling/) — Result, Option, effect fn propagation
230+
- [Error Handling](/docs/guide/error-handling/) — Result, Option, unwrap operators, effect fn
231231
- [Modules & Imports](/docs/guide/modules/) — organizing code across files

src/content/docs/guide/types.md

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ let b = true or false // true
6363
let c = not true // false
6464
```
6565

66-
There is no `!`, `&&`, or `||`. The compiler rejects them with helpful hints.
66+
There is no `&&` or `||`. Use `and`, `or`, and `not` for boolean logic. (The postfix `!` is the unwrap operator, not boolean negation.)
6767

6868
## Unit
6969

@@ -199,7 +199,7 @@ match int.parse(input) {
199199
}
200200
```
201201

202-
See [Error Handling](/docs/guide/error-handling/) for `effect fn` auto-propagation and `guard`.
202+
See [Error Handling](/docs/guide/error-handling/) for `effect fn`, unwrap operators (`!`, `??`, `?`), and `guard`.
203203

204204
## Record types
205205

@@ -321,17 +321,13 @@ fn apply(f: Fn(Int) -> Int, x: Int) -> Int = f(x)
321321
Types can derive built-in conventions using `:` after the type name:
322322

323323
```almide
324-
type AppError: From =
325-
| Io(IoError)
326-
| Parse(ParseError)
327-
328324
type Color: Eq =
329325
| Red
330326
| Green
331327
| Blue
332328
```
333329

334-
This generates implementations automatically. Available conventions: `Eq`, `Repr`, `Ord`, `Hash`, `Codec`, `From`. See [Error Handling](/docs/guide/error-handling/) and [Protocols](/docs/guide/protocols/) for details.
330+
This generates implementations automatically. Available conventions: `Eq`, `Repr`, `Ord`, `Hash`, `Codec`. See [Protocols](/docs/guide/protocols/) for details.
335331

336332
## Built-in protocols
337333

src/content/docs/reference/cheatsheet.md

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ type Name = Type // type alias
2222
type Handler = (String) -> String // function type alias
2323
type ConfigError =
2424
| Io(IoError) | Parse(ParseError)
25-
deriving From // auto From conversion
2625
```
2726

2827
**Built-in types:** `Int`, `Float`, `String`, `Bool`, `Unit`, `Path`, `List[T]`, `Map[K, V]`, `Set[T]`, `Result[T, E]`, `Option[T]`
@@ -124,9 +123,33 @@ test "description" {
124123

125124
No `print` function. Use `println` for all output.
126125

126+
## Unwrap Operators
127+
128+
```almd
129+
expr! // propagate error (effect fn only)
130+
expr ?? fallback // unwrap with default
131+
expr? // Result -> Option (discard error)
132+
```
133+
134+
```almd
135+
effect fn load(path: String) -> Result[String, String] = {
136+
let text = fs.read_text(path)! // propagate on err
137+
ok(text)
138+
}
139+
let port = int.parse(s) ?? 8080 // fallback
140+
let name = map.get(env, "USER") ?? "anonymous"
141+
let parsed = int.parse(input)? // Option[Int]
142+
```
143+
144+
Error type conversion (replaces `From`):
145+
146+
```almd
147+
fs.read_text(path) |> result.map_err(_, (e) => Io(e))!
148+
```
149+
127150
## Operators (precedence high to low)
128151

129-
`. ()` > `not -` > `^` > `* / %` > `+ -` > `.. ..=` > `== != < > <= >=` > `and` > `or` > `|> >>`
152+
`. () [] ! ?? ?` > `not -` > `^` > `* / %` > `+ -` > `.. ..=` > `== != < > <= >=` > `and` > `or` > `|> >>`
130153

131154
## Stdlib Modules
132155

@@ -139,7 +162,8 @@ No `print` function. Use `println` for all output.
139162
- Newline = statement separator (no semicolons needed)
140163
- `[]` for generics, NOT `<>`
141164
- `effect fn` for side effects, NOT `fn name!()`
142-
- `?` suffix is for Bool predicates only
165+
- `fn name?()` suffix is for Bool predicates only
166+
- Postfix `!` / `??` / `?` are unwrap operators (see above)
143167
- No exceptions -- use `Result[T, E]`
144168
- No null -- use `Option[T]`
145169
- No inheritance, no macros, no operator overloading, no implicit conversions
@@ -166,7 +190,7 @@ No `print` function. Use `println` for all output.
166190
effect fn main(args: List[String]) -> Result[Unit, AppError] = {
167191
let cmd = list.get(args, 1)
168192
match cmd {
169-
some("run") => do_something(),
193+
some("run") => do_something()!,
170194
some(other) => err(UnknownCommand(other)),
171195
none => err(NoCommand),
172196
}

src/content/docs/reference/operators.md

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Operators are listed from highest precedence (binds tightest) to lowest.
99

1010
| Precedence | Operators | Associativity | Description |
1111
|:---:|---|---|---|
12-
| 1 | `. ()` `[]` | Left | Member access, function call, index |
12+
| 1 | `. ()` `[]` `!` `??` `?` | Left | Member access, call, index, unwrap ops |
1313
| 2 | `not` `-` (unary) | Right | Boolean negation, numeric negation |
1414
| 3 | `^` | Right | Exponentiation (power) |
1515
| 4 | `*` `/` `%` | Left | Multiplication, division, modulo |
@@ -22,7 +22,7 @@ Operators are listed from highest precedence (binds tightest) to lowest.
2222

2323
## Detailed Reference
2424

25-
### Member Access and Calls (`. ()` `[]`)
25+
### Member Access, Calls, and Unwrap (`. ()` `[]` `!` `??` `?`)
2626

2727
```almd
2828
user.name // field access
@@ -31,14 +31,32 @@ xs[0] // index read
3131
xs[i] = value // index write (var only)
3232
```
3333

34+
#### Unwrap operators
35+
36+
Three postfix operators for unwrapping `Result` and `Option` values:
37+
38+
```almd
39+
fs.read_text(path)! // unwrap or propagate error (effect fn only)
40+
int.parse(s) ?? 0 // unwrap with fallback value
41+
int.parse(s)? // convert Result to Option (discard error)
42+
```
43+
44+
| Operator | On Result | On Option | Valid in |
45+
|----------|-----------|-----------|----------|
46+
| `expr!` | `ok(v)` -> `v`, `err(e)` -> propagate | `some(v)` -> `v`, `none` -> propagate | `effect fn` only |
47+
| `expr ?? fallback` | `ok(v)` -> `v`, `err(_)` -> `fallback` | `some(v)` -> `v`, `none` -> `fallback` | Anywhere |
48+
| `expr?` | `ok(v)` -> `some(v)`, `err(_)` -> `none` | Passthrough | Anywhere |
49+
50+
See [Error Handling](/docs/guide/error-handling/) for detailed examples.
51+
3452
### Unary Operators (`not`, `-`)
3553

3654
```almd
3755
not active // boolean negation
3856
-x // numeric negation
3957
```
4058

41-
There is no `!` operator. Use `not` for boolean negation.
59+
There is no boolean `!` operator. Use `not` for boolean negation. The postfix `!` is the unwrap/propagate operator.
4260

4361
### Exponentiation (`^`)
4462

0 commit comments

Comments
 (0)