I've ported my first full function to Roc from TypeScript. Anyone want to code review?
It was a little tricky figuring out what List functions to use here but I got through it.
Here's the TypeScript version:
image.png
Here's the Roc version:
image.png
Full project here:
https://github.com/ashleydavis/book-of-examples/tree/main/gallery/roc
It looks like a faithful port of the Typescript version - using recursion to check a number of properties of a list.
You could also implement the same functionality using headingsMatch = \headingA, headingB -> headingA == headingB
but I guess there's less opportunity to explore the syntax. :smiley:
I think the module syntax has changed recently, it would now be module [headingsMatch]
to expose this function.
Another idea, sometimes I like to use variables to document things instead of comments. So you could also write it like;
headingsMatch = \headingA, headingB ->
isDifferentLength = List.len headingA != List.len headingB
isMatchingEmpty = List.len headingA == 0 || List.len headingB == 0
isFirstItemNotMatching = List.first headingA != List.first headingB
if isMatchingEmpty then
Bool.true
else if isDifferentLength || isFirstItemNotMatching then
Bool.false
else
headingsMatch (List.dropFirst headingA 1) (List.dropFirst headingB 1)
Thanks so much for your feedback.
The best thing about this impl is that I understand what you're doing because it's easily readable. It's good that you added the if statements at the top as "guard" statements to check for basic stuff first, like list length disparity, but since this is written recursively, those checks will run every time we recurse, so N times for a list with N items. Consider embedding a helper recursing function that avoids repeating those checks
Like so:
headingsMatch = \headingA, headingB ->
if List.len headingA != List.len headingB then
# Different lengths.
Bool.false
else if List.len headingA == 0 && List.len headingB == 0 then
# Matching empty lists.
Bool.true
else
listsAreEqual = \listA, listB ->
when (listA, listB) is
([], []) -> Bool.true
([first, ..], []) -> Bool.false
([], [second, ..]) -> Bool.false
([first, .. as restOfFirst], [second, .. as restOfSecond]) ->
if first == second then
listsAreEqual restOfFirst restOfSecond
else
Bool.false
listsAreEqual headingA headingB
Ooh another idea
headingsMatch = \headingA, headingB ->
when (headingA, headingB) is
([], []) -> Bool.true
([a, .. as restA], [b, .. as restB] if a == b -> headingsMatch restA restB
_ -> Bool.false
Just wanted to post a pattern match version but you beat me to it :big_smile:
Yeah, it leverages a powerful tool. I think from an education perspective (I see it’s part of the book), it’s a perfect place to show both versions I think
I simplified it a bit using a guard.
Yeah, one of the best parts of roc is the pattern matching. I find that, paired with recursion, can make for some really nice implementations for algorithms.
Also, most importantly, in typescript there may be reason to use a list because it's running in a dynamic system, but because you're literally just comparing 2 lists of strings, the simplest and most efficient way to do this is headingsMatch = \headingsA, headingsB -> headingsA == headingsB
And it won't have type issues
Also, while we are looking at it. Before we had tuples I would hav used a Tag. I think it also looks pretty nice.
headingsMatch = \headingA, headingB ->
when T headingA headingB is
T [] [] -> Bool.true
T [a, .. as restA] [b, .. as restB] if a == b -> headingsMatch restA restB
_ -> Bool.false
Yeah, I think these kinds of examples are a great way to learn FP concepts like recursion. Sam makes a great point that for this specific use-case you would just use structural equality.
I'm super-duper brand-new to Roc so I wanted to test my intuition but in this specific case would a naïve comparison be (probably) as efficient because the String comparison operator does optimisations like checking length for you? (Understand that this is an algorithm exercise and so the point is to be able to write the procedure from first principles as it were.)
Oh, I see @Sam Mohr already answered this. My bad.
I like how this simple example with the sequence of refactorings tells so much about the language!
@pyrmont a lot of the performance-sensitive std library operations are written in Zig, so this is what runs when you compare strings in Roc: https://github.com/roc-lang/roc/blob/7e609bfdbf37b51dd8ae576462a99b7ff404ca63/crates/compiler/builtins/bitcode/src/str.zig#L179
@Sam Mohr Thanks! Yep, that's what I expected :)
Also while looking at this I found this behaviour which seemed wrong so I made an issue https://github.com/roc-lang/roc/issues/6839
Should a definition only used in a test give a warning?
Yeah, I was gonna say... if it's only used to test itself and nowhere else, then probably
But if it's used only in testing, but it's a helper to test something else, then I'd say no warning
But if it's exposed, then it's fine to only be used in a test IMO
Tests are dead code unless it's tests execution. Unless the def is exposed, it is expected to have the warning
Thanks for clarifying. That makes sense. I've closed the Issue with a comment.
it definitely needs to be possible to write test helper functions and constants that are only ever referenced in tests! Those should not be reported as unused; that would make them essentially impossible to use :big_smile:
@Richard Feldman should I reopen that issue? I'm not 100% becuase of the specific issue reported is about recursion. Maybe I've found an edge case from somehwere.
I think test helpers would require explicit identification then. I don’t think it’s a good idea to lock dead code with tests
hm, as in you write something that’s not intended to be a test helper, but also isn’t exposed, but is referenced in a test, and therefore never gets reported as unused even though it’s actually dead code?
actually we could address that situation by reporting “dead tests” which don’t reference anything exposed (including indirectly)
which would also address the other scenario, because after deleting the dead tests, the not-actually-a-helper would no longer be referenced anywhere, and would be reported as unused
Yeah, let's say my module is:
module [foo]
foo = \a ->
a + 1
bar = \b ->
b - 1
baz = \c ->
c * 2
expect
foo 3
|> bar
|> == 3
expect
baz 4 == 8
foo
is exported, so it's not dead code. bar
isn't exported, but it's used to text something that was exported, so it's not dead code. baz
was only used in tests by itself, so it's dead code
Reporting "dead tests" would be a simpler way to achieve that goal than to do analysis on whether a function was used in an expect
with an exposed function.
However, it would make test helpers easy to leak into release env. Probably not a problem but without distinguishing mechanism, there is no difference between test helpers and just helpers
Yeah, if we wanted strong separation of tests from code, we would need something like test modules in rust
An aside on this: at work we've built a component library, where each component has a set of examples. We occasionally run into components having dead code which isn't caught because there's still an example which exercises it. But because it's not of use in production, we would rather remove it.
But we also have the reverse, where we have a library of utility functions we can use, and for that, we also keep dead functions around sometimes because they can still be useful when you need them.
So dead code detection and what to do about that code definitely can get more complicated than just "is it called anywhere?"
can still be useful
In practice, it almost never happens. At least not in my experience.
Thanks everyone. There's a lot to process here. Pattern matching is very cool, but seems very different to the languages I am coming from. I'm going back to the Roc tutorial to try and understand it better.
Luke Boswell said:
Also, while we are looking at it. Before we had tuples I would hav used a Tag. I think it also looks pretty nice.
headingsMatch = \headingA, headingB -> when T headingA headingB is T [] [] -> Bool.true T [a, .. as restA] [b, .. as restB] if a == b -> headingsMatch restA restB _ -> Bool.false
What's the difference between the tag and tuple versions? I can read the tuple version and understand that, but the tag version is a form of syntax I haven't seen in Roc yet.
Quick advice. Let's not teach using T
or other single tags for tuples.
headingsMatch = \a, b ->
when (a, b) is
([], []) -> Bool.true
([a, .. as restA], [b, .. as restB] if a == b -> headingsMatch restA restB
_ -> Bool.false
They are identical in function
So really only the tuple version should be used/recommend for language consistency
They're identical, but I think people prefer the tag approach because it's slightly more terse
The tag version just is constructioning a tag with the type [T Str Str]
. It only has a single possible variant. Assuming heading is List Str
But tuples are more readable
In that you don't have to write \a, b -> (a, b)
Aside, this is just headingsA == headingsB
, right?
Yep
It was a learning exercise I think
I mean the difference is a single character in length assuming the code is formatted
T a b
(a, b)
List.map2 T vs List.map2 \a, b -> (a, b)
Super minor I know
Didn't notice a version using List.map2
But yeah, I see your point
Yes, I was saying why someone would prefer it in general yes
Ah
Obviously means we need to steal T or something short to mean make a tuple... :joy:
Lmao my thoughts exactly
But probably not worth it yet
When people complain more
List.map2 (,)
ofc :stuck_out_tongue:
Jokes aside, I don’t think it will ever be worth adding a shorthand for this
Last updated: Jul 06 2025 at 12:14 UTC