Skip to content

Commit

Permalink
feat(Seq): sequenceResultA
Browse files Browse the repository at this point in the history
  • Loading branch information
bartelink committed Feb 15, 2024
1 parent 2f234ba commit 93115e8
Show file tree
Hide file tree
Showing 6 changed files with 145 additions and 2 deletions.
1 change: 1 addition & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
### 4.16.0
- [refactor!: Seq.sequenceResultM returns Array instead of seq](https://github.com/demystifyfp/FsToolkit.ErrorHandling/pull/255) [@bartelink](https://github.com/bartelink)
- [feat(Seq): sequenceResultA](https://github.com/demystifyfp/FsToolkit.ErrorHandling/pull/255) [@bartelink](https://github.com/bartelink)

### 4.15.1 - January 15, 2024
- [Doc updates](https://github.com/demystifyfp/FsToolkit.ErrorHandling/pull/247) Credits @1eyewonder
Expand Down
1 change: 1 addition & 0 deletions gitbook/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
* [sequenceResultA](list/sequenceResultA.md)
* Seqs
* [sequenceResultM](seq/sequenceResultM.md)
* [sequenceResultA](seq/sequenceResultA.md)
* Transforms
* [ofChoice](result/ofChoice.md)

Expand Down
2 changes: 1 addition & 1 deletion gitbook/list/sequenceResultA.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ let checkIfAllPrime (numbers : int list) =
numbers
|> List.map isPrime // Result<bool, string> list
|> List.sequenceResultA // Result<bool list, string list>
|> Result.map (List.forall id) // shortened version of '|> Result.map (fun boolList -> boolList |> List.map (fun x -> x = true))'
|> Result.map (List.forall id) // shortened version of '|> Result.map (fun boolList -> boolList |> List.forAll (fun x -> x = true))
let a = [1; 2; 3; 4; 5;] |> checkIfAllPrime
// Error ["1 must be greater than 1"]
Expand Down
69 changes: 69 additions & 0 deletions gitbook/seq/sequenceResultA.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Seq.sequenceResultA

Namespace: `FsToolkit.ErrorHandling`

## Function Signature

```fsharp
seq<Result<'a, 'b>> -> Result<'a[], 'b[]>
```

This is applicative, collecting all errors. Compare the example below with [sequenceResultM](sequenceResultM.md).

See also Scott Wlaschin's [Understanding traverse and sequence](https://fsharpforfunandprofit.com/posts/elevated-world-4/).

## Examples

### Example 1

```fsharp
// string -> Result<int, string>
let tryParseInt str =
match Int32.TryParse str with
| true, x -> Ok x
| false, _ -> Error $"unable to parse '{str}' to integer"
["1"; "2"; "3"]
|> Seq.map tryParseInt
|> Seq.sequenceResultA
// Ok [| 1; 2; 3 |]
["1"; "foo"; "3"; "bar"]
|> Seq.map tryParseInt
|> Seq.sequenceResultA
// Error [| "unable to parse 'foo' to integer"
// "unable to parse 'bar' to integer" |]
```

### Example 2

```fsharp
// int -> Result<bool, string>
let isPrime (x: int) =
if x < 2 then Error $"{x} must be greater than 1"
elif x = 2 then Ok true
else
let rec isPrime' (x : int) (i : int) =
if i * i > x then Ok true
elif x % i = 0 then Ok false
else isPrime' x (i + 1)
isPrime' x 2
// seq<int> -> Result<bool, string[]>
let checkIfAllPrime (numbers: seq<int>) =
seq { for x in numbers -> isPrime x } // Result<bool, string> seq
|> Seq.sequenceResultA // Result<bool[], string[]>
|> Result.map (Seq.forall id) // shortened version of '|> Result.map (fun results -> results |> Array.forall (fun x -> x = true))'
let a = [| 1; 2; 3; 4; 5 |] |> checkIfAllPrime
// Error [| "1 must be greater than 1" |]
let b = [ 1; 2; 3; 4; 5; 0 ] |> checkIfAllPrime
// Error [| "1 must be greater than 1"; "0 must be greater than 1" |]
let a = seq { 2; 3; 4; 5 } |> checkIfAllPrime
// Ok false
let a = seq { 2; 3; 5 } |> checkIfAllPrime
// Ok true
```
16 changes: 16 additions & 0 deletions src/FsToolkit.ErrorHandling/Seq.fs
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,19 @@ let sequenceResultM (xs: seq<Result<'t, 'e>>) : Result<'t[], 'e> =
err <- e

if ok then Ok(acc.ToArray()) else Error err

let sequenceResultA (xs: seq<Result<'t, 'e>>) : Result<'t[], 'e[]> =
if isNull xs then
nullArg (nameof xs)

let oks = ResizeArray()
let errs = ResizeArray()

for x in xs do
match x with
| Ok r -> oks.Add r
| Error e -> errs.Add e

match errs.ToArray() with
| [||] -> Ok(oks.ToArray())
| errs -> Error errs
58 changes: 57 additions & 1 deletion tests/FsToolkit.ErrorHandling.Tests/Seq.fs
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,60 @@ let sequenceResultMTests =
Expect.equal counter 0 "evaluation of the sequence stops at the first error"
]

let allTests = testList "Seq Tests" [ sequenceResultMTests ]
let sequenceResultATests =
testList "Seq.sequenceResultA Tests" [
testCase "valid data only"
<| fun _ ->
let tweets = seq {
"Hi"
"Hello"
"Hola"
}
let expected = Ok [| for t in tweets -> tweet t |]

let actual = Seq.sequenceResultA (Seq.map Tweet.TryCreate tweets)

Expect.equal actual expected "Should yield an array of valid tweets"

testCase "valid and multiple invalid data"
<| fun _ ->
let tweets = [
""
"Hello"
aLongerInvalidTweet
]
let expected = Error [| emptyTweetErrMsg; longerTweetErrMsg |]

let actual = Seq.sequenceResultA (Seq.map Tweet.TryCreate tweets)

Expect.equal
actual
expected
"traverse the seq and return all the errors"

testCase "iterates exacly once"
<| fun _ ->
let mutable counter = 0

let tweets =
seq {
"Hi"
"Hello"
"Hola"
aLongerInvalidTweet

counter <-
counter
+ 1
}

let expected = Error longerTweetErrMsg

let actual = Seq.sequenceResultM (Seq.map Tweet.TryCreate tweets)

Expect.equal actual expected "traverse the seq and return all the errors"

Expect.equal counter 1 "evaluation of the sequence completes, with exactly one traversal"
]

let allTests = testList "Seq Tests" [ sequenceResultMTests; sequenceResultATests ]

0 comments on commit 93115e8

Please sign in to comment.