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

Aggregate all or some/partial/none #899

Open
atifaziz opened this issue Nov 24, 2022 · 2 comments
Open

Aggregate all or some/partial/none #899

atifaziz opened this issue Nov 24, 2022 · 2 comments

Comments

@atifaziz
Copy link
Member

atifaziz commented Nov 24, 2022

I'd like to propose adding an overload of Aggregate that will use a function to determine the validity of each item in the source sequence. As long as items are valid, they will be accumulated and a function will be called at the end to turn the accumulator into a result. As soon as one item is invalid, the iteration of the source sequence will be halted and a function will be called to turn the partial accumulation into a result.

This enables one to implement the following strategies:

  • Accumulate all good items or none.
  • Accumulate all good items or the good ones so far (partial/some case).

A prototype of such an extension would be as follows:

static partial class MoreEnumerable
{
    public static TResult
        Aggregate<TFirst, TState, TSecond, TResult>(
            this IEnumerable<TFirst> source,
            TState seed,
            Func<TFirst, (bool, TSecond)> secondChooser,
            Func<TState, TSecond, TState> folder,
            Func<TState, TResult> allSelector,
            Func<TState, TFirst, TResult> partialSelector)
    {
        var state = seed;

        foreach (var first in source)
        {
            if (secondChooser(first) is (true, var second))
                state = folder(state, second);
            else
                return partialSelector(state, first);
        }

        return allSelector(state);
    }
}

This is like Choose + Aggregate rolled into one, but which cannot be done otherwise by combining the two without considerable and additional effort. The following example shows the above in action, together with how it differs from Choose:

using System;
using System.Collections.Immutable;
using MoreLinq;

var inputs =
    from s in new[]
    {
        "O,l,2,3,4,S,6,7,B,9",
        "0,1,2,3,4,5,6,7,8,9",
    }
    select new
    {
        Source = s,
        Tokens = s.Split(','),
    };

foreach (var input in inputs)
{
    var xs = input.Tokens.Choose(s => (int.TryParse(s, out var n), n));
    Console.WriteLine($"The good stuff: {string.Join(", ", xs)}");

    if (input.Tokens
             .Aggregate(ImmutableArray<int>.Empty,
                        s => (int.TryParse(s, out var n), n),
                        (a, n) => a.Add(n),
                        a => a,
                        (_, _) => default) is { IsDefault: false } ys)
    {
        Console.WriteLine($"All okay: {string.Join(", ", ys)}");
    }
    else
    {
        Console.WriteLine($"Input is bad: {input.Source}");
    }
}

The output of the above example will be as follows:

The good stuff: 2, 3, 4, 6, 7, 9
Input is bad: O,l,2,3,4,S,6,7,B,9
The good stuff: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
All okay: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
@declard
Copy link

declard commented Mar 28, 2023

It could've been simpler with a single discriminated union type:

TResult Aggregate<TElement, TState, TResult>(
    this IEnumerable<TElement> source,
    TState seed,
    Func<TState, TSecond, FoldResult<TState, TResult>> folder,
    Func<TState, TResult> selector)

where FoldResult<,> is either Accept(TState) or Break(TResult)

input.Tokens.Aggregate(
    ImmutableArray<int>.Empty,
    s => int.TryParse(s, out var n) ? FoldResult.Accept(a.Add(n)) : FoldResult.Break(default(ImmutableArray<int>)),
    a => a) is { IsDefault: false } ys)

The only question is how to implement that type. It should be a structure + a static class with smart ctors for the two options + a couple of structures for Accept and Break cases implicitly converted to FoldResult<,>...

Unless there will be just a version without final TState->TResult projection and without TResult type param at all

@atifaziz
Copy link
Member Author

atifaziz commented Mar 28, 2023

@declard That's true but the approach taken so far in MoreLINQ is what I like to call bring your own types (BYOT). This can complicate signatures a bit, but it gives the caller full control and also alleviates the burden of introducing and maintaining new types. If you squint, then secondChooser returns an option type disguised as (bool, T) (see also Optuple) and allSelector and partialSelector act as constructors for each outcome type/arm. We went through a similar discussion and design for Choose. While what you're suggesting is ideal, discriminated unions are not very straightforward to code efficiently in C#, but this may change in the future. There's still the problem that unless some common types like option and result exist in .NET, each library introducing their own version is only going to lead to misery.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants