Back in the Before Times, I went to a Haskell-flavored FP conference, where one of the speakers said something that blew my mind. Sadly, it seems that I didn’t write this up at the time (although I swear I wrote it somewhere… maybe in an internal company memo) and I’ve lost the details of who said it. If by some quirk of odds, it was you, dear reader, please let me know!

The speaker posed a question: Given the following function signature–and no additional information at all–how many possible implementations of f are there?

f :: a -> a

For those who don’t read Haskell, it says “the function f takes an argument of type a and returns a value of type a”. We have no information about a whatsoever, and that’s the key to the koan. Without any information about type a, f cannot apply any operation to the value it receives. It cannot create a new instance of a because it doesn’t know how to construct it. It cannot modify the value because it doesn’t know any other function to apply. In fact, the only possible implementation of f is id, the identity function. If you give me an a, all I can do is give it back to you.

At first glance, this seems trivial or even tautological. But there’s something profound under the surface. If, like me, you come from the dynamic side of FP or the world of OOP where you always have a base class with some operations available, this might need a little bit of unpacking. I don’t want to turn this into a Haskell tutorial (I would certainly get a lot of it wrong!) but a couple of ideas are necessary.

Suppose we wanted to say that a different function g could operate on any integer. We would then have a different signature:

g :: Integer -> Integer

In this case, g can take any possible integer into any other possible integer. Now we know way less about the implementation. It could increment or decrement its argument. It could add 42 to odd values and subtract 99 from even ones. If we’re working with 64 bit integers, there are 18,446,744,073,709,552,000 implementations of this function that just ignore their argument and return a constant!

We could also use typeclasses to indicate partial information about the values. Suppose we wanted to create a function h that accepts values which can be compared and placed in an ordering. That would look something like :

h :: (Ord a) => a -> a

(I probably have the syntax wrong, but bear with me… it’s the idea that is important here.)

This says that h can take any value of any type, as long as that type is known to fulfill the requirements of the Ord typeclass. Ord is fairly minimal, it just requires that the type implements a few operations like “less than” and “greater than”. (It also brings in Eq, but we don’t need that at the moment.)

Let’s get back to our original f :: a -> a: By using the type variable a with zero additional information about a, the compiler enforces constraints on the implementer of f. f must accept any value of any type whatsoever. f is therefore not allowed to know anything about the arguments. It cannot make any assumptions about the values it will be called with. In a sense, f is maximally constrained because it has the least information possible about its arguments.

On the flip side, a caller of f is maximally liberated. Because the implementation cannot make any assumptions, it also means the implementation cannot impose any constraints on the caller. The caller can pass any value it wants to. The caller can use f in contexts and situations that the implementer of f never dreamed of.

I think this generalizes to a principle: Constrain the provider to liberate callers.

In my experience, elegant systems emerge when we have functions or modules that can be used in multiple contexts. So we would like to have multiple callers. But whatever functionality the provider offers can only be used in contexts that meet its assumptions. Callers are restricted to meeting the provider’s assumptions. Therefore assumptions in the provider constrain the caller. We should invert this: limit the provider’s ability to make assumptions thereby allowing callers to use it in various situations.

This extends to services across an enterprise, too. The more a provider is allowed to know about how it will be used, the narrower the ability to reuse or recompose the services will be. Extend that forward and backward along call chains in a distributed services environment, and you will see inflexibility set in as every caller of some service is itself a provider to others.

I think this is also linked to the phenomenon of single-use API definitions, where an API is written for a specific point-to-point interaction. The implementation inevitably makes too many assumptions about how it will be used. So you get an environment with a proliferation of APIs each with their own payload types.

Another related idea is selective amnesia. When designing an API to offer, you can choose to temporarily forget what you know about the caller. Instead think about how a second or third caller might want to invoke your service. This leads to an API that is “one notch more abstract” than you might otherwise design. (Before you shout YAGNI, please recall that we must weaken YAGNI across service boundaries.) Selective amnesia can help constrain the provider from making assumptions.