From 5840ac4ebc9b7bf9e0c760c77c8c9a12d1d2d932 Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Thu, 15 Feb 2024 01:49:51 +0000 Subject: [PATCH 1/7] Roll on '24 --- src/Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 1694e3306274147bdbd2aca00b394daf482dab2e Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Wed, 14 Feb 2024 23:55:06 +0000 Subject: [PATCH 2/7] refactor(Seq.sequenceResultM)!: Change Ok to Array --- src/FsToolkit.ErrorHandling/Seq.fs | 34 +++++++------ tests/FsToolkit.ErrorHandling.Tests/Seq.fs | 58 +++++++++------------- 2 files changed, 41 insertions(+), 51 deletions(-) diff --git a/src/FsToolkit.ErrorHandling/Seq.fs b/src/FsToolkit.ErrorHandling/Seq.fs index 8a3fe81f..281a2ba1 100644 --- a/src/FsToolkit.ErrorHandling/Seq.fs +++ b/src/FsToolkit.ErrorHandling/Seq.fs @@ -1,19 +1,21 @@ -namespace FsToolkit.ErrorHandling - [] -module Seq = +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() - 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 - ) + while ok + && e.MoveNext() do + match e.Current with + | Ok r -> acc.Add r + | Error e -> + ok <- false + err <- e - // Seq.cache prevents double evaluation in Seq.tail - loop (Seq.cache xs) [] + if ok then Ok(acc.ToArray()) else Error err diff --git a/tests/FsToolkit.ErrorHandling.Tests/Seq.fs b/tests/FsToolkit.ErrorHandling.Tests/Seq.fs index 522b7041..925d92f0 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 @@ -12,60 +11,50 @@ open Expecto #endif open SampleDomain open TestData -open TestHelpers -open System open FsToolkit.ErrorHandling - let sequenceResultMTests = testList "Seq.sequenceResultM Tests" [ - testCase "traverseResult with an empty sequence" + testCase "sequenceResult with an 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 "sequenceResult with a sequence of 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 "sequenceResult with few 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 "sequenceResult stops after first invalid data" <| fun _ -> let mutable counter = 0 @@ -81,12 +70,11 @@ 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" ] From 5ffe402d6383c9ea4da6849f7fdb18f191cf9c4f Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Thu, 15 Feb 2024 01:24:17 +0000 Subject: [PATCH 3/7] docs: sequenceResultM --- RELEASE_NOTES.md | 3 + gitbook/SUMMARY.md | 2 + gitbook/seq/sequenceResultM.md | 69 ++++++++++++++++++++++ tests/FsToolkit.ErrorHandling.Tests/Seq.fs | 10 ++-- 4 files changed, 79 insertions(+), 5 deletions(-) create mode 100644 gitbook/seq/sequenceResultM.md diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index ae4f28e5..62cdc913 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,3 +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) + ### 4.15.1 - January 15, 2024 - [Doc updates](https://github.com/demystifyfp/FsToolkit.ErrorHandling/pull/247) Credits @1eyewonder diff --git a/gitbook/SUMMARY.md b/gitbook/SUMMARY.md index b28bc49c..af5244eb 100644 --- a/gitbook/SUMMARY.md +++ b/gitbook/SUMMARY.md @@ -25,6 +25,8 @@ * [sequenceResultM](list/sequenceResultM.md) * [traverseResultA](list/traverseResultA.md) * [sequenceResultA](list/sequenceResultA.md) + * Seqs + * [sequenceResultM](seq/sequenceResultM.md) * Transforms * [ofChoice](result/ofChoice.md) diff --git a/gitbook/seq/sequenceResultM.md b/gitbook/seq/sequenceResultM.md new file mode 100644 index 00000000..e2f821e1 --- /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 // Result list + |> 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/tests/FsToolkit.ErrorHandling.Tests/Seq.fs b/tests/FsToolkit.ErrorHandling.Tests/Seq.fs index 925d92f0..908d138d 100644 --- a/tests/FsToolkit.ErrorHandling.Tests/Seq.fs +++ b/tests/FsToolkit.ErrorHandling.Tests/Seq.fs @@ -9,13 +9,13 @@ open Fable.Mocha #if !FABLE_COMPILER open Expecto #endif +open FsToolkit.ErrorHandling open SampleDomain open TestData -open FsToolkit.ErrorHandling let sequenceResultMTests = testList "Seq.sequenceResultM Tests" [ - testCase "sequenceResult with an empty sequence" + testCase "empty sequence" <| fun _ -> let tweets = Seq.empty let expected = Ok [||] @@ -24,7 +24,7 @@ let sequenceResultMTests = Expect.equal actual expected "Should have an empty list of valid tweets" - testCase "sequenceResult with a sequence of valid data" + testCase "valid data" <| fun _ -> let tweets = seq { @@ -39,7 +39,7 @@ let sequenceResultMTests = Expect.equal actual expected "Should have a list of valid tweets" - testCase "sequenceResult with few invalid data" + testCase "valid and invalid data" <| fun _ -> let tweets = seq { @@ -54,7 +54,7 @@ let sequenceResultMTests = Expect.equal actual expected "traverse the sequence and return the first error" - testCase "sequenceResult stops after first invalid data" + testCase "stops after first invalid data" <| fun _ -> let mutable counter = 0 From d145e21ee69f0a307df164a8ad982782fc31143e Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Thu, 15 Feb 2024 01:27:44 +0000 Subject: [PATCH 4/7] feat(Seq): sequenceResultA --- RELEASE_NOTES.md | 1 + gitbook/SUMMARY.md | 1 + gitbook/list/sequenceResultA.md | 2 +- gitbook/seq/sequenceResultA.md | 69 ++++++++++++++++++++++ src/FsToolkit.ErrorHandling/Seq.fs | 16 +++++ tests/FsToolkit.ErrorHandling.Tests/Seq.fs | 66 ++++++++++++++++++++- 6 files changed, 153 insertions(+), 2 deletions(-) create mode 100644 gitbook/seq/sequenceResultA.md diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 62cdc913..1dc56cce 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -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 diff --git a/gitbook/SUMMARY.md b/gitbook/SUMMARY.md index af5244eb..3aacc637 100644 --- a/gitbook/SUMMARY.md +++ b/gitbook/SUMMARY.md @@ -27,6 +27,7 @@ * [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/src/FsToolkit.ErrorHandling/Seq.fs b/src/FsToolkit.ErrorHandling/Seq.fs index 281a2ba1..da3ad5ae 100644 --- a/src/FsToolkit.ErrorHandling/Seq.fs +++ b/src/FsToolkit.ErrorHandling/Seq.fs @@ -19,3 +19,19 @@ let sequenceResultM (xs: seq>) : Result<'t[], 'e> = 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 908d138d..c6d88f2f 100644 --- a/tests/FsToolkit.ErrorHandling.Tests/Seq.fs +++ b/tests/FsToolkit.ErrorHandling.Tests/Seq.fs @@ -79,4 +79,68 @@ 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.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 + ] From 76286ccb637617427e030c91af1ef91de5c21063 Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Thu, 15 Feb 2024 02:03:17 +0000 Subject: [PATCH 5/7] f sequenceResultM docs --- gitbook/seq/sequenceResultM.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gitbook/seq/sequenceResultM.md b/gitbook/seq/sequenceResultM.md index e2f821e1..681bc0dd 100644 --- a/gitbook/seq/sequenceResultM.md +++ b/gitbook/seq/sequenceResultM.md @@ -48,11 +48,11 @@ let isPrime (x: int) = else isPrime' x (i + 1) isPrime' x 2 -// int seq -> Result +// int seq -> Result let checkIfAllPrime (numbers: seq) = numbers - |> Seq.map isPrime // Result list - |> Seq.sequenceResultM // Result + |> 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 From d08822690777a4f5a54babfd7a8eb373bc92c055 Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Thu, 15 Feb 2024 23:11:57 +0000 Subject: [PATCH 6/7] Supress compile error --- build/build.fsproj | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 + From 9d38bc2a95190c1b5584a5d67713ec291b63c0bb Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Thu, 15 Feb 2024 23:50:57 +0000 Subject: [PATCH 7/7] Fix proposed version --- RELEASE_NOTES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 1dc56cce..db31623b 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,4 +1,4 @@ -### 4.16.0 +### 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)