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 abstraction, for grouping details into a higher-order expression
Quick Exercise
Make a list of five features from your favorite programming language. Here's a start:
ifstatements- ...
Label each item on your list as:
- a primitive expression
- a means of combination
- a means of abstraction
- none of the above
All Kinds of Things
Notice that there are lots of things not on the list of three things every programming language.
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 your Intro course, 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 (selection).
But there is a difference between
being able to make a choice
in a language and the language
having an if statement.
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
again 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. We can imagine them disappearing
entirely.
Racket has for loops, a whole bunch of them.
They are useful when writing certain kinds of code. They
won't be helpful to us this semester, so we won't use them.
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. +
- primitives (notes, intervals, durations, ...)
- means of combining primitives (motives, chords, transposition, inversion ...)
- means of abstraction (phrases, harmonic progressions, and forms)
Over the next several sessions, we will be learning Racket. Our efforts will be guided by this framework.
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 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 operators for creating conditional expressions. But first...
Your Questions about Racket from Homework 0
Introduction
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. Like most years, I received a few questions that boil down to Why is Racket so weird?
Let's answer a few of your questions now, plus some common ones...
On syntax
The most common questions about specific syntax at this point are:
- 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 from 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.
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 uniform 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.
Error messages will help us, as they do in most languages — if we read them carefully. They will be most helpful when working with small expressions. This is one reason that Racket programmers build and debug their code in the the Interaction pane before moving it into a function definition in the Definitions pane.
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
Introduction
I also asked you to email me another question about Racket in Homework 1.
By the due date and time, email me at least one new question that you have about Racket now that you've worked with it for a while.
Not surprisingly, you had a lot of questions about lists.
Next session, we will begin to look at lists in detail, so let's save some 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
firstthroughtenth(!),last, andrest -
older functions like
carandcdr, which we will learn about next time -
list-ref, which accesses elements by their integer position in the list
first and rest are especially
convenient in specific situations. You will be surprised
just how useful they can be 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, on Homework 1 I wanted you to see that Racket lists are linked lists, which means that accessing them is a linear operation. And no, you won't 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 for us.
What's up with Problem 7?
This is a problem where you get to think like a scientist.
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!
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.
How does Dr. Racket handle different languages efficiently, or at all?
This question also gets to the heart of the questions
Why use Racket?
and What is Racket good for?
and
What makes Racket different, other than syntax?
The parts of Dr. Racket that read source code, parse it, and evaluate it are themselves Racket code.
We can swap them out for code that reads, parses, and
evaluates another language by changing the
#lang directive at the top of the source file.
Racket is a language for making languages. We will return to this idea in some detail after spring break.
Practice
Someone asked a really good question, not about Racket but about learning Racket:
What are common pitfalls when learning Racket? How can we avoid them?
Racket is different. I suggest you practice daily.
Have you ever learned to play the piano or trained for a race? Practicing once 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. Don't be fooled. Some of these ideas are deeper than they may seem at first.
In any case, 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 the 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! As you might imagine, we were all impressed.
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 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 an operator, 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 in Lisp the empty list — ()
— counts as false. In Racket, it 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 parentheses) 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.
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 Thoughts
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 link is now dead now, too, but here is the passage:
The truth is that bicycles and motorcycles operate quite differently than wheeled vehicles that 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 Section 6.3.2 of the Scheme language standard. Stop when you get to Section 6.3.3, unless you also want to read about symbols.
-
Homework
- Homework 2 will be available soon and due in approximately one week. (See the assignment for details.) 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.