Week 3: 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? bodies can be processed statically (e.g., Haskell 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 , 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 : 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 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