I wrote a first draft of an article about looping in Roc - any feedback welcome!
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.
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
maybe it's worth a longer explanation though
the fib
example might be too much of a digression, but maybe not
Yeah, probably too much of a digression
Maybe some different wording could help....
or maybe link to a separate article about recursion, which could get into both tail recursion and also modulo cons :thinking:
That seems reasonable
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!
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.
could be, although I'm already concerned it's too long :sweat_smile:
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
Richard Feldman said:
could be, although I'm already concerned it's too long :sweat_smile:
Links don't make it longer though!
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.
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
I think it is worth to mention that walk also goes by the name reduce or fold in many other languages. :)
Similar to earlier comments, I also think it would be good to split this up a bit and use links for the different parts.
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.
I think the article would work well with some small exercises for the reader in between but that's also something for the future.
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
Interesting, thanks @Brian Carroll!
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?
oh yeah that's a mistake, thanks!
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?
Yes, although List.map will be a lot easier if you only want to do what you described
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
The type of Stdout is Task {} *
, so the type for describe is describe: Person -> Task {} *
Also see this thread for how you might loop over the person list to prints them out
Also, no need to apologise, it took me a while to understand how to use Tasks properly :smiley:
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
Something like Task.list : List a, (a -> Task c d) -> Task (List c) d
maybe?
The above would then be
main = folks |> Task.list describe
Probably want two forms. The version you listed is equvalent to map
we also want a forEach
equivalent that just runs and returns nothing.
So Task.forEach : List a, (a -> Task {} b) -> Task {} b
?
I can add this to basic-cli, just not sure what API we actually want
Yeah
Not sure best names, but those two would be the functions, both built on Task.loop
Or, List.walk
I guess
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.
You might want Task.loop
https://www.roc-lang.org/packages/basic-cli/Task#loop
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:
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).
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