Stream: announcements

Topic: Iterators


view this post on Zulip Richard Feldman (May 27 2026 at 02:43):

FYI, I recently merged a new thing: the Iter builtin type!

here's how they work on a basic level:

this is a huge milestone in the new compiler because Iter was never implementable in the old Rust compiler; it always crashed that compiler (because of the closure stuff and lambda sets) so Iter marks the first time the new compiler has demonstrated a fix to the type of longstanding lambda set bug that we concluded could not possibly be fixed without rearchitecting...and now it's real! :smiley:

view this post on Zulip Karl (May 27 2026 at 03:00):

I'm happy about this. As someone with a Clojure background it's always bugged me that functions building on map/fold in typed functional languages were tied to List instead of being seq/iterator based.

view this post on Zulip Krzysztof Skowronek (May 28 2026 at 18:54):

That's, but I have a couple questions if you don't mind :)

1) what is the goal with the Skip() case in the iterator? is it for some future optimizations? I am coming from C# where the skip of elements is silent - the Next() call just spins until it can yield the next non-skipped value. This implementation also has to do that to fill the rest part

2) why the until and to (like U8.to(1, 4) have the length as unknown?

3) why are the until and to function repeated for all the types? I though there is something like Num * and Int *, and I would think that you can make this one a generic? I guess you want them as methods inside of the type, and since there is no inheritance, you have to repeat them

4) are there plans for a yield keyword to have the iterator state machine like other languages have? Implementing something like Fibonacci sequence iterator is much easier this way, but it "looks" stateful. On the other hand, loops and variables are already "a thing", so maybe?

5) will there be "effectful iterators"? like File.iter_lines(). That would need the next function to be called next!, which would make writing functions that can take both pure and unpure iterators a bit wonky.

I know this is a lot, and it's not a code review! but I just want to learn :)

view this post on Zulip Richard Feldman (May 28 2026 at 19:13):

thanks for the questions @Krzysztof Skowronek!

1) what is the goal with the Skip() case in the iterator? is it for some future optimizations? I am coming from C# where the skip of elements is silent - the Next() call just spins until it can yield the next non-skipped value. This implementation also has to do that to fill the rest part

yeah it's for performance - things like .drop(1000) can be represented much more efficiently, but also it means keep_if doesn't need recursion, which is especially relevant when we get to deforestation/fusion (e.g. in this paper the section on "No recursion: filter" talks about why Skip is important to have)

view this post on Zulip Richard Feldman (May 28 2026 at 19:13):

2) why the until and to (like U8.to(1, 4) have the length as unknown?

ha, that's an oversight, thanks! I'll populate that in a separate PR.

view this post on Zulip Richard Feldman (May 28 2026 at 19:13):

3) why are the until and to function repeated for all the types? I though there is something like Num * and Int *

There used to be, but in the static dispatch world, that hierarchy is gone (by design), so this is the optimal way to do it.

view this post on Zulip Richard Feldman (May 28 2026 at 19:13):

4) are there plans for a yield keyword

no plans - I'd want to start with a concrete use case (e.g. "I want to build ____ but it is currently impossible")

view this post on Zulip Richard Feldman (May 28 2026 at 19:13):

5) will there be "effectful iterators"?

I think these would actually be called "Streams" - I have some design ideas in this direction, but I don't think it's a high priority right now. :smile:

view this post on Zulip Krzysztof Skowronek (May 28 2026 at 19:21):

Richard Feldman said:

2) why the until and to (like U8.to(1, 4) have the length as unknown?

ha, that's an oversight, thanks! I'll populate that in a separate PR.

glad I could help!

view this post on Zulip Krzysztof Skowronek (May 28 2026 at 19:22):

3) that's a bit of a shame, C# got so much better after they introduced the INumber interface. There is no hierachy at all now?

view this post on Zulip Krzysztof Skowronek (May 28 2026 at 19:24):

5) I guess the streams will also include asyncness in them? I haven't seen anything around async for Roc yet, but I am sure something is coming, right?

view this post on Zulip Richard Feldman (May 28 2026 at 19:34):

C# got so much better after they introduced the INumber interface

:raised: how so? Like what did people use it for?

view this post on Zulip Richard Feldman (May 28 2026 at 19:37):

5) I guess the streams will also include asyncness in them? I haven't seen anything around async for Roc yet, but I am sure something is coming, right?

the plan is to do it like Go: effects can be nonblocking as a behind-the-scenes implementation detail (the platform controls this - we have a proof-of-concept of doing this, but don't have a production platform implementing it yet), but you don't need async or await. So for example, when you call response = Http.get!(...) then it's semantically the equivalent of writing response = http_get(...).await in Rust or response = await http_get(...) in JS/TS, and you don't need to mark the function as async. It's just nonblocking behind the scenes with performance implications only.

view this post on Zulip Krzysztof Skowronek (May 28 2026 at 20:01):

Richard Feldman said:

C# got so much better after they introduced the INumber interface

:raised: how so? Like what did people use it for?

C# doesn't have structural typing or duck typing (usually, it's a long story :) for example the await keyword uses duck typing - you can await anything that has GetAwaiter method, no interface implementation)

The INumber interface was built around a new at a time feature of having static members in interfaces. INumber docs

I guess it solves things that Roc already has solutions for, but before that intetrface having a generic mathematic function wasn't possible:

T CartValue<T>(T[] numbers)
    where T : INumber<T>  // <-- Don't forget the constraint!
{
    var result = T.Zero; // this is a static interface member
    foreach (var i in numbers) result += i; // the + operator is also part of the interface
    return result;
}

There is a whole tree of those interfaces, for integer number, non-negative and so on.

In case of Roc, first example that comes to mind are these iterator ranges - there could have been a generic implementation that you just call from each type, instead of copy-pasting many times. Even the fix for the iterator length is now more involved because of that.

I actually really like that in Roc, but if it is a trade off worth doing - I won't cry because of that

view this post on Zulip Krzysztof Skowronek (May 28 2026 at 20:13):

as for async I was thinking more about paralelism probably

I use F# at work now and I am in awe about how easy it is to handle async/parallel stuff, especially around collectiosn, and lazy collections at that. It has lazy async futures likes Rust, so you can do something like

// this probably won't compile, but you get the idea

Directory.EnumerateFiles("some_path", Options.OnlyDirs) // this is "lazy" iterator
|> Seq.map (fun dir -> async {
    let! zip = zipDir dir //let! is await
    return! uploadAsync zip
})
|> Async.Parallel 5

At work I have a pipeline that reads from an sql cursor in batches, feeds that as async stream/sequence, that get's filtered in smaller batches with an api call, then it gets uploaded one by one (up to 10 at a time) to S3 (there is some funky logic around paths, so it's easier one by one)

the code reads like english - do that, for each batch do that 5 at a time and so on - all with Result based error handling built in

Everything around threadpool, backpressure is also built in - you just set a limit on RAM and that's it.

I've been joking with the team that I will rewrite this service in Roc as soon it comes out :)

view this post on Zulip Richard Feldman (May 28 2026 at 20:18):

yeah regarding this:

T CartValue<T>(T[] numbers)
    where T : INumber<T>  // <-- Don't forget the constraint!
{
    var result = T.Zero; // this is a static interface member
    foreach (var i in numbers) result += i; // the + operator is also part of the interface
    return result;
}

here's how this would look when directly ported to Roc - you don't need the formal interface at all:

cart_value : Iter(num) -> num
    where [
        num.default : () -> num,
        num.plus : num, num -> num,
    ]
cart_value = |numbers| {
    Num : num

    var $result = Num.default()
    for i in numbers {
        $result = $result + i
    }

    $result
}

and you could pass any builtin number to that, as well as any user-defined number that defined both plus and default

view this post on Zulip Richard Feldman (May 28 2026 at 20:18):

that F# example is exactly the type of thing I want to make super nice

view this post on Zulip Richard Feldman (May 28 2026 at 20:19):

I don't think the concurrency stuff is a hard requirement for the 0.1.0 release we want to ship in 2026 though

view this post on Zulip Richard Feldman (May 28 2026 at 20:19):

more like 0.2.0

view this post on Zulip Krzysztof Skowronek (May 28 2026 at 20:21):

Cool, fingers crossed :) that also would it a bit complicated with the platforms so good call

view this post on Zulip Lukas Juhrich (May 29 2026 at 07:27):

Krzysztof Skowronek said:

I use F# at work now and I am in awe about how easy it is to handle async/parallel stuff, especially around collectiosn, and lazy collections at that. It has lazy async futures likes Rust, so you can do something like

// this probably won't compile, but you get the idea

Directory.EnumerateFiles("some_path", Options.OnlyDirs) // this is "lazy" iterator
|> Seq.map (fun dir -> async {
    let! zip = zipDir dir //let! is await
    return! uploadAsync zip
})
|> Async.Parallel 5

Out of curiosity, is there anything specific to F# compared to C# that makes that work especially well? in my mind this kind of thing could be easily done in C# as well using something like

await Directory
    .EnumerateDirectories("some_path")
    .Select(dir => async (CancellationToken ct) =>
    {
        var zip = await dir.ZipAsync(ct);
        await zip.UploadAsync(ct);
    })
    .ParExec(maxConcurrency: 5, cancellationToken);

where .ParExec could be a self-defined extension member on any TIter: IEnumerable<Task<TResult>>.

view this post on Zulip Krzysztof Skowronek (May 29 2026 at 07:31):

You would build more complex flows around https://fsprojects.github.io/FSharp.Control.AsyncSeq/

And yeah, it's pretty much the same, but in C# you don't get the Result out of the box, and you have to make the async functions lazy by hand like you did.

It just goes to show that C# is also a really good language, even getting discriminated unions in the next release

view this post on Zulip Lukas Juhrich (May 29 2026 at 07:40):

Thanks for the clarification. I agree, there have been lots of cool improvements to C# lately and are in planning. Working in roc would still be more fun though ;)

view this post on Zulip Jonathan (Jun 02 2026 at 21:06):

I might have missed it - but is there a collect type function for consuming an iterator into a list or collection? I would have thought there would be something like List.collect()/from_iter() or Iter.to_list() etc. but couldn't see one.

view this post on Zulip Luke Boswell (Jun 02 2026 at 23:11):

Would fold : Iter(a), acc, (acc, a -> acc) -> acc be the solution?

view this post on Zulip Jonathan (Jun 02 2026 at 23:27):

Oh for sure that's what I have currently, but given that there may be a known Iter length to guide an optimisation, or just as shorthand for what feels like a common operation instead of it.fold([], Str.append) (which really isn't bad haha I guess I'm just used to Elixir)

view this post on Zulip Richard Feldman (Jun 03 2026 at 01:13):

oh yeah we should add List.from_iter : Iter(item) -> List(item) - just an oversight!

view this post on Zulip Richard Feldman (Jun 03 2026 at 01:30):

happy to accept a contribution for that :smiley:

view this post on Zulip Richard Feldman (Jun 03 2026 at 01:33):

and yeah it can automatically use List.with_capacity and Iter.len_if_known : Iter(_item) -> [Known(U64), Unknown] (which we should also add - another oversight!)

view this post on Zulip Jonathan (Jun 03 2026 at 20:19):

Richard Feldman said:

happy to accept a contribution for that :smiley:

I would but I'm out for the week! I'll give it a shot when I get back if it's still available.

view this post on Zulip Krzysztof Skowronek (Jun 03 2026 at 21:35):

I would be happy to add this to the builtins :)

so:

does that sound about right? for now I am trying to compile roc on my windows machine which so far is not trivial :)

view this post on Zulip Luke Boswell (Jun 03 2026 at 22:06):

Im interested to know more about the Windows issues your experiencing... I assumed it was just install Zig and your gtg. Is our docs for contributing up to date based on your experience?

view this post on Zulip Krzysztof Skowronek (Jun 03 2026 at 22:11):

no, it's pretty much that, but my windows installation is a bit borked - "just install zig" is enough of a challenge

I had some random dev build laying around that was installed by choco pre-release package, and it was taking precedence over the new install I did with winget (I of course forgot that I played with zig 3 years ago and that I used choco then). zls was a similar story, and then git bash kept crashing when I was setting up commit signing

so yeah, 2 hours to do 10 lines of contribution

view this post on Zulip Jonathan (Jun 03 2026 at 23:07):

Krzysztof Skowronek said:

I would be happy to add this to the builtins :)

so:

does that sound about right? for now I am trying to compile roc on my windows machine which so far is not trivial :)

Would it also make sense to have what Richard suggested with the collecting module defining a from_iter? That way I think(?) you'd also be able to dispatch on the destination type, in case a dict, set, etc wants to define its own from_iter but the caller is generic to the destination? I'm not sure if I'm off base here.

view this post on Zulip Krzysztof Skowronek (Jun 03 2026 at 23:18):

I was trying to create a Fibonacci sequence iterator, but I can't get it to work:

    fib_iter : Iter(U64)
    fib_iter = {
        adv : ((U64, U64) -> Try((U64, Iter(U64)), [NoMore])) # same thing without this type annotation
        adv = |(a, b)| Try.Ok((a, Iter.custom((b, a + b), Unknown, adv)))

        Iter.custom(
            (0.U64, 1.U64),
            Unknown,
            adv,
        )
    }

    for f in fib_iter.take_first(5) {
        dbg (f)
    }

it compiles, but crashes in runtime:

roc.exe .\my.roc
thread 21812 panic: postcheck invariant violated: iterator step type was missing Skip

but it's 1AM for me, so time to go to sleep

view this post on Zulip Richard Feldman (Jun 03 2026 at 23:31):

I'll look into that, thanks for the repro!

view this post on Zulip Luke Boswell (Jun 03 2026 at 23:54):

That fib_iter looks :fire: (if it was working as expected) .. I'm so hyped for Roc

view this post on Zulip Luke Boswell (Jun 03 2026 at 23:55):

I'm really happy about the syntax and how these language features come together to enable something like that

view this post on Zulip Richard Feldman (Jun 04 2026 at 01:09):

@Krzysztof Skowronek yeah, Iter.collect should in theory be:

collect : Iter(item) -> output where [
    output.from_iter : Iter(item) -> output
]
collect = |iter| {
    Output : output

    Output.from_iter(iter)
}

view this post on Zulip Richard Feldman (Jun 04 2026 at 02:58):

fix for the fib iter example: https://github.com/roc-lang/roc/pull/9535

view this post on Zulip Krzysztof Skowronek (Jun 04 2026 at 07:20):

Btw, I read the iterator fusion paper, and I noticed that the current implementations of take_first, skip_first (and my fib iterator) are recursive

I don't claim to fully understand the paper, but one of the topic they repeat over and over is that the steps cannot be recursive if you want to final compiler to eliminate them

Also, if they are recursive, bit not in the tail position, doesn't that impose a limit on the lenght based on the stack depth?

I will get back to this later, now I have to do my banking job, even though this is way more interesting

view this post on Zulip Richard Feldman (Jun 04 2026 at 11:09):

oh yeah good point :smile:

view this post on Zulip Krzysztof Skowronek (Jun 04 2026 at 17:56):

I looked at this again, and in the paper the next function (step in Roc) actually takes the previous "state" as parameter - that's how you can get the next state without recursio

view this post on Zulip Richard Feldman (Jun 04 2026 at 19:21):

hm I haven't thought deeply about this since I looked into it over a year, but I remember needing to be careful about not accidentally storing extra copies of things and therefore making them ineligible for in-place mutation

view this post on Zulip Richard Feldman (Jun 04 2026 at 19:22):

I don't think passing the old state as a parameter to the function would do that, but I'd want to double-check :smile:

view this post on Zulip Richard Feldman (Jun 04 2026 at 19:23):

I want to have some Zig tests that assert uniqueness of various builtin operations, and this would be a good example of why!

view this post on Zulip Krzysztof Skowronek (Jun 04 2026 at 19:25):

I am playing with changing these things into what the paper describes now, but then you have to have a second generic parameter for the state, and I am not sure if it can be made hidden by some type tricks

and I wouldn't be happy if all that went to waste

view this post on Zulip Krzysztof Skowronek (Jun 04 2026 at 19:28):

in terms of in-place mutation, if the state object is hidden from the outside consumers, then it should be fine, since it would only get ref count above 1 if you actually copy the iterator reference - and that should be fine

view this post on Zulip Krzysztof Skowronek (Jun 04 2026 at 19:43):

btw, I noticed this error showing up really often for me:

The name iter is being redeclared in this scope.

The redeclaration is here:
    ÔöîÔöÇ C:\Users\KrzysztofSkowronek\source\roc\src\build\roc\Builtin.roc:429:24
    Ôöé
429 Ôöé             collect = |iter| {
    Ôöé                        ^^^^

But iter was already defined here:
    ÔöîÔöÇ C:\Users\KrzysztofSkowronek\source\roc\src\build\roc\Builtin.roc:411:3
    Ôöé
411 Ôöé         iter = |self| self
    Ôöé

(I don't know why the zig build rebuild-builtins doesn't print utf right :D) the names of lambda parameters are in conflict with functions in the type - is that on purpopse? I guess there is no shadowing in Roc but this is surprising

view this post on Zulip Krzysztof Skowronek (Jun 04 2026 at 20:18):

I will throw out one more thing - in case of known length, shouldn't it go down by 1 on every step?

view this post on Zulip Richard Feldman (Jun 04 2026 at 20:39):

in case of known length, shouldn't it go down by 1 on every step?

ooo, good call!

view this post on Zulip Richard Feldman (Jun 04 2026 at 20:42):

I am playing with changing these things into what the paper describes now, but then you have to have a second generic parameter for the state, and I am not sure if it can be made hidden by some type tricks

can you say more about this? I think I remember looking into this and concluding there was a way to do what we needed to do without introducing a second type param, but that was so long ago I don't remember the details anymore :sweat_smile:

view this post on Zulip Richard Feldman (Jun 04 2026 at 20:42):

as an aside, I appreciate your getting into this! :smiley:

view this post on Zulip Krzysztof Skowronek (Jun 04 2026 at 20:42):

there is this range_done method that returns know(0) - so probably it was the idea from the start

view this post on Zulip Richard Feldman (Jun 04 2026 at 20:42):

I'm really excited about actually implementing fusion, and it helps having someone else focusing on it

view this post on Zulip Krzysztof Skowronek (Jun 04 2026 at 20:45):

Richard Feldman said:

I am playing with changing these things into what the paper describes now, but then you have to have a second generic parameter for the state, and I am not sure if it can be made hidden by some type tricks

can you say more about this? I think I remember looking into this and concluding there was a way to do what we needed to do without introducing a second type param, but that was so long ago I don't remember the details anymore :sweat_smile:

so I ended up with something like this:

    Iter(item, state) :: {
        len_if_known : [Known(U64), Unknown],
        state : state,
        step : state -> [One({ item : item, state : state }), Skip({ state : state }), Done],
    }.{
        # The general unfold: the stream-fusion `Stream` constructor. `advance` maps a
        # seed to either the next element paired with the next seed, or `NoMore`.
        custom : state, [Known(U64), Unknown], (state -> Try((item, state), [NoMore])) -> Iter(item, state)
        custom = |seed, len_if_known, advance| {
            len_if_known,
            state: seed,
            step: |s|
                match advance(s) {
                    Ok((item, next_seed)) => One({ item, state: next_seed })
                    Err(NoMore) => Done
                },
        }

        iter : Iter(item, state) -> Iter(item, state)
        iter = |self| self

        # Take a single step. The `rest` returned reuses the *same* stepper and only
        # swaps in the advanced seed, which is what keeps the producer non-recursive.
        next : Iter(item, state) -> [One({ item : item, rest : Iter(item, state) }), Skip({ rest : Iter(item, state) }), Done]
        next = |iterator| {
            rebuild = |new_state| { len_if_known: match iterator.len_if_known {
                Unknown => Unknown,
                Known(l) => Known(l - 1)
            }, state: new_state, step: iterator.step }

            match (iterator.step)(iterator.state) {
                Done => Done
                Skip({ state }) => Skip({ rest: rebuild(state) })
                One({ item, state }) => One({ item, rest: rebuild(state) })
            }
        }

but at this point I think that the state part needs to be [Done, State(state)] or something like that so that you can return the range_done version

also, my claude is out of tokens for this session so I probably won't get into the zig compiler part today - it's a bit too new to me

view this post on Zulip Krzysztof Skowronek (Jun 04 2026 at 20:46):

the builtin.roc doesn't compile because of all the for loops with this, and I don't know the new design of Roc enough to try to work around the generic param

view this post on Zulip Richard Feldman (Jun 04 2026 at 20:47):

cool, I can look into it tonight

view this post on Zulip Richard Feldman (Jun 04 2026 at 20:47):

and see if I can remember/figure out how to do it without the second type param, assuming it's possible

view this post on Zulip Krzysztof Skowronek (Jun 04 2026 at 20:48):

also, this: match (iterator.step)(iterator.state) { I wanted to replace with match (iterator.state)->iterator.step {; but that didn't work

view this post on Zulip Krzysztof Skowronek (Jun 04 2026 at 20:48):

I can push the branch if you want, but it's not very impressive for sure

view this post on Zulip Richard Feldman (Jun 04 2026 at 20:53):

(iterator.state)->iterator.step - ha, that's cool! I don't think anyone has tried that before :laughing:

view this post on Zulip Richard Feldman (Jun 04 2026 at 20:53):

can you open an issue for it?

view this post on Zulip Richard Feldman (Jun 04 2026 at 20:53):

(iterator.state)->(iterator.step) might work btw

view this post on Zulip Richard Feldman (Jun 04 2026 at 20:53):

(but both should)

view this post on Zulip Krzysztof Skowronek (Jun 04 2026 at 20:57):

Richard Feldman said:

can you open an issue for it?

for (iterator.state)->iterator.step? I guess :) I can try to prepare a more focus repro than the builtins

view this post on Zulip Richard Feldman (Jun 04 2026 at 21:14):

right, I had a chat with Claude about this and got a pretty nice answer - short version is that this should optimize the way we want (once we get rid of the recursion, as you noted earlier) because Roc's lambda sets perform polymorphic defunctionalization, which means our existing use of closure captures to avoid a second type parameter should Just Work once we get the optimization flow implemented

view this post on Zulip Richard Feldman (Jun 04 2026 at 21:16):

longer version, summary from Claude:

Straight answer: with Iter at one type parameter, closure-captured state, and polymorphic defunctionalization, you can reach the same fused result the paper gets. It's not a workaround or a near-miss — it's the same destination by a different route.

Here's the design-level why, in three steps:

  1. The state lives in the lambda set, so one type parameter is genuinely enough. The paper hand-threads a state value of some concrete type per pipeline. You instead capture that state in step's closure, and defunctionalization collects all those captures into the lambda set attached to step's function type — not to Iter's parameter list. So Iter(item) stays single-parameter while the per-stage state is fully represented. The one-param invariant and "carry a state" are not in conflict once the carrier is the lambda set.
  2. Defunctionalization produces exactly the concrete state type the paper assumes. The paper's whole trick is that, for a given pipeline, the existential state resolves to one concrete, finite, flat type, and the stepper is a fixed function over it. Polymorphic defunctionalization at a monomorphic use site gives you precisely that: a straight-line pipeline's captured state becomes a finite tag value of invariant shape, and the consumer loop threads it. You arrive at "fixed stepper over a concrete state type" by computing the closed lambda set instead of by quantifying it away — but the thing you end up with is identical, and you actually know it more statically than the paper does at the equivalent point.
  3. The set of programs that fuse is the same set the paper fuses. Fusion is reachable exactly where the defunctionalized state is finite and non-recursive at the use site — straight-line pipelines. Where an Iter is chosen at runtime (a join, a branch, stored heterogeneously), the lambda set is genuinely recursive/open, there's no single static state, and it won't fuse — but the paper doesn't fuse those either; an existential Stream selected at runtime forces dynamic dispatch in the consumer just the same. So you're not giving anything up relative to the paper by going the defunctionalization route. Same fusable class, same fallback class.

So: one type parameter + closure captures + polymorphic defunctionalization is, at the design level, sufficient to express paper-grade fusion. The capability is in the model you already have; nothing about the single-parameter constraint blocks it.

view this post on Zulip Richard Feldman (Jun 04 2026 at 21:17):

I now vaguely remember reading through the paper and concluding "we don't have to do the separate state parameter for a Roc-specific reason" and in retrospect I think that reason was lambda sets :smile:

view this post on Zulip Krzysztof Skowronek (Jun 04 2026 at 21:22):

so that covers the recursion also I guess

it will be a decent mental exercise to create the iterators like the Fib one though, but if it works it works :D

view this post on Zulip Richard Feldman (Jun 04 2026 at 21:23):

yeah!

view this post on Zulip Richard Feldman (Jun 04 2026 at 21:23):

I really want it to be 1-param haha...worth the extra effort

view this post on Zulip Richard Feldman (Jun 04 2026 at 21:23):

I think the ergonomics difference is huge

view this post on Zulip Krzysztof Skowronek (Jun 04 2026 at 21:23):

I can see why

view this post on Zulip Krzysztof Skowronek (Jun 04 2026 at 21:25):

I'll create the repro for pipe operator, and google like 15 words from the claude response

view this post on Zulip Richard Feldman (Jun 04 2026 at 21:31):

do you want to make a PR for making those other operations non recursive?

view this post on Zulip Krzysztof Skowronek (Jun 04 2026 at 21:32):

I though you just told me that they can stay recursive?

view this post on Zulip Richard Feldman (Jun 04 2026 at 21:32):

no, sorry

view this post on Zulip Richard Feldman (Jun 04 2026 at 21:33):

I meant the above analysis was assuming we'd already gotten rid of the recursion

view this post on Zulip Richard Feldman (Jun 04 2026 at 21:33):

the recursion blocks the optimization no matter what, but it's a separate consideration from the state

view this post on Zulip Richard Feldman (Jun 04 2026 at 21:34):

I think the only thing we're missing before we could actually implement the actual fusion "remove pairs of nested of to/from calls" optimization is doing our own inlining. Right now we just let llvm do that, but we want to inline earlier than that

view this post on Zulip Richard Feldman (Jun 04 2026 at 21:35):

that shouldn't be a big task though, I can try to land that and then we can look into eliminating the call pairs

view this post on Zulip Krzysztof Skowronek (Jun 04 2026 at 21:35):

I am happy to try something, but I don't see how you code that without the state passing and without recursion

view this post on Zulip Richard Feldman (Jun 04 2026 at 21:35):

in theory, since our lambdas are unboxed, llvm should do the test of the optimization for us once we've eliminated the pairs (because at that point our code becomes equivalent to Rust iterators, which already optimize all the way down to the equivalent of for loops)

view this post on Zulip Richard Feldman (Jun 04 2026 at 21:35):

for which one specifically?

view this post on Zulip Krzysztof Skowronek (Jun 04 2026 at 21:36):

Krzysztof Skowronek said:

Richard Feldman said:

I am playing with changing these things into what the paper describes now, but then you have to have a second generic parameter for the state, and I am not sure if it can be made hidden by some type tricks

can you say more about this? I think I remember looking into this and concluding there was a way to do what we needed to do without introducing a second type param, but that was so long ago I don't remember the details anymore :sweat_smile:

so I ended up with something like this:

    Iter(item, state) :: {
        len_if_known : [Known(U64), Unknown],
        state : state,
        step : state -> [One({ item : item, state : state }), Skip({ state : state }), Done],
    }.{
        # The general unfold: the stream-fusion `Stream` constructor. `advance` maps a
        # seed to either the next element paired with the next seed, or `NoMore`.
        custom : state, [Known(U64), Unknown], (state -> Try((item, state), [NoMore])) -> Iter(item, state)
        custom = |seed, len_if_known, advance| {
            len_if_known,
            state: seed,
            step: |s|
                match advance(s) {
                    Ok((item, next_seed)) => One({ item, state: next_seed })
                    Err(NoMore) => Done
                },
        }

        iter : Iter(item, state) -> Iter(item, state)
        iter = |self| self

        # Take a single step. The `rest` returned reuses the *same* stepper and only
        # swaps in the advanced seed, which is what keeps the producer non-recursive.
        next : Iter(item, state) -> [One({ item : item, rest : Iter(item, state) }), Skip({ rest : Iter(item, state) }), Done]
        next = |iterator| {
            rebuild = |new_state| { len_if_known: match iterator.len_if_known {
                Unknown => Unknown,
                Known(l) => Known(l - 1)
            }, state: new_state, step: iterator.step }

            match (iterator.step)(iterator.state) {
                Done => Done
                Skip({ state }) => Skip({ rest: rebuild(state) })
                One({ item, state }) => One({ item, rest: rebuild(state) })
            }
        }

but at this point I think that the state part needs to be [Done, State(state)] or something like that so that you can return the range_done version

also, my claude is out of tokens for this session so I probably won't get into the zig compiler part today - it's a bit too new to me

this

view this post on Zulip Richard Feldman (Jun 04 2026 at 21:37):

ah, ok I can look into it tonight

view this post on Zulip Richard Feldman (Jun 04 2026 at 21:37):

on mobile right now :smile:

view this post on Zulip Krzysztof Skowronek (Jun 04 2026 at 21:38):

to my understanding you can either have each iterator have this state object and next function takes it as input, or you make the next function recursive so the state is "baked in" so you don't need the type param for the state

view this post on Zulip Krzysztof Skowronek (Jun 04 2026 at 21:55):

Krzysztof Skowronek said:

I'll create the repro for pipe operator, and google like 15 words from the claude response

https://github.com/roc-lang/roc/issues/9548 seems like record.some_function needs to wrapped in parenthesis, otherwise the compiler looks for a method with that name on the type (anonymous in this case)

view this post on Zulip Richard Feldman (Jun 04 2026 at 22:00):

oh I see

view this post on Zulip Richard Feldman (Jun 04 2026 at 22:01):

I guess the error message didn't make that clear

view this post on Zulip Richard Feldman (Jun 04 2026 at 22:02):

I guess it's useful to keep as-is

view this post on Zulip Richard Feldman (Jun 04 2026 at 22:02):

bc then you can express more different things compared to if we made it do the same thing with and without the parens

view this post on Zulip Richard Feldman (Jun 04 2026 at 22:02):

but maybe we could improve the error message

view this post on Zulip Krzysztof Skowronek (Jun 04 2026 at 22:08):

open System
let r = {| Number = 1; Double = fun x -> x + x |}

let result = r.Number |> r.Double

printfn "%d" result

this works in F#

view this post on Zulip Krzysztof Skowronek (Jun 04 2026 at 22:11):

also, I think the only case for record.something(some) for conflict is something like

Record :: { field: |x| x}.{
   field = ....
}

which is already illegal

view this post on Zulip Richard Feldman (Jun 05 2026 at 00:43):

ok here's the change: https://github.com/roc-lang/roc/pull/9550

view this post on Zulip Richard Feldman (Jun 05 2026 at 00:43):

I also included your "decrement known count" change and also removed the Skip payload - turns out if we actually used that it would break fusion, so having Skip not have a payload (like it doesn't in the paper) is actually the only way to do it if we want fusion to work :smile:

view this post on Zulip Richard Feldman (Jun 05 2026 at 00:44):

the critical difference is that instead of Iter.custom's "callback" returning another Iter, it just closes over a state and produces another one of those

view this post on Zulip Richard Feldman (Jun 05 2026 at 00:46):

- (state -> Try((item, Iter(item)), [NoMore]))
+ (state -> Try((item, state     ), [NoMore]))

view this post on Zulip Krzysztof Skowronek (Jun 05 2026 at 06:41):

Looks nice! So because of this "defunctionalization" it's ok that Iter.custom calls Iter.custom and take_first calls take_first and so on?

That's the recursion I was talking about

view this post on Zulip Richard Feldman (Jun 05 2026 at 13:12):

ah I thought you were talking about Iter.custom returning Iter

view this post on Zulip Krzysztof Skowronek (Jun 05 2026 at 13:16):

I mean, it's the same thing, different operations

Currently the map will fail on a long enough iterator with attack overflow, right?

view this post on Zulip Richard Feldman (Jun 05 2026 at 13:37):

sorry, I don't follow :sweat_smile:

can you give an example?

view this post on Zulip Krzysztof Skowronek (Jun 05 2026 at 13:44):

Isn't it a recursive function in a non-tail position? Or just because it assigned the same function to a field in a record that breaks the recursion?

view this post on Zulip Krzysztof Skowronek (Jun 05 2026 at 13:44):

Hmm, maybe I need more sleep then

view this post on Zulip Krzysztof Skowronek (Jun 05 2026 at 13:47):

Basically, inside of map you return a closure that calls map

view this post on Zulip Krzysztof Skowronek (Jun 05 2026 at 13:47):

That's like lazy recursion I guess

view this post on Zulip Richard Feldman (Jun 05 2026 at 14:10):

I don't think that's specific to iterators, is it? :smile:

view this post on Zulip Richard Feldman (Jun 05 2026 at 14:10):

you can do that in general, and then if the original function gets called, it'll potentially call itself

view this post on Zulip Krzysztof Skowronek (Jun 05 2026 at 14:13):

I'll try to let the fib iterator go on for a while in the evening mapping to 1 or something to calm my worries

view this post on Zulip Richard Feldman (Jun 10 2026 at 17:47):

update on fusion: I learned about a problem with it I wasn't aware of previously, and I actually think we shouldn't do it after all - details/discussion in #ideas > Iterators and fusion proposal @ 💬

view this post on Zulip Richard Feldman (Jun 11 2026 at 23:13):

a big breaking change to the host-platform ABI boundary just landed, so if you're working on a platform you'll want to regenerate glue for it!

benefits:


Last updated: Jun 16 2026 at 16:19 UTC