Avoiding Overload Hell in C#

February 26, 2008 c-sharp code

Update 2021/09/22: C# got support for default parameters in 4.0.

C# lacks default parameters. The C# answer to default params is overloaded methods. With a lot of options, this can quickly scale to hell. Even if it had default parameters, they are pretty limited too. This post talks a bit about a couple of other solutions I found to the problem that avoid the combinatorial sea of overloads.

The problem

For kicks, I wrote a little curses-like terminal library in C#. It only really has a few core methods: Write() for writing text, Fill() to fill in a space, and Draw() to draw lines and boxes using awesome ASCII lines. The tricky part is that each of those can take a bunch of different parameters. For our talk, we’ll simplify it down:

  1. Position: You can specify where on the screen to write using a Point, x and y coordinates, or not at all to write at the current cursor position.

  2. Color: You can specify both the fore- and background color, just the foreground color, or neither to use the current colors.

And that’s it. Three methods, and two kinds of parameters with three options each.

Overloads: the vanilla solution

So the normal way to address this is by overloading Write(), Fill(), and Draw(). If we go with the vanilla overload solution, we have 27 overloaded methods to implement every combination. Using the code looks like this:

Terminal terminal = new Terminal();

terminal.Write("foo");
terminal.Fill(1, 2, Color.Red);
terminal.Draw(Color.Green, Color.Blue);

Not bad. But implementing it looks like:

public class Terminal
{
    public void Write(Point pos, Color fore, Color back,
        string text) { ... }
    public void Write(int x, int y, Color fore, Color back,
        string text) { ... }
    public void Write(Color fore, Color back, string text) { ... }

    public void Write(Point pos, Color fore, string text) { ... }
    public void Write(int x, int y, Color fore, string text) { ... }
    public void Write(Color fore, string text) { ... }

    public void Write(Point pos, string text) { ... }
    public void Write(int x, int y, string text) { ... }
    public void Write(string text) { ... }

    public void Fill(Point pos, Color fore, Color back) { ... }
    public void Fill(int x, int y, Color fore, Color back) { ... }
    public void Fill(Color fore, Color back) { ... }

    public void Fill(Point pos, Color fore) { ... }
    public void Fill(int x, int y, Color fore) { ... }
    public void Fill(Color fore) { ... }

    public void Fill(Point pos) { ... }
    public void Fill(int x, int y) { ... }
    public void Fill() { ... }

    public void Draw(Point pos, Color fore, Color back) { ... }
    public void Draw(int x, int y, Color fore, Color back) { ... }
    public void Draw(Color fore, Color back) { ... }

    public void Draw(Point pos, Color fore) { ... }
    public void Draw(int x, int y, Color fore) { ... }
    public void Draw(Color fore) { ... }

    public void Draw(Point pos) { ... }
    public void Draw(int x, int y) { ... }
    public void Draw() { ... }
}

Lame! Worse, if you start adding more options, it goes up combinatorially. 27 is just for our toy terminal. For my actual terminal lib, it would take 324 overloaded methods to support every combination. There has to be a better way.

Variable argument lists: who needs strong typing anyway?

It’s true that C# doesn’t support default parameters, but it does support variable length argument lists. Maybe that’s a solution that lets us pass in a variety of options without having to overload. Calling it still looks like:

Terminal terminal = new Terminal();

terminal.Write("foo");
terminal.Fill(1, 2, Color.Red);
terminal.Draw(Color.Green, Color.Blue);

But the class itself just looks like:

public class Terminal
{
    public void Write(string text, params object[] options) { ... }
    public void Fill(params object[] options) { ... }
    public void Draw(params object[] options) { ... }
}

That doesn’t look so bad. Except when you try to implement one of the methods:

public void Write(string text, params object[] options)
{
    Point pos = mCurrentPos;
    Color? foreColor = null;
    Color? backColor = null;
    int? x = null;
    int? y = null;

    foreach (object obj in options)
    {
        if (obj is Point)
        {
            pos = (Point)obj;
        }
        else if (obj is Color)
        {
            if (foreColor == null) foreColor = (Color)obj;
            else backColor = (Color)obj;
        }
        else if (obj is int)
        {
            if (x == null) x = (int)obj;
            else y = (int)obj;
        }
    }

    if (x == null) x = mCurrentX;
    if (y == null) y = mCurrentY;

    // write using pos, color, etc.
}

That’s… just… no. And that doesn’t even have any error handling. The following is totally valid:

terminal.Write("foo", 17, Color.White, Color.Aqua,
    Color.Beige, "wtf?");

OK, scratch that.

Parameter object: strong typing is strong

So we know we want strong typing. How about if we bundle all of the parameters into a parameter object? This is basically the ProcessStartInfo solution.

We define a class something like:

public class TerminalParams
{
    // Nullable so that null = default value.
    public Point? Pos;
    public Color? Fore;
    public Color? Back;
}

And our Terminal looks something like:

public class Terminal
{
    public void Write(TerminalParams paramObj, string text) { ... }
    public void Fill(TerminalParams paramObj) { ... }
    public void Draw(TerminalParams paramObj) { ... }
}

In C# 3.0 with object initializers, we can call it something like:

Terminal terminal = new Terminal();

terminal.Write(new TerminalParams(), "foo");
terminal.Fill(new TerminalParams
        { Pos = new Point(1, 2), Fore = Color.Red });
terminal.Draw(new TerminalParams
        { Fore = Color.Green, Back = Color.Blue });

I won’t say that’s the greatest thing ever, but it’s not too bad. Of course, if you’re not on 3.0 yet, you’re up a creek. All you’ve basically accomplished is moved your overloads to constructor overloads in TerminalParams. And I still think there’s a little too much… junk… in those lines of code. The new TerminalParams { and new Point(...) parts don’t really add any value.

Parameter groups

Really, the overloaded methods do have the best calling convention so far. Let’s look at it a little closer:

terminal.Write(1, 2, Color.Blue, Color.Red, "foo");

While that’s an uninterrupted list of parameters, they’re conceptually grouped like this:

terminal.Write(    1, 2, Color.Blue, Color.Red, "foo");
//             ^^^^^^^^  ^^^^^^^^^^^^^^^^^^^^^  ^^^^
//             Position  Color                  Text

How cool would it be if we could actually group the parameters like that in the code? Something like:

terminal.Write(1, 2)(Color.Blue, Color.Red)("foo");

C# doesn’t have functors, but maybe we’re on to something.

Insert several hours of mucking around with properties that return delegates that return delegates, etc.

Or… not. But let’s not give up yet. Let’s try re-arranging things:

terminal.Write("foo")(1, 2)(Color.Blue, Color.Red);

This is a little better because it puts the method (Write()) next to the one parameter it really needs, the string. The problem is that that line, if it were possible to write, would execute from left to right. So we would have to write before we looked at the parameters.

Let’s do some more shuffling:

terminal(1, 2)(Color.Blue, Color.Red).Write("foo");

This looks promising, but we’re stuck with the fact that C# doesn’t have functors. We can’t do terminal(). Is there anything that sort of looks like ()?

You’re using what?!

So the question is, “what kind of grouping construct can we define at the object level?” Astute readers are already thinking it: indexers. Right now, some of you may be shuddering in horror at the weirdness we’re about to unleash, but let’s do it anyway. Can we make all of these lines of code work:

terminal.Write("foo");
terminal[new Point(3, 4)].Write("foo");
terminal[1, 2][Color.Red].Write("foo");
terminal[Color.Brown].Write("foo");
terminal[Color.Green, Color.Blue].Draw();

And can we make them work all at the same time? Not only that, can we make the following not work:

// Bad, cannot specify position twice.
terminal[1, 2][new Point(3, 4)].Write("foo");

// Bad, cannot specify half a position.
terminal[1].Write("foo");

// Sorta bad, for consistency would always like
// to have position before color.
terminal[Color.Brown][1, 2].Write("foo");

It turns out, we can. The core idea is to define a chain of interfaces that inherit from each other.

public interface ITerminalMethods
{
    void Write(string text);
    void Fill();
    void Draw();
}

public interface ITerminalColor : ITerminalMethods
{
    ITerminalMethods this[Color fore] { get; }
    ITerminalMethods this[Color fore, Color back] { get; }
}

public interface ITerminalPosColor : ITerminalColor
{
    ITerminalColor this[Point pos] { get; }
    ITerminalColor this[int x, int y] { get; }
}

public interface ITerminal : ITerminalPosColor { }

Each one represents one of the parameter types: position, color, etc. The innermost one, ITerminalMethods, contains the actual methods, Write(), Fill(), etc.

The other interfaces define indexers for each parameter type that return the next interface up the chain. The position interface, ITerminalPosColor, has indexers that return the color interface, ITerminalColor, which has indexers which return the method interface, ITerminalMethods. Since the interfaces inherit from each other too, you can also skip a step. You can go from ITerminalPosColor straight to ITerminalMethods because ITerminalColor interface inherits from it.

Calling it looks like:

terminal.Write("foo");
terminal[new Point(3, 4)].Write("foo");
terminal[1, 2][Color.Red].Write("foo");
terminal[Color.Brown].Write("foo");
terminal[Color.Green, Color.Blue].Draw();

This is all well and good for defining it, but is it possible to actually implement this thing?

We can do that too. The idea is that the Terminal class really contains two kinds of state: the core “set of characters on screen” state and the ephemeral state that can be overridden by these indexers—current color and position. We let a Terminal create a shallow clone of itself that shares the core state, but has a copy of the ephemeral state that can be changed. All of the indexers create new Terminals that write to the same core terminal data but keep their own copy of the position and color. Like so:

public class TerminalData
{
    // Terminal core state:
    public char[] Characters;
    public Color[] ForeColors;
    public Color[] BackColors;
}

public class Terminal : ITerminal
{
    public Terminal() { mData = new TerminalData(); }

    #region ITerminalPosColor Members

    public ITerminalColor this[Point pos]
    {
        get { return new Terminal(mData, pos, mFore, mBack); }
    }

    public ITerminalColor this[int x, int y]
    {
        get { return new Terminal(mData,
            new Point(x, y), mFore, mBack); }
    }

    #endregion

    #region ITerminalColor Members

    public ITerminalMethods this[Color fore]
    {
        get { return new Terminal(mData, mPos, fore, mBack); }
    }

    public ITerminalMethods this[Color fore, Color back]
    {
        get { return new Terminal(mData, mPos, fore, back); }
    }

    #endregion

    #region ITerminalMethods Members

    public void Write(string text) { ... }
    public void Fill() { ... }
    public void Draw() { ... }

    #endregion

    private Terminal(TerminalData data, Point pos,
        Color fore, Color back)
    {
        mData = data;
        mPos  = pos;
        mFore = fore;
        mBack = back;
    }

    private TerminalData mData;

    // Ephemeral state:
    private Point mPos;
    private Color mFore;
    private Color mBack;
}

Is it worth doing?

So this is how my little terminal library works, and I’ve grown pretty fond of it. However, this is a personal project, so my willingness to deal with very idiomatically strange code is pretty high. (Ask me about NotNull<T> sometime.)

In a production environment, this might be too strange looking for some teams. But maybe if it gets popularized enough what was once strange will become familiar.