Search code examples
haskelltypeclasshugs

Expressing multi-parameter typeclasses in terms of single-parameter ones


For a certain reason (weird Hugs installation on target computer) I want Haskell98 compliant code, which means no common language extensions. One annoyance in particular is the absence of multi-parameter typeclasses, for example in my case:

class Semiring d where
  zero :: d
  one :: d
  plus :: d -> d -> d
  times :: d -> d -> d

class Monoid e where
  mzero :: e
  mplus :: e -> e -> e

class (Semiring d, Monoid e) => Module d e where
  mtimes :: d -> e -> e

I get a feeling that this issue in particular can be stepped around somehow, maybe by defining a few helper typeclasses and "packaging" the information about the types somehow, but I cannot figure it out.

Is my intuition justified at all? I am reading that multiparam typeclasses were widely used even at the time of Haskell98 standard's writing, were there ways of working around this limitation back then?


Solution

  • Oleg Kiselyov wrote a demonstration of this in 2007, Haskell with only one typeclass:

    [I]f we remove the type class declaration and the standard type classes, leaving the language with a single, fixed, pre-defined type class with a single method, no expressivity is lost. We can still write all Haskell98 type-class programming idioms including constructor classes, as well as multi-parameter type classes and even some functional dependencies.

    Briefly, he introduces a two-parameter class C with a functional dependency and a single method:

    class C l t | l -> t where ac :: l -> t
    

    This relates a “label” l with its type t, for example:

    data Add a
    
    instance C (Add Int) (Int -> Int -> Int) where
      ac _ x y = x + y
    
    (+) :: forall a. (C (Add a) (a -> a -> a)) => a -> a -> a
    (+) = ac (undefined :: Add a)
    

    This isn’t strictly H98 because it uses a scoped type variable, but I believe those were implemented in Hugs fairly early on.

    Now, as a multi-parameter class is effectively a set of tuples of types, we can rephrase it as a single-parameter class whose elements are tuples, at the loss of the functional dependency.

    class Overload a where overload :: a -> a
    
    data Add a
    
    instance Overload (Add Int, Int -> Int -> Int) where
      overload (label, _) = (label, \x y -> x + y)
    
    apply :: (Overload (label, signature)) => label -> signature
    apply label = f
      where
        (_, f) = overload (label, f)
    
    (+) :: forall a. (Overload (Add a, a -> a -> a)) => a -> a -> a
    (+) = apply (undefined :: Add a)
    

    In modern Haskell, you may wish to use a proxy, but I’ve kept the use of undefined/bottom, both in the spirit of old-timey Haskell, and for the sake of similarity with the previous example.

    Of course, the lack of a fundep now means that the compiler won’t be able to infer an unambiguous type in many cases, because it can’t determine that each label such as Add Int has only one corresponding signature Int -> Int -> Int. So this may require a type annotation. On the other hand, it grants you some flexibility, in that you can technically give multiple types to the same term if you want to construct things like subtypes or overlapping instances. And if all you want to avoid is multi-parameter classes, then you can avoid a lot of the ambiguity by not restricting yourself to just one class.

    You can also encode functional dependencies manually using equality constraints, if they’re available:

    -- |
    -- Compare:
    --
    -- > class Widen a b | a -> b where widen :: a -> b
    -- > instance Widen Bool Int where widen = fromEnum
    
    instance (b ~ Int) => Overload (Widen Bool b, Bool -> Int) where
        overload (label, _) = (label, fromEnum)
    

    Many more details and examples are in the article and accompanying code.