Stream: beginners

Topic: ✔ How to use try instead of `Err` branches in this scenario?


view this post on Zulip Felipe Vogel (Sep 12 2024 at 18:34):

I'm trying to wrap my head around how results work, and how to work with them less verbosely. The results example is very helpful, but I still can't connect the dots in the CLI program below, which shows a menu of options and takes user input until a valid option number is inputted. (It actually doesn't compile due to a compiler bug, but that's not my question; I'll file the bug.)

app [main] { pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.15.0/SlwdbJ-3GR7uBWQo6zlmYWNYOxnvo8r6YABXD-45UOw.tar.br" }

import pf.Stdout
import pf.Stdin

main = getChoice

options = ["parse", "validate", "analyze", "analyze and correct"]

exitInputs = ["e", "exit", "q", "quit"]

menu =
    options
    |> List.mapWithIndex \option, index ->
        "$(Num.toStr (index + 1)). $(option)"
    |> Str.joinWith "\n"

getChoice =
    Stdout.line! "\nPlease enter a number for an option below:\n\n$(menu)"
    Stdout.write! "\n> "
    choice = Stdin.line!

    if exitInputs |> List.contains choice then
        Task.ok {}
    else
        retry =
            Stdout.line! "\nOops, that's not an option."
            getChoice

        when Str.toU64 choice is
            Ok choiceNum ->
                when options |> List.get choiceNum is
                    Ok option ->
                        Stdout.line! option

                    Err OutOfBounds -> retry

            Err InvalidNumStr -> retry

The double when with Ok and Err isn't too bad in this simple example, but I want to write it differently just to practice the "try" style. The closest I've gotten is to change the else branch to this:

    else
        retry =
            Stdout.line! "\nOops, that's not an option."
            getChoice

        choiceNum =
            Str.toU64 choice
                |> Result.mapErr? \_ -> retry
        option =
            options
                |> List.get choiceNum
                |> Result.mapErr? \_ -> retry

        Stdout.line! option

But that gives me a type mismatch error, saying that the first branch has the type Task {} * and the else branch has a Result type.

Ideally I'm looking for how to avoid the repeated error handling altogether, and do something like this:

    else
        retry =
            Stdout.line! "\nOops, that's not an option."
            getChoice

        option =
            options
                |> List.get (Str.toU64 choice)
                |> Result.mapErr? \_ -> retry

        Stdout.line! option

But that gives me yet another type mismatch error in the value of option.

Any pointers would be much appreciated.

view this post on Zulip Brendan Hansknecht (Sep 12 2024 at 19:13):

Not at my computer, but two immediate thoughts:

  1. getChoice probably needs to be a function work correctly with the recursive definitions
  2. Something like Task.loop would probably help here.

view this post on Zulip Brendan Hansknecht (Sep 12 2024 at 19:14):

  1. Result.try and Result.map are your friend to avoid the nested error handling.

view this post on Zulip Brendan Hansknecht (Sep 12 2024 at 19:15):

Let me grab a laptop to give a better answer

view this post on Zulip Brendan Hansknecht (Sep 12 2024 at 19:29):

I might do something like this, but not fully sold, feel like there is more cleanup here to do:

code

Edit: made it a bit better.

view this post on Zulip Brendan Hansknecht (Sep 12 2024 at 19:38):

If you specifically want to mix Results and Task in the "try" style, I think that you probably should convert the results to tasks via Task.fromResult.

Something roughly like:

    else
        retry =
            Stdout.line! "\nOops, that's not an option."
            getChoice

        choiceNum =
            Str.toU64 choice
                |> Task.fromResult
                |> Task.onErr! \_ -> retry
        option =
            options
                |> List.get choiceNum
                |> Task.fromResult
                |> Task.onErr! \_ -> retry

        Stdout.line! option

That said, I don't expect that to compile today. I think to make this work today, Task.loop is required.

view this post on Zulip Felipe Vogel (Sep 12 2024 at 20:30):

Thank you! I ran across the Task.loop example too, but I thought, "I don't think I need this because I won't be passing state between iterations. So I'll just call getChoice recursively." But now I see that I can just pass an empty record.

I like your suggestion better than anything compilable that I've come up with, but I still wanted to take a stab at a solution that doesn't use Task.loop and this is the best I could get:

app [main] { pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.15.0/SlwdbJ-3GR7uBWQo6zlmYWNYOxnvo8r6YABXD-45UOw.tar.br" }

import pf.Stdout
import pf.Stdin

main = getChoice

options = ["parse", "validate", "analyze", "analyze and correct"]

exitInputs = ["e", "exit", "q", "quit"]

menu =
    options
    |> List.mapWithIndex \option, index ->
        "$(Num.toStr (index + 1)). $(option)"
    |> Str.joinWith "\n"

retry =
    Stdout.line! "\nOops, that's not an option."
    getChoice

getChoice =
    Stdout.line! "\nPlease enter a number for an option below:\n\n$(menu)"
    Stdout.write! "\n> "
    choice = Stdin.line!

    if exitInputs |> List.contains choice then
        Task.ok {}
    else
        optionRes =
            choiceNum = Str.toU64? choice
            List.get options (choiceNum - 1)

        when optionRes is
            Ok option -> Stdout.line! option
            _ -> retry

There are some things I don't like about it, which boil down to: I wish I could just do the operations I need to do and stick a |> Result.mapErr at the end of the pipeline where any errors can be produced, as in this example which I know is incorrect and produces a bunch of errors:

    else
        option =
            options
                |> List.get ((Str.toU64 choice) - 1)
                |> Result.mapErr? \_ ->
                    Stdout.line! "\nOops, that's not an option."
                    getChoice

        Stdout.line! option

I know that's not how Result.mapErr or Result works, but is there any way I can approximate this style in a correct way?

(I know this is not a recipe for very robust or self-documenting code; I'm just exploring possibilities for scripting, prototyping, etc.)

view this post on Zulip Brendan Hansknecht (Sep 12 2024 at 20:34):

Yeah, the issues of task and result not fully mixing. This all should change with some of the proposal in ideas rn. When task will go away and instead be represented by effectful functions returning results. That should make them mingle with result much better

view this post on Zulip Felipe Vogel (Sep 12 2024 at 20:47):

Gotcha, I will keep an eye out then. Thanks for your help!

view this post on Zulip Felipe Vogel (Sep 12 2024 at 21:20):

Another question: instead of outputting the chosen option at the end of getChoice, how can I return it so that it can be used in main? In the code below I'm still just printing it out, but the goal later is to take different actions based on the chosen option.

This is as far as I've gotten, but I'm getting a type mismatch error in main. I don't feel I understand Tasks or Results very well.

code

view this post on Zulip Brendan Hansknecht (Sep 12 2024 at 21:52):

Ah yeah, you can simply match on a task. You have to await it and match on the result getChoice! and then match without the word Task and it should work I think

view this post on Zulip Felipe Vogel (Sep 12 2024 at 22:41):

Thanks, that worked!

view this post on Zulip Aurélien Geron (Sep 13 2024 at 02:50):

I also struggled to get Task and Result to play nicely together. Task.fromResult was the key. But I still felt it was a bit weird to have both Task and Result, I'm very glad to read Brendan's comment about the idea of getting rid of Task. :+1:

view this post on Zulip Notification Bot (Sep 17 2024 at 18:39):

Felipe Vogel has marked this topic as resolved.


Last updated: Jul 06 2025 at 12:14 UTC