diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index ae4f28e5..db31623b 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -1,3 +1,7 @@
+### 5.0.0-alpha.1
+- [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
diff --git a/build/build.fsproj b/build/build.fsproj
index 8a227d7a..e05d688c 100644
--- a/build/build.fsproj
+++ b/build/build.fsproj
@@ -4,10 +4,12 @@
Exe
net7.0
false
+
+ false
-
\ No newline at end of file
+
diff --git a/gitbook/SUMMARY.md b/gitbook/SUMMARY.md
index b28bc49c..3aacc637 100644
--- a/gitbook/SUMMARY.md
+++ b/gitbook/SUMMARY.md
@@ -25,6 +25,9 @@
* [sequenceResultM](list/sequenceResultM.md)
* [traverseResultA](list/traverseResultA.md)
* [sequenceResultA](list/sequenceResultA.md)
+ * Seqs
+ * [sequenceResultM](seq/sequenceResultM.md)
+ * [sequenceResultA](seq/sequenceResultA.md)
* Transforms
* [ofChoice](result/ofChoice.md)
diff --git a/gitbook/list/sequenceResultA.md b/gitbook/list/sequenceResultA.md
index a2e0dacc..f904d42b 100644
--- a/gitbook/list/sequenceResultA.md
+++ b/gitbook/list/sequenceResultA.md
@@ -59,7 +59,7 @@ let checkIfAllPrime (numbers : int list) =
numbers
|> List.map isPrime // Result list
|> List.sequenceResultA // Result
- |> 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"]
diff --git a/gitbook/seq/sequenceResultA.md b/gitbook/seq/sequenceResultA.md
new file mode 100644
index 00000000..60b6761b
--- /dev/null
+++ b/gitbook/seq/sequenceResultA.md
@@ -0,0 +1,69 @@
+# Seq.sequenceResultA
+
+Namespace: `FsToolkit.ErrorHandling`
+
+## Function Signature
+
+```fsharp
+seq> -> 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
+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
+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 -> Result
+let checkIfAllPrime (numbers: seq) =
+ seq { for x in numbers -> isPrime x } // Result seq
+ |> Seq.sequenceResultA // Result
+ |> 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
+```
diff --git a/gitbook/seq/sequenceResultM.md b/gitbook/seq/sequenceResultM.md
new file mode 100644
index 00000000..681bc0dd
--- /dev/null
+++ b/gitbook/seq/sequenceResultM.md
@@ -0,0 +1,69 @@
+# Seq.sequenceResultM
+
+Namespace: `FsToolkit.ErrorHandling`
+
+## Function Signature
+
+```fsharp
+seq> -> Result<'a[], 'b>
+```
+
+This is monadic, stopping on the first error. Compare the example below with [sequenceResultA](sequenceResultA.md).
+
+See also Scott Wlaschin's [Understanding traverse and sequence](https://fsharpforfunandprofit.com/posts/elevated-world-4/).
+
+## Examples
+
+### Example 1
+
+```fsharp
+// string -> Result
+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.sequenceResultM
+// Ok [| 1; 2; 3 |]
+
+seq { "1"; "foo"; "3"; "bar" }
+|> Seq.map tryParseInt
+|> Seq.sequenceResultM
+// Error "unable to parse 'foo' to integer"
+```
+
+### Example 2
+
+```fsharp
+// int -> Result
+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
+
+// int seq -> Result
+let checkIfAllPrime (numbers: seq) =
+ numbers
+ |> Seq.map isPrime // seq>
+ |> Seq.sequenceResultM // Result
+ |> Result.map (Array.forall id) // shortened version of '|> Result.map (fun bools -> bools |> 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" |]
+
+let a = seq { 2; 3; 4; 5 } |> checkIfAllPrime
+// Ok false
+
+let a = [2; 3; 5;] |> checkIfAllPrime
+// Ok true
+```
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
index c4be768f..65508527 100644
--- a/src/Directory.Build.props
+++ b/src/Directory.Build.props
@@ -4,7 +4,7 @@
FsToolkit.ErrorHandling is an extensive utility library based around the F# Result type, enabling consistent and powerful error handling.
demystifyfp, TheAngryByrd
- Copyright © 2018-23
+ Copyright © 2018-24
https://demystifyfp.gitbook.io/fstoolkit-errorhandling
MIT
README.md
diff --git a/src/FsToolkit.ErrorHandling/Seq.fs b/src/FsToolkit.ErrorHandling/Seq.fs
index 8a3fe81f..da3ad5ae 100644
--- a/src/FsToolkit.ErrorHandling/Seq.fs
+++ b/src/FsToolkit.ErrorHandling/Seq.fs
@@ -1,19 +1,37 @@
-namespace FsToolkit.ErrorHandling
-
[]
-module Seq =
-
- let sequenceResultM (xs: seq>) : Result<'t seq, 'e> =
- let rec loop xs ts =
- match Seq.tryHead xs with
- | Some x ->
- x
- |> Result.bind (fun t -> loop (Seq.tail xs) (t :: ts))
- | None ->
- Ok(
- List.rev ts
- |> List.toSeq
- )
-
- // Seq.cache prevents double evaluation in Seq.tail
- loop (Seq.cache xs) []
+module FsToolkit.ErrorHandling.Seq
+
+let sequenceResultM (xs: seq>) : Result<'t[], 'e> =
+ if isNull xs then
+ nullArg (nameof xs)
+
+ let acc = ResizeArray()
+ let mutable err = Unchecked.defaultof<_>
+ let mutable ok = true
+ use e = xs.GetEnumerator()
+
+ while ok
+ && e.MoveNext() do
+ match e.Current with
+ | Ok r -> acc.Add r
+ | Error e ->
+ ok <- false
+ err <- e
+
+ if ok then Ok(acc.ToArray()) else Error err
+
+let sequenceResultA (xs: seq>) : 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
diff --git a/tests/FsToolkit.ErrorHandling.Tests/Seq.fs b/tests/FsToolkit.ErrorHandling.Tests/Seq.fs
index 522b7041..c6d88f2f 100644
--- a/tests/FsToolkit.ErrorHandling.Tests/Seq.fs
+++ b/tests/FsToolkit.ErrorHandling.Tests/Seq.fs
@@ -1,6 +1,5 @@
module SeqTests
-
#if FABLE_COMPILER_PYTHON
open Fable.Pyxpecto
#endif
@@ -10,62 +9,52 @@ open Fable.Mocha
#if !FABLE_COMPILER
open Expecto
#endif
+open FsToolkit.ErrorHandling
open SampleDomain
open TestData
-open TestHelpers
-open System
-open FsToolkit.ErrorHandling
-
let sequenceResultMTests =
testList "Seq.sequenceResultM Tests" [
- testCase "traverseResult with an empty sequence"
+ testCase "empty sequence"
<| fun _ ->
- let tweets = []
- let expected = Ok []
+ let tweets = Seq.empty
+ let expected = Ok [||]
- let actual =
- Seq.sequenceResultM (Seq.map Tweet.TryCreate tweets)
- |> Result.map Seq.toList
+ let actual = Seq.sequenceResultM (Seq.map Tweet.TryCreate tweets)
Expect.equal actual expected "Should have an empty list of valid tweets"
- testCase "traverseResult with a sequence of valid data"
+ testCase "valid data"
<| fun _ ->
- let tweets = [
- "Hi"
- "Hello"
- "Hola"
- ]
+ let tweets =
+ seq {
+ "Hi"
+ "Hello"
+ "Hola"
+ }
- let expected =
- List.map tweet tweets
- |> Ok
+ let expected = Ok [| for x in tweets -> tweet x |]
- let actual =
- Seq.sequenceResultM (Seq.map Tweet.TryCreate tweets)
- |> Result.map Seq.toList
+ let actual = Seq.sequenceResultM (Seq.map Tweet.TryCreate tweets)
Expect.equal actual expected "Should have a list of valid tweets"
- testCase "sequenceResultM with few invalid data"
+ testCase "valid and invalid data"
<| fun _ ->
let tweets =
- [
+ seq {
""
"Hello"
aLongerInvalidTweet
- ]
- :> seq<_>
+ }
+
+ let expected = Error emptyTweetErrMsg
let actual = Seq.sequenceResultM (Seq.map Tweet.TryCreate tweets)
- Expect.equal
- actual
- (Error emptyTweetErrMsg)
- "traverse the sequence and return the first error"
+ Expect.equal actual expected "traverse the sequence and return the first error"
- testCase "sequenceResultM stops after first invalid data"
+ testCase "stops after first invalid data"
<| fun _ ->
let mutable counter = 0
@@ -81,14 +70,77 @@ let sequenceResultMTests =
+ 1
}
+ let expected = Error longerTweetErrMsg
+
let actual = Seq.sequenceResultM (Seq.map Tweet.TryCreate tweets)
- Expect.equal
- actual
- (Error longerTweetErrMsg)
- "traverse the sequence and return the first error"
+ Expect.equal actual expected "traverse the sequence and return the first error"
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.sequenceResultA (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 exactly once"
+ ]
+
+let allTests =
+ testList "Seq Tests" [
+ sequenceResultMTests
+ sequenceResultATests
+ ]