Search code examples
haskelltypeclassgeneric-programming

Parametric typeclasses


Is there a way to declare a generic parametrized type Conf that offers/provides a function frames depending on the type parameter d, like

{-# LANGUAGE GeneralizedNewtypeDeriving
           , MultiParamTypeClasses
           , FunctionalDependencies #-}

import Control.Applicative
import Control.Monad
import Control.Monad.Identity
import Control.Monad.Trans.Class
import Control.Monad.Trans.State
import Control.Monad.Trans.Except

data MyConf d = MyConf { frames :: [d] } -- my parametric type

-- define a class to expose the interface in monads
class ConfM d m | m -> d where
    getFrames :: m [d]

-- wrap StateT to include MyConf and allow instance of ConfM
newtype MyStateT d m a = MyStateT { runMyStateT :: StateT (MyConf d) m a }
    deriving (Functor, Applicative, Monad, MonadTrans)

-- expose the interface over selected transformers
instance Monad m => ConfM d (MyStateT d m) where
    getFrames = MyStateT $ fmap frames get

instance (ConfM d m, Monad m) => ConfM d (ExceptT a m) where
    getFrames = lift getFrames

so that then one can write something like:

-- hide the gory implementation details
type MyMonad d = ExceptT A (MyStateT d B) C

class SomeClass a

-- this is the desired goal:
-- to concisely write an 'algorithm' in MyMonad only once
-- but for all possible choices of d from SomeClass
example :: SomeClass d => MyMonad d
example = do
    fs <- getFrames
    -- do SomeClass stuff with d
    return ()

-- assume Int is instance of SomeClass
instance SomeClass Int

-- give me an instance of the above generic 'algorithm'
exampleInt :: MyMonad Int
exampleInt = example

-- assuming for example
type A = ()
type B = Identity
type C = ()

In the above code I am stuck at:

test.hs:23:25:
    Illegal instance declaration for ‘ConfM d (MyStateT d m)’
      (All instance types must be of the form (T a1 ... an)
       where a1 ... an are *distinct type variables*,
       and each type variable appears at most once in the instance head.
       Use FlexibleInstances if you want to disable this.)
    In the instance declaration for ‘ConfM d (MyStateT d m)’

test.hs:26:38:
    Illegal instance declaration for ‘ConfM d (ExceptT a m)’
      (All instance types must be of the form (T a1 ... an)
       where a1 ... an are *distinct type variables*,
       and each type variable appears at most once in the instance head.
       Use FlexibleInstances if you want to disable this.)
    In the instance declaration for ‘ConfM d (ExceptT a m)’

With additional FlexibleInstances

test.hs:27:14:
    Illegal instance declaration for ‘ConfM d (ExceptT 
      The coverage condition fails in class ‘ConfM’
        for functional dependency: ‘m -> d’
      Reason: lhs type ‘ExceptT a m’ does not determine
      Using UndecidableInstances might help
    In the instance declaration for ‘ConfM d (ExceptT a

So I need UndecidableInstances.


Solution

  • Your question seems a bit vague, but it sounds like a potential use case for a multi-parameter typeclass with a functional dependency.

    {-# LANGUAGE  MultiParamTypeClasses
                , FunctionalDependencies #-}
    
    class Monad m => MyClass d m | m -> d where
      getDs :: m [d]
    

    Then MyClass d m means that m is a Monad and that getDs can be used to produce a value of type m [d]. The purpose of the functional dependency is to indicate that m determines d. There is exactly one instance declaration of MyClass for each m, and that class must determine what d is. So you could write an instance like

    instance MyClass Int IO where ...
    

    (which would then be the only one allowed for IO), but not something wishy-washy like

    instance MyClass d IO where ...
    

    because the latter does not determine d.


    You might find my choice of argument order for MyClass a bit strange. There is some method to this madness. The main reason for it is that MyClass can be partially applied. Partially applying it to m isn't too useful, because that leaves a constraint that can only be satisfied by one possible argument. Partially applying it to d, on the other hand, could be useful, because there could be multiple choices of m for a given choice of d. Thus given {-# LANGUAGE ConstraintKinds #-},

    type MakesInts = MyClass Int
    

    is a potentially useful constraint. I believe using this order may also help avoid the need for UndecidableInstances in some cases, but I'm not certain.


    An alternative others mentioned is to use an associated type family.

    {-# LANGUAGE TypeFamilies #-}
    
    class Monad m => MyClass m where
      type Available m
      getDs :: m [Available m]
    

    This does essentially the same thing, but

    1. Anyone writing an instance of MyClass must include a line like, e.g., type Available IO = Int.

    2. Anyone placing a constraint on the Available type will need to use Available in the constraint, and will need FlexibleContexts (not a big deal).

    3. The type family offers access to the associated type.

    4. Type families are expressed in GHC Core (AKA System FC), so they're better-behaved than functional dependencies in some regards.

    1 (especially) and 2 are arguably downsides of the type family approach; 3 and 4 are upsides. This largely comes down to a question of taste.