Inferring Process Behaviour from Type

One of Haskell’s big selling points is its powerful type system which, among many other things, allows for easy type-parameterisation of functions. For example, the identity function has type a -> a, meaning that it will take something of any type a and return a value of the same type. Think about this function for a moment — it promises to return something of type a, no matter what type that is. The only way it can do so is to return the value given to it — that’s the only thing available that’s guaranteed to be of the right type. So in fact the only function that satisfies the type a -> a is the identity function. (Consider everything in this blog post to have the disclaimer: …as long as no bottom values are involved.)

CHP Process Types

We can extend this type of reasoning to CHP processes. Let’s start with the process type: a -> Chanout a -> CHP (). A process of this type has an output channel carrying type a, and the only value it has available to it of that type is the one given as the first parameter. So all values sent out on the channel must be the value passed as the first parameter. That’s useful to know. Reasonable behaviours of the process might be to do nothing (semi-reasonable, anyway: const (const (return ()))), write the value once (writeValue) or write it repeatedly (repeat).

We can also consider processes with types such as Chanout b -> CHP () or Chanin a -> Chanout b -> CHP (). We can tell that a process with either of these types will never generate any output, because it cannot form any values of the right type. This is a bit like the function with type a -> b in Haskell, which cannot be properly implemented.

Another process we can consider is one with type (a -> b) -> Chanin a -> Chanout b -> CHP (). In this case, any values sent out on the output channel must be values that were read from the input channel that have been transformed by the given function. The process may re-order, drop or delay values that are sent in — for example, it may send on only every third value. Since the process also has access to the IO monad, it may also drop values randomly. But we know that its behaviour cannot be affected by the values being passed through, because the process has no means by which to inspect values of type a or b. Contrast this with a process that has type (a -> Bool) -> Chanin a -> Chanout a -> CHP(); there the process has a function that it can use to make a decision about the given value.

There is another useful fact we can deduce about the process with type (a -> b) -> Chanin a -> Chanout b -> CHP (). If we never send in any values, it won’t have any to output — so we know this process cannot produce any output before it has first read some input. That’s useful information, and we know it just by looking at the type of the process. This reasoning extends to any process with a type like Chanin a -> Chanout a -> CHP (); such processes must consume input before they can produce any output. Similarly, the process with type Chanin a -> Chanin b -> Chanout (a, b) -> CHP () must input at least once from each of its input channels before it is able to output.

Free Theorems

One well-known bit of work along these lines is Wadler’s Free Theorems paper (bottom of the page). The paper uses these ideas to automatically form theorems based on functions. For example, you can deduce that for any two functions f :: [a] -> a and g :: a -> a, it must be the case that f . map g = g . f. Intuitively, the result of f must be one item from the input list, so whether you apply g to that item afterwards, or apply g to all the list elements beforehand, you must get the same answer. Other examples abound in the paper.

So can we apply similar reasoning to processes? With processes, we have two considerations: the values being passed through the processes, and the communication behaviour. Given two processes p, q :: Chanin a -> Chanout a -> CHP (), what can we say about them? We might wonder if composing them in either order (p <=> q vs q <=> p) gives the same results. This is not true, for several reasons.

Let’s start with values. Imagine that p replicates each value three times, and q sends on every third value. The composition p <=> q will act like the identity process; of every three repeats that p sends on, q will drop all but one. But the composition q <=> p will act differently; q will send on every third value, and p will then repeat it three times. So the same number of values will emerge (perhaps this, at least, is always true?), but they will be different.

The communication behaviour can also be different. Consider if p drops all the values it receives without forwarding them, while q refuses to do anything. The composition p <=> q will act as p does, silently dropping all values received — but it will at least accept all the values it is given. The composition q <=> p will refuse to read any values, so attempting to send a value to it will result in deadlock. It seems that attempts to form theorems about the composition of processes does not work as well as it does for functions.

All this reasoning collapses as soon as the processes have more specific values. The process with type Chanin Int -> Chanout Int -> CHP () might do anything, because it can construct Int values itself. It’s generally good practice in Haskell to keep the types as general as possible, and this type-based reasoning is one reason why that’s a good idea.