We need better interoperability between dynamic and statically compiled C#

It’s 2013 now, .NET DLR has been out for more than three years, and there are plenty of great libraries around with clever mix of statically compiled and dynamically evaluated code. I am guilty of one myself.

Unfortunately ‘dynamic’ keyword doesn’t only bring joy to the art of programming. Its power often results in unintended behavior that can usually be explained by digging deep into language implementation, but still looking unreasonable.

Look at the interface below. its first method returns a reference to the interface itself, and the second method returns a result, so both methods can be chained:

public interface IFluentClient
{
    IFluentClient SaveText(string text);
    string GetText();
}

Now in order to be able to use this interface we need a class that implements it. Here it is:

class FluentClient : IFluentClient
{
    private string _text;

    public IFluentClient SaveText(string text)
    {
        _text = text;
        return this;
    }

    public string GetText()
    {
        return _text;
    }
}

This class is not public and we can’t instantiate it in a different assembly, so we will create a factory for it:

public class ClientFactory
{
    public IFluentClient GetFluentClient()
    {
        return new FluentClient();
    }
}

Finally we can write a test for the code above:

[TestMethod]
public void SaveAndGetText()
{
    var client = new ClientFactory().GetFluentClient();
    var text = client.SaveText("a").GetText();
    Assert.AreEqual("a", text);
}

So far so good. The test passes of course. Now we will send a dynamic object to SaveText:

[TestMethod]
public void SaveAndGetText()
{
    dynamic str = "a";
    var client = new ClientFactory().GetFluentClient();
    var text = client.SaveText(str).GetText();
    Assert.AreEqual("a", text);
}

Let’s run the test and it will fail with the following error:

Microsoft.CSharp.RuntimeBinder.RuntimeBinderException: 'object' does not contain a definition for 'GetText'

Out of curiosity I tested this code on Mono, and it failed too, with a stranger error:

FluentClient.GetText() is inaccessible due to its protection level

Obviously a string casted to a dynamic object confused the runtime. But why? Eric Lippert in a blog post explained some of the reasons why the DLR-to-CLR interoperability was implemented in a way that makes the runtime treat statically compiled code as dynamic after a dynamic object enters the scene. He defends the decision by stating that “if you and I and the compiler know that overload resolution is going to choose a particular method then why are we making a dynamic call in the first place?” This argument was probably fair enough some time ago when developers were still new to DLR and used dynamic C# only on special occasions. I’d say this is no longer a good argument. It’s easy and convenient to mix statically compiled and dynamic C#, and the language should make their interoperability more natural.

How can we make the failing test work? For example by breaking a fluent call chain:

[TestMethod]
public void SaveAndGetText()
{
    dynamic str = "a";
    var client = new ClientFactory().GetFluentClient();
    client = client.SaveText(str);
    var text = client.GetText();
    Assert.AreEqual("a", text);
}

Suddenly it all works, even with a use of dynamic object. But we can even make the original code work without changing a single line in the test code! The strange error message on Mono that complained about protection level gave me a hint: what if I change the visibility of FluentClass changing its protection level from private to public? Bingo! Suddenly all tests passed. Both on Windows and Mono platforms. But who could expect such behavior?

Use of dynamic code requires more thorough testing. But as you could see in examples above, it sometimes requires rewriting some calls or class definitions without changing any functionality, only because .NET runtime accepts one but not other perfectly valid syntax. This makes interoperability between CLR and DLR fragile. I believe there is a need to make it more predictable and developer-friendly.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s