Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add TryAggregate for Option and Result #28

Merged
merged 1 commit into from
Jan 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions docs/core/option.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,25 @@ Option<IReadOnlyList<int>> 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`:
Expand Down
15 changes: 15 additions & 0 deletions docs/core/result.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,21 @@ Result<IReadOnlyList<int>, 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<int> numbers = [1, 2, 3, 4];
Result<int, string> sumResult = numbers.TryAggregate(
seed: 0,
func: (acc, item) => item > 0
? Ok<int, string>(acc + item)
: Err<int, string>("Negative number encountered")
);
```

## Prelude

The `Prelude` class provides the following functions for `Result`:
Expand Down
33 changes: 33 additions & 0 deletions src/FxKit.Tests/UnitTests/Option/Option.TraverseTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,37 @@ public void ResultSequence_ShouldFlipTheFunctorsCorrectly()
Some(Err<int, string>("error")).Sequence().Should().BeErr("error");
Option<Result<int, string>>.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<int>()
.TryAggregate(
seed: 0,
func: (acc, item) => Some(acc + item))
.Should()
.BeSome(0);
}
}
36 changes: 36 additions & 0 deletions src/FxKit.Tests/UnitTests/Result/Result.TraverseTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,40 @@ public void EnumerableSequence_ShouldFlipTheFunctorsCorrectly()
Result<int, string> Ok(int x) => x;
Result<int, string> Err(string x) => x;
}

[Test]
public void TryAggregate_ShouldAggregateValuesCorrectly()
{
ListOf.Many(1, 2, 3, 4)
.TryAggregate(
seed: 0,
func: (acc, item) => Ok<int, string>(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<int, string>(acc + item)
: Err<int, string>("Negative number encountered"))
.Should()
.BeErr("Negative number encountered");
}

[Test]
public void TryAggregate_ShouldHandleEmptySequence()
{
Array.Empty<int>()
.TryAggregate(
seed: 0,
func: (acc, item) => Ok<int, string>(acc + item))
.Should()
.BeOk(0);
}
}
51 changes: 48 additions & 3 deletions src/FxKit/Option/Option.Traverse.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using FxKit.CompilerServices;
using JetBrains.Annotations;

namespace FxKit;

Expand All @@ -19,7 +20,7 @@ public static partial class Option
[GenerateTransformer]
public static Validation<Option<TValid>, TInvalid> Traverse<T, TValid, TInvalid>(
this Option<T> source,
Func<T, Validation<TValid, TInvalid>> selector)
[InstantHandle] Func<T, Validation<TValid, TInvalid>> selector)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Always wondered if there was an attribute to tell the analyzer that the function is being invoked immediately - didn't know this was it, thanks!

where T : notnull
where TValid : notnull
where TInvalid : notnull =>
Expand Down Expand Up @@ -73,7 +74,7 @@ public static Validation<Option<TValid>, TInvalid> Sequence<TValid, TInvalid>(
[GenerateTransformer]
public static Option<IReadOnlyList<R>> Traverse<T, R>(
this IEnumerable<T> source,
Func<T, Option<R>> selector)
[InstantHandle] Func<T, Option<R>> selector)
where T : notnull
where R : notnull
{
Expand Down Expand Up @@ -106,6 +107,50 @@ public static Option<IReadOnlyList<T>> Sequence<T>(
this IEnumerable<Option<T>> source)
where T : notnull => source.Traverse(Identity);

/// <summary>
/// Attempts to aggregate a sequence of values using a specified function
/// that may return an optional result, starting with an initial seed value.
/// </summary>
/// <typeparam name="T">The type of the elements in the source sequence.</typeparam>
/// <typeparam name="TAccumulate">The type of the accumulator value.</typeparam>
/// <param name="source">The sequence of elements to aggregate.</param>
/// <param name="seed">The initial value for the accumulator.</param>
/// <param name="func">
/// A function that takes the current accumulator value and an element from
/// the sequence, returning an <see cref="Option{T}"/> containing either
/// the new accumulator value or no value.
/// </param>
/// <returns>
/// An <see cref="Option{T}"/> containing the final accumulated value if the operation succeeds
/// for all elements, or <see cref="Option.None"/> if the aggregation fails at any step.
/// </returns>
/// <remarks>
/// This method iteratively applies the specified function to the accumulator and each
/// element of the source sequence. If the function returns <see cref="Option.None"/>
/// at any step, the aggregation stops and the method returns <see cref="Option.None"/>.
/// </remarks>
[GenerateTransformer]
public static Option<TAccumulate> TryAggregate<T, TAccumulate>(
this IEnumerable<T> source,
TAccumulate seed,
[InstantHandle] Func<TAccumulate, T, Option<TAccumulate>> func)
where TAccumulate : notnull
{
var result = Some(seed);

foreach (var item in source)
{
if (!result.TryGet(out var value))
{
return Option<TAccumulate>.None;
}

result = func(value, item);
}

return result;
}

#endregion

#region Result traversal
Expand Down Expand Up @@ -135,7 +180,7 @@ public static Option<IReadOnlyList<T>> Sequence<T>(
[GenerateTransformer]
public static Result<Option<TOk>, TErr> Traverse<T, TOk, TErr>(
this Option<T> source,
Func<T, Result<TOk, TErr>> selector)
[InstantHandle] Func<T, Result<TOk, TErr>> selector)
where T : notnull
where TOk : notnull
where TErr : notnull =>
Expand Down
44 changes: 43 additions & 1 deletion src/FxKit/Result/Result.Traverse.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using FxKit.CompilerServices;
using JetBrains.Annotations;

namespace FxKit;

Expand Down Expand Up @@ -26,7 +27,7 @@ public static partial class Result
[GenerateTransformer]
public static Result<IReadOnlyList<TOk>, TErr> Traverse<T, TOk, TErr>(
this IEnumerable<T> source,
Func<T, Result<TOk, TErr>> selector)
[InstantHandle] Func<T, Result<TOk, TErr>> selector)
where T : notnull
where TOk : notnull
where TErr : notnull
Expand Down Expand Up @@ -64,5 +65,46 @@ public static Result<IReadOnlyList<TOk>, TErr> Sequence<TOk, TErr>(
where TErr : notnull
=> source.Traverse(Identity);

/// <summary>
/// Attempts to aggregate a sequence of values using a specified function that can fail, returning the result or an error.
/// </summary>
/// <typeparam name="TSource">The type of the elements in the source sequence.</typeparam>
/// <typeparam name="TAccumulate">The type of the accumulated value.</typeparam>
/// <typeparam name="TError">The type of the error value.</typeparam>
/// <param name="source">The sequence of elements to aggregate.</param>
/// <param name="seed">The initial accumulator value.</param>
/// <param name="func">A function to apply to each element and the current accumulator value, which returns a result or an error.</param>
/// <returns>
/// A <see cref="Result{TAccumulate, TError}"/> containing the final accumulated value if the aggregation succeeds,
/// or an error if the aggregation fails at any step.
/// </returns>
/// <remarks>
/// This method iterates over the <paramref name="source"/> sequence and applies the <paramref name="func"/> 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.
/// </remarks>
[GenerateTransformer]
public static Result<TAccumulate, TError> TryAggregate<TSource, TAccumulate, TError>(
this IEnumerable<TSource> source,
TAccumulate seed,
[InstantHandle] Func<TAccumulate, TSource, Result<TAccumulate, TError>> func)
where TAccumulate : notnull
where TError : notnull
{
var result = Ok<TAccumulate, TError>(seed);

foreach (var item in source)
{
if (!result.TryGet(out var value, out var error))
{
return error;
}

result = func(value, item);
}

return result;
}

#endregion
}
Loading