Repository F# setup
open Reified
open Reified.Refinements
open Reified.Result

Working with collections

Applying a fallible operation to every item produces a list of results. traverse and sequence turn that into one result holding every value.

Verified
open System
open Reified.Result

type SignupError =
    | AgeMissing
    | AgeNotANumber of string
    | AgeOutOfRange of int

let parseAge (raw: string) =
    Int32.TryParse raw
    |> Result.fromTry
    |> Result.orError (AgeNotANumber raw)
    |> Result.bind (fun age ->
        if age >= 0 && age < 130 then Ok age else Error (AgeOutOfRange age))

traverse

traverse maps each item with a Result-returning function and collects the successes.

Verified
[ "1"; "2"; "3" ] |> Result.traverse parseAge
// Ok [1; 2; 3]

[ "1"; "abc"; "500" ] |> Result.traverse parseAge
// Error (AgeNotANumber "abc")

sequence

sequence is the same operation when you already hold the results, it is traverse id.

Verified
[ Ok 1; Ok 2 ] |> Result.sequence
// Ok [1; 2]

[ Ok 1; Error AgeMissing; Ok 3 ] |> Result.sequence
// Error AgeMissing

It stops at the first error

Traversal stops mapping after the first failure. This matters when mapping performs a lookup, request, or write.

Verified
let mutable visited = []

let recordAndParse raw =
    visited <- raw :: visited
    parseAge raw

[ "1"; "abc"; "3" ] |> Result.traverse recordAndParse
// Error (AgeNotANumber "abc")

List.rev visited
// ["1"; "abc"]

"3" was never visited. Only the first failure is reported; later items are not examined.

traverseAll and sequenceAll

Use these when every item should be tried and every failure reported:

Verified
[ "1"; "abc"; "500" ] |> Result.traverseAll parseAge
// Error [AgeNotANumber "abc"; AgeOutOfRange 500]

[ Ok 1; Error AgeMissing; Error (AgeNotANumber "x") ] |> Result.sequenceAll
// Error [AgeMissing; AgeNotANumber "x"]

The rules:

  • Every mapping runs, in input order, even after one has failed. If the mapping does real work, it does that work for every item.
  • Errors come back in input order, one per failing item. Nothing is flattened: a mapping that itself returns Result<_, 'error list> produces a list of lists, so map it down first if you want one flat list.
  • The input sequence is enumerated once, up front, so a lazy sequence is fully forced and an infinite one will not terminate.

To accumulate across differently-typed independent steps rather than across one collection, see collecting every error.

Shape in and shape out

Both take any seq<_> and produce a list. The four complete signatures are in the Result API reference.

An array or a seq goes in; a list comes out. Convert afterwards with Result.map when you need a different shape:

Verified
[| "1"; "2" |] |> Result.traverse parseAge |> Result.map Array.ofList
// Ok [|1; 2|]

All four convert the input to a list up front, so a lazy sequence is fully forced and an infinite one will not terminate through any of them. traverse and sequence then stop mapping at the first failure, while traverseAll and sequenceAll run every mapping.