New languages come along all the time, sometimes with the sort of corporate oomph that helped Swift and Go become popular quickly.
Most of the reasons why languages are favorites are common from class to class: simplicity, utility, familiarity. This time around, several students mentioned that they like object-oriented programming or large libraries, and that their favorite language supports them well.
I will 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. 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 tag 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. Languages like Ruby would fare as well in this regard.
My favorite 'favorite' comment 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 Java. That says nothing about the language, of course, but it reminds me that solving fun problems is why so many of us like to program at all.
Now we know which programming languages we 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. The new language will help us to achieve the course objectives.
In this course, we will describe languages and their behavior in two ways:
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 running f on E.
I have left many essential questions unanswered. Where do we find E? Do we evaluate the expression? 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. It might not even be 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 and produces its behavior.
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, a program can be unclear. Consider a few possibilities:
We will do our best to circumvent these these sources of ambiguity. 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.
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 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 the number and the 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:
... give examples of each in Python, using the program we saw earlier.
Some languages have more kinds of features than these, but they cover most of the features of most languages. They will serve us well as we learn Racket.
Learn a new language and get a new soul.
-- Czech proverb
(... you may even get a new personality!)
We begin our study of programming languages by learning Racket. This part of the course allows us to do three different things:
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 the programming environment we will use throughout the course.
Some practical things to pay attention to as we work in Dr. Racket:
Racket has two kinds of primitive expressions, both of which will be quite familiar to you:
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 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.
These primitive expressions can be combined to create more complex expressions. There is exactly one means of combination: the operator application. In Racket, all non-primitive expressions have the following features:
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) ; and 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:
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) 2What 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 -.)
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. 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 does not evaluate all of its arguments before executing 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.
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 have an arbitrary 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, perhaps 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 its own 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, "This 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.
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. 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!