Andy's observations as he continues to attempt to know all that is .NET...

Sunday, March 29, 2009

Ruby Ranges in .NET

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:

About Me

My photo
Im a freelance consultant for .NET based technology. My last real job, was at Cisco System were I was a lead architect for Cisco's identity solutions. I arrived at Cisco via aquisition and prior to that worked in small startups. The startup culture is what appeals to me, and thats why I finally left Cisco after seven years.....I now filll my time through a combination of consultancy and teaching for Developmentor...and working on insane startups that nobody with an ounce of sense would look twice at...