Lexical scope is a static property, which is why it is sometimes referred to as static scope (e.g., in the wikipedia entry about scope). Lexical scope will be familiar because Java uses it. Consider, for example, the following program:
public class Scope { public static int x = 10; public static void main(String[] args) { System.out.println(x); if (x > 0) { int x = 20; System.out.println(x); } int x = -30; System.out.println(x); } }In Java, every set of curly braces introduces a new lexical scope (a new region of the program known as a block). In the program above, there is an outer scope for the overall class and an inner scope for method main and for the if:
and inner scopes for each of the three methods:
+-------------------------------------------------+ | public class Scope { | | public static int x = 10; | | +------------------+ | | public static void main|(String[] args) { | | | +------------------------+ | | | | System.out.println(x); | | | | +----------------+ | | | | if (x > 0)| { | | | | | +-----------+ | | | | | | int x = 20; | | | | | | System.out.println(x); | | | | | | } | | | | | +----------------------------+ | | | | int x = -30; | | | | System.out.println(x); | | | | } | | | +-------------------------------------------+ | | } | +-------------------------------------------------+We want to pay attention to the identifier "x" as it is used in each of these scopes. There is a global variable x declared in the outer scope that is printed by the first call on println. There is a local x that shadows the global x that is defined inside the if block and is printed by the second println. And there is a different local x that shadows the global x and that is printed by the third println. So the output of this program is 10, 20, -30.
I then asked people to think about how scope works in OCaml. OCaml uses lexical scoping, but where do the scopes come from? Function declarations introduce a scope for the parameters that are listed for the function, as in:
let f(m, n) = [this creates a local scope for identifiers m and n] (* m and n are not visible here after the function definition *) fun x -> [this creates a local scope for identifier x]We also get a different local scope for each let construct. You can think of each "let" as introducing its own scope box. Because there can be let constructs inside of let constructs, we can have scopes inside of scopes.
I then briefly discussed the idea of dynamic scope. Dynamic scope is less common in programming languages today. If you're writing an interpreter, it's easier to do dynamic scoping, so it was common in the early days for interpretted languages to use it. But most people found it sufficiently confusing that it is rarely used in modern languages.
I mentioned that I'm not expecting everyone to master the dynamic scope approach. But there are some languages that still use it. For example, most shell scripts use dynamic scope. I asked people to consider this script:
#!/bin/sh x="hello" foo() { echo $x } bar() { local x="foo" foo } foo bar echo $xUsing lexical scope rules, we would expect that foo always prints the global x:
+-----------------------+ | #!/bin/sh | | | | x="hello" | | | | foo() | | +-------------+ | | | { | | | | echo $x | | | | } | | | +-------------+ | | | | bar() | | +-------------------+ | | | { | | | | local x="foo" | | | | foo | | | | } | | | +-------------------+ | | | | foo | | bar | | echo $x | +-----------------------+But the shell script uses dynamic scope. In dynamic scope, you keep track of the commands in the order that they are executed. Each time you call a function, that opens a new dynamic scope. So the scopes that are generated by this script are quite different from the lexical scope:
+-----------------------+ | #!/bin/sh | | | | x="hello" | | | | foo | | +-------------+ | | | { | | | | echo $x | | | | } | | | +-------------+ | | | bar | | +-------------------+ | | | { | | | | local x="foo" | | | | foo | | | | +-------------+ | | | | { | | | | | echo $x | | | | | } | | | | +-------------+ | | | } | | +-------------------+ | | | | echo $x | +-----------------------+Notice that when we call bar and it calls foo, that creates an inner dynamic scope for foo. When it goes to echo $x, it looks to the containing dynamic scope for bar and finds a different definition for $x than in the simple call on foo. So this script produces the following output:
hello foo helloI then spent some time discussing why it is difficult in a language like Java to create a closure. I first showed some code that used inheritance to define a variation of the Stack class that overrides the push method to call super.push twice:
import java.util.*; class MyStack extends Stack<Integer> { public Integer push(Integer n) { super.push(n); return super.push(n); } } public class StackStuff { public static void main(String[] args) { Stack<Integer> s = new MyStack(); s.push(8); s.push(10); s.push(17); s.push(24); while (!s.isEmpty()) { System.out.println(s.pop()); } } }When we compiled and ran it, we could see that every value was being duplicated in the stack:
24 24 17 17 10 10 8 8I mentioned that in Java you can define an anonymous inner class that uses inheritance the same way that the MyStack class does:
import java.util.*; public class StackStuff2 { public static void main(String[] args) { Stack<Integer> s = new Stack<>() { public Integer push(Integer n) { super.push(n); return super.push(n); } }; s.push(8); s.push(10); s.push(17); s.push(24); while (!s.isEmpty()) { System.out.println(s.pop()); } } }This class had the same behavior. But what if we introduce a local variable and try to access it inside the inner class? That would introduce a free variable.
public static void main(String[] args) { int x = 3; Stack<Integer> s = new Stack<>() { public Integer push(Integer n) { super.push(x); super.push(n); return super.push(n); } }; ...This is very tricky because x is a stack-allocated variable. How can the inner class preserve access to it? It would be even worse if this code appeared in a method called by main that returned a reference to the stack that it has constructed. That method's local variables would be freed up, so how could the stack keep a reference to a variable that no longer exists?
But this compiled and ran and behaved as expected by pushing a 3 on top of the stack along with two copies of each value. That would seem to be a closure. This inner class seems to be preserving the environment in which it was defined, including that free variable x that was a local variable in main. But looks can be deceiving. I asked if anyone could think of a way to test whether it really is keeping track of that local variable and someone suggested that we could change x later in the program:
public static void main(String[] args) { int x = 3; Stack<Integer> s = new Stack<>() { public Integer push(Integer n) { super.push(x); super.push(n); return super.push(n); } }; s.push(8); x = 10; s.push(10); ...This program did not compile. The compiler said:
StackStuff2.java:8: error: local variables referenced from an inner class must be final or effectively finalIn other words, Java is cheating. It treated our local variable x as if it were a constant. That means it wasn't a free variable at all. Java does not have true closures. C#, on the other hand, has true closures and there is a lot of work that goes into making that work.
This is yet another example of mutable state causing difficulty that immutability resolves. It's difficult to provide access to a local variable of a method, but it's not difficult to provide access to an immutable constant.
Then I spent some time discussing modules. I mentioned that the languages that are most familiar to us today come from Algol, which was created in the late 1950s. Algol had many great features (what was generally called "structured programming") many of which could be described as "modular" programming. The key ideas behind modularity are:
From Algol we got Pascal, C, and C++, none of which had a good module capability initially (C++ has been changing over the years and C++20 seems to have a module construct). We got Ada as well, which did have a robust module construct, but it was unpopular because it had been invented by the Department of Defense.
In the late 1980s we started seeing languages like Modula 2, which was invented by the same researcher who invented Pascal. The main difference was that Modula 2 had modules. When Java came along soon afterwards, they included something like modules, although in a funny way.
Java has packages, which are collections of classes and interfaces. There are also mechanisms for information hiding within a package, which is good. But we normally think of a module as including functions and variables as well. In Java we collect those things together by putting them in a class. What would normally be free-standing functions and variables in a module become static elements of a class. The Math class in Java is a classic example. It has static variables Math.PI and Math.E and static methods like Math.sin, Math.cos, and Math.abs.
We briefly discussed some of the benefits of modules:
When you write a large amount of code you find yourself having trouble managing the name space. For example, what if you load two different OCaml files that each have a function with a particular name? The second file loaded wipes out the definition from the first file. And what if you have a utility like to_string that you want to implement for many different types? A structure provides a way to establish independent namespaces. For example, OCaml has a function called List.length that is included in a module called List and also a function called Array.length that is included in a module called Array.
The basic form of a structure is:
So our overall structure will look like this:
module Rational = struct (* we'll fill in definitions here *) endWe began with a type definition. Rational numbers are numbers that can be expressed as a ratio of two integers, so it would make sense to store them as a tuple of two ints. But most of us don't think of integers like 23 as a tuple. We know that 23 is a rational number, but we don't like to think of it as being "23 divided by 1". So we included two different cases, one for whole numbers and one for rationals that we need to express as a fraction:
module Rational = struct type rational = Whole of int | Fraction of int * int endIf we were providing a full-blown implementation, we'd include functions for adding, subtracting, multiplying and dividing such numbers. For our purposes, we'll implement just an add method as a way to explore these issues.
The signature for the add function is that it takes a tuple of rationals (rational * rational) and it returns a rational. Because there are two forms for a rational, we end up with four total cases for add:
let add(r1, r2) = match (r1, r2) with | (Whole i, Whole j) -> ?? | (Whole i, Fraction(a, b)) -> ?? | (Fraction(a, b), Whole i) -> ?? | (Fraction(a1, b1), Fraction(a2, b2)) -> ??It took us a while to work out the math, but together we filled in the following definitions:
let add(r1, r2) = match (r1, r2) with match (r1, r2) with | (Whole i, Whole j) -> Whole(i + j) | (Whole i, Fraction(a, b)) -> Fraction(i * b + a, b) | (Fraction(a, b), Whole(i)) -> Fraction(i * b + a, b) | (Fraction(a1, b1), Fraction(a2, b2)) -> Fraction(a1 * b2 + a2 * b1, b1 * b2)We then added a to_string function:
let to_string(r) = match r with | Whole i -> string_of_int(i) | Fraction(a, b) -> string_of_int(a) ^ "/" ^ string_of_int(b)This gives us our first version of the Rational structure:
(* initial version of the Rational structure that shows how we can group a type, its constructors, and some functions into a single unit. *) module Rational = struct type rational = Whole of int | Fraction of int * int let add(r1, r2) = match (r1, r2) with | (Whole i, Whole j) -> Whole(i + j) | (Whole i, Fraction(a, b)) -> Fraction(i * b + a, b) | (Fraction(a, b), Whole(i)) -> Fraction(i * b + a, b) | (Fraction(a1, b1), Fraction(a2, b2)) -> Fraction(a1 * b2 + a2 * b1, b1 * b2) let to_string(r) = match r with | Whole i -> string_of_int(i) | Fraction(a, b) -> string_of_int(a) ^ "/" ^ string_of_int(b) endWe were able to test this in the OCaml interpreter:
# let r1 = Rational.Fraction(2, 3);; val r1 : Rational.rational = Rational.Fraction (2, 3) # let r2 = Rational.Fraction(7, 8);; val r2 : Rational.rational = Rational.Fraction (7, 8) # let r3 = Rational.add(r1, r2);; val r3 : Rational.rational = Rational.Fraction (37, 24)In the interpreter, we can give the open command to add all of the defitions from the Rational module to the current environment, which is like giving an import command in Java:
# open Rational;; # let r4 = add(r1, r3);; val r4 : Rational.rational = Fraction (159, 72)Someone pointed out that we should be reducing the result we get when we add two fractional numbers together. For example, if you were to add the rational numbers 1/8 with 1/4, our function will produce the rational number 12/32. Really this should be reduced to its lowest terms of 3/8.
How do we reduce a rational number? We find the greatest common divisor of the numerator and the denominator. We can compute the gcd as follows:
let rec gcd(x, y) = if x < 0 || y < 0 then gcd(abs(x), abs(y)) else if y = 0 then x else gcd(y, x mod y)Using gcd, we were able to write a function to reduce a rational to its simplest form:
let rec reduce_rational(r) = match r with | Whole(i) -> Whole(i) | Fraction(a, b) -> let d = gcd(a, b) in Fraction(a/d, b/d)I said that we would complete this in the next lecture.