Lecture 23: Control Flow ======================== Exceptions ---------- Exceptions allow programmers to signal an error, interrupt normal execution, and handle the error non-locally. They provide several advantages: * Separate error-handling code from algorithmic code * Propagate errors up the call stack automatically until an appropriate handler is reached * Handle multiple sources of exceptions together * Separately handle different types of exceptions Constructs e ::= ... | throw ExnKind(e) | try e handle ExnKind(x) => e Semantics: "try e1 handle ExnKind(x) => e2" evaluates e1 and, if no exception is thrown, evalutes to whatever e1 evalutes to. "throw ExnKind(e)" evaluates e to a value, searches for a surrounding handler for ExnKind, binds the value from e to the variable x, and evalutes the handler body e2. The try construct then evalutes to the result of e2 rather than the result of e1. Typing: Each exception kind ExnKind is associated with an argument type. We define the rules, assuming this type is tau_in: Gamma |- e1 : tau Gamma, x:tau_in |- e2 : tau --------------------------------------------- Gamma |- try e1 handle ExnKind(x) => e2 : tau Gamma |- e : tau_in ------------------------------- Gamma |- throw ExnKind(e) : tau Note that the "throw" construct can be given any type at all, because it does not ever result in a value! Resumable Exceptions using Algebraic Effects -------------------------------------------- Sometimes it's nice to recover from an exception! If the exception results from the inability to compute a value, but a different value can be provided, then we can recover from the exception. We'll do this using a construct called Algebraic Effects. We motivate Algebraic Effects with resumable exceptions, but Algebraic Effects are a more general construct, so we'll generalize our terms slightly. We will use the keyword "perform" instead of "throw." The keyword "resume" allows us to recover from an exception and substitute a new value. This example is from https://overreacted.io/algebraic-effects-for-the-rest-of-us/: function getName(user) { let name = user.name; if (name === null) { name = perform 'ask_name'; } return name; } function makeFriends(user1, user2) { user1.friendNames.push(getName(user2)); user2.friendNames.push(getName(user1)); } const arya = { name: null, friendNames: [] }; const gendry = { name: 'Gendry', friendNames: [] }; try { makeFriends(arya, gendry); } handle (effect) { if (effect === 'ask_name') { resume with 'Arya Stark'; } } We can write typing rules for these effects. This time, we need two types: tau_in for the "input" type of the exception, and tau_out for the "output" type. Gamma |- e1 : tau Gamma, x:tau_in, resume_with: (tau_out -> tau) |- e2 : tau ---------------------------------------------------------- Gamma |- try e1 handle ExnKind(x) => e2 : tau Gamma |- e : tau_in ----------------------------------- Gamma |- throw ExnKind(e) : tau_out [Technical note: the try rule has resume_with actually returning! This happens if it is implemented with delimited continuations (see below). If we used regular continuations, the return type of resume_with would be void, not tau.] Non-local Returns ----------------- Many languages that support closures also provide nonlocal returns (Smalltalk is an example). A "return e" statement inside a closure returns not from the closure, but from the lexically surrounding function. If the closure is passed to some other function and then invoked, this is a "non-local return" because it skips out of as many stack frames as needed to get to the lexically enclosing function, then returns from that: function find(element:int, lst: int list):boolean { lst.for_each( (x) => { if (x == element) return true; // nonlocal - jumps out of for_each() }); return false; } If they are not built into the language, non-local returns can also be implemented using algebraic effects: function find(element:int, lst: int list):boolean { try { lst.for_each( (x) => { if (x == element) perform Return(true); }); perform Return(false); } handle Return(result) { return result } } Generators ---------- Languages like Python support generators: special functions that can be invoked multiple times, yielding a sequence of values: function generate_from_list(lst: int list):generator { for (current = lst; current != null; current = current.next) { yield current.value; } } for (i in generate_from_list(lst)) { ... do something with i ... } Python generators are somewhat limited: yield statements can only be in the generator statement itself, not in functions that are called (other than nested generators invoked with a special "yield from" construct). But, once again, with algebraic effects, we can support "nonlocal yields:" function gen(t:int tree):unit { t match { Branch(left, value, right) => gen(left); perform Yield(value); gen(right); Leaf() => () } } function tree_to_list(t:int tree):int list { try { gen(t); nil } handle Yield(i:int) { i::resume with (); } } EXERCISE Simulate the tree_to_list function above on the following tree: tree = 10 / \ 4 7 / \ 15 5 How Algebraic Effects Work, Conceptually ---------------------------------------- When we come to a "perform," we capture the stack between the "try" and the "perform" in an abstraction called a "delimited continuation." We then unwind the stack and run the handler. The delimited contiuation is bound to "resume" in the handler; if resume is invoked, then the captured continuation is pasted onto the stack right where the "resume" is (so: in tree_to_list, whatever resume returns will be appended to i in the returned list) and execution resumes with whatever value is passed to resume replacing the original "perform" expression. Continuations and Continuation Passing Style -------------------------------------------- Algebraic effects can be implemented using continuations, a lower-level primitive. Continuations are a way of capturing a set of stack frames. Full continuations capture all the stack frames in a program, while delimited continuations capture the stack frames above a certain point (the "try" in our formulation, which derives from https://overreacted.io/algebraic-effects-for-the-rest-of-us/). Delimited continuations are the primitive most closely associated with algebraic effects, but they can be implemented in terms of continuations, which are the more basic concept, so we'll focus on those here. The basic idea of continuations is this: a continuation is a procedure that represents the remaining steps in a computation (definition by Matt Might). The "remaining steps" might be based on the value you are computing, so continuations take an argument which is the value returned to the continuation from the "current execution point." The continuation will execute and at some point it will complete by passing the result of the program to the operating system. Continuations can be represented as functions that do not return, but which take an argument and ultimately invoke a call to the operating system saying "I'm done now, here's the result." Continuations can be compiled into regular functions in an idiom called continuation passing style, where every function takes a "continuation" argument representing what to do when it is done, and invokes the continuation instead of returning. The analogy in assembly language is that a return instruction is really an indirect jump--and calling a first-class function is also an indirect jump, so continuation passing style just unifies these ideas. Some examples due to Matt Might make this clear: << see Might's examples at https://matt.might.net/articles/by-example-continuation-passing-style/ >> Continuations as well as high-level constructs such as algebraic effects are typically implemented by transforming the program into continuation passing style. The nice thing is that the continuation argument captures the stack, which can then be stored and manipulated to implement the many control constructs described above (and below). Coroutines ---------- Coroutines are two cooperating routines that execute for a while, then yield to the other one. They execute concurrently in the sense of interleaving, but do not execute in parallel as threads would, and they interleave at well-define yield points. Example: check file system & updating screen (from textbook). While we could explicitly do a little checking and then an update in a single top-level loop, this would break up the checking code, which is likely a nested recursive procedure. Coroutines make this more elegant: <> Coroutines are implemented by capturing the stack for each of the two coroutines and swapping them. The program uses a cactus stack instead of a regular stack. Since implementation of coroutines involves capturing the stack, they can be implemented elegantly on top of algebraic effects or more low-level continuations. Async/Await ----------- A final interesting control structure is async/await. Async denotes a function that is invoked asynchronously and will run until it performs a blocking operation, at which point it will suspend and the caller can proceed. At some point the caller can invoke await() in order to wait for the callee to produce a value. If the value is not ready yet, the caller in turn will suspend if it is also asynchronous. Example (C#, from Wikipedia): public async Task FindPageSizeAsync(Uri uri) { var client = new HttpClient(); byte[] data = await client.GetByteArrayAsync(uri); return data.Length; } This is implemented in terms of lambdas: public Task FindPageSizeAsync(Uri uri) { var client = new HttpClient(); Task dataTask = client.GetByteArrayAsync(uri); Task afterDataTask = dataTask.ContinueWith((originalTask) => { return originalTask.Result.Length; }); return afterDataTask; } Ideas such as algebraic effects or continuations can also be used, although they are "overkill" for this construct. Async/await have been added to JavaScript and are used ubiquitously in web applications. More to Learn ------------- CPS technically gives you global continuations (capture entire stack), but algebraic effetcs and the other constructs use delimited continuations that only capture part of a stack. Have to tweak the compilation process above but CPS can still be used Current research statically scoped effects types that capture possible effects Notes for next year ------------------- Consider giving a semantics for algebraic effects and/or continuations Put an example earlier that requires a cactus stack for implementation