Building Domain Specific Languages in C#, Part 2

Building Domain Specific Languages in C#, Part 2

he first installment of this article showed how to leverage C# syntax to create a specific type of domain specific language in C# called a “fluent interface.” Building a fluent interface involves converting a complex API into something with a fighting chance of readability. This follow-up article shows you how to take advantage of some of C#’s cool new features to push the envelope on this style of coding.

Using Extension Methods for Additional Fluency

The previous article described a simple way for a .NET bakery to keep track of discount programs for their customers. Using the same “bakery” theme, you can create a fluent interface to allow the baker to keep track of recipes. Here’s an example of the type of unit test needed to succeed:

   var expected =       new SimpleIngredient("Flour");   expected.Quantity = 42;   var actual = 42.grams().of("Flour");   Assert.AreEqual(     expected.Name, actual.Name);   Assert.AreEqual(     expected.Quantity, actual.Quantity);

The interesting code here is the line that includes:

   42.grams().of("Flour");

Wow, that’s really fluent. But how can it possibly compile and run? To find out, you need to take a little diversion to extension methods.

Simple Extension Methods

One new feature in C# 3.5 is the extension method, added to allow LINQ to do its magic. An extension method lets you create a class that extends the functionality of an existing class without subclassing. The extension methods appear as class methods. Here’s a simple example. Suppose you’d really like to have a WordCount method on the String class?of course, String is sealed, so you can’t subclass it to include custom methods. Fortunately, extension methods let you create a new method that appears directly on the String class.

   public static class StringExtension   {      public static int WordCount(         this String str)      {         return str.Split(new char[]             { ' ', '.', '?' },          StringSplitOptions.RemoveEmptyEntries)           .Length;      }   }
Eric Evans and Martin Fowler coined the term “fluent interface,” and Martin popularized it in his blog in December of 2005. Since then, it has become the accepted term for a certain type of DSL.

Extension methods must appear in a static class definition, and the methods themselves must have static access. The extension method must take at least one parameter?the type of class extended. Notice in the WordCount method, the first (and, in this case, only) parameter is of type String. To enable the extension method, you need merely place a using directive at the top of the file that includes your extension classes. In this case, any class that includes using StringExtension at the top of the file may call the WordCount method on any string.

Extension methods exist because LINQ needs them to perform its syntactic sugar magic. But, of course, when new features appear, developers can use them for whatever purpose arises, such as recipes.

Recipe Extensions

Extension methods work for all classes, both built-in library classes and your own classes. Domain specific language fluent interfaces frequently need quantities of things: “2 weeks,” “5 pounds,” “12 months,” “6.2 miles.” Therefore, to create truly fluent interfaces, you need to be able to use numbers to specify quantities (such as 42.grams()). For such constructs to work, you must add a new method to the int “class.” But, of course, you can’t: int is one of the built-in system types that represent primitives. However, auto-boxing works just fine for constant primitives (such as 42). That means that you can add an extension method to the Int32 class?which the compiler will use when it auto-boxes the primitive 42 into a class. The following code shows the extension class specifying the gram method:

   public static class RecipeExtensions   {             public static int Gram(         this int weight)      {         return weight;      }   }

As required, RecipeExtensions is a static class, which includes a static method called gram. The gram method accepts a single parameter (of type int, which is auto-boxed up to an Int32) and simply returns the value. Why return just weight? For the recipe DSL, all weights are expressed in grams. Because this is a gram method, it simply returns the number of grams. Later you’ll see how to create a pound method that does some conversions.

This next snippet shows the unit test demonstrating the new extension method:

   [Test]   public void gram_extension_for_integer()   {      Assert.AreEqual(3, 3.gram());      Assert.AreNotEqual(4, 3.gram());   }

C# handles the syntactic sugar that lets you call a new extension method on a constant integer, creating a fluent interface that uses actual constants for quantities.

Editor’s Note: This article was first published in the January/February 2009 issue of CoDe Magazine, and is reprinted here by permission.

Writing a Simple Ingredient

Now that the quantity extension method exists, you can extend this to handle the other methods required by the recipe fluent interface. Readability is one of the goals of a fluent interface; it sounds clumsy to say 4.gram(), instead of the more natural 4.grams().

To add a plural grams method, simply create another extension method for int:

   public static int grams(      this int weight)   {      return weight;   }

Gram isn’t the only unit of weight you need to support. To add pound (and pounds) extension methods, you need to be able to convert from pounds to grams (because all internal weights appear in grams). To that end, the extension class shown below adds a constant and a couple of new methods:

   private const Double GRAMS_IN_POUND = 453.59237;   public static Double pound(this int weight)   {      return GRAMS_IN_POUND * weight;   }   public static Double pounds(this int weight)   {      return GRAMS_IN_POUND * weight;   }   public static Double lb(this int weight)   {      return GRAMS_IN_POUND * weight;   }   public static Double lbs(this int weight)   {      return GRAMS_IN_POUND * weight;   }

To create the most fluent interface possible, you can create methods for pound, pounds, lb, and lbs. You’ll find a pattern occurs frequently when writing fluent interfaces: Much more work for the developer of the fluent interface means a richer experience for the interface consumer. In this way, building fluent interfaces is exactly like building good APIs. The code above contains a lot of duplication, which you can clean up using well-known refactoring techniques that I won’t go into here because doing so wouldn’t add anything to the DSL discussion.

Defining grams and pounds covered the weight units. Now, you can tackle the of part of the DSL, for example:

   42.grams().of("Flour");

This extension method ultimately creates an Ingredient. Here’s the code for the simple Ingredient class:

   public class SimpleIngredient   {      private String _name;      private int _quantity;      public SimpleIngredient(String name)      {         _name = name;      }      public int Quantity      {         get         {            return _quantity;         }         set         {            _quantity = value;         }      }      public String Name      {         get         {            return _name;         }      }   }

To add the of method, you can add another extension method, shown below:

   public static SimpleIngredient of(      this int quantity, String name)   {      var i = new SimpleIngredient(name);      i.Quantity = quantity;      return i;   }

This method extends the int class (or the boxed up equivalent). It takes an additional parameter indicating the ingredient name, creates a new SimpleIngredient, sets its quantity, and returns the new ingredient. After you add this code the following test passes successfully:

   [Test]   public void integer_version_of_OF()   {      var expected = new SimpleIngredient("Flour");      expected.Quantity = 42;      var actual = 42.grams().of("Flour");      Assert.AreEqual(expected.Name, actual.Name);      Assert.AreEqual(expected.Quantity, actual.Quantity);   }

Note that the line that defines the actual result is exactly the target syntax shown at the beginning. You now have a fluent interface for recipes.

Fluent Types

However, nothing is ever quite so simple. It turns out that the above code works great for grams, but not for pounds. Why one but not the other? This illustrates a common problem when writing fluent interfaces, so common in fact that I’ve given it a name: the “Type Transmogrification Problem.” The reason pounds doesn’t work derives from the definition of the of extension method, specifically, the of method extends int, but when you call the pounds method, you’re no longer extending an int, because the pounds method returns a floating point number. Fluent interfaces use quantities frequently, meaning that this problem rears its head often. Worse, in .NET, integers and floating point numbers don’t share a common ancestor, so you can’t add it at the top of the hierarchy. Although Int32 and Double do have Object in common, it would be bad form to add the gram and pounds methods to every single class!

Within C# (because it’s strongly typed), you have to do a little bit of behind the scenes magic. To that end, I’m changing SimpleIngredient to the more general Ingredient class, shown in Listing 1.

The Ingredient class now adds a flag to help determine the appropriate return type. The Quantity property now returns an Object, meaning that the code that consumes this fluent interface may have to take that into account. You can lean heavily on autoboxing to handle most of these cases. Also, I’ve added code to both the get and set portions of the Quantity property to handle the type information correctly. Obviously, this doesn’t scale well for applications that consume many types, but in this case, the application needs to support only two types, so the code isn’t too ugly. Mostly, I’m fighting with C#’s strong typing, which interferes with the ability to create a really simple fluent interface. However, after this change, the type transmogrification problem is fixed, as shown in the (now successful) test below:

   [Test]   public void double_version_of_OF()   {      var expected =          new Ingredient("Test");      expected.Quantity =          3 * GRAMS_IN_POUND;      var  actual =          3.pounds().of("Test");      Assert.AreEqual(expected.Name, actual.Name);      Assert.AreEqual(expected.Quantity, actual.Quantity);      Assert.AreEqual(actual.Quantity.GetType(),          typeof(Double));   }

Fluency in Dynamic Languages

Building fluent interfaces becomes easier when you move to more dynamic languages. While type safety helps the compiler and tools like Visual Studio work, it interferes with building really flexible APIs. To that end, this section shows the same examples in a really dynamic language on the CLR: IronRuby. Ruby is often used to build DSLs because its syntax is so flexible and because it has powerful features that go beyond C#, such as true open classes.

IronRuby

In Ruby (and IronRuby), you don’t have to use extension methods to add new behaviors to classes (either the built-in ones or your own). You can reopen the class and add new behaviors directly. For example, this snippet shows the Ruby code that adds the weight units gram and pound to all numbers (encompassing both integers and floating point numbers):

Ruby is older than Java! Ruby was developed in Japan by Yukihiro “matz” Matsumoto and released in 1995. It has slowly grown in stature, mostly because of the Ruby on Rails web framework.
   class Numeric      def gram         self      end      alias_method :grams, :gram      def pound         self * 453.59237      end      alias_method :pounds, :pound      alias_method :lb, :pound      alias_method :lbs, :pound   end

In Ruby, the Numeric class is the superclass of all numbers, meaning that you can add the new methods to a single location. In Ruby, defining a class for a class that’s already in the class path reopens the class for modifications?meaning you can add new behavior to classes without the formality of special types of classes and methods required in .NET. Ruby also includes the powerful alias_method mechanism, allowing you to easily create additional names for new methods.

The of method is similarly simple in Ruby, as shown here:

   class Numeric      def of(name)         ingredient = Ingredient.new(name)         ingredient.quantity = self         return ingredient      end   end

Here, the of method doesn’t have to worry about types because Ruby is entirely dynamically typed. To see this fluent interface in action, the following code shows an appropriate unit test:

   def test_ingredient      expected = Ingredient.new("Test")      expected.quantity = 42      actual = 42.grams.of "Test"      assert_equal(expected.name, actual.name)      assert_equal(expected.quantity, actual.quantity)   end

Ruby’s flexible syntax (note that parenthesis are optional, adding to the fluency of the interface), true open classes, and dynamic typing make it a good choice for this style of DSL. All these examples run in the current version of IronRuby, so now is a good time to check it out.

Comparing Open Classes to Extension Methods

Let me make one last important distinction between the C# and Ruby examples. C# uses extension methods to add methods to the built-in types, while Ruby uses open classes. So what is the difference?

Extension methods add capabilities to existing classes. When resolving a method call, the CLR first checks to see if the method exists on the original class. If it doesn’t, it checks to see if there is an extension method with the appropriate signature. If it finds one, it calls that method, passing the object instance as the first parameter.

Open classes are a much more powerful mechanism. When you reopen a class, you can do whatever you like: add new methods, override existing methods, and even delete methods programmatically. Using extension methods, you can only add new methods. So, open classes give you full access to the class definition, allowing you to make more far-reaching changes. Of course, with any powerful mechanism, you must choose when to wield that power. With Ruby, like any dynamic language, testing isn’t optional!

The changes made to C# to make LINQ possible make it easy to build your own fluent interfaces. One of the goals of a fluent interface is readability. The ability to change core classes (such as the numeric classes) make the code much more readable because you can use the numeric argument as the entry point to the expression, rather than send it as a parameter. Why does that make a difference? Supporting syntax that’s similar to the way people think makes it easier for non-developers to read code?because it looks similar to the way they would write or say it. Readability matters; leveraging that is one of the goals of DSLs (and fluent interfaces).

devx-admin

devx-admin

Share the Post:
5G Innovations

GPU-Accelerated 5G in Japan

NTT DOCOMO, a global telecommunications giant, is set to break new ground in the industry as it prepares to launch a GPU-accelerated 5G network in

AI Ethics

AI Journalism: Balancing Integrity and Innovation

An op-ed, produced using Microsoft’s Bing Chat AI software, recently appeared in the St. Louis Post-Dispatch, discussing the potential concerns surrounding the employment of artificial

Savings Extravaganza

Big Deal Days Extravaganza

The highly awaited Big Deal Days event for October 2023 is nearly here, scheduled for the 10th and 11th. Similar to the previous year, this

5G Innovations

GPU-Accelerated 5G in Japan

NTT DOCOMO, a global telecommunications giant, is set to break new ground in the industry as it prepares to launch a GPU-accelerated 5G network in Japan. This innovative approach will

AI Ethics

AI Journalism: Balancing Integrity and Innovation

An op-ed, produced using Microsoft’s Bing Chat AI software, recently appeared in the St. Louis Post-Dispatch, discussing the potential concerns surrounding the employment of artificial intelligence (AI) in journalism. These

Savings Extravaganza

Big Deal Days Extravaganza

The highly awaited Big Deal Days event for October 2023 is nearly here, scheduled for the 10th and 11th. Similar to the previous year, this autumn sale has already created

Cisco Splunk Deal

Cisco Splunk Deal Sparks Tech Acquisition Frenzy

Cisco’s recent massive purchase of Splunk, an AI-powered cybersecurity firm, for $28 billion signals a potential boost in tech deals after a year of subdued mergers and acquisitions in the

Iran Drone Expansion

Iran’s Jet-Propelled Drone Reshapes Power Balance

Iran has recently unveiled a jet-propelled variant of its Shahed series drone, marking a significant advancement in the nation’s drone technology. The new drone is poised to reshape the regional

Solar Geoengineering

Did the Overshoot Commission Shoot Down Geoengineering?

The Overshoot Commission has recently released a comprehensive report that discusses the controversial topic of Solar Geoengineering, also known as Solar Radiation Modification (SRM). The Commission’s primary objective is to

Remote Learning

Revolutionizing Remote Learning for Success

School districts are preparing to reveal a substantial technological upgrade designed to significantly improve remote learning experiences for both educators and students amid the ongoing pandemic. This major investment, which

Revolutionary SABERS Transforming

SABERS Batteries Transforming Industries

Scientists John Connell and Yi Lin from NASA’s Solid-state Architecture Batteries for Enhanced Rechargeability and Safety (SABERS) project are working on experimental solid-state battery packs that could dramatically change the

Build a Website

How Much Does It Cost to Build a Website?

Are you wondering how much it costs to build a website? The approximated cost is based on several factors, including which add-ons and platforms you choose. For example, a self-hosted

Battery Investments

Battery Startups Attract Billion-Dollar Investments

In recent times, battery startups have experienced a significant boost in investments, with three businesses obtaining over $1 billion in funding within the last month. French company Verkor amassed $2.1

Copilot Revolution

Microsoft Copilot: A Suit of AI Features

Microsoft’s latest offering, Microsoft Copilot, aims to revolutionize the way we interact with technology. By integrating various AI capabilities, this all-in-one tool provides users with an improved experience that not

AI Girlfriend Craze

AI Girlfriend Craze Threatens Relationships

The surge in virtual AI girlfriends’ popularity is playing a role in the escalating issue of loneliness among young males, and this could have serious repercussions for America’s future. A

AIOps Innovations

Senser is Changing AIOps

Senser, an AIOps platform based in Tel Aviv, has introduced its groundbreaking AI-powered observability solution to support developers and operations teams in promptly pinpointing the root causes of service disruptions

Bebop Charging Stations

Check Out The New Bebob Battery Charging Stations

Bebob has introduced new 4- and 8-channel battery charging stations primarily aimed at rental companies, providing a convenient solution for clients with a large quantity of batteries. These wall-mountable and

Malyasian Networks

Malaysia’s Dual 5G Network Growth

On Wednesday, Malaysia’s Prime Minister Anwar Ibrahim announced the country’s plan to implement a dual 5G network strategy. This move is designed to achieve a more equitable incorporation of both

Advanced Drones Race

Pentagon’s Bold Race for Advanced Drones

The Pentagon has recently unveiled its ambitious strategy to acquire thousands of sophisticated drones within the next two years. This decision comes in response to Russia’s rapid utilization of airborne

Important Updates

You Need to See the New Microsoft Updates

Microsoft has recently announced a series of new features and updates across their applications, including Outlook, Microsoft Teams, and SharePoint. These new developments are centered around improving user experience, streamlining

Price Wars

Inside Hyundai and Kia’s Price Wars

South Korean automakers Hyundai and Kia are cutting the prices on a number of their electric vehicles (EVs) in response to growing price competition within the South Korean market. Many

Solar Frenzy Surprises

Solar Subsidy in Germany Causes Frenzy

In a shocking turn of events, the German national KfW bank was forced to discontinue its home solar power subsidy program for charging electric vehicles (EVs) after just one day,

Electric Spare

Electric Cars Ditch Spare Tires for Efficiency

Ira Newlander from West Los Angeles is thinking about trading in his old Ford Explorer for a contemporary hybrid or electric vehicle. However, he has observed that the majority of

Solar Geoengineering Impacts

Unraveling Solar Geoengineering’s Hidden Impacts

As we continue to face the repercussions of climate change, scientists and experts seek innovative ways to mitigate its impacts. Solar geoengineering (SG), a technique involving the distribution of aerosols

Razer Discount

Unbelievable Razer Blade 17 Discount

On September 24, 2023, it was reported that Razer, a popular brand in the premium gaming laptop industry, is offering an exceptional deal on their Razer Blade 17 model. Typically

Innovation Ignition

New Fintech Innovation Ignites Change

The fintech sector continues to attract substantial interest, as demonstrated by a dedicated fintech stage at a recent event featuring panel discussions and informal conversations with industry professionals. The gathering,

Import Easing

Easing Import Rules for Big Tech

India has chosen to ease its proposed restrictions on imports of laptops, tablets, and other IT hardware, allowing manufacturers like Apple Inc., HP Inc., and Dell Technologies Inc. more time