Haskell/Lists II

Earlier, we learned that Haskell builds lists via the cons operator  and the empty list. We saw how we can work on lists bit by bit using a combination of recursion and pattern matching. In this chapter and the next, we will consider more in-depth techniques for list processing and discover some new notation. We will get our first taste of Haskell features like infinite lists, list comprehensions, and higher-order functions.

Rebuilding lists
Here's a function that doubles every element from a list of integers:

Here, the base case is the empty list which evaluates to an empty list. In the recursive case, doubleList builds up a new list by using. The first element of this new list is twice the head of the argument, and we obtain the rest of the result by recursively calling doubleList on the tail of the argument. When the tail gets to an empty list, the base case will be invoked and recursion will stop.

Let's study the evaluation of an example expression:

We can work it out longhand by substituting the argument into the function definition, just like schoolbook algebra:

Thus, we rebuilt the original list replacing every element by its double.

In this longhand evaluation exercise, the moment at which we choose to evaluate the multiplications does not affect the result. We could just as well have evaluated the doublings immediately after each recursive call of doubleList.

Haskell uses this flexibility on evaluation order in some important ways. As a pure functional programming language, the compiler makes most of the decisions about when to actually evaluate things. As a lazy language, Haskell usually defers evaluation until a final value is needed (which may sometimes never occur). From the programmer's point of view, evaluation order rarely matters.

Generalizing
To triple each element in a list, we could follow the same strategy as with :

But we don't want to write a new list-multiplying function for every different multiplier (such as multiplying the elements of a list by 4, 8, 17 etc.). So, let's make a general function to allow multiplication by any number. Our new function will take two arguments: the multiplicand as well as a list of s to multiply:

This example deploys  as a "don't care" pattern. The multiplicand is not used for the base case, so we ignore that argument instead of giving it a name (like,  , or  ).

We can test  to see that it works as expected:

Generalizing even further
In this chapter, we started with a function constrained to multiplying the elements by. Then, we recognized that we could avoid hard-coding a new function for each multiplicand by making  to easily use any. Now, what if we wanted a different operator such as addition or to compute the square of each element?

We can generalize still further using a key functionality of Haskell. However, because the solution can seem surprising, we will approach it in a somewhat roundabout way. Consider the type signature of :

The first thing to know is that the  arrow in type signatures is right associative. That means we can read this signature as:

How should we understand that? It tells us that  is a function that takes one   argument and evaluates to another function. The function thus returned, then, takes a list of s and returns another list of  s.

Consider our old  function redefined in terms of  :

Writing this way, we can clearly cancel out the `xs`:

This definition style (with no argument variables) is called point-free style. Our definition now says that applying only one argument to  doesn't fail to evaluate, rather it gives us a more specific function of type   instead of finishing with a final   value.

We now see that functions in Haskell behave much like any other value. Functions can return other functions, and functions can stand alone as objects without mentioning their arguments. Functions seem almost like normal constants. Can we use functions themselves as arguments even? Yes, and that's the key to the next step in generalizing. We need a function that takes any other appropriate function and applies the given function to the elements of a list:

With, we can take any   function and apply it to the elements of a list of  s. We can thus use this generalized function to redefine  :

That uses the  function with a single initial argument to create a new function which is ready to take one more argument (which, in this use case, will come from the numbers in a given list).

Currying
If all this abstraction confuses you, consider a concrete example: When we multiply 5 * 7 in Haskell, the  function doesn't just take two arguments at once, it actually first takes the 5 and returns a new 5* function; and that new function then takes a second argument and multiplies that by 5. So, for our example, we then give the 7 as an argument to the 5* function, and that returns us our final evaluated number (35).

So, all functions in Haskell really take only one argument. However, when we know how many intermediate functions we will generate to reach a final result, we can treat functions as if they take many arguments. The number of arguments we generally talk about functions taking is actually the number of one-argument functions we get between the first argument and a final, non-functional result value.

The process of creating intermediate functions when feeding arguments into a complex function is called currying (named after Haskell Curry, also the namesake of the Haskell programming language).

The function
While  has type , the definition itself contains nothing specific to integers. To use the same logic with other types of lists, we could define versions such as,   and so on. They would all have the same definition but different type signatures. We can avoid all that redundancy with the final step in generalizing: making a fully polymorphic version with signature. Prelude already has this function, and it is called :

With, we can effortlessly implement functions as different as...

... and...

is the general solution for applying a function to each and every element of a list. Our original  problem was simply a specific version of. Functions like  which take other functions as arguments are called higher-order functions. We will learn about more higher-order functions for list processing in the next chapter.

Tips and Tricks
A few miscellaneous notes about lists in Haskell:

Dot Dot Notation
Haskell has a convenient shorthand for writing ordered lists of regularly-spaced integers. Some examples to illustrate it:

The same notation works with characters and even with floating point numbers. Unfortunately, floating-point numbers are problematic due to rounding errors. Try this:

Infinite Lists
Thanks to lazy evaluation, Haskell lists can be infinite. For example, the following generates the infinite list of integers starting with 1:

(If you try this in GHCi, remember you can stop an evaluation with Ctrl-c).

The same effect could be achieved with a recursive function:

Infinite lists are useful in practice because Haskell's lazy evaluation never actually evaluates more than it needs at any given moment. In most cases, we can treat an infinite list like an ordinary one. The program will only go into an infinite loop when evaluation requires all the values in the list. So, we can't sort or print an infinite list, but:

will define "evens" to be the infinite list [2,4,6,8..], and we can then pass "evens" into other functions that only need to evaluate part of the list for their final result. Haskell will know to only use the portion of the infinite list needed in the end.

Compared to hard-coding a long finite list, it's often more convenient to define an infinite list and then take the first few items. An infinite list can also be a handy alternative to the traditional endless loop at the top level of an interactive program.

A note about and
Given the choice of using either the  pattern or  /  to split lists, pattern matching is almost always preferable. It may be tempting to use  and   due to simplicity and terseness, but it is too easy to forget that they fail on empty lists (and runtime crashes are never good). We do have a Prelude function,, which returns   for empty lists and   otherwise, so that provides a sane way of checking for empty lists without pattern matching; but matching an empty list tends to be cleaner and clearer than the corresponding if-then-else expression using.