Stream: beginners

Topic: Any high level advice for exercism problems


view this post on Zulip Nathan Kramer (Oct 15 2024 at 07:15):

I'm finding the roc track really fun, I've solved about 58 exercises.

However, I'm starting to get to the point where I feel stuck on every challenge.

Does anyone have any high level advice for how to get unstuck without resorting to "cheating"?

For example, I just solved the saddle points exercise but I had to look at the exemplar code to do it, and my solution is basically the same as a result, just with some minor differences.

I want to continue solving exercises at a good pace and coming up with unique solutions but I feel like I'm not able to :sweat_smile:

The next exercise is https://exercism.org/tracks/roc/exercises/rectangles and I can already feel myself wanting to look at the example code because I don't know where to start.

view this post on Zulip Anton (Oct 15 2024 at 10:20):

@Aurélien Geron or @Isaac Van Doren should be able to give some good tips

view this post on Zulip Luke Boswell (Oct 15 2024 at 10:21):

Wow, so cool to hear you've done that many examples already! Awesome.

view this post on Zulip Isaac Van Doren (Oct 15 2024 at 12:29):

Wow congrats, that's awesome!

You could try looking at solutions to some of the exercises in other language tracks. That could give you a good start but require you to still adapt the solution you see to Roc.

view this post on Zulip Aurélien Geron (Oct 15 2024 at 21:32):

58 exercises, wow that's impressive, we're going to have to add many more! :sweat_smile:
I've occasionally taken a peak at the Haskell or Elm solutions: it's good to try your best for a while, but it's no use getting stuck for too long, I feel it's better to look for inspiration, learn and move on to the next exercise.
That said, what kind of things do you get stuck on? Personally, as I come from 20+ years of imperative languages, the main difficulty is understanding how to think like a functional programming engineer. I mostly struggled on loops: generally List.walk does what I need it to, it's just not natural to me. Often, writing a little recursive function (ideally using tail recursion) is a simpler option, especially when you don't know in advance how many iterations you'll need.
Hope this helps, and feel free to ask questions here if you get stuck. :+1:

view this post on Zulip Nathan Kramer (Oct 15 2024 at 23:56):

Thank you for the encouragement

That said, what kind of things do you get stuck on?

I think I often get stuck trying to come up with a way of iterating that makes sense for the problem. Walk is useful but sometimes doesn't feel like the right fit. I like the recursive function idea.
I also sometimes struggle with the amount of ceremony needed handling things like array indexing, especially in cases where you happen to know that the array is not empty. I guess there is a skill to dealing with results in an elegant way. I have spent a fair bit of time with Elm so I should be used to this but it still sort of trips me up.

You could try looking at solutions to some of the exercises in other language tracks. That could give you a good start but require you to still adapt the solution you see to Roc.

Yeah that's a good idea.
I also have been prompting claude with the problem description saying "give me high level guidance without showing any code", and that sometimes works quite well.

view this post on Zulip Isaac Van Doren (Oct 16 2024 at 00:36):

Asking an LLM for advice without spoilers is a great idea!

view this post on Zulip Aurélien Geron (Oct 16 2024 at 03:32):

Yeah, I'll try that as well! I asked Claude and ChatGPT for Roc code a few times, but they barely know Roc, so they generate a weird mixture of various languages, they're not helpful at all. It will certainly improve once Roc is the #1 language in the world :wink:, but in the meantime, asking for general guidelines is a much better idea.

Regarding the ceremony around accessing arrays and so on, I must admit that I feel the same way, it's one of my main problems with the language right now, tbh, some things feel overly verbose, and I often wish there was an unwrap() function like in Rust, to say: "please trust me on this, compiler, I know what I'm doing". But unwrap() is really overused in Rust, and this leads to unstable programs, so Roc is trying to avoid it (disclaimer: I'm not a Roc developer, and in fact I'm fairly new to the language). Since I'm writing many of the Exercism solutions, I try to do things the Roc way and avoid unwrap(), but if it was a personal side-project, I would definitely be using my own unwrap() function.

Here are a few variants of a tripleFirst function that takes the first element of a list that we know for sure is not empty, and it returns the triple of the first element:

# Recommended Roc approach
tripleFirst1 = \list ->
    firstElement =
        when list |> List.first is
            Err ListWasEmpty -> crash "Unreachable: the list cannot be empty here"
            Ok first -> first
    firstElement * 3


# Using a custom `unwrap()` function:
tripleFirst2 = \list ->
    firstElement = list |> List.first |> unwrap "the list cannot be empty here"
    firstElement * 3

unwrap = \result, message ->
    when result is
        Ok value -> value
        Err _ -> crash "Unwrap failed: $(message)"


# Using Result.withDefault => quick & easy, but probably a very bad idea
tripleFirst3 = \list ->
    firstElement = list |> List.first |> Result.withDefault 0
    firstElement * 3


# If the function returns a `Result` (for some other errors) you could write:
tripleFirst4 = \list ->
    firstElement = list |> List.first |> Result.mapErr? \ListWasEmpty -> crash "Unreachable: the list cannot be empty here"
    if List.len list > 100 then
        Err ListIsTooLong
    else
        Ok (firstElement * 3)

I find the recommended approach a bit too heavy, and the unwrap option is actually quite nice, imho. The Result.withDefault option is so easy that I'm sure many people will be tempted to use it (I certainly have), but it's probably a bad idea because if the assumption is one day broken (through refactoring, for example), we may not notice it straight away, the code will just start using the default value. I hope this helps.

view this post on Zulip Luke Boswell (Oct 16 2024 at 03:35):

I feel like I would do

tripleFirst1 = \list -> list |> List.first |> Result.map \f -> f * 3

view this post on Zulip Nathan Kramer (Oct 16 2024 at 03:36):

I have to admit, I have been opting for Result.withDefault quite a lot.

view this post on Zulip Luke Boswell (Oct 16 2024 at 03:37):

Or maybe

tripleFirst1 = \list ->
    first = list |> List.first?
    first * 3

view this post on Zulip Nathan Kramer (Oct 16 2024 at 03:37):

ohh taht's quite nice

view this post on Zulip Nathan Kramer (Oct 16 2024 at 03:38):

I'm sort of scared to use the crash keyword in actual code outside of scripting

view this post on Zulip Luke Boswell (Oct 16 2024 at 03:38):

Wait I'm not sure how to use the ? operator... I thought we added that

view this post on Zulip Aurélien Geron (Oct 16 2024 at 03:38):

Luke Boswell said:

I feel like I would do

tripleFirst1 = \list -> list |> List.first |> Result.map \f -> f * 3

Yes, I should have made it clear that if your function can just return an error, then that's great, do that, it's easy (especially with the ? try operator). But I've found that I often run into cases where the error just cannot happen, so I don't want it to be part of the function signature.

view this post on Zulip Aurélien Geron (Oct 16 2024 at 03:39):

Luke Boswell said:

Wait I'm not sure how to use the ? operator... I thought we added that

You just need to return an Ok value at the end

view this post on Zulip Aurélien Geron (Oct 16 2024 at 03:39):

tripleFirst1 = \list ->
    first = list |> List.first?
    Ok (first * 3)

view this post on Zulip Luke Boswell (Oct 16 2024 at 03:41):

But I've found that I often run into cases where the error just cannot happen

I guess this is what crash is (can be used) for

view this post on Zulip Aurélien Geron (Oct 16 2024 at 03:44):

Yes, I believe that crash in the right solution in this case, but when you want to unwrap an Ok value, when you know for sure that it cannot be an Err, then it can be a bit verbose, and I think an unwrap() function is fine in this case (others might disagree). Just be careful not to overuse it.

view this post on Zulip Aurélien Geron (Oct 16 2024 at 03:47):

I personally think that there should be a Result.unwrap function in the standard library, or else:

  1. many people will just use Result.withDefault which is much worse (as explained earlier)
  2. other people will write their own unwrap function, and various libraries will use various unwrap variants, possibly with different names. In the end it will be worse than including it in the standard library (with a big warning: "please don't overuse!").
  3. many people will struggle like Nathan and I did when learning the language.

view this post on Zulip Aurélien Geron (Oct 16 2024 at 03:48):

Just my 2 cents, again, I'm still very new to Roc!

view this post on Zulip Nathan Kramer (Oct 16 2024 at 03:57):

I feel less guilty using Result.withDefault than I do using crash. Crash feels super spooky to me lol

view this post on Zulip Aurélien Geron (Oct 16 2024 at 04:47):

Haha, yes, I think it's meant to be scary.
Result.withDefault is fine if you actually need a default value, but if you're using it to unwrap the value and the default value can never be used, then I fear that this can lead to hard-to-debug bugs in the future, so I think crash is better when you know for sure that a path is impossible.

view this post on Zulip Niklas Konstenius (Oct 16 2024 at 06:06):

My 2 cents regarding crash or withDefault.

I would argue that a function tripleFirst : List a -> a is impossible to implement in a "good way" since its not a total function. Either you need to resort to use crashor Result.withDefault, both are equally bad imho.

The problem is that the function has a bad design from the beginning. The right (imho) approach is to change the signature of the function to make it total. Either you can change the argument to a non empty list (a list that's guaranteed to have at least one element) or you can change the signature to return a result.

# a non empty list can be represented as a tuple (a,List a)
tripleFirst : (a,List a) -> a

or

# return an error on empty list
tripleFirst : List a -> Result a [EmptyList]

I don't know how the Roc problems on Exercism are designed in general but I think it would make sense to try to design them in a way to avoid forcing the student to use bad habits like crash or withDefault.

view this post on Zulip Aurélien Geron (Oct 16 2024 at 06:18):

That's a really interesting insight, thanks.
Looking at the 96 exercises we wrote, I just counted 19 uses of crash in the solutions. Seven of these are due to the fact that the instructions explicitly say that the user should assume that the inputs are ASCII, so when calling Str.fromUtf8, we just crash rather than bubble up the error. Perhaps that's a bad idea. In some cases, we start by checking that all characters are digits, then we call some function which can then assume that it's only getting digits as input, so after it does whatever it needs to do (e.g., reversing the order of the digits), it can convert the characters back to a string and it can be certain that there will be no UTF-8 error, so it just crashes. I haven't looked in detail at the other cases yet, but they seem to be mostly access to lists that we know are not empty, or we know they have N elements and we're accessing the element e < N. That sort of thing.

view this post on Zulip Nathan Kramer (Oct 16 2024 at 06:19):

I was going to say, one trivial example is when you're wanting to map over a string.
You call toUtf8 and then map, and then call fromUtf8 which returns a result. But we know the characters are all valid utf8 because we just got them from a string.

e.g

digits = string
        |> Str.toUtf8
        |> List.keepIf \c -> c >= '0' && c <= '9'
        |> Str.fromUtf8
        |> Result.withDefault "" # This is unreachable

(caveats about unicode/graphemes not withstanding)

view this post on Zulip Aurélien Geron (Oct 16 2024 at 06:19):

Yes, that's a great example

view this post on Zulip Anton (Oct 16 2024 at 08:43):

dependent types can solve this but that would add more complexity to the compiler , we'd like to avoid that for now :p


Last updated: Jul 06 2025 at 12:14 UTC