Session 3
More Primitive Racket
Opening Exercise: Three Things
Last time, we saw that every programming language has three kinds of things:
- primitive expressions
- a means of combination, for building compound expressions out of simpler ones
- a means of abstracting, for grouping details into a higher-order expression
Quick Exercise:
- Make a list of five features from your favorite programming language.
-
Categorize each item on your list as a primitive, a means
of combination,
a means of abstraction, or none.
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
messages: objects send messages to other objects. In
this language, True
and False
are
objects. They respond to messages like any other object. Not
surprisingly, 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!
- primitives (notes, intervals, durations, ...)
- means of combining primitives (motives, chords, transposition, inversion ...)
- means of abstraction (phrases, harmonic progressions, and forms)
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 used this idea to begin 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, on Homework 0 I asked you to:
Write at least one question that you have about Racket and the type of programming being done.
Your questions were wide-ranging and showed that many of you have begun to think about the language more deeply. Unlike many years, I did not receive any questions asking Why is Racket so weird?
Let's answer a few of your questions now, plus some common ones...
On syntax
You didn't ask many questions about specific syntax in this round. The most common were:
- Why are there so many parentheses?
- Does Racket have useful libraries like Python and Java?
- Does Racket have classes or objects?
We began to see last time why Racket code contains so parentheses: the only means of combination is the fully-parenthesized prefix expression. One of the trade-offs this gives us is that we use fewer other punctuation marks.
The question about classes sometimes replaces that feature with some other feature we know and like frm another programming language: for loops, hash tables, and so on. The answer is quite often 'yes'. Racket is a full-featured language.
A couple of years ago, one person asked a question I had never
received before: Why close parentheses all on the same line,
rather than line-by-line as in C or Java? With so much
nesting, C-style closing would use a lot of lines and leave a lot
of unhelpful whitespace. Indentation conveys the code's structure.
(Racket > Reindent
)
Prefix notation
Usually, several students ask about about prefix notation, which we discussed some last session. Here are a few more thoughts...
Function calls in most languages are prefix!
f(x,y)
is prefix, with the function name
outside the parentheses. (f x y)
is hardly
different.
Racket is a language of function definitions and function calls. Even the built-in operators are defined as functions and called as functions. So it makes some sense to use a common syntax.
This means that the built-in operators can take any number of arguments. Infix operators are limited to taking two.
The bulk of most Racket programs consists of many user-defined functions and calls to them.
On software engineering
I didn't receive many questions of this sort this year. Some common questions you might have:
- Can we write unit tests?
- Can we use functions defined in other files?
The simple syntax will affect how we read and write code. We really must use small functions, good names, and whitespace to help us create larger programs. We will all get used to it.
Meta Questions
Despite my encouragement, I usually receive some questions about Racket's place in the world. Students are curious!
- Is Racket popular?
- Why use Racket instead of Python or Java?
- In what domains is Racket the right language to use?
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 email me another question about Racket in Homework 1. This year, questions about symbols and lists stood out.
Symbols
Symbol is another data type. A symbol is just a value, like a number. The fact that we are used to working with numbers and not symbols doesn't make symbols less real.
Consider this Python statement: a = b + 1
.
It consists of four symbols and a number. (If we want to treat the symbols differently, then we might say that it consists of two symbols, two operators, and a number.)
a
and b
are values, just like
1
.
Compare and contrast with strings.
Compare to enums in Python and Java.
This semester, we will be using lists of symbols and numbers as our way to represent programs. In the meantime, we will process lots and lots of lists of symbols as we learn functional and recursive programming patterns. As we use them, they will become more familiar to you.
Lists
Next session, we will begin to look at lists in detail, so let's save most of our discussion for then. But many of you are curious now:
first
and rest
are cumbersome.
Are there other ways to access elements in lists?
There are a bunch of functions for accessing list items, including:
-
functions named
first
throughtenth
(!),last
, andrest
-
older functions like
car
andcdr
, which we will learn about next time -
list-ref
, which accesses elements by their integer position in the list
They can be convenient in specific. You will be surprised just
how useful first
and rest
on their own
as we write code that walks down a list one element at a time.
You have experience working with lists in other languages. Consider these Python versions of Racket's functions. We will see more soon!
Don't worry. This assignment was intended as a way for you to experiment with Racket and begin to learn some of its features. We'll begin learning and using more of Racket's features today. In particular, I wanted you to see that Racket lists are linked lists, so accessing them is a linear operation. And no, you wrote have to write expressions such as
(first (rest (first (rest (rest '(1 (2 (3) 4 5) (6 x 8)))))))
much in the future! Our functions will do the work.
Thinking like a scientist. Or: What's up with Problem 7?
Problems 5-7 require you to experiment and deduce. This is a great way for you to figure out how the language works. We won't write expressions like this throughout the course, but we will have to experiment and deduce all the time. And you do want to know how lists work. Experiment!
Solve Problem 7.
Checking expectations. How can we record the answers we expected for Problem 8 in my interaction?
Racket provides a module called
rackunit
that gives us just what we need.
Check out
this Racket file.
This also answers a common question about modules and
encapsulation, and another about unit testing. Yes, Racket
has them! We will use rackunit
for unit testing
beginning with Homework 2, and begin to write our own modules
for Homework 3.
~~~~~
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.
Definitions
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 3.141592653589793
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 value 2.718281828459045
.
> e 2.718281828459045
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 do not return
values, 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 value
3.141592653589793
, so we can refer to that object by
name. For instance:
> pi 3.141592653589793 > (+ 2 pi) 5.141592653589793 > (define r 3) > (* 2 pi r) ; NOTICE: one * with 3 arguments! 18.84955592153876 > (define circumference (* 2 pi r)) > circumference 18.84955592153876
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) 18.84955592153876 > (define circumference (* 2 pi r)) > circumference 18.84955592153876
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)) 0)
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 away from 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?
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> <expression1> (if <predicate2> <expression2> . . . (if <predicaten> <expressionn>)...))
Is that clear?
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) 'zero 'nonzero)
What if we wanted to return true or false instead? This would work:
(if (= x 0) #t #f)
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...)
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:
<
, >
, etc. 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
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... tell the story.
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 link is now dead, but here is the passage:
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
-
Reading
Read all of today's lecture notes, especially the sections on conditionals and boolean predicates. They cover Racket features you know from other languages that we did not touch on in class today. Then:- Read about boolean values in the The Racket Guide.
- Read about pairs and lists in the Scheme language standard.
-
Homework
- Homework 2 will be available soon and due in one week. It asks you to use your understanding of simple Racket primitives to write a few simple Racket functions. You will learn a bit more about how to use the Dr. Racket interpreter, adding the Definitions pane to your arsenal.