From fa8009e6d7b7623e06411445e4b1007693f57a08 Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Sat, 6 Apr 2024 16:29:47 +0100 Subject: [PATCH 01/11] feat(Seq.traverse/sequence*)!: Yield arrays --- src/FsToolkit.ErrorHandling/Seq.fs | 117 ++++++++++++++++------------- 1 file changed, 65 insertions(+), 52 deletions(-) diff --git a/src/FsToolkit.ErrorHandling/Seq.fs b/src/FsToolkit.ErrorHandling/Seq.fs index 021da617..b285d458 100644 --- a/src/FsToolkit.ErrorHandling/Seq.fs +++ b/src/FsToolkit.ErrorHandling/Seq.fs @@ -10,40 +10,43 @@ module FsToolkit.ErrorHandling.Seq /// The initial state /// The function to apply to each element /// The input sequence -/// A result with the ok elements in a sequence or the first error occurring in the sequence +/// A result with the ok elements in an array or the first error occurring in the sequence let inline traverseResultM' - state + (state: Result<'okOutput seq, 'error>) ([] f: 'okInput -> Result<'okOutput, 'error>) (xs: 'okInput seq) - = + : Result<'okOutput[], 'error> = + if isNull xs then + nullArg (nameof xs) + match state with - | Error _ -> state - | Ok oks -> + | Error e -> Error e + | Ok initialSuccesses -> + use enumerator = xs.GetEnumerator() - let rec loop oks = - if enumerator.MoveNext() then - match f enumerator.Current with - | Ok ok -> - loop ( - seq { - yield ok - yield! oks - } - ) - | Error e -> Error e - else - Ok(Seq.rev oks) + let acc = ResizeArray(initialSuccesses) + let mutable err = Unchecked.defaultof<'error> + let mutable ok = true + use e = xs.GetEnumerator() + + while ok + && e.MoveNext() do + match f e.Current with + | Ok r -> acc.Add r + | Error e -> + ok <- false + err <- e - loop oks + if ok then Ok(acc.ToArray()) else Error err /// /// Applies a function to each element of a sequence and returns a single result /// /// The function to apply to each element /// The input sequence -/// A result with the ok elements in a sequence or the first error occurring in the sequence -/// This function is equivalent to but applying and initial state of 'Seq.empty' +/// A result with the ok elements in an array, or the first error occurring in the sequence +/// This function is equivalent to but applying an initial state of 'Seq.empty' let traverseResultM f xs = traverseResultM' (Ok Seq.empty) f xs /// @@ -60,45 +63,55 @@ let sequenceResultM xs = traverseResultM id xs /// The initial state /// The function to apply to each element /// The input sequence -/// A result with the ok elements in a sequence or a sequence of all errors occuring in the original sequence -let inline traverseResultA' state ([] f: 'okInput -> Result<'okOutput, 'error>) xs = - let folder state x = - match state, f x with - | Error errors, Error e -> - seq { - e - yield! Seq.rev errors - } - |> Seq.rev - |> Error - | Ok oks, Ok ok -> - seq { - ok - yield! Seq.rev oks - } - |> Seq.rev - |> Ok - | Ok _, Error e -> - Seq.singleton e - |> Error - | Error _, Ok _ -> state +/// If no Errors encountered, an Ok result bearing an array of the ok elements from the 'state' followed by those gathered from the sequence, or an Error bearing an array of all errors from the 'state' and/or those in the sequence +let inline traverseResultA' + (state: Result<'okOutput seq, 'error seq>) + ([] f: 'okInput -> Result<'okOutput, 'error>) + xs + = - Seq.fold folder state xs + if isNull xs then + nullArg (nameof xs) + + match state with + | Error failuresToDate -> + let errs = ResizeArray(failuresToDate) + + for x in xs do + match f x with + | Ok _ -> () // as the initial state was failure, oks are irrelevant + | Error e -> errs.Add e + + Error(errs.ToArray()) + | Ok initialSuccesses -> + + let oks = ResizeArray(initialSuccesses) + let errs = ResizeArray() + + for x in xs do + match f x with + | Error e -> errs.Add e + | Ok r when errs.Count = 0 -> oks.Add r + | Ok _ -> () // no point saving results we won't use given the end result will be Error + + match errs.ToArray() with + | [||] -> Ok(oks.ToArray()) + | errs -> Error errs /// /// Applies a function to each element of a sequence and returns a single result /// /// The function to apply to each element /// The input sequence -/// A result with the ok elements in a sequence or a sequence of all errors occuring in the original sequence -/// This function is equivalent to but applying and initial state of 'Seq.empty' +/// A result with the ok elements in an array or an array of all errors from across the 'state' and the sequence +/// This function is equivalent to but applying an initial state of Seq.empty' let traverseResultA f xs = traverseResultA' (Ok Seq.empty) f xs /// /// Converts a sequence of results into a single result /// /// The input sequence -/// A result with the ok elements in a sequence or a sequence of all errors occuring in the original sequence +/// A result with the ok elements in an array or an array of all errors from across the 'state' and the sequence /// This function is equivalent to but auto-applying the 'id' function let sequenceResultA xs = traverseResultA id xs @@ -146,7 +159,7 @@ let inline traverseAsyncResultM' /// The function to apply to each element /// The input sequence /// An async result with the ok elements in a sequence or the first error occurring in the sequence -/// This function is equivalent to but applying and initial state of 'Seq.empty' +/// This function is equivalent to but applying an initial state of 'Seq.empty' let traverseAsyncResultM f xs = traverseAsyncResultM' (async { return Ok Seq.empty }) f xs @@ -205,7 +218,7 @@ let inline traverseAsyncResultA' /// The function to apply to each element /// The input sequence /// An async result with the ok elements in a sequence or a sequence of all errors occuring in the original sequence -/// This function is equivalent to but applying and initial state of 'Seq.empty' +/// This function is equivalent to but applying an initial state of 'Seq.empty' let traverseAsyncResultA f xs = traverseAsyncResultA' (async { return Ok Seq.empty }) f xs @@ -256,7 +269,7 @@ let inline traverseOptionM' /// The function to apply to each element /// The input sequence /// An option containing Some sequence of elements or None if any of the function applications return None -/// This function is equivalent to but applying and initial state of 'Seq.empty' +/// This function is equivalent to but applying an initial state of 'Seq.empty' let traverseOptionM f xs = traverseOptionM' (Some Seq.empty) f xs /// @@ -311,7 +324,7 @@ let inline traverseAsyncOptionM' /// The function to apply to each element /// The input sequence /// An async option containing Some sequence of elements or None if any of the function applications return None -/// This function is equivalent to but applying and initial state of 'Async { return Some Seq.empty }' +/// This function is equivalent to but applying an initial state of 'Async { return Some Seq.empty }' let traverseAsyncOptionM f xs = traverseAsyncOptionM' (async { return Some Seq.empty }) f xs @@ -364,7 +377,7 @@ let inline traverseVOptionM' /// The function to apply to each element /// The input sequence /// A voption containing Some sequence of elements or None if any of the function applications return None -/// This function is equivalent to but applying and initial state of 'ValueSome Seq.empty' +/// This function is equivalent to but applying an initial state of 'ValueSome Seq.empty' let traverseVOptionM f xs = traverseVOptionM' (ValueSome Seq.empty) f xs From 44b317fee3366248a334ee3fa50fe57ab591bd11 Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Mon, 17 Feb 2025 17:33:53 +0000 Subject: [PATCH 02/11] Port the rest --- src/FsToolkit.ErrorHandling/Seq.fs | 261 ++++++++++++++--------------- 1 file changed, 123 insertions(+), 138 deletions(-) diff --git a/src/FsToolkit.ErrorHandling/Seq.fs b/src/FsToolkit.ErrorHandling/Seq.fs index b285d458..60352869 100644 --- a/src/FsToolkit.ErrorHandling/Seq.fs +++ b/src/FsToolkit.ErrorHandling/Seq.fs @@ -1,5 +1,6 @@ -// See here for previous design disccusions: +// See here for previous design discussions: // 1. https://github.com/demystifyfp/FsToolkit.ErrorHandling/pull/277 +// 2. https://github.com/demystifyfp/FsToolkit.ErrorHandling/pull/310 [] module FsToolkit.ErrorHandling.Seq @@ -10,7 +11,7 @@ module FsToolkit.ErrorHandling.Seq /// The initial state /// The function to apply to each element /// The input sequence -/// A result with the ok elements in an array or the first error occurring in the sequence +/// If state and all elements in 'sequence' are Ok, a result with the elements in an array. Alternately, the first error encountered let inline traverseResultM' (state: Result<'okOutput seq, 'error>) ([] f: 'okInput -> Result<'okOutput, 'error>) @@ -23,22 +24,20 @@ let inline traverseResultM' | Error e -> Error e | Ok initialSuccesses -> - use enumerator = xs.GetEnumerator() - - let acc = ResizeArray(initialSuccesses) - let mutable err = Unchecked.defaultof<'error> + let oks = ResizeArray(initialSuccesses) let mutable ok = true + let mutable err = Unchecked.defaultof<'error> use e = xs.GetEnumerator() while ok && e.MoveNext() do match f e.Current with - | Ok r -> acc.Add r + | Ok r -> oks.Add r | Error e -> ok <- false err <- e - if ok then Ok(acc.ToArray()) else Error err + if ok then Ok(oks.ToArray()) else Error err /// /// Applies a function to each element of a sequence and returns a single result @@ -53,7 +52,7 @@ let traverseResultM f xs = traverseResultM' (Ok Seq.empty) f xs /// Converts a sequence of results into a single result /// /// The input sequence -/// A result with the ok elements in a sequence or the first error occurring in the sequence +/// A result bearing all the results as an array, or the first error occurring in the sequence /// This function is equivalent to but auto-applying the 'id' function let sequenceResultM xs = traverseResultM id xs @@ -103,7 +102,7 @@ let inline traverseResultA' /// /// The function to apply to each element /// The input sequence -/// A result with the ok elements in an array or an array of all errors from across the 'state' and the sequence +/// A result with the ok elements in an array or an array of all errors from the sequence /// This function is equivalent to but applying an initial state of Seq.empty' let traverseResultA f xs = traverseResultA' (Ok Seq.empty) f xs @@ -111,7 +110,7 @@ let traverseResultA f xs = traverseResultA' (Ok Seq.empty) f xs /// Converts a sequence of results into a single result /// /// The input sequence -/// A result with the ok elements in an array or an array of all errors from across the 'state' and the sequence +/// A result with the elements in an array or an array of all errors from the sequence /// This function is equivalent to but auto-applying the 'id' function let sequenceResultA xs = traverseResultA id xs @@ -121,36 +120,33 @@ let sequenceResultA xs = traverseResultA id xs /// The initial state /// The function to apply to each element /// The input sequence -/// An async result with the ok elements in a sequence or the first error occurring in the sequence +/// An async result with the ok elements in an array or the first error encountered in the state or the sequence let inline traverseAsyncResultM' - state + (state: Async>) ([] f: 'okInput -> Async>) (xs: 'okInput seq) - = + : Async> = + if isNull xs then + nullArg (nameof xs) + async { match! state with - | Error _ -> return! state - | Ok oks -> - use enumerator = xs.GetEnumerator() - - let rec loop oks = - async { - if enumerator.MoveNext() then - match! f enumerator.Current with - | Ok ok -> - return! - loop ( - seq { - yield ok - yield! oks - } - ) - | Error e -> return Error e - else - return Ok(Seq.rev oks) - } - - return! loop oks + | Error e -> return Error e + | Ok initialSuccesses -> + let oks = ResizeArray(initialSuccesses) + let mutable ok = true + let mutable err = Unchecked.defaultof<'error> + use e = xs.GetEnumerator() + + while ok + && e.MoveNext() do + match! f e.Current with + | Ok r -> oks.Add r + | Error e -> + ok <- false + err <- e + + return if ok then Ok(oks.ToArray()) else Error err } /// @@ -158,7 +154,7 @@ let inline traverseAsyncResultM' /// /// The function to apply to each element /// The input sequence -/// An async result with the ok elements in a sequence or the first error occurring in the sequence +/// An async result with the ok elements in an array or the first error occurring in the sequence /// This function is equivalent to but applying an initial state of 'Seq.empty' let traverseAsyncResultM f xs = traverseAsyncResultM' (async { return Ok Seq.empty }) f xs @@ -167,7 +163,7 @@ let traverseAsyncResultM f xs = /// Converts a sequence of async results into a single async result /// /// The input sequence -/// An async result with the ok elements in a sequence or the first error occurring in the sequence +/// An async result with the ok elements in an array or the first error occurring in the sequence /// This function is equivalent to but auto-applying the 'id' function let sequenceAsyncResultM xs = traverseAsyncResultM id xs @@ -177,47 +173,48 @@ let sequenceAsyncResultM xs = traverseAsyncResultM id xs /// The initial state /// The function to apply to each element /// The input sequence -/// An async result with the ok elements in a sequence or a sequence of all errors occuring in the original sequence +/// An async result with the ok elements in an array or an array of all errors from the state and the original sequence let inline traverseAsyncResultA' - state + (state: Async>) ([] f: 'okInput -> Async>) - xs - = - let folder state x = - async { - let! state = state - let! result = f x - - return - match state, result with - | Error errors, Error e -> - seq { - e - yield! Seq.rev errors - } - |> Seq.rev - |> Error - | Ok oks, Ok ok -> - seq { - ok - yield! Seq.rev oks - } - |> Seq.rev - |> Ok - | Ok _, Error e -> - Seq.singleton e - |> Error - | Error _, Ok _ -> state - } - - Seq.fold folder state xs + (xs: 'okInput seq) + : Async> = + if isNull xs then + nullArg (nameof xs) + + async { + match! state with + | Error failuresToDate -> + let errs = ResizeArray(failuresToDate) + + for x in xs do + match! f x with + | Ok _ -> () // as the initial state was failure, oks are irrelevant + | Error e -> errs.Add e + + return Error(errs.ToArray()) + | Ok initialSuccesses -> + + let oks = ResizeArray(initialSuccesses) + let errs = ResizeArray() + + for x in xs do + match! f x with + | Error e -> errs.Add e + | Ok r when errs.Count = 0 -> oks.Add r + | Ok _ -> () // no point saving results we won't use given the end result will be Error + + match errs.ToArray() with + | [||] -> return Ok(oks.ToArray()) + | errs -> return Error errs + } /// /// Applies a function to each element of a sequence and returns a single async result /// /// The function to apply to each element /// The input sequence -/// An async result with the ok elements in a sequence or a sequence of all errors occuring in the original sequence +/// An async result with the ok elements in an array or an array of all errors occuring in the sequence /// This function is equivalent to but applying an initial state of 'Seq.empty' let traverseAsyncResultA f xs = traverseAsyncResultA' (async { return Ok Seq.empty }) f xs @@ -226,7 +223,7 @@ let traverseAsyncResultA f xs = /// Converts a sequence of async results into a single async result /// /// The input sequence -/// An async result with the ok elements in a sequence or a sequence of all errors occuring in the original sequence +/// An async result with all ok elements in an array or an error result with an array of all errors occuring in the sequence /// This function is equivalent to but auto-applying the 'id' function let sequenceAsyncResultA xs = traverseAsyncResultA id xs @@ -236,39 +233,36 @@ let sequenceAsyncResultA xs = traverseAsyncResultA id xs /// The initial state /// The function to apply to each element /// The input sequence -/// An option containing Some sequence of elements or None if any of the function applications return None +/// An option containing Some array of elements or None if any of the function applications return None let inline traverseOptionM' - state + (state: seq<'okOutput> option) ([] f: 'okInput -> 'okOutput option) (xs: 'okInput seq) - = + : 'okOutput[] option = + if isNull xs then + nullArg (nameof xs) + match state with - | None -> state + | None -> None | Some values -> + let values = ResizeArray(values) + let mutable ok = true use enumerator = xs.GetEnumerator() - let rec loop values = - if enumerator.MoveNext() then - match f enumerator.Current with - | Some value -> - loop ( - seq { - yield value - yield! values - } - ) - | None -> None - else - Some(Seq.rev values) - - loop values + while ok + && enumerator.MoveNext() do + match f enumerator.Current with + | Some value -> values.Add value + | None -> ok <- false + + if ok then Some(values.ToArray()) else None /// /// Applies a function to each element of a sequence and returns a single option /// /// The function to apply to each element /// The input sequence -/// An option containing Some sequence of elements or None if any of the function applications return None +/// An option containing Some array of elements or None if any of the function applications return None /// This function is equivalent to but applying an initial state of 'Seq.empty' let traverseOptionM f xs = traverseOptionM' (Some Seq.empty) f xs @@ -276,7 +270,7 @@ let traverseOptionM f xs = traverseOptionM' (Some Seq.empty) f xs /// Converts a sequence of options into a single option /// /// The input sequence -/// An option containing Some sequence of elements or None if any of the function applications return None +/// An option containing Some array of elements or None if any of the function applications return None /// This function is equivalent to but auto-applying the 'id' function let sequenceOptionM xs = traverseOptionM id xs @@ -286,36 +280,30 @@ let sequenceOptionM xs = traverseOptionM id xs /// The initial state /// The function to apply to each element /// The input sequence -/// An async option containing Some sequence of elements or None if any of the function applications return None +/// An async option containing Some array of elements or None if any of the function applications return None let inline traverseAsyncOptionM' - state + (state: Async option>) ([] f: 'okInput -> Async<'okOutput option>) (xs: 'okInput seq) - = + : Async<'okOutput[] option> = + if isNull xs then + nullArg (nameof xs) + async { match! state with - | None -> return! state + | None -> return None | Some values -> + let values = ResizeArray(values) + let mutable ok = true use enumerator = xs.GetEnumerator() - let rec loop values = - async { - if enumerator.MoveNext() then - match! f enumerator.Current with - | Some value -> - return! - loop ( - seq { - yield value - yield! values - } - ) - | None -> return None - else - return Some(Seq.rev values) - } - - return! loop values + while ok + && enumerator.MoveNext() do + match! f enumerator.Current with + | Some value -> values.Add value + | None -> ok <- false + + return if ok then Some(values.ToArray()) else None } /// @@ -323,7 +311,7 @@ let inline traverseAsyncOptionM' /// /// The function to apply to each element /// The input sequence -/// An async option containing Some sequence of elements or None if any of the function applications return None +/// An async option containing Some array of elements or None if any of the function applications return None /// This function is equivalent to but applying an initial state of 'Async { return Some Seq.empty }' let traverseAsyncOptionM f xs = traverseAsyncOptionM' (async { return Some Seq.empty }) f xs @@ -332,7 +320,7 @@ let traverseAsyncOptionM f xs = /// Converts a sequence of async options into a single async option /// /// The input sequence -/// An async option containing Some sequence of elements or None if any of the function applications return None +/// An async option containing Some array of elements or None if any of the function applications return None /// This function is equivalent to but auto-applying the 'id' function let sequenceAsyncOptionM xs = traverseAsyncOptionM id xs @@ -344,39 +332,36 @@ let sequenceAsyncOptionM xs = traverseAsyncOptionM id xs /// The initial state /// The function to apply to each element /// The input sequence -/// A voption containing Some sequence of elements or None if any of the function applications return None +/// A voption containing an Array of elements or None if any of the function applications return None let inline traverseVOptionM' - state + (state: ValueOption<'okOutput seq>) ([] f: 'okInput -> 'okOutput voption) (xs: 'okInput seq) - = + : ValueOption<'okOutput[]> = + if isNull xs then + nullArg (nameof xs) + match state with - | ValueNone -> state + | ValueNone -> ValueNone | ValueSome values -> + let values = ResizeArray(values) + let mutable ok = true use enumerator = xs.GetEnumerator() - let rec loop values = - if enumerator.MoveNext() then - match f enumerator.Current with - | ValueSome value -> - loop ( - seq { - yield value - yield! values - } - ) - | ValueNone -> ValueNone - else - ValueSome(Seq.rev values) - - loop values + while ok + && enumerator.MoveNext() do + match f enumerator.Current with + | ValueSome value -> values.Add value + | ValueNone -> ok <- false + + if ok then ValueSome(values.ToArray()) else ValueNone /// /// Applies a function to each element of a sequence and returns a single voption /// /// The function to apply to each element /// The input sequence -/// A voption containing Some sequence of elements or None if any of the function applications return None +/// A voption containing Some array of elements or None if any of the function applications return None /// This function is equivalent to but applying an initial state of 'ValueSome Seq.empty' let traverseVOptionM f xs = traverseVOptionM' (ValueSome Seq.empty) f xs @@ -385,7 +370,7 @@ let traverseVOptionM f xs = /// Converts a sequence of voptions into a single voption /// /// The input sequence -/// A voption containing Some sequence of elements or None if any of the function applications return None +/// A voption containing Some array of elements or None if any of the function applications return None /// This function is equivalent to but auto-applying the 'id' function let sequenceVOptionM xs = traverseVOptionM id xs From 4b6dbc3e2ca1e7bbd141494b0761f0445d9d2378 Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Sat, 1 Mar 2025 17:04:08 +0000 Subject: [PATCH 03/11] Impl traverseTaskResultM' traverseTaskResultM --- src/FsToolkit.ErrorHandling/Seq.fs | 45 +++++++++++++++++++++ tests/FsToolkit.ErrorHandling.Tests/Seq.fs | 46 +++++++++++++++++++++- 2 files changed, 90 insertions(+), 1 deletion(-) diff --git a/src/FsToolkit.ErrorHandling/Seq.fs b/src/FsToolkit.ErrorHandling/Seq.fs index 60352869..be275251 100644 --- a/src/FsToolkit.ErrorHandling/Seq.fs +++ b/src/FsToolkit.ErrorHandling/Seq.fs @@ -167,6 +167,51 @@ let traverseAsyncResultM f xs = /// This function is equivalent to but auto-applying the 'id' function let sequenceAsyncResultM xs = traverseAsyncResultM id xs +/// +/// Applies a function to each element of a sequence and returns a single Task result +/// +/// The initial state +/// The function to apply to each element +/// The input sequence +/// A task result with the ok elements in an array or the first error encountered in the state or the sequence +let inline traverseTaskResultM' + (state: TaskResult<'okOutput seq, 'error>) + ([] f: 'okInput -> TaskResult<'okOutput, 'error>) + (xs: 'okInput seq) + : TaskResult<'okOutput[], 'error> = + if isNull xs then + nullArg (nameof xs) + + task { + match! state with + | Error e -> return Error e + | Ok initialSuccesses -> + let oks = ResizeArray(initialSuccesses) + let mutable ok = true + let mutable err = Unchecked.defaultof<'error> + use e = xs.GetEnumerator() + + while ok + && e.MoveNext() do + match! f e.Current with + | Ok r -> oks.Add r + | Error e -> + ok <- false + err <- e + + return if ok then Ok(oks.ToArray()) else Error err + } + +/// +/// Applies a function to each element of a sequence and returns a single Task result +/// +/// The function to apply to each element +/// The input sequence +/// A task result with the ok elements in an array or the first error occurring in the sequence +/// This function is equivalent to but applying an initial state of 'Seq.empty' +let traverseTaskResultM f xs = + traverseTaskResultM' (TaskResult.ok Seq.empty) f xs + /// /// Applies a function to each element of a sequence and returns a single async result /// diff --git a/tests/FsToolkit.ErrorHandling.Tests/Seq.fs b/tests/FsToolkit.ErrorHandling.Tests/Seq.fs index 2059337d..001f20ef 100644 --- a/tests/FsToolkit.ErrorHandling.Tests/Seq.fs +++ b/tests/FsToolkit.ErrorHandling.Tests/Seq.fs @@ -470,7 +470,6 @@ let traverseAsyncResultATests = } ] - let sequenceAsyncResultMTests = let userIds = seq { @@ -512,6 +511,50 @@ let sequenceAsyncResultMTests = } ] +let traverseTaskResultATests = + let userIds = + seq { + userId1 + userId2 + userId3 + userId4 + } + |> Seq.map UserId + + testList "Seq.traverseTaskResultA Tests" [ + testCaseAsync "traverseTaskResultA with a sequence of valid data" + <| async { + let expected = + userIds + |> Seq.map (fun (UserId user) -> (newPostId, user)) + |> Seq.toList + + let! actual = Seq.traverseTaskResultA (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! actual = Seq.traverseTaskResultA (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 sequenceAsyncOptionMTests = let userIds = @@ -723,6 +766,7 @@ let allTests = traverseAsyncResultMTests traverseAsyncOptionMTests traverseAsyncResultATests + traverseTaskResultATests sequenceAsyncResultMTests sequenceAsyncOptionMTests sequenceAsyncResultATests From b5080d47fd57d506aa4d1a5df732af21a7bca8be Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Sat, 1 Mar 2025 17:06:15 +0000 Subject: [PATCH 04/11] sequenceTaskResultM --- src/FsToolkit.ErrorHandling/Seq.fs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/FsToolkit.ErrorHandling/Seq.fs b/src/FsToolkit.ErrorHandling/Seq.fs index be275251..4b71488a 100644 --- a/src/FsToolkit.ErrorHandling/Seq.fs +++ b/src/FsToolkit.ErrorHandling/Seq.fs @@ -212,6 +212,14 @@ let inline traverseTaskResultM' let traverseTaskResultM f xs = traverseTaskResultM' (TaskResult.ok Seq.empty) f xs +/// +/// Converts a sequence of Task results into a single Task result +/// +/// The input sequence +/// A task result with the ok elements in an array or the first error occurring in the sequence +/// This function is equivalent to but auto-applying the 'id' function +let sequenceTaskResultM xs = traverseTaskResultM id xs + /// /// Applies a function to each element of a sequence and returns a single async result /// From dc6525bfa864d43bedad8dcec6198ad5ab85e682 Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Sat, 1 Mar 2025 17:35:04 +0000 Subject: [PATCH 05/11] Fix tests --- tests/FsToolkit.ErrorHandling.Tests/Seq.fs | 201 +++++++++++---------- 1 file changed, 110 insertions(+), 91 deletions(-) diff --git a/tests/FsToolkit.ErrorHandling.Tests/Seq.fs b/tests/FsToolkit.ErrorHandling.Tests/Seq.fs index 001f20ef..91d6f855 100644 --- a/tests/FsToolkit.ErrorHandling.Tests/Seq.fs +++ b/tests/FsToolkit.ErrorHandling.Tests/Seq.fs @@ -29,13 +29,11 @@ let traverseResultMTests = let expected = Seq.map tweet tweets - |> Seq.toList + |> Seq.toArray let actual = Seq.traverseResultM Tweet.TryCreate tweets - let actual = - Expect.wantOk actual "Expected result to be Ok" - |> Seq.toList + let actual = Expect.wantOk actual "Expected result to be Ok" Expect.equal actual expected "Should have a sequence of valid tweets" @@ -72,13 +70,10 @@ let traverseOptionMTests = "Hola" } - let expected = Seq.toList tweets + let expected = Seq.toArray tweets let actual = Seq.traverseOptionM tryTweetOption tweets - let actual = - Expect.wantSome actual "Expected result to be Some" - |> Seq.toList - + let actual = Expect.wantSome actual "Expected result to be Some" Expect.equal actual expected "Should have a sequence of valid tweets" testCase "traverseOption with few invalid data" @@ -109,13 +104,11 @@ let sequenceResultMTests = let expected = Seq.map tweet tweets - |> Seq.toList + |> Seq.toArray let actual = Seq.sequenceResultM (Seq.map Tweet.TryCreate tweets) - let actual = - Expect.wantOk actual "Expected result to be Ok" - |> Seq.toList + let actual = Expect.wantOk actual "Expected result to be Ok" Expect.equal actual expected "Should have a sequence of valid tweets" @@ -185,12 +178,10 @@ let sequenceOptionMTests = "Hola" } - let expected = Seq.toList tweets + let expected = Seq.toArray tweets let actual = Seq.sequenceOptionM (Seq.map tryTweetOption tweets) - let actual = - Expect.wantSome actual "Expected result to be Some" - |> Seq.toList + let actual = Expect.wantSome actual "Expected result to be Some" Expect.equal actual expected "Should have a sequence of valid tweets" @@ -250,13 +241,11 @@ let traverseResultATests = let expected = Seq.map tweet tweets - |> Seq.toList + |> Seq.toArray let actual = Seq.traverseResultA Tweet.TryCreate tweets - let actual = - Expect.wantOk actual "Expected result to be Ok" - |> Seq.toList + let actual = Expect.wantOk actual "Expected result to be Ok" Expect.equal actual expected "Should have a sequence of valid tweets" @@ -271,14 +260,12 @@ let traverseResultATests = let actual = Seq.traverseResultA Tweet.TryCreate tweets - let actual = - Expect.wantError actual "Expected result to be Error" - |> Seq.toList + let actual = Expect.wantError actual "Expected result to be Error" - let expected = [ + let expected = [| emptyTweetErrMsg longerTweetErrMsg - ] + |] Expect.equal actual expected "traverse the sequence and return all the errors" ] @@ -296,13 +283,11 @@ let sequenceResultATests = let expected = Seq.map tweet tweets - |> Seq.toList + |> Seq.toArray let actual = Seq.sequenceResultA (Seq.map Tweet.TryCreate tweets) - let actual = - Expect.wantOk actual "Expected result to be Ok" - |> Seq.toList + let actual = Expect.wantOk actual "Expected result to be Ok" Expect.equal actual expected "Should have a sequence of valid tweets" @@ -317,14 +302,12 @@ let sequenceResultATests = let actual = Seq.sequenceResultA (Seq.map Tweet.TryCreate tweets) - let actual = - Expect.wantError actual "Expected result to be Error" - |> Seq.toList + let actual = Expect.wantError actual "Expected result to be Error" - let expected = [ + let expected = [| emptyTweetErrMsg longerTweetErrMsg - ] + |] Expect.equal actual expected "traverse the sequence and return all the errors" ] @@ -350,13 +333,11 @@ let traverseAsyncResultMTests = let expected = userIds |> Seq.map (fun (UserId user) -> (newPostId, user)) - |> Seq.toList + |> Seq.toArray let! actual = Seq.traverseAsyncResultM (notifyNewPostSuccess (PostId newPostId)) userIds - let actual = - Expect.wantOk actual "Expected result to be Ok" - |> Seq.toList + let actual = Expect.wantOk actual "Expected result to be Ok" Expect.equal actual expected "Should have a sequence of valid data" } @@ -372,6 +353,50 @@ let traverseAsyncResultMTests = } ] +let traverseTaskResultMTests = + + let notifyNewPostSuccess (PostId post) (UserId user) = TaskResult.ok (post, user) + + let notifyNewPostFailure (PostId _) (UserId uId) = TaskResult.error $"error: %O{uId}" + + let userIds = + seq { + userId1 + userId2 + userId3 + } + |> Seq.map UserId + + testList "Seq.traverseTaskResultM Tests" [ + testCaseAsync "traverseTaskResultM with a sequence of valid data" + <| async { + let expected = + userIds + |> Seq.map (fun (UserId user) -> (newPostId, user)) + |> Seq.toArray + + let! actual = + Seq.traverseTaskResultM (notifyNewPostSuccess (PostId newPostId)) userIds + |> Async.AwaitTask + + let actual = Expect.wantOk actual "Expected result to be Ok" + + Expect.equal actual expected "Should have a sequence of valid data" + } + + testCaseAsync "traverseResultA with few invalid data" + <| async { + let expected = $"error: %O{userId1}" + + let actual = + Seq.traverseTaskResultM (notifyNewPostFailure (PostId newPostId)) userIds + + do! + Expect.hasTaskErrorValue expected actual + |> Async.AwaitTask + } + ] + let traverseAsyncOptionMTests = let userIds = @@ -386,14 +411,12 @@ let traverseAsyncOptionMTests = <| async { let expected = userIds - |> Seq.toList + |> Seq.toArray |> Some let f x = async { return Some x } - let actual = - Seq.traverseAsyncOptionM f userIds - |> AsyncOption.map Seq.toList + let actual = Seq.traverseAsyncOptionM f userIds match expected with | Some e -> do! Expect.hasAsyncSomeValue e actual @@ -425,7 +448,6 @@ let notifyFailure (PostId _) (UserId uId) = return Ok() } - let traverseAsyncResultATests = let userIds = seq { @@ -442,29 +464,25 @@ let traverseAsyncResultATests = let expected = userIds |> Seq.map (fun (UserId user) -> (newPostId, user)) - |> Seq.toList + |> Seq.toArray let! actual = Seq.traverseAsyncResultA (notifyNewPostSuccess (PostId newPostId)) userIds - let actual = - Expect.wantOk actual "Expected result to be Ok" - |> Seq.toList + let actual = Expect.wantOk actual "Expected result to be Ok" Expect.equal actual expected "Should have a sequence of valid data" } testCaseAsync "traverseResultA with few invalid data" <| async { - let expected = [ + let expected = [| sprintf "error: %s" (userId1.ToString()) sprintf "error: %s" (userId3.ToString()) - ] + |] let! actual = Seq.traverseAsyncResultA (notifyFailure (PostId newPostId)) userIds - let actual = - Expect.wantError actual "Expected result to be Error" - |> Seq.toList + let actual = Expect.wantError actual "Expected result to be Error" Expect.equal actual expected "Should have a sequence of errors" } @@ -486,15 +504,13 @@ let sequenceAsyncResultMTests = let expected = userIds |> Seq.map (fun (UserId user) -> (newPostId, user)) - |> Seq.toList + |> Seq.toArray let! actual = Seq.map (notifyNewPostSuccess (PostId newPostId)) userIds |> Seq.sequenceAsyncResultM - let actual = - Expect.wantOk actual "Expected result to be Ok" - |> Seq.toList + let actual = Expect.wantOk actual "Expected result to be Ok" Expect.equal actual expected "Should have a sequence of valid data" } @@ -511,7 +527,18 @@ let sequenceAsyncResultMTests = } ] -let traverseTaskResultATests = +let sequenceTaskResultMTests = + let notifyNewPostSuccess (PostId post) (UserId user) = TaskResult.ok (post, user) + + let notifyFailure (PostId _) (UserId uId) = + if + uId = userId1 + || uId = userId3 + then + TaskResult.error $"error: %O{uId}" + else + TaskResult.ok () + let userIds = seq { userId1 @@ -521,37 +548,35 @@ let traverseTaskResultATests = } |> Seq.map UserId - testList "Seq.traverseTaskResultA Tests" [ - testCaseAsync "traverseTaskResultA with a sequence of valid data" + testList "Seq.sequenceTaskResultM Tests" [ + testCaseAsync "sequenceTaskResultM with a sequence of valid data" <| async { let expected = userIds |> Seq.map (fun (UserId user) -> (newPostId, user)) - |> Seq.toList + |> Seq.toArray - let! actual = Seq.traverseTaskResultA (notifyNewPostSuccess (PostId newPostId)) userIds + let! actual = + Seq.map (notifyNewPostSuccess (PostId newPostId)) userIds + |> Seq.sequenceTaskResultM + |> Async.AwaitTask - let actual = - Expect.wantOk actual "Expected result to be Ok" - |> Seq.toList + let actual = Expect.wantOk actual "Expected result to be Ok" Expect.equal actual expected "Should have a sequence of valid data" } - testCaseAsync "traverseResultA with few invalid data" + testCaseAsync "sequenceTaskResultM with few invalid data" <| async { - let expected = [ - sprintf "error: %s" (userId1.ToString()) - sprintf "error: %s" (userId3.ToString()) - ] - - let! actual = Seq.traverseTaskResultA (notifyFailure (PostId newPostId)) userIds + let expected = sprintf "error: %s" (userId1.ToString()) let actual = - Expect.wantError actual "Expected result to be Error" - |> Seq.toList + userIds + |> Seq.map (notifyFailure (PostId newPostId)) + |> Seq.sequenceTaskResultM + |> Async.AwaitTask - Expect.equal actual expected "Should have a sequence of errors" + do! Expect.hasAsyncErrorValue expected actual } ] @@ -568,7 +593,7 @@ let sequenceAsyncOptionMTests = testCaseAsync "sequenceAsyncOptionM with a sequence of valid data" <| async { let expected = - Seq.toList userIds + Seq.toArray userIds |> Some let f x = async { return Some x } @@ -576,7 +601,6 @@ let sequenceAsyncOptionMTests = let actual = Seq.map f userIds |> Seq.sequenceAsyncOptionM - |> AsyncOption.map Seq.toList match expected with | Some e -> do! Expect.hasAsyncSomeValue e actual @@ -614,27 +638,25 @@ let sequenceAsyncResultATests = let expected = userIds |> Seq.map (fun (UserId user) -> (newPostId, user)) - |> Seq.toList + |> Seq.toArray 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 = [ + 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" @@ -658,11 +680,9 @@ let traverseVOptionMTests = "Hola" } - let expected = Seq.toList tweets + let expected = Seq.toArray tweets - let actual = - Seq.traverseVOptionM tryTweetVOption tweets - |> ValueOption.map Seq.toList + let actual = Seq.traverseVOptionM tryTweetVOption tweets match actual with | ValueSome actual -> @@ -698,11 +718,9 @@ let sequenceVOptionMTests = "Hola" } - let expected = Seq.toList tweets + let expected = Seq.toArray tweets - let actual = - Seq.sequenceVOptionM (Seq.map tryTweetOption tweets) - |> ValueOption.map Seq.toList + let actual = Seq.sequenceVOptionM (Seq.map tryTweetOption tweets) match actual with | ValueSome actual -> @@ -764,10 +782,11 @@ let allTests = traverseResultATests sequenceResultATests traverseAsyncResultMTests + traverseTaskResultMTests traverseAsyncOptionMTests traverseAsyncResultATests - traverseTaskResultATests sequenceAsyncResultMTests + sequenceTaskResultMTests sequenceAsyncOptionMTests sequenceAsyncResultATests #if !FABLE_COMPILER From 7e14c298201adae5f4ea5340a96c640c7a14b1a7 Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Sat, 1 Mar 2025 17:42:18 +0000 Subject: [PATCH 06/11] #ifdef out task support for Fable --- src/FsToolkit.ErrorHandling/Seq.fs | 3 +++ tests/FsToolkit.ErrorHandling.Tests/Seq.fs | 12 ++++++++++++ 2 files changed, 15 insertions(+) diff --git a/src/FsToolkit.ErrorHandling/Seq.fs b/src/FsToolkit.ErrorHandling/Seq.fs index 4b71488a..67b9428b 100644 --- a/src/FsToolkit.ErrorHandling/Seq.fs +++ b/src/FsToolkit.ErrorHandling/Seq.fs @@ -167,6 +167,7 @@ let traverseAsyncResultM f xs = /// This function is equivalent to but auto-applying the 'id' function let sequenceAsyncResultM xs = traverseAsyncResultM id xs +#if !FABLE_COMPILER /// /// Applies a function to each element of a sequence and returns a single Task result /// @@ -220,6 +221,8 @@ let traverseTaskResultM f xs = /// This function is equivalent to but auto-applying the 'id' function let sequenceTaskResultM xs = traverseTaskResultM id xs +#endif + /// /// Applies a function to each element of a sequence and returns a single async result /// diff --git a/tests/FsToolkit.ErrorHandling.Tests/Seq.fs b/tests/FsToolkit.ErrorHandling.Tests/Seq.fs index 91d6f855..ef8fff18 100644 --- a/tests/FsToolkit.ErrorHandling.Tests/Seq.fs +++ b/tests/FsToolkit.ErrorHandling.Tests/Seq.fs @@ -353,6 +353,8 @@ let traverseAsyncResultMTests = } ] +#if !FABLE_COMPILER + let traverseTaskResultMTests = let notifyNewPostSuccess (PostId post) (UserId user) = TaskResult.ok (post, user) @@ -397,6 +399,8 @@ let traverseTaskResultMTests = } ] +#endif + let traverseAsyncOptionMTests = let userIds = @@ -527,6 +531,8 @@ let sequenceAsyncResultMTests = } ] +#if !FABLE_COMPILER + let sequenceTaskResultMTests = let notifyNewPostSuccess (PostId post) (UserId user) = TaskResult.ok (post, user) @@ -580,6 +586,8 @@ let sequenceTaskResultMTests = } ] +#endif + let sequenceAsyncOptionMTests = let userIds = @@ -782,11 +790,15 @@ let allTests = traverseResultATests sequenceResultATests traverseAsyncResultMTests +#if !FABLE_COMPILER traverseTaskResultMTests +#endif traverseAsyncOptionMTests traverseAsyncResultATests sequenceAsyncResultMTests +#if !FABLE_COMPILER sequenceTaskResultMTests +#endif sequenceAsyncOptionMTests sequenceAsyncResultATests #if !FABLE_COMPILER From db7279f6a57bd3f065848e780a4b51066f57013d Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Sun, 2 Mar 2025 22:27:28 +0000 Subject: [PATCH 07/11] ArrayCollector/allocations --- src/FsToolkit.ErrorHandling/Seq.fs | 94 +++++++++++++++++------------- 1 file changed, 54 insertions(+), 40 deletions(-) diff --git a/src/FsToolkit.ErrorHandling/Seq.fs b/src/FsToolkit.ErrorHandling/Seq.fs index 67b9428b..a60580e1 100644 --- a/src/FsToolkit.ErrorHandling/Seq.fs +++ b/src/FsToolkit.ErrorHandling/Seq.fs @@ -5,6 +5,8 @@ [] module FsToolkit.ErrorHandling.Seq +open Microsoft.FSharp.Core.CompilerServices + /// /// Applies a function to each element of a sequence and returns a single result /// @@ -24,9 +26,10 @@ let inline traverseResultM' | Error e -> Error e | Ok initialSuccesses -> - let oks = ResizeArray(initialSuccesses) - let mutable ok = true + let oks = ArrayCollector() + oks.AddMany initialSuccesses let mutable err = Unchecked.defaultof<'error> + let mutable ok = true use e = xs.GetEnumerator() while ok @@ -34,10 +37,10 @@ let inline traverseResultM' match f e.Current with | Ok r -> oks.Add r | Error e -> - ok <- false err <- e + ok <- false - if ok then Ok(oks.ToArray()) else Error err + if ok then Ok(oks.Close()) else Error err /// /// Applies a function to each element of a sequence and returns a single result @@ -74,28 +77,31 @@ let inline traverseResultA' match state with | Error failuresToDate -> - let errs = ResizeArray(failuresToDate) + let errs = ArrayCollector() + errs.AddMany failuresToDate for x in xs do match f x with - | Ok _ -> () // as the initial state was failure, oks are irrelevant | Error e -> errs.Add e + | Ok _ -> () // as the initial state was failure, oks are irrelevant - Error(errs.ToArray()) + Error(errs.Close()) | Ok initialSuccesses -> - let oks = ResizeArray(initialSuccesses) - let errs = ResizeArray() + let oks = ArrayCollector() + oks.AddMany initialSuccesses + let errs = ArrayCollector() + let mutable ok = true for x in xs do match f x with - | Error e -> errs.Add e - | Ok r when errs.Count = 0 -> oks.Add r + | Ok r when ok -> oks.Add r | Ok _ -> () // no point saving results we won't use given the end result will be Error + | Error e -> + errs.Add e + ok <- false - match errs.ToArray() with - | [||] -> Ok(oks.ToArray()) - | errs -> Error errs + if ok then Ok(oks.Close()) else Error(errs.Close()) /// /// Applies a function to each element of a sequence and returns a single result @@ -133,9 +139,10 @@ let inline traverseAsyncResultM' match! state with | Error e -> return Error e | Ok initialSuccesses -> - let oks = ResizeArray(initialSuccesses) - let mutable ok = true + let oks = ArrayCollector() + oks.AddMany initialSuccesses let mutable err = Unchecked.defaultof<'error> + let mutable ok = true use e = xs.GetEnumerator() while ok @@ -143,10 +150,10 @@ let inline traverseAsyncResultM' match! f e.Current with | Ok r -> oks.Add r | Error e -> - ok <- false err <- e + ok <- false - return if ok then Ok(oks.ToArray()) else Error err + return if ok then Ok(oks.Close()) else Error err } /// @@ -187,9 +194,10 @@ let inline traverseTaskResultM' match! state with | Error e -> return Error e | Ok initialSuccesses -> - let oks = ResizeArray(initialSuccesses) - let mutable ok = true + let oks = ArrayCollector() + oks.AddMany initialSuccesses let mutable err = Unchecked.defaultof<'error> + let mutable ok = true use e = xs.GetEnumerator() while ok @@ -197,10 +205,10 @@ let inline traverseTaskResultM' match! f e.Current with | Ok r -> oks.Add r | Error e -> - ok <- false err <- e + ok <- false - return if ok then Ok(oks.ToArray()) else Error err + return if ok then Ok(oks.Close()) else Error err } /// @@ -241,28 +249,31 @@ let inline traverseAsyncResultA' async { match! state with | Error failuresToDate -> - let errs = ResizeArray(failuresToDate) + let errs = ArrayCollector() + errs.AddMany failuresToDate for x in xs do match! f x with | Ok _ -> () // as the initial state was failure, oks are irrelevant | Error e -> errs.Add e - return Error(errs.ToArray()) + return Error(errs.Close()) | Ok initialSuccesses -> - let oks = ResizeArray(initialSuccesses) - let errs = ResizeArray() + let oks = ArrayCollector() + oks.AddMany initialSuccesses + let mutable ok = true + let errs = ArrayCollector() for x in xs do match! f x with - | Error e -> errs.Add e - | Ok r when errs.Count = 0 -> oks.Add r + | Ok r when ok -> oks.Add r | Ok _ -> () // no point saving results we won't use given the end result will be Error + | Error e -> + errs.Add e + ok <- false - match errs.ToArray() with - | [||] -> return Ok(oks.ToArray()) - | errs -> return Error errs + return if ok then Ok(oks.Close()) else Error(errs.Close()) } /// @@ -300,8 +311,9 @@ let inline traverseOptionM' match state with | None -> None - | Some values -> - let values = ResizeArray(values) + | Some initialValues -> + let values = ArrayCollector() + values.AddMany initialValues let mutable ok = true use enumerator = xs.GetEnumerator() @@ -311,7 +323,7 @@ let inline traverseOptionM' | Some value -> values.Add value | None -> ok <- false - if ok then Some(values.ToArray()) else None + if ok then Some(values.Close()) else None /// /// Applies a function to each element of a sequence and returns a single option @@ -348,8 +360,9 @@ let inline traverseAsyncOptionM' async { match! state with | None -> return None - | Some values -> - let values = ResizeArray(values) + | Some initialValues -> + let values = ArrayCollector() + values.AddMany initialValues let mutable ok = true use enumerator = xs.GetEnumerator() @@ -359,7 +372,7 @@ let inline traverseAsyncOptionM' | Some value -> values.Add value | None -> ok <- false - return if ok then Some(values.ToArray()) else None + return if ok then Some(values.Close()) else None } /// @@ -399,8 +412,9 @@ let inline traverseVOptionM' match state with | ValueNone -> ValueNone - | ValueSome values -> - let values = ResizeArray(values) + | ValueSome initialValues -> + let values = ArrayCollector() + values.AddMany initialValues let mutable ok = true use enumerator = xs.GetEnumerator() @@ -410,7 +424,7 @@ let inline traverseVOptionM' | ValueSome value -> values.Add value | ValueNone -> ok <- false - if ok then ValueSome(values.ToArray()) else ValueNone + if ok then ValueSome(values.Close()) else ValueNone /// /// Applies a function to each element of a sequence and returns a single voption From 85c94d7109f60c59257ca918176fdd635179dd17 Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Sun, 2 Mar 2025 22:50:19 +0000 Subject: [PATCH 08/11] Fix shadow copy anomalies --- src/FsToolkit.ErrorHandling/Seq.fs | 24 +++++++++++----------- tests/FsToolkit.ErrorHandling.Tests/Seq.fs | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/FsToolkit.ErrorHandling/Seq.fs b/src/FsToolkit.ErrorHandling/Seq.fs index a60580e1..d3388e88 100644 --- a/src/FsToolkit.ErrorHandling/Seq.fs +++ b/src/FsToolkit.ErrorHandling/Seq.fs @@ -26,7 +26,7 @@ let inline traverseResultM' | Error e -> Error e | Ok initialSuccesses -> - let oks = ArrayCollector() + let mutable oks = ArrayCollector() oks.AddMany initialSuccesses let mutable err = Unchecked.defaultof<'error> let mutable ok = true @@ -77,7 +77,7 @@ let inline traverseResultA' match state with | Error failuresToDate -> - let errs = ArrayCollector() + let mutable errs = ArrayCollector() errs.AddMany failuresToDate for x in xs do @@ -88,9 +88,9 @@ let inline traverseResultA' Error(errs.Close()) | Ok initialSuccesses -> - let oks = ArrayCollector() + let mutable oks = ArrayCollector() oks.AddMany initialSuccesses - let errs = ArrayCollector() + let mutable errs = ArrayCollector() let mutable ok = true for x in xs do @@ -139,7 +139,7 @@ let inline traverseAsyncResultM' match! state with | Error e -> return Error e | Ok initialSuccesses -> - let oks = ArrayCollector() + let mutable oks = ArrayCollector() oks.AddMany initialSuccesses let mutable err = Unchecked.defaultof<'error> let mutable ok = true @@ -194,7 +194,7 @@ let inline traverseTaskResultM' match! state with | Error e -> return Error e | Ok initialSuccesses -> - let oks = ArrayCollector() + let mutable oks = ArrayCollector() oks.AddMany initialSuccesses let mutable err = Unchecked.defaultof<'error> let mutable ok = true @@ -249,7 +249,7 @@ let inline traverseAsyncResultA' async { match! state with | Error failuresToDate -> - let errs = ArrayCollector() + let mutable errs = ArrayCollector() errs.AddMany failuresToDate for x in xs do @@ -260,10 +260,10 @@ let inline traverseAsyncResultA' return Error(errs.Close()) | Ok initialSuccesses -> - let oks = ArrayCollector() + let mutable oks = ArrayCollector() oks.AddMany initialSuccesses let mutable ok = true - let errs = ArrayCollector() + let mutable errs = ArrayCollector() for x in xs do match! f x with @@ -312,7 +312,7 @@ let inline traverseOptionM' match state with | None -> None | Some initialValues -> - let values = ArrayCollector() + let mutable values = ArrayCollector() values.AddMany initialValues let mutable ok = true use enumerator = xs.GetEnumerator() @@ -361,7 +361,7 @@ let inline traverseAsyncOptionM' match! state with | None -> return None | Some initialValues -> - let values = ArrayCollector() + let mutable values = ArrayCollector() values.AddMany initialValues let mutable ok = true use enumerator = xs.GetEnumerator() @@ -413,7 +413,7 @@ let inline traverseVOptionM' match state with | ValueNone -> ValueNone | ValueSome initialValues -> - let values = ArrayCollector() + let mutable values = ArrayCollector() values.AddMany initialValues let mutable ok = true use enumerator = xs.GetEnumerator() diff --git a/tests/FsToolkit.ErrorHandling.Tests/Seq.fs b/tests/FsToolkit.ErrorHandling.Tests/Seq.fs index ef8fff18..9307892b 100644 --- a/tests/FsToolkit.ErrorHandling.Tests/Seq.fs +++ b/tests/FsToolkit.ErrorHandling.Tests/Seq.fs @@ -782,7 +782,7 @@ let sequenceVOptionMTests = #endif let allTests = - testList "List Tests" [ + testList "Seq Tests" [ traverseResultMTests traverseOptionMTests sequenceResultMTests From fae5515363f30f932abe1d9259b1d1880f5eea1d Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Sun, 2 Mar 2025 23:26:52 +0000 Subject: [PATCH 09/11] traverseTaskResultA et al --- src/FsToolkit.ErrorHandling/Seq.fs | 67 ++++++++++++ tests/FsToolkit.ErrorHandling.Tests/Seq.fs | 121 ++++++++++++++++++++- 2 files changed, 183 insertions(+), 5 deletions(-) diff --git a/src/FsToolkit.ErrorHandling/Seq.fs b/src/FsToolkit.ErrorHandling/Seq.fs index d3388e88..30c4595f 100644 --- a/src/FsToolkit.ErrorHandling/Seq.fs +++ b/src/FsToolkit.ErrorHandling/Seq.fs @@ -294,6 +294,73 @@ let traverseAsyncResultA f xs = /// This function is equivalent to but auto-applying the 'id' function let sequenceAsyncResultA xs = traverseAsyncResultA id xs +#if !FABLE_COMPILER + +/// +/// Applies a function to each element of a sequence and returns a single task result +/// +/// The initial state +/// The function to apply to each element +/// The input sequence +/// A task result with the ok elements in an array or an array of all errors from the state and the original sequence +let inline traverseTaskResultA' + (state: TaskResult<'okOutput seq, 'error seq>) + ([] f: 'okInput -> TaskResult<'okOutput, 'error>) + (xs: 'okInput seq) + : TaskResult<'okOutput[], 'error[]> = + if isNull xs then + nullArg (nameof xs) + + task { + match! state with + | Error failuresToDate -> + let mutable errs = ArrayCollector() + errs.AddMany failuresToDate + + for x in xs do + match! f x with + | Ok _ -> () // as the initial state was failure, oks are irrelevant + | Error e -> errs.Add e + + return Error(errs.Close()) + | Ok initialSuccesses -> + + let mutable oks = ArrayCollector() + oks.AddMany initialSuccesses + let mutable ok = true + let mutable errs = ArrayCollector() + + for x in xs do + match! f x with + | Ok r when ok -> oks.Add r + | Ok _ -> () // no point saving results we won't use given the end result will be Error + | Error e -> + errs.Add e + ok <- false + + return if ok then Ok(oks.Close()) else Error(errs.Close()) + } + +/// +/// Applies a function to each element of a sequence and returns a single task result +/// +/// The function to apply to each element +/// The input sequence +/// A task result with the ok elements in an array or an array of all errors occuring in the sequence +/// This function is equivalent to but applying an initial state of 'Seq.empty' +let traverseTaskResultA f xs = + traverseTaskResultA' (TaskResult.ok Seq.empty) f xs + +/// +/// Converts a sequence of task results into a single task result +/// +/// The input sequence +/// A task result with all ok elements in an array or an error result with an array of all errors occuring in the sequence +/// This function is equivalent to but auto-applying the 'id' function +let sequenceTaskResultA xs = traverseTaskResultA id xs + +#endif + /// /// Applies a function to each element of a sequence and returns a single option /// diff --git a/tests/FsToolkit.ErrorHandling.Tests/Seq.fs b/tests/FsToolkit.ErrorHandling.Tests/Seq.fs index 9307892b..1815c10f 100644 --- a/tests/FsToolkit.ErrorHandling.Tests/Seq.fs +++ b/tests/FsToolkit.ErrorHandling.Tests/Seq.fs @@ -452,7 +452,20 @@ let notifyFailure (PostId _) (UserId uId) = return Ok() } -let traverseAsyncResultATests = +let traverseTaskResultATests = + let notifyNewPostSuccess (PostId post) (UserId user) = TaskResult.ok (post, user) + + let notifyNewPostFailure (PostId _) (UserId uId) = TaskResult.error $"error: %O{uId}" + + let notifyFailure (PostId _) (UserId uId) = + if + uId = userId1 + || uId = userId3 + then + TaskResult.error $"error: %O{uId}" + else + TaskResult.ok () + let userIds = seq { userId1 @@ -462,15 +475,17 @@ let traverseAsyncResultATests = } |> Seq.map UserId - testList "Seq.traverseAsyncResultA Tests" [ - testCaseAsync "traverseAsyncResultA with a sequence of valid data" + testList "Seq.traverseTaskResultA Tests" [ + testCaseAsync "traverseTaskResultA with a sequence of valid data" <| async { let expected = userIds |> Seq.map (fun (UserId user) -> (newPostId, user)) |> Seq.toArray - let! actual = Seq.traverseAsyncResultA (notifyNewPostSuccess (PostId newPostId)) userIds + let! actual = + Seq.traverseTaskResultA (notifyNewPostSuccess (PostId newPostId)) userIds + |> Async.AwaitTask let actual = Expect.wantOk actual "Expected result to be Ok" @@ -484,7 +499,9 @@ let traverseAsyncResultATests = sprintf "error: %s" (userId3.ToString()) |] - let! actual = Seq.traverseAsyncResultA (notifyFailure (PostId newPostId)) userIds + let! actual = + Seq.traverseTaskResultA (notifyFailure (PostId newPostId)) userIds + |> Async.AwaitTask let actual = Expect.wantError actual "Expected result to be Error" @@ -533,6 +550,46 @@ let sequenceAsyncResultMTests = #if !FABLE_COMPILER +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.toArray + + let! actual = Seq.traverseAsyncResultA (notifyNewPostSuccess (PostId newPostId)) userIds + + let actual = Expect.wantOk actual "Expected result to be Ok" + + 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! actual = Seq.traverseAsyncResultA (notifyFailure (PostId newPostId)) userIds + + let actual = Expect.wantError actual "Expected result to be Error" + + Expect.equal actual expected "Should have a sequence of errors" + } + ] + let sequenceTaskResultMTests = let notifyNewPostSuccess (PostId post) (UserId user) = TaskResult.ok (post, user) @@ -672,6 +729,58 @@ let sequenceAsyncResultATests = ] #if !FABLE_COMPILER +let sequenceTaskResultATests = + let notifyNewPostSuccess (PostId post) (UserId user) = TaskResult.ok (post, user) + let notifyFailure (PostId _) (UserId uId) = + if + uId = userId1 + || uId = userId3 + then + TaskResult.error $"error: %O{uId}" + else + TaskResult.ok () + + let userIds = + seq { + userId1 + userId2 + userId3 + userId4 + } + |> Seq.map UserId + + testList "Seq.sequenceTaskResultA Tests" [ + testCaseAsync "sequenceTaskResultA with a sequence of valid data" + <| async { + let expected = + userIds + |> Seq.map (fun (UserId user) -> (newPostId, user)) + |> Seq.toArray + + let actual = + Seq.map (notifyNewPostSuccess (PostId newPostId)) userIds + |> Seq.sequenceTaskResultA + + do! Expect.hasTaskOkValue expected actual |> Async.AwaitTask + } + + testCaseAsync "sequenceTaskResultA 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.sequenceTaskResultA + |> Async.AwaitTask + + let actual = Expect.wantError actual "Expected result to be Error" + Expect.equal actual expected "Should have a sequence of errors" + } + ] + let traverseVOptionMTests = testList "Seq.traverseVOptionM Tests" [ let tryTweetVOption x = @@ -797,11 +906,13 @@ let allTests = traverseAsyncResultATests sequenceAsyncResultMTests #if !FABLE_COMPILER + traverseTaskResultATests sequenceTaskResultMTests #endif sequenceAsyncOptionMTests sequenceAsyncResultATests #if !FABLE_COMPILER + sequenceTaskResultATests traverseVOptionMTests sequenceVOptionMTests #endif From b017a0a237ab98fc9060b60c6f311e1fec46fe7c Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Sun, 2 Mar 2025 23:35:04 +0000 Subject: [PATCH 10/11] Revert ArrayCollector for Fable --- src/FsToolkit.ErrorHandling/Seq.fs | 69 ++++++++++------------ tests/FsToolkit.ErrorHandling.Tests/Seq.fs | 38 ++++++------ 2 files changed, 50 insertions(+), 57 deletions(-) diff --git a/src/FsToolkit.ErrorHandling/Seq.fs b/src/FsToolkit.ErrorHandling/Seq.fs index 30c4595f..959caf81 100644 --- a/src/FsToolkit.ErrorHandling/Seq.fs +++ b/src/FsToolkit.ErrorHandling/Seq.fs @@ -26,8 +26,7 @@ let inline traverseResultM' | Error e -> Error e | Ok initialSuccesses -> - let mutable oks = ArrayCollector() - oks.AddMany initialSuccesses + let oks = ResizeArray(initialSuccesses) let mutable err = Unchecked.defaultof<'error> let mutable ok = true use e = xs.GetEnumerator() @@ -40,7 +39,7 @@ let inline traverseResultM' err <- e ok <- false - if ok then Ok(oks.Close()) else Error err + if ok then Ok(oks.ToArray()) else Error err /// /// Applies a function to each element of a sequence and returns a single result @@ -77,31 +76,29 @@ let inline traverseResultA' match state with | Error failuresToDate -> - let mutable errs = ArrayCollector() - errs.AddMany failuresToDate + let errs = ResizeArray failuresToDate for x in xs do match f x with | Error e -> errs.Add e | Ok _ -> () // as the initial state was failure, oks are irrelevant - Error(errs.Close()) + Error(errs.ToArray()) | Ok initialSuccesses -> - let mutable oks = ArrayCollector() - oks.AddMany initialSuccesses - let mutable errs = ArrayCollector() - let mutable ok = true + let oks = ResizeArray initialSuccesses + let errs = ResizeArray() for x in xs do match f x with - | Ok r when ok -> oks.Add r + | Ok r when errs.Count = 0 -> oks.Add r | Ok _ -> () // no point saving results we won't use given the end result will be Error - | Error e -> - errs.Add e - ok <- false + | Error e -> errs.Add e - if ok then Ok(oks.Close()) else Error(errs.Close()) + if errs.Count = 0 then + Ok(oks.ToArray()) + else + Error(errs.ToArray()) /// /// Applies a function to each element of a sequence and returns a single result @@ -139,8 +136,7 @@ let inline traverseAsyncResultM' match! state with | Error e -> return Error e | Ok initialSuccesses -> - let mutable oks = ArrayCollector() - oks.AddMany initialSuccesses + let oks = ResizeArray initialSuccesses let mutable err = Unchecked.defaultof<'error> let mutable ok = true use e = xs.GetEnumerator() @@ -153,7 +149,7 @@ let inline traverseAsyncResultM' err <- e ok <- false - return if ok then Ok(oks.Close()) else Error err + return if ok then Ok(oks.ToArray()) else Error err } /// @@ -194,8 +190,7 @@ let inline traverseTaskResultM' match! state with | Error e -> return Error e | Ok initialSuccesses -> - let mutable oks = ArrayCollector() - oks.AddMany initialSuccesses + let oks = ResizeArray initialSuccesses let mutable err = Unchecked.defaultof<'error> let mutable ok = true use e = xs.GetEnumerator() @@ -208,7 +203,7 @@ let inline traverseTaskResultM' err <- e ok <- false - return if ok then Ok(oks.Close()) else Error err + return if ok then Ok(oks.ToArray()) else Error err } /// @@ -249,31 +244,29 @@ let inline traverseAsyncResultA' async { match! state with | Error failuresToDate -> - let mutable errs = ArrayCollector() - errs.AddMany failuresToDate + let errs = ResizeArray failuresToDate for x in xs do match! f x with | Ok _ -> () // as the initial state was failure, oks are irrelevant | Error e -> errs.Add e - return Error(errs.Close()) + return Error(errs.ToArray()) | Ok initialSuccesses -> - let mutable oks = ArrayCollector() - oks.AddMany initialSuccesses - let mutable ok = true - let mutable errs = ArrayCollector() + let oks = ResizeArray initialSuccesses + let errs = ResizeArray() for x in xs do match! f x with - | Ok r when ok -> oks.Add r + | Ok r when errs.Count = 0 -> oks.Add r | Ok _ -> () // no point saving results we won't use given the end result will be Error - | Error e -> - errs.Add e - ok <- false + | Error e -> errs.Add e - return if ok then Ok(oks.Close()) else Error(errs.Close()) + if errs.Count = 0 then + return Ok(oks.ToArray()) + else + return Error(errs.ToArray()) } /// @@ -379,8 +372,7 @@ let inline traverseOptionM' match state with | None -> None | Some initialValues -> - let mutable values = ArrayCollector() - values.AddMany initialValues + let values = ResizeArray initialValues let mutable ok = true use enumerator = xs.GetEnumerator() @@ -390,7 +382,7 @@ let inline traverseOptionM' | Some value -> values.Add value | None -> ok <- false - if ok then Some(values.Close()) else None + if ok then Some(values.ToArray()) else None /// /// Applies a function to each element of a sequence and returns a single option @@ -428,8 +420,7 @@ let inline traverseAsyncOptionM' match! state with | None -> return None | Some initialValues -> - let mutable values = ArrayCollector() - values.AddMany initialValues + let values = ResizeArray initialValues let mutable ok = true use enumerator = xs.GetEnumerator() @@ -439,7 +430,7 @@ let inline traverseAsyncOptionM' | Some value -> values.Add value | None -> ok <- false - return if ok then Some(values.Close()) else None + return if ok then Some(values.ToArray()) else None } /// diff --git a/tests/FsToolkit.ErrorHandling.Tests/Seq.fs b/tests/FsToolkit.ErrorHandling.Tests/Seq.fs index 1815c10f..1a2cc202 100644 --- a/tests/FsToolkit.ErrorHandling.Tests/Seq.fs +++ b/tests/FsToolkit.ErrorHandling.Tests/Seq.fs @@ -438,25 +438,11 @@ let traverseAsyncOptionMTests = | 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() - } +#if !FABLE_COMPILER let traverseTaskResultATests = let notifyNewPostSuccess (PostId post) (UserId user) = TaskResult.ok (post, user) - let notifyNewPostFailure (PostId _) (UserId uId) = TaskResult.error $"error: %O{uId}" - let notifyFailure (PostId _) (UserId uId) = if uId = userId1 @@ -508,6 +494,19 @@ let traverseTaskResultATests = Expect.equal actual expected "Should have a sequence of errors" } ] +#endif +let notifyFailure (PostId _) (UserId uId) = + async { + if + (uId = userId1 + || uId = userId3) + then + return + sprintf "error: %s" (uId.ToString()) + |> Error + else + return Ok() + } let sequenceAsyncResultMTests = let userIds = @@ -548,8 +547,6 @@ let sequenceAsyncResultMTests = } ] -#if !FABLE_COMPILER - let traverseAsyncResultATests = let userIds = seq { @@ -590,6 +587,8 @@ let traverseAsyncResultATests = } ] +#if !FABLE_COMPILER + let sequenceTaskResultMTests = let notifyNewPostSuccess (PostId post) (UserId user) = TaskResult.ok (post, user) @@ -731,6 +730,7 @@ let sequenceAsyncResultATests = #if !FABLE_COMPILER let sequenceTaskResultATests = let notifyNewPostSuccess (PostId post) (UserId user) = TaskResult.ok (post, user) + let notifyFailure (PostId _) (UserId uId) = if uId = userId1 @@ -761,7 +761,9 @@ let sequenceTaskResultATests = Seq.map (notifyNewPostSuccess (PostId newPostId)) userIds |> Seq.sequenceTaskResultA - do! Expect.hasTaskOkValue expected actual |> Async.AwaitTask + do! + Expect.hasTaskOkValue expected actual + |> Async.AwaitTask } testCaseAsync "sequenceTaskResultA with few invalid data" From 50052d291f1f8936bf4cd60586dc4a7955bc70a6 Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Mon, 3 Mar 2025 00:12:08 +0000 Subject: [PATCH 11/11] Typos --- README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 8297cbd9..f11f6989 100644 --- a/README.md +++ b/README.md @@ -34,12 +34,10 @@ GitHub Actions | | FsToolkit.ErrorHandling.AsyncSeq | [![NuGet](https://buildstats.info/nuget/FsToolkit.ErrorHandling.AsyncSeq)](https://www.nuget.org/packages/FsToolkit.ErrorHandling.AsyncSeq) | [![NuGet](https://buildstats.info/nuget/FsToolkit.ErrorHandling.AsyncSeq?includePreReleases=true)](https://www.nuget.org/packages/FsToolkit.ErrorHandling.AsyncSeq/absoluteLatest) | FsToolkit.ErrorHandling.IcedTasks | [![NuGet](https://buildstats.info/nuget/FsToolkit.ErrorHandling.IcedTasks)](https://www.nuget.org/packages/FsToolkit.ErrorHandling.IcedTasks) | [![NuGet](https://buildstats.info/nuget/FsToolkit.ErrorHandling.IcedTasks?includePreReleases=true)](https://www.nuget.org/packages/FsToolkit.ErrorHandling.IcedTasks/absoluteLatest) - - ## Developing locally ### Devcontainer -This repository has a devcontainer setup for VSCode. For more infomation see: +This repository has a devcontainer setup for VSCode. For more information see: - [VSCode](https://code.visualstudio.com/docs/devcontainers/containers) ### Local Setup @@ -79,7 +77,7 @@ Without specifying a build target, the default target is `DotnetPack`, which wil - `RunTests` - Will run tests for `dotnet`, `fable-javascript` and `fable-python` projects - `FormatCode` - Will run `fantomas` to format the codebase -This is not an exhausting list. Additional targets can be found in the `./build/build.fs` file. +This is not an exhaustive list. Additional targets can be found in the `./build/build.fs` file. A motivating example