Stream: show and tell

Topic: Chaining Results in expect


view this post on Zulip Johan Lövgren (Dec 05 2023 at 19:50):

So I started implementing some fun datatypes in Roc, for example NonEmpty which encodes a non-empty list. For this particular case I found myself writing expect statements that called several functions in a row that returned Result. I found this a bit awkward. I have for example the functions

fromList: List a -> Result (NonEmpty a) [ListWasEmpty]
get: NonEmpty a, Nat -> Result a [OutOfBounds]

And then I want to write a test that first creates a NonEmpty using fromList, to then test get. My first attempt was the standard chaining of when expressions:

expect
    when [1, 2] |> fromList is
        Err _ -> Bool.false
        Ok nonempty ->
            when nonempty |> get 1 is
                Err _ -> Bool.false
                Ok value -> value == 2

view this post on Zulip Johan Lövgren (Dec 05 2023 at 19:52):

But this feels kind of awkward. For all Err branches we are just returning Bool.false. If this was in just a regular function, I would probably use Result.try to keep going. But that is not a natural fit inside expect since we in the end want to return a Bool. It would look something like this:

expect
    result =
        nonempty <- [1, 2] |> fromList |> Result.try
        nonempty |> get 1
    when result is
        Err _ -> Bool.false
        Ok value -> value == 2

view this post on Zulip Brendan Hansknecht (Dec 05 2023 at 19:52):

expect
    result = [1, 2] |> fromList
    result == Ok (NonEmpty 1 (NonEmpty 2 Empty))

view this post on Zulip Johan Lövgren (Dec 05 2023 at 19:54):

But then I got the idea that I could use an analogous function to continue chaining Results inside the expect. Not sure what to call it, for now I just call it test. So I defined

test: Result a *, (a -> Bool) -> Bool
test = \res, pred ->
    when res is
        Err _ -> Bool.false
        Ok x -> x |> pred

And using test I can now write

expect
    nonempty <- [1, 2] |> fromList |> test
    value <- nonempty |> get 1 |> test
    value == 2

view this post on Zulip Johan Lövgren (Dec 05 2023 at 19:56):

Which I thought was kind of neat! Not sure if anyone else has use of that, but I wanted to share it because it was a nice use of backpassing that I had not realised was useful before.

view this post on Zulip Johan Lövgren (Dec 05 2023 at 19:58):

Brendan Hansknecht said:

expect
    result = [1, 2] |> fromList
    result == Ok (NonEmpty 1 (NonEmpty 2 Empty))

Sorry I did not share my datatype, I am here modelling NonEmpty as

NonEmpty a := {body: List a, tail: a}

view this post on Zulip Johan Lövgren (Dec 05 2023 at 20:01):

And yes for calculations just featuring one Result, I can just write like you wrote there Brendan. The issue I was addressing was when you need to use the value from that result to further test something.

view this post on Zulip Brendan Hansknecht (Dec 05 2023 at 20:08):

Ah, I see. Yeah, I would have simpler tests and for longer tests just unwrap cause the simpler tests should catch that.

That or I would just use Result.try with backpassing as a more direct helper.

view this post on Zulip Brendan Hansknecht (Dec 05 2023 at 20:09):

Though maybe look at our list tests. They should hit this issue? Lots of results

view this post on Zulip Johan Lövgren (Dec 05 2023 at 20:17):

Brendan Hansknecht said:

Ah, I see. Yeah, I would have simpler tests and for longer tests just unwrap cause the simpler tests should catch that.

That or I would just use Result.try with backpassing as a more direct helper.

Hmm Yes it might be that this is the right approach. It is of course good to write very simple unit tests to test individual functions very clearly. I guess what I am trying to write might be more complicated than necessary

view this post on Zulip Brendan Hansknecht (Dec 05 2023 at 20:19):

your example with the test function could instead be:

expect
    value =
        nonempty <- [1, 2] |> fromList |> Result.try
        nonempty |> get 1
    value == Ok 2

view this post on Zulip Johan Lövgren (Dec 05 2023 at 20:24):

That is true, and is not too bad at all, thanks!

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

I would use Result.try and Result.map (with backpassing or piping). For the predicate, if it's a simple comparison then comparing to Ok value should be fine, but you can also do |> Result.map pred |> Result.withDefault Bool.False.
For example, test could be rewritten to

test = \res, pred ->
    Result.map res pred |> Result.withDefault Bool.false

Last updated: Jul 06 2025 at 12:14 UTC