morelinq / MoreLINQ

Extensions to LINQ to Objects
https://morelinq.github.io/
Apache License 2.0
3.68k stars 413 forks source link

Match: ([x], x → bool) → [(bool, x)] #624

Open atifaziz opened 4 years ago

atifaziz commented 4 years ago

Like Where except instead of filtering, it couples the predicate's result with the tested element of the sequence.

Example

var strs = new[] { "foo", "bar", "baz" };
foreach (var (matched, s) in strs.Match(s => Regex.IsMatch(s, @"^b")))
    Console.WriteLine($"{s} => {matched}");

Prototype

public static IEnumerable<(bool Success, T Element)>
    Match<T>(this IEnumerable<T> source, Func<T, bool> predicate) =>
        from e in source
        select (predicate(e), e);

It's very trivial so the only value proposition here is tuple construction with reasonably named tuple elements (although alternatives for Success would be Matched and IsMatch).

Orace commented 4 years ago

In your proposition, the projected value type is limited to bool. I propose a more generalized Map method :

public static IEnumerable<(TSource element, TResult projection)> Map<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TResult> func);

I reversed the order of the element in the tuple since this order is more logical for me.

Implementation is straightforward.

The two line below are equivalent:

source.Map(func);
source.Select(v=>(v, func(v));
atifaziz commented 4 years ago

In your proposition, the projected value type is limited to bool.

But that's the whole idea! Map or Select already exists.

Orace commented 4 years ago

Actually Map doesn't exist in MoreLinq or I can't find it. I neither can't find a method that do what I propose for Map. It's generic and work with your example:

var strs = new[] { "foo", "bar", "baz" };
foreach (var (s, matched) in strs.Map(s => Regex.IsMatch(s, @"^b")))
    Console.WriteLine($"{s} => {matched}");

It's replace .Select(v => (v, ...)) by .Map(v => ...). every penny counts

atifaziz commented 4 years ago

Your foreach example is fine but it's one example where it looks good. However, the deconstruction into s and matched doesn't work (yet) with neither lambdas (when using method chaining) nor from binding in LINQ query syntax. The closest you can come to is this:

var outputs =
    from (string Input, bool Matched) e in strs.Map(s => Regex.IsMatch(s, @"^b"))
    select $"{e.Input} => {e.Matched}";

foreach (var output in outputs)
    Console.WriteLine(output);

That's a Cast in disguise, and even worse, doesn't work without explicit types so throws anonymous types out of the window. You might as well just use a normal projection:

var outputs =
    from e in strs.Select(s => (Input: s, Matched: Regex.IsMatch(s, @"^b")))
    select $"{e.Input} => {e.Matched}";

I am completely happy with closing this as too trivial and I was hoping to be challenged to that conclusion. We reject trivial additions (especially one to two liners that don't provide much algorithmic intelligence) all the time. My idea, frankly, with Match was to somewhat complement Choose:

var items =
    strs.Match(s => Regex.IsMatch(s, @"^b"))
        .Pipe(e => Console.WriteLine($"{e.Element} => {e.Success}")) // log
        .Choose(e => e);

foreach (var item in items)
    Console.WriteLine(item);

This is why it's not just a generic mapping. This is just making the following Select easier, especially with respect to tuple naming and element ordering:

var items =
    strs.Select(s => (Success: Regex.IsMatch(s, @"^b"), Element: s))
        .Pipe(e => Console.WriteLine($"{e.Element} => {e.Success}"))
        .Choose(e => e);

Your Map works fine too but requires Choose to reverse the tuple elements:

var items =
    strs.Map(s => Regex.IsMatch(s, @"^b"))
        .Pipe(e => Console.WriteLine($"{e.Element} => {e.Result}"))
        .Choose(e => (e.Result, e.Element));

every penny counts

What are the pennies we are measuring or talking about here?

Orace commented 4 years ago

every penny counts

What are the pennies we are measuring or talking about here?

Chocolate ones ;)