Sam Mohr said:
We have
- List.walk
- List.walkWithIndex
- List.walkWithIndexUntil
- List.walkUntil
- List.walkBackwards
- and so on...
here's an idea for how to simplify these: what if we collapsed all of these functions into two?
List.walk :
List elem
state,
(state, elem -> state)
-> state
List.walkWith :
List elem,
{ init : state, direction ? [Forward, Backward] }
(state, elem, U64 -> ([Continue, Stop], state))
-> state
so walkWith always gives you the index (you can _ it if you don't care), always lets you end early, and then lets you specify Forward (the default) or Backward (optionally) if you want that
could also just put all the different options in one place:
List.walkWith :
List elem,
state,
[
Index (state, elem, U64 -> state),
IndexBackwards (state, elem, U64 -> state),
Until (state, elem -> ([Continue, Stop], state)),
BackwardsUntil (state, elem -> ([Continue, Stop], state)),
IndexUntil (state, elem, U64 -> ([Continue, Stop], state)),
IndexBackwardsUntil (state, elem, U64 -> ([C, S], state)),
]
-> state
kinda similar to what List.range does
We can't use ? in the API today.
Though maybe we should just fix that
I looked once or twice before and wasnt really sure though
As much as this is a little bit of runtime cost, I think either API would be a maintenance improvement on our end, and a discoverability improvement on the user's end
And generally inlining should fix the runtime cost
I do something like this for my 2d array library https://github.com/mulias/roc-array2d/blob/main/package/Array2D.roc#L440
WalkOptions a : {
direction : [Forwards, Backwards],
orientation ? [Rows, Cols],
start ? Index2D,
}a
walk : Array2D a, state, WalkOptions *, (state, a, Index2D -> state) -> state
walk = \array, startState, options, fn ->
direction = options.direction
{ orientation ? Rows, start ? walkStart array options.direction } = options
walkUntil : Array2D a, state, WalkOptions *, (state, a, Index2D -> [Continue state, Break state]) -> state
Using WalkOptions * in the type sig avoids the ? bug. The direction field is required because it's used to set the default of start to the first/last index, and all the ways I tried to first get the optional direction and then use if for a different optional field were broken.
Not saying you should do that - definitely better to fix optional record fields
an argument in favor of 1 function apiece is that this:
|> List.walkBackwardsUntil initialState \state, elem -> ...
...is nicer to write than:
|> List.walkWith initialState (BackwardsUntil \state, elem ->
...
)
basically always requires parens
where status quo does not
Using
WalkOptions *in the type sig avoids the?bug.
That's a really cool fix. Make it have specialization to help roc generate correctly. Maybe we can somehow force all records with ? to have a secret type variable and that will just fix our issues?
Like somehow we need roc to understand it needs to specialize the type
Richard Feldman said:
an argument in favor of 1 function apiece is that this:
|> List.walkBackwardsUntil initialState \state, elem -> ......is nicer to write than:
|> List.walkWith initialState (BackwardsUntil \state, elem -> ... )
Not only this, but the formatter is gonna put a bunch of newlines in there.
the formatter is gonna put a bunch of newlines in there.
That's something that's under our control :P
I think walkTry is a useful member of the family, but it doesn't have backwards and indexed variants at the moment
Trying to combine many things into walkWith makes it more complicated in my opinion. With separate functions, you can have a single example per function in the docs. With walkWith you'd have to provide a bunch, it seems like that would result in significant cognitive load. I already think walk is not easy to use.
One half-way option is to always include the index. You can use _ to ignore it if you don't need it. After using the function a heap of times it will just be normal to have it there, and less confusing.
This is another case where iterators have an advantage cause you can decompose the pieces better.
List.toIter list
|> Iter.reverse
|> Iter.enumerate
|> Iter.walk state \state, (elem, i) ->
You still need walk, walkTry and walkUntil, but it is a way simpler mental model
kinda, but another way to look at it is that the current design offers a convenient one-function alternative to iterators :big_smile:
List.toIter list
|> Iter.reverse
|> Iter.enumerate
|> Iter.walk state \state, (elem, i) ->
List.walkBackwardsWithIndex state \state, (elem, i) ->
That's not true though
It only does so for lists
As discussed elsewhere, there are other places where iterators would be nice.
(deleted)
Brendan Hansknecht said:
As discussed elsewhere, there are other places where iterators would be nice.
I agree with this. Richard's example could be updated to any data structure just by updating the first line, like Set.toIter
Yes, across our different data collections, an Iter/Seq/Stream type would be the single place to handle everything. If it compiles efficiently, requires fewer allocations, and is still expressive, I definitely prefer it to needing to learn different APIs for different types.
But I'm a big Rust fan, so I'm biased towards iterators for sure
Last updated: Jun 16 2026 at 16:19 UTC