Stream: show and tell

Topic: article about looping in Roc


view this post on Zulip Richard Feldman (Dec 24 2023 at 16:15):

I wrote a first draft of an article about looping in Roc - any feedback welcome!

view this post on Zulip Brendan Hansknecht (Dec 24 2023 at 16:28):

is a tail-recursive function because no code path ends in a call to another function. (They all end in either a recursive call to runLogic itself, or else they return a value instead of calling any function at all, such as the [] -> state branch.)

I think we need a better definition here. Cause it makes it sound like this should be tail recursive (at least if someone is new to tail recursion):

fib = \x ->
    if x <= 1 then
         x
    else
         (fib (x - 1)) + (fib (x - 2))

Also, you can end a tail recursive function by calling another function that returns a value.

view this post on Zulip Richard Feldman (Dec 24 2023 at 16:29):

yeah I originally had a more comprehensive definition but I tried changing it to something that explained why the current example in question was tail-recursive

view this post on Zulip Richard Feldman (Dec 24 2023 at 16:29):

maybe it's worth a longer explanation though

view this post on Zulip Richard Feldman (Dec 24 2023 at 16:30):

the fib example might be too much of a digression, but maybe not

view this post on Zulip Brendan Hansknecht (Dec 24 2023 at 16:48):

Yeah, probably too much of a digression

view this post on Zulip Brendan Hansknecht (Dec 24 2023 at 16:49):

Maybe some different wording could help....

view this post on Zulip Richard Feldman (Dec 24 2023 at 16:52):

or maybe link to a separate article about recursion, which could get into both tail recursion and also modulo cons :thinking:

view this post on Zulip Brendan Hansknecht (Dec 24 2023 at 16:54):

That seems reasonable

view this post on Zulip Brian Carroll (Dec 24 2023 at 18:37):

For the first example, maybe explicitly state that it's C? Does everyone still know it, or do lots of people go straight to higher level stuff these days? I don't know!

view this post on Zulip Brian Carroll (Dec 24 2023 at 18:52):

When we first get to the list walk code, we start seeing arrows pointing both ways. Syntax for lambda, back passing, and pipe. We often get beginner questions about all the arrows! This might be a good place for some links to explain them.

view this post on Zulip Richard Feldman (Dec 24 2023 at 19:00):

could be, although I'm already concerned it's too long :sweat_smile:

view this post on Zulip Brian Carroll (Dec 24 2023 at 19:00):

I'm not sure if it's worth bothering with List.forEach. Feels like thar section is pulling me in two directions at once. I think it would be better to just teach it the more "traditional FP" way and commit to the new concept. Take away the crutch of foreach

view this post on Zulip Brian Carroll (Dec 24 2023 at 19:01):

Richard Feldman said:

could be, although I'm already concerned it's too long :sweat_smile:

Links don't make it longer though!

view this post on Zulip Alexander Pyattaev (Dec 24 2023 at 19:18):

Good work, I was missing that in the tutorial! Some improvement ideas below:

Can you change the walk example to walkUntil? Or write out the version with walkUntil? Right now it reads like switching to walkUntil will suddenly make everything worse, since you mention "tagging", and tagging may be unfamiliar to people from C/C++/Python lands.

Missed opportunities for concurrency section reads kind of "meh", as you are not showing how to actually achieve concurrent sendEmail in roc.

view this post on Zulip Eli Dowling (Dec 24 2023 at 21:38):

I came to comment basically exactly what @Alexander Pyattaev said about the concurrency bit. It feels out of place because it doesn't actually express a solution, just that this issue potentially exists.

Also I'd like to see a link to some more reading.
A link to the list of List.* Functions when it's first brought up that there are lots.

A link to more reading about tail recursion. You don't really explain the ins and outs of how to recognise if a function is tail recursive or not and that's not something people intuitively understand. So a link to a good explainer post elsewhere would be great I think.

Otherwise it's a good little read and I think it'll definitely help answer some of the questions people have on that topic

view this post on Zulip Hannes Nevalainen (Dec 25 2023 at 06:22):

I think it is worth to mention that walk also goes by the name reduce or fold in many other languages. :)

view this post on Zulip Anton (Dec 25 2023 at 10:54):

Similar to earlier comments, I also think it would be good to split this up a bit and use links for the different parts.

view this post on Zulip Anton (Dec 25 2023 at 10:54):

This is no fault of the article, but List.walk really is a lot more complicated than the for loop. It makes me want to have an editor plugin that displays them as for loops. I'm not sure how often List.walk would actually come up in real Roc code though. It's a pain point worth thinking about sometime.

view this post on Zulip Anton (Dec 25 2023 at 10:56):

I think the article would work well with some small exercises for the reader in between but that's also something for the future.

view this post on Zulip Brian Carroll (Dec 25 2023 at 11:53):

I'm not sure how often List.walk would actually come up in real Roc code though

Probably about as often as List.foldl in Elm.
We have 329k lines of Elm code at work and I just searched for occurrences of the common List iteration functions
List.map 2266
List.filter 403
List.filterMap 420
List.foldl 222

EDIT: reduced all the numbers because I was double-counting due to symlinks

view this post on Zulip Anton (Dec 25 2023 at 11:53):

Interesting, thanks @Brian Carroll!

view this post on Zulip Alexander Kiel (Jan 02 2024 at 13:09):

The tail recursive function runLogic contains the pattern [user, ..rest]. Is there an as missing like [user, .. as rest] or does this pattern work in newer versions of Roc?

view this post on Zulip Richard Feldman (Jan 02 2024 at 13:09):

oh yeah that's a mistake, thanks!

view this post on Zulip Osazuwa Oyegun (Jan 12 2024 at 19:52):

If I have a list of employee records - {name, salary, position, gender} - will List.walk enable one to iterate over the list and pass each record to another function that returns a string description of the employee?

view this post on Zulip Anton (Jan 12 2024 at 19:56):

Yes, although List.map will be a lot easier if you only want to do what you described

view this post on Zulip Osazuwa Oyegun (Jan 12 2024 at 22:18):

Apologies for this newbie dump but I'm stumped and would appreciate any help to get this working. This is my Day3 on Roc so I'm sure it's probably something simple that I'm missing, but I'm tired of going around in circles. Thanks in advance.

app "learn"
    packages {
        pf: "https://github.com/roc-lang/basic-cli/releases/download/0.7.1/Icc3xJoIixF3hCcfXrDwLCu4wQHtNdPyoJkEbkgIElA.tar.br",
    }
    imports [
        pf.Stdout,
    ]
    provides [main] to pf

Person: {name: Str, age: U8}

joe: Person
joe = {name: "Joseph First", age: 19}

mary: Person
mary = {name: "Mary McPherson", age: 23}

folks: List Person
folks = [joe, mary]

# describe: Person -> Str
describe =
    \p ->
        Stdout.line "\(p.name) is \(Num.toStr p.age) years old."

main =
    List.map folks describe

view this post on Zulip Luke Boswell (Jan 12 2024 at 22:24):

The type of Stdout is Task {} *, so the type for describe is describe: Person -> Task {} *

view this post on Zulip Luke Boswell (Jan 12 2024 at 22:27):

Also see this thread for how you might loop over the person list to prints them out

view this post on Zulip Luke Boswell (Jan 12 2024 at 22:28):

Also, no need to apologise, it took me a while to understand how to use Tasks properly :smiley:

view this post on Zulip Richard Feldman (Jan 12 2024 at 23:48):

I think we should consider adding some helpers to Task for common scenarios like this, and then moving them into List once Task is a builtin

view this post on Zulip Luke Boswell (Jan 12 2024 at 23:52):

Something like Task.list : List a, (a -> Task c d) -> Task (List c) d maybe?

view this post on Zulip Luke Boswell (Jan 12 2024 at 23:53):

The above would then be

main = folks |> Task.list describe

view this post on Zulip Brendan Hansknecht (Jan 13 2024 at 00:28):

Probably want two forms. The version you listed is equvalent to map we also want a forEach equivalent that just runs and returns nothing.

view this post on Zulip Luke Boswell (Jan 13 2024 at 00:32):

So Task.forEach : List a, (a -> Task {} b) -> Task {} b?

view this post on Zulip Luke Boswell (Jan 13 2024 at 00:32):

I can add this to basic-cli, just not sure what API we actually want

view this post on Zulip Brendan Hansknecht (Jan 13 2024 at 00:35):

Yeah

view this post on Zulip Brendan Hansknecht (Jan 13 2024 at 00:35):

Not sure best names, but those two would be the functions, both built on Task.loop

view this post on Zulip Brendan Hansknecht (Jan 13 2024 at 00:35):

Or, List.walk I guess

view this post on Zulip Anne Archibald (Feb 01 2024 at 20:05):

Thank you, I was just trying to figure out how to print lines in a loop! I suppose one could make a concurrent version by accumulating a list of Tasks and handing them to some concurrency primitive that combined the lot into a single Task that fired them all off and waited for them all to finish?

I find your explanation of tail recursion a little confusing. Your example runLogic calls itself and then returns immediately (has a tail call to itself). But it has another exit. In this case it just returns its state argument, but if it called another function to transform the state before returning it (sorted the list, say), then runLogic would be no less tail-recursive.

If a recursive function is one that calls itself, then I would say that a tail-recursive function is one in which all the calls to itself are the last action, immediately returning the result of the inner call.

view this post on Zulip Brian Carroll (Feb 01 2024 at 20:17):

You might want Task.loop
https://www.roc-lang.org/packages/basic-cli/Task#loop

view this post on Zulip Brian Carroll (Feb 01 2024 at 20:33):

You make a good point about tail recursion. I'll quote the original article because this thread is quite old and long.

runLogic is a tail-recursive function because no code path ends in a call to another function.

I think the intention was probably to distinguish mutual recursion from self recursion. But it never actually says that to be tail recursive the function should call itself! You could have a non-recursive function where no code path ends in a call to another function! :sweat_smile:

view this post on Zulip Norbert Hajagos (Feb 01 2024 at 22:52):

Maybe for the tail recursive "definition", it could be something like: "runLogic is a tail-recursive function because the code paths containing recursive calls only have those as the very final expression that gets returned."

Was also waiting for a solution for the concurrency problem.

I know the language is in this form right now, but this is a beginner article in which backpassing seems quite out of place. "What's that?!"- I imagine my C# -budy already asking. Looking forward for the ! operator! Would help so much with simplifying the examples.

Great that you aspire to teach FP with Roc. It is my first FP language and it was much smoother than I imagined (was scared off by the reputation of Haskell).

view this post on Zulip Anne Archibald (Feb 03 2024 at 11:49):

The concurrency thing is probably a ways down the road.

I think you'd need to build a platform primitive (for each platform) that took a list of Tasks and fed them to a thread pool, producing some kind of combined result. You would also need (for example) roc's reference counting (on those platforms) to be thread-safe.

Or you could ensure that all (some of) the primitives the platform provided were async-enabled and then use an async spawner/collector. (This wouldn't get you parallel processing, though, only I/O concurrency, which seems a waste of the possibilities enabled by roc's purity guarantees.)


Last updated: Jul 06 2025 at 12:14 UTC