We face the same difficulty in the Tezos code-base. I don’t think there is a well known, very satisfying solution to that. We have syntactically distinct operators (bind, return, fail) for the different monads so our problem is slightly different.
I’d recommend that you define a few additions to Stdlib.Result
to help with moving from one monad to the other. E.g., Result.recover : ('b -> 'a) -> ('a, 'b) result -> 'a
will help you go from Lwt+result into Lwt only.
Also, whilst @CraigFe has given a very good answer already, there are some things he did not mention in text. So here are a few comments about his code.
-
Pulling out the common pattern into a separate function is useful for readability, but you end up also pulling an error literal (
A
,B
,C
) out of a branching pattern. If you are in a more realistic case where the error is actually an evaluated expression you will evaluate it even in the success path. You can either (a) avoid pulling this into its own function or (b) wrap it in alazy
or a closure (fun () -> …
). -
The
Lwt_result.map_err
is doing the heavy lifting here. It is actually sufficient to avoid nesting altogether. (You could also useResult.iter_error
to separate the logging from the erroring.)let () = let* a = MyService.get_a () in let* a = Lwt_result.map_err (fun e -> ErrorService.log "error with a" e; Error A) in <same for b and c>
One of the issue you might encounter is that
Result
andLwt_result
have a different set of functions (e.g.,Result.iter_error
does not have a a counterpart inLwt_result
andResult.map_error
’s counterpart is spelledLwt_result.map_err
). If you start relying on these modules a lot, I’d recommend recoding your own to have a uniform view of results. -
Do use
List.compare_length_with
when comparing lengths: it returns early if the list is longer than the integer.