Inject and other goodness in C#
October 3, 2006 03:00 PM • 0 comments
Two of my favourite C# 2.0 features come together in something of a perfect storm in a set of static methods on the Array class. These generic methods accept delegates that can be used to search and change arrays in a nice strongly typed way. A lot of these are inspired by features in functional and dynamic languages like lisp, smalltalk, ruby and python and are discussed in that context by Martin Fowler at http://www.martinfowler.com/bliki/CollectionClosureMethod.html.
The methods include gems like Find, FindAll, Exists, TrueForAll, the amazingly useful ConvertAll and overloads of BinarySearch and Sort that accept a generic delegate to compare items.
Find and the related methods can be used like this:
Track[] allTracks = GetAllMyTracks();
Track[] greatMusic = Array.FindAll(allTracks, delegate(Track t) {
return t.Rating == 5;
});
Track firstToDelete = Array.Find(allTracks, delegate(Track t) {
return t.Rating == 1;
});
bool someTracksUnrated = Array.Exists(allTracks, delegate(Track t) {
return t.Rating == 0;
});
ConvertAll is a real workhorse and very similar to the map or collect function in dynamic languages.
Track[] allTracks = GetAllMyTracks();
ListViewItem[] items = Array.ConvertAll<Track, ListViewItem>(allTracks, delegate(Track t) {
return new ListViewItem(new string [] {t.Name, t.Album, t.Rating});
});
string[] albums = Array.ConvertAll<Track, string>(allTracks, delegate(Track t) {
return t.Album;
});
These examples don’t do map/collect justice — once you start using map/collect you realise it solves a basic problem that you encounter all the time. Unfortunately the C# syntax is a little long-winded and not helped by the need to explicitly state the generic parameters to the method — in most cases the compiler can figure these out for you, but it falls down in this case. In ruby the second example would be:
albums = tracks.map{|t| t.album}
In fact the long-windedness is a bit of a problem with all of these methods, particularly when compared with their dynamic language counterparts. Luckily, the situation improves a LOT with C# 3.0, due late next year as part of Visual Studio Orcas.
C# 3.0 takes the generic delegates introduced in version 2.0 and makes them much more compact by relying on the type inference features introduced in Orcas. Writing the albums code in C# 3.0 looks like this:
albums = Array.ConvertAll(tracks, t => t.Album);
One method that seems to be missing from both the static methods on Array and from the current LINQ bits though, is the inject function. The inject function is a tricky little feller and I must admit I was completely stumped by it when I first encountered it in Ruby. Inject basically allows you to create some single accumulated value based on the entries in a list. A great example is summing numbers:
int[] numbers = new int[] { 1, 2, 3 };
int sum = Inject(numbers, 0, delegate(int total, int number)
{
return total + number;
}));
Here, the zero we pass as the second parameter to the Inject method is the initial value of our accumulator. Inject then calls our delegate for each member of the numbers array, passing both the accumulator (total in this case) and the array member. The delegate then returns a new value for the accumulator.
In C# 3.0, we again get a much nicer syntax:
int sum = Inject(numbers, 0, (total, number) => total + number);
While summing is the classic inject example, once you get the hang of it you see how useful this simple operation is. Here’s max:
int max = Inject(numbers, int.MinValue, (current, number) => Math.Max(current, number));
If you want to count the number of times a value occurs, you can use:
int count = Inject(numbers, 0, (total, number) => number == 1 ? total + 1 : total);
Building up a hash or dictionary is also a great use of Inject, though it’s a little clunky in C#, even in version 3.0. This counts the number of times each value occurs in the array “a”.
Dictionary<int, int> frequencies = a.Inject(new Dictionary<int, int>(), (dict, i) =>
{
dict[i] = (dict.ContainsKey(i) ? dict[i] + 1 : 1);
return dict;
});
It’s much neater in ruby:
frequencies = a.inject({}){|hsh,i| hsh[i] ||= 0; hsh[i] += 1; hsh }
Ruby’s more concise syntax also means you can do a “group by” very easily:
# a is an array of customers, let's group them by city
by_city = a.inject({}){|hsh,c| hsh[c.city] ||= []; hsh[c.city] << c; hsh}
In C#, this would be:
Dictionary<string, List<Customer>> byCity = a.Inject(new Dictionary<string, List<Customer>>(), (dict, c) =>
{
if (dict.ContainsKey(c.City) == false)
dict[c.City] = new List<Customer>();
dict[c.City].Add(c);
return dict;
});
Inject is pretty easy to write, though in C# 2.0 you’ll have to define a delegate for it as well:
public delegate T2 Injector<T1, T2>(T2 accumulator, T1 item);
public T2 Inject<T1, T2>(IEnumerable<T1> enumerable, T2 initialValue, Injector<T1, T2> func)
{
T2 accumulator = initialValue;
foreach (T1 item in enumerable)
{
accumulator = func(accumulator, item);
}
return accumulator;
}
Just for good measure, here’s both inject and map in C# 3.0, this time written as extension methods on IEnumerable, which means they can be lazily evaluated:
public static T2 Inject<T1, T2>(this IEnumerable<T1> enumerable, T2 accumulator, Func<T2, T1, T2> func)
{
foreach (T1 item in enumerable)
{
accumulator = func(accumulator, item);
}
return accumulator;
}
public static IEnumerable<T2> Map<T1, T2>(this IEnumerable<T1> enumerable, Func<T1, T2> func)
{
foreach (T1 item in enumerable)
{
yield return func(item);
}
}