API clarity with types

API design is hard. Really hard. It’s one of the reasons I like TDD – it forces you to use the API as a regular client and it usually comes out all the better for it. At a previous job we’d design APIs as C headers, review them without implementation and call it done. Not one of those didn’t have to change as soon as we tried implementing them.

The Win32 API is rife with examples of what not to do: functions with 12 parameters aren’t uncommon. Another API no-no is several parameters of the same type – which means which? This is ok:

auto p = Point(2, 3);

It’s obvious that 2 is the x coordinate and 3 is y. But:

foo("foo", "bar", "baz", "quux", true);

Sure, the actual strings passed don’t help – but what does true mean in this context? Languages like Python get around this by naming arguments at the call site, but that’s not a feature of most curly brace/semicolon languages.

I semi-recently forked and extended the D wrapper for nanomsg. The original C API copies the Berkely sockets API, for reasons I don’t quite understand. That means that a socket must be created, then bound or connect to another socket. In an OOP-ish language we’d like to just have a contructor deal with that for us. Unfortunately, there’s no way to disambiguate if we want to connect to an address or bind to it – in both cases a string is passed. My first attempt was to follow in Java’s footsteps and use static methods for creation (simplified for the blog post):

struct NanoSocket {
    static NanoSocket createBound(string uri) { /* ... */ }
    static NanoSocket createConnected(string uri) { /* ... */ }
    private this() { /* ... */ } // constructor
}

I never did feel comfortable: object creation shouldn’t look *weird*. But I think Haskell has forever changed by brain, so types to the rescue:

struct NanoSocket {
    this(ConnectTo connectTo) { /* ... */ }
    this(BindTo bindTo) { /* ... */ }
}

struct ConnectTo {
    string uri;
}

struct BindTo {
    string uri;
}

I encountered something similar when I implemented a method on NanoSocket called trySend. It takes two durations: a total time to try for, and an interval to wait to try again. Most people would write it like so:

void trySend(ubyte[] data, 
             Duration totalDuration, 
             Duration retryDuration);

At the call site clients might get confused about which order the durations are in. I think this is much better, since there’s no way to get it wrong:

void trySend(ubyte[] data, 
             TotalDuration totalDuration, 
             RetryDuration retryDuration);

struct TotalDuration {
    Duration duration;
}

struct RetryDuration {
    Duration duration;
}

What do you think?

Advertisement
Tagged , , , , , , , ,

4 thoughts on “API clarity with types

  1. alexmatto says:

    I think it’s a great idea! It’s really important to disambiguate parameters and make the wrong action impossible.

    Your strategy is the way to go in static-typed languages, at-least in the public interface/API level.

    In Dynamic languages, obligatory named parameters are the right strategy and Python does great on that level. Also, all dynamic languages are adding optional typing since it helps to make self-documented APIs.

    PS: I love named parameters, it’s a shame that most static languages do not offer them =p

  2. nooboncode says:

    It’s actually a great idea! I usually leave comments in the header file explaining the inputs outputs for each function, but most people never read it and they prefer to talk to me in person about it.
    Thanks for showing me yet another way to keep people away! :p

  3. After reading this I wanted to try this idea in C++ (which I need to use for a project) and it works. But if I capture a string in class then it means copying the data into the string, and therefore memory allocations and deallocations which seems a lot of overhead just for passing a parameter. I can capture a string_view instead but then I have to be sure that the original string remains in scope as it’s just a reference so it’s confusing and error prone.

    There doesn’t seem to be any simple way in C++ to have a string which is a reference to the existing string but “safe”. I could use a shared_ptr to a string, but that this point the solution is worse than the original problem.

    I’m not always a huge fan of garbage collection in D but this is a case where it would make it so much easier to be both efficient and simple.

    Wishing that I could use D, or that C++ went a slightly different way,…

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 )

Connecting to %s

%d bloggers like this: