Session 2
Learning a New Language: Racket
And the Survey Says... Your Favorite Programming Languages
The numbers.
- Eleven different languages are known by at least two of you. This is similar to 2021's Programming Languages class (12) and like most semesters. This number has reached as high as fourteen in the past.
-
Python is far and away
the most common favorite language,
with 16 (80%) of you favoring it and Java (2) far, far behind.
This is the widest gap between favotite and also-rans than ever
before, when Python usually gathers 1/2 of the votes and Java
around 1/4. Beyond Python and Java, only C++ and R garnered a
vote. Before 2023, 6-8 different languages were usually listed
as favorites, though that has been changing in recent years.
Our students tend to prefer Python these days.
Alas, no one listed Smalltalk or Scheme (my favorites, as I admitted last time), or Pascal or PL/I, my favorites while in college. - As in past years, I allowed SQL as an entry. SQL is not a programming language in the same way that the others on the list are, though the implementations most of you know are. As in past years, I did not count HTML or CSS, but I am weakening... HTML5 + CSS3 together are Turing complete, and I'm becoming less judgmental in my old age.
- Ada, VB, and bash were among the languages that fell off the list from last year. PHP fell off the list from last year. Go, and Matlab remain no-shows.
- PHP and Kotlin reappeared this year after a year off. Brand new to the list this year was Scratch.
- There were several big movers up the list this year: C, SQL, and especially R and C++. Data science is beginning to have an effect at UNI.
- Perl did not appear on the list year. Larry Wall will be sad.
New languages come along all the time, sometimes with the sort of corporate oomph that helped Swift and Go become popular quickly.
My new favorite answer to "Where did you learn the language" is Learned for my significant other so I could talk to them about their work at [another school].
Most of the reasons why languages are favorites are common from class to class: simplicity, utility, familiarity. Simplicity dominated this year, which is not surprising given Python's control of the board.
One person even said "because it's popular". Being popular means having a big community of users and many sources of help.
I note that many of the features that you listed as reasons you like a language are not strictly language features. They are "second order" features at best, conclusions you draw based on actual features of the languages and your own experience. Simplicity, utility, and familiarity are about you as much as they are about the language. One of the goals of this course is to give you a larger vocabulary for talking about what you like or dislike about a language.
That said, Python probably does owe a lot of its popularity to how easy it is to whip up useful programs quickly. For example, to generate the word cloud above from the list of languages and their counts, I needed a text file containing each language's name as often as it appeared in the survey. It took me only a couple of minutes to write this Python program to generate the text file for me. This combination of utility and programming speed stands out starkly in comparison to Java, C, and C++, the other languages most of you know. Other scripting languages like Ruby and Perl fare well in this regard, too.
One 'favorite' comment I like is: "... because it is the only one I know". This perfectly reasonable, and the only answer possible if you know only one language, or know only one language well. I hope that this course, and the others you take in our program, give every CS student the chance to make a reasoned choice from a much larger set of choices. Knowing more languages won't diminish your love for your favorite; it will give depth to your preference.
My favorite answer of all time to "Why is this language your favorite?" was I liked the challenges we had to solve while I was learning it. That doesn't say much about the language, of course, but it reminds me that solving fun problems is why so many of us like to program at all.
~~~~~
We have now seen which programming languages you know. Let's begin to learn what we can know about programming languages and learn a new one as both a case study and a tool. Our goal is to develop a scientific mindset for programming languages. We seek to explore the basic building blocks of programs and some of the fundamental rules of composition.
Describing Programming Languages
In this course, we will describe languages and their behavior in two ways:
- using English text, to motivate the ideas and design issues, and
- using computer programs, to specify the meaning of a language using an algorithm.
We will also use a little mathematics, but we could certainly use even more to reason about programs and computation. That is not a major concern of this course, though.
Human language works well for introducing ideas because it lets us tell stories that people understand quickly. People are motivated by good stories. In this course, I'll try to give you simple, understandable English descriptions of programming languages and their features.
Why, then, do we need to use programs, or mathematics, or some other kind of language, to describe programming languages?
English allows too much ambiguity. For example, suppose that I tell you:
The meaning of a procedure call,f(E)
, is runningf
onE
.
I have left many essential questions unanswered. Where do we
find the expression E
? Do we evaluate it? Is
E
passed by value or by reference, or by some
other protocol? We can ask many of the same questions about
f
as well! Unless you are already a programmer
who has written and executed functions, it probably is not even
clear what we mean when we say "running"!
We need a language with formal semantics in order to eliminate such ambiguities. For this reason, we will use computer programs as our main way of explaining what sentences in a language mean. In particular, we will use an interpreter, which takes a sentence in a language and produces its behavior. Sometimes, we will use an interpreter that is given to us, and sometimes we will write our own interpreter to demonstrate an idea, or to experiment.
But wait a minute... Can a program be unclear as an explanation? It surely can! As you are first learning Racket this semester, you may well think that some of the Racket programs you read are unclear. Eventually, though, you will understand Racket better and not be confused by lack of experience.
Even when we are working in a familiar programming language, though, a program can be unclear. Consider a few possibilities:
-
undisciplined transfers of control
—
goto
s and breaks from long blocks - side effects on global variables — or to local variables, if pervasive
- implicit arguments to functions — by using default values or by accessing globals
- too much detail
- too many small pieces
We will do our best to circumvent these sources of ambiguity this semester. First, we do not use the first three in our interpreters. Second, we try to avoid the effects of the fourth through abstraction: the use of sub-programs and high-level data structures. Finally, we will try not to create so many sub-programs that we can't see meaningful behavior.
This is where functional programming and Racket come in handy as tools. Functional programming encourages decomposition into smallish procedures, with no assignment statements, and Racket's flexibility allows us to abstract in ways that other languages do not.
Learning a New Language
Often, when we learn a new language, we dive immediately into its syntax, its operators, and its data types and try to make sense of them based on the languages we already know. That may work when the new language is enough like the languages we know that we both find what we expect and understand what we find. But programming languages are much too diverse for us to rely only on our past experience. We need a set of principles to guide us.
Let us begin our study of programming languages with a question:
What kinds of things must be present in every programming language?
Your answer to this question is almost certainly limited by the languages you know now, both their number and their variety. If you learn only one language, or one kind of language, then your perspective on what is essential will be determined by those experiences.
When we survey the full range of languages, we find 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 expressions
... give examples of each in Python, using the program we saw earlier.
Some languages have more kinds of features than these, but this list covers most of the features of most languages. It will serve us well as we learn Racket.
Learning Racket: Prelude
Learn a new language and get a new soul.
— Czech proverb
We begin our study of programming languages by learning Racket. This part of the course allows us to do three different things:
- As you learn the language, we can discuss it in the terms we will use to study languages in general. This provides us with a concrete example through which to learn the terminology and techniques of PL study.
- Racket helps us to introduce you to a second (or third) programming style: functional programming.
- We can use Racket as a tool for studying languages and paradigms by simulating their features in Racket programs.
Let's use the taxonomy of three things every programming language has to learn Racket. It will help us to understand the features of every Racket program and to categorize the things we learn about the language. It will also illustrate the three things every programming language has with concrete examples in Racket, clearing up the ambiguity you may see in the English descriptions.
To make things even more concrete, we will explore these features of Racket using an interpreter. We will type Racket expressions and let the interpreter evaluate them for us. Using Dr. Racket will also introduce you to the programming environment we will use throughout the course.
Some practical things to pay attention to as we work in Dr. Racket:
- using the Interactions window and the Definitions window
- working with files: open, execute, and evaluate
-
<ctrl><up>
and<ctrl><down>
— on your platform, this may be<up>
or<ctrl><P>
Learning Racket: Primitive Expressions
Racket has two kinds of primitive expressions, both of which will be quite familiar to you:
- literal expressions: numbers, booleans, characters, strings, and symbols are among these
- named expressions: symbols that stand in the place of particular data values
Every primitive expression has a value.
Whenever we enter an expression at Dr. Racket's prompt, the interpreter evaluates the expression and prints the result. Because a literal is a literal value, the value is the same as the expression.
> 25 ;; a number 25 > 1.2 ;; handles integers and floats in the same way 1.2 > #t ;; a boolean ... also #f #t > #\a ;; a character ... we won't use these much #\a > "Eugene" ;; a string ... ditto "Eugene" > 'a ;; a symbol -- both identifier and value ... a ;; we use these a lot as data! Notice the quote. > 'a-symbol ;; a symbol -- Racket has fewer constraints on what a-symbol ;; can be a symbol than most other languages > '123->321 ;; see what I mean? 123->321
Symbols serve as identifiers in Racket. That is, a Racket symbol can name a value. We will talk quite a bit more about definitions, identifiers, and values soon.
The set of values that can be given a name includes all literal expressions, as well as higher-order objects. The most surprising of these to you may be functions, which we will explore soon in depth. For now, though, note that some identifiers have values when we first start a Racket session:
> min #<procedure:min> ; a primitive function on numbers > not #<procedure:not> ; a primitive function on booleans > string-length #<procedure:string-length> ; a primitive function on strings > list #<procedure:mlist> ; a primitive function on any args > + #<procedure:+> ; even + is a function!
These are some of Racket's primitive functions, the built-in behaviors provided by the language.
Here we see an important behavior of Racket interpreter: it evaluates every expression it reads. Primitive objects evaluate to themselves, and the "print form" of the object (what we see in an answer) is usually the same as the form we write. Identifiers evaluate to the value that they name.
In Racket, functions are named by symbols, just like any other values. That is correct: all Racket functions, even the primitive operation for adding numbers. This turns out to be a remarkably powerful and useful idea, one that we'll come back to later.
Recall that symbols are a valid literal expression in Racket. We type symbol literals with a single quote upfront. What happens, though, if we evaluate a symbol that is not being used as an identifier?
> 'upper upper > upper upper: undefined; cannot reference an identifier before its definiton
We'll learn about definitions in a few minutes.
Learning Racket: Means of Combination
Primitive expressions can be combined to create more complex expressions. There is exactly one means of combination: operator application. In Racket, all non-primitive expressions have the following features:
- They are expressed using prefix notation: the operator comes first, followed by any arguments to the operator.
- Arguments are separated by spaces, not commas.
- They are fully parenthesized, so there is no ambiguity to evaluation. Because Racket uses prefix notation and treats almost everything as data, this is essential.
The syntax of an application expression is:
(<operator> <operand1> <operand2> <operand3> ...)
Here are some examples in Dr. Racket:
> (* 2 2) 4 > (- 4 2) 2 > (+ 3 5.2) ; handles integers and floats with equanimity 8.2 > (/ 4 2) 2 > (/ 1 3) ; rationals are numbers, too! 1/3 > (- -3 -5) 2 > (min 1 3 5 1000 -1 10000) -1
There are several important points for you to note about our Racket interactions. First, note that all compound expressions conform to our definition: each is a parenthesized prefix expression, with a leftmost element that is an operator, followed by the operands. The Racket evaluator determines the value of the expression by applying the function specified by the operator to the values specified by the operands.
Most of the operators we use are functions. To evaluate a compound expression built with a function, we evaluate each of the operands and pass their values to the function, which then produces the value of the expression.
This means there are no "statements" in Racket. Every compound expression is formed like any other. Every expression has a value, and nearly every value has a printable form. This is an important difference from most other languages you know, because we will use these values to drive our programming in Racket.
Racket's only mechanism for building compound expressions is the fully-parenthesized prefix expression. The parentheses are not optional; a compound expression is always enclosed in parentheses. This is probably different from your experience with other programming languages, where parentheses are usually optional. Keep this in mind always:
You cannot add parentheses to or delete parentheses
from any Racket expression without changing its meaning.
When programming in Racket, randomly inserting or deleting parentheses will get you nowhere, more so and faster than in other languages. Think about what you want to say, and ask questions if you don't know how to make it work.
Consider this Racket expression:
> (- -3 -5) 2
What happens if we insert a pair of parentheses somewhere, anywhere?
Earlier, I said that the Racket evaluator determines the value of an expression by applying the function specified by the operator to the values specified by the operands.
That sentence is extremely important, and more complicated than you might think at first, so make sure you understand what it says:
The Racket evaluator determines the value of the expression by applying the function specified by the operator to the values specified by the operands.
Think about this. We will come back to it in a few sessions.
Notice that in the last subtraction expression, the "-" occurs
three times and means two different things. When it's the
leftmost element in the expression, it represents the operation
that is to take place; when its appended to the front of a
number, it means that the number is negative. Spacing is
important here: (- - 3 -5)
would produce an error:
> (- - 3 -5) -: contract violation expected: number? given: #<procedure:-> argument position: 1st other arguments...: 3 -5
This points out a feature of Racket we will talk more about soon:
we are allowed to pass functions as arguments to other functions!
(But not to the subtraction function, -
.)
Learning Racket: Means of Abstraction
Finally, Racket has a number of mechanisms for abstraction.
For now, we will focus on just one: the ability to give a
name to a value. We create names in the same way we
create any compound expression, using a fully-parenthesized
prefix expression. To name a value, we use the operator
define
:
(define <name> <expression>)
When we define a new name, we're telling Racket to substitute a value (perhaps created by a computation) wherever it sees the identifier. For example,
(define upper 10)
Thereafter, whenever the Racket interpreter sees
upper
, it will replace it with the value 10.
This completes the picture we saw earlier in which a symbol can be both a literal value and the name for another value:
> 'upper upper > upper upper: undefined; cannot reference an identifier before its definiton > (define upper 10) > upper 10
The value being named doesn't have to be a literal expression. It can be a value computed by another compound expression:
(define area (* 10 4))
The <expression>
is evaluated and associated
with the <name>
. We evaluate a definition for
its side effect -- the naming of a value -- and not for
its own value. That's the reason Dr. Racket does not print a
value for the define
expression: we don't really
care what its value is.
For the most part, definition is the only form of side effect we will use in this course. It is how we name our programs, the top-level data used by our programs, and the data we use for testing.
Notice that define
is not a function,
because it does not evaluate all of its arguments. The first
argument is taken literally, as the symbol to be used as the
name. In Racket, we call an operator that has its own evaluation
rule a special form. In a session or two, we will return
to the idea of a special form and consider it in more detail.
Soon, we will use another special form, lambda
, to
create our own functions. ... as a teaser, I showed a
simple function for the sum of squares and used define
to name it sum-of-squares
.
More on Prefix Notation
A common question I receive is Why does Racket use prefix notation? Students usually already know infix notation for operators. This seems like an unnecessary complication.
Prefix notation has several advantages over other notations, such as infix and postfix. Two stand out.
First, using prefix notation, we can pass any number of
arguments without repeating the operator. For example, it
is easy to write (+ 3 4 8 6 5 4 7 6 5 8 9)
using
prefix notation. In other notations, this expression would
be longer and involve 11 - 1 = 10 operator symbols. (And,
even worse, ten separate evaluations!) This, despite the fact
that children as young as second grade learn how to operate on
multiple values as a single operation:
3 4 8 6 5 4 7 6 5 8 + 9 --- 65
Second, we don't have to worry about issues of precedence when
we use prefix notation. For example, in the expression
3 + 4 * 5 / 6 - 7
, which operation is performed
first? You may know, because as a young child you learned
arithmetic and memorized some rules. Or perhaps you know a
programming language with specific rules for evaluating such
expressions. Soon you learn that different programming
languages use different rules. You'll have to memorize those,
too.
Using prefix notation, however, the issues of precedence are
obvious without anyone memorizing precedence rules. The above
example would be written (- (+ 3 (/ (* 4 5) 6)) 7)
in prefix notation.
Now, you may be saying to yourself at this point, "That expression isn't clear at all". And that's probably true. Reading it requires that we pay attention to different details than we are used to. But we can determine the value of the expression by considering only the expression itself; we don't need to consult or memorize external rules. I think you will find your comfort level with such expressions is mostly a matter of exposure. You will be as comfortable with this system as any other after you use it for a while.
There is another reason for using prefix notation in a language like Racket, though. Recall: In Racket, arithmetic operators, and all the other operators you are used to, are functions! Using prefix notation for them is simply being consistent. This will give us some unexpected benefits later, when we decide to swap out an operator and replace it with a function or a different operator. In those cases, having predefined operators and user-defined functions be interchangeable will be a huge win.
More Numbers in Racket
In our evaluations above, we saw that Racket's numeric functions accept both integers and real numbers without any explicit type coercion or casting. The result of adding 3 to 5.2 is 8.2. That's what most people would say. The result of dividing 1 by 3 is 1/3, a fraction, or a rational number. This, too, is obvious to people with no programming experience. Rational numbers are a data type in Racket.
Other computer languages usually make all sorts of distinctions among different types of numbers, but those distinctions are driven by the implementation of the language on physical computers, and not by our understanding of numbers.
Somehow, we programmers become conditioned by our languages into thinking that these distinctions are a necessary ones. They are not. Racket characterizes numbers as exact or inexact and makes distinctions in behavior driven by this mathematical idea.
If you need one more example of how Racket hides implementation details about its numbers, let's execute this Racket program to compute the factorial of a number. Try that in Java [ loop or recursive ], Python or Ada... The numbers overflow their datatypes quickly.
(With a different version of the program, we can squeeze a few more digits out before Dr. Racket bogs down in garbage collection. Check out this file for a few sample runs.)
Don't worry about the details of these Racket functions for a while; we'll learn how to write such functions soon enough. The key here is that Racket lets us compute with numbers the way we think about numbers, not the way our computer thinks about them. This will take some getting used to; old habits die hard.
NOTE: These programs, along with the sample interactions, are available in the.zip
file for today's session. I'll bundle up a.zip
file of code for you with almost every class session. Be sure to download the code, study it, run it, and modify it. That's the best way to learn the ideas we are studying!
Wrap Up
-
Reading
- Read through these lecture notes. They discuss a couple of small ideas that we didn't get to in class.
- Read a short section on the read-eval-print loop that is the heart of the Racket interpreter.
- Browse Chapter 3 of The Racket Guide, through the end of Section 3.6. Stop there, because it gets a bit too deep for us yet starting in Section 3.7. These sections contain some material we haven't discussed in class yet. Don't worry; we will be covering those ideas soon.
-
Homework
- Homework 1 is available and due by the beginning of our next session.