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.
Not at my computer, but two immediate thoughts:
getChoice
probably needs to be a function work correctly with the recursive definitionsResult.try
and Result.map
are your friend to avoid the nested error handling.Let me grab a laptop to give a better answer
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.
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.
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.)
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
Gotcha, I will keep an eye out then. Thanks for your help!
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
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
Thanks, that worked!
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:
Felipe Vogel has marked this topic as resolved.
Last updated: Jul 06 2025 at 12:14 UTC