tags:

views:

447

answers:

4

I'm trying to create a dynamic filter using a combination of lambda functions. But the number of lambda functions could vary based on the number of different filters the user applies.

I want something that behaves like this

//images is a List<ImageInfo>
var result1 = images
.Where(image => image.Bytes < 1000)
.Where(image => image.Height < 100)
.Where(image => image.Width < 100);

Where the height filter is only applied to those images that pass the bytes filter. And the width filter is only applied by to those images that pass the height filter.

But, the dynamic fashion of how the filters are turned on and off by the users doesnt allow me to create a single lambda function.

Instead I'll create a list of lambda functions and then need to apply them to the list of images. So I would end up with something like this; multiple individual lambda functions.

var filter1 = images.Where(image => image.Bytes < 1000);
var filter2 = images.Where(image => image.Height < 100);
var filter3 = images.Where(image => image.Width < 100);

How can I join multiple lambda functions together to get my final list of filtered images?

I did this

var result = filter1.Intersect<ImageInfo>(filter2).Intersect<ImageInfo>(filter3);

But each filter spins through the main list of images to get its subset of images, and then does an intersection calculation, which takes way too much CPU.

So what I'm looking for is a way to take an arbitrary list of lambda functions (Expressions...whatever) and join them in a way that gets executed the way the first example I showed does.

A: 

why don't you just apply the functions one after the other, as in the first example?

Youre filters have the signature of

Func<IEnumerable<ImageInfo>, IEnumerable<ImageInfo>>

so just apply each filter to the result of the last one?

like this?

IEnumerable<ImageInfo> filtered = images;

if(filterByBytes)
    filtered = filtered.Where(image => image.Bytes < 1000);

if(filterByHeight)
    filtered = filtered.Where(image => image.Height < 100);

if(filterByWidth)
    filtered = filtered.Where(image => image.Width < 100);

Edit re: comments, off the top of my head, something like...

List<Func<IEnumerable<ImageInfo>, IEnumerable<ImageInfo>>> lambdas = new List<Func<IEnumerable<ImageInfo>, IEnumerable<ImageInfo>>>();

lambdas.add(x => x.Where(image => image.Bytes < 1000));
lambdas.add(x => x.Where(image => image.Height < 100));
lambdas.add(x => x.Where(image => image.Width < 100));

foreach(var lambda in lambdas)
    images = lamdba.Invoke(images);
Andrew Bullock
True, I could do that, but because the list of filters is fairly dynamic (new ones could be added at any time from any source) I'd like to store the lambda function in a List of lambda functions, and then spin through the list, applying them one at a time.
How do you store a lambda function as a variable?
remember that lambdas are just delegates (a function call). From my answer "Func<IEnumerable<ImageInfo>, IEnumerable<ImageInfo>>" that is the type for a delegate which takes an IEnum of imageinfo and returns the same
Andrew Bullock
+1  A: 

Okay, how about:

static Func<T, bool> CombineWithAnd<T>(IEnumerable<Func<T, bool>> filters)
{
    return x =>
    {
        foreach (var filter in filters)
        {
            if (!filter(x))
            {
                return false;
            }
        }
        return true;
    };
}

Is that what you're after? Basically that captures the set of filters within the lambda expression, and applies them one after another (with short-circuiting) when the returned lambda is used. Obviously you could do the same in an "Or" fashion too:

static Func<T, bool> CombineWithOr<T>(IEnumerable<Func<T, bool>> filters)
{
    return x =>
    {
        foreach (var filter in filters)
        {
            if (filter(x))
            {
                return true;
            }
        }
        return false;
    };
}

Note that if the collection of filters is modified after calling the method (e.g. it's a List<T> and you add a new filter) then the returned "combination" filter will reflect those changes. If you don't want that behaviour, add:

filters = filters.ToList();

as the first line of the method, to effectively take a copy. Note that delegates are immutable, so you don't need to worry about them changing.

Jon Skeet
A: 

Oh sweet! You were very close, but close enough the get me the answer. To apply all filters, the return true needs to move outside the foreach loop (shown below). But yes, that's exactly what I'm looking for.

One question, or comment. What really threw me about this function was the x variable. I had to run and debug the code to figure out that the x was going to be of type . I've never seen a variable that didnt have a type or var declaration before and that really threw me. Can you explain a bit the C# rules for allowing the variable x without any kind of declaration?

Very elegant solution by the way

static Func<T, bool> CombineWithAnd<T>(IEnumerable<Func<T, bool>> filters)
{
    return x =>
    {
     foreach (var filter in filters)
     {
      if (!filter(x))
      {
       return false;
      }
     }
     return true;
    };
}
Doh, thanks, fixed my answer. The rules around the parameters for lambda expressions are basically that if the compiler can work out their types based on what the lambda expression has to be converted to, you don't have to specify the types explicitly.
Jon Skeet
A: 

For what it's worth, you needn't use a loop. (In Python since I haven't used C#:)

def conjoin(filter1, filter2):
    return lambda x: filter1(x) and filter2(x)

def conjoin_list(filters):
    return reduce(conjoin, filters)

(Equivalent of the reduce function in C#, called fold there.)

Darius Bacon