Essence and ceremony, Ruby and C#

In a recent post by Rob Conery there was an interesting comment exchange between Scott Koon and Ely Lucas. Scott suggested writing some little command line apps in Ruby or Python to do things like move files around and compare it to C#, and Ely replied:

Ruby:
FileUtils.copy('myfile.txt', 'myfile2.txt')

c#
File.Copy("myfile.txt", "myfile2.txt")

Now I can see it. The Ruby way is so much better!

This raises a few interesting points. First, a usable API is possible in most languages and platforms, which is encouraging for those of us that work primarily in C# and .NET. Just because we work in a static language doesn’t mean we should settle for verbose, unintuitive APIs.

The second point, and the topic of this post, is the idea of essence versus ceremony. Both lines of code above clearly show the essence of what we are trying to achieve: create a copy of myfile.txt in a file called myfile2.txt. Of course these two lines of code do not quite tell the entire story; there is a certain amount of ceremony we need to go through to get them to run. Let’s see how many incantations we need to mutter to get these things to work.

C# is for ceremony?

Until C# 5.0 and compiler-as-a-service (or a few weeks ago for Mono users ;)), we need to do some work to get our C# line of code to satisfy the compiler. This means wrapping the line in a class with a static Main entry point that will be called when we compile it into an EXE.

using System.IO;
public class FileCopy {
  public static void Main() {
    File.Copy("sample\\src\\1.txt", "sample\\dest\\1.txt");
  }
}

Then from a shell (or bat/PS script) we call the compiler, and run the resulting EXE:

> C:\Windows\Microsoft.NET\Framework\v3.5\csc.exe FileCopy.cs
> FileCopy.exe

So our single line of code takes around 6 ceremonial lines/operations (plus some brace noise) to make work. We need to reference the IO library to access the API we need; we need a class declaration that for this code is completely useless, and we need to utter ye olde "public static void Main(…)" incantation before we get to the essence of our program. We’ve got access modifiers and a plethora of punctuation. We then need to compile and run the resulting EXE. Of these, only the required library and executing the program bears any relation to the problem at hand.

Note: Yes, I could ditch the using statement, but I want to keep the original lines of code intact, and I want to show referencing the library as a separate logical operation. I’ll give Ruby the same treatment.

Expressing essence with Ruby

As Ruby is interpretted it has a pretty unfair advantage here. We just need to tell Ruby where to find the library we need for our single line of code, then execute it using the Ruby interpretter.

require 'FileUtils'
FileUtils.copy 'sample/src/1.txt', 'sample/dest/'
> ruby file_copy.rb

Here pretty much everything relates to the essence of our code. True, this is a silly little example, but it does show that the two single lines of code posted in the original comment don’t tell the whole story, and that the single line of Ruby code takes less work to get going than the equivalent C#.

But wait! There’s more…

How many times do you actually compile .NET code in anything other than an IDE? I used Vim to write both the C# and Ruby, but let’s pretend for a moment I’m normal rather than the insane, rambling lunatic I actually am and code this in Visual Studio. :)

//FileCopy.sln

Microsoft Visual Studio Solution File, Format Version 11.00
# Visual Studio 2010
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileCopy", "FileCopy.csproj", "{4277DB77-71AF-4BFE-AC57-E44A899DE9A4}"
EndProject
Global
 GlobalSection(SolutionConfigurationPlatforms) = preSolution
  Debug|x86 = Debug|x86
  Release|x86 = Release|x86
 EndGlobalSection
 GlobalSection(ProjectConfigurationPlatforms) = postSolution
  {4277DB77-71AF-4BFE-AC57-E44A899DE9A4}.Debug|x86.ActiveCfg = Debug|x86
  {4277DB77-71AF-4BFE-AC57-E44A899DE9A4}.Debug|x86.Build.0 = Debug|x86
  {4277DB77-71AF-4BFE-AC57-E44A899DE9A4}.Release|x86.ActiveCfg = Release|x86
  {4277DB77-71AF-4BFE-AC57-E44A899DE9A4}.Release|x86.Build.0 = Release|x86
 EndGlobalSection
 GlobalSection(SolutionProperties) = preSolution
  HideSolutionNode = FALSE
 EndGlobalSection
EndGlobal

//FileCopy.csproj
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
    <Platform Condition=" '$(Platform)' == '' ">x86</Platform>
    <ProductVersion>8.0.30703</ProductVersion>
    <SchemaVersion>2.0</SchemaVersion>
    <ProjectGuid>{4277DB77-71AF-4BFE-AC57-E44A899DE9A4}</ProjectGuid>
    <OutputType>Exe</OutputType>
    <AppDesignerFolder>Properties</AppDesignerFolder>
    <RootNamespace>FileCopy</RootNamespace>
    <AssemblyName>FileCopy</AssemblyName>
    <TargetFrameworkVersion>v4.0</TargetFrameworkVersion>
    <TargetFrameworkProfile>Client</TargetFrameworkProfile>
    <FileAlignment>512</FileAlignment>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|x86' ">
    <PlatformTarget>x86</PlatformTarget>
    <DebugSymbols>true</DebugSymbols>
    <DebugType>full</DebugType>
    <Optimize>false</Optimize>
    <OutputPath>bin\Debug\</OutputPath>
    <DefineConstants>DEBUG;TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x86' ">
    <PlatformTarget>x86</PlatformTarget>
    <DebugType>pdbonly</DebugType>
    <Optimize>true</Optimize>
    <OutputPath>bin\Release\</OutputPath>
    <DefineConstants>TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
  </PropertyGroup>
  <ItemGroup>
    <Reference Include="System" />
    <Reference Include="System.Core" />
    <Reference Include="System.Xml.Linq" />
    <Reference Include="System.Data.DataSetExtensions" />
    <Reference Include="Microsoft.CSharp" />
    <Reference Include="System.Data" />
    <Reference Include="System.Xml" />
  </ItemGroup>
  <ItemGroup>
    <Compile Include="Program.cs" />
    <Compile Include="Properties\AssemblyInfo.cs" />
  </ItemGroup>
  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
  <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 
       Other similar extension points exist, see Microsoft.Common.targets.
  <Target Name="BeforeBuild">
  </Target>
  <Target Name="AfterBuild">
  </Target>
  -->
</Project>

//Program.cs
using System.IO;

namespace FileCopy {
    class Program {
        static void Main(string[] args) {
            File.Copy("Sample\\src\\1.txt", "Sample\\dest\\1.txt");
        }
    }
}

//Properties/AssemblyInfo.cs
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

// General Information about an assembly is controlled through the following 
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("FileCopy")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("FileCopy")]
[assembly: AssemblyCopyright("Copyright -®  2010")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]

// Setting ComVisible to false makes the types in this assembly not visible 
// to COM components.  If you need to access a type in this assembly from 
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]

// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("4c8aeb8f-6700-4ba5-96cc-30cdcc61e44a")]

// Version information for an assembly consists of the following four values:
//
//      Major Version
//      Minor Version 
//      Build Number
//      Revision
//
// You can specify all the values or you can default the Build and Revision Numbers 
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

That’s over 120 lines across 4 files and a couple of directories. True, there are many great advantages to using VS, but lack of ceremony isn’t one of them. ;)

But this is a ridiculous example!

Yes, I agree! So why bother pointing out obvious differences between compiled and interpreted languages? Because it’s important to understand the difference between essence and ceremony so we can emphasise the essence of our code as much as possible, and it’s hard to find a simpler, more obvious example than this. Once looking for it, it becomes very obvious that satisfying the compiler isn’t the only form of ceremony C#/VB.NET devs (et al.) go through.

Consider this: with Ruby (and many other languages), you don’t really need an IoC container. You don’t really need factories. Explicit interface files for extensibility? Don’t need them for a dynamic language; you’re programming to an implicit interface. And say goodbye to code noise like public / static / virtual / abstract / override etc. Ruby doesn’t want any of that mumbo-jumbo thanks very much! Then there’s Ruby’s meta-programming which lends itself to coding via conventions, and the loose syntax which makes some very expressive, internal DSLs possible. (Have a look at Design Patterns in Ruby for some great examples of how common design patterns can become much simpler with Ruby.)

Take away some of the ceremony that is absolutely vital to effective C# development, and you start to appreciate why so many devs get excited by things like Ruby and Rails.

None of this means C# is bad. It’s not. It’s an awesome language. Static typing provides a number of advantages, as does compilation. But it is very important to consider the ceremony you need to go through to get your job done. It is definitely worth looking at other languages and platforms like Ruby, Python, Rails and Django to help find ways of reducing the amount of ceremony you need for your C# code, but also to help you pick the right tool for the right job.

Comments