BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage News Effectful Effects - Unifying Bidirectional Communication between Software Components

Effectful Effects - Unifying Bidirectional Communication between Software Components

This item in japanese

Yizhou Zhang, assistant professor at the University of Waterloo, presented bidirectional algebraic effects, a new programming abstraction that subsumes current control flow patterns (e.g., exceptions, promises, generators) while supporting bidirectional flows of control. The new typed abstraction guarantees that all declared effects are handled, and no effects are accidentally handled (e.g., by the wrong handler).

In his talk at SPLASH, a conference on the applications of programming languages, Zhang first recalled the adoption in many languages of increasingly complex control flow features. Regarding JavaScript, ECMAScript 3 introduced exceptions; ECMAScript 6 (also called ES6 or ES2015) added promises and generators that may throw exceptions; ECMAScript 8 (ES2017) later added with async. functions, which ECMAScript 9 (ES9) subsequently complemented with async. generators.

Zhang then explained:

Software [has] become increasingly event-driven. Callback functions are a conventional pattern for event-driven programming, but unconstrained callbacks become complex and hard to reason about as applications grow. Hence, it is currently in vogue for programming languages to build in support for advanced control-flow transfer features like generators and async–await. These features support more structured programming of asynchronous, event-driven code.

Algebraic effects have emerged as a powerful alternative that allows programmers to define their own control effects; […] subsumes a wide range of features including exceptions, generators, and async–await; [… provides] a nice separation between the syntax (i.e., a set of effect operations) and the semantics (i.e., handling of those operations).

However, even with these advanced language features at hand, programmers today still find certain complex control-flow patterns painful to manage.

In particular, Zhang argued that common control language features are not expressive enough to capture bidirectional control transfer while guaranteeing that all effects are indeed handled. Zhang summarizes the issues with existing control flow constructs commonly found in mainstream languages as follows:

Issues with existing control flow constructs
(Source: SPLASH talk)

Programmers have indeed noted the potential for footguns when using JavaScript promises and async/await to handle asynchronous computations. Ian Segers detailed in a blog post error-handling issues picked up during code reviews with junior developers. Promises in try...catch block have to be awaited for the catch block to handle a rejected promise.

async function thisThrows() {
    throw new Error("Thrown from thisThrows()");
}

try {
    thisThrows();
} catch (e) {
    console.error(e);
} finally {
    console.log('We do cleanup here');
}

// output:
// We do cleanup here
// UnhandledPromiseRejectionWarning: Error: Thrown from thisThrows()

The previous code for instance does not catch the thrown exception. The UnhandledPromiseRejectionWarning warning only appears at runtime. Jake Archibald explained the subtleties around await vs. return vs. return await. The team behind CatchJS explored the pitfalls of asynchronously raised exceptions: they are thrown in a different call stack – meaning that the error may propagate outside of the application code:

function fails3() {
    return new Promise((resolve, reject) => {
        setTimeout(function() {
            throw new Error();
        }, 100);
    });
}

async function myFunc3() {
    try {
        await fails3();
    } catch (e) {
        console.log("that failed", e); //<-- never gets called
    }
}

The JavaScript onunhandledrejection global handler may thus be the wrong handler for some exceptions, and would better be called onPossiblyUnhandledRejection — as the Q and Bluebird promise libraries do. The CatchJS team notes:

When dealing with promises, you have no way of knowing if an error will be handled sometime in the future. The promise might call reject(), and some code might come along 10 minutes later and call .catch(() => {}) on that promise, in which case the error will be handled.

Zhang reported similar issues in C#:

static byte[] HttpGet(String url);  
static async Task<Json> HttpGetJson(String url) {  
  Task<Json> t = Task.Run(() => HttpGet(url));  
  byte[] bytes = await t;  
  return JsonParse(bytes);  
}  
static async Task Main() {  
  Task<Json> t = HttpGetJson("xyz.org");  
  ... // do things that do not depend on the query result  
  Json json = await t; // block execution until query terminates  
 ...  
}

The C# compiler will run the previous program without requiring that an exception handler be provided. Nonetheless, if the asynchronous query does result in an exception, the program crashes. Zhang wrote in a related paper:

The situation is worse in JavaScript: an exception raised asynchronously is silently swallowed if not otherwise caught. Such unhandled exceptions have been identified as a common vulnerability in JavaScript programs.

To the previously described issues (unhandled or accidentally handled effects) must be added those related to generators. Generators allow bidirectional control flow between a generator and its clients. However, they do not allow clients to concurrently modify the data iterated on. Zhang detailed some use cases:

A client iterating over a priority queue might want to change the priority of a received element; similarly, a client iterating over a stream of database records might want to remove one of those records from the database.

After describing the existing tradeoffs with existing control flow constructs, Zhang then described a possible solution that generalizes algebraic effects, called bidirectional algebraic effects. Bidirectional algebraic effects statically guarantee that there are no unhandled or accidentally handled effects, while allowing for bidirectional control flows.

Algebraic effects support signatures for control effects and for handlers as implementations of these signatures. In this case, effectful code raises effects that propagate up the dynamic call stack to their handlers. The functionality of generators may be replicated with algebraic effects as follows (example of a Node iterator):

// Effect signature
effect Yield[X] {  
  def yield(X) : void  
}

// Iterator
class Node[X] {  
  var head : X  
  var tail : Node[X]  
  ...  
  def iter() : void raises Yield[X] {  
    yield(head)  
    if (tail != null)  
      tail.iter()  
  }  
}

The Node iterator can then be used as follows:

try { node.iter() }  
with yield(x) {  
  print(x)  
  resume()  
}

The try... with block allows segregating the code that may raise effects from the code that handles them. Type signatures at the effect and iterator declaration level contribute to providing type safety to the constructs.

Bidirectional effects go further by empowering the iterator’s client to modify the iterator state by raising what Zhang termed effectful effects. With effectful effects, effect handlers can raise subsequent effects that propagate in the opposite direction to the site where the initiating effect was raised, transmitting information and control between program fragments.

Bidirectional algebraic effects

The previous illustration showcases an Async effect, an Exn (exception) effect, and a Yield effect that are used to implement an asynchronous generator that can throw exceptions. The Yield effectful effect may be handled by a function that raises an Async effect or an exception. The graph shows the 3-parties control flow that can be implemented with the effects in a type-safe way and without unhandled exceptions, as may occur in JavaScript.

Zhang explained:

We can have an iterator that schedules asynchronous computations and yields promises, which, when awaited, can raise exceptions to be handled by the iterator. The underlying control flow is complex, but still remains manageable, because static checking of effects provides guidance on where to apply effect handling. We can also view the signatures of effectual effects to be choreographing communication between two processes.

In a related paper, Zhang proved that bidirectional algebraic effects guarantees that all declared effects are handled, and no effects are accidentally handled (e.g., by the wrong handler).

The full talk of Prof. Yizhou Zhang is available online and contains additional details, examples, and illustrations. SPLASH is a conference on the applications of programming languages that embraces all aspects of software construction and delivery.

Rate this Article

Adoption
Style

BT