Lists in Racket

Every programming language has some method to store a collection of items. These generally fall into two broad categories lists and arrays. An array tends to have a fixed size. A list can be resized as needed. Racket has a special kind of list designed specifically for recursive programming.

What is a List

The list is a fundamental data type in Racket and other Lisp related languages. It is inspired by a traditional pen and paper concepts, matched to the limits of computers. A list is just a collection of items stored in a specific order.

Take a grocery store list as an example.

  1. 1lb carrots

  2. 3 boxes of cereal

  3. 4 chicken thighs

  4. 1 gallon milk

The items in the list are always in an order. The first item is the one at the top of the list. In this case, 1lb carrots is the first item. The rest of the items are everything in the list with the first element removed.

  1. 3 boxes of cereal

  2. 4 chicken thighs

  3. 1 gallon milk

If we put the first item into our shopping cart, we are left with the rest of the items to find in the store.

With just these two terms, we can refer to any point in the list.

  • First: The current first item in a list.

  • Rest: All remaining elements after the first item.

This gives us a recursive understanding of lists.

How would we talk about the second element in the list? It is clearly not the first element, so it is part of the rest. Specifically, it is the first element of the rest of the list. We remove the real first element by saying rest, then the original second element becomes the new first element.

  1. 1lb carrots First Element

  2. 3 boxes of cereal First of the Rest

  3. 4 chicken thighs First of the Rest of the Rest

  4. 1 gallon milk First of the Rest of the Rest of the Rest

Why is the third element in the list called the First of the Rest of the Rest? We unpack this statement backwards. The of the rest command means remove the first element from the list.

  1. 3 boxes of cereal

  2. 4 chicken thighs

  3. 1 gallon milk

There is a second of the rest, we again remove the first element.

  1. 4 chicken thighs

  2. 1 gallon milk

Lastly, we take the first element of the remaining values. In this case, the first element is now “4 chicken thighs”. It was originally third, but since we removed two elements it has become the first.

To get the \(n\)-the element, we need to take the rest of the list \(n-1\) times. Then the element we want will have moved to the first position.

What if we want to add to a list? In Racket, this always happens in the front. The only place we can insert new elements is into the very first position. This pushes all the other values back by one.

The command to add new elements is the cons command. It is short for construct. We always construct a new list by adding one existing element to an old list. We start with our original list of four items.

  1. 1lb carrots

  2. 3 boxes of cereal

  3. 4 chicken thighs

  4. 1 gallon milk

We want to construct a new list by adding “1 box rice” to the list. The new element goes in the front. All other elements are pushed back one position.

  1. 1 box rice

  2. 1lb carrots

  3. 3 boxes of cereal

  4. 4 chicken thighs

  5. 1 gallon milk

We always add to the front for two reasons. First, it is computationally efficient. Using pointers, we can put a new element in front of the old elements without moving anything else around. The old list stays exactly where is was, the new value just needs to store a pointer to the old data. Secondly, it is more naturally recursive. When building lists and taking lists apart, we always work from the front.

How Lists Work

The commands available for lists are very closely related to hardware. The are based on pointers.

Note

A pointer is a memory address on a computer. It tells us where in our memory a value or data structure lives. Pointers are generally very small compared to data structures. We can always travel to the memory location and get any data we need.

If a list is empty, that means it has no elements in it. We call the empty list null. The null value is a non-existent place in memory. It is traditionally equal to 0, but not always. It is used to say “we don’t actually have anything in memory yet.” Since the empty list has no values to store in memory, it meets this definition. It is a list whose pointer doesn’t get us to any data.

If a list is not null, then it has values. At any given time, we only store two pointers related to the list. Everything else can be accessed by using these values.

  • first - a pointer to the first value in the list.

  • rest - a pointer to the rest of the list.

The idea of thinking about lists in terms of only the first and rest was inspired by hardware. On the IBM 704 a single word of memory was broken up into two component parts [Wikipediacontributors21a]. The first part was used for the first part of the list.. The second part was used for a pointer to the rest of the list. When the word was loaded into memory, these two values would go to their respective registers on the processor.

The developers of the original LISP realized this could be used to store a list. We could store information about the first value in the first section and a pointer to the rest of the list in the rest section. This would allow programmers to generate lists in a way that naturally matched the hardware of the system.

Computers are much bigger and have more memory now, but these efficiencies still exist. By only needing to load one element at a time, we can work with massively long lists without wasting memory. Regardless of how big the list is, the process is always only focused on a very small area.

A basic implementation of the Racket list functions are built in Python below. Of course, the practical implementation is defined at a most lower level in assembly in Racket. These functions need to be exceptionally efficient. They are the foundation on which almost every program is built. This Python implementation should give you a feel for the underlying concepts.

We start out by making a class to replicate the word of memory. Since everything in Python can be a pointer, we just need two attributes for our two values.

class node:
    def __init__(self):
        self.__first = None
        self.__rest = None

To work with the list, we need some predicates. It is useful to know if the list is null or not. We can check this by seeing if the value is set or not. Python calls the empty value None.

def isNull(L):
    return L==None

We might also want to ask if the list contains any nodes. Python has a command isinstance that can solve this problem. We call this command isCons because nodes can only be created by using the construct command. We are asking “was this built by construct?”

def isCons(L):
    return isinstance(L,node)

Python and Racket are both dynamically typed languages. The programmer never tells the code what type a variable has. It has to be determined dynamically. We might want to ask “is this a list?” A variable could also store a string or number. A value is a list if it is either null or contains a node.

def isList(L):
    return isNull(L) or isCons(L)

We also want to be able to ask what the first value is. As long as the list is not null, this is easy to get from our attribute.

def first(L):
    if isCons(L):
        return L.__first
    raise error("No first!")

The code to get the rest of the list is almost a mirror image. We just select a different attribute.

def rest(L):
    if isCons(L):
        return L.__rest
    raise error("No rest!")

When we want to create a node, we allocate the memory then set values. The construct command has two inputs. We give it a value we want added to the list and the current list. It places this value at the front of the old list. Since all we have for a list is a pointer, we just need to store the value and place the pointer in the rest section of the node.

def cons(a,L):
    n = node()
    n.__first = a
    n.__rest = L
    return n

These six commands form the basis for all list work in Racket. As you can see, none contain any loops. They are very simple functions that can easily be implemented at a very low level for a specific processor. These means they can all run exceptionally fast.

We can use these functions to build more complex tools. What if we wanted to create a list? We set a variable and used construct repeatedly.

L = None
L=cons(1,L)
L=cons(2,L)
L=cons(3,L)

If we want to determine the length of this list, we can do so recursively. The empty list has no elements, so its length is trivially zero. What about longer lists? When we are looking at the list, all we have is the current first element. We have the first element, so there is at least one element. We just need to know how many values are left in the list. This can be handled by rest.

The length function has two cases.

  • Base Case: The empty list has a size of zero.

  • Recursive Case: The length of a list with values is 1 + the length of the rest of the list.

The recursive case counts the first element, then recursively counts the rest. We can implement this in Python using our functions.

def length(L):
    if isNull(L):
        return 0
    else:
        return 1+length(rest(L))

We can run a few tests on our code.

print("Is the list null?")
print(isNull(L)) #False
print("Does the list have elements?")
print(isCons(L)) #True
print("Is it a list?")
print(isList(L)) #True
print("What is the first element?")
print(first(L)) #3
print("What is the second element?")
print(first(rest(L))) # 2
print("What is the length?")
print(length(L))

The output shows we have created a list.

Is the list null?
False
Does the list have elements?
True
Is it a list?
True
What is the first element?
3
What is the second element?
2
What is the length?
3

Racket Lists

The Racket language provides the basic list commands we saw in Python. In the case of Racket, these commands are very efficiently implemented. We can pretend they are perfect implementation and build our function on them.

We need to start with the empty list. There are two ways to write the null list. We can explicitly say null. We can also use a single quote followed by empty parenthesis '(). This second method visually represents an empty list.

The construct command is implemented as the cons function. The cons function takes two inputs. The first is the element to put in front of the list. The second is the list to add the value to. The null list is always our starting point.

We build lists from the back towards the front. If we want to make a list with the elements \([2,4,6,8]\), we need to start with the last element.

We start by making a list with just the number 8, the last element in our list. Since there is nothing after it, we add this number to the empty list.

(cons 8 null) ;A list with just 8

When Racket displays a list, it shows it in parenthesis. To distinguish between functions and lists, a single quote is put at the start of a list. The return value of the previous cons command in '(8).

If we want to put 6 before the 8, we need two cons commands.

(cons 6 (cons 8 null)) ;The list '(6 8)

The single quote is critical to differentiate between applying the function named 6 to the input 8 and a list containing 6 and 8. There is no function named 6. The command (6 8) would be nonsense because we can’t apply the function 6.

To build our retire target list, we just need to cons all the values backwards. Each time we add the new value to a list. Except for the starting point, the list is the return value of the nested call.

(cons 2 (cons 4 (cons 6 (cons 8 null)))) ;The list '(2 4 6 8)

The cons command can construct lists our of anything. Racket is dynamically typed. We can put an integer, float, string, or another list into a cons.

Lists is Racket are always displayed with a single quote and parenthesis. The values are separated by spaces.

Once we have built a list, we can define it as a constant.

(define X (cons 4 (cons 6 (cons 7 null)))) ;The list '(4 6 7)

There is one very big difference between this list and arrays or Python lists. You cannot change the values in the list X. You could make a new list with modified values, but you cannot modify the values in place.

If we ran (cons 5 X) the list X will not change. We made a new list where 5 is the first element, but that does not change X. This may seem like a poor use of memory, but it is not. Everything is managed with pointers. Because the list positions cannot be changed, many lists can overlap in the same areas of memory.

We may want to ask questions about lists. There are three primary predicates about lists.

(null? X) ;Returns True if X is an empty list
(cons? X) ;Returns True if X is a list with one or more values
(list? X) ;Returns True if X is of the data type list

Once we have a list, we can get the first element out of that list. The first command gets the first element out of a list. The first command includes error checking and will give you nice warnings.

Both the following commands will get the first value in the list.

;Get First Value in List
(first X) ;Modern Format Returns 4

We can also get the rest of a list. In practice, this means we get the a pointer to the list with the first element removed.

(rest X) ;Modern Format Returns '(6 7)

We need to create lists often. There are two short-cuts that can be used. One is the quoted list. We can write the list out exactly as we want it using a single quote.

(define L '(1 2 3 4 5))

This can cause some unexpected behavior.

(define M '(+ 1 9))

You might think the list M would contain the number 9. It does not. It contains exactly what was typed. It is a list with three values.

The values are

  1. The addition function

  2. The number `

  3. The number 9

This method is useful for symbolic reasoning, but may not be what we want. If we want the evaluation to happen, we can use the list command. This does all evaluations first, then makes the list.

(define N (list (+ 1 9)))

Both these can be implemented using the cons command, but they save us time.

Note

Which command you use should depend on the situation. Use the quoted list when you want to type exactly what you want in the list. Use the list command when you need the list elements evaluated first. Use the cons command when you need to programmatically build the list.

If we create a multi-element list, we can access each element using just first and rest.

(define Q '(1 2 3 4 5))
(first Q) ; the first element 1
(first (rest Q)) ; The second element 2
(first (rest (rest Q))) ; The third element 3
(first (rest (rest (rest Q)))) ; The fourth element 4
(first (rest (rest (rest (rest Q))))) ; The fifth element 5

We can build very nested list using just these commands. We can build '(1 (2 (3)) (4) () 5) using just cons and null. We always start from the end. Since this example has nested lists, it is useful to build them before added them.

The last element is 5.

(cons 5 null) ;Makes '(5)

Before the 5, we have a null list. We can add a null as the element at the first on the list.

(cons null (cons 5 null)) ;Makes '(() 5)

The next value working backwards is a list containing the number 4. First, we test building that list. Then we add it to our command sequence.

(cons 4 null) ;Makes '(4)
(cons (cons 4 null) (cons null (cons 5 null))) ;Makes '((4) () 5)

The next element is a nested list with two values '(2 (3)). The number 2 followed by a list containing the number 3.

(cons 2 (cons (cons 3 null) null)) ; Makes '(2 (3))
(cons (cons 2 (cons (cons 3 null) null))
      (cons (cons 4 null)
            (cons null (cons 5 null))
       )
) ; Makes '((2 (3)) (4) () 5)

The first value is just the number 1. The final command to generate the list is tabbed in for better readability.

(cons 1
      (cons (cons 2 (cons (cons 3 null) null))
            (cons (cons 4 null)
                  (cons null
                        (cons 5 null)
                  )
            )
       )
) ; Makes '(1 (2 (3)) (4) () 5)

Using just the seven most basic list commands, we can build any list. The last piece we need is recursive functions to make our lists.

  • (null? A) is A a null list

  • (cons? A) is A a list with at least one element

  • (list? A) is A a list

  • (cons x A) add element A to the front of list A

  • (first A) get the first element of A

  • (rest A) get the rest of list A

  • null a constant for the empty list.

Note

Racket has a feature called a dotted pair. It should be avoided unless you have a very good reason to use it. Since the rest position in the list is a pointer, you can technically ask it to point to anything. This works up to a point. The data is stored, but you no longer have a list.

If we execute (cons 7 6) we have made a dotted pair '(7 . 8). Notice the dot between the values. This is not a list. You cannot use first or rest on it. Most list operations will fail. Dotted pairs should be avoided. They have some special uses, but should be avoided in most cases.

Recursive Functions on Lists

Making and using lists in Racket requires recursive thinking. We implement a few common list methods to show different ways to recursively interact with a list.

List Length

The first function we look at determines the length of a list. This is a build in command, but it provides a good case study. This function only takes lists as inputs. The input-contract line has no meaning within the language, it is just commenting style. It tells every only programmer that this function only works in situations where (list? myList) returns true. If that is not the case, there is no promise this code will work. The output-contract tells the programmer what to expect as output. In this case, an integer. That integer is the length of the list.

To compute the length of a list recursively, we need a base case. The smallest list in existence is the null list. We know for a fact it as a length of 0 because it contains nothing. We can start with (if (null? myList) 0 ...).

What about the recursive case? We need to think of the list in terms of first and rest. When we are concerned about length, all that matters about first is its existence. The first element counts for 1 unit of length. The rest of the list also has a length. We can compute this recursively. We can just ask (length (rest myList)). We then need to add one to the result to account for the first value.

Racket already has a length function build it, so we use numItems instead. This is just to make it clear we are not messing with the built-in length function. We could overwrite it, but in general it is bad practice to overwrite a built-in. You never know what effects you might have.

The full function is given below.

;Compute Size of a List
;input-contract: (list? myList)
;output-contract: (int? (numItems myList))
;                  and it is the size of list
(define (numItems myList)
  (if (null? myList)
      0
      (+ 1 (numItems (rest myList)))
  )
)
(numItems '(1 2 3 4 5 6)) ;Returns 6
(numItems '()) ;Returns 0

Total List Values

What if we want to look at a problem where the first value matters? We want to sum up all the values in a list. This time our function acts on a list which must only contain integers. If this is not the case, we make no promises about it working. We might guess that it would probably also work for floats, but might not for strings. This isn’t relevant. The input-contract only promises it will work on integers. If there are any other data types that happen to work, it is pure luck and not promised.

The function says it will return to us an integer that is the sum of all values. Even if the function worked with other data types, the comment is telling us it is only tested and confirmed in this situation.

Designing the function requires looking at the base case and recursive case. Again, the smallest possible list is the empty list. We need some value for the total of no elements. The most obvious solution is to return 0.

When the list has elements, we need to look at the first and rest. What do we do with each? The first value needs to be added to the running total. If we add all first values until we run out, we will have totaled the entire list. For the rest of the list, we also need to know the total. We can recursively ask what the total of the rest of the list is.

;Total Values in List
;input-contract: (lists? myList) and all elements are integers
;output-contract: (int? (total myList))
;and returns sum of all items
(define (total myList)
  (if (null? myList)
      0
      (+ (first myList) (total (rest myList)))
  ))

We can expand out the recursive calls one at a time to see how the function works.

(total '(2 4 6 8 10))
(+ 2 (total '(4 6 8 10)))
(+ 2 (+ 4 (total '(6 8 10))))
(+ 2 (+ 4 (+ 6 (total '(8 10)))))
(+ 2 (+ 4 (+ 6 (+ 8 (total '(10))))))
(+ 2 (+ 4 (+ 6 (+ 8 (+ 10 (total null))))))
(+ 2 (+ 4 (+ 6 (+ 8 (+ 10 0)))))
30

Create a List of Even Numbers

We also need to create lists recursively. This will be built on the cons command. This function creates a list of even numbers. Generating a sequence of \(n\) even numbers requires us to start at zero and add 2 with each step.

\( \begin{align} & 0, (0+2), (0+2+2), (0+2+2+2), \cdots \\ & 0, 2, 4, 6, \cdots \end{align} \)

This function always starts at zero, but how can we implement that in Racket? We have no global or local variables to assign to zero and update. We need to use function inputs. We can create two functions, the first function (evennums n) just takes the number of values we need. It defaults our second value in a helper (evennums_h n 0).

What is the smallest list of even numbers? It is the empty list which contains zero even numbers. This gives us a base case. If \(n\)=0 then we can just return the null list.

To come up with the recursive case think about how (cons a B) works. It takes a value and puts that in the front of the list. Think about the list '(0 2 4 6). It is a list of the first 4 even numbers. We could also think about it as “the number zero followed by three more even numbers.”

This approach leads us to a recursive definition. We need to (cons currentValue (...)). To make the recursive list, we need to decrease \(n\) by one because we have added one element to the list. Since we want even numbers, we also increase currentValue by 2 so that it has the next even value.

The full implementation is given below.

;Create a list of even numbers
;input-contract: (int? n)
;output-contract: (list? (evennums n))
;with the first n even numbers
(define (evennums n)
  (evennums_h n 0))
;This function actually solves the problem 
;using the start define above
(define (evennums_h n currentValue)
  (if (= n 0)
      null
      (cons currentValue
            (evennums_h (- n 1) (+ currentValue 2))))
)
(evennums 200)

Since these functions are pure, we can easily break up each step and evaluate it individually to see what is happening.

(evennums 4)
(evennums_h 4 0)
(cons 0 (evennums_h 3 2))
(cons 0 (cons 2 (evennums_h 2 4)))
(cons 0 (cons 2 (cons 4 (evennums_h 1 6))))
(cons 0 (cons 2 (cons 4 (cons 6 (evennums_h 0 8)))))
(cons 0 (cons 2 (cons 4 (cons 6 null))))
'(0 2 4 6)

Get Element

A common issue with Racket is the programmers design to have arrays where each index can be accessed. This is possible in Racket, but it is less efficient then a recursive pattern.

This example uses a new command called cond. The condition allows us to chain together if statements. It is similar to a switch or elif sequence in a iterative language. We give a sequence of pairs commands [(condition) (return value when condition is true)]. The code goes down the chain until it finds a case that is true. There is also a special keywork else which tells Racket to execute this code if none of the previous conditions were true.

The reason a condition helps us in this example is there are two base cases. We want to access the value at index \(x\) from the list \(A\). It is possible the list will be empty. If the list contains no elements, it doesn’t matter what index you asked for. That index does not exist and we need to return nothing.

That was one base case, what is the other? If we are asked to get the value at index zero, traditionally the first value, this is trivial. We already have a command called first. We can just call it.

If neither of these cases happen, we need recursion. If we remove the first index from the list, we can imagine all the indexes shifting down by one. In Python, imagine we have L=[2,4,6]. Right now L[0] is two and L[1] is four. If we remove the first element, we are left with L=[4,6]. The value that used to be at index one is not at index zero. It has shifted down by one.

Each time we take the rest of the list, we can shift the target index down by one. Eventually, it will reach zero or the list will run out of elements. This gives us a recursive call (getElement (- x 1) (rest A)).

The full implementation is given below.

;Gets the element at index x (starting from 0)
;out of the list A
;input-contract: (int? x) and (list? A)
;output-contact: element at index x or null if index does not exist
(define (getElement x A)
  (cond
    ;No element to return if list is null
    [(null? A) null ]
    ;The first element has a command
    [(equal? x 0) (first A)]
    ;For the rest, remove the first value and decrease counter
    [else (getElement (- x 1) (rest A))]
  )
)
;the following code will return 5
(getElement 4 '(1 2 3 4 5 6 7 8 9 10) )