I love going off and exploring new languages, particularly when it allows (forces) me to think differently about the kind of stuff I generally take for granted. Take Ruby for instance. In Ruby you can do this
10.times do |i|
puts "The square of #{i} is #{i * i}"
end
Which outputs
The square of 0 is 0
The square of 1 is 1
The square of 2 is 4
The square of 3 is 9
The square of 4 is 16
The square of 5 is 25
The square of 6 is 36
The square of 7 is 49
The square of 8 is 64
The square of 9 is 81
This is cool - the iteration construct reads like a sentence (perhaps not uttered in the speaker's first language, but a sentence all the same). The do ... end
bit is called a block and it's a bit of code you can pass to a function to be executed at that function's discretion; the i
between the pipe symbols is the parameter list for the block and the funky #{ ... }
construct allows string interpolation - the evaluation of expressions inside a string literal.
Now I'm not going to get carried away here - the string interpolation can probably wait for a later post - but I can see value in being able to pass a lump of code, in a very readable way, to another function. The trite example above doesn't really do justice to the power this construct offers; features such as logging, timing, thread synchronisation, iteration, error protection, deterministic cleanup, inversion of control etc. are all candidates for this sort of code injection - with heavy emphasis on readability.
Now, I say to myself, modern C# supports lambdas and extension methods mean that you can simulate Ruby's ability to re-open and add to a class definition, I can do that; and this is what I come up with
public static class Extensions
{
public static void Times(this int number, Action<int> action)
{
for (var i = 0; i < number; ++i)
action(i);
}
}
Which is used like this
10.Times(
i => Console.WriteLine("The square of {0} is {1}", i, i * i)
);
That's not bad, I think, it's pretty terse, but I can almost hear my Ruby friends tut as they catch sight of the lone punctuation on the last line. I can do better, but I'll need a new class.
public sealed class Block<T>
{
private readonly Action<Action<T>> wrapper;
public Block(Action<Action<T>> wrapper)
{
if (wrapper == null)
throw new ArgumentNullException("wrapper");
this.wrapper = wrapper;
}
public Action<T> Do { set { wrapper(value); } }
}
public static class Extensions
{
public static Block<int> Times(this int number)
{
return new Block<int>(action =>
{
for (var i = 0; i < number; ++i) action(i);
});
}
}
Having set this up I can now call it like this
10.Times().Do = i =>
Console.WriteLine("The square of {0} is {1}", i, i * i);
I like this. Assigning the lambda to the write-only property has the effect of passing it to whatever wrapper the Block was initialised with. The Block class is a reusable little nugget of orchestration that's totally decoupled from whatever logic you choose to use it with, but the best part is it's quite declarative at the call site; and if you squint a bit you don't even see the assignment and lambda operators :)
Ok, but this seems pretty much geared to enumeration/iteration, what about a more generalised code injection scenario?
public sealed class Block
{
private readonly Action wrapper;
public Block(Action wrapper)
{
if (wrapper == null)
throw new ArgumentNullException("wrapper");
this.wrapper = wrapper;
}
public Action Do { set { wrapper(value); } }
}
public static class Wrap
{
public static Block With(Action wrapper)
{
return new Block(wrapper);
}
}
It's the same class! Well, nearly - it's the Block equivalent to Action<T>'s non-generic counterpart (I'd love to find a way to coalesce the two). The Wrap class here doesn't really add anything other than making the call site a bit more sentencey, as shown below
var withLogging = Wrap.With(action =>
{
Console.Write("1. Before\n2. During: ");
try
{
action();
Console.WriteLine("3. After");
}
catch (Exception ex)
{
Console.WriteLine("3. Exception: {0}", ex.Message);
}
finally
{
Console.WriteLine("4. Cleanup");
}
});
With that variable in scope I can do this
withLogging.Do = () => Console.WriteLine("This is going ok.");
Console.WriteLine();
withLogging.Do = () =>
{
Console.WriteLine("This might not go so well...");
throw new Exception("KERBLAM!");
};
With the following output
1. Before
2. During: This is going ok.
3. After
4. Cleanup
1. Before
2. During: This might not go so well...
3. Exception: KERBLAM!
4. Cleanup
All in all I'm pretty pleased with this. The idea could also be extended to functions returning values by means of the Func<T> family of delegates.
Afterthought: is this an instantiation of a named pattern?
No comments:
Post a Comment