From b0ba796c63dfce6b695fb8a70794895f01f7417a Mon Sep 17 00:00:00 2001 From: Giovanni Costagliola Date: Mon, 12 Feb 2024 23:28:49 +0100 Subject: [PATCH] feat(Result): extension methods to enumerables of Result --- CHANGELOG.md | 17 +++ Monads.sln | 1 + README.md | 9 ++ .../ConsoleApp/Pipelines/GamblingPipeline.cs | 2 +- .../Extensions/EnumerableResultExtensions.cs | 123 ------------------ src/Monads/Maybe/IMaybe.cs | 1 + .../MaybeEnumerableExtensions.cs} | 2 +- src/Monads/Result/IResult.cs | 6 +- src/Monads/Result/ResultCollection.cs | 34 +++++ .../ResultEnumerableExtensions.Filters.cs | 30 +++++ .../ResultEnumerableExtensions.Predicates.cs | 57 ++++++++ ...ResultEnumerableExtensions.SelectValues.cs | 61 +++++++++ ...s.cs => MaybeEnumerableExtensionsTests.cs} | 2 +- ....cs => ResultEnumerableExtensionsTests.cs} | 80 +++++++++++- 14 files changed, 289 insertions(+), 136 deletions(-) create mode 100644 CHANGELOG.md delete mode 100644 src/Monads/Extensions/EnumerableResultExtensions.cs rename src/Monads/{Extensions/EnumerableMaybeExtensions.Maybe.cs => Maybe/MaybeEnumerableExtensions.cs} (98%) create mode 100644 src/Monads/Result/ResultCollection.cs create mode 100644 src/Monads/Result/ResultEnumerableExtensions.Filters.cs create mode 100644 src/Monads/Result/ResultEnumerableExtensions.Predicates.cs create mode 100644 src/Monads/Result/ResultEnumerableExtensions.SelectValues.cs rename test/Monads.UnitTests/MaybeTests/{EnumerableMaybeExtensionsTests.cs => MaybeEnumerableExtensionsTests.cs} (98%) rename test/Monads.UnitTests/ResultTests/{EnumerableResultExtensionsTests.cs => ResultEnumerableExtensionsTests.cs} (58%) diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..f859632 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,17 @@ +# Bogoware Monads Changelog + +## 9.0.0 + +### New Features + +- The following extension methods on `IEnumerable>` have been introduced: + - `MapEach`: maps each `Result` in the sequence, preserving the failed `Result`s + - `BindEach`: binds each `Result` in the sequence, preserving the failed `Result`s + - `MatchEach`: matches each `Result` in the sequence + - `AggregateResult`: transforms a sequence of `Result`s into a single `Result` that contains a sequence of the successful values. If the original sequence contains any `Error` then will return a failed `Result` with an `AggregateError` containing all the errors found. + +### Breaking Changes +- The following extension methods on `IEnumerable>` have been removed: + - `Map`: use `MapEach` instead. The latter will preserve the failed `Result`s + - `Bind`: use `BindEach` instead. The latter will preserve the failed `Result`s + - `Macth` renamed to `MatchEach`. \ No newline at end of file diff --git a/Monads.sln b/Monads.sln index 2b08e30..4b224ba 100644 --- a/Monads.sln +++ b/Monads.sln @@ -11,6 +11,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution LICENSE = LICENSE Monads.sln.DotSettings = Monads.sln.DotSettings Monads.sln.DotSettings.user = Monads.sln.DotSettings.user + CHANGELOG.md = CHANGELOG.md EndProjectSection EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Monads", "src\Monads\Monads.csproj", "{DF5B4155-880E-4E7E-AC80-905C15D62E18}" diff --git a/README.md b/README.md index 5cc6a2f..15f90e6 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,15 @@ public Result Publish() => Result .ExecuteIfSuccess(() => PublishingStatus = PublishingStatus.Published); ``` +## Manipulating `IEnumerable>` + +The library provide a set of extension methods that enable manipulation of sequences of `Result` instances. + +* `MapEach`: Maps each `Result` in the sequence, preserving the failed `Result`s +* `BindEach`: Binds each `Result` in the sequence, preserving the failed `Result`s +* `MatchEach`: Matches each `Result` in the sequence +* `AggregateResult`: Transforms a sequence of `Result`s into a single `Result` that contains a sequence of the successful values. If the original sequence contains any `Error` then will return a failed `Result` with an `AggregateError` containing all the errors found. + ## Design Goals for `Error` The `Error` class is used for modeling errors and works in conjunction with the `Result` monad. diff --git a/sample/ConsoleApp/Pipelines/GamblingPipeline.cs b/sample/ConsoleApp/Pipelines/GamblingPipeline.cs index 5f4d83a..a428825 100644 --- a/sample/ConsoleApp/Pipelines/GamblingPipeline.cs +++ b/sample/ConsoleApp/Pipelines/GamblingPipeline.cs @@ -34,7 +34,7 @@ Task> FourBetsInARow() => Result.Success(new Amount(initialAmount var attempts = attemptsTasks.Select(task => task.Result); - var messages = attempts.Match( + var messages = attempts.MatchEach( win => $"You won {win.Value}", $"You lost {initialAmount}"); diff --git a/src/Monads/Extensions/EnumerableResultExtensions.cs b/src/Monads/Extensions/EnumerableResultExtensions.cs deleted file mode 100644 index cc3a16c..0000000 --- a/src/Monads/Extensions/EnumerableResultExtensions.cs +++ /dev/null @@ -1,123 +0,0 @@ -using System.Runtime.CompilerServices; - -// ReSharper disable MemberCanBePrivate.Global - -namespace Bogoware.Monads; - -public static class EnumerableResultExtensions -{ - /// - /// Determines if all s of a sequence are Successs. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool AllSuccess(this IEnumerable successes) - => successes.All(r => r.IsSuccess); - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool AllSuccess(this IEnumerable> successes) - => successes.All(v => v.IsSuccess); - - /// - /// Determines if all s of a sequence are Failures. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool AllFailure(this IEnumerable successes) - => successes.All(r => r.IsFailure); - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool AllFailure(this IEnumerable> successes) - => successes.All(v => v.IsFailure); - - /// - /// Determines if any of a sequence is Success. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool AnySuccess(this IEnumerable successes) - => successes.Any(r => r.IsSuccess); - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool AnySuccess(this IEnumerable> successes) - => successes.Any(v => v.IsSuccess); - - /// - /// Determines if any of a sequence is Failure. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool AnyFailure(this IEnumerable successes) - => successes.Any(r => r.IsFailure); - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool AnyFailure(this IEnumerable> successes) - => successes.Any(v => v.IsFailure); - - /// - /// Extract values from s. - /// Failures are discarded. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static IEnumerable SelectValues(this IEnumerable> successes) - => successes.SelectMany(v => v); - - /// - /// Bind values via the functor. - /// Failures are discarded but new Failures can be produced - /// by the functor. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static IEnumerable> Bind( - this IEnumerable> successes, Func> functor) - => successes.SelectValues().Select(functor); - - /// - /// Maps values via the functor. - /// Failures are discarded. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static IEnumerable> Map( - this IEnumerable> successes, Func functor) - => successes.Bind(v => new Result(functor(v))); - - /// - /// Matches results. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static IEnumerable Match( - this IEnumerable> results, - Func mapSuccesses, - Func mapFailures) - => results.Select(result => result.Match(mapSuccesses, mapFailures)); - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static IEnumerable Match( - this IEnumerable> results, - Func mapSuccesses, - TResult failure) - => results.Select(result => result.Match(mapSuccesses, failure)); - - /// - /// Filters Successes via the predicate. - /// Failures are discarded. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static IEnumerable> Where( - this IEnumerable> successes, Func predicate) - => successes.SelectValues() - .Where(predicate) - .Select(v => new Result(v)); - - /// - /// Filters Successes via negated predicate. - /// Failures are discarded. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static IEnumerable> WhereNot( - this IEnumerable> successes, Func predicate) - => successes.SelectValues() - .Where(v => !predicate(v)) - .Select(v => new Result(v)); -} \ No newline at end of file diff --git a/src/Monads/Maybe/IMaybe.cs b/src/Monads/Maybe/IMaybe.cs index f962029..0e063ae 100644 --- a/src/Monads/Maybe/IMaybe.cs +++ b/src/Monads/Maybe/IMaybe.cs @@ -1,3 +1,4 @@ +// ReSharper disable UnusedTypeParameter namespace Bogoware.Monads; /// diff --git a/src/Monads/Extensions/EnumerableMaybeExtensions.Maybe.cs b/src/Monads/Maybe/MaybeEnumerableExtensions.cs similarity index 98% rename from src/Monads/Extensions/EnumerableMaybeExtensions.Maybe.cs rename to src/Monads/Maybe/MaybeEnumerableExtensions.cs index f8c4e38..accd412 100644 --- a/src/Monads/Extensions/EnumerableMaybeExtensions.Maybe.cs +++ b/src/Monads/Maybe/MaybeEnumerableExtensions.cs @@ -4,7 +4,7 @@ namespace Bogoware.Monads; -public static class EnumerableMaybeExtensions +public static class MaybeEnumerableExtensions { /// /// Determines if all s of a sequence are Somes. diff --git a/src/Monads/Result/IResult.cs b/src/Monads/Result/IResult.cs index cad8f0d..dabcc70 100644 --- a/src/Monads/Result/IResult.cs +++ b/src/Monads/Result/IResult.cs @@ -1,5 +1,5 @@ // ReSharper disable UnusedMemberInSuper.Global -// ReSharper disable TypeParameterCanBeVariant +// ReSharper disable UnusedTypeParameter namespace Bogoware.Monads; @@ -10,7 +10,7 @@ public interface IResult public Error GetErrorOrThrow(); } -public interface IResult : IResult +public interface IResult : IResult { - public TValue GetValueOrThrow(); + //public TValue GetValueOrThrow(); } \ No newline at end of file diff --git a/src/Monads/Result/ResultCollection.cs b/src/Monads/Result/ResultCollection.cs new file mode 100644 index 0000000..a280364 --- /dev/null +++ b/src/Monads/Result/ResultCollection.cs @@ -0,0 +1,34 @@ +namespace Bogoware.Monads; + +/// +/// Represents a collection of s. +/// +internal class ResultCollection +{ + public bool IsSuccess { get; } + public bool IsFailure => !IsSuccess; + public Error GetErrorOrThrow() => _error ?? throw new ResultSuccessException(); + + private readonly List> _results; + private readonly AggregateError? _error; + + internal ResultCollection(IEnumerable> results) + { + _results = [..results]; + IsSuccess = _results.Count == 0 + || _results.All(r => r.IsSuccess); + + if (IsSuccess) return; + + var errors = _results.Where(r => r.IsFailure).Select(r => r.GetErrorOrThrow()); + _error = new (errors); + } + + internal Result> ToResult() + { + if (IsFailure) return Result.Failure>(_error); + if (_results.Count == 0) return Result.Success(Enumerable.Empty()); + var values = _results.Select(r => r.Value); + return Result.Success(values); + } +} \ No newline at end of file diff --git a/src/Monads/Result/ResultEnumerableExtensions.Filters.cs b/src/Monads/Result/ResultEnumerableExtensions.Filters.cs new file mode 100644 index 0000000..bfdee8f --- /dev/null +++ b/src/Monads/Result/ResultEnumerableExtensions.Filters.cs @@ -0,0 +1,30 @@ +using System.Runtime.CompilerServices; + +namespace Bogoware.Monads; + +public static partial class ResultEnumerableExtensions +{ + + /// + /// Filters Successes via the predicate. + /// Failures are discarded. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static IEnumerable> Where( + this IEnumerable> successes, Func predicate) + => successes.SelectValues() + .Where(predicate) + .Select(v => new Result(v)); + + /// + /// Filters Successes via negated predicate. + /// Failures are discarded. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static IEnumerable> WhereNot( + this IEnumerable> successes, Func predicate) + => successes.SelectValues() + .Where(v => !predicate(v)) + .Select(v => new Result(v)); + +} \ No newline at end of file diff --git a/src/Monads/Result/ResultEnumerableExtensions.Predicates.cs b/src/Monads/Result/ResultEnumerableExtensions.Predicates.cs new file mode 100644 index 0000000..3384cb4 --- /dev/null +++ b/src/Monads/Result/ResultEnumerableExtensions.Predicates.cs @@ -0,0 +1,57 @@ +using System.Runtime.CompilerServices; + +// ReSharper disable MemberCanBePrivate.Global + +namespace Bogoware.Monads; + +public static partial class ResultEnumerableExtensions +{ + /// + /// Determines if all s of a sequence are Successs. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool AllSuccess(this IEnumerable successes) + => successes.All(r => r.IsSuccess); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool AllSuccess(this IEnumerable> successes) + => successes.All(v => v.IsSuccess); + + /// + /// Determines if all s of a sequence are Failures. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool AllFailure(this IEnumerable successes) + => successes.All(r => r.IsFailure); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool AllFailure(this IEnumerable> successes) + => successes.All(v => v.IsFailure); + + /// + /// Determines if any of a sequence is Success. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool AnySuccess(this IEnumerable successes) + => successes.Any(r => r.IsSuccess); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool AnySuccess(this IEnumerable> successes) + => successes.Any(v => v.IsSuccess); + + /// + /// Determines if any of a sequence is Failure. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool AnyFailure(this IEnumerable successes) + => successes.Any(r => r.IsFailure); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool AnyFailure(this IEnumerable> successes) + => successes.Any(v => v.IsFailure); + +} \ No newline at end of file diff --git a/src/Monads/Result/ResultEnumerableExtensions.SelectValues.cs b/src/Monads/Result/ResultEnumerableExtensions.SelectValues.cs new file mode 100644 index 0000000..73c4707 --- /dev/null +++ b/src/Monads/Result/ResultEnumerableExtensions.SelectValues.cs @@ -0,0 +1,61 @@ +using System.Runtime.CompilerServices; + +namespace Bogoware.Monads; + +public static partial class ResultEnumerableExtensions +{ + + /// + /// Extract values from s. + /// Failures are discarded. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static IEnumerable SelectValues(this IEnumerable> successes) + => successes.SelectMany(v => v); + + /// + /// Maps values via the functor. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static IEnumerable> MapEach( + this IEnumerable> results, Func functor) + => results.Select(result => result.Map(functor)); + + /// + /// Bind values via the functor. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static IEnumerable> BindEach( + this IEnumerable> results, Func> functor) + => results.Select(result => result.Bind(functor)); + + /// + /// Matches results. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static IEnumerable MatchEach( + this IEnumerable> results, + Func mapSuccesses, + Func mapFailures) + => results.Select(result => result.Match(mapSuccesses, mapFailures)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static IEnumerable MatchEach( + this IEnumerable> results, + Func mapSuccesses, + TResult failure) + => results.Select(result => result.Match(mapSuccesses, failure)); + + /// + /// Aggregates an enumeration of Result into a Result of an enumeration. + /// If all s are Success then return a Success . + /// otherwise return a Failure with an . + /// + /// + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Result> AggregateResult(this IEnumerable> results) + => new ResultCollection(results).ToResult(); +} \ No newline at end of file diff --git a/test/Monads.UnitTests/MaybeTests/EnumerableMaybeExtensionsTests.cs b/test/Monads.UnitTests/MaybeTests/MaybeEnumerableExtensionsTests.cs similarity index 98% rename from test/Monads.UnitTests/MaybeTests/EnumerableMaybeExtensionsTests.cs rename to test/Monads.UnitTests/MaybeTests/MaybeEnumerableExtensionsTests.cs index 3fe8c9e..fb2070b 100644 --- a/test/Monads.UnitTests/MaybeTests/EnumerableMaybeExtensionsTests.cs +++ b/test/Monads.UnitTests/MaybeTests/MaybeEnumerableExtensionsTests.cs @@ -2,7 +2,7 @@ namespace Bogoware.Monads.UnitTests.MaybeTests; -public class EnumerableMaybeExtensionsTests +public class MaybeEnumerableExtensionsTests { private static readonly List _allIMaybeSome = new() { diff --git a/test/Monads.UnitTests/ResultTests/EnumerableResultExtensionsTests.cs b/test/Monads.UnitTests/ResultTests/ResultEnumerableExtensionsTests.cs similarity index 58% rename from test/Monads.UnitTests/ResultTests/EnumerableResultExtensionsTests.cs rename to test/Monads.UnitTests/ResultTests/ResultEnumerableExtensionsTests.cs index 7875270..482d43c 100644 --- a/test/Monads.UnitTests/ResultTests/EnumerableResultExtensionsTests.cs +++ b/test/Monads.UnitTests/ResultTests/ResultEnumerableExtensionsTests.cs @@ -2,7 +2,7 @@ namespace Bogoware.Monads.UnitTests.ResultTests; -public class EnumerableResultExtensionsTests +public class ResultEnumerableExtensionsTests { private static readonly List> _allResultSuccess = new() { @@ -114,17 +114,21 @@ public void SelectValues_discards_Nones() } [Fact] - public void Map_remap_values_to_new_Some() + public void MapEach_remap_values_to_new_Some() { - IEnumerable> actual = _resultMixed.Map(v => new AnotherValue(v.Val)); - actual.Should().HaveCount(2); + IEnumerable> actual = _resultMixed.MapEach(v => new AnotherValue(v.Val)); + actual.Should().HaveCount(3); + actual.Count(r => r.IsSuccess).Should().Be(2); + actual.Count(r => r.IsFailure).Should().Be(1); } [Fact] - public void Bind_remap_values_to_new_Some() + public void BindEach_remap_values_to_new_Some() { - IEnumerable> actual = _resultMixed.Bind(v => Result.Success(new AnotherValue(v.Val))); - actual.Should().HaveCount(2); + IEnumerable> actual = _resultMixed.BindEach(v => Result.Success(new AnotherValue(v.Val))); + actual.Should().HaveCount(3); + actual.Count(r => r.IsSuccess).Should().Be(2); + actual.Count(r => r.IsFailure).Should().Be(1); } [Fact] @@ -157,4 +161,66 @@ public void WhereNot_works() IEnumerable> even = results.WhereNot(v => v.Val % 2 == 0); even.Should().HaveCount(3); } + + [Fact] + public void AggregateResult_AllSuccess_Returns_a_Success() + { + IEnumerable> results = new List> + { + Result.Success(new Value(0)), + Result.Success(new Value(1)), + Result.Success(new Value(2)), + Result.Success(new Value(3)), + Result.Success(new Value(5)) + }; + + var actual = results.AggregateResult(); + + actual.Should().BeOfType>>(); + actual.IsSuccess.Should().BeTrue(); + actual.IsFailure.Should().BeFalse(); + actual.GetValueOrThrow().Should().BeEquivalentTo(results.Select(r => r.GetValueOrThrow())); + } + + [Fact] + public void AggregateResult_AllFailures_Returns_a_Failure() + { + IEnumerable> results = new List> + { + Result.Failure("Error 1"), + Result.Failure("Error 2"), + Result.Failure("Error 3"), + Result.Failure("Error 4"), + Result.Failure("Error 5") + }; + + var actual = results.AggregateResult(); + var expectedError = new AggregateError(results.Select(r => r.GetErrorOrThrow())); + + actual.Should().BeOfType>>(); + actual.IsFailure.Should().BeTrue(); + actual.IsSuccess.Should().BeFalse(); + actual.GetErrorOrThrow().Should().BeEquivalentTo(expectedError); + } + + [Fact] + public void AggregateResult_SomeFailures_Returns_a_Failure() + { + var results = new List> + { + Result.Success(new Value(0)), + Result.Failure("Error 1"), + Result.Success(new Value(2)), + Result.Failure("Error 3"), + Result.Success(new Value(5)) + }; + + var actual = results.AggregateResult(); + var expectedError = new AggregateError(results.Where(r => r.IsFailure).Select(r => r.GetErrorOrThrow())); + + actual.Should().BeOfType>>(); + actual.IsFailure.Should().BeTrue(); + actual.IsSuccess.Should().BeFalse(); + actual.GetErrorOrThrow().Should().BeEquivalentTo(expectedError); + } } \ No newline at end of file