Session 3

More Primitive Racket

CS 3540
Programming Languages and Paradigms

Opening Exercise: Three Things

Last time, we saw that every programming language has three kinds of things:

Quick Exercise:

Where, for example, would you classify a Python if statement?

All Kinds of Things

Notice that there are lots of things not on the list. For instance, if statements are not one of the three things that every language has. Do you know of any programming language without if statements? Probably not.

In CS1, you may have learned that, in order to implement the computations that programmers need to solve most problems, a language must support three kinds of control flow: sequence, selection, and repetition. So any complete programming language will offer programmers a way to make choices.

But there is a difference between being able to make a choice and having an if statement, or any explicit conditional statement, in a language.

Take, for example, Smalltalk. I use Smalltalk as an example occasionally throughout this course, because it is different from the languages you know in several ways. Smalltalk does not have a conditional statement, nor does it have a special form for making selections. (We'll talk more about the notion of a "special form" soon.) How can that be?

It turns out that Smalltalk has no statements or special forms for control flow of any kind. All Smalltalk has is message passing: objects sending messages to other objects. In this language, True and False are objects. They respond to messages like any other object. Of course, they respond to the same messages, only in different ways.

True and False can also be returned as responses from other objects. So we can write expressions such as

    inputFile isOpen ifTrue: [inputFile readLine].

This gives programmers the ability to make decisions in a program. When I send a message to True, it behaves one way. When I send the same message to False, it behaves another way. So Smalltalk does provide the ability to make choices, but not through a conditional statement. There is no place in the Smalltalk compiler where you can find the behavior for "if...". The behavior is defined as a method of the class Boolean!

Your view of what a programming language must have may be changing already.

Likewise, a language need not have a looping statement such as for or while. How so? As Java programmers learn, a collection of objects can know how to return a subset that meets some condition, return a sorted version of itself, or determine whether a particular kind of object exists. In Python, we can use list comprehensions to select sub-lists and create new lists, and lists can sort themselves. Both languages still have for or while statements, but programmers don't use them as much as they do in languages such as C and Ada. We can imagine them disappearing entirely.

Why does it help us to know that some things must be present in a language, but other do not? It helps us to know what to look for. If I think that a programming language must have an if statement, then when I encounter a language that doesn't, I may become disoriented, disappointed, or even angry. None of those emotions help me to learn the new language, and they may well close my mind to learning something useful.

Whenever you are faced with the task of learning a new language, first try determining what is primitive in the language, how things get combined, and how details are abstracted away. This will give you a framework to guide your task. Over the next several sessions, we will be learning Racket. Our efforts will be guided by this framework.

Optional Digression: The "three things" framework can be useful when we study other formal languages, too, not just programming languages. At PyCon 2012, the Python community's annual conference, there was a workshop called Making and understanding music with Python [...] that described music as a language made up of:

Very nice!

Where Are We?

Last time, we began our study of programming languages by saying that every programming language consists of three kinds of entity: primitive expressions, some means of combining expressions into larger expressions, and some means of abstracting detail. We then applied this idea as we began to learn Racket.

In our initial explorations, we found that, at its heart, Racket is primarily (but not solely) a language of expressions. Expressions are evaluated for their values, not their side effects. We also saw that Racket has only one means of combining expressions into compound expressions: the fully parenthesized prefix expression, (<operator> <operand>*). This syntax takes some getting used to but has some nice side effects for language processing.

Today, we discuss abstraction in Racket and introduce two of its primitives for creating conditional expressions. But first...

Your Questions about Racket from Homework 0

Last week, I asked you to come to class with

at least one question you have about Racket and the type of programming being done
in your reading assignment. Your questions were wide-ranging and showed that many of you have begun to think about the language more deeply. Even still, I received a couple of questions basically asking Why is Racket so weird?

Lisp is hard!

Let's answer a few of your questions now, plus some common ones...

Despite my encouragement, some of you asked "meta" questions, about Racket's place in the world. Students always do!

We will see, talk about, and use functional style throughout the semester. If you have the same questions about Racket and functional programming at the end of the course, let's talk then.

At this point in the course, I usually encounter some questions repeatedly. These are natural questions that people learning Racket have, especially after learning Java, Python, C, and Ada first. I have gathered many of these questions and their answers in a list of Frequently Asked Questions about Racket and Scheme. Check it out! I will try to answer any other questions you ask, either in writing or in class. Keep on asking questions, too. That is the one of the best ways that you will learn.

Your questions show a natural curiosity. That's good! Let your curiosity open you up to something different.

Oh, and why is Racket so weird? Because you don't know it yet.

Your Questions about Racket from Homework 1

I also asked you to send me email with another question about Racket in Homework 1. Here are a few that stood out:

To be less wrong than yesterday...

I suggest you practice daily. Have you ever learned to play the piano or trained for a marathon? Practicing once a week the night before a lesson or race doesn't work. Our brains change slowly, so they need repeated practice. Another advantage to daily work is that you can ask questions sooner -- and get answers sooner -- and use those answers to help you work better the next day.

The lecture notes for this week may seem to move slowly. But it's important not to throw a lot of new material at you while you are becoming familiar with Racket. To use a language well, we must achieve a level of familiarity with it; with a language so different from your past experience, this takes time and practice. Please use this time productively to read and explore inside Dr. Racket. It will be time well invested.


Now let's return to our direct study of Racket.

One of the most important things a programming language does is provide tools for building abstractions. The simplest but most frequently used abstraction mechanism is the ability to give a name to a computational object. The complement to this form of abstraction is reference, to mention something by name.

Consider for a moment how one talks about problems in daily life. To find the circumference of a circle with a radius of 3, we say two pi r, which means to multiply 2 times pi times 3. r is the name for the radius, 3. pi is also the name of a number. It is a name that means something to anyone who has had grade-school arithmetic, and it gives us a convenient handle for referring to a not-so-convenient number.

(Upon hearing me say this, a student in this course once rattled off 50 digits of pi without batting an eye!)

pi is a primitive object in Racket. That is, the name has already been given to a specific number:

      > pi

We can also associate a name with a number or other value, using the operator define. When you enter:

      > (define e 2.718281828459045)

you cause the Racket interpreter to associate the name e with the computational object 2.718281828459045.

      > e

Note that that when we evaluate a define expression, Dr. Racket does not print a value. That's because define does not produce value. This may seem odd... Wouldn't it be handy if the value of a define expression were the value being named? In our e example, that would be 2.718281828459045.

Racket takes seriously the difference between computing a value and computing a side effect. We define something to cause a side effect, so Racket does not return any value from a define expression. Because the expression does not have a value, Dr. Racket does not print a value. If you try to use the value of a define expression, Racket gives you a targeted error message:

      > (define PIE (define pi 3.14))
      define: not allowed in an expression context
        in: (define pi 3.14)

We do not mind that define expressions have no specified value, because we use define only for its side effect: the binding of a name to a value. In this course, we use define only at the top level, never nested in an expression.

Racket associates a name with the object 3.141592653589793, so we can refer to that object by name. For instance:

      > pi

      > (+ 2 pi)

      > (define r 3)

      > (* 2 pi r)       ; NOTICE: one * with 3 arguments!

      > (define circumference (* 2 pi r))

      > circumference

Notice that, when we evaluate circumference, its value is 18.84955592153876, not "(* 2 pi radius)" or '(* 2 pi radius). The compound expression (* 2 pi radius) is evaluated at the time of the definition and its value is given the name circumference.

How could I bind the name circumference to the value (* 2 pi radius)? If I want my value to be that list -- literally -- then I can use that list as a literal. Do you remember how we expressed a symbol literal in Session 2, so that the symbol was not evaluated?

We used the single quote to say "don't evaluate this symbol". The single quote is actually shorthand for the quote special form, which we can also use with lists. Watch:

      > '(* 2 pi r)
      '(* 2 pi r)

      > (quote (* 2 pi r))
      '(* 2 pi r)

      > (define circumference '(* 2 pi r))
      > circumference
      '(* 2 pi r)

      > (* 2 pi r)

      > (define circumference (* 2 pi r))

      > circumference

Naming the process for computing a circumference is a different sort of abstraction: the creation of a function. You have a lot of experience creating functions in other languages and will begin toying with simple functions on Homework 2. We will begin to create functions, and use them more broadly, beginning in Session 5.

Recall that last time, I said that define is a special form. It is an operator, used in the same way we use any function to create a compound expression. But it is special in that a Racket interpreter has a special rule for evaluating define expressions, different from the standard rule described in Session 2. The first argument to define, the name being created, is not evaluated; it is merely used as the "target" for the value of define's second argument.

A special form is an exception to the standard evaluation mechanism in Racket for combinations, in which all operands are evaluated and the values passed to the operator. define is one of several special forms in Racket.

We have now seen two special forms: define and quote. In the next few sessions, we'll learn about two more essential special forms, (if and lambda) and one optional but handy form (cond). Much later in the semester, we'll encounter Racket's other two required special forms, set! and begin. You may already have seen the optional but handy let operator in your reading. We will study it in some detail in a few weeks.

define is Racket's simplest means of abstraction. Being able to name computational objects allows names to be associated with complex results, such as circumference. In addition, we don't need to repeat the whole string and, more importantly, re-evaluate it each time we need to know the circumference. Finally, we can build programs incrementally by successive definition.

This last point is very important. We program in Racket by defining one thing, and then the next, and then the next, building up a program in a series of steps. At each point along the way, the objects that have been defined are available for use and testing. This is not so different from how we might program in languages such as Java, though the interactive "feel" is different. Plus, because functional programming does not use (many) variable assignments, we end up creating lots of small definitions.

Conditional Expressions

Yes, expressions -- not statements! As I've said before, almost everything in Racket is an expression that has a value. This is true even of decision-making operators such as if expressions.

The if Expression

The syntax of if is:

(if test-expr then-expr else-expr)

For example, we could write an if expression to compute the amount of tax on a particular amount of income. Suppose that we pay no tax on the first $20,000 of income, and 20% tax on amounts over $20,000:

      (if (> income 20000)
          (* 0.20 (- income 20000))

if is not a function. Like define, it is a special form. The standard behavior of Racket's evaluator is to evaluate all arguments and then pass their values to the procedure being evaluated. But the purpose of an if expression is to evaluate only one of its two branches. Either the then-expr is evaluated or the else-expr is evaluated, but never both.

Which expression is evaluated? That depends on the value of test-expr, which is always evaluated. Ideally, the test expression will return a boolean value, either true or false. In Racket, the literal values of true and false are #t and #f, respectively. If the test evaluates to true, then then-expr is evaluated; otherwise, the else-expr is evaluated. (Makes sense, huh?)

In one of its few breaks with simplicity and clean semantics, Racket has "truthiness". As in many other languages, if treats #f as false, and everything else as true. This is a place where we see Racket's genes in the Lisp family peeking through. It is also a place where Racket breaks awayfrom Lisp, though, because the empty list -- () -- counts as true.

A conditional expression (or statement) isn't one of the three things that every programming language has. Do you remember why not? This is an important distinction that you will want to learn and know well. Otherwise, how will you know what you can and cannot do with some new language that you encounter?

Quicker Exercise: Can you think of a way to write this expression without an if expression? How about this:

      (max 0 (* 0.20 (- income 20000)))

We can write many expressions that make choices without using an if expression in our own code. This is true in other languages, too. Functional programmers tend to think more in terms of What function can help me compute this result? than in terms of explicit control flow.

The cond Expression

In Racket, as in many other languages, there is a second conditional form. The second form is the more general and is called cond. We use cond like this:

   (cond ((= x 0) 'zero)          ;; a list of one or more items
         ((< x 0) 'negative )
         ((> x 0) 'positive )

If x is equal to zero, this expression returns the symbol zero. If x is less than zero, it returns the symbol negative. If x is more than zero, it returns the symbol positive.

Quick Exercise: Notice the use of the quotation. What would happen if we left the quotes off those symbols?

The general form of the cond expression in Racket is:

    (cond (<predicate1> <expression1>)
          (<predicate2> <expression2>)
          (<predicaten> <expressionn>)

Each argument to the cond expression, called a clause, is a list of two expressions. The first one, the predicate, is a boolean expression. The second, called the consequent, is an expression to evaluate only if the predicate is true.

When the interpreter encounters a cond expression, it evaluates the predicates in the order listed until it finds a predicate that evaluates to true. It then evaluates the consequent expression associated with that predicate and returns that expression's value as the value of the cond. If none of the predicates evaluates to #t, the value of the cond is undefined.

That's a pretty complex explanation in English. As noted last session, sometimes the best way to describe the meaning of a piece of code is with another piece of code. Perhaps the simplest way to understand a cond is to look at the equivalent if expression:

    (if <predicate1>
        (if <predicate2>
            (if <predicaten>

is that clearer?

It is easy to make mistakes with the parentheses in a cond expression, but if you remember a few points, it's not that hard. Think of cond as an operator with any number of arguments. Each argument is a list, and so is enclosed in parentheses. The predicate and consequent expressions are just like any expressions that we have seen so far. They can be simple expressions (no parenthesess) or compound expressions.

The value of a cond with no true predicates is undefined. To avoid this case, we generally use an else clause as the last clause in the expression. If the interpreter encounters an else while evaluating a cond, it automatically returns the value of its consequent as the value of the cond.

For example, reconsider our cond example above:

   (cond ((= x 0) 'zero)
         ((< x 0) 'negative )
         ((> x 0) 'positive )

We could write this expression using an else as:

   (cond ((= x 0) 'zero)
         ((< x 0) 'negative )
         (else    'positive )

We can use a cond with an else even if we have only two branches. Suppose that we want our cond to return zero if x equals 0, and nonzero otherwise. We can express this as:

   (cond ((= x 0) 'zero)
         (else    'nonzero)

Note: We rarely have to write code like this, because Racket provides a primitive function, zero?, to do the job.

You can think of else is a predicate that always evaluates to true. Really, though, it's a keyword, one of the few in Racket. Note that it is syntactically incorrect to enclose the else in parentheses -- or to quote it.

Of course, we could also rewrite our zero-testing code as an if expression, like this:

   (if (= x 0)

What if we wanted to return true or false instead? This would work:

   (if (= x 0)

But if that is really what we want to say, then we don't need an if or cond expression at all! All expressions are created equal in Racket, including conditional expressions. We can simply use the test expression above in place of the if:

    (= x 0)      ;; FUNCTIONALLY EQUIVALENT TO (if (= x 0)
                 ;;                                #t
                 ;;                                #f)
                 ;;                            ^^^^^^^^^^^

Watch for unnecessary if expressions as you begin to write Racket. They are hard to read and just plain ugly! (And, for those of you who don't like all the parentheses and all of the nested expressions that are a part of programming in Racket, the if expression here adds gratuitous parens and an unnecessary level of nesting...)

Quick Exercise: Re-write our if expression that computes tax using a cond expression.

      (if (> income 20000)
          (* 0.20 (- income 20000))

Here is a possible solution:
      (cond ((> income 20000) (* 0.20 (- income 20000)))
            (else 0))

Sometimes, an if expression seems more appropriate, and other times a cond expression seems more appropriate. In the end, the choice is a matter of preference.

Boolean Predicates

Any expression that returns either #t or #f -- and nothing else -- is called a Boolean predicate. The test expressions in our cond and if expressions are Boolean predicates.

Racket makes no distinction between Boolean predicates (expressions) and "ordinary" expressions. The expressions that we have written thus far to determine if x is equal to 0 are also Boolean predicates, since they return only #t or #f.

As do most other languages, Racket provides a number of built-in operators for creating Boolean predicates. For example, we can use standard comparison operators to compare numbers. Each basic data type provides a set of predicate expressions on values of the type. Racket also provides ways to combine Boolean expressions into compound expressions. For example, we can use the built-in Boolean operators and, or, and not. These Boolean operators work just like arithmetic operators, using prefix notation.

Closing Thought

Alan Kay
Alan Kay

Racket is probably different from any other language you have learned. Don't dip your toe in the water. Get wet.

If you don't like analogies that involve potential drowning (and that is probably the sensation you feel when you see all those parentheses for the first time), consider Alan Kay's analogy to cars and airplanes...

While searching for a URL to cite for Kay's story, I ran across a different use of airplanes in an analogy, in a long-ish article about Ruby and Smalltalk:

The truth is that bicycles and motorcycles operate quite differently than wheeled vehicles which keep three or more wheels on the ground. For one thing you steer by leaning, not with the handlebars or steering wheel. Learning to fly an airplane gives even stronger examples of having to learn that your instincts are wrong, and that you have to train yourself to "instinctively" know not only that you turn by banking rather than with the rudder, but that you control altitude primarily with the throttle, not the elevators, speed primarily with the elevators not the throttle, and so forth.

Learning to program in a functional style takes some getting used to, but it's worth the effort -- even if you choose never to do it again. Then again, you may not have that choice.

Wrap Up

Eugene Wallingford ..... ..... January 24, 2023