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

Thursday, March 26, 2009

Devweek 2009

Just posted all my demos from my sessions at devweek, checkout all the Rock Solid Knowledge guys demos via Rock Solid Knowledge Conferences.  Also just like to say what a fantastic conference Devweek is, met loads of interesting people and had a lot of fun delivering the talks.  Patterns Dartboard worked out really well...Tim tried hard to get me to consider moving to Ruby...and Oliver scared me with F#...

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...