F# already gives us the vocabulary for missing values and expected failures: Option and Result. Gleam keeps both, with one small ecosystem difference. Many standard library lookups return Result(value, Nil) even when F# would commonly use an option.
Table of contents
Optional Data
Use an option when absence is an ordinary part of the model:
let greet name =
match name with
| Some value -> "Hello, " + value
| None -> "Hello, guest"scratch.fsx
Option belongs to Gleam’s standard library and needs importing. Result, Ok, and Error are built in:
import gleam/int
import gleam/option.{type Option, None, Some}
fn greet(name: Option(String)) -> String {
case name {
Some(value) -> "Hello, " <> value
None -> "Hello, guest"
}
}src/learning_gleam.gleam
There isn’t an implicit null hiding inside String. If a name may be absent, put that possibility in its type.
Give Failures Useful Names
A quantity can fail because it isn’t an integer or because it isn’t positive. Those are different problems, so give them different variants:
type QuantityError = NotAnInteger | NotPositive
let parseQuantity (text: string) =
match System.Int32.TryParse text with
| true, value when value > 0 -> Ok value
| true, _ -> Error NotPositive
| false, _ -> Error NotAnIntegerscratch.fsx
Gleam’s int.parse returns Result(Int, Nil). A case expression can turn its unspecific failure into our domain error:
pub type QuantityError {
NotAnInteger
NotPositive
}
fn parse_quantity(text: String) -> Result(Int, QuantityError) {
case int.parse(text) {
Error(Nil) -> Error(NotAnInteger)
Ok(value) if value > 0 -> Ok(value)
Ok(_) -> Error(NotPositive)
}
}src/learning_gleam.gleam
The return type documents both the successful value and the possible errors. These examples express the same validation stages, not identical numeric ranges or parser grammars. F#‘s int is a 32-bit integer, while Gleam’s integer representation depends on its target.
Decide at the Boundary
A user-facing caller can now choose a useful message by matching Ok(quantity), Error(NotAnInteger), or Error(NotPositive). Lower-level code doesn’t need to print an error or invent a fallback quantity.
For independent transformations, result.map changes a successful value, and result.map_error changes the error type. To chain an operation that can itself fail, use result.try. That’s the role filled by Result.bind in F#.
Gleam doesn’t provide try/catch syntax for normal error handling. That doesn’t mean programs can’t crash. panic, failed assertions, and foreign code can still fail at runtime. Keep expected bad input in a Result rather than turning it into a crash.
Try It
Combine the Gleam blocks in src/learning_gleam.gleam and run gleam run:
pub fn main() -> Nil {
assert greet(Some("Ada")) == "Hello, Ada"
assert greet(None) == "Hello, guest"
assert parse_quantity("12") == Ok(12)
assert parse_quantity("0") == Error(NotPositive)
assert parse_quantity("-2") == Error(NotPositive)
assert parse_quantity("twelve") == Error(NotAnInteger)
assert parse_quantity("") == Error(NotAnInteger)
}src/learning_gleam.gleam
The same cases can follow the F# definitions:
assert (greet (Some "Ada") = "Hello, Ada")
assert (greet None = "Hello, guest")
assert (parseQuantity "12" = Ok 12)
assert (parseQuantity "0" = Error NotPositive)
assert (parseQuantity "-2" = Error NotPositive)
assert (parseQuantity "twelve" = Error NotAnInteger)
assert (parseQuantity "" = Error NotAnInteger)scratch.fsx
These assertions check known examples. In application code, don’t use let assert Ok(value) = ... on untrusted input. It crashes on an error rather than handling it.
Further reading: gleam/option and gleam/result.