Session 12

Recursive Programs and Programming Language

CS 3540
Programming Languages and Paradigms

an array with max and second max marked

Opening Exercise

This is an easy problem to visualize. Suppose you have a list of at least two numbers:

    (6 1 2 -3 8 4 -1 2 9 1 2 4)

Your task:

Write a function (2nd-max lon) that returns the value of the second-largest item in the list of numbers lon.

For example:

    > (2nd-max '(6 1 2 -3 8 4 -1 2 9 1 2 4))

You may solve this problem with or without recursion. Without recursion, you will need higher-order functions. With recursion, follow the data structure!

If you would like to use a function but aren't sure whether it is a Racket primitive, ask.

Bonus points to the shortest solution and to the most efficient solution!


How many of you wrote recursive solutions? Functional ones? How short? How efficient? What ideas did you try?

Functional Solutions

What functions can help us?

If run-time efficiency is no concern, we can solve this in almost no code: sort the array from largest to smallest, then return the second element!

    (define 2nd-max
      (lambda (lon)
        (second (sort lon >))))

Note that Racket has a primitive sort function that takes as arguments a list of values and a function for comparing the items in the list. sort can handle a list of any type as long as the comparator works on the values.

With a list of numbers, demo with these comparators: With a list of strings, demo with these comparators:

If the thought of sorting the entire list to find two items makes you uneasy, then we might try another approach:

  1. Find the largest value in the list.
  2. Remove that item from the list.
  3. Find the largest value in the remaining list.

That may sound like it requires looping, but think back to the functional style of programming we saw in Session 6 when we computed total fuel for a set of space modules and in Session 7 when we computed total error for a set of predictions. In both cases, we used function calls and higher-order functions to replace loops.

We can use the Racket primitives apply and max to do Steps 1 and 3. For Step 2, we need a function that removes an item from a list. We wrote such a function for lists of symbols in Session 9, called remove-first. It turns out that Racket has a primitive function named remove that does the same thing, but for any type of list. Let's use it!

All we need to do is invoke this sequence functionally:

    (define 2nd-max
      (lambda (lon)
        (apply max                   ; step 3
               (remove               ;        step 2
                   (apply max lon)   ;               step 1
                   lon))))           ;        step 2

... how do these compare to other solutions the class offered?

The first solution is O(n log n). The latter is O(n), but makes three passes down the list. Can we do better?

Recursive Solutions

As we know, a list of numbers such as:

    (6 1 2 -3 9 4 -1 2 8 1 2 4)
has an inductive definition:
    <list-of-numbers> ::= ()
                        | (<number> . <list-of-numbers>)

We know that the lists given to 2nd-max have at least two items, so maybe we need to define a new data type. Let's think about the problem a bit first.

How might we approach this problem if we could write a loop? We would probably...

This seems easy enough, though we have to think about several cases as we initialize and update the variables. Notice, though, that after we peel the first two numbers off the front of the list, we have a good old list of numbers, empty or a pair. Our existing data type is what we need after all.

We can use structural recursion to start building a function that works this way:

    (define 2nd-max
      (lambda (lon)
        (if (null? lon)
            ; return the answer
            ; handle a pair

But this isn't quite right... The recursive code will process the list of numbers after the first two are removed, and it needs access to the local variables largest and 2nd-largest. So, our recursive function has to be a helper:

    (define 2nd-max-tr
      (lambda (largest 2nd-largest lon)
        (if (null? lon)
            ; return the answer
            ; handle a pair

The then clause deals with the case where we have seen all the numbers in the list. At this point, 2nd-largest holds the second largest value in the list, so we return it:

    (define 2nd-max-tr
      (lambda (largest 2nd-largest lon)
        (if (null? lon)
            ; handle a pair

The else clause handles a pair. It can do what our loop would do in an imperative program: compare the next item in the list, (first lon), to largest and 2nd-largest and update them accordingly.

    (define 2nd-max-tr
      (lambda (largest 2nd-largest lon)
        (if (null? lon)
            (cond ((> (first lon) largest)
                        (2nd-max-tr (first lon) largest (rest lon)))
                  ((> (first lon) 2nd-largest)
                        (2nd-max-tr largest (first lon) (rest lon)))
                  (else (2nd-max-tr largest 2nd-largest (rest lon)))))))

Now all we have to do is write 2nd-max as an interface procedure that kicks off the computation, initializing the variables with the values of the first two items in the list:

    (define 2nd-max
      (lambda (lon)
              (max (first lon) (second lon))
              (min (second lon) (first lon))
              (rest (rest lon)))))

The result is a function that examines each item in the list exactly once and finds the second-largest item. Our recursive solution is longer but more efficient: it makes only one pass over the list. We have traded programmer time for execution time.

The helper function we wrote is a bit complicated. We handle all three possibilities for (first lon) -- largest, second largest, and neither -- with a cond expression. This requires us to repeat the recursive call in all three cases. Perhaps we can do better...

Let's use our interface procedure as an inspiration for something simpler:

    (define 2nd-max-tr
      (lambda (largest 2nd-largest lon)
        (if (null? lon)
               new value of largest  ; -- handle (first lon)
               new value of second   ; -- in these expressions
               new value of lon      ; handle (rest lon)

In an imperative solution, we would assign new values to the two variables and branch to the top of the loop, which updates the position in the array for the next pass. Doing the same thing here, on the recursive call, simplifies the helper function. After a little work (think of it as a tournament with three teams), we arrive at:

    (define 2nd-max-tr
      (lambda (largest 2nd-largest lon)
        (if (null? lon)
               (max largest (first lon))
               (max 2nd-largest (min largest (first lon)))
               (rest lon)))))

Notice that 2nd-max-tr is tail-recursive, an idea we learned about last time. It looks a bit like a loop -- and acts like one, too! Not only does this solution make only pass over the loop, it uses one stack frame, no matter how long the list is. And it's shorter than our first recursive function.

I don't know about you, but I think this is a dandy little solution.

Comparing Our Solutions

Note: This section is just an outline of the things we talked about in class. I'll fill it in as time permits.

How might we compare these solutions? Here are some of the dimensions we might consider:

It's worth taking some time to think about which of these dimensions are objective, or relatively so, and which are subjective. Subjective evaluations can change with time, learning, and experience. You may have already begun to think differently about Racket and functional programming than you did at the beginning of the course.

What happens if we are not given the precondition that 2nd-max always receives a list with at least two items? How should our function act if it receives fewer than two items?

A few passing thoughts...

A Transition in the Course

First, we learned a new language, using that process to learn a new way to think about languages: in terms of primitives, means of combination, and means of abstraction.

Then we used the new language to learn a new style of programming: functional programming. It uses functions, both ordinary and higher-order, to solve problems.

Then we used the new language and style to learn patterns of recursive programming. These patterns are especially useful for processing inductive data types.

Now we are ready to begin moving into a new phase of the course, in which we apply our new skills to learn how our programming languages work.

Next session, and for most of the rest of the course, we will use what we have learned thus far about functional and recursive programming to explore issues in programming languages and their paradigms. That is a big space to work in. Let's start in one small corner of the programming languages world: where variables get their values from.

Static Properties of Variables

A property is static when its value can be determined by looking at the text of a program. A property is dynamic when the program must be executed in order to determine the property. We can use static properties of a program to detect errors and to improve program performance at interpretation or compile time.

We have already discussed one property of variables this semester: their data type. In Ada, the data type of a variable is static. In Java, it is static, with a twist allowed by the substitutability of objects of different classes. In Racket, the data type of a variable is dynamic -- though we typically write our code with a specific set of values and operations in mind.

In this session, we will begin to explore some of the static properties that variables can have.

A Little Language

How can recursive programming help us explore language features? It turns out that the syntax of a programming language is defined inductively!

The definitions of Python, Java, and Racket are quite large. Many of the features of these languages are syntactic sugar, added to make programmers' lives easier. But we don't need all of those features to understand how the languages work.

Much of our work the rest of the semester will use small languages with essential features as a way to study how languages work. Today we introduce such a language. We will use a family of similar languages to study different language features throughout the course.

If our goal is to explore where variables get their values from, what features does out little language need? Think about how function calls work in Racket and other languages. They require:

Today's little language has just these features:

     <exp> ::= <varref>
             | (lambda (<var>) <exp>)
             | (<exp> <exp>)

This little language provides the bare minimum: an expression in the language is either a variable reference, a function of one parameter, or an application of a function to one argument. This is all we need to study the topic du jour, which is the static property of free and bound variables.

Quick Exercise: Earlier, we discussed the three things that every programming language has. Which of these features does our little language have? Which features is it missing?

Quick Answer: This language does not have primitive values of all sorts you are used to; its only primitives are variable references. Its means of combination is a subset of Racket's parenthesized operations. The language has procedural abstraction via lambda. It has naming abstractions, too: a value is associated with a formal parameter whenever we call a function.

This language is Racket-like in its use of parentheses and its use of lambda to define a nameless function, but it is universal in the features it provides. All languages you know and love have the same three features, plus many more. We will add more features to this language as time goes by, always with an eye to how our language processors would manipulate programs written in the language.

Free and Bound Variables

As you know, a variable can have a value or not have a value. Of course, in some languages, such as Java, every variable has a value, even if the program does not initialize it. For example, a Java integer defaults to 0, and object variables default to null.

Unfortunately, null isn't really a value at all. Many of you encounter "null pointer exceptions" at run-time as you learn how to write Java programs. These exceptions point out that the variable in question doesn't really have a value, and as a result the program cannot proceed.

On top of this idea of a variable having a value is the idea of how the variable receives its value. If a variable has the same name as the formal parameter on a method, then we know that the variable is "bound" to that declaration, and that the value of the variable will be the value passed as the corresponding argument to the function.

    int sumOfSquares( int m, int n )
        // m and n are bound to formal parameters
        return m*m + n*n;

Let's define "boundedness" and some related ideas that apply to all programming language:

In Racket and our little language, above, lambda expressions define functions and so are the source of boundedness.


Now let's see some examples from our little language that illustrate these concepts:

We often write combinators in Racket. For example, the compose function, which implements the sort of function composition you learn about in algebra, is a combinator:

    (define compose
      (lambda (f g)
        (lambda (x)
          (f (g x)))))

However, some of the functions you may think of as having only bound variables have free variables. This Racket function is not a combinator:

    (define sum-of-applications
      (lambda (f x y)
        (+ (f x) (f y))))

Why? Because + is a free variable! Remember: Racket functions are bound to names in just the same way as any other value. You can verify this for yourself by evaluating:

    > (let ((+ *))
        ((lambda (f x y)
           (+ (f x) (f y)))
         add1 1 4))

One note before we proceed. We will be writing programs to process programs our little language. The data type we will be processing is the grammar of the little language. That grammar uses Racket list notation, but that is an implementation detail. Calls to car, cdr, and cons will litter our code with implementation details. They will also obscure what our code means.

Soon we will write functions to access the parts of the expressions in our data type. These functions will allow us to

The former are called type predicates, and the latter are called access procedures.

We will read more about these next time.

Wrap Up

Eugene Wallingford ..... ..... February 23, 2023