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
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
expect
result = [1, 2] |> fromList
result == Ok (NonEmpty 1 (NonEmpty 2 Empty))
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
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.
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}
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.
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.
Though maybe look at our list tests. They should hit this issue? Lots of results
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
your example with the test
function could instead be:
expect
value =
nonempty <- [1, 2] |> fromList |> Result.try
nonempty |> get 1
value == Ok 2
That is true, and is not too bad at all, thanks!
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