[Haskell-cafe] Type constrain in instance?

Casey McCann syntaxglitch at gmail.com
Fri Apr 9 13:15:23 EDT 2010

On Fri, Apr 9, 2010 at 11:22 AM, Louis Zhuang <louis.zhuang at acm.org> wrote:
> However GHC only has kinds for class/instance like (*->*->*) so we are forced to
> allow all possible types in instance code. I'm not sure if I'm modelling things
> correctly or is there another way to do the same thing?

As far as I know, it is indeed not generally possible to constrain
unmentioned parameters to a type constructor in an instance
declaration. There are workarounds involving modifications to the
class definition, but as you want to use a class from the standard
libraries, that helps very little. For the most part this is by
design; the standard type classes are intended to be fully generic in
their parameters.

However, it seems that you are creating your own special-purpose data
type, so one possible solution presents itself: Constrain the
ChainableFunction type to permit construction only with numeric types.
Simply placing a Num context on the data declaration fails, however,
as this demands a similar constraint on functions using the
type--which is exactly what we're trying to achieve, and so is
spectacularly useless. One conventional solution is to instead conceal
the actual data constructor from client code, instead exposing only
constructor/deconstructor functions with appropriate constraints; the
downsides to this are that client code cannot use pattern matching on
the type, and that internal code must carefully maintain constraints
anywhere the type is used.

A more aesthetically appealing approach, if you're not averse to
language extensions, is GADTs: Place constraints on the CF data
constructor, not the type. With CF the sole constructor the
constraints will be enforced everywhere, and best of all pattern
matching on CF will provide the necessary context--making the
constraint visible even inside the instance declaration for the
supposedly fully-generic (.)!

Alas, it now seems difficult to describe id; it must create a
ChainableFunction with any type (as per the Category class), and
without pattern matching on a ChainableFunction argument it has no way
of getting the constraints. But consider that, for the same reasons,
id has no way of actually doing anything with the type parameters with
which it must construct a ChainableFunction, and thus shouldn't need
to have them at all; further, the semantics of id are quite simple and
essentially independent of its parameterized "content". Thus, we can
add another constructor to ChainableFunction, that takes no arguments
and constructs a value of type (ChainableFunction a a), and extend the
definition of (.) to make the behavior of the identity explicit. The
result will look something like this:

{-# LANGUAGE MultiParamTypeClasses, GADTs #-}
import qualified Control.Category as Cat

data ChainableFunction a b where
    CF :: (Num a, Num b) => (a->b) -> (a->b) -> ChainableFunction a b
    CFId :: ChainableFunction a a

instance Cat.Category ChainableFunction where
    id = CFId
    CF g g' . CF f f' = CF (g.f) (\a -> f' a *> g' (f a))
    CFId . f = f
    g . CFId = g

You've probably noticed that I've been ignoring the Module class.
Unfortunately, the solution thus far is insufficient; a Module
constraint on the CF constructor does work as expected, providing a
context with (Module a b, Module b c), but the result requires an
instance for Module a c, which neither available, nor easily obtained.
I'm not sure how best to handle that issue; if you find the rest of
this useful, hopefully it will have given you enough of a start to
build a complete solution.

- C.

More information about the Haskell-Cafe mailing list