Declaring a "subclass" in Haskell

2.2k Views Asked by At

I have troubles with the following simple code in Haskell:

import Prelude hiding (cycle).

class ICycle a where
    cycle :: a -> a

instance ICycle [a] where
    cycle [] = []
    cycle (x:xs) = xs ++ [x]

instance ICycle Bool where
    cycle True = False
    cycle False = True

instance Num a => ICycle a where
    cycle n = n+1

main = do
    print $ cycle $ [1,2,3]
    print $ cycle $ True
    print $ cycle $ 42

Here the first two instance declarations work as expected, but the third one triggers various sorts of errors depending on flag combinations.

I know that Num a is no shorter than ICycle a and hence the compiler can not finish type checking. In the examples, I have seen this is circumvented by either making the right-hand side a bigger term or by declaring a class of interest a subclass of other classes in the first place. Here, to the contrary, I essentially want to declare an existing class to be a subclass of a new one.

I wonder if there are objections against this kind of use of type classes. Or else, if there is a natural solution.

2

There are 2 best solutions below

1
On

For this particular example, I think you're best off using a newtype to wrap the instance:

{-# LANGUAGE GeneralizedNewtypeDeriving #-}
import Prelude hiding (cycle)

class ICycle a where
    cycle :: a -> a

newtype Succ a = Succ { runSucc :: a }
  deriving (Num, Eq, Ord, Bounded, Enum, Show, Read)

newtype Pred a = Pred { runPred :: a }
  deriving (Num, Eq, Ord, Bounded, Enum, Show, Read)

instance Enum a => ICycle (Succ a) where
    cycle = Succ . succ . runSucc

instance Enum a => ICycle (Pred a) where
    cycle = Pred . pred . runPred

main = do
    print $ cycle $ (42 :: Succ Int)
    print $ cycle $ (42 :: Pred Int)

There are multiple ways one could cycle through numbers - by succ, by pred, by doubling, by halving. The advantage of using a newtype for the instance (making the RHS "bigger", as you noted in your question) is that it lets us have ALL of them.

The standard library does the same trick with Product and Sum for Monoid.

Looking at it another way, if it were possible to define a new superclass for Num, adding a default implementation for all instances of Num, then you'd be taking that choice away from all those implementations. In possibly a way that doesn't make sense.

3
On

According to the Haskell report 2010, chapter 4, Declarations and Bindings, the thing that is to be defined as an instance needs to be a type constructor. Thus,

instance Num a => ICycle a where
    ...

is invalid, because a is a type variable, not a type constructor.

Therefore the valid way to do it is, unfortunately, type by type. One has to say:

instance ICycle Int where
    ...

instance ICycle Double where
    ...

and so on.