Logo




Subscribe:
RSS 2.0 | Atom 1.0
Categories:

Sign In


[Giagnocavo]Michael::Write()

# Monday, August 13, 2007
Practical Functional C# - Part II

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.

Code
Monday, August 13, 2007 11:07:26 PM UTC  #    Comments [8]  |  Trackback

Tuesday, August 14, 2007 5:30:55 AM UTC
Nice, code.
I suggest small improvement

TReturn UseService<TChannel, TReturn>(Func<TChannel, TReturn> code) where TChannel : IClientChannel
{
var chanFactory = GetCachedFactory<TChannel>();
TChannel channel = chanFactory.CreateChannel();
bool error = true;
try {
TReturn result = code(channel);
channel.Close();
error = false;
return result;
}
finally {
if (error) {
channel.Abort();
}
}
}
Tuesday, August 14, 2007 6:57:50 AM UTC
Hey, this is actually quite interesting. Thanks for sharing your knowledge.
Anonymous
Tuesday, August 14, 2007 8:50:25 AM UTC
I would do this the following way:
This way we don't have to use a bool variable.
Abort is only called when invoking code or Close fails.

In your example when CreateChannel fails, channel remains null and then we cannot call Abort (i.e. your example amy throw NullReferenceException)

TReturn UseService<TChannel, TReturn>(Func<TChannel, TReturn> code) where TChannel : IClientChannel
{
var chanFactory = GetCachedFactory<TChannel>();
TChannel channel = chanFactory.CreateChannel();
try {
TReturn result = code(channel);
channel.Close();
} catch (Exception) {
channel.Abort();
}
}
Rook
Tuesday, August 14, 2007 9:25:51 AM UTC
I'm a little confused with the type inferring. Shouldn't this:

---
int sum = UseService(delegate(ICalculator calc) {
return calc.Add(a, b);
});
---

Be something like this (in C# 2.0):

---
int sum = UseService<ICalculator, int>(delegate(ICalculator calc) {
return calc.Add(a, b);
});
---

I tried cooking up a sample like the first one to try it out in C# 2.0, but I get "error CS0411: The type arguments for method `UseService' cannot be inferred from the usage". Maybe I did something wrong, but still, could somebody explain this a little bit?

Also, would this mean that in C# 3.0 something like the following would work?

---
int a = 1;
int b = 2;
var pair = new KeyValuePair(a, b);
// pair becomes of type KeyValuePair<int, int> ?
Eric
Tuesday, August 14, 2007 2:14:56 PM UTC
@akosows: Actually, the interfaces are not constrained to IClientChannel. The interfaces can be anything to be used with WCF (any interface).

@Rook: Are you sure that CreateChannel returns null? I'm pretty sure it just throws an exception if there's a problem.

@Eric: You're right on the C# 2.0 type inference; it got enhanced in 3.0. I fixed the example.

It'd be nice if the C# example you gave would work, but unfortunately type inference doesn't happen on constructors. You would have to wrap the constructor with a simple generic method and then it'd work:

public static KeyValuePair<K, V> Create<K, V>(K key, V val)
{ return new KeyValuePair<K, V>(key, val); }

Tuesday, August 14, 2007 4:08:41 PM UTC
My mistake.
Somehow I have read that
TChannel channel = chanFactory.CreateChannel();
was inside try block. Have to get stronger glasses I suppose :)

Nonetheless I think using catch rather than finally is more appropriate here.
Rook
Sunday, September 23, 2007 9:05:41 AM UTC
If the interfaces aren't constrained to IClientChannel, isn't there a problem with your typecast?
Sunday, September 23, 2007 3:18:19 PM UTC
No, because objects created from the ChannelFactory will have those interfaces. Meanwhile, WCF interfaces don't implement any other interfaces usually.
OpenID
Please login with either your OpenID above, or your details below.
Name
E-mail
Home page

Comment (HTML not allowed)  

Enter the code shown (prevents robots):

Live Comment Preview