Section Notes for Recitation #5 CS 312, Cornell University, Fall 2004 Written up by Harlan Crystal Logical Operators ------------------------------- Although we have covered logical operators in previous sections, some students did not cover it so for a quick review of the topic: If you are familiar with C or Java, you know the && and || operators. These are the logical AND and OR functions. The keywords in SML for these features are "andalso" and "orelse." Like C and Java, these operators are short-circuit. For some intuition of what short-circuit means, consider the following: x andalso y Let's say we know x is false. Then it is not necessary to look at y to know that the whole expression is false, so y is not evaluated. Similarly if "x orelse y" finds that x is true, then there is no reason to evaluate y. This is useful in situations such as the following: true orelse ((0 div 3) = 3) This will not throw a divide-by-zero error because the second expression is not evaluated. false andalso (longRunningReallySlowFunction("input")) SML will not bother spending two weeks running the slow, long function because SML already knows the entire expression is false. Scope and Shadowing ------------------------------- Variables are only bound within a certain scope. Outside of that region that binding does not apply. For example: (* x not bound here *) let val x = 1 in x + 5 end (* x not bound here *) This same idea of scope applies in function definitions: (* neither f nor x bound here *) fun f(x:int):int = x + 1 (* both x and f bound here *) (* x not bound here, but f is *) Locality of scoping rules also apply to bindings made in case expression patterns: let val x = [1, 2] in (* y not defined here *) case x of [] => 1 (* y not defined here *) | y::_ => y (* y not defined here *) end Being a functional-style language, these bindings behave are differently than the variable assignments which you are probably familiar with in imperative languages. In C/Java, when a variable is assigned, the value of that variable may be changed later. In SML, after the variable is bounds its value may not be changed. The variable binding may be over-shadowed (as we will see next) but this new binding does not change the actual value pointed to by the variable, it simply hides that binding within that scope. To introduce shadowing, consider the following example: let val x = 5 in (let val x = 6 in x + 7 (* this x = 6 *) end) + x (* this x = 5 *) end Notice that while we were allowed to bind "x" again even though it was already bound, that shadowed "x" only binds within the inner scope, and that the older binding of "x" still applies once you have exited that scope. Therefore this entire expression evaluates to 18. Shadowing occur in any other place which creates bindings. Consider the following example of Shadowing in the case of function definitions. fun f(x:int):int = let fun g(x:int):int = x + 1 (* "x + 1" refers to inner x *) in g(x + 5) (* refers to outer x *) end Also, consider this case of shadowing in case-expression patterns: let val x = [1, 2, 3] in case x of [] => 1 | x::_ => x + 5 (* "x + 5 " refers to 1, not the list [1,2,3] *) end The one other case to consider is where binding occurs within a single "val" line. let val x = 5 in (let val x = x + 1 (* x on right of = refers to 5 *) in x + 3 end) end What the above example shows is that the variables on the left of the "=" are not bound until the expression on the right of the "=" are finished evaluating in the old set of bindings. Substitution ------------------------------- Subtitution and the substitution model was covered in detail, but exactly along the lines of the "substitution" section at the bottom of the following webpage: http://www.cs.cornell.edu/courses/cs312/2004fa/lectures/lecture5.htm Printing ------------------------------- In order to print strings to the screen, use the print function: print "hello!\n" print ((Char.toString #"h") ^ "\n") print ("The answer is: " ^ (Int.toString 5) ^ "\n") The last two of the above examples used the ^ operator, which is used to concatinate a pair of strings into a larger string. The print function does not return anything, this the type of the print function is string->unit. unit means "no type", similar to the void type in C or Java. Notice that since evaluating the print function does not return any sort of value, it does not fit well into our model of how SML works until this point. It can not be embedded or combined in a larger expression and exists completely for its side effect. Having just learned the substitution model, we already are introducing a case where it is too simple a model: Functions with side effects won't be modelled correctly by the substitution model because before there was no notion of time. Now the order of evaluation can effect the sequence of side effects that take place. Another example of side-effects which we have seen implicitly are exceptions. Sequencing ------------------------------- One SML language feature which goes hand-in-hand with the side-effect function "print" is the sequencing feature. The sequence feature allows two expressions to be evaluated but only use the value of the second one. In the expressions: (e1; e2) While e1 is evaluated first, its value is thrown away. The second expression is then evaluated and its value is used to stand for the value of the entire expression. Here are some concrete examples: (2 + 3; "hello") (* evaluates to "hello" of type string *) (print "testing"; 4.3) (* prints "testing" and returns 4.3 of type real *) For an example of embedding this sequencing into a function, consider the following factorial function which also prints "n" at each step: fun fact(n:int):int = (print ("N = " ^ (Int.toString n) ^ "\n"); if n = 1 orelse n = 0 then 1 else n * fact(n - 1))