diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 7b5e990e..4cc5917f 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,3 +1,8 @@ + +### 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.18.0 - October 23, 2024 - [Add Array errorhandling](https://github.com/demystifyfp/FsToolkit.ErrorHandling/pull/279) Credits @DashieTM 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/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 index 6364913a..60b6761b 100644 --- a/gitbook/seq/sequenceResultA.md +++ b/gitbook/seq/sequenceResultA.md @@ -5,11 +5,9 @@ Namespace: `FsToolkit.ErrorHandling` ## Function Signature ```fsharp -Result<'a, 'b> seq -> Result<'a seq, 'b seq> +seq> -> Result<'a[], 'b[]> ``` -Note that `sequence` is the same as `traverse id`. See also [Seq.traverseResultA](traverseResultA.md). - 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/). @@ -23,30 +21,27 @@ See also Scott Wlaschin's [Understanding traverse and sequence](https://fsharpfo let tryParseInt str = match Int32.TryParse str with | true, x -> Ok x - | false, _ -> - Error (sprintf "unable to parse '%s' to integer" str) + | false, _ -> Error $"unable to parse '{str}' to integer" ["1"; "2"; "3"] |> Seq.map tryParseInt |> Seq.sequenceResultA -// Ok [1; 2; 3] +// 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"] +// 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 - sprintf "%i must be greater than 1" x |> Error - elif - x = 2 then Ok true +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 @@ -54,22 +49,21 @@ let isPrime (x : int) = else isPrime' x (i + 1) isPrime' x 2 -// int seq -> Result -let checkIfAllPrime (numbers : int seq) = - numbers - |> Seq.map isPrime // Result seq - |> Seq.sequenceResultA // Result - |> Result.map (Seq.forall id) // shortened version of '|> Result.map (fun boolSeq -> boolSeq |> Seq.map (fun x -> x = true))' +// 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 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 b = [ 1; 2; 3; 4; 5; 0 ] |> checkIfAllPrime +// Error [| "1 must be greater than 1"; "0 must be greater than 1" |] -let a = [2; 3; 4; 5;] |> checkIfAllPrime +let a = seq { 2; 3; 4; 5 } |> checkIfAllPrime // Ok false -let a = [2; 3; 5;] |> checkIfAllPrime +let a = seq { 2; 3; 5 } |> checkIfAllPrime // Ok true ``` diff --git a/gitbook/seq/sequenceResultM.md b/gitbook/seq/sequenceResultM.md index 4dddaa43..681bc0dd 100644 --- a/gitbook/seq/sequenceResultM.md +++ b/gitbook/seq/sequenceResultM.md @@ -5,11 +5,9 @@ Namespace: `FsToolkit.ErrorHandling` ## Function Signature ```fsharp -Result<'a, 'b> seq -> Result<'a seq, 'b> +seq> -> Result<'a[], 'b> ``` -Note that `sequence` is the same as `traverse id`. See also [Seq.traverseResultM](traverseResultM.md). - 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/). @@ -23,15 +21,14 @@ See also Scott Wlaschin's [Understanding traverse and sequence](https://fsharpfo let tryParseInt str = match Int32.TryParse str with | true, x -> Ok x - | false, _ -> - Error (sprintf "unable to parse '%s' to integer" str) + | false, _ -> Error $"unable to parse '{str}' to integer" ["1"; "2"; "3"] |> Seq.map tryParseInt |> Seq.sequenceResultM -// Ok [1; 2; 3] +// Ok [| 1; 2; 3 |] -["1"; "foo"; "3"; "bar"] +seq { "1"; "foo"; "3"; "bar" } |> Seq.map tryParseInt |> Seq.sequenceResultM // Error "unable to parse 'foo' to integer" @@ -41,11 +38,9 @@ let tryParseInt str = ```fsharp // int -> Result -let isPrime (x : int) = - if x < 2 then - sprintf "%i must be greater than 1" x |> Error - elif - x = 2 then Ok true +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 @@ -53,20 +48,20 @@ let isPrime (x : int) = else isPrime' x (i + 1) isPrime' x 2 -// int seq -> Result -let checkIfAllPrime (numbers : int seq) = +// int seq -> Result +let checkIfAllPrime (numbers: seq) = numbers - |> Seq.map isPrime // Result seq - |> Seq.sequenceResultM // Result - |> Result.map (Seq.forall id) // shortened version of '|> Result.map (fun boolSeq -> boolSeq |> Seq.map (fun x -> x = true))'; + |> 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 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 b = [| 1; 2; 3; 4; 5; 0 |] |> checkIfAllPrime +// Error [| "1 must be greater than 1" |] -let a = [2; 3; 4; 5;] |> checkIfAllPrime +let a = seq { 2; 3; 4; 5 } |> checkIfAllPrime // Ok false let a = [2; 3; 5;] |> checkIfAllPrime 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 89d47916..9cee77a4 100644 --- a/src/FsToolkit.ErrorHandling/Seq.fs +++ b/src/FsToolkit.ErrorHandling/Seq.fs @@ -1,7 +1,5 @@ -namespace FsToolkit.ErrorHandling - [] -module Seq = +module FsToolkit.ErrorHandling.Seq /// /// Applies a function to each element of a sequence and returns a single result diff --git a/tests/FsToolkit.ErrorHandling.Tests/Seq.fs b/tests/FsToolkit.ErrorHandling.Tests/Seq.fs index 2059337d..516be12d 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,6 +9,7 @@ open Fable.Mocha #if !FABLE_COMPILER open Expecto #endif +open FsToolkit.ErrorHandling open SampleDomain open TestData open TestHelpers @@ -130,12 +130,39 @@ let sequenceResultMTests = 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 "Should have an empty list of valid tweets" - testCase "sequenceResultM with few invalid data should exit early" + testCase "valid data" + <| fun _ -> + let tweets = + seq { + "Hi" + "Hello" + "Hola" + } + + let expected = Ok [| for x in tweets -> tweet x |] + + let actual = Seq.sequenceResultM (Seq.map Tweet.TryCreate tweets) + + Expect.equal actual expected "Should have a list of valid tweets" + + testCase "valid and invalid data" + <| fun _ -> + let tweets = + seq { + "" + "Hello" + aLongerInvalidTweet + } + + let expected = Error emptyTweetErrMsg + + let actual = Seq.sequenceResultM (Seq.map Tweet.TryCreate tweets) + + Expect.equal actual expected "traverse the sequence and return the first error" + + testCase "stops after first invalid data" <| fun _ -> let mutable lastValue = null @@ -185,12 +212,11 @@ let sequenceOptionMTests = "Hola" } - let expected = Seq.toList tweets - let actual = Seq.sequenceOptionM (Seq.map tryTweetOption tweets) + let expected = Error longerTweetErrMsg - let actual = - Expect.wantSome actual "Expected result to be Some" - |> Seq.toList + let actual = Seq.sequenceResultM (Seq.map Tweet.TryCreate tweets) + + Expect.equal actual expected "traverse the sequence and return the first error" Expect.equal actual expected "Should have a sequence of valid tweets" @@ -237,55 +263,9 @@ let sequenceOptionMTests = Expect.equal actual None "traverse the sequence and return none" ] -let traverseResultATests = - testList "Seq.traverseResultA Tests" [ - testCase "traverseResultA with a sequence of valid data" - <| fun _ -> - let tweets = - seq { - "Hi" - "Hello" - "Hola" - } - - let expected = - Seq.map tweet tweets - |> Seq.toList - - let actual = Seq.traverseResultA Tweet.TryCreate tweets - - let actual = - Expect.wantOk actual "Expected result to be Ok" - |> Seq.toList - - Expect.equal actual expected "Should have a sequence of valid tweets" - - testCase "traverseResultA with few invalid data" - <| fun _ -> - let tweets = - seq { - "" - "Hello" - aLongerInvalidTweet - } - - let actual = Seq.traverseResultA Tweet.TryCreate tweets - - let actual = - Expect.wantError actual "Expected result to be Error" - |> Seq.toList - - let expected = [ - emptyTweetErrMsg - longerTweetErrMsg - ] - - Expect.equal actual expected "traverse the sequence and return all the errors" - ] - let sequenceResultATests = testList "Seq.sequenceResultA Tests" [ - testCase "traverseResult with a sequence of valid data" + testCase "valid data only" <| fun _ -> let tweets = seq { @@ -294,440 +274,57 @@ let sequenceResultATests = "Hola" } - let expected = - Seq.map tweet tweets - |> Seq.toList + let expected = Ok [| for t in tweets -> tweet t |] let actual = Seq.sequenceResultA (Seq.map Tweet.TryCreate tweets) - let actual = - Expect.wantOk actual "Expected result to be Ok" - |> Seq.toList - - Expect.equal actual expected "Should have a sequence of valid tweets" + Expect.equal actual expected "Should yield an array of valid tweets" - testCase "sequenceResultM with few invalid data" + testCase "valid and multiple invalid data" <| fun _ -> - let tweets = - seq { - "" - "Hello" - aLongerInvalidTweet - } - - let actual = Seq.sequenceResultA (Seq.map Tweet.TryCreate tweets) - - let actual = - Expect.wantError actual "Expected result to be Error" - |> Seq.toList - - let expected = [ - emptyTweetErrMsg - longerTweetErrMsg - ] - - Expect.equal actual expected "traverse the sequence and return all the errors" - ] - -let userId1 = Guid.NewGuid() -let userId2 = Guid.NewGuid() -let userId3 = Guid.NewGuid() -let userId4 = Guid.NewGuid() - -let traverseAsyncResultMTests = - - let userIds = - seq { - userId1 - userId2 - userId3 - } - |> Seq.map UserId - - testList "Seq.traverseAsyncResultM Tests" [ - testCaseAsync "traverseAsyncResultM with a sequence of valid data" - <| async { - let expected = - userIds - |> Seq.map (fun (UserId user) -> (newPostId, user)) - |> Seq.toList - - let! actual = Seq.traverseAsyncResultM (notifyNewPostSuccess (PostId newPostId)) userIds - - let actual = - Expect.wantOk actual "Expected result to be Ok" - |> Seq.toList - - Expect.equal actual expected "Should have a sequence of valid data" - } - - testCaseAsync "traverseResultA with few invalid data" - <| async { - let expected = sprintf "error: %s" (userId1.ToString()) - - let actual = - Seq.traverseAsyncResultM (notifyNewPostFailure (PostId newPostId)) userIds - - do! Expect.hasAsyncErrorValue expected actual - } - ] - -let traverseAsyncOptionMTests = - - let userIds = - seq { - userId1 - userId2 - userId3 - } - - testList "Seq.traverseAsyncOptionM Tests" [ - testCaseAsync "traverseAsyncOptionM with a sequence of valid data" - <| async { - let expected = - userIds - |> Seq.toList - |> Some - - let f x = async { return Some x } - - let actual = - Seq.traverseAsyncOptionM f userIds - |> AsyncOption.map Seq.toList - - match expected with - | Some e -> do! Expect.hasAsyncSomeValue e actual - | None -> failwith "Error in the test case code" - } - - testCaseAsync "traverseOptionA with few invalid data" - <| async { - let expected = None - let f _ = async { return None } - let actual = Seq.traverseAsyncOptionM f userIds - - match expected with - | Some _ -> failwith "Error in the test case code" - | None -> do! Expect.hasAsyncNoneValue actual - } - ] - -let notifyFailure (PostId _) (UserId uId) = - async { - if - (uId = userId1 - || uId = userId3) - then - return - sprintf "error: %s" (uId.ToString()) - |> Error - else - return Ok() - } - - -let traverseAsyncResultATests = - let userIds = - seq { - userId1 - userId2 - userId3 - userId4 - } - |> Seq.map UserId - - testList "Seq.traverseAsyncResultA Tests" [ - testCaseAsync "traverseAsyncResultA with a sequence of valid data" - <| async { - let expected = - userIds - |> Seq.map (fun (UserId user) -> (newPostId, user)) - |> Seq.toList - - let! actual = Seq.traverseAsyncResultA (notifyNewPostSuccess (PostId newPostId)) userIds - - let actual = - Expect.wantOk actual "Expected result to be Ok" - |> Seq.toList - - Expect.equal actual expected "Should have a sequence of valid data" - } - - testCaseAsync "traverseResultA with few invalid data" - <| async { - let expected = [ - sprintf "error: %s" (userId1.ToString()) - sprintf "error: %s" (userId3.ToString()) + let tweets = [ + "" + "Hello" + aLongerInvalidTweet ] - let! actual = Seq.traverseAsyncResultA (notifyFailure (PostId newPostId)) userIds - - let actual = - Expect.wantError actual "Expected result to be Error" - |> Seq.toList - - Expect.equal actual expected "Should have a sequence of errors" - } - ] - - -let sequenceAsyncResultMTests = - let userIds = - seq { - userId1 - userId2 - userId3 - userId4 - } - |> Seq.map UserId - - testList "Seq.sequenceAsyncResultM Tests" [ - testCaseAsync "sequenceAsyncResultM with a sequence of valid data" - <| async { - let expected = - userIds - |> Seq.map (fun (UserId user) -> (newPostId, user)) - |> Seq.toList - - let! actual = - Seq.map (notifyNewPostSuccess (PostId newPostId)) userIds - |> Seq.sequenceAsyncResultM - - let actual = - Expect.wantOk actual "Expected result to be Ok" - |> Seq.toList - - Expect.equal actual expected "Should have a sequence of valid data" - } - - testCaseAsync "sequenceAsyncResultM with few invalid data" - <| async { - let expected = sprintf "error: %s" (userId1.ToString()) - - let actual = - Seq.map (notifyFailure (PostId newPostId)) userIds - |> Seq.sequenceAsyncResultM - - do! Expect.hasAsyncErrorValue expected actual - } - ] - -let sequenceAsyncOptionMTests = - - let userIds = - seq { - userId1 - userId2 - userId3 - } - - testList "Seq.sequenceAsyncOptionM Tests" [ - testCaseAsync "sequenceAsyncOptionM with a sequence of valid data" - <| async { let expected = - Seq.toList userIds - |> Some - - let f x = async { return Some x } + Error [| + emptyTweetErrMsg + longerTweetErrMsg + |] - let actual = - Seq.map f userIds - |> Seq.sequenceAsyncOptionM - |> AsyncOption.map Seq.toList - - match expected with - | Some e -> do! Expect.hasAsyncSomeValue e actual - | None -> failwith "Error in the test case code" - } - - testCaseAsync "sequenceOptionA with few invalid data" - <| async { - let expected = None - let f _ = async { return None } - - let actual = - Seq.map f userIds - |> Seq.sequenceAsyncOptionM - - match expected with - | Some _ -> failwith "Error in the test case code" - | None -> do! Expect.hasAsyncNoneValue actual - } - ] - -let sequenceAsyncResultATests = - let userIds = - seq { - userId1 - userId2 - userId3 - userId4 - } - |> Seq.map UserId - - testList "Seq.sequenceAsyncResultA Tests" [ - testCaseAsync "sequenceAsyncResultA with a sequence of valid data" - <| async { - let expected = - userIds - |> Seq.map (fun (UserId user) -> (newPostId, user)) - |> Seq.toList - - let actual = - Seq.map (notifyNewPostSuccess (PostId newPostId)) userIds - |> Seq.sequenceAsyncResultA - |> AsyncResult.map Seq.toList - - do! Expect.hasAsyncOkValue expected actual - } - - testCaseAsync "sequenceAsyncResultA with few invalid data" - <| async { - let expected = [ - sprintf "error: %s" (userId1.ToString()) - sprintf "error: %s" (userId3.ToString()) - ] - - let! actual = - Seq.map (notifyFailure (PostId newPostId)) userIds - |> Seq.sequenceAsyncResultA - |> AsyncResult.mapError Seq.toList - - let actual = Expect.wantError actual "Expected result to be Error" - Expect.equal actual expected "Should have a sequence of errors" - } - ] - -#if !FABLE_COMPILER -let traverseVOptionMTests = - testList "Seq.traverseVOptionM Tests" [ - let tryTweetVOption x = - match x with - | x when String.IsNullOrEmpty x -> ValueNone - | _ -> ValueSome x - - testCase "traverseVOption with a sequence of valid data" - <| fun _ -> - let tweets = - seq { - "Hi" - "Hello" - "Hola" - } - - let expected = Seq.toList tweets - - let actual = - Seq.traverseVOptionM tryTweetVOption tweets - |> ValueOption.map Seq.toList + let actual = Seq.sequenceResultA (Seq.map Tweet.TryCreate tweets) - match actual with - | ValueSome actual -> - Expect.equal actual expected "Should have a sequence of valid tweets" - | ValueNone -> failwith "Expected a value some" + Expect.equal actual expected "traverse the seq and return all the errors" - testCase "traverseVOption with few invalid data" + testCase "iterates exacly once" <| fun _ -> - let tweets = - seq { - "Hi" - "Hello" - String.Empty - } + let mutable counter = 0 - let actual = Seq.traverseVOptionM tryTweetVOption tweets - Expect.equal actual ValueNone "traverse the sequence and return value none" - ] - -let sequenceVOptionMTests = - testList "Seq.sequenceVOptionM Tests" [ - let tryTweetOption x = - match x with - | x when String.IsNullOrEmpty x -> ValueNone - | _ -> ValueSome x - - testCase "traverseVOption with a sequence of valid data" - <| fun _ -> let tweets = seq { "Hi" "Hello" "Hola" - } - - let expected = Seq.toList tweets - - let actual = - Seq.sequenceVOptionM (Seq.map tryTweetOption tweets) - |> ValueOption.map Seq.toList - - match actual with - | ValueSome actual -> - Expect.equal actual expected "Should have a sequence of valid tweets" - | ValueNone -> failwith "Expected a value some" - - testCase "sequenceVOptionM with few invalid data" - <| fun _ -> - let tweets = - seq { - String.Empty - "Hello" - String.Empty - } - - let actual = Seq.sequenceVOptionM (Seq.map tryTweetOption tweets) - Expect.equal actual ValueNone "traverse the sequence and return value none" - - testCase "sequenceVOptionM with few invalid data should exit early" - <| fun _ -> - - let mutable lastValue = null - let mutable callCount = 0 - - let tweets = - seq { - "" - "Hello" aLongerInvalidTweet - } - let tryCreate tweet = - callCount <- - callCount - + 1 + counter <- + counter + + 1 + } - match tweet with - | x when String.IsNullOrEmpty x -> ValueNone - | x -> ValueSome x + let expected = Error [| longerTweetErrMsg |] - let actual = Seq.sequenceVOptionM (Seq.map tryCreate tweets) + let actual = Seq.sequenceResultA (Seq.map Tweet.TryCreate tweets) - match actual with - | ValueNone -> () - | ValueSome _ -> failwith "Expected a value none" + Expect.equal actual expected "traverse the seq and return all the errors" - Expect.equal callCount 1 "Should have called the function only 1 time" - Expect.equal lastValue null "" + Expect.equal counter 1 "evaluation of the sequence completes exactly once" ] -#endif - let allTests = - testList "List Tests" [ - traverseResultMTests - traverseOptionMTests + testList "Seq Tests" [ sequenceResultMTests - sequenceOptionMTests - traverseResultATests sequenceResultATests - traverseAsyncResultMTests - traverseAsyncOptionMTests - traverseAsyncResultATests - sequenceAsyncResultMTests - sequenceAsyncOptionMTests - sequenceAsyncResultATests -#if !FABLE_COMPILER - traverseVOptionMTests - sequenceVOptionMTests -#endif ]