Simple type isomorphisms
I found some simple notes that I wrote for a class that followed Pierce's excellent book "Types and Programming Languages" on type isomorphisms. The course is no longer running, so I thought I'd reproduce the notes here, for posterity. The examples are written in OCaml, but the ideas easily translate to other languages such as Haskell or F#.
Type Isomorphisms
In this note, we'll look at "Type Isomorphisms", which are a general notion of conversion between types.
Motivation
Imagine that I've written a particularly good parsing library, and that you want to use it in your code. Unfortunately, I've written it using types that are not equal to your types, so you can't use my library; disaster!
However, if it's the case that you can write conversion functions, that transform from my types into yours, and vice-versa, and further, if these functions don't lose or gain any information, you could write a "wrapper" around my library, and be able to parse away to your heart's content!
This wrapper would convert function arguments into types that my library understands, call the relevant parsing functions in my library, before converting the result back into types that your code understands. As long as our conversion functions don't "change" the arguments or result in a way that loses/gains information, then this transformation is "safe", and will guarantee the correct data goes into and out of my library.
Isomorphisms
It turns out that this notion of conversion, or adaptation without loss of information between types has a fancy title: an isomorphism between the two types. We say that types A and B are isomorphic, if we have conversion functions that witness the isomorphism:
f : A -> B
g : B -> A
such that:
f . g = id_B
g . f = id_A
the notation .
is function composition (from right to left) and can be
defined as:
g . f == fun x -> g (f x)
that is, the composition of f and g (confusingly, we usually read "g . f" as "g composed with f"!) is itself a function which first applies f, then applies g.
The notation id_X means "the identity function for type X" where the identity function can be easily defined as:
let id x = x
or as a "lambda expression", or "anonymous function":
fun x -> x
So, we can study type isomorhpisms as an interesting general technique; they
are just two types A
, B
, with conversion functions f : A -> B
and g: B
-> A
such that applying the composite of f
and g
in either order is the
same as doing nothing.
Right, onto some examples:
Example: (un)curry
Recall that "Currying" transforms a function that takes as input multiple arguments (via a pair), into a function that accepts the first element of the pair, and returns a function that accepts the second argument, before returning the result.
We can give the functions for currying and in the "other direction", so called "uncurrying":
let curry (f : (('a * 'b) -> 'c)) =
fun (a : 'a) ->
fun (b : 'b) ->
f (a, b);;
let uncurry (f : ('a -> 'b -> 'c)) =
fun ((a, b) : ('a * 'b)) ->
f a b;;
So, what is the result of using these functions like this:
let doBoth f = uncurry (curry f);;
Exercise: what is the type of doBoth
? "Follow the types" through the call
to curry and then uncurry to see that it checks out.
In fact, the curried, and uncurried form of a given function type are isomorphic:
let uncurryCurry f = curry(uncurry f);; (* = id at type "('a -> 'b -> c" *)
let curryUncurry g = uncurry(curry g);; (* = id at type "('a * 'b) -> c" *)
We don't prove here that the compositions really are equal to id
, but with
some simple equational reasoning, we can convince ourselves it's ok.
Exercise: do the proof!
So, we've found one isomorphism - between a function and its curried form; let's try to find some more!
Sums and Products
Another commonly used data type is the "Sum" type, defined thus:
type ('a , 'b) sum = Left of ('a)
| Right of ('b) ;;
We can't write it in OCaml, but sometimes we write ('a, 'b) sum
as
'a + 'b
.
So a value of Sum type ('a, 'b) sum
is either a value of type 'a
, or a
value of type 'b
. Similarly, we have the well-known Product (or pair) type
('a * 'b)
, values of which contain a value of type 'a
, and a value of
type 'b
.
Are Sum and Product isomorphic? If so, we can write the witnessing functions. If not, can we say why not?
Let's try in the Product to Sum direction:
let prodToSum (a, b) = Left a;; (* Any alternatives here? *)
Ok, that appeared to sort of work; what about in the reverse direction?
exception Oops;;
let sumToProd (ab : ('a, 'b) sum) : ('a * 'b) = match ab with
| Left a -> raise Oops
| Right b -> raise Oops;;
Oh. We can't construct witnesses between Product and Sum! So, Product and Sum are not isomorphic. But, why not?
Exercise: Answer the question, thinking about missing information.
Example: The algebra of types
Now for my final example. If we follow our intuitions from high school algebra,
we should have that for any integers a,b,c: (a * (b + c)) = ((a * b) + (a *
c))
Can we translate this equality into an isomorphism between the aptly-named Sum and Product types? Yes we can:
type ('a, 'b, 'c) pairsum = 'a * (('b, 'c) sum);;
type ('a, 'b, 'c) sumpair = (('a * 'b), ('a * 'c)) sum;;
let trans (f: ('a, 'b, 'c) pairsum) : ('a, 'b, 'c) sumpair = match f with
| (a, Left b) -> Left (a, b)
| (a, Right c)-> Right (a, c) ;;
(* snart == trans backwards *)
let snart (f : ('a,'b,'c) sumpair) : ('a, 'b, 'c) pairsum = match f with
| Left(a, b) -> (a, Left b)
| Right(a, c) -> (a, Right c);;
Exercise: What do the following evaluate to?
trans (snart (Left(3, true)));;
trans (snart (Right("Hello", fun x -> x)));;
snart (trans (3, Left(true)));;
snart (trans ("Hello", Right(fun x -> x)));;
So, there we have it. I've described type isomorphisms: why you might want to know about/use them, what they are, and how you know if you've got one.