osa1 github about atom

Throwing iterators in Fir

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 looking for contributors

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!


  1. I started this work in one country, and when finished, I was living in another! This PR really felt like an eternity to finish.↩︎

  2. Implementation-wise, it’s closer to Rust than Haskell as we monomorphise.↩︎