Week 3: Scope and Evaluation Order Assignment 1 Has Been Posted!
Total Page:16
File Type:pdf, Size:1020Kb
Week 3: Scope and Evaluation Order CSC324 Principles of Programming Languages David Liu, Department of Computer Science Assignment 1 has been posted! Closures and static scope static (adjective) determined only by the program source code dynamic (adjective) determined only when the program is run E.g., referential transparency is a static property How are closures implemented? Are they static or dynamic? Or both? Function bodies can be processed statically (e.g., Haskell compiler generates code once per lambda). The closure environment (and therefore the closure itself) can only be generated dynamically. The closure depends only on where the function is evaluated, not where that function is called. (define (make-adder n) (lambda (x) (+ x n))) (define adder (make-adder 1)) (adder 100) So we can determine where each free identifier obtains its values statically, based on where its enclosing function is defined. scope (of an identifier) The parts of program code that may refer to that identifier. static (aka lexical) scope The scope of every identifier is determined by the structure of the source code (e.g., by nesting of lambdas and lets). Every identifier obtains its value from the closest enclosing expression that binds it. (define (make-adder n) (lambda (x) (+ x n))) (define adder (make-adder 1)) ; (0x..., {n: 1}) (let* ([n 100]) (adder 2)) Implementing static scope in an interpreter A simple interpreter (define/match (interpret env expr) [(_ (? number?)) expr] [(_ (? symbol?)) (hash-ref env expr)] [(_ (list '+ l r)) (+ (interpret env l) (interpret env r))]) A simple interpreter (define/match (interpret env expr) [(_ (? number?)) expr] [(_ (? symbol?)) (hash-ref env expr)] [(_ (list '+ l r)) (+ (interpret env l) (interpret env r))]) The environment is passed recursively when interpreting each subexpression. Creating the environment (define x 10) (let* ([x 10] (define y x) [y x] (define z (+ x y)) [z (+ x y)]) (* x y z)) (* x y z) Interpreting function calls (define (f x) (+ x 10)) ... (... (f 2) ...) (interpret env '(f 2)) ; ==> (interpret {x: 2} '(+ x 10)) The environment is not passed recursively! (define (make-adder n) (lambda (x) (+ x n))) (define adder (make-adder 1)) ; (0x..., {n: 1}) ... (... (adder 2) ...) (interpret env '(adder 2)) ; ==> (interpret {n: 1, x: 2} '(+ x n)) Static scope vs. dynamic scope static (aka lexical) scope The scope of every identifier is determined by the structure of the source code (e.g., by nesting of lambdas and lets). dynamic scope The scope of every identifier is determined by when bindings are evaluated during program execution. (interpret env '(adder 2)) ; ==> (interpret (hash-union env {n: 2}) '(+ x n)) Scope and the call stack def f(x): a = 1 return g(x) def g(y): return y + a f(100) Demo: dynamic scope in bash Demo: static scope and mutation Evaluation order When all subexpressions are semantically valid, evaluation order doesn’t matter. (define (f x) (+ x 2)) (f (* 10 20)) ; Could substitute: (f 200) => (+ 200 2) ; Or: (+ (* 10 20) 2) But when a subexpression raises an error/does not terminate, what happens? strict denotational semantics (for an expression) The expression is valid if and only if all of its subexpressions are valid. In almost every programming language, function calls have strict denotational semantics. Implemented using left-to-right eager evaluation of arguments. Functions can’t “skip” arguments. and, or, and if do not (always) evaluate every subexpression. They have non-strict denotational semantics. (and #f (/ 1 0)) (or #t (/ 1 0)) (if (equal? n 0) 0 (/ 1 n)) So in Racket (and pretty much every other language), and, or, and if are not functions! Function value expressions are non-strict. (lambda (x) (+ x 1)) thunk: a nullary function that wraps an expression to delay its evaluation. (lambda () (/ 1 0)) Demo: event-handler callbacks in JavaScript “Useless” parameters ; Racket (define return1 (lambda (x) 1)) (return1 (error "NOOO")) ; Raises an error -- Haskell return1 = \x -> 1 return1 (error "NOOO") -- Doesn't raise an error! In Haskell, function calls have non-strict denotational semantics. Implemented using lazy evaluation: arguments are only evaluated when necessary to evaluate the entire program. (&&) and (||) are functions in Haskell, but still short-circuit! We can implement lazy evaluation naively using thunks to wrap all function arguments, and call them when they’re referenced in the function body. Haskell uses memoization to prevent the same thunk from being called more than once. In Haskell, name bindings are non-strict: x = error "David is not cool" -- No error at this line! In fact, the above code binds x to a thunk! Demo: lazy evaluation and space leaks (skipped, but code posted in Tail.hs) Strictness analysis As an optimization, a compiler can (sometimes) perform static analysis to determine whether a given function parameter is “useless” or not. f x y = x + 1 f1 x y z = if x then y + 1 else z * 10 Intuitively, a parameter is strict if it is always required to evaluate the function body. x is strict for both f and f1, but no other parameters are. strictness analysis Given a set of function definitions, identify all of the strict parameters. David’s fun picture .