Course Hero - We put you ahead of the curve!
You have requested the below document.
- Title: discprog
- Type: Notes
- School: Wheaton IL
- Course: M 243
- Term: Fall
Functionality Discrete Thomas VanDrunen October 6, 2005 Contents 1 Expressions and Types 1.1 Expressions . . . . . . . 1.2 Types as sets . . . . . . 1.3 Type agreement . . . . . 1.4 Tuples . . . . . . . . . . 1.5 Making your own types 1.6 Exercises . . . . . . . . 2 Predicates 2.1 Boolean operators 2.2 Predicates . . . . . 2.3 Conditionals . . . . 2.4 Pattern-matching . 2.5 Exercises . . . . . 3 Lists 3.1 The list type 3.2 Lists as sets . 3.3 Set predicates 3.4 Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 5 7 7 9 11 12 13 13 14 16 17 20 21 22 25 26 29 30 30 32 32 33 34 35 36 4 Algorithms 4.1 Things that are algorithms . . 4.2 Things that compound . . . . . 4.3 Things that repeat . . . . . . . 4.4 Things that change . . . . . . . 4.5 Things that are self-contained . 4.6 Things that are parameterized 4.7 Exercises . . . . . . . . . . . . 5 From Theorems to Algorithms 38 5.1 The Division Algorithm . . . . . . . . . . . . . . . . . . . . . . . 38 5.2 The Euclidean Algorithm . . . . . . . . . . . . . . . . . . . . . . 40 5.3 The Euclidean Algorithm, another way . . . . . . . . . . . . . . . 43 1 CONTENTS 5.4 Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . lists . . . . . . . . . predicates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 44 46 47 48 53 55 56 58 58 61 63 67 6 Relations 6.1 Relations represented by 6.2 Operations on relations 6.3 Relations represented by 6.4 Lists versus predicates . 6.5 Exercises . . . . . . . . 7 Functions 7.1 De nition . 7.2 Functions as 7.3 Scope . . . 7.4 Exercises . . . . . . . . . components . . . . . . . . . . . . . . . . . 8 Set operations 68 8.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68 8.2 Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70 9 First-class Functions 9.1 Problem . . . . . . 9.2 Decomposition . . 9.3 Assembly . . . . . 9.4 Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71 71 74 77 78 Preface Discrete mathematics underlies computer science, and computer science applications readily illustrate concepts from discrete math. If you have interest in computer science, the programming examples and tasks in this course should prove to you the relevance of discrete math. If you are taking this course for its other applications or to prepare for further study in mathematics, the programming component of this course should prove the relevance of computer science. Many people equate computer science with programming, and it may seem unusual to dispel that notion by adding a programming component to a mathematics course. However, this supplement makes no e ort to teach programming in a complete or systematic way, but to introduce computing in parallel with discrete math, and as with any computing, interaction with a computer through a programming language is a necessary component. No prior programming experience is expected in fact, coming without any preconceived ideas of programming conventions may be an advantage. The programming language which we use here, ML (which originally stood for MetaLanguage) is unconventional. Most widely-used programming languages such as C, Java, and VisualBasic are based on the idea of giving a sequence of commands to the computer. ML, on the otherhand, is based on the de nition and application of mathematics functions and that is one reason why it is appropriate for a course like this, in which the study of functions plays a major role. ML s interactive mode Those of you with programming experience are used to writing a program in a text editor, compiling the program, and then running the compiled version. You can do that with ML; however ML also supports an interactive mode which allows you to type expressions and have them be evaluated immediately. We will use the interactive mode for our purposes. You all have accounts on the machines in the computer science lab (Armerding 131); the machines there have two operating systems installed: Windows XP and NetBSD. If you haven t used that lab before and are not used to Unix operating systems (like NetBSD), then you will want to use Windows mode. If 3 CONTENTS 4 the computer you sit down at is in NetBSD mode, just click on the button to reboot in Windows. (If you have your own computer, you can download a copy of ML for free from www.smlnj.org.) To start ML s interactive mode in Windows, click on the icon marked Standard ML of New Jersey. In NetBSD, type sml in the shell. Interaction with ML is done through a prompt. When you see a hyphen on a line, it means ML is waiting for you to tell it something. Then it will give you a response, the result of whatever expression you type. Everything the user should type will appear in typewriter font in this handout, everything ML prints in response will be in slanted typewriter font , and the names of types will be in san serif. Every once in a while, ML will spit out a message like GC #0.0.0.1.9: (0 ms) That s just a report on some background processes. You can disregard it. Resources For help and more information on ML, I recommend two books. The Little MLer by Matthias Felleisen and Daniel Friedman is a fun and quirky introduction to functional programming with ML. I recommend reading chapters 1-7 through the course of the semester. Elements of ML Programming by Je rey Ullman is a serious intrdocution to ML as well as a good reference book. Both are available in the lab. Please keep these copies there or in the Brandt room. You can also nd help online at http://www.dcs.napier.ac.uk/course-notes/sml/manual.html. Instructions for assignments On Unix: Run sml in a typescript; print and turn in the le produced. You may edit the le to remove mistakes and error messages, as long as you demonstrate correct usage. On Windows: Have a Notepad window open while you work. When you are nished with a problem, right-click on the SML window and select Mark from the menu. Select the work you have done on that problem. Then, in Notepad, select Paste from the Edit menu. When you are nished, print and turn in the le from Notepad. Chapter 1 Expressions and Types This chapter introduces you to expressions, the building blocks of programming. values, the results from evaluating expressions. variables, units for storing results/values. types, an application of the set concept, applied to values. datatypes, kinds of types you can de ne yourself. 1.1 Expressions An expression is something that expresses something a mathematical concept, for example. When you are using ML s interactive mode, the prompt (indicated by a hyphen) tells you that ML is waiting for you to enter a expression, which it will consider and evaluate. A value is the result of the evaluation. The way to speak to ML is to give it an expression followed by a semicolon. The semicolon indicates that you are done. 1 <expression> ; The symbol 5 is an example of an expression, since it expresses the numerical concept of the number ve. Try entering that at the prompt. - 5; val it = 5 : 1 Of int course, you also have to press Enter. The reason you also have to use a semicolon (that is, why just pressing Enter is not enough) is that some expressions get pretty long and take more than one line. If you hit Enter without ending with a semicolon, you will get a di erent prompt that looks like =; this means ML assumes you have more to say. 5 CHAPTER 1. EXPRESSIONS AND TYPES Here is what the response means: 6 val is short for value ; ML is saying this is the value it has found for the expression you entered. it is a variable. ML has variables like you are familiar with from mathematics (or other programming languages, if you have programmed before); you also can think of variables as boxes that store values. ML automatically stores the value of the most recently evaluated expression in a variable called it , unless you specify otherwise. 5 , in this case, is the value of the expression (not surprisingly, the value of the expression 5 is 5 ). int is the type of the expression (in this case, short for integer ), about which we will talk more soon. What about that variable, it ? Variables, like numbers, are expressions. Try enterint it into the prompt. - it; val it = 5 : int Again, not very interesting. If you want to specify your own variable in which to store the result, use this form: val <identi er> = <expression>; where identi er means valid variable name. In ML, variable names can be made up of letters, digits, underscores, and apostrophes, but the rst character must be a letter. 2 Give ML something more interesting. - val x = 67 + 4 * 13; val x = 15 : int An asterisk stands for multiplication; a tilde is used for a negative sign (to prevent confusing it with -, which is used for subtraction); this is how to express 67 + 4 -13. Self-exercise. Use ML to calculate the number of seconds in a year. Compute the number of seconds in an hour and the number of hours in a year separately, storing the values in variables. Then use the variables to compute the nal value. 2 Identi ers can begin with an apostrophe, but such identi ers are used only for a special kind of variable. CHAPTER 1. EXPRESSIONS AND TYPES 7 1.2 Types as sets So far, all our expressions have had the type int. A type is a set of values that are related in terms of what operations can be performed on them. This is the rst important bridge between discrete math and programming: the computing concept of type is an application of the mathematical concept of set. We emphasize that it is an application of the set concept, and not an exact t; ML does not provide a way to use the concepts of subsets, unions, intersections, etc. on types. Nevertheless, the type int characterizes the computational values that correspond to the mathematical set Z. ML provides two other basic types of interest to us now: real numbers, with the type real; and boolean values (true or false), with the type bool. - 5.0; val it = 5.0 : real - 5.3 / 3.0; val it = 1.76666666667 : - true; val it = true: bool - 3.4 < 5.7; val it = true : bool real Notice that 5.0 has type real, not type int. This is one place where the set analogy breaks down. int is not a subset of real, and 5 is a completely di erent expression/value from 5.0 . All expressions, values, and variables have types. Notice that each expression above overwrites it, and it may end up with a new type. Self-exercise. Determine the types of 5.3 + 0.3 5.3 - 0.3 5.3 < 0.3 24.0 / 6.0 24.0 * 6.0 24 * 6 1.3 Type agreement A consequence of int and real being unrelated is that you cannot mix them in an arithmetic expressions. That is, +, for example, is de ned for adding two reals and for adding two ints, but not one of each. Attempting to mix them will generate an error. CHAPTER 1. EXPRESSIONS AND TYPES - 7.3 + 5 Error: operator and operand don t agree [literal] . operator domain: real * real operand: real * int in expression : 7.3 + 5 8 This guarantees that the result of arithmetic operations will have the same type as the operands. You may wonder, then, what about dividing two ints? Surely that will not always produce an int. Actually, the / operator is not de ned for ints at all. - 5 / 4 Error: overloaded variable not defined at type symbol: / type: int There is, however, another operator that performs integer division , which computes the integer quotient (that is, ignoring the remainder) resulting from dividing two integers. Such an operation is di erent enough from real number division that it uses a di erent symbol: the word div. The remainder is calculated by the modulus operator, mod. - 43 div val it = - 43 mod val it = 7; 6 : 7; 1 : int int You also may not compare a real with an int; being of di erent types, they are uncomparable3 . It would be like trying to compare an int with a bool. - 5 = true; Error: operator and operand don t agree [literal] operator domain: int * int operand: int * bool in expression: 5 = true - 5 = 5.0; Error: operator and operand don t agree [literal] operator domain: int * int operand: int * real in expression: 5 = 5.0 3 An unusual rule in ML is that reals cannot be tested for equality (or inequality) at all, even against each other. This is because of how machines store real numbers; round-o error makes equality on reals unreliable. CHAPTER 1. EXPRESSIONS AND TYPES 9 But would it not be useful to include both reals and ints in some computations? Yes, but to preserve type purity and reduce the chance of error, ML requires that you convert such values explicitly using one of the converters in the table below4 : Converts real() round() floor() ceil() trunc() from int real real real real to real int int int int by appending a 0 decimal portion conventional rounding rounding down rounding up throwing away the decimal portion For example, - 15.3 / real(6); val it = 2.55 : real - trunc(15.3) div 6; val it = 2 : int Self-exercises. 1. Is ceil(15.2) an expression? What is its value? What is its type? Would ML accept ceil(15)? Why or why not? 2. To do the self-exercise in section 1.1, you probably assumed a non-leap year. Redo that exercise, this time considering an average year, with 365.25 days. 1.4 Tuples From your study of sets, recall the Cartesian product. The most familiar example of Cartesian products is that of points in the xy plane (also called the Cartesian plane), denoted as a pair of real numbers, for example (7.3, 27.85). What if we typed an expression like this into ML? - (7.3, 27.85); val it = (7.3, 27.85) : real * real Note the type that ML infers for this expression, real * real. This corresponds to what we would write as R R in standard mathematical notation. To use a value like this, we can extract the x and y components using the notation #1 and #2, respectively. Suppose we want to shift a point right 5 units and up .3 units. 4 Note the use of parentheses. These converters are functions, as we will see in a later chapter. CHAPTER 1. EXPRESSIONS AND TYPES - val point = (7.3, 27.85); val point = (7.3, 27.85) : real * real - val newx = #1(point) + 5.0; val newx = 12.3 : real - val newy = #2(point) + 0.3; val newy = 27.55 : real - (newx, newy); val it = (12.3, 27.55) : real * real 10 There are several things we should take note of at this point. First, even though the paragraph above talked about shifting the point, we are not actually e ecting a change on the point, but rather creating a new point. Conceptually, a point or location on the graph is, well, a location, and a location is not going to change. In terms of the ML we did, the variable point never changed. If you have programmed before, note well that this is di erent from changing the state of an object or array. Second, the expressions used to de ne variables could be substituted for the variables themselves. We would get the same result if we wrote - (#1(7.3, 27.85) + 5.0, #2(7.3, 27.85) + 0.3); val it = (12.3, 27.55) : real * real . . . but notice that this equivalence depends on point not being reassigned. The use of the variable is a convenience, since we use the value twice (in de ning the new x and in de ning the new y). newx and newy are each used once, so let us use an intermediate version, where the original point is stored in a variable, but the expressions de ning newx and newy are propagated to their uses. - val point = (7.3, 27.85); val point = (7.3, 27.85) : real * real -(#1(point) + 5.0, #2(point) + 0.3); val it = (12.3, 27.55) : real * real We can analyze the types of this expression and its subexpressions: (#1(point) + 5.0, #2(point) + 0.3) real * real real real * real real real * real real real From what we have done so far, we might infer that #n, where n is an int, consumes a pair (type real * real) and produces a real. Likewise, parentheses and a comma consumes two real s and produces a pair of reals (real * real). Notice CHAPTER 1. EXPRESSIONS AND TYPES 11 the di erence between two reals (two values) and a pair of reals (one value). Actually, these operators are more general; they can be used to construct and retrieve from triples, quadruples, or any other sort of tuple, and on types other than real. In fact, the elements of tuples need not all be the same type. - (true, false); val it = (true,false) : bool * bool - (true, 17); val it = (true,17) : bool * int - (21, 8, 4.3); val it = (21,8,4.3) : int * int * real - ( 3.5, false, (13, 5)); val it = ( 3.5,false,(13,5)) : real * bool * (int * int) Self-exercise. Make two points, stored in variables point1 and point2, and calculate the distance between the two points. (Recall that this can be done using the Pythagorean theorem. You can calculate x in ML by Math.sqrt(x), and Math.pow(x, n) computes xy for reals x and y.) 1.5 Making your own types Finally, we will brie y look at how to de ne your own type. A programmerde ned type is called a datatype, and you can think of such a type as a set of arbitrary elements. Suppose we want to model species of felines or trees. - datatype feline = Leopard | Lion | Tiger | Cheetah | Panther; datatype feline = Cheetah | Leopard | Lion | Panther | Tiger - datatype tree = Oak | Maple | Pine | Elm | Spruce; datatype tree = Elm | Maple | Oak | Pine | Spruce When de ning a datatype, separate the elements by vertical lines called pipes. That character is found on the same key as the backslash, and sometimes appears with a break in the middle. The name of the type and the elements must be valid identi ers. As demonstrated here, it is conventional for the names of types to be all lower case, whereas the elements have their rst letters capitalized. Notice that in its response, ML alphabetizes the elements; as with any set, their order does not matter. Until we learn to de ne functions, there is little of interest we can use datatypes for. The equals operator is prede ned. - val cat = Leopard; val cat = Leopard : - cat = Leopard; val it = true : bool - cat = Cheetah; CHAPTER 1. EXPRESSIONS AND TYPES val it = false : bool - cat = Elm; Error: operator and operand don t agree [tycon mismatch] operator domain: feline * feline operand: feline * tree in expression: cat = Elm Self-exercise. Make a data type of colors. 12 1.6 Exercises 1. Store the values 4.5 and 6.7 in variables standing for base and height, respectively, of a rectangle, and use the variables to calculate the area. Then do the same but assuming they are the base and height of a triangle. 2. Store values 9.4 and 13.6 in variables standing for radius and height, respectively, of a cylinder, and use the variables to calculate the volume and surface area. 3. Repeat exercises 1 and 2 but instead store the values in tuples and extract them. 4. Use datatype to de ne a type of interest to you. 5. Mercury orbits the sun in 87.969 days. Calculate how old you will be in 30 Mercury-years. Your answer should depend on your birthday (that is, don t simply add a number of years to your age), but it should be an int. Chapter 2 Predicates This chapter introduces you to boolean operators, elements of the language that produce truth values and compute combinatorial logic. predicates, boolean-valued functions. conditionals, expressions in if-then-else form. matching, an technique for de ning predicates (and other functions). 2.1 Boolean operators - 5.3 / 3.0; val it = 1.76666666667 : - 3.4 < 5.7; val it = true : bool Recall the following two expressions from the previous chapter: real Note that / consumes two reals and produces a another real. On the other hand, < consumes two reals but produces a bool; we call it a boolean operator. Note that this is a simple example of what we call a statement or proposition. It makes an expression whose value is either true or false. Recall that we can combine terms to make more complex statements by using combinational operators, (and ), (or ), and (not ). They take (one or two) truth values and produce another truth value. Such operators exist in ML; they are, respectively, andalso, orelse, and not. - not (3 < 5); val it = false : - not true; bool 13 CHAPTER 2. PREDICATES val it = false : bool - 3 < 5 andalso 4.5 > 2.76; val it = true : bool - 6 = 2 + 4 orelse 3 <= 1; val it = true : bool - not (true orelse false); val it = false : bool - (not true) andalso (not false); val it = false : bool 14 Self-exercises. 1. Test if 637 is divisible by 7 (hint: use the mod operator). 2. Notice that the last two ML expressions in this section verify one of DeMorgan s laws. In a similar way, verify the distributive and associative logic laws. 2.2 Predicates In order for an expression to be a sentence in order for it to be true or false every element must be well de ned. Primarily we are concerned that it does not have any holes in it; in terms of natural language, there must not be any pronoun with a lost antecedent. The sentence He missed the bus this morning cannot be true or false unless it is speci ed to whom he refers. Neither does it make any sense mathematically to assert x < 15.3 unless x s value is known. In such an open sentence, x is called a free variable. We can capture a free variable by naming the open sentence: P (x) = x < 15.3 A predicate with respect to some set D is a sentence that is a statement (that is, it is true or false) when any value of D is substituted for its free variable. For example, letting D = R, P (1.5) = P (15.3) = P (27.4) = 1.5 < 15.3 = true 15.3 < 15.3 = f alse 27.4 < 15.3 = f alse In ML, we can de ne predicates using this form: fun <identi er>(<identi er>) = <expression> The keyword fun is similar to val in that it assigns a meaning to an identi er, namely the rst identi er, which is the name of the predicate. The second identi er, enclosed in parentheses, is the variable, and it should be free in the expression that follows. The expression should have bool as its type. CHAPTER 2. PREDICATES - fun P(x) = x < 15.3; val P = fn : real -> bool - P(1.5); val it = true : bool - P(15.3); val it = false : bool - P(27.4); val it = false : bool 15 Let us compose predicates to test if a real number is within the range [ 3.4, 47.3) and if an integer is divisible by three. - fun Q(x) = x >= 3.4 andalso x < 47.3; val Q = fn : real -> bool - Q( 16.5); val it = false : bool - Q(0.1); val it = true : bool - Q(57.9); val it = false : bool - fun R(n) = n mod 3 = 0; val R = fn : int -> bool - R(2); val it = false : bool - R(4); val it = false : bool - R(6); val it = true : bool We should take note of several things. First, the keyword fun and the use of parentheses in both the de nition and application of the predicate should remind you that a predicate is a function speci cally, a function whose codomain is the set of truth values. Next, note ML s response to the de nition of a predicate, say val Q = fn : real -> bool . This means that the variable Q is assigned to a certain value. The value is not printed, but fn (another keyword based on an abbreviation for function ) stands in for it. The type of the value is real bool, which essentially means a function whose domain is real and whose co-domain is bool. Remember that * accords to in mathematical notation for Cartesian products. In mathematical notation, we would say that a predicate like Q is a function R {true, false}. You should use your knowledge of functions from earlier study in mathematics to understand this for now, but general functions and the concept of function types with be examined more carefully in a later chapter. The function is quite the crucial concept in ML programming. CHAPTER 2. PREDICATES 16 Self-exercise. Write a predicate to determine if a real is an integer (that is, has 0 for its decimal part). 2.3 Conditionals A step function is a function over real numbers whose value is zero up until some number, after which it is one, for example, f (x) = 0 if x < 5 1 otherwise Step functions have applications in electronics and statistics. Notice that computing them requires making a decision, and that decision is based on evaluating a statement: is x less than 5 or not? Suppose we de ne a predicate for this question (be careful that you are testing real numbers). - fun P(x) = x < 5.0; val P = fn : real -> bool Now, how can we make a decision based on this predicate? Logically, we would say, if x is less than 5, then the value is 0, else the value is 1. We can express this in ML using the form if <expression>1 then <expression>2 else <expression>3 The rst expression is called the condition. The second two are called the then-clause and else-clause, respectively. The condition must have type bool. The the other expressions must have the same type, but that type can be anything. If the condition is true, then the value of the entire expression is the value of the then-clause; if it is false, then the entire expression s value is that of the else-clause. Thus the type of the second two expressions is also the type of the entire expression. - if P( 3.47) then 0.0 else 1.0; val it = 0.0 : real - if P(82.3) then 0.0 else 1.0; val it = 1.0 : real - if P(82.3) then 0 else 1; val it = 1 : int - if P(82.3) then 0 else 1.0; stdIn:31.1-31.27 Error: types of rules don t agree [literal] earlier rule(s): bool -> int this rule: bool -> real in rule: false => 1.0 CHAPTER 2. PREDICATES 17 Notice that for any predicates P (x) and Q(x), P(x) is equivalent to if P(x) then true else false and P(x) andalso Q(x) is equivalent to if P(x) then Q(x) else false. - fun P(x) = x > 3.5; val P = fn : real -> bool - fun Q(x) = x < 6.4; val Q = fn : real -> bool - fun P2(x) = if P(x) then true else false; val P2 = fn : real -> bool - P(1.0); val it = false : bool - P(10.0); val it = true : bool - P2(1.0); val it = false : bool - P2(10.0); val it = true : bool - fun PandQ(x) = P(x) andalso Q(x); val PandQ = fn : real -> bool - fun PandQ2(x) = if P(x) then Q(x) else false; val PandQ2 = fn : real -> bool - PandQ(1.0); val it = false : bool - PandQ2(1.0); val it = false : bool - PandQ(4.0); val it = true : bool - PandQ2(4.0); val it = true : bool - PandQ(10.0); val it = false : bool - PandQ2(10.0); val it = false : bool Self-exercise. What if-then-else statements are equivalent to not P(x) and P(x) orelse Q(x)? 2.4 Pattern-matching As a nale for this chapter, we consider writing predicates for non-numerical data. In the previous chapter, we learned how to create a new datatype. For example, we considered the set of trees (here expanded): - datatype tree = Oak | Maple | Pine | Elm | Spruce | Fir | Willow; CHAPTER 2. PREDICATES 18 datatype tree = Elm | Fir | Maple | Oak | Pine | Spruce | Willow We know that equality is de ned automatically. By chaining comparisons using orelse, we can create more complicated statements and encapsulate them in predicates. - fun isConiferous(tr) = = tr = Pine orelse tr = Spruce orelse tr = Fir; val isConiferous = fn : tree -> bool - isConiferous(Willow); val it = false : bool - isConiferous(Pine); val it = true : bool Notice that we never explicitly consider the cases for non-coniferous trees. An equivalent way of writing this is - fun isConiferous2(tr) = = if (tr = Pine) = then true = else if (tr = Spruce) = then true = else if (tr = Fir) = then true = else false; val isConiferous2 = fn : tree -> bool - isConiferous2(Oak); val it = false : bool - isConiferous2(Spruce); val it = true : bool Although this is much more verbose, a pattern like this would be necessary if such a predicate (or, more generally, function) needed to perform more computation to determine its result (as opposed to merely returning literals true and false). ML does, however, provide a cleaner way of using a pattern conceptually the same as that used above. Instead of naming a variable for the predicate to receive, we write a series of expressions for explicit input values: - fun isConiferous3(Pine) = true = | isConiferous3(Spruce) = true = | isConiferous3(Fir) = true = | isConiferous3(x) = false; val isConiferous3 = fn : tree -> bool - isConiferous3(Maple); val it = false : bool - isConiferous3(Fir); val it = true : bool CHAPTER 2. PREDICATES 19 This is referred to as pattern matching because the predicate is evaluated by nding the de nition that matches the input. Patterns can become more complicated than we have seen here. Notice also that we still use a variable in the last line of the de nition of isConiferous3 as a default case. It is legal to de ne a predicate so that it leaves some cases unde ned. However, that will generate a warning when you de ne it and an error message if you try to use it on a value for which it is not de ned. - fun isConiferous4(Pine) = true = | isConiferous4(Spruce) = true = | isConiferous4(Fir) = true; stdIn:52.1-54.30 Warning: match nonexhaustive Pine => ... Spruce => ... Fir => ... val isConiferous4 = fn : tree -> bool - isConiferous4(Spruce); val it = true : bool - isConiferous4(Pine); val it = true : bool - isConiferous4(Elm); uncaught exception nonexhaustive match failure As an example of a more complicated pattern, consider a 2-place predicate. Suppose you are having guests over and want to serve food that will not violate any of your guests dietary restrictions. Arwen tells you that she is a vegetarian, Luca says he is on a low-sodium diet, and Jael tells you that she eats kosher. Estella says she will eat anything, and Bogdan claims he eats nothing, but you gure that everyone will eat ice cream. To test what combination of foods would be palatable, you use the following ML system: - datatype guests = Arwen | Luca | Jael | Estella | Bogdan; datatype guests = Arwen | Bogdan | Estella | Jael | Luca - datatype foods = FrenchFries | Chicken | Bacon | Spinach | IceCream; datatype foods = Bacon | Chicken | FrenchFries | IceCream | Spinach - fun isKosher(Bacon) = false = | isKosher(x) = true; val isKosher = fn : foods -> bool - fun isMeat(Chicken) = true = | isMeat(Bacon) = true = | isMeat(x) = false; val isMeat = fn : foods -> bool - fun isSalty(Bacon) = true CHAPTER 2. PREDICATES = | isSalty(FrenchFries) = true = | isSalty(x) = false; val isSalty = fn : foods -> bool - fun eats(x, IceCream) = true = | eats(Estella, y) = true = | eats(Arwen, x) = not (isMeat(x)) = | eats(Luca, x) = not (isSalty(x)) = | eats(Jael, x) = isKosher(x) = | eats(Bogdan, x) = false; val eats = fn : guests * foods -> bool 20 Self-exercise. Using your color type from the previous chapter, write a predicate that uses pattern-matching to determine if a color is primary. 2.5 Exercises After your dinner party you and your guests (from the earlier example) will be watching a movie. Nobody wants to watch Titanic. Luca, a Californian, wants to see the Governator in Terminator. Jael is up for anything but a drama. Estella is in the mood for a good comedy. Bogdan has a crush on Estella and wants to watch whatever she wants. 1. Create a movie genre datatype with elements Drama, Comedy, and Action. Create a movie datatype with elements Terminator, Shrek, Titanic, Alien, GoneWithTheWind, and Airplane. Copy the guest datatype from the example. 2. Write three predicates using pattern-matching to determine the genre of a movie (i.e. isDrama, isComedy, isAction). 3. Write a predicate wantsToWatch(guest, movie) that determines if a guest will watch a particular movie. 4. Will Bogdan watch Shrek ? Will Jael watch Gone With The Wind ? Exercises by Joey Hurst. Chapter 3 Lists This chapter introduces you to Lists, a compound data type of inde nite length. Sets, as an application of lists. Set predicates, illustrating the manipulation of lists. Types were described in Chapter 1 as sets of values. However, we emphasized that types do not capture the full range of the mathematical concept of sets. For example, take the type - datatype Bird = Sparrow | Finch | Robin | Owl | Gull | Dove = | Eagle | Vulture | Hawk | Falcon | Penguin | Duck | Loon = | Goose | Chicken | Turkey; datatype Bird = Chicken | Dove | Duck | Eagle | Falcon | Finch | Goose| Gull | Hawk | Loon | Owl | Penguin | Robin | Sparrow | Turkey | Vulture This set of birds (values) makes up a type, because we have declared it to be so. However, that is not to say that an arbitrary set of values like { Finch, 5, Sparrow, 18.3, false } can be a type. ML has no way to expand a type, like adding Parrot to Bird. There also is no way to represent a subset as a type, nor an elegant way to construct a union of sets represented as types or to construct a superset 1 . We brie y looked at tuples as types based on the Cartesian product or other types. In one sense, these could be used to represent sets, since - (Robin, Duck, Chicken); val it = (Robin,Duck,Chicken) : 1 You Bird * Bird * Bird can achieve something like union or superset of datatypes using a constructor expression. See Ullman, pg 199 or Felleisen and Friedman, pg 4. This is not an elegant solution because members of the subset are not truly members of the superset. 21 CHAPTER 3. LISTS 22 is indeed a set of birds. This is a poor solution because even though a tuple s length is arbitrary, it is still xed. The type Bird * Bird * Bird is much more restricted than set of Birds. An attempted de nition of a set-inclusion predicate like - fun elementOf(b, set) = = b = #1(set) orelse b = #2(set) orelse b = #3(set); would generate errors because ML would be unable to infer the type of set (how many elements are there?). It is possible to name the type explicitly. fun elementOf(b, set:(Bird * Bird * Bird)) = = b = #1(set) orelse b = #2(set) orelse b = #3(set); Yet this would work only on sets of cardinality three, not arbitrary sets. 3.1 3.1.1 The list type De nition Here we introduce a new type (or family of types) called a list which will enable us better to represent the mathematical concepts of sets. Cosmetically, the di erence is to use square brackets instead of parentheses: - [Finch, Robin, Owl]; val it = [Finch,Robin,Owl] : Bird list - [Vulture, Sparrow]; val it = [Vulture,Sparrow] : Bird list - [Eagle]; val it = [Eagle] : Bird list - [14, 18, 22, 23, 4, 57, 86]; val it = [14,18,22,23,4,57,86] : int list - []; val it = [] : a list - [] : Bird list; val it = [] : Bird list - [5, true, Sparrow]; stdIn:163.1-163.19 Error: operator and operand don t agree [tycon mismatch] operator domain: bool * bool list operand: bool * Bird list in expression: true :: Sparrow :: nil Note that every list made up of birds has the same type, Bird list, regardless of how many birds there are. An empty list can even be a list of Birds, if we say it is. Likewise we can have a list of ints or a list of unknown base type. We CHAPTER 3. LISTS 23 cannot have a list of more than one type. The appostrophe in the front of a indicates that it is a type variable, that is, a variable that holds the place of a type. Thus a stands for an unknown type, and a list means list of an unknown type. Pay careful attention to the di erence between types and lists. An n-tuple has n components, and n is a fundamental aspect of the type. Any list (with base type a) has precisely two components: the rst element (called the head, of type a) and the rest of the list (called the tail, of type a list). Defying the grammar school notion that one cannot de ne something in terms of itself, an a list is an empty list, or an a followed by a a list. 3.1.2 Accessors Since the number of base elements is unknown, we cannot use the tuple method of access on them, that is #1, #2, etc. What we can use for access are functions for head and tail, hd and tl, respectively. - val sundryBirds = [Robin, Penguin, Gull, Loon]; val sundryBirds = [Robin,Penguin,Gull,Loon] : Bird list - hd(sundryBirds); val it = Robin : Bird - tl(sundryBirds); val it = [Penguin,Gull,Loon] : Bird list - tl(it); val it = [Gull,Loon] : Bird list - tl(it); val it = [Loon] : Bird list - tl(it); val it = [] : Bird list - tl(it); uncaught exception Empty Notice how hd and tl can be used to slice up a list, one element at a time. Further, it is an error to try extracting the tail (or head, for that matter) of an empty list. Self-exercise. Extract Penguin from [Gull, Eagle, Hawk, Penguin, Turkey]. 3.1.3 Constructors There are other ways to build lists than with square brackets. There are two additional operators that express lists: The operator @ takes two lists and concatenates them, that is, chains them together to form a new list. The operator CHAPTER 3. LISTS 24 :: takes an element and a list (assuming the types agree) and produces a new list with the element as the head and the old list as the tail. Observe some examples, both correct and incorrect. - [Owl, Finch] @ [Eagle, Vulture]; val it = [Owl,Finch,Eagle,Vulture] : Bird list - Robin::it; val it = [Robin,Owl,Finch,Eagle,Vulture] : Bird list - it::Hawk; stdIn:178.1-178.9 Error: operator and operand don t agree [tycon mismatch] operator domain: Bird list * Bird list list operand: Bird list * Bird in expression: it :: Hawk - Sparrow::Robin::Turkey; stdIn:1.1-164.2 Error: operator and operand don t agree [tycon mismatch] operator domain: Bird * Bird list operand: Bird * Bird in expression: Robin :: Turkey - Sparrow::Robin::[Turkey]; val it = [Sparrow,Robin,Turkey] : Bird list - Sparrow::Robin::Turkey::[]; val it = [Sparrow,Robin,Turkey] : Bird list - it::[[Loon]]; val it = [[Sparrow,Robin,Turkey],[Loon]] : Bird list list - it@[[Duck]]; val it = [[Sparrow,Robin,Turkey],[Loon],[Duck]] : Bird list list Note that a list of lists is perfectly legal. The hd operation of a list of lists will produce a list, for example - [[Loon],[Robin, Duck]]; val it = [[Loon],[Robin,Duck]] : - hd(it); val it = [Loon] : Bird list - hd(it); val it = Loon : Bird Bird list list Self-exercise. Use :: and @ in various combinations to build [Duck, Gull, Vulture, Owl] and [[Falcon, Dove], [Finch], [Chicken, Loon]]. Extract Loon from [[Sparrow,Robin,Turkey],[Loon],[Duck]]. CHAPTER 3. LISTS 25 3.1.4 Lists vs. tuples By using a list instead of a tuple, we are surrendering the ability to extract an element from an arbitrary position in one step (called random access). We are constrained instead to sequential access, requiring iteration over the list. This is the price of using lists we sacri ce random access for inde nite length. Much of the work of programming is weighing the trade-o s among options; in this case, we are choosing the most appropriate data structure, each of which has its advantages and liabilities. The following table summarizes the di erences between tuples and lists2 . Tuples Lists Access random sequential Length xed inde nite Element types unrelated uniform 3.2 Lists as sets Ultimately, our choice to use lists comes because the concept list of X is so similar to set of X. - val waterFowl = [Gull, Duck, Loon, Goose]; val waterFowl = [Gull,Duck,Loon,Goose] : Bird list - val smallBirds = [Sparrow, Finch, Robin, Dove]; val smallBirds = [Sparrow,Finch,Robin,Dove] : Bird list - val birdsOfPrey = [Eagle, Vulture, Hawk, Falcon, Owl]; val birdsOfPrey = [Eagle,Vulture,Hawk,Falcon,Owl] : Bird list - val isFarmed = [Chicken, Turkey]; val isFarmed = [Chicken,Turkey] : Bird list We can emulate the union operation using @. - val waterOrFarm = waterFowl @ isFarmed; val waterOrFarm = [Gull,Duck,Loon,Goose,Chicken,Turkey] : Bird list Nevertheless, lists do not t the set concept perfectly. A list may contain multiple copies of the same element. In particular, @ will keep both copies if an element belongs in both subsets that are being unioned. Worse, there is no operation that corresponds to intersection or set subtraction. tl helps us only if we are lucky enough to have the undesired item rst. - val isEaten = tl(waterOrFarm); val isEaten = [Duck,Loon,Goose,Chicken,Turkey] : Bird list 2 Most programming languages also have array types. Arrays are like tuples in that they support random access, but are like lists in that all the elements must be of the same type. From a type perspective, they are like lists in that their length is inde nite, but they behave more like tuples in that you cannot build new arrays from smaller ones. CHAPTER 3. LISTS 26 To use lists as sets, we must write our own such operations. We will program operations for union and intersection later in this course, after we have developed more ML concepts. Self-exercise. Write sets for birds that swim, birds that do not y (or at least do not y very well), and birds that have bald heads. 3.3 Set predicates We do not yet have the tools for writing union or intersect operations, but predicates, particularly set membership, subset, and set equality, are within our reach. If set membership were all we wanted, we could have de ned that with predicates like the ones we saw at the end of the previous chapter. This would have been all we need for representing sets. For the examples in this section, we will enrich our universal set of birds by adding Ostriches. Then we de ne various subsets using predicates - datatype Bird = Sparrow | Finch | Robin | Owl | Gull = | Eagle | Vulture | Hawk | Falcon | Penguin | Duck | = | Goose | Chicken | Turkey | Ostrich; datatype Bird = Chicken | Dove | Duck | Eagle | Falcon | Goose | Gull | Hawk | Loon | Ostrich | Owl | Penguin | Sparrow | Turkey | Vulture - fun doesNotFly(Penguin) = true = | doesNotFly(Chicken) = true = | doesNotFly(Turkey) = true = | doesNotFly(Ostrich) = true = | doesNotFly(b) = false; val doesNotFly = fn : Bird -> bool - fun swims(Duck) = true = | swims(Loon) = true = | swims(Goose) = true = | swims(Penguin) = true = | swims(b) = false; val swims = fn : Bird -> bool | Dove Loon | Finch | Robin The predicates doesNotFly and swims, for example, are a representations of the set of birds that do not y or birds that do swim (say, DNF and S), respectively). Thus we can compute expressions like Hawk DNF : - doesNotFly(Hawk); val it = false : bool - doesNotFly(Penguin); val it = true : bool - swims(Penguin); val it = true : bool CHAPTER 3. LISTS - swims(Falcon); val it = false : bool 27 Many set operations are now easily performed by logical connectives. A bird neither swims nor ies (NSNF ) if it does not swim and it does not y. Set theoretically, NSNF = DNF S. Logically, b NSNF b DNF b S / b DNF (b S). In ML, - fun neitherSwimsNorFlies(b) = = doesNotFly(b) andalso not (swims(b)); val neitherSwimsNorFlies = fn : Bird -> bool - neitherSwimsNorFlies(Penguin); val it = false : bool - neitherSwimsNorFlies(Robin); val it = false : bool - neitherSwimsNorFlies(Ostrich); val it = true : bool From our present perspective, the biggest disadvantage of this approach is that it requires an undue amount of typing; a list de nition is much more succinct. What we would like is a way to compute set inclusion based on a list implementation of a set, that is, a predicate elementOf(x, s) which tests whether item x is an element in the list (a representation of a set) s. To compute this, recall that a list is an element (the head) and the rest of the list (the tail). Hence, an item is in a list if it is the head item, or it is an item in the tail. Like the de nition of a list, this de nition is self-referential, and strict though ML may be, it is more permissive than grammar school teachers on this point. We can write the above idea in ML thus: - fun elementOf(x,s) = x = hd(s) orelse elementOf(x, tl(s)); Warning: calling polyEqual val elementOf = fn : a * a list -> bool The warning is because we are using equality on an unknown type (x = hd(s), as ML says, calling polyEqual ). This assumes that equals is de ned for whatever type we are using. While this means we could not use this predicate on a list of reals (why not?), we are safe for our purposes because equals is de ned for all datatypes. Trying out our new predicate: - val doesNotFly = [Penguin, Chicken, Turkey, Ostrich]; val doesNotFly = [Penguin,Chicken,Turkey,Ostrich] : Bird list - val swims = [Duck, Loon, Goose, Penguin]; CHAPTER 3. LISTS val swims = [Duck,Loon,Goose,Penguin] : - elementOf(Penguin, doesNotFly); val it = true : bool - elementOf(Penguin, swims); val it = true : bool - elementOf(Falcon, swims); uncaught exception Empty Bird list 28 Why did the last query fail? Note that this is the same error we received when we attempted to extract the tail from an empty list. Consider what happened: 1. elementOf(Falcon, [Duck, Loon, Goose, Penguin]): we found hd([Duck, Loon, Goose, Penguin]) = Duck, which does not equal Falcon, so we tested Falcon for inclusion in tl([Duck, Loon, Goose, Penguin]) = [Loon, Goose, Penguin]. 2. elementOf(Falcon, [Loon, Goose, Penguin]): we found hd([Loon, Goose, Penguin]) = Loon, which does not equal Falcon, so we tested Falcon for inclusion in tl([Loon, Goose, Penguin]) = [Goose, Penguin]. 3. elementOf(Falcon, [Goose, Penguin]): we found hd([Goose, Penguin]) = Goose, which does not equal Falcon, so we tested Falcon for inclusion in tl([Goose, Penguin]) = [Penguin]. 4. elementOf(Falcon, [Penguin]): we found hd([Penguin]) = Penguin, which does not equal Falcon, so we tested Falcon for inclusion in tl([Penguin]) = []. 5. elementOf(Falcon, []): we attempted to nd hd([]), which is an error, since an empty list has no head. To avoid this error, we need to re ne our de nition to account for the case of the empty list or set. Since nothing is an element of an empty list, an item is in a list if the list is not empty, and either it is the head item, or it is an item in the tail. That is, - fun elementOf(x, s) = s <> [] andalso (x = hd(s) orelse elementOf(x, tl(s))); val elementOf = fn : a * a list -> bool - elementOf(Penguin, doesNotFly); CHAPTER 3. LISTS val it = true : bool - elementOf(Penguin, swims); val it = true : bool - elementOf(Falcon, swims); val it = false : bool 29 For a nal (cosmetic) improvement, we can use pattern matching. A list is either empty or it has a head and a tail; therefore, a list can have the patterns [] or first::rest. Hence we can write - fun elementOf(x, []) = false = | elementOf(x, first::rest) = x = first orelse elementOf(x, rest); val elementOf = fn : a * a list -> bool Self-exercise. Write a function elementOfIntersection(x, s1, s2) which tests if x is in the intersection of s1 and s2. It may call elementOf. 3.4 Exercises 1. Write a predicate for set inclusion (subsetOf); demonstrate that it works using a datatype of your choosing. 2. Write a predicate for set equality (setEquals); demonstrate that it works. 3. Write a predicate that tests if two sets are disjoint (setsDisjoint); demonstrate that it works. Chapter 4 Algorithms This chapter introduces you to algorithms, step-wise problem-solving techniques. statements, expressions whose values are ignored. while loops, statements whose execution is repeated until a condition fails. reference variables, variables whose value can be reset. local variables, variables that exist only for a speci c expression. functions, as ways to parameterize an algorithm. 4.1 Things that are algorithms An algorithm is a series of steps to solving a problem or discovering a result. Almost all non-trivial algorithms require the repetition of some of the steps. Much of the mathematics you learned in the early grades involved mastering algorithms. For example, when you want to add two multi-digit integers, you intuitively know to 1. Start with the one s column. 2. While the current column is a valid column, repeatedly (a) Add the two numbers in the current column plus any carry from last time. (b) Write the one s column of the sum in the current column of the answer. (c) Write the ten s column of the sum as the carry (for next time). (d) Move one column to the left. 30 CHAPTER 4. ALGORITHMS 31 3. If the last round of the repetition had a carry, write that in a new column for the answer. Notice All the sentences are in the imperative mood1 . This is what is meant when we say an algorithm is a series of steps; when composing an algorithm you can think of these steps as commands. The sentence of step 2 is actually a yoking of four sentences together. We are straining English when we do this (we could have made them independent clauses connected by commas and and ), but the concept is that we can take several commands and compound them into a single e ective command. This is not unlike how we can use simple expressions to build more complex expressions, except that expressions need to be knit together with operators and such; the commands can simply be chained together. Steps 2 and 3 are guarded by constructs that make decisions (how many times to perform a command and whether to perform a command, respectively). Those decisions are based on the truth of the independent clauses governed by the dependent markers while and if. Such clauses (in the indicative mood) are sentences in the logical sense, and so they correspond to boolean expressions in a language like ML. Does that seem like a lot of grammar for a math course? Understanding everyday language is vital for understanding the languages of mathematics and programming. If it has been too long since you have thought about clauses or moods, consult a grammar reference and start this chapter again2 . We already know how to do certain pieces of this in ML: Logical sentences, as already said, are boolean expressions, and if expressions make decisions. What we have not yet seen is how to repeat, how to compound, or how to do anything that is e ectively imperative. These are the tools we need in order to write algorithms in ML3 . The pieces of ML code we will see in this chapter will appear rather clunky, and getting it right will require extra attention to detail. That is because it goes against the grain of the kind of programming ML was designed for. It is still worth a little of our time to learn imperative algorithms, however. Bear with the unusual syntax for this and the next chapter; after that ML will appear elegant again. Self-exercise. A geometric series is one in the form n ari , for example i=0 3 1 1 i 1 1 1 (taking a = 1, r = 2 , and n = 3), i=0 ( 2 ) = 1 + 2 + 4 + 8 . Informally compose a series of steps to calculate such a series. 1 It is a convenient aspect of English, however, that the to at the end of the previous paragraph makes them in nitives. 2 Hey, that was an algorithm. 3 At least that is what we need for writing conventional algorithms in the imperative style. The applicative style we will use from Chapter 6 on produces perfectly good algorithms without while statements or anything imperative. CHAPTER 4. ALGORITHMS 32 4.2 Things that compound A statement is an expression whose value is thrown away. If you evaluate a statement in ML, it will respond with val it = () : unit . . . which basically means nothing. The type unit stands in as the result type of any statement. () stands for no result and is the only value of the type unit. A simple way to write statements is to use them in a statement list. A statement list is an expression (not a statement) make up of a list of several expressions separated by semicolons and enclosed in parentheses. It is evaluated by evaluating each expression in order and throwing away the values (thus making them statements) except for the last, which it evaluates and returns as the value of the entire statement list. For example, - (7 + 3; 15.6 / 34.7; 4 < 17; Tiger); val it = Tiger : feline Statements are primarily used for their side e ects. Since we do not yet know anything that has a side e ect, let us move on. Self-exercise. Write a statement list whose last expression calculates 3 ( 1 )i . i=0 2 Be careful about types. Fill the other expressions with whatever you wish. 4.3 Things that repeat while <expression> do <expression> The form to evaluate an expression repeatedly, as long as a condition is true, is It is a statement. It will always result in (). Suppose we have a variable x set to 4, and as long as it is less than 5, we want to subtract 3 from it. - val x = 4; val x = 4 : int - while x < 5 do x - 3; Notice there is no response from ML. That is because we told it to keep subtracting 3 from x as long as x is less than 5, and since x is 4, it is less than 5 and always will be. In other words, the execution of this statement will go on forever4. Press control-C to make it stop. 4 Technically, we should have said that a while statement will always result in () if it nishes. CHAPTER 4. ALGORITHMS 33 The algorithm for adding two multi-digit integers had a concrete stopping point: when you run out of columns in the addends. The problem here is that since the variables do not change during the evaluation of an expression or statement, there is no way a boolean expression will change its value either, and so we will never hit a stopping point. Thus things that repeat will be of no use until we have. . . 4.4 Things that change In ML, variables are like the laws of the Medes and the Persians, which, once instated, cannot be repealed (Daniel 6:12, Esther 1:19). This seems to contradict your experience with ML, since you probably have reused variables in the same session. King Xerxes realized that even though he could not revoke an old decree, he could issue a new one to counteract the old (Esther 8:8). Technically, when your reuse an identi er in a variable declaration, you are not changing the value of the old variable, but making a new variable of the same name that supersedes the old one. The above detail is transparent in most of ML programming, and you can get by without understanding it. The only di erence it makes is that it constrains writing imperative algorithms, because there is no way to redeclare a variable in the middle of an expression or statement and make the rest of the evaluation use the new variable. To deal with this, we introduce a new type of variable, called a reference variable, which can change value. There are three rules that distinguish reference variables from ordinary variables: To declare a reference variable, precede the expression with the keyword ref. To use a reference variable, precede it with !. To set a reference variable, use an assignment statement in the form <identi er> := <expression> We will use calculating a factorial as a running example. Using product notation, we de ne n! as n i i=1 Speci cally, to nd 5!, 5 i=1 i=1 2 3 4 5 Notice that this de nition (or even just the product notation) is algorithmic. It states to keep a running product while repeatedly multiplying by a multiplier, CHAPTER 4. ALGORITHMS 34 incrementing the multiplier by one at each step, starting at one, until a limit for the multiplier is reached. Using our newly discovered reference variables, we can express this in ML: - val i = ref 0; val i = ref 0 : int ref - val fact = ref 1; val fact = ref 1 : int ref - while !i < 5 do = (i := !i + 1; = fact := !fact * !i); val it = () : unit - !fact; val it = 120 : int Notice that the type of i is int ref. A use of a reference variable is an expression; the operator ! take an int ref and produces an int. (! also works similarly for other reference types, such as real ref.) This example also is our rst good illustration of a statement. Note that the while statement produces (); even though it computes 5!, it does not report the answer. Instead, it has a side e ect, that is, it does something other than (or, in addition to) returning a value. In this case, its side e ect is to set the variable fact to contain the result of the computation, 5!. (Changing i is another side e ect.) For this reason, after the while statement we inspect the contents of fact to discover our answer. To re ne this a bit, recall the statement list construct. It compounds a series of expressions or statements, executing them all and returning the value of the last expression. We can make this into an expression that evaluates to 5! by binding the while and the use together. - val i = ref 0; val i = ref 0 : int ref - val fact = ref 1; val fact = ref 1 : int ref - (while !i < 5 do = (i := !i + 1; = fact := !fact * !i); = !fact); val it = 120 : int Self-exercise. Using the pattern from this section, write an algorithm in ML 3 1 to compute i=0 ( 2 )i . 4.5 Things that are self-contained The modi cation using a statement list is slight, but what it gives us is a better sense of packaging, in that the factorial computation is a unit. However, the CHAPTER 4. ALGORITHMS 35 variables i and fact still exist after the expression that computes factorial, even though they no longer have a purpose. The duration of a variable s validity is called its scope. A variable declared directly in the prompt has scope starting then and continuing until you exit the ML interactive mode. What we would like is to have variables that are local to the expression variables that the expression alone can use and that disappear when the expression nishes executing. We can make local variables by using a let expression, which has the following form. let <var declaration>1; <var declaration>2; . . . <var declaration>n in <expression> end Rewriting our factorial, now as a single, self-contained expression: - let = val i = ref 0; = val fact = ref 1; = in = (while !i < 5 do = (i := !i + 1; = fact := !fact * !i); = !fact) = end; val it = 120 : int Self-exercise. Make your algorithm from the previous self-exercise self-contained using a let expression. 4.6 Things that are parameterized The only thing de cient in our program is that it is not reusable. Why would we bother, after all, with a 9-line algorithm for computing 5! when the one line 1 * 2 * 3 * 4 * 5 would have produced the same result? The value of an algorithm is its generality. This is the same algorithm we would use to compute any other factorial n!, only that we would replace 5 in the fth line with n. In other words, we would like to parameterize the expression, or wrap it in a package that accepts n as input and produces the factorial for any n. Just as we parameterized statements with fun to make predicates, so we can here. - fun factorial(n) = = let val i = ref 0; = val fact = ref 1; = in = (while !i < n do = (i := !i + 1; = fact := !fact * !i); = !fact) CHAPTER 4. ALGORITHMS = end; val factorial = fn : int -> int - factorial(1); val it = 1 : int - factorial(5); val it = 120 : int - factorial(8); val it = 40320 : int - factorial(10); val it = 3628800 : int 36 For a second time, you are informally introduced to the notion of a function. We see it here as a way to package an algorithm so that it can be used as an expression when given an input value. Later we will see how to knit functions together to eliminate the need for while loops and reference variables in almost all cases. Self-exercise. Package your expression from the previous self-exercise into a n 1 function, parameterizing the limit (i.e., i=0 ( 2 )i ). 4.7 Exercises n 1. Recall that the general form of the geometric series is i=0 ari . Write a function in ML, based on your solution to the self-exercises, which has n, a, and r as parameters. 2. Suppose ML did not have a multiplication operator de ned for integers. Write a function multiply which takes two integers and produces their product, without using direct multiplication (that is, using repeated addition). 3. The Fibonacci sequence is de ned by repeatedly adding the two previous numbers in the sequence (starting with 0 and 1) to obtain the next number, i.e. 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, . . . Write a function to compute the nth Fibonacci number, given n. (Hint: You will need four reference variables local to the function: one to serve as a counter, one to hold the current value, one to hold the previous value, and one to hold a temporary value as you shu e the current value to the previous: temp := ... previous := !current current := !temp CHAPTER 4. ALGORITHMS 37 Then think about how you can rewrite this so that the temporary value can be local to the while statement, and a regular variable instead of a reference variable.) Chapter 5 From Theorems to Algorithms This chapter continues the previous chapter, using an extended example to illustrate the use of imperative programming for implementing algorithms. In addition, this chapter introduces you to theorems, as facts that lay the foundations for algorithms. applicative style, an alternative to imperative style. recursion, functional self-reference. Applicative style and recursion are merely previewed here; they will be treated thoroughly later. 5.1 The Division Algorithm You have already seen in class how we can use techniques like induction to prove the correctness of an algorithm. We will now see how a result can help us construct an algorithm. To begin, consider the following result, sometimes called the Quotient-Remainder Theorem (QRT): Lemma 1 If n Z and d Z+ , then there exist unique integers q and r, such that n = d q + r and 0 r < d. Consult your discrete math text for a proof. This provides the foundation for integer division and modulo, which we have seen previously. That is, div and mod are de ned so that in the above lemma, q = n div d (the quotient) and r = n mod d (the remainder). This result guarantees that div and mod are well-de ned, that a quotient and remainder exist for any two integers. Why did we mark this as a lemma, while at the same time naming it the Quotient-Remainder Theorem? Theorems are important results. We usually 38 CHAPTER 5. FROM THEOREMS TO ALGORITHMS 39 think of lemmas as auxiliary results whose main purpose is to be used in the proof of a more important theorem. However, the QRT is a lemma for our purposes because we are using it to support something else in this case, an algorithm. The QRT not only guarantees that a quotient and a remainder exist; it also tells us how to compute them. In other words, it implies an algorithm To see how we can derive an algorithm from the QRT, consider what it tells about q and r. The two assertions about q and r can be thought of as restrictions that need to be met as we try to nd suitable values. The restriction n = d q +r is not di cult to satisfy simply take q = 0 and r = n. That, however, might con ict with the restriction that r < d. Nevertheless, we can then take the strategy of altering our initial guess, so long as we do not violate the earlier restriction, and repeat this until we satisfy the other restriction, too. Notice the elements of our algorithm, which you should recognize from studying correctness proofs of algorithms in class. We have: Initial conditions: q = 0 and r = n. A loop invariant: n = d q + r. A termination condition: r < d The only thing that is missing is a way to mutate q and r so as to preserve the invariant and make progress towards the termination condition. Progress is marked by making r smaller. It might seem backwards to determine the husk of a loop before its body or to set up a proof that what we are doing is correct before we even have gured out what we are doing. However, this type of thinking encourages programming that is amenable to correctness proof and enforces a discipline of programming and proving in parallel. If the termination condition does not hold, then r d. Hence there is a nonnegative integer, say y, such that r = d + y. Note that n = d q + r = d q + d + y = d (q + 1) + y and so, if r and q are the versions of r and q updated by the body of the loop, r q = = y q+1 = r d In other words, we want to decrease r by d and increase q by one. This preserves the loop invariant and reduces r. We terminate when r is less than d. Now we put this algorithm into the ML format we saw in the previous chapter. - fun divisionAlg(n, d) = = let val r = ref n; = val q = ref 0 = in = (while !r >= d do CHAPTER 5. FROM THEOREMS TO ALGORITHMS = (r := !r - d; = q := !q + 1); = (!q, !r)) = end; val divisionAlg = fn : - divisionAlg(16, 3); val it = (5,1) : int * - divisionAlg(264, 18); val it = (14,12) : int - divisionAlg(49, 7); val it = (7,0) : int * 40 int * int -> int * int int * int int Self-exercise. Note that the function divisionAlg returns a tuple standing for the quotient and the remainder. Write functions quot and remain that return only the quotient and remainder, respectively (that is, they should be equivalent to ML s div and mod). Do not re-type the algorithm for each one; instead, enter it once as we did here and have remain and quot call divisionAlg. You may need to refer to Section 1.4 to review how to use tuples. 5.2 The Euclidean Algorithm The greatest common divisor (GCD) of two integers a and b is a positive integer d such that d | a and d | b (that is, there exist integers p and q such that a = p d and b = q d). for all c such that c | a and c | b, c | d. The GCD of a and b is written gcd(a, b), and it is most commonly recognized in its use for simplifying fractions. Clearly, a simple and e cient way to compute the GCD would be useful for any application involving integers or rational numbers. As before, we have a result that implies an algorithm, which we shall derive. The result comes this time as two lemmas. Lemma 2 If a Z, then gcd(a, 0) = a. For a quick, informal proof, note that a = a 1 and 0 = a 0, implying a divides both a and 0, which is the rst requirement for a to be the GCD of a and 0.. The second requirement is trivial since the hypothesis assumes c | a, which is also the conclusion, since d = a. Lemma 3 If a, b Z, b = 0, q, r Znonneg , and a = b q + r, then gcd(a, b) = gcd(b, r). CHAPTER 5. FROM THEOREMS TO ALGORITHMS 41 For a proof, consult your discrete math textbook. In constructing an algorithm, we will follow the same steps as in the previous section. Consider this section to be a self-exercise; you should gure out what to do at each step before reading it. We structure this by asking a series of questions. What do the lemmas tell us? This step is more complicated than in the previous section because we have two lemmas. Do they not tell us di erent things? We can simplify this by unifying their conclusions. In fact, they both tell us an equivalent expression for gcd(a, b), except that Lemma 2 deals with the special case of b = 0. This tips us o that the loop invariant will probably involve the gcd. To pin it down, we will rst need to know what changes. What does Lemma 2 tell us? What distinguishes Lemma 2 from Lemma 3 is that Lemma 2 gives us a nal answer, not just a new gcd problem. This means that if the special condition of this lemma is met, we are nished. Thus we have our termination condition: b = 0. What does Lemma 3 tell us? Lemma 3 gives us another gcd problem, but it is helpful because, although it has di erent parameters, it has the same result. This helps us identify what will change during the course of the computation: the parameters to gcd. Knowing what changes is necessarily contrasted to what does not change: the gcd of those parameters. If we consistently call the rst parameter a and the second parameter b (and distinguish the original parameters as a0 and b0 ), we can now state our three properties: Initial conditions: a = a0 and b = b0 . Loop invariant: gcd(a, b) = gcd(a0 , b0 ). Termination condition (again): b = 0. What does the body of the loop do? It mutates a and b, of course. Following Lemma 2, we replace a with b and b with r (we know what r is because of the division algorithm). Notice that since r < b (by the QRT), we are decreasing b and therefore making progress towards termination. Let us call the variables after iterations i ai and bi (in line with out names for the original parameters, since a0 and b0 represent a and b after 0 iterations). Then, for qi and ri such that ai = qi bi + ri , ai+1 bi+1 = = bi ri = ai mod bi That is all we need to construct the program. This algorithm is called the Euclidean Algorithm. - fun gcd(a0, b0) = = let val a = ref a0; CHAPTER 5. FROM THEOREMS TO ALGORITHMS = val b = ref b0; = in = (while !b <> 0 do = (a := !b; = b := !a mod !b); = !a) = end; val gcd = fn : int * int -> int - gcd(21, 36); val it = 36 : int 42 What went wrong? 36 certainly is not the GCD of 21 and 36. Is not developing loop invariants and termination conditions in parallel to writing algorithms supposed to prevent errors like that? This kind of situation is what Donald Knuth had in mind when he said, Beware of bugs in the above code; I have only proved it correct, not tried it. However, our formal reasoning still speeds our identi cation of the problem. In this case, a0 is 21 and b0 is 36. a1 should be set to b0 , also 36. b1 should be set to a0 mod b0 = 21 mod 36 = 21. But look carefully. a has already changed, so b1 is instead set to a1 mod b0 = 36 mod 36 = 0. There is our problem we need to calculate r before we change a. We could do this using a let expression. - fun gcd(a0, b0) = = let val a = ref a0; = val b = ref b0; = in = (while !b <> 0 do = let val r = !a mod !b = in = (a := !b; = b := r) = end; = !a) = end; val gcd = fn : int * int -> int - gcd(21, 36); val it = 3 : int - gcd(36, 21); val it = 3 : int - gcd(17, 20014); val it = 1 : int - gcd(24, 64); val it = 8 : int Self-exercise. Correct the earlier gcd() function without using an extra let expression. Instead, make r to be another reference variable in the rst let CHAPTER 5. FROM THEOREMS TO ALGORITHMS 43 statement and update it. The initial value of r will not be used; it can be a dummy value. 5.3 The Euclidean Algorithm, another way The rst thing we did in deriving the Euclidean algorithm was unify the statements in Lemmas 2 and 3. Restating them as one lemma, we could have Lemma 2-3 If a, b Z, then gcd (a, b) = a if b = 0 gcd (b, a mod b) otherwise Not only is this more concise (partly because we employed the mod operation guaranteed by the QRT), but it also expresses the result in an if-then-else format. We have taken the special condition of Lemma 2 and made it the conditional of the if. Could we use this in constructing an ML function? We have already seen predicates and functions that are de ned in terms of other predicates and functions (your solution to the self-exercise of Section 5.1 is the most recent example). This is di erent because the GCD is de ned in terms of itself rather than in terms of another function. This invites us to do some wishful thinking: If only the function gcd were already de ned, de ning it would be so much easier. ML, like most programming languages, allows this sort of wishing: - fun gcd(a, b) = if b = 0 then a else gcd(b, a mod b); val gcd = fn : int * int -> int - gcd(21, 36); val it = 3 : int - gcd(36, 21); val it = 3 : int - gcd(17, 20014); val it = 1 : int - gcd(24, 64); val it = 8 : int What happened to the loop? Instead of e ecting repetition by a command to iterate (as with a while statement), repetition happens implicitly by the repeated calling of the function. Instead of mutating reference variables, we feed di erent parameters into gcd each time. Essentially we have gcd(21, 36) = gcd(36, 21) = gcd(21, 15) = gcd(15, 6) = gcd(6, 3) = gcd(3, 0) = 0 CHAPTER 5. FROM THEOREMS TO ALGORITHMS 44 This style of programming is called recursion, meaning self-reference. A simple recursive function has a conditional statement separating two cases: a base case, a point at which the recursion stops, and a recursive case, which involves a call to itself. Naturally, recursion can become more complicated than that, but for the time being, notice how the termination condition of the iterative version corresponds to the base case of the recursive version. What s most striking is how much more concise the recursive version is. Recursive solutions do tend to be compact and elegant. Something else, however, is also at work here. One of the traits of the programming style for which ML is intended is a heavy use of recursion. More generally, ML is geared toward a functional or applicative style of programming, where the central concept of building algorithms is the applying of functions, as opposed to the repeating of loop bodies or the stringing together of statements. This style will be taught for the remainder of this book. Self-exercise. Try writing a recursive version of the division algorithm. If you have trouble, do not become discouraged. 5.4 Exercises The exercises in this chapter are developed from Abelson and Sussman, Structure and Interpretation of Computer Programs, McGraw Hill, 1996, pages 46-47. 1. The following lemmas give no particularly surprising result. Lemma 4 If a, b, Z, then a b0 = a. Lemma 5 If a, b, n Z, then a bn = (a b)bn 1 . However, they can be used to generate an algorithm for exponentiation using repeated multiplication. Write an ML function for such an algorithm, to compute xy . (Hint: If initially a = 1, b = x, and n = y, then a bn = xy , which is also your invariant. Your postcondition is that a = xy , b = x, and n = 0. 2. The algorithm you used in Exercise 1 required n multiplication operations. Write a function implementing a faster version of this algorithm by making use of the following additional lemma. Lemma 6 If a, b, n Z and n is even, then a bn = a(b2 ) 2 . n (If you have done this correctly, your function/algorithm should require about log2 n multiplications. Can you tell why?) CHAPTER 5. FROM THEOREMS TO ALGORITHMS 45 3. In a similar way, use the following three lemmas to write a function that computes x y without using multiplication. (To make it fast, you will need to use division, but only dividing by 2.) Lemma 7 If a, b Z, then a + b 0 = a. Lemma 8 If a, b, c Z, then a + b c = a + b + b (c 1). Lemma 9 If a, b, c Z and c is even, then a + b c = a + (b + b)(c 2). 4. Write a recursive version of your function from Exercise 1. 5. Write a recursive version of your function from Exercise 2. 6. Write a recursive version of your function from Exercise 3. Chapter 6 Relations This chapter continues the discussion from Chapter 3 and introduces you to Representing relations as a set of ordered pairs, in turn representing sets as lists. Representing relations as predicates. The centrality of the function to ML programming has been asserted repeatedly, even though we have deferred a formal treatment of functions. Relations provide a good prologue to functions because a function is a specialized or restricted kind of relation. As we now turn our attention to relations, recall that we have found several ways to represent sets in ML: datatypes, lists, and predicates can all be used to capture certain aspects of the idea of sets. Here we will dissect two ways of representing relations in ML which correspond to two common ways to write them mathematically. First, since a relation is a set of ordered pairs, we might write a relation that relates cities to baseball teams as { (Toronto, BlueJays), (Chicago, Cubs), (Cincinnati, Reds), (New York, Yankees), (St Louis, Cardinals), (Chicago, White Sox) (Houston, Astros), (New York, Mets), (Boston, Red Sox) } R= However, if (a, b) R, then we also write a R b or R(a, b). The latter notation looks like a predicate, and indeed, a relation de nes a predicate, implying another way to think about relations The principal matter of this chapter is to begin thinking of relations as entities or objects or, in keeping with terminology previously used in discussing ML, values. Since this is most apparent when represented as lists, we begin our discussion there. 46 CHAPTER 6. RELATIONS 47 6.1 Relations represented by lists For our running example we will explore the relations among geographic entities of biblical Israel. As raw material, we represent (as datatypes) the sets of tribes and bodies of water. - datatype Tribe = Asher | Naphtali | Zebulun | Issachar = | Dan | Gad | Manasseh | Reuben | Ephraim | Benjamin = | Judah | Simeon; datatype Tribe = Asher | Benjamin | Dan | Ephraim | Gad | Issachar | Judah | Manasseh | Naphtali | Reuben | Simeon | Zebulun - datatype WaterBody = Mediterranean | Dead | Jordan = | Chinnereth | Arnon | Jabbok; datatype WaterBody = Arnon | Chinnereth | Dead | Jabbok | Jordan | Mediterranean Now let us consider the relation from Tribe to WaterBody de ned so that tribe t is related to water body w if t is bordered by w. - val tribeBordersWater = = [(Asher, Mediterranean), (Manasseh, Mediterranean), = (Ephraim, Mediterranean),(Naphtali, Chinnereth), = (Naphtali, Jordan), (Issachar, Jordan), (Manasseh, Jordan), = (Gad, Jordan), (Benjamin, Jordan), (Judah, Jordan), = (Reuben, Jordan), (Judah, Dead), (Reuben, Dead), = (Simeon, Dead), (Gad, Jabbok), (Reuben, Jabbok), = (Reuben, Arnon)]; val tribeBordersWater = [(Asher,Mediterranean),(Manasseh,Mediterranean), (Ephraim,Mediterranean), (Naphtali,Chinnereth), (Naphtali,Jordan),(Issachar,Jordan), (Manasseh,Jordan),(Gad,Jordan),(Benjamin,Jordan), (Naphtali,Jordan),(Issachar,Jordan), (Judah,Jordan), (Reuben,Jordan), (Judah,Dead),...] : (Tribe * WaterBody) list The important thing is the type reported by ML: (Tribe * WaterBody) list. Since (Tribe * WaterBody) is the Cartesian product or pair (2-tuple) and list is how we represent sets, this accords precisely with our de nition of a relation as a set or ordered pairs. It is now only a slight modi cation to our predicate for set membership from Chapter 3 which gives us a predicate for determining if a tribe is thus related to a body of water. - fun relatedTo(a, b, []) = false = | relatedTo(a, b, (h1, h2)::rest) = = (a = h1 andalso b = h2) orelse relatedTo(a, b, rest); CHAPTER 6. RELATIONS Chinnereth Jordan Dead Arnon Jabbok 48 Chinnereth Jabbok Mediterranean Arnon Jordan Dead Mediterranean (a) Partition inducing equivalence relation is vertically aligned with (b) Hasse diagram of partial order is west of Figure 6.1: Illustration of relations val relatedTo = fn : a * b * ( a * b) list -> bool - relatedTo(Asher, Mediterranean, tribeBordersWater); val it = true : bool - relatedTo(Asher, Arnon, tribeBordersWater); val it = false : bool - relatedTo(Judah, Dead, tribeBordersWater); val it = true : bool 6.2 Operations on relations Now that we have relations represented as entities, we would like to operate on them, such as testing them for certain properties (is a relation re exive, symmetric, antisymmetric, or transitive; is a relation an equivalence relation or a partial order?) and deriving other relations from them (re exive, symmetric, and transitive closures). We will use the fact that bodies of water in ancient Israel line up fairly well north and south to generate working examples for this section. The Mediterranean Sea is by itself on the west. The Sea of Chinnereth, the Jordan River, and the Dead Sea ow in an approximately straight, vertical line. The Arnon River and Jabbok River each ow into and perpendicular to the Jordan from the east, parallel to each other. Hence relations is vertically aligned with and is west of should be an equivalence relation and a partial order, respectively, as illustrated in Figure 6.1. 6.2.1 = = = = = Property testing val waterWestOf = [(Mediterranean, Chinnereth), (Mediterranean, Jordan), (Mediterranean, Dead), (Mediterranean, Jabbok), (Mediterranean, Arnon), (Chinnereth, Jabbok), (Chinnereth, Arnon) (Jordan, Jabbok), (Jordan, Arnon), (Dead, Jabbok), (Dead, Arnon)]; Let us (na vely) represent these two relations. CHAPTER 6. RELATIONS 49 val waterWestOf = [(Mediterranean,Chinnereth),(Mediterranean,Jordan), (Mediterranean,Dead), (Mediterranean,Jabbok), (Mediterranean,Arnon),(Chinnereth,Jabbok), (Chinnereth,Arnon),(Jordan,Jabbok),(Jordan,Arnon), (Dead,Jabbok), (Dead,Arnon)] : (WaterBody * WaterBody) list - val waterVerticalAlign = = [(Chinnereth, Jordan), (Chinnereth, Dead), = (Jordan, Chinnereth), (Jordan, Dead), (Dead, Chinnereth), = (Dead, Jordan), (Jabbok, Arnon), (Arnon, Jabbok)]; val waterVerticalAlign = [(Chinnereth,Jordan),(Chinnereth,Dead), (Jordan,Chinnereth),(Jordan,Dead), (Dead,Chinnereth), (Dead,Jordan),(Jabbok,Arnon), (Arnon,Jabbok)] : (WaterBody * WaterBody) list Equivalence relations and partial orders are transitive. A test for transitivity is delicate. Recall that a relation R is transitive if (a, b) and (b, c) R, (a, c) R. We can tame this by enforcing that (a, b) and (b, c) are chosen one at a time. Thus we rewrite, that R is transitive if (a, b) R, (b, c) R, (a, c) R. Now we can break down the problem. Assume that (a, b) is already chosen, and we want to know if transitivity holds with respect to (a, b). In other words, is it the case that for all elements to which b is related, a is also related to that element? Remembering that a relation is represented as a list, there are three cases for which we need to test, based on the makeup of the list: 1. The list is empty. Then true, the list is (vacuously) transitive with respect to (a, b). 2. The list begins with (b, c). Then the list is transitive with respect to (a, b) if (a, c) exists in the relation and the rest of the list is transitive with respect to (a, b). 3. The list begins with (c, d) for some c = b. Then the list is transitive with respect to (a, b) if the rest of the list is. Here is this de nition expressed in ML, where relation is the original relation. - fun testOnePair((a, b), []) = true = | testOnePair((a, b), (c,d)::rest) = = ((not (b=c)) orelse relatedTo(a, d, relation)) = andalso testOnePair((a,b), rest); CHAPTER 6. RELATIONS 50 Note two things. First, our symbolic notation is more expressive than ML in that we can write f orall (a, b) and (b, c) R , using b twice, but we cannot use a variable more than once in a pattern. That is, to write ... testOnePair((a, b), (b, c)::rest) = ... is not allowed. Hence we coalesce cases 2 and 3 as far as pattern matching goes and test b and c for equality. Second, it is crucial that in case 2 we test if (a, c) exists in the original relation, not just the rest of the list currently being examined. It would be a serious error to write ...((not (b=c)) orelse relatedTo(a, d, rest)) Self-exercise. Explain the assertion we just made about case 2. The di cult part is complete. Now we must cover the rst for all, which can be done by iterating over the list, testing each pair. fun test([]) = true = | test((a,b)::rest) = = testOnePair((a,b), relation) andalso test(rest) Note again that each pair must be tested on the original relation, not just the rest of the list. We saw in Chapter 4 the value of self-containment. We also require a valid variable relation that always holds the original. Hence, we package testOnePair and test into a single function. - fun isTransitive(relation) = = let fun testOnePair((a, b), []) = true = | testOnePair((a, b), (c,d)::rest) = = ((not (b=c)) orelse relatedTo(a, d, relation)) = andalso testOnePair((a,b), rest); = fun test([]) = true = | test((a,b)::rest) = testOnePair((a,b), relation) = andalso test(rest) = in = test(relation) = end; val isTransitive = fn : ( a * a) list -> bool Again, note the type. This predicate takes a ( a * a) list, that is, a relation. Self-exercise. Exercise 1 asks you to write a predicate isAntisymmetric after the pattern of isTransitive. Begin thinking about that now. CHAPTER 6. RELATIONS 51 6.2.2 Closure constructing Now we test our predicate isTransitive. - isTransitive(waterWestOf); val it = true : bool - isTransitive(waterVerticalAlign); val it = false : bool To our surprise (unless you are keen enough to spot the reason), it reports that waterVerticalAlign, a supposed equivalence relation, is not transitive. It is not even readily apparent whether the problem is with waterVerticalAlign or isTransitive, although the fact that we get the correct response for waterWestOf makes waterVerticalAlign suspect. The test for transitivity fails when pairs (a, b) and (c, d) exist in the list and b = c but (a, d) does not exist in the list. The most useful information we could get would be to say to isTransitive, Okay, what pairs did you nd that contradict transitivity? As we discovered from the previous chapter, functions do not need to be predicates they may return values other than boolean. What if we modi ed isTransitive so that it returns a set of counterexamples instead of a simple true or false? This would require the following changes to testOnePair: Instead of returning true on an empty list or when a is related to d, return [], that is, an empty list, indicating no contradicting pairs are found. Instead of returning false when a is not related to d, return [(a, d)], that is, a list containing the pair you expected. Instead of anding the (boolean) value of testOnePair on the rest of the list with what we found for the current pair, concatenate the result for the current pair to the (list) value of testOnePair. Observe the new function. - fun counterTransitive(relation) = = let fun testOnePair((a, b), []) = [] = | testOnePair((a, b), (c,d)::rest) = = (if ((not (b=c)) orelse relatedTo(a, d, relation)) = then [] else [(a, d)]) = @ testOnePair((a,b), rest); = fun test([]) = [] = | test((a,b)::rest) = testOnePair((a,b), relation) @ test(rest) = in = test(relation) = end; val counterTransitive = fn : ( a * a) list -> ( a * a) list CHAPTER 6. RELATIONS 52 Note the type. It is no trouble to write the same things again, and it is a safeguard for you. - counterTransitive(waterWestOf); val it = [] : (WaterBody * WaterBody) list - counterTransitive(waterVerticalAlign); val it = [(Chinnereth,Chinnereth),(Chinnereth,Chinnereth), (Jordan,Jordan), (Jordan,Jordan),(Dead,Dead), (Dead,Dead), (Jabbok,Jabbok), (Arnon,Arnon)] : (WaterBody * WaterBody) list There is the problem. We forgot to include self-loops when we de ned waterVerticalAlign. As we consider how to remedy this, note that counterTransitive is more than just a debugging tool. It provides the set of pairs missing from the relation which would make the relation transitive. With counterTransitive in our toolkit, a function that computes the transitive closure follows immediately. - fun transitiveClosure(relation) = = counterTransitive(relation) @ relation; This is not quite right. Suppose we test it out on a relation that relates tribes descended from Leah according to who immediately precedes whom in birth (since we are considering tribes as geographic entities, we have ignored Levi, who received no land). - val immediatelyPrecede = = [(Reuben, Simeon), (Simeon, Judah), = (Judah, Issachar), (Issachar, Zebulun)]; val immediatelyPrecede = [(Reuben,Simeon),(Simeon,Judah), (Judah,Issachar),(Issachar,Zebulun)] : (Tribe * Tribe) list - transitiveClosure(immediatelyPrecede); val it = [(Reuben,Judah),(Simeon,Issachar), (Judah,Zebulun),(Reuben,Simeon), (Simeon,Judah),(Judah,Issachar), (Issachar,Zebulun)] : (Tribe * Tribe) list The transitive closure should completely express who is older than whom, yet the answer is missing, for example, (Reuben, Issachar). By adding the pair (Reuben, Judah), we have also created a new missing pair. To compute the transitive closure correctly, we must repeatedly add the missing pairs until the relation is transitive. Algorithmically, we could write CHAPTER 6. RELATIONS 53 - fun transitiveClosure(relation) = = let val closure = ref relation = in = (while not (isTransitive(!closure)) do = closure := counterTransitive(!closure) @ !closure; = !closure) = end; val transitiveClosure = fn : ( a * a) list -> ( a * a) list - transitiveClosure(immediatelyPrecede); val it = [(Reuben,Zebulun),(Reuben,Issachar),(Simeon,Zebulun), (Reuben,Issachar), (Simeon,Zebulun),(Reuben,Judah), (Simeon,Issachar),(Judah,Zebulun), (Reuben,Simeon), (Simeon,Judah),(Judah,Issachar),(Issachar,Zebulun)] : (Tribe * Tribe) list This better informs our de nition. The transitive closure of a relation is The relation itself, if it is already transitive, or the transitive closure of the concatenation of the relation to its missing pairs. Hence in the applicative style, we have - fun transitiveClosure(relation) = = if isTransitive(relation) = then relation = else transitiveClosure(counterTransitive(relation) = @ relation); val transitiveClosure = fn : ( a * a) list -> ( a * a) list - transitiveClosure(immediatelyPrecede); val it = [(Reuben,Zebulun),(Reuben,Issachar),(Simeon,Zebulun), (Reuben,Issachar), (Simeon,Zebulun),(Reuben,Judah), (Simeon,Issachar),(Judah,Zebulun), (Reuben,Simeon), (Simeon,Judah),(Judah,Issachar),(Issachar,Zebulun)] : (Tribe * Tribe) list Self-exercise. Think about Exercise 2, which asks you to write a function that constructs a set of counterexamples to antisymmetry. 6.3 Relations represented by predicates Recall that our initial problem with the relation waterVerticalAlign was that it was missing self-loops. Hence the re exive closure would have solved our CHAPTER 6. RELATIONS 54 problem just as well as the transitive closure. Unfortunately, there is no way to compute the re exive closure given our way of representing relations. In fact, we cannot even test for re exivity. That would require iterating through all elements in a set (that is, datatype), for which ML provides no mechanism. This is particularly frustrating because the re exive closure is so simple. For example, we could write a predicate like relatedTo except that it tests if two elements are related by the relation s re exive closure: - fun relatedToReflexiveClosure(a, b, relation) = = a = b orelse relatedTo(a, b, relation); val relatedToReflexiveClosure = fn : a * a * ( a * a) list -> bool This suggests that our situation could be remedied if we followed the intuition of the second form of notation for relations; that is, what if we thought of relations as 2-place predicates? We did this already back in Section 2.4; the 2-place predicate eats is a relation. What we do not want to lose is the ability to treat relations as what we have been calling entities. What we mean is that we should be able to pass a relation to a function and have a function return a relation, as transitiveClosure does. The technical term for this (something that can be passed and returned) is rst-class value. A pillar of functional programming is that functions are rst-class values. A relation represented by a predicate will have the type ( a * b) bool. This means function that maps an a/ b pair to a bool. Our rst task is to write a function that will convert from list representation to predicate representation. To return a predicate from a function, simply de ne the predicate (locally) within the function and return it by naming it without giving any parameters. - fun listToPredicate(oldRelation) = = let fun newRelation(a, b) = relatedTo(a, b, oldRelation); = in = newRelation = end; val listToPredicate = fn : ( a * b) list -> a * b -> bool In a similar way, a function can receive a predicate. Observe this function to compute the re exive closure: - fun reflexiveClosure(relation) = = let fun closure(a, b) = a = b orelse relation(a, b); = in = closure = end; val reflexiveClosure = fn : ( a * a -> bool) -> a * a -> bool CHAPTER 6. RELATIONS List yes indirectly no yes yes yes no yes yes yes Predicate yes directly no no no no yes yes no no 55 rst class value membership test isReflexive isSymmetric isTransitive isAntiSymmetric reflexiveClosure symmetricClosure transitiveClosure convert to other Table 6.1: Comparison of list and predicate representations for relations Computing the symmetric closure is left as an exercise. We cannot compute the transitive closure directly, but if the relation is originally represented as a list, we could compute the transitive closure before converting to predicate form. 6.4 Lists versus predicates As in Chapter 3, we are faced with the dilemma of choosing among two representations, each of which have favorable features and drawbacks. The list representation is the more intuitive, at least in terms of the formal de nition of a relation; with the predicate representation, however, we can test a pair for membership directly, as opposed to relying on a predicate like relatedTo. Because we can iterate through all pairs, the list representation allows testing and computing of transitivity, but with the predicate representation we can compute re exive and symmetric closures. Conversion is one-way, from lists to predicates. Neither representation allows us to test re exivity. These aspects are summarized in Table 6.1. Every no would become a yes if only we had the means to iterate through all elements of a datatype. Finally, a word of interest only to those who have programmed in an objectoriented language such as Java. When you read examples like these, you should be thinking about the best way to represent a concept in other programming languages. In an object-oriented language, objects are rst-class values; hence we would want to write a class to represent relations. The primary di erence between the list and predicate representations presented here is that the former represents the relation as data, the latter as functionality. The primary characteristic of objects is that they encapsulate data and functionality together in one package. Since the predicate representation can be built from a list representation, one would expect that it would be strictly more powerful; however, in the conversion we lost the ability to test for transitivity, and this is because we have lost access to the list. If instead we made the list to be an instance variable of a class, the methods could do the functional work of reflexiveClosure as CHAPTER 6. RELATIONS 56 well as the iterative work of isTransitive. Moreover, Java 5 has enum types unavailable in earlier versions of Java, which provide the functionality of ML s datatypes, as well as a means of iterating over all elements of a set, something that hinders representing relations as lists or predicates in ML (ML s datatype is more powerful than Java s enum types in other ways, however). Figure 6.2 shows an implementation of relations using Java 5. 6.5 Exercises 1. Write a predicate isAntisymmetric which operates on the list representation of relations, similar to isTransitive. 2. Write a function counterAntisymmetric which operates on the list representation of relations, constructing a list of counterexamples to antisymmetry, doing for isAntisymmetric what counterTransitive does for transitivity (except that it creates a list of extraneous pairs, not missing pairs). 3. Write isSymmetric and counterSymmetric which operate of the list representation of relations. 4. When we say that there is no way to compute re exivity or the re exive closure using the list representation, we are assuming the relation is de ned over the entire set, not just the subset of element that are represented somewhere in the relation. This all becomes possible if we assume the subset view. Write such functions, isReflexive and reflexiveClosure. 5. Write a function that computes the symmetric closure for the predicate representation. CHAPTER 6. RELATIONS /** * Class Relation to model mathematical relations. * The relation is assumed to be over a set modeled * by a Java enum. * * @author ThomasVanDrunen * Wheaton College * June 30, 2005 */ public class Relation<E extends Enum<E>> { private class List { public E first; public E second; public List tail; public List(E first, E second, List tail) { this.first = first; this.second = second; this.tail = tail; } /** * Concatenate a give list to the end of this list. * @param other The list to add. * POSTCONDITION: The other list is added to * the end of this list; the other list is not affected. */ public void concatenate(List other) { if (tail == null) tail = other; else tail.concatenate(other); } } /** * The set ordered pairs of the relation. */ private List pairs; /** * Constructor to create a new relation of n pairs from an * n by 2 array. * @param input The array of pairs; essentially an array of * length-2 arrays of the base enum. */ public Relation(E[][] input) { for (int i = 0; i < input.length; i++) { assert input[i].length == 2; pairs = new List(input[i][0], input[i][1], pairs); } } public boolean relatedTo(E a, E b) { for (List current = pairs; current != null; current = current.tail) if (current.first == a && current.second == b) return true; return false; } public boolean isReflexive() { // if there are no pairs, we can assume this is // not reflexive. if (pairs == null) return false; try { for (E t : (E[]) pairs.first.getClass() .getMethod("values").invoke(null)) if (! relatedTo(t, t)) return false; } catch (Exception e) { } // won t happen return true; } public boolean isSymmetric() { for (List current = pairs; current != null; current = current.tail) if (! relatedTo(current.second, current.first)) return false; return true; } private boolean isTransitiveWRTPair(E a, E b) { for (List current = pairs; current != null; current = current.tail) if (b == current.first && ! relatedTo(a, current.second)) return false; return true; } public boolean isTransitive() { for (List current = pairs; current != null; current = current.tail) if (! isTransitiveWRTPair(current.first, current.second)) return false; return true; } public boolean isAntisymmetric() { if (isSymmetric()) return false; for (List current = pairs; current != null; current = current.tail) if (current.first != current.second && relatedTo(current.second, current.first)) return false; return true; } public Relation reflexiveClosure() { if (isReflexive()) return this; else return new Relation<E>() { public boolean isReflexive() { return true; } public boolean relatedTo(E a, E b) { return a == b || Relation.this.relatedTo(a, b); } }; } public Relation symmetricClosure() { if (isSymmetric()) return this; else return new Relation<E>() { public boolean isSymmetric() { return true; } public boolean relatedTo(E a, E b) { return Relation.this.relatedTo(a, b) || Relation.this.relatedTo(b, a); } }; } 57 private List counterTransitiveWRTPair(E a, E b) { List toReturn = null; for (List current = pairs; current != null; current = current.tail) if (b == current.first && ! relatedTo(a, current.second)) toReturn = new List(a, current.second, toReturn); return toReturn; } private List counterTransitive() { List toReturn = null; for (List current = pairs; current != null; current = current.tail) { List currentCounter = counterTransitiveWRTPair(current.first, current.second); if (currentCounter != null) { currentCounter.concatenate(toReturn); toReturn = currentCounter; } } return toReturn; } /** * Default constructor used by transitiveClosure(). */ private Relation() {} public Relation transitiveClosure() { if (isTransitive()) return this; Relation toReturn = new Relation(); toReturn.pairs = counterTransitive(); toReturn.pairs.concatenate(pairs); return toReturn.transitiveClosure(); } public boolean isEquivalenceRelation() { return isReflexive() && isSymmetric() && isTransitive(); } public boolean isPartialOrder() { return isReflexive() && isAntisymmetric() && isTransitive(); } } Figure 6.2: Implementation relations in Java 5. Chapter 7 Functions This chapter formally introduces the concept of a function in ML, discussing about functions that they are de ned as parameterized expressions. that they are used as components in an algorithm. that the scope of their names includes their bodies. 7.1 De nition There are many ways to approach or think about functions. Perhaps your rst encounter with functions was in the context of curves that pass the vertical line test. In science and other mathematical elds, we use functions to describe the relationship between independent and dependent variables. The most rigorous way to de ne the term function is to consider it a special case of a relation from A to B (a set of ordered pairs, a subset of A B) in that each element of A occurs in the rst position of a pair exactly once in the relation. Thus we also think of functions as mappings from A to B. Functions most commonly are speci ed by a rule1 , such as 1 However, there do exist functions that cannot be speci ed by a rule, that is, cannot be computed. Since computer programs are stored as series of bits which can be interpreted as integers, the number of algorithms is countable. It is easy to derive a distinct function from every real number, an uncountable set. Thus there are more functions than algorithms, implying some functions cannot be computed. 58 CHAPTER 7. FUNCTIONS 59 f (x) g(x) = = 5x2 12x + 3 0 x2 1 x 1 x=1 otherwise h(true) = h(false) = 1 0 A rule or formula is much more expressive than the set representation we have seen with relations. Suppose we want to express the squares of all positive integers. Since R+ is an in nite set, we would write {(1, 1), (2, 4), (3, 9), (4, 16), . . .} The ellipses notation is permissible for sets as long as we clearly establish a pattern. That may be ne for a countable set that is in nite in only one direction (like the positive integers), but it would be near impossible to denote a function over real numbers intelligibly. To use predicate notation would be comical since R(x, y) = if x2 = y gives the impression that we would discover y by trying (x, y) pairs until we found one that t. Since this is a mapping, where x as an independent variable is chosen and y as a dependent variable is calculated based on x, the most useful way to specify a function is to write it as an expression in terms of x. We know how to represent each of the earlier examples in ML. We simply generalize our concept of representing predicates, allowing them to have types other than bool. - fun parabola(x) = 5*x*x - 12*x + 3; val parabola = fn : int -> int - parabola(5); val it = 68 : int - parabola(0); val it = 3 : int - parabola( 3); val it = 84 : int - fun curve(x) = if x < 1.0 orelse x > 1.0 = then (x * x - 1.0) / (x - 1.0) else 0.0; val curve = fn : real -> real - curve(0.0); val it = 1.0 : real - curve (1.0); val it = 0.0 : real - curve(2.0); val it = 3.0 : real CHAPTER 7. FUNCTIONS - fun boolToInt(true) = 1 = | boolToInt(false) = 0; val boolToInt = fn : bool -> int - boolToInt(3 > 5); val it = 0 : int - boolToInt(false orelse true); val it = 1 : int 60 Notice we are talking about representing functions, just as we recently grappled with how best to represent relations. The di erence is that we need not bend a language construct (like list or predicate, in the case of relations) to model functions because ML already supports a representation of functions. Thus, in the context of ML, a function is a parameterized expression. Although we had not introduced the term parameterize at the time, this de nition is conceptually consistent with that which we gave for predicates in Chapter 2. The pattern-matching forms seem to follow this concept less well; however, we can consider them to be shorthand for long if-then-else chains. Look again at the above examples of functions in ML. We would readily recognize 5*5*5 - 12*5 + 3 as an expression. By replacing some of the 5 s with the free variable x and encapsulating it in a fun form, we see how we have parameterized it so it can be evaluated for many values of x. However, there is more to consider. As anticipated in the previous chapter, in ML, functions are values. An identi er like curve is really a variable; it happens to store a value that is a function. As we have seen before, it can be used in an expression without applying any value to its parameters. - curve; val it = fn : real -> real - it(2.0); val it = 3.0 : real Once the value of curve has been saved to it, it can be used as a function. Of course the type of this function is real real, meaning function from real to real , or something which, if applied to a real, produces a real. In fact, we need not store a function in a variable; to write an anonymous function, use the form fn <identi er> => <expression> Thus we have - fn x => (x + 3) mod 5; val it = fn : int -> int - it(15); val it = 3 : int Or even CHAPTER 7. FUNCTIONS - (fn x => (x + 3) mod 5) (15); val it = 3 : int Make sure you understand that last example. Note that fun <identi er>1 ( <identi er>2 ) = <expression>; is equivalent to val <identi er>1 = fn <identi er>2 => <expression>; 61 As a nal example of this section, consider something from the real world. To convert from Fahrenheit to Celsius, we need to take into consideration that 5 a degree Fahrenheit is 9 of a degree Celsius and that degrees Fahrenheit are o by 32 , that is, 32 F corresponds to 0 C. Thus we parameterize an expression to write the following function to convert from Fahrenheit to Celsius. - fun fahrToCel(t) = 5 * (t - 32) div 9; val fahrToCel = fn : int -> int - fahrToCel(32); val it = 0 : int - fahrToCel(98); val it = 36 : int - fahrToCel( 15); val it = 27 : int Self-exercise. Write a corresponding function celToFahr. Then rewrite both functions using the val/fn form. 7.2 Functions as components An interesting topic in the study of mathematical functions is function composition given two functions f (x) and g(x), the composition of f and g is the function that results from feeding the result of f into the input of g, say h(x) = g f (x) = g(f (x)). Since this idea comes readily in ML, we will cover a more generalized concept, using an example. We are already used to the idea of a function application being an expression and therefore usable wherever a subexpression is appropriate in a larger expression. The input to a function may be any expression, so we can apply a function to another function application. Functions f and g may be composed only if the co-domain of f is a subset of the domain of g. In ML, we are concerned about types, and so a function of type a b can be applied to any expression whose type matches the function s input type ( a), and an application of the function may be used wherever a subexpression of the function s return type ( b) is required. CHAPTER 7. FUNCTIONS 62 The important matter is to consider functions as components. They are black box machines into which you can put raw material, out of which comes a product depending on the raw material, and which can be hooked up or arranged in ways to produce new machines. Our approach to building algorithms from this point on is the de ning and applying of functions, like so many interlocking machines. For our example, suppose we want to write a program that will predict the height of a tree after a given number of years. The prediction is calculated based on three pieces of information: the variety of tree (assuming that this determines the tree s growth rate), the number of years, and the initial height of the tree. Obviously this assumes tree growth is linear, as well as making other simplifying assumptions not based on any real botany. Suppose we associate the following growth rates in terms of units per month for the following varieties of tree. - datatype treeGenus = Oak | Elm | Maple | Spruce | Fir | = Pine | Willow; datatype treeGenus = Elm | Fir | Maple | Oak | Pine | Spruce | Willow - fun growthRate(Elm) = 1.3 = | growthRate(Maple) = 2.7 = | growthRate(Oak) = 2.4 = | growthRate(Pine) = 0.4 = | growthRate(Spruce) = 2.9 = | growthRate(Fir) = 1.1 = | growthRate(Willow) = 5.3; val growthRate = fn : treeGenus -> real growthRate is a function mapping tree genera to their rates of growth. We have overlooked something, though. There is a larger categorization of these trees that will a ect how this growth rate is applied: coniferous trees grow all year round, but deciduous trees are inactive during the winter. The number of months used for growing, therefore, is a function of the taxonomic division. - datatype treeDivision = Deciduous | Coniferous; datatype treeDivision = Coniferous | Deciduous - fun growingMonths(Deciduous) = 6.0 = | growingMonths(Coniferous) = 12.0; val growingMonths = fn : treeDivision -> real Because of the hierarchy of taxonomy, we can determine a tree s division based on its genus. To avoid making the mapping longer than necessary, we pattern match on coniferous trees and make deciduous the default case. - fun division(Pine) = Coniferous = | division(Spruce) = Coniferous = | division(Fir) = Coniferous CHAPTER 7. FUNCTIONS = | division(x) = Deciduous; val division = fn : treeGenus -> treeDivision 63 Since division maps treeGenus to treeDivision and growingMonths maps treeDivision to real, their composition e ectively maps treeGenus to real. The predicted height of the tree is of course the initial height plus the growth rate times growth time. The growth rate is calculated from the genus, and the growth time is calculated by multiplying years times the growing months. Thus we have - fun predictHeight(genus, initial, years) = = initial + (years * growingMonths(division(genus)) = * growthRate(genus)); val predictHeight = fn : treeGenus * real * real -> real - predictHeight(Pine, 7.3, 5.0); val it = 31.3 : real - predictHeight(Willow, 8.1, 4.0); val it = 135.3 : real - predictHeight(Spruce, 6.0, 3.0); val it = 110.4 : real - predictHeight(Oak, 6.0, 3.0); val it = 49.2 : real Self-exercise. As you can observe from the applications of predictHeight, we intend this to calculate only based on whole years. After all, calculating the growth for, say, half a year would depend heavily on which half of the year for deciduous trees. Rewrite predictHeight so its type is treeGenus * real * int real. (Hint: remember the conversion functions from Section 1.3.) 7.3 Scope Our nal observation is also one that has been anticipated. Put in terms of the rules of ML, we note that the scope of a function s name (when used with the fun form2 ) includes the body of the function. What this means is that a function can call itself. Furthermore, since each call of the function (including calls that are simultaneously active) has its own local copy of the parameters, we can use this in place of reference variables. Functional programming is a style of programming in which no variables are modi ed. Instead, all variables are considered parameters of a function. The distinction and how recursive calls make this possible will be demonstrated by transforming our iterative factorial function from Chapter 4 into the functional style. Here is that function again (with the slight modi cation that we count from 1 to n instead of from 0 to n 1, and accordingly we update fact before i): 2 Thus the form using fun and the form using val and fn are not truly equivalent, because val/fn does not allow for a recursive use of the variable name. CHAPTER 7. FUNCTIONS -fun factorial(n) = = let val i = ref 1; = val fact = ref 1; = in = (while !i <= n do = (fact := !fact * !i; = i := !i + 1); = !fact) = end; val factorial = fn : int -> int 64 Our rst change is to encapsulate the body of the while loop into a function, which we will call factBody. The body of the while loop does two things: it updates both fact and i. Our function then will consume the old fact and i and produce new values. We can handle the fact that two values are produced by returning a tuple. Thus we also consolidate fact and i into one value, the tuple current. -fun factorial(n) = = let fun factBody(fact, i) = (fact * i, i + 1); = val current = ref (1, 1); = in = (while #2(!current) <= n do = current := factBody(!current);3 = #1(!current)) = end; val factorial = fn : int -> int Next we want to be more ambitious about how much of the work we subsume into the function factBody. Here is also where we make use of the fact that function names can be used recursively. All that the while loop is doing now is repeatedly calling factBody. Since factBody can call itself, it can eat up the rest of the while loop. Notice that the while is e ectively replaced with an if. Both are making the same decision either stop (and do not change the state of current or (fact, i)) or make the change and repeat. -fun factorial(n) = = let fun factBody(fact, i) = = if i <= n then factBody(i * fact, i + 1) = else (fact, i); = val current = ref (1, 1); = in = (current := factBody(!current); 3 The function factBody has two parameters, both ints. How then can we give it a tuple? Technically, ML functions can receive only one parameter, and the multi-parameter functions we have written in fact receive tuples. In the places where we apparently pass such functions separate items, tuples are automatically created. CHAPTER 7. FUNCTIONS = #1(!current)) = end; val factorial = fn : int -> int 65 Now we notice that the second item in current is no longer used. current can be a single int; accordingly, that is all factBody should return, and the main call to factBody needs to be given the initial value for the second item in the old current. -fun factorial(n) = = let fun factBody(fact, i) = = if i <= n then factBody(i * fact, i + 1) = else fact; = val current = ref 1; = in = (current := factBody(!current, 1); = !current) = end; val factorial = fn : int -> int Next, consider the statement list. It is rather silly to store the result of the main call to factBody and immediately retrieve it. Why not replace the statement list with just the call? -fun factorial(n) = = let fun factBody(fact, i) = = if i <= n then factBody(i * fact, i + 1) = else fact; = val current = ref 1; = in = factBody(!current, 1) = end; val factorial = fn : int -> int Now that current is never updated, there is no need for it to be a reference variable or a variable at all, for that matter. We replace its one use with its value, 1. -fun factorial(n) = = let fun factBody(fact, i) = = if i <= n then factBody(i * fact, i + 1) = else fact; = in = factBody(1, 1) = end; val factorial = fn : int -> int CHAPTER 7. FUNCTIONS 66 Next we make use of the associativity of multiplication. Our current version of factBody performs its multiplication rst and then passes the result to the next call, essentially (. . . (((1 1) 2) 3) . . . n). fact gets bigger on the way down and the result is unchanged as it comes back up. Instead we can do the multiplication after the call, so fact stays the same on the way down but the result gets bigger as it comes back up, essentially (1 (1 (2 (3 . . . (n) . . .)))). -fun factorial(n) = = let fun factBody(fact, i) = = if i <= n then i * factBody(fact, i + 1) = else fact; = in = factBody(1, 1) = end; val factorial = fn : int -> int We can also make use of the commutativity of multiplication. Instead of starting at 1 and proceeding until we hit n, we can start at n and regress until we hit 0, essentially (n (n 1 (n 2 (. . . (1) . . .)))). -fun factorial(n) = = let fun factBody(fact, i) = = if i = 0 then fact = else i * factBody(fact, i - 1); = in = factBody(1, n) = end; val factorial = fn : int -> int We also could have noticed before the previous step that fact no longer varies with each call. We can eliminate it from the parameter list and replace its use by its only value, 1. -fun factorial(n) = = let fun factBody(i) = = if i = 0 then 1 = else i * factBody(i - 1); = in = factBody(n) = end; val factorial = fn : int -> int Now all factorial does is make a local function and apply it to its input without modi cation. We may as well replace its body with the body of factBody but be careful to rename i to be n. -fun factorial(n) = = if n = 0 then 1 CHAPTER 7. FUNCTIONS = else n * factorial(n - 1); val factorial = fn : int -> int Finally, we use the magic of pattern-matching. -fun factorial(0) = 1 = | factorial(n) = n * factorial(n - 1); val factorial = fn : int -> int We now have a two-line function equivalent to our old nine-line function. 67 7.4 Exercises 1. Write functions to compute the circumference and area of a circle, based on radius, the surface area and volume of a sphere based on radius, and the surface area and volume of a cylinder based on radius of base and height. Use real values and reuse functions as much as possible. 2. Make a data type that represents a set of six of your friends. Then write a function cpo that maps these friends to their CPO (campus post o ce) address. At the mailbox section of the CPO, boxes 1-2039 are on the west wall, 2040-2579 are on the north wall, and 2580-3299 are on the east wall. Make a data type for the set of these three walls, and write a function that maps CPO address to their appropriate wall. Then write a function (using the two functions you just wrote) that maps your friends to the wall where one can nd their box. 3. Write a recursive version of your solution to Exercise 1 of Chapter 4. 4. Write a recursive version of your solution fo Exercise 2 of Chapter 4. Chapter 8 Set operations This chapter introduces a set of exercises which ask you to complete the representation of sets begun in Chapter 3 using our understanding of functions from the previous chapter. This chapter also furnishes practice and examples of recursive problem-solving. 8.1 Introduction Suppose we are modeling the courses that make up a typical mathematics or computer science program, and we wish to analyze the requirements, overlap, etc. We have found that datatypes are a good t for modeling a universal set. We can then use lists to model sets of elements within the universe. - datatype Course = Calculus | Discrete | Programming | LinearAlg = | DiffEq | OperatingSystems | RealAnalysis | Algebra = | Statistics | Algorithms | Compilers; datatype Course = Algebra ... - val csCourses = [Discrete, Programming, OperatingSystems, = Algorithms, Compilers]; val csCourses = [Discrete,Programming,OperatingSystems, Algorithms,Compilers] : Course list - val mathCourses = [Calculus, Discrete, LinearAlg, DiffEq, = RealAnalysis, Algebra, Statistics]; val mathCourses = [Calculus,Discrete,LinearAlg,DiffEq,RealAnalysis, Algebra,Statistics] : Course list In Chapter 3 we wrote a predicate elementOf that tested for set inclusion by comparing the element in question with the head of the list representing the set, and, if the element is not equal to the head, testing for the inclusion in the subset represented by the rest of the list. The process bottoms out at the 68 CHAPTER 8. SET OPERATIONS 69 base case of the list or set being empty, as no element is a member of the empty set. - fun elementOf(x, []) = false = | elementOf(x, y::rest) = x = y orelse elementOf(x, rest); val elementOf = fn : a * a list -> bool - elementOf(Calculus, csCourses); val it = false : bool - elementOf(Discrete, csCourses); val it = true : bool We also discussed how using lists to model sets has its drawback because in general there is no analogue in ML for standard list operations. Our present concern is to provide list operations by writing functions as we did for elementOf. Most of these operations in some way require iterating over the list. The basic pattern for deriving such an algorithm is to ask the questions How can I split the problem into what to do with the rst item, and what to do with the rest of the list? What do I do with an empty list? We answered these questions when formulating elementOf, and you did as well in your answers to the exercises of Chapter 3. What makes our current problems more di cult is that some set operations are not mere predicates but produce new sets. This raises the question of how to construct a set, starting with the base case and adding at the recursive case. For example, we have noted that lists allow for elements to appear multiple times, a property that is undesirable for representing sets. Therefore it is useful to have a function that will purge a list of all repeats. An empty list obviously has no repeats, so when given an empty list our function should just return it. If the list is non-empty (and hence ts the pattern a::rest where a is the head and rest is the tail), we check if a exists anywhere in rest if it does, we should return rest without it, but otherwise a x it to the front, but in either case we must rst purge rest itself. - fun makeNoRepeats([]) = [] = | makeNoRepeats(a::rest) = = if elementOf(a, rest) then makeNoRepeats(rest) = else a::makeNoRepeats(rest); val makeNoRepeats = fn : a list -> a list - makeNoRepeats([Calculus, RealAnalysis, Compilers, = Calculus, Calculus, RealAnalysis]); val it = [Compilers,Calculus,RealAnalysis] : Course list CHAPTER 8. SET OPERATIONS 70 makeNoRepeats is a useful tool. We can now use it to clean up after a haphazard use of @ as a union operation. - fun union(a, b) = makeNoRepeats(a@b); val union = fn : a list * a list -> a list - val csCore = [Discrete, Programming]; val csCore = [Discrete,Programming] : Course list - val mathCore = [Calculus, Discrete, DiffEq]; val mathCore = [Calculus,Discrete,DiffEq] : Course list - union(csCore, mathCore); val it = [Programming,Calculus,Discrete,DiffEq] : Course list Self-exercise. Write a di erent version of union that uses elementOf to add selectively the elements of one set to the other; do not use makeNoRepeats or @. 8.2 Exercises 1. Write a function that computes the intersection of two sets. 2. Write a function that computes the cardinality of a set. 3. Write a function that computes the di erence of one set from another. 4. Write a function that reverses a list. Chapter 9 First-class Functions This chapter explores the usefulness of rst-class functions in examples with functions that receive functions as parameters and return functions as results and, consequently, functions that are parameterized by functions and create functions. It also introduces you to Currying, the partial evaluation of a function to eliminate a parameter. Fixed point iteration, a process for solving equations in the form x = G(x). The running example in this section is adapted from Abelson and Sussman, Structure and Interpretation of Computer Programs. 9.1 Problem In this section we will apply our ML programming knowledge to a non-trivial problem, that of calculating square roots. One approach is to adapt Newton s method for nding general roots, which you should recall from calculus. Suppose the curve of a function f (x) crosses the x-axis (has a root) at x . This situation is illustrated in Figure 9.1. Finding x exactly may be di cult or even impossible the root may not have a repeating or nite decimal expansion, and if the curve is not a polynomial or rational function, the root might not even be an algebraic number. Instead, Newton s method is used to approximate the root. The approximation is done by making an initial guess and then improving the guess until it is within a desired tolerance. Suppose xi is a guess in this process. To improve that guess, we draw a tangent to the curve at that point and calculate where the tangent strikes the x-axis. We can nd the point on the curve at xi (point A) by evaluating f (xi ). The slope of the tangent can be found by the derivative of f at that point, f (xi ). Using a point and the slope, we can formulate the tangent as a function, g(x) = f (x + i)(x xi ) + f (xi ). Solving g(x) = 0 lets us nd point B, the intersection of the tangent and the axis. 71 CHAPTER 9. FIRST-CLASS FUNCTIONS 72 f(x) g(x) A C D xi xi+1 B x Figure 9.1: Newton s method for nding the root of f (x). 0 = xi+1 = = f (xi )(xi+1 xi ) + f (xi ) xi f (xi ) f (xi ) f (xi ) xi f (xi ) f (xi ) (9.1) Drawing a vertical line through B leads us to C, the next point on the curve on which we want to draw a tangent. Observe how repeating this process brings us closer to x , the actual root (at point D). Equation 9.1 tells us how to generate the next approximation from the current one. When the absolute value of f (xi ) is small enough, we declare xi to be our answer. We can use this method to nd c by noting that the square root is the positive root of the function f (x) = x2 c. In this case f (x) = 2x, and we can apply Equation 9.1 to produce a function for improving a given guess x: I(x) = x x2 c 2x CHAPTER 9. FIRST-CLASS FUNCTIONS In ML: - fun improve(x) = = x - (x * x - c) / (2.0 * x); val improve = fn : real -> real 73 Obviously this will work only if c has been given a valid de nition already. Now that we have our guess-improver in place, our concern is the repetition necessary to achieve a result. Stated algorithmically, while our current approximation is not within the tolerance, improve the approximation. Since we want to progress beyond the iterative solutions we saw in Chapters 4 and 5, we omit ML code for this approach. Instead, we set up a recursive solution based on the data that is being tested and updated: the current guess. There are two cases whether our current guess is within the tolerance or not. Base case: if the current guess is within the tolerance, return it as the answer. Recursive case: if the current guess is not within the tolerance, improve it and reapply this test, returning the result as the answer. In ML, - fun sqrtBody(x) = = if inTolerance(x) = then x = else sqrtBody(improve(x)) We called this function sqrtBody instead of sqrt because it is a function of the previous guess, x, not a function of the radicand, c. Two things remain: a predicate to determine if a guess is within the tolerance (say, .001; then |x2 c |< .001), and an initial guess (say, 1). If we package all these together, we have - fun sqrt (c) = = let fun inTolerance(x) = = abs((x * x) - c) < 0.001; = fun improve(x) = = x - (x * x - c) / (2.0 * x); = fun sqrtBody(x) = = if inTolerance(x) = then x = else sqrtBody(improve(x)) = in = sqrtBody(1.0) = end; val sqrt = fn : real -> real - sqrt(2.0); val it = 1.41421568627 : real CHAPTER 9. FIRST-CLASS FUNCTIONS - sqrt(16.0); val it = 4.00000063669 - sqrt(17.0); val it = 4.12310671696 - sqrt(121.0); val it = 11.0000000016 - sqrt(121.75); val it = 11.0340382471 74 : : : : real real real real Self-exercise. Write a similar function to calculated cubed roots. 9.2 Decomposition Whenever you solve a problem in mathematics and computer science, then next question to ask is whether the solution can be generalized so that it applies to a wider range of problems and thus can be reused more readily. When an idea is generalized, it means that we reduce the number of assumptions we are making, instead acknowledging and working with a greater number of unknowns; in other words, we are replacing constants with variables. Our square root algorithm was a specialization of Newton s method. The natural next question is how to program Newton s method in general. What assumptions or restrictions did we make on Newton s method when we specialized it? Principally, we assumed the function for which we were nding a root was in the form x2 c where c is a variable to the system. Let us focus on how this a ects one segment of the solution, testing for tolerance. ... = fun inTolerance(x) = = abs((x * x) - c) < 0.001; ... The assumed function shows itself in the expression (x * x) - c; by taking the absolute value of that function for a supplied value of x and comparing with .001, we are essentially checking if the function is within an epsilon of zero. We know that functions can be passed as parameters to functions; here, as it frequently is, generalization is parameterization. - fun inTolerance(function, x) = = abs(function(x)) < 0.001; val inTolerance = fn : ( a -> real) * a -> bool The type implies that the function inTolerance takes two things: a function mapping from type a to real and a value of type a. Note that we have not given information that would allow ML to infer what type function would except, so it is represented by the type variable a. How function s return type is inferred to be real is more subtle. abs is a special kind of function that is CHAPTER 9. FIRST-CLASS FUNCTIONS 75 de ned so it can accept either reals or ints, but it must return the same type that it receives. Since we compare its result against 0.001, its result must be real; thus its parameter must also be real, and nally we conclude that function must return a real. inTolerance is now less easy to use because we must pass in the function whenever we want to use it, (unless function is in scope already and we can eliminate it as a parameter). However, we know that functions can also return functions; to make this more general, instead of writing a function to test the tolerance, we write a function that produces a tolerance tester, based on the given function. - fun toleranceTester(function) = = fn x => abs(function(x)) < 0.001; val toleranceTester = fn : ( a -> real) -> a -> bool Note the type. The -> operator is right associative, which means that it groups items on the right side unless parentheses force it to do otherwise. toleranceTester accepts something of type a real and returns something of type a real. Now we need call toleranceTester only once and call the function it returns whenever we want to test for tolerance. To further generalize, let us no longer assume = .001, but instead parameterize it. - fun toleranceTester(function, tolerance) = = fn x => abs(function(x)) < tolerance; val toleranceTester = fn : ( a -> int) * int -> a -> bool What happened to the type? Since 0.001 no longer appears, there is nothing to indicate that we are dealing with reals. Yet ML cannot simply introduce a new type variable (for, say, ( a b) * b a bool) because abs is not de ned for all types, just int and real. Instead, ML has to guess, and when it comes between real and int, it goes with int. We will force it to chose real. - fun toleranceTester(function, tolerance) = = fn x => abs(function(x):real) < tolerance; val toleranceTester = fn : ( a -> real) * real -> a -> bool Next, consider the function improve. We can generalize this by stepping back and considering how we formulated it in the rst place. It comes from applying Equation 9.1 to a speci c function f (x). Thus we can generalize it by making the function of the curve a parameter. Since we do not have a means of di erentiating f (x), we will need f (x) to be supplied as well. - fun nextGuess(guess, function, derivative) = = guess - (function(guess) / derivative(guess)); val nextGuess = fn : real * (real -> real) * (real -> real) -> real CHAPTER 9. FIRST-CLASS FUNCTIONS 76 However, just as with tolerance testing, we would prefer to think of our next-guesser as a function only of the previous guess, not of the curve function and derivative. We can modify nextGuess easily so that it produces a function like improve: - fun nextGuesser(function, derivative) = = fn guess => guess - (function(guess) / derivative(guess)); val nextGuesser = fn : (real -> real) * (real -> real) -> real -> real Notice that this process amounts to the partial application of a function. nextGuess has three parameters; nextGuesser allows us to supply values for some of the parameters, and the result is another function. This transformation of a function of n arguments to a function of m < n arguments that returns a function of n m arguments is called currying after Haskell Curry, a mathematician who studied this technique (though he did not invent it). sqrtBody also demonstrates a widely applicable technique. If we generalize our function I(x) based on Equation 9.1 we have G(x) = x f (x) f (x) If x is an actual root, then f (x) = 0, and so G(x) = x. In other words, a root of f (x) is a solution to the equation x = G(x) Problems in this form are called xed point problems because they seek a value which does not change when G(x) is applied to it (and so it is xed). If the xed point is a local minimum or maximum and one starts with a good initial guess, one approach to solving (or approximating) a xed point problem is xed point iteration, the repeated application of the function G(x), that is G(G(G(. . . G(x ) . . .))) where x is the initial guess, until the result is good enough. In parameterizing this process by function, initial guess, and tester, we have this generalized version of sqrtBody: - fun fixedPoint(function, = if tester(guess) then = else = val fixedPoint = fn : ( a guess, tester) = guess fixedPoint(function, function(guess), tester); -> a) * a * ( a -> bool) -> a Self-exercise. Convince yourself that a root of f (x) is a local minimum or maximum of G(x). Under what circumstances is it a minimum and under what circumstances a maximum? CHAPTER 9. FIRST-CLASS FUNCTIONS 77 9.3 Assembly We have analyzed our implementation of the square root function to uncover the elements in Newton s method (and more generally, a xed point iteration). Now we synthesize these to make useful, applied functions. In the analysis, parameters proliferated; as we synthesize the components into something more useful, we will reduce the parameters, or ll in the blanks. Simply hooking up fixedPoint, nextGuesser, and toleranceTester, we have - fun newtonsMethod(function, derivative, guess) = = fixedPoint(nextGuesser(function, derivative), guess, = toleranceTester(function, 0.001)); val newtonsMethod = fn : (real -> real) * (real -> real) * real -> real Given a function, its derivative, and a guess, we can approximate a root. However, one parameter in particular impedes our use of newtonsMethod. We are required to supply the derivative of function; in fact, many curves on which we wish to use Newton s method may not be readily di erentiable. In those cases, we would be better o nding a numerical approximation to the derivative. The easiest such approximation is the secant method, where we take a point on the curve near the point at which we want to evaluate the derivative and calculate the slope of the line between those points (which is a secant to the curve). Thus for small , f (x) f (x+ ) f (x) . In ML, taking = .001, - fun numDerivative(function) = = fn x => (function(x + 0.001) - function(x)) / 0.001; val numDerivative = fn : (real -> real) -> real -> real Now we make an improved version of our earlier function. Since any user of this new function is concerned only about the results and not about how the results are obtained, our name for it shall re ect what the function does rather than how it does it. - fun findRoot(function, guess) = = newtonsMethod(function, numDerivative(function), guess); val findRoot = fn : (real -> real) * real -> real Coming full circle, we can apply these pre-packaged functions to a special case: nding the square root. We can use newtonsMethod directly and provide an explicit derivative or we can use findRoot and rely on a numerical derivative, with di erent levels of precision. Since x2 c is monotonically increasing, 1 is a safe guess, which we provide. - fun sqrt(c) = = newtonsMethod(fn x => x * x - c, fn x => 2.0 * x, 1.0); val sqrt = fn : real -> real CHAPTER 9. FIRST-CLASS FUNCTIONS - sqrt(2.0); val it = 1.41421568627 : real - sqrt(16.0); val it = 4.00000063669 : real - sqrt(17.0); val it = 4.12310671696 : real - fun sqrt(c) = = findRoot(fn x => x * x - c, 1.0); val sqrt = fn : real -> real - sqrt(2.0); val it = 1.41421657988 : real - sqrt(16.0); val it = 4.00000092633 : real - sqrt(17.0); val it = 4.12310709017 : real 78 There are several lessons here. First, this has been a demonstration of the interaction between mathematics and computer science. The example we used comes from an area of study called numerical analysis which straddles the two elds. Numerical analysis is concerned with the numerical approximation of calculus and other topics of mathematical analysis. More importantly, this also demonstrates the interaction between discrete and continuous mathematics. We have throughout assumed that f (x) is a real-valued function like you are accustomed to seeing in calculus or analysis. However, the functions themselves are discrete objects. The most important lesson is how functions can be parameterized to become more general, curried to reduce parameterization, and, as discrete objects, passed and returned as values. Self-exercise. Write functions similar to the versions of sqrt in this section for calculating cubed roots. 9.4 Exercises 1. The implementation of fixedPoint presented in this chapter di ers from traditional xed point iteration because it requires the user to provide a function which determines when the iteration should stop, based on the current guess. That approach is tied to the application of nding roots, since our criterion for termination how close f (guess) is to zero. Instead, termination should depend on how close successive guesses are to each other, that is, if | xi xi 1 |< . Rewrite fixedPoint so that it uses this criterion and receives a tolerance instead of a tester function.
Find millions of documents here - Study Guides, Homework Solutions, Papers, Exam Answer Keys and more.
Course Hero has millions of course related materials that will enable you to learn better, faster and get an A in all your courses.
Below is a small sample set of documents:
loopconstructs.pdf
Path: Wheaton IL >> CS >> 241 Fall, 2009
Path: Wheaton IL >> CS >> 394 Spring, 2009
Path: Wheaton IL >> CS >> 241 Fall, 2009
Path: Wheaton IL >> CS >> 241 Fall, 2009
Path: Wheaton IL >> CS >> 241 Fall, 2009
Path: Wheaton IL >> M >> 243 Fall, 2009
Path: Wheaton IL >> CS >> 243 Fall, 2009
Path: N.E. Illinois >> MAT >> 3670 Fall, 2009
Path: N.E. Illinois >> MAT >> 3670 Fall, 2009
Path: N.E. Illinois >> MAT >> 3670 Fall, 2009
Path: N.E. Illinois >> MAT >> 3670 Fall, 2009
Path: N.E. Illinois >> MAT >> 3670 Fall, 2009
Path: N.E. Illinois >> MAT >> 3670 Fall, 2009
Path: N.E. Illinois >> MAT >> 3670 Fall, 2009
Path: N.E. Illinois >> MAT >> 3670 Fall, 2009
Path: N.E. Illinois >> MAT >> 3670 Fall, 2009
Path: N.E. Illinois >> MAT >> 3670 Fall, 2009
Path: N.E. Illinois >> MAT >> 3670 Fall, 2009
Path: N.E. Illinois >> MAT >> 3670 Fall, 2009
Path: N.E. Illinois >> MAT >> 3670 Fall, 2009
Path: N.E. Illinois >> MAT >> 3670 Fall, 2009
Path: N.E. Illinois >> MAT >> 3670 Fall, 2009
Path: N.E. Illinois >> MAT >> 3670 Fall, 2009
Path: N.E. Illinois >> MAT >> 3670 Fall, 2009
Path: N.E. Illinois >> UX >> 2435 Fall, 2009
Path: N.E. Illinois >> UX >> 1 Fall, 2009
Path: N.E. Illinois >> UX >> 1395 Fall, 2009
Path: N.E. Illinois >> UX >> 1395 Fall, 2009
Path: N.E. Illinois >> UX >> 3450 Fall, 2009
Path: N.E. Illinois >> UX >> 1490 Fall, 2009
Path: N.E. Illinois >> UX >> 1315 Fall, 2009
Path: N.E. Illinois >> UX >> 5070 Fall, 2009
Path: N.E. Illinois >> UX >> 1 Fall, 2009
Path: N.E. Illinois >> UX >> 1 Fall, 2009
Path: N.E. Illinois >> UX >> 1 Fall, 2009
Path: N.E. Illinois >> UX >> 2435 Fall, 2009
Path: N.E. Illinois >> UX >> 1300 Fall, 2009
Path: N.E. Illinois >> UX >> 1 Fall, 2009
Path: N.E. Illinois >> UX >> 1 Fall, 2009
Path: N.E. Illinois >> EIUPSC >> 1 Fall, 2009
Path: N.E. Illinois >> EIUPSC >> 2 Fall, 2009
Path: N.E. Illinois >> UX >> 1 Fall, 2009
Path: Wheaton IL >> CS >> 394 Spring, 2009
Path: Wheaton IL >> CS >> 394 Spring, 2009
Path: Wheaton IL >> CS >> 241 Fall, 2009
Path: Wheaton IL >> CS >> 241 Fall, 2009
Path: Wheaton IL >> CS >> 241 Fall, 2009
Path: Wheaton IL >> CS >> 241 Fall, 2009
Path: Wheaton IL >> M >> 243 Fall, 2009
Path: N.E. Illinois >> MAT >> 1160 Fall, 2009
Path: N.E. Illinois >> MAT >> 1160 Fall, 2009
Path: N.E. Illinois >> MAT >> 1160 Fall, 2009
Path: N.E. Illinois >> MAT >> 1160 Fall, 2009
Path: N.E. Illinois >> MAT >> 1160 Fall, 2009
Path: N.E. Illinois >> MAT >> 1160 Fall, 2009
Path: N.E. Illinois >> MAT >> 1160 Fall, 2009
Path: N.E. Illinois >> MAT >> 1160 Fall, 2009
Path: N.E. Illinois >> MAT >> 1160 Fall, 2009
Path: N.E. Illinois >> MAT >> 1160 Fall, 2009
Path: N.E. Illinois >> MAT >> 1160 Fall, 2009
Path: N.E. Illinois >> MAT >> 1160 Fall, 2009
Path: N.E. Illinois >> MAT >> 1160 Fall, 2009
Path: N.E. Illinois >> MAT >> 1160 Fall, 2009
Path: N.E. Illinois >> EIUPSC >> 2 Fall, 2009
Path: N.E. Illinois >> SOC >> 1 Fall, 2009
Path: N.E. Illinois >> SOC >> 1 Fall, 2009
Path: N.E. Illinois >> SOC >> 1 Fall, 2009
Path: N.E. Illinois >> SOC >> 1 Fall, 2009
Path: N.E. Illinois >> SOC >> 1 Fall, 2009
Path: N.E. Illinois >> SOC >> 1 Fall, 2009