Commentary and Examples
This is mostly some examples of evaluation order discussed in the second half of the lecture, and an explanation of how this fits with the upcoming assignment.
Evaluation order and calling style is a big issue in language design! The wikipedia page https://en.wikipedia.org/wiki/Evaluation_strategy lists a substantial collection of styles. Some of these are of some interest. Call-by-value is our default, where an integer argument is just a numeric integer value. Call-by-reference is roughly what happens when the & reference-of operator is used in C to give a called function a reference to the mutable variable in its caller. Finally Haskell's lazy call-by-need style passes an integer argument which is really a computation that may be resumed to get the final numeric integer if it is needed.
For the first and probably only time, here is a demo of some SML code, a functional call-by-value language.
val x = [1, 2, 3, 4];
(* After each value I'll include a comment saying what the
PolyML terminal prints, e.g.:
val x = [1, 2, 3, 4]: int list
*)
fun f x y = x + y;
(*
val f = fn: int -> int -> int
*)
fun g x y = let
in
print ("1st arg: " ^ Int.toString x ^ "\n");
print ("2nd arg: " ^ Int.toString y ^ "\n");
x + y
end;
(*
val g = fn: int -> int -> int
*)
fun h x = let
val _ = print ("1st arg: " ^ Int.toString x ^ "\n")
fun h1 y = let
in
print ("2nd arg: " ^ Int.toString y ^ "\n");
x + y
end
in
h1
end;
(*
val h = fn: int -> int -> int
*)
(* All these functions have the same type but produce additional
I/O effects at different times. BTW, we don't have to use this
very explicit style, here's another way to write h:
*)
val h2 = (fn x => (print ("1st arg: " ^ Int.toString x ^ "\n");
(fn y => (print ("2nd arg: " ^ Int.toString y ^ "\n");
x + y))));
map (f 1) [1, 2, 3];
(*
val it = [2, 3, 4]: int list
*)
map (g 1) [1, 2, 3];
(*
1st arg: 1
2nd arg: 1
1st arg: 1
2nd arg: 2
1st arg: 1
2nd arg: 3
val it = [2, 3, 4]: int list
*)
map (h 1) [1, 2, 3];
(*
1st arg: 1
2nd arg: 1
2nd arg: 2
2nd arg: 3
val it = [2, 3, 4]: int list
*)
(*
The h function was given its 1st argument just once, but its 2nd
argument on three different occasions. This almost highlights a
major implementation challenge for the language: when (h 2 3) is
evaluated, the language must carefully manage the first phase of
computing (h 2) and then the second phase of computing (h 2) 3.
This ought to be skipped for a function like f, that needs both
arguments to do anything, but h and f have the same type, so the
language needs to do non-type analysis to tell them apart.
*)
By comparison, Haskell is lazy everywhere.
-- If you want to print diagnostics, you can import debug -- tracing. Because evaluation order is wacky, read the -- results with care. import Debug.Trace -- In Haskell, the type [Int] may be infinite. f :: () -> Int f () = -- Fully evaluating xs would never finish. let xs = [1 ..] in 12 -- The type list is equivalent to: data List2 a = Nil2 | Cons2 a (List2 a) deriving (Show, Eq) -- In our slides and inference rules etc, a type like -- List2 would come with an induction scheme. countForeverFromList2 :: Integer -> List2 Integer countForeverFromList2 i = traceShow ("i is", i) (Cons2 i (countForeverFromList2 (i + 1))) countForeverList2 :: List2 Integer countForeverList2 = countForeverFromList2 1 -- This is a contradiction! We should be able to prove -- that there is a greatest element in countForeverList2 -- by induction. That means that these aren't really -- inductive datatypes in the usual sense. -- In Haskell, laziness is permitted at every *constructor*. -- e.g. we can construct Cons2 1 countForeverList2, -- and a Cons2 actually gets created, even though its second -- argument can't be fully evaluated. Each argument to Cons2 -- may be *thunk*. A thunk is a lot like a closure, it carries -- an environment around so a computation can be run in the -- right context. -- You could convert a lazy language into a strict language, -- by replacing every lazy value of type a with a strict value -- of type () -> a. For instance, a lazy list/sequence in -- a strict language might have a Cons constructor of type -- LCons :: a -> (() -> LList a) -> LList a -- So what happens? Like in function closures (() -> a), we -- need to capture the environment. Because the language is -- pure, evaluation order doesn't matter, so we can put off -- evaluating the actual list, saving any environment args -- as necessary as we would in exporting a function. -- Here is another demo of laziness. This function doesn't -- look useful, but it can be used to produce a result. The -- key thing is that f has to be the kind of function that -- lets us learn something without fully inspecting its -- argument value x. fixpoint :: (a -> a) -> a -> a fixpoint f x = f (fixpoint f x) v1 = fixpoint (\xs -> [1, 2] ++ xs) [] -- Nothing like the above could be useful in a strict semantics.
Why are we chatting about evaluation order right now?
Assignment 1 will be released later this week.
Assignment 1 will involve a call-by-push-value language. This language has both strict-style call-by-value semantics and call-by-name (close-ish to Haskell) in the one language.
In the "hindsight" language in Assignment 1, there are two "regions" of the syntax, the one that inhabits the strict part of the language and the one that lives in the lazy part of the language. This will be clearer soon!