Marius Schulz
Marius Schulz
Front End Engineer

Designing Extension Methods in .NET for Composition

I recently started working on the 2.0.0 version of ExtraLINQ, a small class library that provides additional extension methods for working with .NET collections and sequences. This second major release sports some internal infrastructure changes, but also a variety of new extension methods.

#The TakeEvery Method

One of the new extension methods is TakeEvery, which returns every n-th element of a given sequence. Let's use the following array of Fibonacci numbers as an example:

int[] fibonaccis = { 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55 };

Here's how you would use TakeEvery to select every other number from the array:

int[] everyOtherFibonacci = fibonaccis.TakeEvery(2).ToArray();
// [0, 1, 3, 8, 21, 55]

Similarly, you can also select only every third number:

int[] everyThirdFibonacci = fibonaccis.TakeEvery(3).ToArray();
// [0, 2, 8, 34]

You get the idea.

#Ambiguity of "take every …"

As you can see, in both cases the first item 0 is returned, and from there every item that is offset by a multiple of step, the parameter passed to TakeEvery. In the first example, the indices of the selected items are 0, 2, 4, 6, 8, and 10.

That's not the only right way to interpret take every other item, though. In fact, I could have implemented TakeEvery to return all the items at indices 1, 3, 5, 7, and 9 instead, thus skipping the first step-1 items. The resulting sequence would equal [1, 2, 5, 13, 34] in this case.

The decision I needed to make when writing the TakeEvery method was where to start the counting. In the end, I went with the first approach shown in the above code snippet because it nicely plays together with other LINQ methods.

If you want to select every other item starting at the second item (skip, take, skip, take, …) rather than the first one (take, skip, take, skip, …), you can combine TakeEvery with Skip, a basic building block of LINQ, to achieve the desired offset:

int[] everyOtherFibonacci = fibonaccis
    .Skip(1)
    .TakeEvery(2)
    .ToArray();

// [1, 2, 5, 13, 34]

Note that it wouldn't have worked the other way around. If the first item were always skipped by TakeEvery, neither Skip nor Take would include it in the resulting sequence.

#Designing for Composition

I could also have added to the TakeEvery method another parameter named offset which specifies how many items to skip before starting the stepping. This, however, would have made the extension method more complicated than necessary.

Instead, I designed it for composition because there's already a method available out of the box (namely Skip) to achieve such an offset. Also, this makes understanding, implementing, and testing the method much easier.

The next time you have to choose between two alternatives like in the above example, opt for the one that allows for flexible functional composition with other logical units already available within your code.