Managed to attend some talks whilst I was at DevWeek 2009, one such talk was by Oliver Sturm on the topic of other cool stuff you can do with C# v3 other than Linq. Really enjoyed the talk introduced me to topics like Fluent Api's. If a series of types have a fluent API they allow you to build up code that reads more like a regular sentence.
E.g. invoices.All.OverDueBy(30.Days).SendReminders();
One of the demo's Oliver did was to show a C# implementation of a standard Ruby technique called Ranges.
numbers = [ 1..10]
Here the numbers variable represents all values between 1 and 10. Some obvious things you would like to do with the range is consume it via foreach, or in Ruby speak Each, or determine if a given value is inside the range.
Oliver took based his implementation from Don Box's implementation and modified a few bits.. Don's implementation is based on .NET 2.0 generics and thus when he builds a Range<T> type you are required to provide delegate instances that provide the next item in the range and determine if a given value is inside the range. So that your don't have to provide delegate instances for the basic primitive types Don supplied a series of static helper methods that supply the necessary delegate instances.
I started thinking if this was necessary in C# 3, could we not use the new Expression syntax to automatically build the ranges without the need to supply delegate instances. The idea is that the Range<T> builds the necessary delegate instances by building expressions, I blogged about this technique a while back that whilst you can't do numeric operations on T as part of standard generics you can build expressions on T that you can attempt to perform an Add operation on.
Based on the previous experience of building a Generic Sum method using Expressions I got to work and built an implementation of Range<T> that attempts to build the the necessary next and isIn delegate instances for you.
public class Range<T> : IEnumerable<T>
{
public T Start { get; private set; }
public T End { get; private set; }
private Func<T, T> next;
private Func<T, bool> isIn;
public Range(T start, T end, T step)
: this(start, end, step, false)
{
}
public Range(T start, T end, T step, bool highToLow)
{
InitStartAndEnd(start, end);
ParameterExpression current = Expression.Parameter(typeof(T), "current");
Expression<Func<T, T>> nextExpr = Expression.Lambda<Func<T, T>>(
Expression.Add(
current,
Expression.Constant(step)),
current);
Expression<Func<T, bool>> isInExpr = null;
if (highToLow)
{
isInExpr = Expression.Lambda<Func<T, bool>>(
Expression.And(
Expression.GreaterThanOrEqual(current, Expression.Constant(end)),
Expression.LessThanOrEqual(current, Expression.Constant(start))),
current);
}
else
{
isInExpr = Expression.Lambda<Func<T, bool>>(
Expression.And(
Expression.GreaterThanOrEqual(current, Expression.Constant(start)),
Expression.LessThanOrEqual(current, Expression.Constant(end))),
current);
}
isIn = isInExpr.Compile();
next = nextExpr.Compile();
}
public Range(T start, T end, Func<T, T> next, Func<T, bool> isIn)
{
InitStartAndEnd(start, end);
this.next = next;
this.isIn = isIn;
}
public bool IsIn(T val)
{
return isIn(val);
}
public override string ToString()
{
return String.Format("{0}..{1}", Start, End);
}
private void InitStartAndEnd(T start, T end)
{
Start = start;
End = end;
}
public IEnumerator<T> GetEnumerator()
{
T current = Start;
yield return current;
while (isIn(current))
{
current = next(current);
yield return current;
}
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
With this type defined you now write code like this
var digits = new Range<int> (0,9,1);
I can now consume all the digits via a simple foreach loop, or perhaps use the range as part of a Linq expression since my Range<T> implements IEnumerable<T>.
digits.Except( new int[] { 0,2,4,6,8 } ).ToList().ForEach( i=>Console.WriteLine(i) );
This will print out all the odd numbers from the range..
With the addition of another C# v3 feature extension methods, we can write a "fluent api" style method.
public static class RangeUtil
{
public static bool In<T>(this T val, Range<T> range)
{
return range.IsIn(val);
}
}
I can now write
bool isSingleDigitValue = 5.In( digits ) ;
I really wanted to support enumerations without the need to define your own next and IsIn delegate instances but couldn't quite get there. Im sure its possible with a bit more thought. But for now enum ranges need to be defined by creating a new type derived from the Range<T> type.
public enum Places { First, Second, Third, Fourth };
public class PlacesRange : Range<Places>
{
public PlacesRange(Places start, Places end)
: base(start, end, p => p + 1, p => p>= start && p <=end )
{
}
}
Note this only works if there are no gaps in the enumeration values.
There are other situations were the normal Add operator is not sufficient
public class DayRange : Range<DateTime>
{
public DayRange(DateTime start, DateTime end )
: base(start.Date, end.Date, d => d.AddDays(1), d => d >= start && d <= end )
{
}
}
To use the DayRange type
DayRange days = new DayRange(new DateTime(2009, 1, 1), new DateTime(2009, 12, 30));
if (DateTime.Now.In(days))
{
Console.WriteLine("You are in Year 2009");
}
In conclusion Ranges do look pretty cool..however it should be noted that using ranges to represent a simple iteration is far more expensive than a normal for loop, but they are great for building fluent api’s.
You can download the complete source here
No comments:
Post a Comment