Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Break from function composition on condition using LINQ

Tags:

c#

linq

Is there a LINQ equivalent to the following?

static T Compose<T>(T source,
    IEnumerable<Func<T, T>> funcs,
    Func<T, bool> cond)
{
    foreach (var func in funcs)
    {
        source = func(source);

        if (cond(source)) break;
    }

    return source;
}

// This ends the composition as soon as a null is returned
var result = Compose(x, funcs, x => x == null);

Essentially, I wish to compose functions on top of each other, and break if the current value matches a particular condition.

result = f(g(h(x)))  // if condition is met somewhere within this chain,
                     // it will immediately break and return

EDIT: I thought of something along the lines of

funcs.Aggregate(seed, (x, f) => cond(x) ? x : f(x));

but that looks confusing and also ends up enumerating over the entire collection, rather than breaking when the condition is met.

like image 248
Mateen Ulhaq Avatar asked Nov 26 '22 04:11

Mateen Ulhaq


2 Answers

One way of doing this is to Aggregate until the condition is met. So, I define an extension method:

public static TAccumulate AggregateUntil<TSource, TAccumulate>(
    this IEnumerable<TSource> source,
    TAccumulate seed,
    Func<TAccumulate, TSource, TAccumulate> func,
    Func<TAccumulate, bool> predicate)
{
    if (source == null)
        throw new ArgumentNullException(nameof(source));

    if (func == null)
        throw new ArgumentNullException(nameof(func));

    if (predicate == null)
        throw new ArgumentNullException(nameof(predicate));

    var accumulate = seed;
    foreach (var item in source)
    {
        accumulate = func(accumulate, item);
        if (predicate(accumulate)) break;
    }
    return accumulate;
}

We may now write ComposeUntil as:

public static T ComposeUntil<T>(
    this IEnumerable<Func<T, T>> source,
    T seed,
    Func<T, bool> predicate)
{
    return source.AggregateUntil(seed, (x, f) => f(x), predicate);
}

And use it in the following manner:

var funcs = new List<Func<int, int>>
{
    x => x + 1,
    x => x + 2,
    x => x + 3,
    x => x + 4
};

var result = funcs.ComposeUntil(0, x => x >= 6);
// result == 6
like image 166
Mateen Ulhaq Avatar answered Nov 28 '22 16:11

Mateen Ulhaq


This works nicely:

var funcs = new Func<string, string>[]
{
    t => t + "1",
    t => t + "2",
    t => t + "3",
};

Func<string, bool> cond = t => t.Length == 3;

Func<string, string> func =
    t => funcs.Aggregate(t, (a, f) => !cond(a) ? f(a) : a);

Console.WriteLine(func("A"));

It produces "A12" a string of length 3.

To avoid iterating the collection then you need to introduce a new operator, .Scan(...), which I get from the Reactive Extensions team's Interactive Extensions (NuGet "Ix-Main").

.Scan(...) works like .Aggregate(...) but it produces a value at each step.

Func<string, string> func =
    t => funcs
        .Scan(t, (a, f) => f(a))
        .SkipWhile(x => !cond(x))
        .FirstOrDefault();

That produces the same result as above, but doesn't iterate the entire collection.

Here are both options inserted into the Compose method:

(1)

static T Compose<T>(T source,
    IEnumerable<Func<T, T>> funcs,
    Func<T, bool> cond)
{
    return funcs.Aggregate(source, (a, f) => !cond(a) ? f(a) : a);
}

(2)

static T Compose<T>(T source,
    IEnumerable<Func<T, T>> funcs,
    Func<T, bool> cond)
{
    return
        funcs
            .Scan(source, (a, f) => f(a))
            .SkipWhile(t => !cond(t))
            .FirstOrDefault();
}
like image 35
Enigmativity Avatar answered Nov 28 '22 18:11

Enigmativity