Previous article: Practical Functional C# - Part I
Last time I demonstrated how to replicate the using keyword as a function that takes a function. In this article, I'm going to show some real-world cases where you'll see a major improvement by taking a more functional approach. I'm going to use WCF as an example space.
WCF allows us to define services as normal C# interfaces. We then use a factory to create a proxy for an interface, specifying the URI, binding types (HTTP, binary, message queue), and other options. However, our interest is how this actually looks from the client side; how we actually make calls.
Since a WCF call is actually invoking code on another machine, any number of bad things can happen to our client channel, resulting in exceptions. When this happens, we have to Abort the channel. But if things complete successfully, we just need to Close the channel. Of course, that's not easy enough: if the Close fails, we then need to Abort the channel anyways. Confused? Well, check out this code snippet that uses an interface called ICalculator to add two numbers:
void WcfExample(){ int a = 1; int b = 2; int sum; var chanFactory = GetCachedFactory<ICalculator>(); ICalculator calc = chanFactory.CreateChannel(); bool error = true; try { sum = calc.Add(a, b); ((IClientChannel)calc).Close(); error = false; } finally { if (error) { ((IClientChannel)calc).Abort(); } } Console.WriteLine(sum); }
Ouch! Out of 17 lines of code, only five relate to our problem. The rest (70%) is pure, ugly overhead. This is a much more complicated pattern than the using pattern, and from experience, I can tell you it is error-prone. Fortunately, this pattern decomposes nicely. The only two unique things are the name of the interface and the statement that acts on the interface. Here is one way you might go about writing this generically:
TReturn UseService<TChannel, TReturn>(Func<TChannel, TReturn> code){ var chanFactory = GetCachedFactory<TChannel>(); TChannel channel = chanFactory.CreateChannel(); bool error = true; try { TReturn result = code(channel); ((IClientChannel)channel).Close(); error = false; return result; } finally { if (error) { ((IClientChannel)channel).Abort(); } }}
This is exactly like the previous code, but we've substituted TChannel and a function parameter named code instead of our actual types. The type of function we want is Func<TChannel, TReturn>. You can think of this as saying: "transforms a TChannel into a TReturn". Now, look at the beauty this allows on the client side:
void WcfExample2(){ int a = 1; int b = 2; int sum = UseService((ICalculator calc) => calc.Add(a, b)); Console.WriteLine(sum); }
Presto! Our overhead went from 12 lines to zero. If that didn't sell you on the power of functions, I suggest finding another career. Now, there is a lambda there, but all it is saying is "Here is a function that takes one parameter of type ICalculator and returns the value of Add(a, b)." The C# compiler automatically infers the return type (int).
Some of you mentioned still using C# 2.0. Two things: First, you can use the 3.0 compiler and run on 2.0 because these examples don't reference any new assemblies. Second, you can achieve the same thing using anonymous methods, but they just look a bit uglier:
void WcfExample3(){ // C# 2.0 example with anonymous methods int a = 1; int b = 2; int sum = UseService<ICalculator, int>(delegate(ICalculator calc) { return calc.Add(a, b); }); Console.WriteLine(sum);}
By this time, I hope you are thinking about places in your code where you can extract a larger pattern and use functions instead. The key concept to understand here is that your source code should reflect your solution. You should not have extra code sitting around just for the sake of appeasing the platform or meeting some "design pattern". Let the platform and libraries take care of the details, and let your code focus on actually solving problems. As always, I welcome your comments, suggestions, and insults, so please let me know what you think!
Next up, we'll take a look at loops, see why they are harmful, and then go about fixing them.
Remember Me