Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

When using MultiParamTypeClasses, do you need to use every type in every class function

Tags:

haskell

When I use MultiParamTypeClasses, I can create class functions that ignore one of the type parameters (ie- like "identity" below).

{-# LANGUAGE MultiParamTypeClasses #-}

data Add = Add
data Mul = Mul

class Test a b where
    identity::a

instance Test Int Add where
    identity = 0

instance Test Int Mul where
    identity = 1

(this is a stripped down version, of course in the full program there would be other functions that would use "b").

The example compiles, but I can never access identity!

main = do
    putStrLn (show (identity::Int))

leads to "No instance for (Test Int b0) arising from a use of 'identity'.

Is there a way to access identity? If not, shouldn't the compiler forbid me from ever creating a class function that doesn't use all type parameters?

like image 209
jamshidh Avatar asked Nov 20 '13 19:11

jamshidh


1 Answers

If not, shouldn't the compiler forbid me from ever creating a class function that doesn't use all type parameters?

Perhaps. Indeed you'll never be able to use such a class method. But as the error occurs always at compile-time, it's not really dangerous.

Fixes that work in some similar cases (not in yours, though):

  • Make the undetermined type variable functionally dependent on one of the others.

    {-# LANGUAGE FunctionalDependencies #-}
    class Group_FD g p | g->p where
      identity :: g
    

    This could be used perhaps like

    data Nat = One | Succ Nat
    
    instance Group_FD Nat Mult where
      identity = One
    
    instance Group_FD Int Add where
      identity = 0
    

    But it's obviously not possible to make multiple instances with the same g element this way.

  • Define a seperate class with only one parameter for the methods that depend only on that. Then make this class a constraint ("superclass") on the other one, to "import" methods:

    class Identity i where
      identity :: i
    class (Identity i) => Test i y
    

    that's no use at all for your application though, since you want the behaviour of identity to depend on the Phantom type variable.

To achieve your goal, you must somehow pass the information of which instance you want. One way to achieve this is phantom arguments:

class Group_PA g p where
  identity :: p -> g

instance Group_PA Int Add where
  identity _ = 0

instance Group_PA Int Mult where
  identity _ = 1

You could then use that like

GHCi> identity Add :: Int
0
GHCi> identity Mult :: Int
1

Perhaps more idiomatic would actually be to make the flag types empty

{-# LANGUAGE EmptyDataDecls #-}
data Add
data Mult

GHCi> identity (undefined :: Add) :: Int
0
GHCi> identity (undefined :: Mult) :: Int
1

This makes it clearer that the phantom argument actually carries no runtime information, just controls what instance the compiler chooses.

Admittedly, this is pretty ugly.

The right™ solution is to make newtype wrappers to contain the phantom information. In fact, such wrappers are already in the standard libraries: Sum and Product.

like image 68
leftaroundabout Avatar answered Nov 15 '22 05:11

leftaroundabout