Enum ranges

Haskell has a convenient notation for writing numeric ranges. It also works with a lot of types other than numbers, including types that you define yourself.

This example uses type applications.

{-# LANGUAGE TypeApplications #-}

Int8 is the type for signed eight-bit integers. We will use this as an example of a bounded type.

import Data.Int (Int8)

The data keyword introduces the definition of a new datatype. The Rank type represents the ranks in a deck of playing cards, as you might need to do in writing a poker or solitaire program. The values are listed here in an order that represents their hierarchy in poker.

The deriving clause tells the compiler to create three things for this type:

  • Bounded creates an upper and lower boundary for the type, based on the order in which the values are listed (two is the lowest rank, and ace is the highest);
  • Enum helps the compiler understand this type as a series of values, with predecessors and successors, allowing us to use functions for creating ranges or enumerated lists; and
  • Show provides a conversion between this type’s values and human-readable strings we can print to the terminal.
data Rank =
      Rank2
    | Rank3
    | Rank4
    | Rank5
    | Rank6
    | Rank7
    | Rank8
    | Rank9
    | Rank10
    | Jack
    | Queen
    | King
    | Ace
    deriving (Bounded, Enum, Show)

In these first two examples, we use range syntax to enumerate lists of numbers and characters. Range syntax is available only for datatypes that have Enum instances. We give the starting point and the ending point of the range we want to construct, and the two dots in between signify enumerate from that start to that finish (inclusive).

main =
  do
    putStrLn (show [3 .. 8])
    putStrLn (show ['a' .. 'z'])

Since we gave our Rank type an instance of Enum, we can use range syntax to create lists of playing card ranks.

    putStrLn (show [Rank2 .. Rank10])

You do not have to give an ending point for the enumeration; here we print a list from Jack to the highest rank.

    putStrLn (show [Jack ..])

minBound and maxBound give the lowest and highest values of a type with a Bounded instance.

These expressions are polymorphic; they have a type parameter. We can use type application (with the @ keyword) to specify which type we want. Usually, the compiler figures out how to fill type parameters automatically. Explicit type application is only needed in situations where the the type would otherwise be ambiguous.

    putStrLn (show (minBound @Rank))
    putStrLn (show (maxBound @Rank))

If we change the type argument, the same expression produces a different result.

    putStrLn (show (minBound @Int8))
    putStrLn (show (maxBound @Int8))

To list all values of the type, use the boundaries determined by deriving Bounded to construct a range from the lower bound to the upper bound.

    putStrLn (show [minBound @Rank .. maxBound @Rank])
$ runhaskell enum-ranges.hs
[3,4,5,6,7,8]

When printing a list of characters, the result appears as a string ("abc...") rather than as a list (['a', b', 'c', ...]).

"abcdefghijklmnopqrstuvwxyz"
[Rank2,Rank3,Rank4,Rank5,Rank6,Rank7,Rank8,Rank9,Rank10]
[Jack,Queen,King,Ace]
Rank2
Ace
-128
127
[Rank2,Rank3,Rank4,Rank5,Rank6,Rank7,Rank8,Rank9,Rank10,
  Jack,Queen,King,Ace]