1

Is it possible to use a GHC extension to define a new type class that generalizes to tuples of arbitrary length?

There have been a few questions already about the behavior of builtin classes in Prelude and Base (some classes support up to 15-element tuples, some up to 7) and about the (non-)possibility of extending these classes.

Prelude and Base behavior: Haskell Tuple Size Limit

extending Show with new definitions: Extend a Show instance to a Tuple of any size

I'm asking a slightly different question. If I'm making an entirely new type class, is it possible to add an instance rule that handles tuples of arbitrary length (possibly using a GHC extension)?

Here's an example of such a class called PartialOrder. I want to allow tuples of arbitrary size to be partially compared using the following rule

(a,b ... , z) <= (a1,b1, ... , z1) iff (a <= a1) && (b <= b1) && ... && (z <= z1)

Here's my first stab at a definition using the traditional "define the class for tuples up to some arbitrary size" approach.

Is there a GHC extension that can be used to write instance definitions that cover tuples of arbitrary length?

I think I can use Template Haskell or an external program to generate definitions in advance, but not generate them on demand like C++ templates.

-- Sets equipped with the (is_subset) operation are an example of a
-- partial order.
--
-- {} < {a}      less than
-- {} = {}       equal to
-- {a, b} > {b}  greater than
-- {a} ~ {b}     incomparable
--
-- in order to define a partial order we need a definition of (<=)

data PartialOrdering = POLessThan | POGreaterThan | POEqual | POIncomparable deriving (Eq, Show)

class PartialOrder a where
    lessThanEq :: a -> a -> Bool

instance PartialOrder PartialOrdering where
    lessThanEq POIncomparable _ = False
    lessThanEq _ POIncomparable = False

    -- with incomparables dealt with...
    lessThanEq POLessThan _ = True

    lessThanEq POEqual POLessThan = False
    lessThanEq POEqual _ = True

    lessThanEq POGreaterThan POGreaterThan = True
    lessThanEq POGreaterThan _ = False


-- note this is different from the semantics for Ord applied to tuples,
-- which uses lexicographic ordering.
--
-- (a,b) is less than or equal to (c,d) iff
-- a <= b and c <= d

-- 2 element tuple
instance (PartialOrder a, PartialOrder b) => PartialOrder (a, b) where
    lessThanEq (a,b) (c,d) = (lessThanEq a c) && (lessThanEq b d)

-- 3 element tuple
instance (PartialOrder a, PartialOrder b, PartialOrder c) => PartialOrder (a, b, c) where
    lessThanEq (a,b,c) (d,e,f) = (lessThanEq a d) && (lessThanEq b e) && (lessThanEq c f)

-- 4 element tuple
instance (PartialOrder a, PartialOrder b, PartialOrder c, PartialOrder d) => PartialOrder (a, b, c, d) where
    lessThanEq (a,b,c,d) (e,f,g,h) = (lessThanEq a e) && (lessThanEq b f) && (lessThanEq c g) && (lessThanEq d h)


-- etc.


main = putStrLn "hi"
Community
  • 1
  • 1
Greg Nisbet
  • 6,710
  • 3
  • 25
  • 65
  • 4
    Just a suggestion: Why don't you use a length-indexed list? Such data-types are isomorphic to tuples. It's easy to define it using GADTs. – Rodrigo Ribeiro Jan 16 '17 at 19:10

1 Answers1

4

The tuple types in Haskell don't really have any awareness of each other. Thankfully, for your particular case, you can solve your problem by using GHC.Generics. Then, you will actually be able to derive your PartialOrder class for any product type, not just tuples.

{-# LANGUAGE TypeOperators, DefaultSignatures, FlexibleContexts, 
             StandaloneDeriving, DeriveAnyClass
  #-}

import GHC.Generics
import Data.Function (on)

data PartialOrdering
  = POLessThan | POGreaterThan | POEqual | POIncomparable deriving (Eq, Show)

class PartialOrder a where
  lessThanEq :: a -> a -> Bool

  default lessThanEq :: (Generic a, GPartialOrder (Rep a)) => a -> a -> Bool
  lessThanEq = gLessThanEq `on` from


-- | Helper generic version of your class
class GPartialOrder f where
  gLessThanEq :: f a -> f a -> Bool

-- | Product types
instance (GPartialOrder a, GPartialOrder b) => GPartialOrder (a :*: b) where
  gLessThanEq (a1 :*: b1) (a2 :*: b2) = gLessThanEq a1 a2 && gLessThanEq b1 b2

-- | Unary type (empty product)
instance GPartialOrder U1 where
  gLessThanEq U1 U1 = True

-- | Meta information on type
instance (GPartialOrder a) => GPartialOrder (M1 i c a) where
  gLessThanEq (M1 a1) (M1 a2) = gLessThanEq a1 a2

-- | Single type
instance (PartialOrder a) => GPartialOrder (K1 i a) where
  gLessThanEq (K1 x1) (K1 x2) = lessThanEq x1 x2

With all that set up, one can automatically derive your class (with -XDeriveAnyClass enabled) if one derives Generic (which can be done with -XDeriveGeneric). Tuples types are already instances of generics, so with -XStandaloneDeriving, you can retro-actively derive instances of Partial Order. All of the below work

deriving instance (PartialOrder a, PartialOrder b) => PartialOrder (a,b)
deriving instance (PartialOrder a, PartialOrder b, PartialOrder c) => PartialOrder (a,b,c)
-- and so on...

data MyProduct a b = MyProduct a b deriving (Generic, PartialOrder) 

After that, you can use your class, as expected:

ghci> (POLessThan, POLessThan) `lessThanEq` (POEqual, POEqual)
True
ghci> (POLessThan, POEqual) `lessThanEq` (POEqual, POEqual)
True
ghci> (POLessThan, POEqual) `lessThanEq` (POEqual, POLessThan)
False
Alec
  • 31,829
  • 7
  • 67
  • 114