April 17, 2025 - Tagged as: en, plt, fir.
Recently I’ve been working on extending Fir’s Iterator
trait to allow iterators to throw exceptions.
It took a few months of work, because we needed multiple parameter traits for it to work, which took a few months of hacking1 to implement.
Then there was a lot of bug fixing and experimentation, but it finally works, and I’m excited to share what you can do with Fir iterators today.
As usual, link to the online interpreter with all of the code in this post is at the end.
Before starting, I recommend reading the previous post. It’s quite short and it explains the basics of error handling in Fir.
Previous post did not talk about traits at all, so in short, traits in Fir is the same feature as Rust’s traits and Haskell’s typeclasses2.
The Iterator
trait in Fir is also the same as the trait with the same name in Rust, and it’s used the same way, in for
loops.
Here’s a simple example of what you can do with iterators:
sum(nums: Vec[U32]): U32
let result: U32 = 0
for i: U32 in nums.iter():
result += i
result
The Vec.iter
method returns an iterator that returns the next element every time its next
method is called. for
loop implicitly calls the next
method to get the next element, until the next
method returns Option.None
.
Similar to Rust’s Iterator
, Fir’s Iterator
trait also comes with a map
method that allows mapping iterated elements:
parseSum(nums: Vec[Str]): U32
let result: U32 = 0
for i: U32 in nums.iter().map(parseU32):
result += i
result
parseU32(s: Str): U32
if s.len() == 0:
panic("Empty input")
let result: U32 = 0
for c: Char in s.chars():
if c < '0' || c > '9':
panic("Invalid digit")
let digit = c.asU32() - '0'.asU32()
result *= 10
result += digit
result
This version takes a Vec[Str]
as argument, and parses the elements as integers.
The problem with this version is that it panics on unexpected cases: invalid digits and empty input, and it ignores overflows.
Until now, there wasn’t a convenient way to use the Iterator
API and for
loops to do this kind of thing, while also propagating exceptions to the call site of the for
loop, or to the loop variable. But now we can do this: (parseU32Exn
is from the previous post)
parseSum(nums: Vec[Str]): [Overflow, EmptyInput, InvalidDigit, ..errs] U32
let result: U32 = 0
for i: U32 in nums.iter().map(parseU32Exn):
result += i
result
Errors that parseU32Exn
can throw are now implicitly thrown from the for
loop and reflected in the function’s type.
This new Iterator
API is flexible enough to allow handling some (or all) of the exceptions thrown by a previous iterator. For example, here’s how we can handle InvalidDigit
exceptions and yield 0
instead:
parseSumHandleInvalidDigits(nums: Vec[Str]): [Overflow, EmptyInput, ..errs] U32
let result: U32 = 0
for i: U32 in nums.iter().map(parseU32Exn).mapResult(handleInvalidDigit):
result += i
result
handleInvalidDigit(parseResult: Result[[InvalidDigit, ..errs], Option[U32]]): [..errs] Option[U32]
match parseResult:
Result.Ok(result): result
Result.Err(~InvalidDigit): Option.Some(0u32)
Result.Err(other): throw(other)
InvalidDigit
is no longer in the exception type of the function because mapResult(handleInvalidDigit)
handles them.
We can also convert exceptions thrown by an iterator to Result
values:
parseSumHandleInvalidDigitsLogRest(nums: Vec[Str]): U32
let result: U32 = 0
for i: Result[[Overflow, EmptyInput], U32] in \
nums.iter().map(parseU32Exn).mapResult(handleInvalidDigit).try():
match i:
Result.Err(~Overflow): printStr("Overflow")
Result.Err(~EmptyInput): printStr("Empty input")
Result.Ok(i): result += i
result
This function no longer has an exception type, because exceptions thrown by the iterator are passed to the loop variable.
In summary, we started with an iterator that doesn’t throw (nums.iter()
), mapped it with a function that throws (map(parseU32Exn)
), which made the for
loop propagate the exceptions thrown by the map function. We then handled one of the exceptions (mapResult(handleInvalidDigit)
), and finally, we handled all of the exceptions and started passing a Result
value to the loop variable (try()
).
The function’s exception type was updated each time to reflect the exceptions thrown by the function.
Once we had multiple parameter traits (which are important even without exceptions, and something we were going to implement anyway), no language features were needed specifically for the throwing iterators API that composes. Changes in the for
loop type checking were necessary to allow throwing iterators in for
loops. Composing iterators like iter().map(...).mapResult(...).try()
in the examples above did not require any changes to the trait system or exceptions.
This demonstrates that Fir traits and exceptions work nicely together.
You can try the code in this blog post in your browser.
I’m planning a blog post on my vision of Fir, why I think it matters, and a roadmap, but if you already like what you see, know a thing or two about implementing programming languages, and have the time to energy to contribute to a new language, please don’t hesitate to reach out!