Stream: API design

Topic: should `walk` take the state as the first argument?


view this post on Zulip Richard Feldman (Jan 03 2024 at 16:06):

In our Rust glue we have a bunch of places where we do something like this:

|> \b -> List.walkWithIndex tags b generateEnumTags

if List.walk (and the WithIndex variants etc) took the state as the first argument, instead of the list as the first argument, then this could have just been:

|> List.walkWithIndex tags generateEnumTags

view this post on Zulip Richard Feldman (Jan 03 2024 at 16:07):

it seems like this comes up pretty often, and in retrospect state, ... -> state fits the general pattern of pipeline-friendliness

view this post on Zulip Richard Feldman (Jan 03 2024 at 16:07):

(of having the first argument be the same type as the return value)

view this post on Zulip Richard Feldman (Jan 03 2024 at 16:08):

does anyone have thoughts one way or the other?

view this post on Zulip Zeljko Nesic (Jan 03 2024 at 16:21):

Usually my states for walking/folding are in some zero/mempty state and as I walk trough the list I build out state. Hence moslty I construct that initial state by hand and pass in the list. I haven't reused the state after the fold, just extract the final result out of it.

view this post on Zulip Richard Feldman (Jan 03 2024 at 16:36):

hm, but in those cases do you build up the list using pipelines? :thinking:

view this post on Zulip Richard Feldman (Jan 03 2024 at 16:36):

(or in other cases)

view this post on Zulip Richard Feldman (Jan 03 2024 at 16:36):

because if not, then argument order doesn't matter either way in those situations :big_smile:

view this post on Zulip Johan Lövgren (Jan 03 2024 at 16:55):

Seems nice. I also like that it reads better. state |> List.walk list func reads like “walk the list”. With the current version it reads like “walk the state”

view this post on Zulip Johan Lövgren (Jan 03 2024 at 16:59):

On the other hand I often construct the list to walk via some series of pipelineing. Then I would need to define an intermediate variable for list. Though that might be fine in practice? Might even be good practice to separate the construction of the list and the consumption of the list, explicitly

view this post on Zulip Richard Feldman (Jan 03 2024 at 17:10):

do you have some examples of that?

view this post on Zulip Richard Feldman (Jan 03 2024 at 17:10):

constructing the list in a pipeline and then walking it at the end, I mean

view this post on Zulip Anton (Jan 03 2024 at 17:14):

does anyone have thoughts one way or the other?

I do think it would surprise the user, the collection is used as the first argument everywhere except here (if it was changed).

view this post on Zulip Johan Lövgren (Jan 03 2024 at 17:14):

Sure, for example parseDraw on line 68 in this AoC solution: https://github.com/Subtlesplendor/roc-aoc-2023/blob/main/day2.roc

view this post on Zulip Johan Lövgren (Jan 03 2024 at 17:15):

Not saying this is necessarily good style though

view this post on Zulip Brendan Hansknecht (Jan 03 2024 at 17:18):

I always end up using those functions with an empty state. So I end up wanting the list first.

view this post on Zulip Brendan Hansknecht (Jan 03 2024 at 17:18):

Cause it is build up the list with some transforms and then pipe it into walk

view this post on Zulip Brendan Hansknecht (Jan 03 2024 at 17:19):

That said, I think most of the code I have written in roc is not really representative of more complex use cases.

view this post on Zulip Brendan Hansknecht (Jan 03 2024 at 17:19):

I do think it would surprise the user, the collection is used as the first argument everywhere except here (if it was changed).

I also really agree with this one.

view this post on Zulip Brendan Hansknecht (Jan 03 2024 at 17:21):

I think glue is about the worst case for these functions cause it is 100% about building one gigantic global buffer

view this post on Zulip Brendan Hansknecht (Jan 03 2024 at 17:21):

I think most code does more transforms and isn't just a single state accumulator that interacts with many various lists it must walk

view this post on Zulip Brendan Hansknecht (Jan 03 2024 at 17:22):

I would say that for now, it may just be best to define a helper function in glue that handles the arg reordering and enables pipelining.

view this post on Zulip Richard Feldman (Jan 03 2024 at 17:28):

interesting, now I notice this one function could take the place of all those |> List.walk uses in RustGlue.roc:

Str.concatAll : Str, List Str -> Str

view this post on Zulip Richard Feldman (Jan 03 2024 at 17:29):

because in every case they're walking over a list and then concatenating all the strings onto the end of the string they're building up

view this post on Zulip Brendan Hansknecht (Jan 03 2024 at 17:39):

That would make way more temporary allocations?

view this post on Zulip Brendan Hansknecht (Jan 03 2024 at 17:40):

Cause you would do:

buf
|> Str.concatAll (List.map tags generateEnumTags)

while changing generateEnumTags to return a string instead of appending onto a string?

view this post on Zulip Richard Feldman (Jan 03 2024 at 17:55):

for sure, but we already do a ton of that in that script :big_smile:

view this post on Zulip Richard Feldman (Jan 03 2024 at 17:55):

such as every time we append an interpolation

view this post on Zulip Richard Feldman (Jan 03 2024 at 17:56):

perf seems to be fine in that use case, and if we needed to optimize it we could always do it by hand

view this post on Zulip Brendan Hansknecht (Jan 03 2024 at 18:14):

For sure. Just clarifying the difference. The code could be greatly simplified if allocations where ignored and we just always did a bottom up building where there was essentially no buffer except at the top level. All other levels would just be a string interpolation. (I think a decent bit of this already happened. My original code was very allocation adverse, but I think the current code is not so much)

view this post on Zulip Richard Feldman (Jan 03 2024 at 19:11):

yeah glue should end up running with bump allocation for everything, so the cost should end up being a bunch of memcpys and that's about it

view this post on Zulip Brendan Hansknecht (Jan 03 2024 at 19:13):

Memcpys and higher memory usage, but it should never use enough memory for that to matter.

view this post on Zulip Isaac Van Doren (Jan 03 2024 at 21:25):

I almost always start walk with an empty state and often pipe the list in. I agree that having the state first would be surprising to the user and it seems very inconsistent with the rest of the builtins

view this post on Zulip Zeljko Nesic (Jan 04 2024 at 05:01):

Richard Feldman said:

hm, but in those cases do you build up the list using pipelines? :thinking:

Usually lists have to come from somewhere, and that is more often than not some sort of a call to outside world. a.k.a. await

In theory if you are doing two consecutive operations on the list, in order to need list pipelined, you should be able to merge it into one.

I do very often pipeline a resulting state into some sort of finalize function, which extracts relevant data from the state.


Last updated: Jul 06 2025 at 12:14 UTC