Subroutines (Part 2) Programming Language Principles Lecture 25 Prepared by Manuel E. Bermúdez, Ph.D. Associate Professor University of Florida Parameter Passing • Formal parameters: Parameters that appear in the declaration of a subroutine • Actual parameters: expressions actually used in procedure call. • Some languages allow only ONE parameter passing mode: – C, Fortran, ML, Lisp. • Other languages allow multiple parameter passing modes: – Pascal, Modula, Ada, Java Main Parameter Passing Modes 1) Call by value: Pass the value of the argument. – C, C++, Pascal (also allows pass by reference), Java (primitive types). – Can be expensive: large structures are copied. 2) Call by reference: Pass the address of the argument. – Smalltalk, Lisp, ML, Clu, C++, Java (non primitive types, including arrays). – C, C++ have const mode: cannot change parameter. 3) Call by name: Pass the text of the argument. – C pre-processor (#define) – More later. Choosing Parameter Passing Mode • Many languages provide a choice: – Pass by value: • Intent is to not modify the argument. – Pass by reference: • Intent is to modify the argument. • Often used to avoid expense of copying large structure, even if there’s no intent to modify it. Leaves door open for mistakes. • In some languages (C, C++) modifying the argument can be explicitly forbidden (const) Parameter Passing in Ada ADA provides 3 modes: in, out, in-out. • In: From caller to callee (call by value). • Out: From callee to caller (call by result). • In-out: Uses both. (call by value/result). – Ada specifies that all three are to be implemented by copying the values. However, Ada specifically permits passing either values or addresses. Parameter Passing Modes: Example Pass by value: Output is (5,6,5) var y:integer; procedure A(x:integer); begin write(x); x := 1; write(y+x); end; begin y := 5; A(y); write(y); end; Parameter Passing Modes: Example Pass by reference: Output is (5,2,1) var y:integer; procedure A(x:integer); begin write(x); x := 1; write(y+x); end; begin y := 5; A(y); write(y); end; Parameter Passing Modes: Example Pass by result: Output is (??,6,1) var y:integer; procedure A(x:integer); begin write(x); x := 1; write(y+x); end; begin y := 5; A(y); write(y); end; Parameter Passing Modes: Example Pass by value/result: Output is (5,6,1) var y:integer; procedure A(x:integer); begin write(x); x := 1; write(y+x); end; begin y := 5; A(y); write(y); end; Parameter Passing in Ada (cont’d) • Example: • This Ada program is considered erroneous: it can tell the difference between pass by reference and value/result. Parameter Passing in Pascal • Use keyword var to effect pass by reference. • With var , output is (5,2,1). • Without var, output is (5,6,5) var y:integer; procedure A(var x:integer); begin write(x); x := 1; write(y+x); end; begin y := 5; A(y); write(y); end; Parameter Passing in C • All parameters passed by value. • However, can use pointers (which are passed by value): void swap (int *a, int *b) { int t; t = *a; *a = *b; *b = t; } ... swap(&p,&q); Parameter Passing in C++ • C++ has reference parameters. • Use the & prefix. void swap (int &a, int &b) { int t; t = a; a = b; b = t; } swap (p,q); • Parameters can be declared const. References in C++ • Any variable can be a reference, allowing aliasing: int int i = j = i; &j = i; 2; 3; • Aliasing is useful for function returns: cout << a << b << c; is short for ((cout.operator<<(a)).operator<<(b)).operator<<(c); References in C++ (cont’d) • Without references, << and >> would have to return a pointer to their stream: ((cout.operator<<(a))->operator<<(b))->operator<<(c); Or worse, *(*(cout.operator<<(a)).operator<<(b)).operator<<(c); • This would spoil the cascading syntax of operator form: *(*cout <<a) << b) << c; Closures as Parameters • Closure: Reference to a subroutine, including the referencing environment. • This allows higher order functions. • In Pascal: procedure apply_to_A (function f(n:integer): integer; var A:array [lo..hi:integer] of integer); var i : integer; begin for i := lo to hi do A[i] := f(A[i]); end; Closures as Parameters (cont’d) • Recall: referencing environments are required by closures, only if subroutines can be nested. • C, C++, Java get by with subroutine pointers because there are no nested subroutines. • In C, void apply_to_A (int(*f)(int), int A[],int A_size) { int i; for (i=0; I < A_size; i++) A[i]=f(A[i]); } Closures as Parameters (cont’d) • Scheme: (define apply-to-L (lambda (f l) (if (null? l) '() (cons (f (car l)) (apply-to-L f (cdr l)))))) • ML: fun apply_to_L (f, l) = case l of nil => nil | h :: t => f (h) :: apply_to_L(f, t); Closures as Parameters (cont’d) • RPAL (just for fun): let apply_to_L f l = helper f l (Order l) where rec helper f l n = n eq 0 -> nil | (helper f l (n-1) aug f(l n)) Call by name • A call-by-name parameter is evaluated in the caller’s referencing environment when (and only when) it is needed. • Call by name equivalent to the "subst" function used in lambda calculus, in normal order. • Call by name allows strange expressions such as infinite lists in Haskell: take 10 [0..] (take first 10 elements from infinite list) Summation routine in Algol 60 Now, y := sum(3*x*x-5*x+2, x, 1, 10); will sum the values of 3x2 -5x+2, for 1 ≤ x ≤ 10. Call-by-name in Algol 60 • Such uses of call-by-name (summation) are rare. • To implement call by name, Algol 60 implementations pass a hidden subroutine that evaluates the actual parameter in the caller's referencing environment. The hidden routine is called a thunk. • Calling thunks proved to be prohibitively expensive. • Call-by-name dropped in Algol 68. Call by Name (cont’d) Pass by name literally substitutes n for a, and m for b. Result: t := n; n := m; m := t; swap is achieved. procedure swap(a,b:integer); var t:integer: begin t := a: a := b: b := t; end; ... var n,m:integer; n := 3; m := 4; swap(n,m); Call by Name (cont’d) However, attempting to swap i and A[i] results in t := i; i := A[i]; A[i] := t; 17 assigned to i, but 3 assigned to A[17] (out of range) swap not achieved. procedure swap(a,b:integer); var t:integer: begin t := a: a := b: b := t; end; ... var i:integer; var A: array[1..10] of integer; i := 3; A[3] := 17; swap(i,A[i]); Default Parameters • Parameter initialized with a default value. • If argument is not given, the default value is used. (C++, Ada, Common Lisp, Fortran 90) • In C++, default parameters must be the last ones: void print (int value, int base=10) {...}; print(31); print(31,10); print(31,16); print(31,2); //output: 31 31 1f 11111 Default Parameters in Ada type field is integer range 0..integer'last; type number_base is integer range 2..16; default_width : field := integer'width default_base : number_base := 10; procedure put (item : in integer; width: in field := default_width; base : in number_base := default_base)is ... put(37) prints 37 in decimal in 11 columns. put(37,4) prints 37 in decimal in 4 columns. put(37,4,8) prints 45 (37 in octal) in 4 columns. Default Parameters in Ada (cont’d) • We assumed that parameters are positional. • Some languages allow parameters to be named. • In Ada, (previous example): put (item => 38, base => 8); same as put (base => 8, item => 38) Variable number of arguments • In C, C++: #include <stdarg.h> /* macros and types defns */ int printf (char *format, ...) { va_list args; va_start (args, format); part of code char cp = va_arg(args,char); double dp = va-arg(args,double); } Here we assume two arguments, of type char and double, are expected. If not, chaos will ensue ... Function Return Values • Many languages restrict return types from functions. • Algol 60, Fortran: must be a scalar value. • Pascal, Modula-2: must be scalar or pointer. • Modula-3, Ada 95: allow a function to return a subroutine (as a closure). • C: function can return a pointer to a function. • Lisp, ML, Algol 68, RPAL: returning closures is common. Generics • Often we need a single operation on a variety of different object types. – Queue a number. – Push something on a stack. – Existing subroutines for stacks of integers, but now we need stacks of *trees* :-) • Polymorphic subroutines (Lisp, RPAL) provide a solution, but . . . – No compile time checking. – Compiler slower and more complicated. – Forces structural view of type equivalence. Generics (cont’d) Other solution: • Generic Modules -- Collection of similar subroutines or modules that operate on different types. • Useful to create containers: – data abstraction that holds a collection of objects, but whose operations are generally oblivious to the type of these objects. (Queue, heap, sorting) • Generics (a.k.a. templates) appear in Modula-3, Clu, Ada, C++, Java. • Generic modules are a purely static mechanism. They provide a mechanism to create needed source at compile time. Generic queue package in Ada: Queue contains up to max_items objects of type item. Operations are enqueue and dequeue. Implemented as circular list. Generics must be instantiated. Generic queues in C++: Generic classes must be instantiated. Generic functions need not be instantiated. Generic Sorting in C++ Generic functions in C++ need not be instantiated. Example: template<class T> void sort(T A[], int A_size) { ... } . . . int ints[10]; double reals[50]; char *string[30]; . . . sort(ints, 10); sort(reals, 50); sort(string, 30); Generics in Ada (cont’d) Ada doesn’t allow subroutines as parameters. Can use generics instead: Generics in Ada (cont’d) • Now, we can instantiate: subtype index is integer range 1 .. 100; scores: array(index) of integer; function foo (n: in integer) return integer is begin ... end; procedure apply_to_ints is new apply_to_array (integer, int_array, foo); apply_to_ints(scores); Generics (cont’d) Important: Generics are strictly a compile-time issue. Designers of Ada: “restricted form of contextsensititve macro facility”. B. Stroustrup (designer of C++): “clever kind of macro that obeys scope, naming, and type rules of C++”. Generics (cont’d) Problem with generics: • A certain type may permit an operation, but the operation may not behave as expected. • Example: sorting algorithm. The < behaves well for ints and doubles. For characters and strings it will compare ASCII values or pointers ! Exceptions • An exception is an unexpected or unusual condition that arises during program execution. May be system-detected or raised explicitly. • Often I/O related, failure due to some I/O related issue (end of file, wrong input, etc.) • Often necessary to repair stack (back up execution), and proceed. Exceptions (cont’d) Before exceptions, to cope with errors: 1. Invent a value, used by caller, to detect failure. 2. Return an explicit status value, examined after every call. 3. Pass a closure (when available) for an errorhandling routine. None of these are satisfactory (clutter, or hard to program). Exceptions (cont’d) • Clu, Ada, Modula-3, C++, Java, ML all provide exception handling: • Programmer provides handlers that are lexically bound to blocks of code. • In general: – If exception is not handled locally, control propagates back up the dynamic chain until either the program stops or a handler is found. Exceptions (cont’d) • Exception handlers are (typically) attached to a list of statements. • Example (Java): try{ int x = 5/n; } //end try block catch(ArithmeticException e){ System.out.println ("in catch, terminating method"); return; } //end catch block Exceptions (cont’d) Exception handling used for 3 purposes: 1. Handler perform some magic recovery (e.g. out of memory, free up garbage memory. 2. If magic fails, at least inform the user nicely (blue screen of death) 3. Handler can clean up the mess (rollback unfinished file writing, deallocate memory, restore previous state, etc.) Exceptions (cont’d) • In Ada, exception is a built in-type: declare empty_queue: exception; • In Java/C++ an exception is (what else?) an object: class empty_queue { }; • In Module-3, an exception is another PL "object": EXCEPTION empty_queue; Exceptions (cont’d) • Explicitly raising exceptions: throw statement (C++, Java, Lisp) . raise statement (Ada, Modula-3, ML). Exceptions (cont’d) • Clu, Modula-3, C++, Java: In each subroutine header, include a list of exceptions that may propagate out of the routine. • Modula-3: List is mandatory: run-time error if exception not listed, and exception not caught locally. • C++: List is optional: if present, same as Modula-3; if not, all exceptions can propagate. • Java: "checked" exceptions, must be declared; "unchecked" exceptions do not. Exception Syntax in C++ try { ... // protected code } catch (end_of_file){ // library exception for eof ... // handle eof error } catch (io_error e) { // any other I/O error ... // handle io_error } catch ( ... ){ // handle any other exception // ... is an actual C++ token. } Exceptions in ML and Lisp • In ML and common Lisp the exception handler is attached to an expression: val foo=(f(a)*b) handle Overflow => max_int; • Overflow is a predefined exception. When exception is raised, max_int will replace the value of the expression. Phrase-level Error Recovery in Recursive Descent Parsing At beginning of procedure A, check Next_Token: If not acceptable, delete tokens until it is. Exception Implementation • To find a handler the system must "unwind" the run-time stack, reclaiming stack frames. • Obvious implementation is a linked-list stack of handlers: – When control enters a protected block, the handler for that block is added to the list. – Upon exception, the run-time system pops the innermost handler off the list: • If the exception matches, call the handler. • If not, pop the list again. Exception Implementation The linked-list stack of handlers incurs run-time overhead. Other solution: • Keep a table that maps exceptions to blocks of code. • When exception occurs, do binary search for block of code, using PC as key. • If exception is reraised, search again, using return address as key. Coroutines • A coroutine is a closure into which we can transfer. • Coroutines are execution contexts: – Exist concurrently. – Execute one at a time. – Transfer control to each other explicitly. • Transfer from one coroutine to another: – Old program counter is saved. – Update coroutine we are leaving. – Different from a goto: after a goto, the PC is forgotten. • A transfer to a coroutine will take up where the previous one left off. Example of Coroutines • Interleave execution of a screen-saving routine, with a file-system-check routine. Stack Allocation for Coroutines • Coroutines are concurrent: can’t share a stack. • Solution: – Each coroutine has fixed amount of stack space (Modula-2): • Out of space: run-time error. • Excess space is wasted. – Stack frames allocated on the heap (Lisp, Scheme): • Increases overhead of subroutine call. • Can allocate “chunks” of memory, big enough for the frame. Cactus Stacks • If coroutines can be created at any nesting level, (Simula), then we need a cactus stack. Side-branch: coroutine creation (A, B, C, D). Static links: arrows. Dynamic links: vertical stacking. Subroutines (Part 2) Programming Language Principles Lecture 25 Prepared by Manuel E. Bermúdez, Ph.D. Associate Professor University of Florida