Feb 15, 2011

LINQ III–Using the Aggregate Operator to replace more complex foreach loops

In a previous blog I discussed some simple techniques for replacing foreach loops. This entry will go a step further and show how you can use LINQ’s Aggregate  operator to replace a foreach loop that calls a method that takes multiple parameters that need to be modified each time through the loop. This requires the Aggregate overload that has this signature:
public static TAccumulate Aggregate <TSource, TAccumulate>(
this IEnumerable<TSource> source, TAccumulate seedValue
Func<TAccumulate, TSource, TAccumulate> theFunc);
If you’ve ever seen the Aggregate method explained it typically uses the simpler form that has only one template argument and the typical example shows how you use it to replace an Add function like this:
var someTotal = someList.Aggregate((a,b) => (a+b)));
Where a, b and the return type are all of the same type. The second form shown above allows much more flexibility in how Aggregate can be used.


imageThe example used herein was taken from some actual code where I had an array (I know, how primitive) of x axis positions. The code has to create touch regions on a touch panel at the specified positions. Each touch region is assigned an index and the hardware returns that index when the corresponding touch region is touched. Each touch region can also have an “up” and a “down” image associated with it. In this example I have an “invisible” image because I want the user to just touch the displayed digit. The array is defined as: 

private readonly int[] _digitPositionsX;
The array is typically populated in the class constructor.

STEP 1 – Write the foreach Approach

Each touch region needs both a Rectangle defining its size as well as the index for the touch region. In this application, there might be multiple widgets as shown above and  the touch region for each  screen widget will start at a different number  but will increase by one for each touch area for the widget. The widget will have a touch region for every element in _digitPositionsX. We can start out just writing the standard foreach approach where we create the Rectangle we need and manually track the touch index value. TsTouchIndices.DIGIT_TOUCH_INDEX just contains the starting touch index for this particular widget and the TsImageIndices.INVISIBLE_BUTTON contains the image index.

private void CreateDigitTouchRegion()
{
byte touch_index = TsTouchIndices.DIGIT_TOUCH_INDEX;
int width = _parentControl.DigitWidth;
int height = _parentControl.DigitHeight;
int y = _parentRectangle.Y; 
const byte INVIS_BTN = TsImageIndices.INVISIBLE_BUTTON;
 
foreach (int x in _digitPositionsX)
{
Rectangle rect = new Rectangle(x, y, width, height);
_touchScreen.DrawTouchRegion(rect, touch_index, INVIS_BTN, INVIS_BTN);
++touch_index;
}
}
There is nothing wrong with this code. In fact, it has the benefit of being very easy to understand. However, I’m interested in seeing if I can use LINQ to make this even cleaner and more maintainable. I can decide after I’m done if the code is going to maintainable by the average programmer on the project. That decision of course will depend on how fluent the team is in LINQ.

STEP 2a – Extract out the call to DrawTouchRegion and the increment of touchIndex

private byte DrawDigitTouchRegion( Rectangle rect, byte touchIndex)
{
const byte INVIS_BUTTON = TsImageIndices.INVISIBLE_BUTTON;
_touchScreen.DrawTouchRegion(rect, touchIndex, INVIS_BUTTON, INVIS_BUTTON);
return ++touchIndex;
}

Step 2b – Rewrite our original method to use the newly extracted method.
private void CreateDigitTouchRegion()
{
byte touch_index = TsTouchIndices.DIGIT_TOUCH_INDEX;
int width = _parentControl.DigitWidth;
int height = _parentControl.DigitHeight;
    int y = _parentRectangle.Y; 

foreach (int x in _digitPositionsX)
{
Rectangle rect = new Rectangle(x, y, width, height);
touch_index = DrawDigitTouchRegion1(rect, touch_index);
}
}

STEP 3 – Convert the foreach to LINQ

private void CreateDigitTouchRegion()
{
int width = _parentControl.DigitWidth;
int height = _parentControl.DigitHeight;
    int y = _parentRectangle.Y;

_digitPositionsX.Select(x => new Rectangle(x, y, width, height))
.Aggregate<Rectangle, byte>(TsTouchIndices.DIGIT_TOUCH_INDEX, 
(current, rect) => DrawDigitTouchRegion1(rect, current));
}


Step 4 – reorder parameters to DrawDigitTouchRegion()
We have an impedance mismatch of sorts, the parameter order to DrawDigitTouchRegion will clean up better if we make the accumulated value the first parameter.
private byte DrawDigitTouchRegion( byte touchIndex, Rectangle rect)
{
const byte INVIS_BUTTON = TsImageIndices.INVISIBLE_BUTTON;
_touchScreen.DrawTouchRegion(rect, touchIndex, INVIS_BUTTON, INVIS_BUTTON);
return ++touchIndex;
}

Step 5 – Reorder the call with the new parameters…

and then simplify by converting
(current, rect) => DrawDigitTouchRegion(rect, current));
(show in green in Step 3) to the method group DrawDigitTouchRegion

private void CreateDigitTouchRegions()
{
int width = _parentControl.DigitWidth;
int height = _parentControl.DigitHeight;
int y = _parentRectangle.Y;

const int START_TOUCH_INDEX = TsTouchIndices.DIGIT_TOUCH_INDEX;
_digitPositionsX.Select(x => new Rectangle(x, y, width , height))
.Aggregate<Rectangle, byte>(START_TOUCH_INDEX, DrawDigitTouchRegion);
}

Summary

Refactoring into two methods was a good idea but it certainly didn’t require LINQ . LINQ did eliminate the temporary touch_index variable. Whether the final code is more maintainable is open to debate, the statement containing the Aggregate function is not something most programmers will immediately grok, so I’m a little skeptical about whether this particular example was “improved” by LINQ.  I’m very receptive to any comments that show how to significantly improve the readability of that statement.

However, the declarative approach often has non-obvious advantages (particularly in the realm of code optimization by the compiler) over the imperative approach. Also, I thought the example was worth showing just to demonstrate some of the power of LINQ that may not be exposed by the typical text on the subject.

About Me

My photo
Tod Gentille (@todgentille) is now a Curriculum Director for Pluralsight. He's been programming professionally since well before you were born and was a software consultant for most of his career. He's also a father, husband, drummer, and windsurfer. He wants to be a guitar player but he just hasn't got the chops for it.