- Saturday, October 21, 2006
Channeling Ruby in C# 3.0
I happened across a posting earlier today which contrasted Java and Ruby code snippets. The intent of the post (an enjoyable read, by the way) was to demonstrate the elegance of Ruby's simple, compact syntax, using the more verbose Java as a counter-example.
This post, on the other hand, attempts to use some of C#'s more recent features to demonstrate how static languages are closing the "elegance" gap (for lack of a better term) which the original poster demonstrated.
Extension Methods
The author opens with this example:
10.times { print "ho" }The author of the post implies that this is possible because Ruby treats
10as an object, while Java does not. The same is true in C# (integers do not inherit fromObject, and are "primitive"); however, with the help of extension methods in C# 3.0 I can very closely emulate Ruby's syntax:10.Times(i => Console.Write("ho"));The integer here is never boxed -- this is just syntactic sugar for explicitly calling the extension method with an explicit argument. Here's the entire source of the extension method:
public static void Times(this int value, Action<int> action) { for (int i = 0; i < value; i++) { action(i); } }The extension method syntax is a C# 3.0 specific feature, but is callable from .NET 2.0 code (and in fact, runs on the 2.0 CLR) using the more explicit syntax:
RubyishExtensions.Times(10, delegate(int) { Console.Write("ho"); });The author lists several other examples which are easily replicated using C# 3.0 extension methods. Looking for an odd number?
if 11.odd? print "Odd!"
In C# 3.0:
if (11.Odd()) Console.Write("Odd!");The relevant extension method:
public static bool Odd(this int value) { return value % 2 != 0; }Counting bytes?
102.megabytes + 24.kbytes + 10.bytes
In C# 3.0:
102.Megabytes() + 24.Kilobytes() + 10.Bytes();
Three very simple extension methods help here:
public static int Megabytes(this int value) { return value * 1024 * 1024; } public static int Kilobytes(this int value) { return value * 1024; } public static int Bytes(this int value) { return value; }How about ordinalizing a number?
print "Currently in the #{2.ordinalize} trimester"Again, in C# 3.0:
Console.Write("Currently in the {0} trimester", 2.Ordinalize());This extension method is a little longer -- but, like all extension methods, it's reusable:
public static string Ordinalize(this int value) { string ordinalized; if ((value % 100).Between(11, 13)) { ordinalized = "th"; } else { switch (value % 10) { case 1: ordinalized = "st"; break; case 2: ordinalized = "nd"; break; case 3: ordinalized = "rd"; break; default: ordinalized = "th"; break; } } return value + ordinalized; } public static bool Between(this int value, int min, int max) { return value >= min && value <= max; }Our extension method fun ends with a few date examples:
puts "Running time: #{1.hour + 15.minutes + 10.seconds} seconds" 20.minutes.ago 20.minutes.until("2006-10-9 11:00:00".to_time)In C# 3.0:
Console.Write("Running time: {0} seconds", (1.Hours() + 15.Minutes() + 10.Seconds()).TotalSeconds); 20.Minutes().Ago(); 20.Minutes().Until(DateTime.Parse("2006-10-9 11:00:00"));Predictably, it's extension methods to the rescue again:
public static TimeSpan Hours(this int value) { return TimeSpan.FromHours(value); } public static TimeSpan Minutes(this int value) { return TimeSpan.FromMinutes(value); } public static TimeSpan Seconds(this int value) { return TimeSpan.FromSeconds(value); } public static DateTime Ago(this TimeSpan timeSpan) { return DateTime.Now - timeSpan; } public static DateTime Until(this TimeSpan timeSpan, DateTime target) { return target - timeSpan; }Properties
The next example demonstrates a feature of Ruby's syntax which I cannot emulate -- its tight property syntax:
class Circle attr_accessor :center, :radius end
C# is a bit lighter than Java, but it's more finger effort here:
class Circle { private Point center; private float radius; public Point Center { get { return this.center; } set { this.center = value; } } public float Radius { get { return this.radius; } set { this.radius = value; } } }Collections
The next section of the post contrasts the collection syntax ... the author argues in favor of Ruby on a number of points here, none of which I wholeheartedly agree on (I prefer to be explicit with collection choice and usage).
Initializer syntax is singled out for non-array types (fairly). This is another problem nicely solved in C# 3.0 by way of collection initializers. The following snippets both result in a strongly typed
intarray of 3 numbers, with the second array coming by way of a generic collection:new int[] { 1, 2, 3 }; new List<int> { 1, 2, 3 }.ToArray();The author next argues for a more complete collection of methods on various collections, including dynamic array growth. Some of this could be accomplished with further extension methods, but I personally feel that
PushandPopbelong onStack, notArray, so I'm not going to bother implementing those.In terms of dynamic array growth, that is handled quite nicely by collection classes which are optimized for that case (
List<T>et al). Array-like syntax is supported (as it has been since C# 1.0), but my personal preference is to call anAddmethod when I want to add something.File I/O and Regular Expressions
The last language-level feature the author contrasts is parsing sentences containing the work "Ruby" from a text file:
File.read('test.txt').scan(/.*?\. /).each { |s| puts s if s =~ /Ruby/ }This is compact, to be sure, but we don't need any C# 3.0 tricks to get close:
using (StreamReader reader = new StreamReader("test.txt")) { foreach (Match match in Regex.Matches(reader.ReadToEnd(), @"(.*?\.)\s+?")) { if (match.Value.Contains("Ruby")) { Console.WriteLine(match.Value); } } }Summary
The point of this post was not to imply that C# is better than Ruby, or Java for that matter. I simply wanted to point out that brevity and elegance are not the exclusive domain of dynamic languages. "Syntactic sugar" can go a long ways, without requiring fundamental changes to a language or its runtime. C# 3.0 is a particularly relevant example, given that it runs atop the 2.0 CLR despite all of its syntactic sugar.
If you're interested in toying around with the examples cited in the program listings above, I've attached the sample code to this post. To use it, install the LINQ CTP and run MSBuild from a command prompt.
Attachments
Comments
- Friday, January 05, 2007 12:34:10 AM by Paul Looijmans
- Monday, March 05, 2007 7:57:12 PM by Alex JamesAnother source of enlightenment is Derek Slager....
- Tuesday, March 06, 2007 3:31:23 PM by DanielThe equivalent of file.read() is File.ReadAllText(), no need to use a StreamReader.
- Friday, May 25, 2007 2:11:03 AM by HaackedVery cool! I'm sure these methods will go in many a developer's utilities class library.
- Tuesday, September 11, 2007 10:46:28 PM by JimVery nice article. I can see someone rolling a ton of methods like these up into a utility class library. It might be a good open-source project for codeplex once C# 3.0 gets out of beta.
- Friday, September 28, 2007 8:30:26 PM by Georges Benatti JrFor more expressiviness in the staticaly typed word, look at Boo, http://boo.codehaus.org
It supports extensions, regex with ruby sintax, a match operator ~=,closures, has [property] and [getter] atributes... and other features... all running on top of the CLR, so, compatible with C#, VB... - Tuesday, December 04, 2007 4:26:06 AM by portrait paintingThanks for sharing this to us. This is our lesson at school this week and our professor seems to be explaining stuff in a very complicated way. Perhaps I can share this to our class and see if my Professor has something to say.
- Sunday, March 16, 2008 10:26:09 PM by PeteRe. a feature of Ruby's syntax which I cannot emulate -- its tight property syntax:
class Circle
attr_accessor :center, :radius
end
C# 3.0 has automatic properties to do just this.
class Circle
{
public int Centre { get; set; }
public int Radius { get; set; }
} - Friday, March 21, 2008 11:26:15 PM by Ryan GaraygayVery insightful post. This should go into utilities class libraries indeed.
- Thursday, July 31, 2008 9:14:45 AM by ErikFile.ReadAllText("test.txt").
Scan(@"(.*?\.)").
Where(s => s.Contains("Ruby")).
Each(s => MessageBox.Show(s)); - Wednesday, August 20, 2008 1:19:04 PM by alejandro varela (rosario)smells like ruby spirit!
- Friday, May 22, 2009 5:48:41 PM by BrianI would like to add that int does infact inherit from System.Object in .NET. The inheritance tree, in reverse, is
System.Int32 -> System.ValueType -> System.Object
http://community.bartdesmet.net/blogs/bart/archive/2006/11/08/C_2300_-Automatic-Properties.aspx
I don't think it's in the current CTP yet though. Let's hope they include it in the January CTP that should be released any day now.