Postel's law as a profunctor by Mark Seemann
When viewing inputs and outputs as sets, Postel's law looks like a profunctor.
This article is part of a series titled Some design patterns as universal abstractions. Including the present article in that series is a bit of a stretch, since Postel's law isn't really a design pattern, but rather a software design principle or heuristic. I still think, however, that the article fits the spirit of the article series, if not the letter.
This article is heavily inspired by Michael Feathers' article The Universality of Postel's Law, in which he writes:
[Postel's law] has been paraphrased over the years as “Be liberal in what you accept, and conservative in what you send” and for people who are mathematically inclined: “be contravariant in your inputs and covariant in your outputs.”
A thing contravariant in input and covariant in output sounds like a profunctor, but why does Michael Feathers write that about Postel's law?
In this article, I'll try to explain.
Perfect fit #
Postel's law is a statement about functions, methods, procedures, or whatever else you'd like to call them. As I've previously outlined, with sufficient squinting, we can think about methods and other operations as functions, so in this article I'll focus on functions.
Functions don't stand alone. Functions have callers. Some other entity, usually client code, passes some input data to the function, which then performs its work and returns output data. When viewed with a set-based perspective, we can depict a function as a pipe:
Client code often use the output of one function as input for another:
Int3 isEven = EncodeEven(number); Int3 decremented = Decrement(isEven);
Even though this code example uses an explicit intermediary variable (isEven
), it's equivalent to function composition:
var composition = EncodeEven.Compose(Decrement);
where Compose
can be implemented as:
public static Func<A, C> Compose<A, B, C>(this Func<A, B> f, Func<B, C> g) { return x => g(f(x)); }
Such a composition we can depict by appending one pipe after another:
This works, but is brittle. It's a close fit. The output set of the first function has to exactly fit the input set of the second function. What happens if the pipes don't perfectly align?
Misalignment #
Functions compose when they fit perfectly, but in the real world, that's rarely the case. For example, it may turn out that Decrement
is defined like this:
static Int3 Decrement(Int3 i) { if (i == 0) throw new ArgumentOutOfRangeException( nameof(i), "Can't decrement 0."); return i - (Int3)1; }
This function is undefined for 0
. If we wanted to peek at the set diagram 'inside' the pipe, we might depict the function like this:
In a sense, it's still a mapping from our hypothetical 3-bit integer to 3-bit integer, but it's a partial function.
Another way to depict the mapping, however, is to constrain the domain to [1..7]
, and narrow the codomain to the function's image, producing a bijection:
Such sets are a little harder to express in code, because how do you represent a set with seven elements? Often, you'd stick with an implementation like the above Decrement
function.
This turns out to be unfortunate, however, because EncodeEven
is defined like this:
static Int3 EncodeEven(Int3 i) { return i.IsEven ? (Int3)1 : (Int3)0; }
As a set diagram, we might depict it like this:
It turns out that half the inputs into the above composition don't work! It's almost as though the pipes are misaligned:
This can easily happen, also in the real world:
This is also why Michael Feathers writes:
We can see Postel in the physical world too. Every time you see a PVC pipe with a flanged end, you’re seeing something that serves as a decent visual metaphor for Postel’s Law. Those pipes fit well together because one end is more accepting.
In other words, there's nothing new in any of the above. I've just been supplying the illustrations.
Flanges #
How should we interpret the idea of flanges? How do we illustrate them? Here's a way:
Given our set-based interpretation of things, how should we interpret a flange? Let's isolate one of them. It doesn't matter which one, but lets consider the left flange. If we attempt to make it transparent, we could also draw it like this:
What does that look like? It looks like a mapping from one set to another.
The left-hand set is slightly larger than the right-hand set, but the illustration includes neither the elements of each set nor the arrows that connect them.
If we think of the 'original' function as a function from the set A to the set B we can also write it in pseudo-code as A -> B
. In Haskell you'd exactly write A -> B
if A
and B
were two concrete types. Polymorphically, though, you'd write any function as a -> b
, or in C# as Func<A, B>
.
Let's think of any function a -> b
as the 'perfect fit' case. While such a function composes with, say, a function b -> c
, the composition is brittle. It can easily become misaligned.
How do we add flanges to the function a -> b
?
As the above illustration of the flange implies, we can think of the flange as another function. Perhaps we should call the slightly larger set to the left a+
(since it's 'like' a
, just larger - that is, more liberal). With that nomenclature, the flange would be a function a+ -> a
.
Likewise, the right flange would be a function b -> b-
. Here, I've called the narrower set of the flange b-
because it's smaller (more conservative) than b
.
Thus, the flanged pipe is just the composition of these three functions: a+ -> a
, a -> b
, and b -> b-
:
That's exactly how dimap
is defined in Haskell:
dimap ab cd bc = cd . bc . ab
The implementation code uses other letters, and recall that Haskell is typically read from right to left. As its name implies, ab
is a function a -> b
, bc
is a function b -> c
, and cd
is a function c -> d
.
In other words, Postel's law is a description of the Reader profunctor, or, as Michael Feathers put it: Be contravariant in your inputs and covariant in your outputs.
Conclusion #
Postel's law is a useful design principle to keep in mind. Intuitively, it makes sense to think of it as making sure that pipes are flanged. The bigger the receiving flange is, and the smaller the nozzle is, the easier it is to compose the flanged pipe with other (flanged) pipes.
Using mostly visual metaphor, this article demonstrates that this is equivalent with being contravariant in input and covariant in output, and thus that the principle describes a profunctor.
Postel's law, however, isn't the only design principle describing a profunctor.