On Inverses of Haskell Functions

Recall that given a function , a function is called a left inverse of f in case . In other words, we want g to undo whatever f did to its parameter. A function has a left inverse if and only if it is injective, a.k.a. one-to-one.

Question: How can we obtain left inverses of Haskell functions automatically?

In other words, I’d like to have a function

f :: a -> b

and obtain, without explicitly writing it, another function

g :: b -> a
g . f = (id :: a -> a)

I might first set out to accomplish the gold standard; to do this automatically, for an arbitrary domain. Alas I wouldn’t get very far. To succeed in that task would imply that the proposition is constructively valid. But it isn’t even classically valid, so that’s hopeless. Of course, if the domain of f is enumerable, I can in principle simply try all inputs to f in parallel until I find one that works… this is not terribly satisfying, however; it’s completely impractical for most purposes.

So what if I restrict the domain of f further? In particular, I will require that f operate polymorphically on values of some type class, and then restrict the type class to ensure that I can get inverses easily. By requiring that f be polymorphic, I can ensure that only “good” (i.e., easily invertible) things happen in the implementation of f. (By the way, we need rank-2 types for this.)

Finished! I did something a little tricky there, I suppose — I defined functions from Fooable things to Fooable things to actually be Fooable things themselves. Not too unusual a trick to play in abstract algebra, really.

Unfortunately, while this worked out, it’s fairly fragile. It turns out that Fooable isn’t really a terribly useful type class. The only fully polymorphic functions that can be defined from Fooable things to other Fooable things are actually finite iterations of one operation. I can add other operations, sure, but the thing I’m missing is any kind of choice. Since I have no way to inspect the value I’ve got as input to my function, I can’t make good use of if, case, pattern matching, or anything else of that form. While it might be interesting to characterize precisely what type signatures can exist in the Fooable type class without making inversion impossible, I’m going in a different direction.

Suppose that Fooable contained functions to inspect properties of the value you’re working with. Then we can’t play the trick above quite as cleanly as one might hope.

Now if we tried to proceed as before, how should we define look in the inversion instance? Really, we can only get an inverse for specific sequences of operations that we end up in, when computing in the forward direction. To implement that, we need to know the input (for the forward function) in advance, and compute with it.

In addition to an inverse function, BarInversion carries around a current value at which the inverse is defined.

The inverse function checks to ensure that all the properties of the current value that are observable via the type class are the same, since the original function might have relied on those properties in deciding what to do. If they don’t match, it gives up.

The intention is that our look function doesn’t really give away the house; i.e., it provides partial, not complete, information about the value. So for our test case, we’ll only give partial information:

In other words, as long as the input is observably the same as the point at which we performed the forward calculation, the inverse is available. If not, then the result is undefined. We’ve got a partial inverse, at least.

It’s worth noting that even if the inverse were only defined at the one point we started with, the inverse function could be valuable. Note that the type of invertBarrable only requires that x be of some Barrable type, and that the inverse operation on som Barrable type. They need not actually be the same type! This is quite useful if, for example, you want to trace some other computation through the calculation of the inverse. I think I’ll write another blog post on the practice of tracing one calculation through another one using type classes… but, some other time.

Related

One Comment

For something fleshed out along the lines of what you are discussing here, you may want to look at my paper “Bidirectionalization for Free!” (http://wwwtcs.inf.tu-dresden.de/~voigt/popl09-2.pdf), which plays this trick in the setting of bidirectional programming (very close to function inversion).