DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

Cover image for Fun with Typeclasses
Riccardo Odone
Riccardo Odone

Posted on • Updated on • Originally published at odone.io

Fun with Typeclasses

You can keep reading here or jump to my blog to get the full experience, including the wonderful pink, blue and white palette.


I've recently started reading "Haskell in Depth". The first part of the book covers the basics of the language. Here, among other cool things I've found an interesting example using two existing typeclasses (i.e. Enum, Bounded) and two new typeclasses that seem to make a lot of sense (i.e. BoundedEnum, CyclicEnum).

Let's see that stuff in action. First of all, we need a type to play with:

data Direction
    = North
    | East
    | South
    | West
    deriving (Show, Enum, Bounded, BoundedEnum, Eq, CyclicEnum)
Enter fullscreen mode Exit fullscreen mode

The instance of Enum enables us to enumerate the values of type Direction. In other words, we can move to the next value or previous value by using succ and pred. Not only that, by making Direction enumerable, we can use the .. operator:

main :: IO ()
main = do
  print $ succ North
  -- East

  print $ pred East
  -- North

  print [North ..]
  -- [North,East,South,West]

  print [East ..]
  -- [East,South,West]

  print [East .. South]
  -- [East,South]
Enter fullscreen mode Exit fullscreen mode

The instance Bounded allows us to generically call the lower-bound and upper-bound minBound and maxBound:

main :: IO ()
main = do
  let
      allDirections :: [Direction]
      allDirections = [minBound .. maxBound]
  print allDirections
  -- [North,East,South,West]
Enter fullscreen mode Exit fullscreen mode

Now, in Haskell an Enum could be not Bounded and the other way around. In case a type is both, it makes sense to define a BoundedEnum typeclass and instance. That way, we can abstract the range [minBound .. maxBound]:

class (Enum a, Bounded a) => BoundedEnum a where
    range :: [a]
    range = enumFrom minBound

main :: IO ()
main = do
  let
      allDirections' :: [Direction]
      allDirections' = range
  print $ allDirections'
  -- [North,East,South,West]
Enter fullscreen mode Exit fullscreen mode

There is still a problem though. In fact, the following raises a runtime exception:

main :: IO ()
main = do
  print $ pred North
  -- "tried to take `pred' of first tag in enumeration"
Enter fullscreen mode Exit fullscreen mode

As a matter of fact, the enumeration does not wrap around and North is the first value in the enum! We can solve that by defining a CyclicEnum typeclass and instance:

class (Eq a, Enum a, Bounded a) => CyclicEnum a where
    cpred :: a -> a
    cpred d
        | d == minBound = maxBound
        | otherwise = pred d

    csucc :: a -> a
    csucc d
        | d == maxBound = minBound
        | otherwise = succ d

main :: IO ()
main = do
  print $ cpred North
  -- West
Enter fullscreen mode Exit fullscreen mode

Get the latest content via email from me personally. Reply with your thoughts. Let's learn from each other. Subscribe to my PinkLetter!

Top comments (0)

The Complete Guide to Full Stack Web3 Development

>> Check out this classic DEV post <<