guhou
guhou

Reputation: 1732

Adding class constraints to typeclass instance

I'm trying to implement the Cantor Pairing Function, as an instance of a generic Pair typeclass, as so:

module Pair (Pair, CantorPair) where

-- Pair interface
class Pair p where
    pi :: a -> a -> p a
    k :: p a -> a
    l :: p a -> a

-- Wrapper for typing
newtype CantorPair a = P { unP :: a }

-- Assume two functions with signatures:
cantorPair :: Integral a => a -> a -> CantorPair a
cantorUnpair :: Integral a => CantorPair a -> (a, a)

-- I need to somehow add an Integral a constraint to this instance,
-- but I can't work out how to do it.
instance Pair CantorPair where
    pi = cantorPair
    k = fst . cantorUnpair
    l = snd . cantorUnpair

How can I add the appropriate Integral constraint to the instance? I have a vague feeling I might need to modify the Pair interface itself, but not sure how to go about this.

Upvotes: 3

Views: 729

Answers (3)

Daniel Wagner
Daniel Wagner

Reputation: 152682

If you have access to the class definition, you can add an Integral constraint to the pi, k, and l methods. This is a bit unsatisfactory, though: there's nothing saying that Integral is going to be the right constraint for all instances, and you after all don't want to reject some instances just because you didn't have enough foresight. So, here's one generalization: we'll allow the constraint to vary in each instance.

{-# LANGUAGE ConstraintKinds, TypeFamilies #-}
import GHC.Exts

newtype CantorPair a = P { unP :: a }
cantorPair :: Integral a => a -> a -> CantorPair a
cantorUnpair :: Integral a => CantorPair a -> (a, a)
cantorPair = undefined
cantorUnpair = undefined

class Pair p where
    type Ctxt p a :: Constraint
    pi :: Ctxt p a => a -> a -> p a
    k  :: Ctxt p a => p a -> a
    l  :: Ctxt p a => p a -> a

instance Pair CantorPair where
    type Ctxt CantorPair a = Integral a
    pi = cantorPair
    k  = fst . cantorUnpair
    l  = snd . cantorUnpair

-- just for fun
data DataPair a = DataPair a a

instance Pair DataPair where
    type Ctxt DataPair a = ()
    pi = DataPair
    k (DataPair a _) = a
    l (DataPair _ a) = a

-- this one made GHC panic! neat, I get to file a bug
data Unit a = Unit

instance Pair Unit where
    type Ctxt Unit a = a ~ ()
    pi _ _ = Unit
    k _ = ()
    l _ = ()

Upvotes: 2

kennytm
kennytm

Reputation: 523154

This solution uses type families, so you need -XTypeFamilies. We put the type constraint on the type itself, instead of the type constructor:

class Pair p where
    type First p :: *
    type Second p :: *
    pi :: First p -> Second p -> p
    k :: p -> First p
    l :: p -> Second p

and then we create the instances like:

instance Integral a => Pair (CantorPair a) where
    type First (CantorPair a) = a
    type Second (CantorPair a) = a
    pi = cantorPair
    k = fst . cantorUnpair
    l = snd . cantorUnpair

instance Pair (a, b) where
    type First (a, b) = a
    type Second (a, b) = b
    pi = (,)
    k = fst
    l = snd

Upvotes: 1

Tikhon Jelvis
Tikhon Jelvis

Reputation: 68152

Do you want all pairs to always contain integral elements? In this case, you could add the constraint to the signatures of the methods:

class Pair p where
  pi :: Integral i => i -> i -> p i
  k :: Integral i => p i -> i
  l :: Integral i => p i -> i

This will make you pair class less general but will ensure that your CantorPair type could be a part of it.

If you want to keep your Pair class somewhat general, you could use a multi parameter type class. (This will require two extensions: MultiParamTypeClasses and FlexibleInstances.)

class Pair p a where
  pi :: a -> a -> p a
  k :: p a -> a
  l :: p a -> a

instance Integral i => Pair CantorPair i where
    pi = cantorPair
    k = fst . cantorUnpair
    l = snd . cantorUnpair

I don't know if this is necessarily the best option from a design standpoint, but it's a good way to learn about how multiparamter type classes work. (Which, admittedly, is fairly simple.)

Upvotes: 1

Related Questions