[Haskell-cafe] category design approach for inconvenient concepts
wren ng thornton
wren at freegeek.org
Thu Dec 20 13:38:29 CET 2012
On 12/18/12 5:03 PM, Christopher Howard wrote:
> Since I received the two responses to my question, I've been trying to
> think deeply about this subject, and go back and understand the core
> ideas. I think the problem is that I really don't have a clear
> understanding of the basics of category theory, and even less clear idea
> of the connection to Haskell programming. I have been reading every link
> I can find, but I'm still finding the ideas of "objects" and especially
> "morphisms" to be quite vague.
As others have mentioned, that "vagueness" is, in fact, intentional.
There are two ways I can think of to help clear up the abstraction. The
first is just to give a bunch of examples:
the category of sets (objects) and set-theoretic functions (morphisms)
the category of Haskell types and Haskell functions[1]
... (small) categories, and functors
... rings, and ring homomorphisms
... groups, and group homomorphisms
... vectors, and linear transformations
... natural numbers, and matrices
... elements of a poset, and the facts that one element precedes another
... nodes of a directed graph, and paths on that graph
The second approach is to compare it to something you're already
familiar with. I'm sure you've encountered monoids before: they're just
an associative operation on some carrier set, plus an element of that
set which is the identity for the operation. Perhaps the most auspicious
one to think about is multiplication, or concatenation of lists.
A category is nothing more than a generalization from monoids to
"monoid-oids". That is, with monoids we give our operator the following
type:
(*) :: A -> A -> A
but sometimes things aren't so nice. Just think about matrix
multiplication, or function composition. These are partial operations
because they only work on some subset of A. The two As must air up in a
nice way. Thus, what we really have is not one carrier, but a family of
carriers which are indexed by their "input" end (domain) and their
"output" end (codomain). Thus, we have the type:
(*) :: A i j -> A j k -> A i k
or
(*) :: A j k -> A i j -> A i k
where i, j, and k, are our indices. Which one of the above two types you
get doesn't matter, it's just the difference between (<<<) and (>>>) in
Haskell. Of course, now that we've indexed everything, we can't have
just one identity element for the operation; instead, we need a whole
family of identity elements:
1 :: A i i
In a significant sense, the objects are really only there to serve as
indices for the domain and codomain of a morphism. They need not have
any other significance. A good example of this is when we compare two of
the example categories above: linear transformations, vs matrices. For
the category of vector spaces and linear transformations, the objects
actually mean something: they're vector spaces. However, in the category
of natural numbers and matrices, the natural numbers only serve to tell
us the dimensions of the matrices so that we know whether we can
multiply them together or not. Thus, these are different categories,
even though they're the same in just about every regard.
[1] Note that this may not actually work out to be a category, but the
basic idea is sound.
> But here I am
> confused: If "functions" are a category, this would seem to imply (by
> the phrasing) that functions are the objects of the category. However,
> since we compose functions, and only morphisms are composed, it would
> follow that functions are actually morphisms. So, in the "function"
> category, are functions objects or morphisms? If they are morphisms,
> then what are the objects of the category?
Objects in Haskell are types, and functions aren't types. But cherish
that confusion for a bit, because it hints at a deeper thing going on
here. In the simplest scenario, functions are the morphisms between
objects (i.e., types). But what happens when we consider higher-order
functions?
In Haskell we write things like:
(A -> B) -> C
D -> (E -> F)
Whereas, in category theory we would distinguish the first-order arrows
from the higher-order arrows:
B^A -> C
D -> F^E
That is, we have an object B^A (read that as an exponent), and we have a
class of morphisms A->B (sometimes this class is instead written
Hom(A,B)). These are different things, though we willfully conflate them
in functional programming. The B^A can be thought of as the class of all
functions from A to B when we consider these functions as data; whereas
the A->B can be thought of as the class of all functions from A to B
when we consider these functions as procedures to be executed. Part of
the reason we conflate these in functional programming is because we
know that the one reflects the other. Whereas the reason category theory
distinguishes them is because this sort of reflection isn't possible in
all categories. Some categories have exponential objects, others don't.[2]
Thus, when you ask whether a function belongs to an object or to a
Hom-set, the answer depends on what exactly you're talking about.
[2] There are reasons to make this distinction in programming as well.
For one, not all programming languages allow higher-order functions. But
more importantly, even for those that do, the compiler writers already
have to make the distinction. Functions-as-data basically require us to
use closures in some form or another. These closures are basically just
records that we can pass around like any other data. But
functions-as-procedures are just code pointers (or, rather, the location
pointed to by them), they're just somewhere to jump to and start
executing code. Clearly these are different things even if we can go
back and forth by storing a code pointer in a record and by jumping to
the address stored in a closure.
--
Live well,
~wren
More information about the Haskell-Cafe
mailing list