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,
>>> [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
:
>> 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
>>> [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,
>>> [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:
>>> [1,3..20]
[1,3,5,7,9,11,13,15,17,19]
>>> ['a','c'..'z']
"acegikmoqsuwy"
We can also count backwards:
>>> [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:
>>> 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.
-
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 asTrue
orFalse
, 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 inData.List.foldl
andData.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
orVector
), a function name (e.g.,foldl
), a type name, etc. If we don't put a space betweenFalse
and..
in the expressionFalse..
, then the compiler interpretsFalse
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 separatingFalse
from..
using a space, we tell the compiler that it should parse the two periods as..
, andFalse
as the constantFalse
. ↩