







Study with the several resources on Docsity
Earn points by helping other students or get them with a premium plan
Prepare for your exams
Study with the several resources on Docsity
Earn points to download
Earn points by helping other students or get them with a premium plan
Standard Description: This summary covers roughly the same material as lecture and section. It can help to read about the material in a narrative style and ...
Typology: Exercises
1 / 13
This page cannot be seen from the preview
Don't miss anything!








Standard Description: This summary covers roughly the same material as lecture and section. It can help to read about the material in a narrative style and to have the material for an entire unit of the course in a single document, especially when reviewing the material later. Please report errors in these notes, even typos. This summary is not a sufficient substitute for attending class, reading the associated code, etc.
Welcome to Programming Languages................................. 1
ML Expressions and Variable Bindings................................ 1
Using use................................................... 3
Variables are Immutable.......................................... 3
Function Bindings.............................................. 4
Pairs and Other Tuples.......................................... 5
Lists...................................................... 6
Let Expressions............................................... 8
Options.................................................... 9
Some Other Expressions and Operators................................ 11
Lack of Mutation and Benefits Thereof................................ 11
The top of the course web page has four documents with important information not repeated here. They are the syllabus, the academic-integrity policy, the challenge-program policy, and a description of how this course relates to a course offered on Coursera. Read these documents thoroughly.
A course titled, “Programming Languages” can mean many different things. For us, it means the opportunity to learn the fundamental concepts that appear in one form or another in almost every programming language. We will also get some sense of how these concepts “fit together” to provide what programmers need in a language. And we will use different languages to see how they can take complementary approaches to representing these concepts. All of this is intended to make you a better software developer, in any language.
Many people would say this course “teaches” the 3 languages ML, Racket, and Ruby, but that is fairly misleading. We will use these languages to learn various paradigms and concepts because they are well- suited to do so. If our goal were just to make you as productive as possible in these three languages, the course material would be very different. That said, being able to learn new languages and recognize the similarities and differences across languages is an important goal.
Most of the course will use functional programming (both ML and Racket are functional languages), which emphasizes immutable data (no assignment statements) and functions, especially functions that take and return other functions. As we will discuss later in the course, functional programming does some things exactly the opposite of object-oriented programming but also has many similarities. Functional programming is not only a very powerful and elegant approach, but learning it helps you better understand all styles of programming.
The conventional thing to do at the very beginning of a course is to motivate the course, which in this case would explain why you should learn functional programming and more generally why it is worth learning different languages, paradigms, and language concepts. We will largely delay this discussion for a few weeks. It is simply too important to cover when most students are more concerned with getting a sense of what the work in the course will be like, and, more importantly, it is a much easier discussion to have after we have built some shared terminology and experience. Motivation does matter; let’s take a “rain-check” with the promise that it will be well worth it.
So let’s just start “learning ML” but in a way that teaches core programming-languages concepts rather than just “getting down some code that works.” Therefore, pay extremely careful attention to the words used to describe the very, very simple code we start with. We are building a foundation that we will expand very quickly over this week and next week. Do not yet try to relate what you see back to what you already know in other languages as that is likely to lead to struggle.
An ML program is a sequence of bindings. Each binding gets type-checked and then (assuming it type-checks) evaluated. What type (if any) a binding has depends on a static environment,^1 which is roughly the types of the preceding bindings in the file. How a binding is evaluated depends on a dynamic environment, which is roughly the values of the preceding bindings in the file. When we just say environment, we usually mean dynamic environment. Sometimes context is used as a synonym for static environment.
There are several kinds of bindings, but for now let’s consider only a variable binding, which in ML has this syntax :
val x = e;
Here, val is a keyword, x can be any variable, and e can be any expression. We will learn many ways to write expressions. The semicolon is optional in a file, but necessary in the read-eval-print loop to let the interpreter know that you are done typing the binding.
We now know a variable binding’s syntax (how to write it), but we still need to know its semantics (how it type-checks and evaluates). Mostly this depends on the expression e. To type-check a variable binding, we use the “current static environment” (the types of preceding bindings) to type-check e (which will depend on what kind of expression it is) and produce a “new static environment” that is the current static environment except with x having type t where t is the type of e. Evaluation is analogous: To evaluate a variable binding, we use the “current dynamic environment” (the values of preceding bindings) to evaluate e (which will depend on what kind of expression it is) and produce a “new dynamic environment” that is the current environment except with x having the value v where v is the result of evaluating e.
A value is an expression that, “has no more computation to do,” i.e., there is no way to simplify it. As described more generally below, 17 is a value, but 8+9 is not. All values are expressions. Not all expressions are values.
This whole description of what ML programs mean (bindings, expressions, types, values, environments) may seem awfully theoretical or esoteric, but it is exactly the foundation we need to give precise and concise definitions for several different kinds of expressions. Here are several such definitions:
Bindings are immutable. Given val x = 8+9; we produce a dynamic environment where x maps to 17. In this environment, x will always map to 17 ; there is no “assignment statement” in ML for changing what x maps to. That is very useful if you are using x. You can have another binding later, say val x = 19;, but that just creates a different environment where the later binding for x shadows the earlier one. This distinction will be extremely important when we define functions that use variables.
Recall that an ML program is a sequence of bindings. Each binding adds to the static environment (for type-checking subsequent bindings) and to the dynamic environment (for evaluating subsequent bindings). We already introduced variable bindings; we now introduce function bindings, i.e., how to define and use functions. We will then learn how to build up and use larger pieces of data from smaller ones using pairs and lists.
A function is sort of like a method in languages like Java — it is something that is called with arguments and has a body that produces a result. Unlike a method, there is no notion of a class, this, etc. We also do not have things like return statements. A simple example is this function that computes xy^ assuming y ≥ 0:
fun pow (x:int, y:int) = (* correct only for y >= 0 *) if y= then 1 else x * pow(x,y-1)
Syntax:
The syntax for a function binding looks like this (we will generalize this definition a little later in the course):
fun x0 (x1 : t1, ..., xn : tn) = e
This is a binding for a function named x0. It takes n arguments x1, ... xn of types t1, ..., tn and has an expression e for its body. As always, syntax is just syntax — we must define the typing rules and evaluation rules for function bindings. But roughly speaking, in e, the arguments are bound to x1, ... xn and the result of calling x0 is the result of evaluating e.
Type-checking:
To type-check a function binding, we type-check the body e in a static environment that (in addition to all the earlier bindings) maps x1 to t1, ... xn to tn and x0 to t1 * ... * tn -> t. Because x0 is in the environment, we can make recursive function calls, i.e., a function definition can use itself. The syntax of a function type is “argument types” -> “result type” where the argument types are separated by * (which just happens to be the same character used in expressions for multiplication). For the function binding to type-check, the body e must have the type t, i.e., the result type of x0. That makes sense given the evaluation rules below because the result of a function call is the result of evaluating e.
But what, exactly, is t – we never wrote it down? It can be any type, and it is up to the type-checker (part of the language implementation) to figure out what t should be such that using it for the result type of x makes, “everything work out.” For now, we will take it as magical, but type inference (figuring out types not written down) is a very cool feature of ML discussed later in the course. It turns out that in ML you
almost never have to write down types. Soon the argument types t1, ..., tn will also be optional but not until we learn pattern matching a little later.^2
After a function binding, x0 is added to the static environment with its type. The arguments are not added to the top-level static environment — they can be used only in the function body.
Evaluation:
The evaluation rule for a function binding is trivial: A function is a value — we simply add x0 to the envi- ronment as a function that can be called later. As expected for recursion, x0 is in the dynamic environment in the function body and for subsequent bindings (but not, unlike in say Java, for preceding bindings, so the order you define functions is very important).
Function calls:
Function bindings are useful only with function calls, a new kind of expression. The syntax is e0 (e1,...,en) with the parentheses optional if there is exactly one argument. The typing rules require that e0 has a type that looks like t1...tn->t and for 1 ≤ i ≤ n, ei has type ti. Then the whole call has type t. Hopefully, this is not too surprising. For the evaluation rules, we use the environment at the point of the call to evaluate e0 to v0, e1 to v1, ..., en to vn. Then v0 must be a function (it will be assuming the call type-checked) and we evaluate the function’s body in an environment extended such that the function arguments map to v1, ..., vn.
Exactly which environment is it we extend with the arguments? The environment that “was current” when the function was defined, not the one where it is being called. This distinction will not arise right now, but we will discuss it in great detail later.
Putting all this together, we can determine that this code will produce an environment where ans is 64 :
fun pow (x:int, y:int) = (* correct only for y >= 0 *) if y= then 1 else x * pow(x,y-1)
fun cube (x:int) = pow(x,3)
val ans = cube(4)
Programming languages need ways to build compound data out of simpler data. The first way we will learn about in ML is pairs. The syntax to build a pair is (e1,e2) which evaluates e1 to v1 and e2 to v2 and makes the pair of values (v1,v2), which is itself a value. Since v1 and/or v2 could themselves be pairs (possibly holding other pairs, etc.), we can build data with several “basic” values, not just two, say, integers. The type of a pair is t1*t2 where t1 is the type of the first part and t2 is the type of the second part.
Just like making functions is useful only if we can call them, making pairs is useful only if we can later retrieve the pieces. Until we learn pattern-matching, we will use #1 and #2 to access the first and second part. The typing rule for #1 e or #2 e should not be a surprise: e must have some type that looks like ta * tb and then #1 e has type ta and #2 e has type tb.
Here are several example functions using pairs. div_mod is perhaps the most interesting because it uses a (^2) The way we are using pair-reading constructs like #1 in this unit and Homework 1 requires these explicit types.
Here are some simple examples of functions that take or return lists:
fun sum_list (xs : int list) = if null xs then 0 else hd(xs) + sum_list(tl xs)
fun countdown (x : int) = if x= then [] else x :: countdown(x-1)
fun append (xs : int list, ys : int list) = if null xs then ys else (hd xs) :: append(tl xs, ys)
Functions that make and use lists are almost always recursive because a list has an unknown length. To write a recursive function, the thought process involves thinking about the base case — for example, what should the answer be for an empty list — and the recursive case — how can the answer be expressed in terms of the answer for the rest of the list.
When you think this way, many problems become much simpler in a way that surprises people who are used to thinking about while loops and assignment statements. A great example is the append function above that takes two lists and produces a list that is one list appended to the other. This code implements an elegant recursive algorithm: If the first list is empty, then we can append by just evaluating to the second list. Otherwise, we can append the tail of the first list to the second list. That is almost the right answer, but we need to “cons on” (using :: has been called “consing” for decades) the first element of the first list. There is nothing magical here — we keep making recursive calls with shorter and shorter first lists and then as the recursive calls complete we add back on the list elements removed for the recursive calls.
Finally, we can combine pairs and lists however we want without having to add any new features to our language. For example, here are several functions that take a list of pairs of integers. Notice how the last function reuses earlier functions to allow for a very short solution. This is very common in functional programming. In fact, it should bother us that firsts and seconds are so similar but we do not have them share any code. We will learn how to fix that later.
fun sum_pair_list (xs : (int * int) list) = if null xs then 0 else #1 (hd xs) + #2 (hd xs) + sum_pair_list(tl xs)
fun firsts (xs : (int * int) list) = if null xs then [] else (#1 (hd xs))::(firsts(tl xs))
fun seconds (xs : (int * int) list) =
if null xs then [] else (#2 (hd xs))::(seconds(tl xs))
fun sum_pair_list2 (xs : (int * int) list) = (sum_list (firsts xs)) + (sum_list (seconds xs))
Let-expressions are an absolutely crucial feature that allows for local variables in a very simple, general, and flexible way. Let-expressions are crucial for style and for efficiency. A let-expression lets us have local variables. In fact, it lets us have local bindings of any sort, including function bindings. Because it is a kind of expression, it can appear anywhere an expression can.
Syntactically, a let-expression is:
let b1 b2 ... bn in e end
where each bi is a binding and e is an expression.
The type-checking and semantics of a let-expression are much like the semantics of the top-level bindings in our ML program. We evaluate each binding in turn, creating a larger environment for the subsequent bindings. So we can use all the earlier bindings for the later ones, and we can use them all for e. We call the scope of a binding “where it can be used,” so the scope of a binding in a let-expression is the later bindings in that let-expression and the “body” of the let-expression (the e). The value e evaluates to is the value for the entire let-expression, and, unsurprisingly, the type of e is the type for the entire let-expression.
For example, this expression evaluates to 7; notice how one inner binding for x shadows an outer one.
let val x = 1 in (let val x = 2 in x+1 end) + (let val y = x+2 in y+1 end) end
Also notice how let-expressions are expressions so they can appear as a subexpression in an addition (though this example is silly and bad style because it is hard to read).
Let-expressions can bind functions too, since functions are just another kind of binding. If a helper function is needed by only one other function and is unlikely to be useful elsewhere, it is good style to bind it locally. For example, here we use a local helper function to help produce the list [1,2,...,x]:
fun countup_from1 (x:int) = let fun count (from:int, to:int) = if from=to then to::[] else from :: count(from+1,to) in count(1,x) end
However, we can do better. When we evaluate a call to count, we evaluate count’s body in a dynamic environment that is the environment where count was defined, extended with bindings for count’s arguments.
The previous example does not properly handle the empty list — it returns 0. This is bad style because 0 is really not the maximum value of 0 numbers. There is no good answer, but we should deal with this case reasonably. One possibility is to raise an exception; you can learn about ML exceptions on your own if you are interested before we discuss them later in the course. Instead, let’s change the return type to either return the maximum number or indicate the input list was empty so there is no maximum. Given the constructs we have, we could “code this up” by return an int list, using [] if the input was the empty list and a list with one integer (the maximum) if the input list was not empty.
While that works, lists are “overkill” — we will always return a list with 0 or 1 elements. So a list is not really a precise description of what we are returning. The ML library has “options” which are a precise description: an option value has either 0 or 1 thing: NONE is an option value “carrying nothing” whereas SOME e evaluates e to a value v and becomes the option carrying the one value v. The type of NONE is ’a option and the type of SOME e is t option if e has type t.
Given a value, how do you use it? Just like we have null to see if a list is empty, we have isSome which evaluates to false if its argument is NONE. Just like we have hd and tl to get parts of lists (raising an exception for the empty list), we have valOf to get the value carried by SOME (raising an exception for NONE).
Using options, here is a better version with return type int option:
fun better_max (xs : int list) = if null xs then NONE else let val tl_ans = better_max(tl xs) in if isSome tl_ans andalso valOf tl_ans > hd xs then tl_ans else SOME (hd xs) end
The version above works just fine and is a reasonable recursive function because it does not repeat any potentially expensive computations. But it is both awkward and a little inefficient to have each recursive call except the last one create an option with SOME just to have its caller access the value underneath. Here is an alternative approach where we use a local helper function for non-empty lists and then just have the outer function return an option. Notice the helper function would raise an exception if called with [], but since it is defined locally, we can be sure that will never happen.
fun better_max2 (xs : int list) = if null xs then NONE else let (* fine to assume argument nonempty because it is local ) fun max_nonempty (xs : int list) = if null (tl xs) ( xs must not be [] *) then hd xs else let val tl_ans = max_nonempty(tl xs) in if hd xs > tl_ans then hd xs else tl_ans end
in SOME (max_nonempty xs) end
ML has all the arithmetic and logical operators you need, but the syntax is sometimes different than in most languages. Here is a brief list of some additional forms of expressions we will find useful:
In ML, there is no way to change the contents of a binding, a tuple, or a list. If x maps to some value like the list of pairs [(3,4),(7,9)] in some environment, then x will forever map to that list in that environment. There is no assignment statement that changes x to map to a different list. (You can introduce a new binding that shadows x, but that will not affect any code that looks up the “original” x in an environment.) There is no assignment statement that lets you change the head or tail of a list. And there is no assignment statement that lets you change the contents of a tuple. So we have constructs for building compound data and accessing the pieces, but no constructs for mutating the data we have built.
This is a really powerful feature! That may surprise you: how can a language not having something be a feature? Because if there is no such feature, then when you are writing your code you can rely on no other code doing something that would make your code wrong, incomplete, or difficult to use. Having
The append example is very similar to the sort_pair example, but it is even more compelling because it is hard to keep track of potential aliasing if you have many lists of potentially large lengths. If I append [1,2] to [3,4,5], I will get some list [1,2,3,4,5] but if later someone can change the [3,4,5] list to be [3,7,5] is the appended list still [1,2,3,4,5] or is it now [1,2,3,7,5]?
In the analogous Java program, this is a crucial question, which is why Java programmers must obsess over when references to old objects are used and when new objects are created. There are times when obsessing over aliasing is the right thing to do and times when avoiding mutation is the right thing to do — functional programming will help you get better at the latter.
For a final example, the following Java is the key idea behind an actual security hole in an important (and subsequently fixed) Java library. Suppose we are maintaining permissions for who is allowed to access something like a file on the disk. It is fine to let everyone see who has permission, but clearly only those that do have permission can actually use the resource. Consider this wrong code (some parts omitted if not relevant):
class ProtectedResource { private Resource theResource = ...; private String[] allowedUsers = ...; public String[] getAllowedUsers() { return allowedUsers; } public String currentUser() { ... } public void useTheResource() { for(int i=0; i < allowedUsers.length; i++) { if(currentUser().equals(allowedUsers[i])) { ... // access allowed: use it return; } } throw new IllegalAccessException(); } }
Can you find the problem? Here it is: getAllowedUsers returns an alias to the allowedUsers array, so any user can gain access by doing getAllowedUsers()[0] = currentUser(). Oops! This would not be possible if we had some sort of array in Java that did not allow its contents to be updated. Instead, in Java we often have to remember to make a copy. The correction below shows an explicit loop to show in detail what must be done, but better style would be to use a library method like System.arraycopy or similar methods in the Arrays class — these library methods exist because array copying is necessarily common, in part due to mutation.
public String[] getAllowedUsers() { String[] copy = new String[allowedUsers.length]; for(int i=0; i < allowedUsers.length; i++) copy[i] = allowedUsers[i]; return copy; }