Skip to content

Range Comprehensions

In the next chapter, we will discuss various type classes that capture the of operations supported by different types. One such type class is Enum. That's the class of types that support the enumeration of values. This includes integers, Booleans, characters, and a number of other types. Four functions that the types in the Enum type class support are enumFrom, enumFromThen, enumFromTo, and enumFromThenTo. These functions are used to implement range comprehensions, that is, comprehensions that specify lists containing all values in a given range.

Bounded Ranges

First, we have the comprehension [x..y]. This is the list of all values between x and y. For example,

GHCi
>>> [1..20]
[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]
>>> ['A'..'Z']
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"

Since strings are lists of characters in Haskell, the second example prints the list it produces as the string containing all uppercase letters.

The Haskell compiler translates the comprehension [x..y] into the function call enumFromTo x y, and a correct implementation of enumFromTo x y generates the list of all values between x and y.

Infinite Lists (At Least Potentially)

Next, we have the comprehension [x..]. For example, we have seen the lists [1..] and [2..] in some code examples before. [1..] is the list of all positive integers. [2..] is the list of all integers from 2 onwards. In general, [x..] is the list of all values starting from x:

GHCi
>> take 10 [100..]
[100,101,102,103,104,105,106,107,108,109]
>>> take 10 ['a'..]
"abcdefghij"

I had to use take 10 to extract only the first 10 values of the infinite lists [100..] and ['a'..]. I don't particularly enjoy looking at a never ending stream of numbers or characters on my screen.

The Haskell compiler translates the comprehension [x..] into the function call enumFrom x, and a correct implementation of enumFrom x generates the list of all values starting from x. Note that this list may be infinite, as it is in the case of the Integer type. For bounded types, the list will be finite because there are only finitely many values of such a type. For example,1

GHCi
>>> [False ..]
[False,True]

Bounded Ranges with a Custom Increments

In Python, we can provide a third argument to the range expression to count up (or down) in increments other than 1. For example,

Python
>>> [x for x in range(1,11,2)]
[1, 3, 5, 7, 9]

In Haskell, we specify the increment by which to count up implicitly. We specify the first two elements in the list, and the difference between them determines the increment by which we count. For example, to enumerate all odd numbers between 1 and 20, we start by listing the first two numbers in this list, 1 and 3, and then the list keeps counting up in increments of 3 – 1 = 2:

GHCi
>>> [1,3..20]
[1,3,5,7,9,11,13,15,17,19]
>>> ['a','c'..'z']
"acegikmoqsuwy"

We can also count backwards:

GHCi
>>> [10,9..1]
[10,9,8,7,6,5,4,3,2,1]

The "increment" here is 9 – 10 = –1, so we count down by 1s.

In general, the Haskell compiler translates the comprehension [x,y..z] into a call to the function enumFromThenTo x y z, which generates the list of all numbers starting at x and counting up in increments of y - x up to z. If the increment is negative, then this counts down instead.

Infinite Lists with Custom Increments

And, finally—you might have guessed—we have a list comprehension that generates an infinite list but counts in increments other than 1:

GHCi
>>> take 10 [1,4..]
[1,4,7,10,13,16,19,22,25,28]
>>> take 5 ['a','e'..]
"aeimq"

In general, the list comprehension [x,y..] generates the list of all values starting at x and counting up in increments of y - x. The Haskell compiler translates this into the call to the function enumFromThen x y. Again, if y - x is negative, then this counts down instead of up.


  1. When using numbers and characters in a list comprehensions, you can write [1 .. 10], [1..10], [100 ..] or [100..]. It is unimportant whether there's a space before or after ... When using types whose values have names such as True or False, then the compiler won't accept [False..] or [False..True], whereas it does accept [False ..], [False .. True], and even [False ..True]. The reason is that, as we will discuss when talking about modules, we can distinguish between functions with the same name imported from different modules by prefixing the function name with the name of the module, as in Data.List.foldl and Data.Vector.foldl. Thus, when the compiler sees a constant followed immediately by a period, it assumes that the constant is the name of some module (e.g., Data) and that the period should be followed by the name of a submodule (e.g., List or Vector), a function name (e.g., foldl), a type name, etc. If we don't put a space between False and .. in the expression False.., then the compiler interprets False as a module name, and the first period as the separators between the components of a qualified name, and then it doesn't know what to do with the second period. By separating False from .. using a space, we tell the compiler that it should parse the two periods as .., and False as the constant False