List comprehension
The powers of list comprehension in a functional programming language are best illustrated when compared alongside their imperative language equivalents
If I wish to create a list of all the odd numbers between 1 and 100
In C++, I could do:
int len = 50;
int[len] m;
counter = 0;
for (int i = 1; i <= 100; ++i)
{
if (i % 2 == 1)
{
m[counter] = i;
++counter;
}
}
In Haskell I could do:
m = [ x | x <- [1..100], x `mod` 2 == 1]
main = print $ m
The C++ was a lot of code, compared to [ x | x <- [1..100], x
mod 2 == 1]
!
Also, notice that I did not have to predict the number of elements that there would be in the final array, as I had to do in C++.
We can even replace the modulo arithmetic with a the built in odd
.
m = [ x | x <- [1..100], odd x]
main = print $ m
The way this works is everything before the pipe, |
,
is what is getting added to the list.
Everything after the pipe describes how that gets added.
One could read that in prose as, "Let the list m be contain elements, where each element is and integer between 1 and 100, and is odd."
The power, and beauty, of this, is that using list comprehensions, one feels more like they are expressing what they want, instead of expressing how to get what they want.
FizzBuzz
A classic beginners programming exercise is FizzBuzz: "Write a program that prints out all of the numbers between 1 and 100. If a number is divisble by 5 print FIZZ instead of that number. If a number is divisble by 3 print BUZZ instead of that number. If a number is divisble by both 3 and 5 print FIZZBUZZ instead of that number."
Try writing this in C++, or some other imperative language.
In C++:
for (int i = 1; i <= 100; ++i)
{
if (i % 15 == 0)
{
std::cout <<"FIZZBUZZ" <<std::endl;
}
else if (i % 5 == 0)
{
std::cout <<"FIZZ" <<std::endl;
}
else if (i % 3 == 0)
{
std::cout <<"BUZZ" <<std::endl;
}
else
{
std::cout <<i <<std::endl;
}
}
Now how would you do the same in Haskell, using list comprehensions?
Firstly, let us just take the list comprehension above,
[ x | x <- [1..100], odd x]
and add an if .. then .. else
block to it
just for FIZZBUZZ
.
m = [ if (x `mod` 15 == 0) then "FIZZBUZZ" else (show x) | x <- [1..100]]
main = print $ m
Note that show
as used here, converts the integer to a string.
This is necessary because, remember that a list is homogenous -
all of its elements must be of the same type.
Now try adding FIZZ
into the mix.
Hint: Nest the new if .. then .. else
block within the else
block of the existing else
.
m = [ if (x `mod` 15 == 0) then "FIZZBUZZ" else (if (x `mod` 5 == 0) then "FIZZ" else (show x)) | x <- [1..100]]
main = print $ m
Now add BUZZ
into the mix.
`` active haskell
m = [ if (x
mod 15 == 0) then "FIZZBUZZ" else (if (x
mod 5 == 0) then "FIZZ" else (if (x
mod` 3 == 0) then "BUZZ" else (show x))) | x <- [1..100]]
main = print $ m
Now that is starting to a look a little unwieldy, isn't it? All we need to do now is to make it look neater, and we will do that by splitting into multiple lines.
Exercise:
Define a function fizzbuzz
which you will use as the filter.
Define this function using guards, as we have previously
in our recursive list functions.
fizzbuzz x
-- your code here
m = [ fizzbuzz x | x <- [1..100] ]
main = print $ m
fizzbuzz x
| (x `mod` 15 == 0) = "FIZZBUZZ"
| (x `mod` 5 == 0) = "FIZZ"
| (x `mod` 3 == 0) = "BUZZ"
| otherwise = show x
m = [ fizzbuzz x | x <- [1..100] ]
main = print $ m
Note that fizzbuzz
is not a recursive function,
like our listMax
and listMin
in our previous exercises.
Thus it may seem that this list comprehension has been
accomplished through iteration.
If you look under the hood, however, it really is
still recursionm. This happens in the list comprehension (and not in the fizzbuzz
function). Tail-order recursion, which is compiled into an iteration by the Haskell compiler.
Although not immediately obvious, the Haskell compiler
also would recognise our implementations for
listMax
and listMin
as tail-order recursion,
and compile it iteratively too.
This is a compiler optimisation, and thus as functional programmers, should be aware of that this happens. We should not, however, not care about it, and instead just let the compiler do its thing.