180 likes | 202 Views
CS 3304 Comparative Languages. Lecture 19: Functional Languages - Perspective 27 March 2012. Evaluation Order Revisited. Applicative order - evaluate function arguments before passing them to a function: Scheme: f unctions use applicative order defined with lambda.
E N D
CS 3304Comparative Languages • Lecture 19:Functional Languages - Perspective • 27 March 2012
Evaluation Order Revisited • Applicative order - evaluate function arguments before passing them to a function: • Scheme: functions use applicative order defined with lambda. • What is usually done in imperative languages. • Usually faster. • Scheme use applicative order in most cases. • Normal order - pass function arguments unevaluated: • Scheme: special forms (hygienic macros) use normal order defined with syntax-rules. • Arises in the macros and call-by-name parameters of imperative languages. • Like call-by-name: don't evaluate argument until you need it. • Sometimes faster. • Terminates if anything will (Church-Rosser theorem).
Example • Function:(define double (lambda (x) (+ x x))) • Applicative order:(double (* 3 4))⇒(double 12)⇒(+ 12 12)⇒24 • Normal order:(double (* 3 4))⇒(+ (* 3 4) (* 3 4))⇒(+ 12 (* 3 4))⇒(+ 12 12)⇒24
Special Forms • Arguments to special forms (such as lambda) are passed unevaluated. • Each special form is free to choose internally when (and if) to evaluate its parameters. • Expression types in Scheme - special forms and functions: • Primitive: built into the language implementation. • Derived: defined in terms of primitive expression types. • lambda: used to create derived functions that can be bound to names with let. • syntax-rules: used to create derived special forms that can be bound to names with define-syntax and let-syntax. • Derived special forms are known as macros in Scheme – hygienic: lexically scoped, integrated into the language’s semantics, and immune from the problems of mistaken grouping an variable capture. • Scheme macros are Turing complete.
Strictness • A (side-effect-free) function is said to be strict if it is undefined (fails to terminate, or encounters an error) when any of its arguments is undefined. • It can safely evaluate all its argument, so its result will not depend on evaluation order. • A strict language requires all arguments to be well-defined, so applicative order can be used: ML and Scheme (with the exception of macros). • A non-strict language does not require all arguments to be well-defined; it requires normal-order evaluation: Miranda and Haskell.
Lazy Evaluation • Lazy evaluation gives the best of both worlds: the advantage of normal-order evaluation while running within a constant factor of the speed of applicative-order evaluation. • Particularly useful for “infinite” data structures. • Scheme: available through explicit use of delay and force: • delay creates a “promise”. • But not good in the presence of side effects. • If an argument contains a reference to a variable that may be modified by an assignment, then the value of the argument will depend on whether it is evaluated before or after the assignment. • If the argument contains an assignment, values elsewhere in the program may depend on when evaluation occurs. • Scheme requires that every use of delay-ed expression be enclosed in force.
Lazy Evaluation Example (define f (lambda () (let ((done #f) (memo ’()) (code (lambda () (* 3 4)))) (if done memo (begin (set! memo (code)) memo))))) … (double (f)) ⇒(+ (f) (f)) ⇒(+ 12 (f)) ⇒(+ 12 12) ⇒24
I/O Streams • Traditional I/O is a major source of side effects: • Scheme: functions read and display. • Model input and output as streams: unbounded-length lists whose elements are generated lazily. • If we model input and output streams, then:(define output (my_prog input)) • When input value is needed, my_prog forces evaluation of the car of input and passes the cdr on to the rest of the program. • Successfully encapsulate the imperative nature of interaction at terminal. • Don’t work well for graphics or random access to files.
Monads • A more general concept (from category theory): • Ability to carry a hidden, structured value of arbitrary complexity from one action to the next. • The pseudorandom number generator example (p. 526): • Passing the state to the function and having it return new state along with the random number. • Monads provide a more general solution to the problem of threading mutable state through a functional program. • In Haskell IO monad serves as the central repository for imperative language features (syntactic sugar): • Additional nomads support partial functions and various container classes. • Coupled with lazy evaluation provides a natural foundation for backtracking search, nodeterminism, and the functional equivalent of iterators.
Higher-Order Functions • Higher-order functions • Take a function as argument, or return a function as a result. • Great for building things. • Map: takes as argument a function and a sequence of lists:(map * '(2 4 6) '(3 5 7)) • One of the most common uses of higher order functions is to build new functions from existing ones:(define fold (lambda (f i l) (if (null? l) i (f (car l) (fold f i (cdr l))))))(define total (lambda (l) (fold + 0 l)))(define total-all (lambda (l) (map total l)))
Implementing Higher-Order Functions • Whey we don’t use higher-order function in imperative programming languages? • Much of the power of the first-class functions depends on the ability to create new functions on the fly - we need a function constructor: a significant departure from the syntax and semantics of traditional imperative languages. • The ability to specify functions as return values, or to store them as variables requires: • Eliminate function nesting: erodes the ability of programs to create functions with desired behaviors on the fly; or • Give local variables unlimited extent: increases the cost of storage management.
Currying • Currying (after Haskell Curry): replace multiargument function with a function that takes a single argument and returns a function that epect the remaining elements:(define curried-plus (lambda (a) (lambda (b) (+ a b))))(plus 3 4)⇒ 7(map (curried-plus 3) ‘(1 2 3))⇒ (4 5 6) • ML, Miranda, and Haskell have especially nice syntax for curried functions. In ML:fun plus (a, b) : int = a + b;==> val plus = fn : int * int -> intfun curried_plus a = fn b : int => a + b;==> val curried_plus = fn : int -> int -> intcurried_plus 3;==> val it = fn : int -> intfun curried_plus a b : int = a + b;==> val curried_plus = fn : int -> int -> int
Theoretical Foundations: Functions • A function is a single-valued mapping: it associates every element in one set (the domain) with (at most) one element in another set (the range):sqrt: R ⟶ R • If a function provides a mapping for every element of the domain, the function is said to be total. Otherwise, it is said to be partial. • It is often useful to characterize functions as sets or, more precisely, as subsets of the Cartesian product of the domain and the range. • One of the limitations of the function-as-set notation is that it is nonconstructive: it doesn’t tell us how to compute the value of a function at a given point (i.e., on a given input).
Theoretical Foundations: Lambda Calculus • Church designed the lambda calculus to address the nonconstructive limitation of function-as-set. In its pure form, lambda calculus represents everything as a function: • Church and Rosser theorem: the simplest forms are unique and that if any evaluation order will terminate, normal order will. • A lambda expression can be defined recursively as: • A name; • A lambda abstraction consisting of the letter λ, a name, a dot, and a lambda expression; • A function application consisting of two adjacent lambda expressions; • A parenthesized lambda expression. To accommodate arithmetic, we will extend this definition to allow numeric literals. • When two expressions appear adjacent to one another, the first is interpreted as a function to be applied to the second.
Naturally Imperative Idioms • There are common programming idioms in which the canonical side effect (assignment) plays a central role, such as I/O and trivial update problem idioms: • Initialization of complex structures: lists are easy to build from old lists but not other data structures, such as multidimensional arrays. • Summarization: the natural way to count occurrences of various item in large data sets is using a dictionary data structure that is repeatedly updated. • In-place mutation: when dealing with large data sets, values should be updated in place rather than copying to a new list or similar. • Solution: a combination of convenient notation (accessing arbitrary elements of a complex structure) and an implementation that can determine when the old version of the structure will never be used gain and can be updated in place.
Perspective: Advantages • Lack of side effects makes programs easier to understand. • Lack of explicit evaluation order (in some languages) offers possibility of parallel evaluation (e.g. MultiLisp). • Lack of side effects and explicit evaluation order simplifies some things for a compiler (provided you don't blow it in other ways). • Programs are often surprisingly short. • Language can be extremely small and yet powerful.
Perspective: Problems • Difficult (but not impossible!) to implement efficiently on von Neumann machines: • Lots of copying of data through parameters. • (Apparent) need to create a whole new array in order to change one element. • Heavy use of pointers (space/time and locality problem). • Frequent procedure calls. • Heavy space use for recursion. • Requires garbage collection. • Requires a different mode of thinking by the programmer. • Difficult to integrate I/O into purely functional model.
Summary • A functional program computes principally through substitution of parameters into functions. • The underlying formal model for functional languages is the lambda calculus. • Many functional languages extend the lambda calculus with additional features, including assignment, I/O, and iteration. • Lists feature prominently in most functional languages.