Stream: beginners

Topic: patterns for working with Result type


view this post on Zulip Jakub (Dec 10 2023 at 17:15):

I built a function that converts hh:mm:ss:ms timestamp to milliseconds. In the process I discovered that Error handling got quite verbose (yet informative). Something tells me that this is too much... What would be a recommended way to make this logic a little shorter?

timeStampArrToMillsec = \timeStampArr ->
    when timeStampArr is
        [hours, minutes, secondsMilliseconds] ->
            hoursInt = when (Str.toI32 hours) is
                Ok val -> val
                Err _ ->  crash "Non numerical value passed to hours"
            minutesInt = when (Str.toI32 minutes) is
                Ok val -> val
                Err _ ->  crash "Non numerical value passed to minutes"
            seconds = when (List.first (Str.split secondsMilliseconds ".")) is
                Ok val -> val
                Err _ ->  crash "Seconds value does not exist in an array "
            milliseconds = when (List.last (Str.split secondsMilliseconds ".")) is
                Ok val -> val
                Err _ ->  crash "Milliseconds value does not exist in an array "
            secondsInt = when (Str.toI32 seconds) is
                Ok val -> val
                Err _ ->  crash "Non numerical value passed to seconds"
            millisecondsInt = when (Str.toI32 milliseconds) is
                Ok val -> val
                Err _ ->  crash "Non numerical value passed to milliseconds"

            (hoursInt * 3600000) + (minutesInt * 60000) + (secondsInt * 1000) + millisecondsInt
        _ -> crash "Invalid agruments passed as timestamp"

expect
    ((timeStampArrToMillsec ["00","00","00.100"]) == 100)
    &&  ((timeStampArrToMillsec ["00","00","01.100"]) == 1100)
    &&  ((timeStampArrToMillsec ["00","01","01.600"]) == 61600)

view this post on Zulip Anton (Dec 10 2023 at 18:04):

This is a nice improvement:

orCrash : Result a err, Str -> a
orCrash = \res, errMsg ->
    when res is
        Ok val -> val
        Err _ -> crash errMsg

hoursInt = orCrash (Str.toI32 hours) "Non numerical value passed to hours"

view this post on Zulip Richard Feldman (Dec 10 2023 at 18:07):

I liked using Result.try with backpassing when parsing dates here! https://github.com/rtfeldman/roc-iso8601/blob/dfbadf1074d3c5bfe9098de01c90745b566c51e0/Iso8601.roc#L310

view this post on Zulip timotree (Dec 10 2023 at 18:11):

Is this how you would use Result.try if you wanted to keep the custom error messages?

timeStampArrToMillsec = \timeStampArr ->
    when timeStampArr is
        [hours, minutes, secondsMilliseconds] ->
            hoursInt <-
                Str.toI32 hours
                |> Result.mapErr \_ -> "Non numerical value passed to hours"
                |> Result.try
            minutesInt <-
                Str.toI32 minutes
                |> Result.mapErr \_ -> "Non numerical value passed to minutes"
                |> Result.try
            seconds <-
                List.first (Str.split secondsMilliseconds ".")
                |> Result.mapErr \_ -> "Seconds value does not exist in an array"
                |> Result.try
            milliseconds <-
                List.last (Str.split secondsMilliseconds ".")
                |> Result.mapErr \_ -> "Milliseconds value does not exist in an array"
                |> Result.try
            secondsInt <-
                Str.toI32 seconds
                |> Result.mapErr \_ -> "Non numerical value passed to seconds"
                |> Result.try
            millisecondsInt <- Str.toI32 milliseconds
                |> Result.mapErr \_ -> "Non numerical value passed to milliseconds"
                |> Result.map
            (hoursInt * 3600000) + (minutesInt * 60000) + (secondsInt * 1000) + millisecondsInt
        _ -> Err "Invalid agruments passed as timestamp"

expect
    ((timeStampArrToMillsec ["00","00","00.100"]) == Ok 100)
    &&  ((timeStampArrToMillsec ["00","00","01.100"]) == Ok 1100)
    &&  ((timeStampArrToMillsec ["00","01","01.600"]) == Ok 61600)

view this post on Zulip Brendan Hansknecht (Dec 10 2023 at 18:11):

Yeah, Result.try with backpassing and 1 final when is the way to go. Can add some error type mapping or wrapping if you want as well.

view this post on Zulip Brendan Hansknecht (Dec 10 2023 at 18:11):

I would use a tag and not a string for the error.

view this post on Zulip Brendan Hansknecht (Dec 10 2023 at 18:11):

I also might just use wrapping here and add details later

view this post on Zulip Brendan Hansknecht (Dec 10 2023 at 18:12):

but yeah, not as great

view this post on Zulip Brendan Hansknecht (Dec 10 2023 at 18:12):

I would do this if i really cared about context

hoursInt <- Str.toI32 hours |> Result.mapErr HoursErr |> Result.try
minutesInt <- Str.toI32 minutes |> Result.mapErr MinutesErr |> Result.try
...

view this post on Zulip Brendan Hansknecht (Dec 10 2023 at 18:13):

Then a when to match on all error types

view this post on Zulip timotree (Dec 10 2023 at 18:14):

In Rust, I would want to use an error enum but give it a custom Display implementation that printed the human-readable message. As an API author in roc, if you want your error to print nicely with e.g. dbg, it seems like you need to return a Str as your error type

view this post on Zulip Brendan Hansknecht (Dec 10 2023 at 18:15):

when res is
    Ok x -> ...
    Err e -> toErrorMessage e

toErrorMessage = \err ->
    HoursErr subErr -> ...
    MinutesErr subErr -> ...

view this post on Zulip Brendan Hansknecht (Dec 10 2023 at 18:16):

Tags will print there name, which is a medium ok message, but yeah, you probably want a toMessage function

view this post on Zulip Brendan Hansknecht (Dec 10 2023 at 18:16):

That or make your error type opaque and add a custom inspect impl, but I would definitely advise just using a when ... is in a function to generate the error message.

view this post on Zulip Brendan Hansknecht (Dec 10 2023 at 18:18):

Easy enough for a user to do either of:

dbg err

# or
dbg Lib.toMessage err

view this post on Zulip Jakub (Dec 10 2023 at 21:29):

thank you very much for the input, Anton's orCrash was the most straightforward at my level and it made code a bit more concise indeed. Examples from Richard, Tim and Brendan went over my head, I don't understand how piping Result.try works actually in this context (even though I consulted the docs). You've been very generous with your help though, I just need to run some experiments to internalize Result.try. Thanks again

view this post on Zulip LoipesMas (Dec 10 2023 at 21:40):

Result.try works there thanks to backpassing. It's an important piece of the equation there!


Last updated: Jul 06 2025 at 12:14 UTC