Stream: ideas

Topic: simplifying walking API


view this post on Zulip Richard Feldman (Aug 27 2024 at 19:32):

Sam Mohr said:

We have

view this post on Zulip Richard Feldman (Aug 27 2024 at 19:32):

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

view this post on Zulip Richard Feldman (Aug 27 2024 at 19:40):

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

view this post on Zulip Richard Feldman (Aug 27 2024 at 19:43):

kinda similar to what List.range does

view this post on Zulip Brendan Hansknecht (Aug 27 2024 at 19:45):

We can't use ? in the API today.

view this post on Zulip Brendan Hansknecht (Aug 27 2024 at 19:45):

Though maybe we should just fix that

view this post on Zulip Brendan Hansknecht (Aug 27 2024 at 19:45):

I looked once or twice before and wasnt really sure though

view this post on Zulip Sam Mohr (Aug 27 2024 at 20:08):

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

view this post on Zulip Brendan Hansknecht (Aug 27 2024 at 21:38):

And generally inlining should fix the runtime cost

view this post on Zulip Elias Mulhall (Aug 27 2024 at 21:41):

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.

view this post on Zulip Elias Mulhall (Aug 27 2024 at 21:42):

Not saying you should do that - definitely better to fix optional record fields

view this post on Zulip Richard Feldman (Aug 27 2024 at 21:46):

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 ->
        ...
    )

view this post on Zulip Richard Feldman (Aug 27 2024 at 21:46):

basically always requires parens

view this post on Zulip Richard Feldman (Aug 27 2024 at 21:46):

where status quo does not

view this post on Zulip Brendan Hansknecht (Aug 27 2024 at 22:12):

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?

view this post on Zulip Brendan Hansknecht (Aug 27 2024 at 22:12):

Like somehow we need roc to understand it needs to specialize the type

view this post on Zulip Sam Mohr (Aug 28 2024 at 00:11):

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.

view this post on Zulip Joshua Warner (Aug 28 2024 at 02:10):

the formatter is gonna put a bunch of newlines in there.

That's something that's under our control :P

view this post on Zulip Alex Nuttall (Aug 28 2024 at 08:19):

I think walkTry is a useful member of the family, but it doesn't have backwards and indexed variants at the moment

view this post on Zulip Anton (Aug 28 2024 at 08:53):

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.

view this post on Zulip Luke Boswell (Aug 28 2024 at 08:56):

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.

view this post on Zulip Brendan Hansknecht (Aug 28 2024 at 16:00):

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) ->

view this post on Zulip Brendan Hansknecht (Aug 28 2024 at 16:01):

You still need walk, walkTry and walkUntil, but it is a way simpler mental model

view this post on Zulip Richard Feldman (Aug 28 2024 at 16:13):

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) ->

view this post on Zulip Brendan Hansknecht (Aug 28 2024 at 19:09):

That's not true though

view this post on Zulip Brendan Hansknecht (Aug 28 2024 at 19:09):

It only does so for lists

view this post on Zulip Brendan Hansknecht (Aug 28 2024 at 19:10):

As discussed elsewhere, there are other places where iterators would be nice.

view this post on Zulip Brendan Hansknecht (Aug 28 2024 at 19:11):

(deleted)

view this post on Zulip Kilian Vounckx (Aug 28 2024 at 19:12):

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

view this post on Zulip Sam Mohr (Aug 28 2024 at 19:17):

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.

view this post on Zulip Sam Mohr (Aug 28 2024 at 19:18):

But I'm a big Rust fan, so I'm biased towards iterators for sure


Last updated: Jun 16 2026 at 16:19 UTC