callcc

callcc can be used as a building block for any control flow. See it in action:

metaesEval(
  `2 + callcc(function(){ console.log("args", arguments); }, 'an argument')`,
  (result) => console.log("result", result),
  console.error,
  { callcc, console }
);

This will result in a following output:

args: [
  'an argument',
  [Function],
  [Function],
  { values:
     { callcc: [Function: callWithCurrentContinuation],
       console: [Console] } },
  { script:
     { source: '2+callcc(function(){ console.log("args:", arguments); }, \'an argument\')',
       ast: [Script],
       scriptId: '0' },
    interpreters: { values: [Object] },
    interceptor: [Function: noop] } ]

As you can see, callcc calls provided function with 5 arguments which conforms to signature of any other evaluator. That means the 2nd argument is a success continuation. Also notice, "result" wasn't logged. Script never finished, because callcc didn't resume evaluation. Let's resume by calling the continuation ourselves:

metaesEval(
  `2 + callcc(function(argument, c) { 
         // here is the missing call
         c(2*argument)
       }, 3)`,
  (result) => console.log("result", result),
  console.error,
  { callcc, console }
);

which allows us to have 8 as a success result.

Custom fetch example

Let's refactor code to make it more readable and create something more useful - asynchronous fetch functions, similar to async/await with Promises:

// [1]
const resources = {
  "/me": { firstName: "User1" },
  "/me/friends": [{ firstName: "User2" }, { firstName: "User3" }, { firstName: "User4" }]
};
// [2]
function fetcher(path, c, cerr) {
  const response = resources[path];
  response ? c(response) : cerr(new Error(`Resource '${path}' does not exist.`));
}
// [3]
let fetch;
metaesEval(`path=>callcc(fetcher, path)`, (fn) => (fetch = fn), console.error, { fetcher, callcc });
metaesEval(
  // [4]
  `fetch('/me').firstName + ' has ' + fetch('/me/friends').length + ' friends'`,
  console.log,
  console.error,
  { console, fetch }
); // User1 has 3 friends

Explanation:

  • [1] - mock network responses,
  • [2] - fetcher is a function that was called by callcc. fetcher is in charge or resuming evaluation using c or cerr with a value,
  • [3] - create metafunction with bound callcc in closure and assign it to variable fetch. fetch will be used as a wrapper for callcc. Direct callcc calls look unnatural and it's a leaking abstraction for us. fetch gets path as an argument and forwards it to callcc. callcc then stops evaluation and asks fetcher function to handle c and cerr. When fetcher calls any of those two continuations, the value of callcc(...) call becomes what was provided by fetcher,
  • [4] demonstrates previous statement. fetch('/me') becomes { firstName: "User1" }.

Advantages and disadvantages of callcc

callcc allows reimplementation of all sorts of control flows: coroutines, asynchronity, iteration, generators and others.

Downside is higher maintenance price tag, especially for beginners. Even after creating many examples it's easy to get confused. That's because call/cc allows to twist execution flow in any direction. Consider this:

const results = [];
let cc;
function receiver(_, _cc) {
  cc = _cc;
  cc([1, 2, 3]);
}
metaesEval(
  `for (let x of callcc(receiver)) {
        results.push(x);
    }`,
  null,
  console.error,
  { callcc, receiver, results }
);
console.log("results", results); // results [ 1, 2, 3 ]
cc([4, 5, 6]);
console.log("results", results); // results [ 1, 2, 3, 4, 5, 6 ]

cc was called completely outside of metaES, after script already finished. Then script finished again.

As exercises you can implement following concepts callable form metaES space:

  1. getCurrentCallstack()
  2. getThisFunctionClosure()
  3. your own Proxy object
  4. your own await function
  5. your own generator function with yield support

lifting

TODO