Skip to content

Commit b8cab35

Browse files
committed
feat(Seq): sequenceResultA
1 parent 2f234ba commit b8cab35

File tree

6 files changed

+153
-2
lines changed

6 files changed

+153
-2
lines changed

RELEASE_NOTES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
### 4.16.0
22
- [refactor!: Seq.sequenceResultM returns Array instead of seq](https://github.com/demystifyfp/FsToolkit.ErrorHandling/pull/255) [@bartelink](https://github.com/bartelink)
3+
- [feat(Seq): sequenceResultA](https://github.com/demystifyfp/FsToolkit.ErrorHandling/pull/255) [@bartelink](https://github.com/bartelink)
34

45
### 4.15.1 - January 15, 2024
56
- [Doc updates](https://github.com/demystifyfp/FsToolkit.ErrorHandling/pull/247) Credits @1eyewonder

gitbook/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
* [sequenceResultA](list/sequenceResultA.md)
2828
* Seqs
2929
* [sequenceResultM](seq/sequenceResultM.md)
30+
* [sequenceResultA](seq/sequenceResultA.md)
3031
* Transforms
3132
* [ofChoice](result/ofChoice.md)
3233

gitbook/list/sequenceResultA.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ let checkIfAllPrime (numbers : int list) =
5959
numbers
6060
|> List.map isPrime // Result<bool, string> list
6161
|> List.sequenceResultA // Result<bool list, string list>
62-
|> Result.map (List.forall id) // shortened version of '|> Result.map (fun boolList -> boolList |> List.map (fun x -> x = true))'
62+
|> Result.map (List.forall id) // shortened version of '|> Result.map (fun boolList -> boolList |> List.forAll (fun x -> x = true))
6363
6464
let a = [1; 2; 3; 4; 5;] |> checkIfAllPrime
6565
// Error ["1 must be greater than 1"]

gitbook/seq/sequenceResultA.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Seq.sequenceResultA
2+
3+
Namespace: `FsToolkit.ErrorHandling`
4+
5+
## Function Signature
6+
7+
```fsharp
8+
seq<Result<'a, 'b>> -> Result<'a[], 'b[]>
9+
```
10+
11+
This is applicative, collecting all errors. Compare the example below with [sequenceResultM](sequenceResultM.md).
12+
13+
See also Scott Wlaschin's [Understanding traverse and sequence](https://fsharpforfunandprofit.com/posts/elevated-world-4/).
14+
15+
## Examples
16+
17+
### Example 1
18+
19+
```fsharp
20+
// string -> Result<int, string>
21+
let tryParseInt str =
22+
match Int32.TryParse str with
23+
| true, x -> Ok x
24+
| false, _ -> Error $"unable to parse '{str}' to integer"
25+
26+
["1"; "2"; "3"]
27+
|> Seq.map tryParseInt
28+
|> Seq.sequenceResultA
29+
// Ok [| 1; 2; 3 |]
30+
31+
["1"; "foo"; "3"; "bar"]
32+
|> Seq.map tryParseInt
33+
|> Seq.sequenceResultA
34+
// Error [| "unable to parse 'foo' to integer"
35+
// "unable to parse 'bar' to integer" |]
36+
```
37+
38+
### Example 2
39+
40+
```fsharp
41+
// int -> Result<bool, string>
42+
let isPrime (x: int) =
43+
if x < 2 then Error $"{x} must be greater than 1"
44+
elif x = 2 then Ok true
45+
else
46+
let rec isPrime' (x : int) (i : int) =
47+
if i * i > x then Ok true
48+
elif x % i = 0 then Ok false
49+
else isPrime' x (i + 1)
50+
isPrime' x 2
51+
52+
// seq<int> -> Result<bool, string[]>
53+
let checkIfAllPrime (numbers: seq<int>) =
54+
seq { for x in numbers -> isPrime x } // Result<bool, string> seq
55+
|> Seq.sequenceResultA // Result<bool[], string[]>
56+
|> Result.map (Seq.forall id) // shortened version of '|> Result.map (fun results -> results |> Array.forall (fun x -> x = true))'
57+
58+
let a = [| 1; 2; 3; 4; 5 |] |> checkIfAllPrime
59+
// Error [| "1 must be greater than 1" |]
60+
61+
let b = [ 1; 2; 3; 4; 5; 0 ] |> checkIfAllPrime
62+
// Error [| "1 must be greater than 1"; "0 must be greater than 1" |]
63+
64+
let a = seq { 2; 3; 4; 5 } |> checkIfAllPrime
65+
// Ok false
66+
67+
let a = seq { 2; 3; 5 } |> checkIfAllPrime
68+
// Ok true
69+
```

src/FsToolkit.ErrorHandling/Seq.fs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,19 @@ let sequenceResultM (xs: seq<Result<'t, 'e>>) : Result<'t[], 'e> =
1919
err <- e
2020

2121
if ok then Ok(acc.ToArray()) else Error err
22+
23+
let sequenceResultA (xs: seq<Result<'t, 'e>>) : Result<'t[], 'e[]> =
24+
if isNull xs then
25+
nullArg (nameof xs)
26+
27+
let oks = ResizeArray()
28+
let errs = ResizeArray()
29+
30+
for x in xs do
31+
match x with
32+
| Ok r -> oks.Add r
33+
| Error e -> errs.Add e
34+
35+
match errs.ToArray() with
36+
| [||] -> Ok(oks.ToArray())
37+
| errs -> Error errs

tests/FsToolkit.ErrorHandling.Tests/Seq.fs

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,4 +79,68 @@ let sequenceResultMTests =
7979
Expect.equal counter 0 "evaluation of the sequence stops at the first error"
8080
]
8181

82-
let allTests = testList "Seq Tests" [ sequenceResultMTests ]
82+
let sequenceResultATests =
83+
testList "Seq.sequenceResultA Tests" [
84+
testCase "valid data only"
85+
<| fun _ ->
86+
let tweets =
87+
seq {
88+
"Hi"
89+
"Hello"
90+
"Hola"
91+
}
92+
93+
let expected = Ok [| for t in tweets -> tweet t |]
94+
95+
let actual = Seq.sequenceResultA (Seq.map Tweet.TryCreate tweets)
96+
97+
Expect.equal actual expected "Should yield an array of valid tweets"
98+
99+
testCase "valid and multiple invalid data"
100+
<| fun _ ->
101+
let tweets = [
102+
""
103+
"Hello"
104+
aLongerInvalidTweet
105+
]
106+
107+
let expected =
108+
Error [|
109+
emptyTweetErrMsg
110+
longerTweetErrMsg
111+
|]
112+
113+
let actual = Seq.sequenceResultA (Seq.map Tweet.TryCreate tweets)
114+
115+
Expect.equal actual expected "traverse the seq and return all the errors"
116+
117+
testCase "iterates exacly once"
118+
<| fun _ ->
119+
let mutable counter = 0
120+
121+
let tweets =
122+
seq {
123+
"Hi"
124+
"Hello"
125+
"Hola"
126+
aLongerInvalidTweet
127+
128+
counter <-
129+
counter
130+
+ 1
131+
}
132+
133+
let expected = Error [| longerTweetErrMsg |]
134+
135+
let actual = Seq.sequenceResultA (Seq.map Tweet.TryCreate tweets)
136+
137+
Expect.equal actual expected "traverse the seq and return all the errors"
138+
139+
Expect.equal counter 1 "evaluation of the sequence completes exactly once"
140+
]
141+
142+
let allTests =
143+
testList "Seq Tests" [
144+
sequenceResultMTests
145+
sequenceResultATests
146+
]

0 commit comments

Comments
 (0)