From 557b168746003678fa52baa5f7b7c2601003b388 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabr=C3=ADcio=20Godoy?= Date: Tue, 31 Dec 2024 13:28:02 -0300 Subject: [PATCH] feat: Support TryAggregate with Option and Result --- docs/core/option.md | 19 +++++++ docs/core/result.md | 15 ++++++ .../UnitTests/Option/Option.TraverseTests.cs | 33 ++++++++++++ .../UnitTests/Result/Result.TraverseTests.cs | 36 +++++++++++++ src/FxKit/Option/Option.Traverse.cs | 51 +++++++++++++++++-- src/FxKit/Result/Result.Traverse.cs | 44 +++++++++++++++- 6 files changed, 194 insertions(+), 4 deletions(-) diff --git a/docs/core/option.md b/docs/core/option.md index be6f500..e6b90a4 100644 --- a/docs/core/option.md +++ b/docs/core/option.md @@ -241,6 +241,25 @@ Option> sequenced = list.Sequence(); ``` +### TryAggregate + +Use `TryAggregate` to attempt an aggregation of a sequence with a custom function that can short-circuit if any step fails. + +```csharp +var numbers = new[] { 1, 2, 3, 4 }; +var result = numbers.TryAggregate( + seed: 0, + func: (acc, item) => item > 0 ? Some(acc + item) : None +); +// Some(10) + +var invalidResult = numbers.TryAggregate( + seed: 0, + func: (acc, item) => item < 2 ? Some(acc + item) : None +); +// None +``` + ## Prelude The `Prelude` class provides the following functions for `Option`: diff --git a/docs/core/result.md b/docs/core/result.md index 0574f65..27b59de 100644 --- a/docs/core/result.md +++ b/docs/core/result.md @@ -233,6 +233,21 @@ Result, string> sequenced = list.Sequence(); ``` +### TryAggregate + +Use `TryAggregate` to attempt an aggregation of a sequence with a custom function that can short-circuit if any step +fails, returning either the final accumulated value or an error. + +```csharp +IReadOnlyList numbers = [1, 2, 3, 4]; +Result sumResult = numbers.TryAggregate( + seed: 0, + func: (acc, item) => item > 0 + ? Ok(acc + item) + : Err("Negative number encountered") +); +``` + ## Prelude The `Prelude` class provides the following functions for `Result`: diff --git a/src/FxKit.Tests/UnitTests/Option/Option.TraverseTests.cs b/src/FxKit.Tests/UnitTests/Option/Option.TraverseTests.cs index 87d8f9e..bc5fc57 100644 --- a/src/FxKit.Tests/UnitTests/Option/Option.TraverseTests.cs +++ b/src/FxKit.Tests/UnitTests/Option/Option.TraverseTests.cs @@ -76,4 +76,37 @@ public void ResultSequence_ShouldFlipTheFunctorsCorrectly() Some(Err("error")).Sequence().Should().BeErr("error"); Option>.None.Sequence().Should().BeOk(None); } + + [Test] + public void TryAggregate_ShouldAggregateValuesCorrectly() + { + ListOf.Many(1, 2, 3, 4) + .TryAggregate( + seed: 0, + func: (acc, item) => Some(acc + item)) + .Should() + .BeSome(10); + } + + [Test] + public void TryAggregate_ShouldReturnError_WhenConditionFails() + { + ListOf.Many(1, -1, 3) + .TryAggregate( + seed: 0, + func: (acc, item) => item >= 0 ? Some(acc + item) : None) + .Should() + .BeNone(); + } + + [Test] + public void TryAggregate_ShouldHandleEmptySequence() + { + Array.Empty() + .TryAggregate( + seed: 0, + func: (acc, item) => Some(acc + item)) + .Should() + .BeSome(0); + } } diff --git a/src/FxKit.Tests/UnitTests/Result/Result.TraverseTests.cs b/src/FxKit.Tests/UnitTests/Result/Result.TraverseTests.cs index 4e6aedd..9ba5bde 100644 --- a/src/FxKit.Tests/UnitTests/Result/Result.TraverseTests.cs +++ b/src/FxKit.Tests/UnitTests/Result/Result.TraverseTests.cs @@ -45,4 +45,40 @@ public void EnumerableSequence_ShouldFlipTheFunctorsCorrectly() Result Ok(int x) => x; Result Err(string x) => x; } + + [Test] + public void TryAggregate_ShouldAggregateValuesCorrectly() + { + ListOf.Many(1, 2, 3, 4) + .TryAggregate( + seed: 0, + func: (acc, item) => Ok(acc + item)) + .Should() + .BeOk(10); + } + + [Test] + public void TryAggregate_ShouldReturnError_WhenConditionFails() + { + ListOf.Many(1, -1, 3) + .TryAggregate( + seed: 0, + func: (acc, item) => + item >= 0 + ? Ok(acc + item) + : Err("Negative number encountered")) + .Should() + .BeErr("Negative number encountered"); + } + + [Test] + public void TryAggregate_ShouldHandleEmptySequence() + { + Array.Empty() + .TryAggregate( + seed: 0, + func: (acc, item) => Ok(acc + item)) + .Should() + .BeOk(0); + } } diff --git a/src/FxKit/Option/Option.Traverse.cs b/src/FxKit/Option/Option.Traverse.cs index 4945f3b..d13370f 100644 --- a/src/FxKit/Option/Option.Traverse.cs +++ b/src/FxKit/Option/Option.Traverse.cs @@ -1,4 +1,5 @@ using FxKit.CompilerServices; +using JetBrains.Annotations; namespace FxKit; @@ -19,7 +20,7 @@ public static partial class Option [GenerateTransformer] public static Validation, TInvalid> Traverse( this Option source, - Func> selector) + [InstantHandle] Func> selector) where T : notnull where TValid : notnull where TInvalid : notnull => @@ -73,7 +74,7 @@ public static Validation, TInvalid> Sequence( [GenerateTransformer] public static Option> Traverse( this IEnumerable source, - Func> selector) + [InstantHandle] Func> selector) where T : notnull where R : notnull { @@ -106,6 +107,50 @@ public static Option> Sequence( this IEnumerable> source) where T : notnull => source.Traverse(Identity); + /// + /// Attempts to aggregate a sequence of values using a specified function + /// that may return an optional result, starting with an initial seed value. + /// + /// The type of the elements in the source sequence. + /// The type of the accumulator value. + /// The sequence of elements to aggregate. + /// The initial value for the accumulator. + /// + /// A function that takes the current accumulator value and an element from + /// the sequence, returning an containing either + /// the new accumulator value or no value. + /// + /// + /// An containing the final accumulated value if the operation succeeds + /// for all elements, or if the aggregation fails at any step. + /// + /// + /// This method iteratively applies the specified function to the accumulator and each + /// element of the source sequence. If the function returns + /// at any step, the aggregation stops and the method returns . + /// + [GenerateTransformer] + public static Option TryAggregate( + this IEnumerable source, + TAccumulate seed, + [InstantHandle] Func> func) + where TAccumulate : notnull + { + var result = Some(seed); + + foreach (var item in source) + { + if (!result.TryGet(out var value)) + { + return Option.None; + } + + result = func(value, item); + } + + return result; + } + #endregion #region Result traversal @@ -135,7 +180,7 @@ public static Option> Sequence( [GenerateTransformer] public static Result, TErr> Traverse( this Option source, - Func> selector) + [InstantHandle] Func> selector) where T : notnull where TOk : notnull where TErr : notnull => diff --git a/src/FxKit/Result/Result.Traverse.cs b/src/FxKit/Result/Result.Traverse.cs index 0822a80..af6547a 100644 --- a/src/FxKit/Result/Result.Traverse.cs +++ b/src/FxKit/Result/Result.Traverse.cs @@ -1,4 +1,5 @@ using FxKit.CompilerServices; +using JetBrains.Annotations; namespace FxKit; @@ -26,7 +27,7 @@ public static partial class Result [GenerateTransformer] public static Result, TErr> Traverse( this IEnumerable source, - Func> selector) + [InstantHandle] Func> selector) where T : notnull where TOk : notnull where TErr : notnull @@ -64,5 +65,46 @@ public static Result, TErr> Sequence( where TErr : notnull => source.Traverse(Identity); + /// + /// Attempts to aggregate a sequence of values using a specified function that can fail, returning the result or an error. + /// + /// The type of the elements in the source sequence. + /// The type of the accumulated value. + /// The type of the error value. + /// The sequence of elements to aggregate. + /// The initial accumulator value. + /// A function to apply to each element and the current accumulator value, which returns a result or an error. + /// + /// A containing the final accumulated value if the aggregation succeeds, + /// or an error if the aggregation fails at any step. + /// + /// + /// This method iterates over the sequence and applies the delegate + /// to each element along with the current accumulator value. If any step in the aggregation process returns an error, + /// the method stops processing and returns the error. + /// + [GenerateTransformer] + public static Result TryAggregate( + this IEnumerable source, + TAccumulate seed, + [InstantHandle] Func> func) + where TAccumulate : notnull + where TError : notnull + { + var result = Ok(seed); + + foreach (var item in source) + { + if (!result.TryGet(out var value, out var error)) + { + return error; + } + + result = func(value, item); + } + + return result; + } + #endregion }