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 usingc
orcerr
with a value,[3]
- create metafunction with boundcallcc
in closure and assign it to variablefetch
.fetch
will be used as a wrapper forcallcc
. Directcallcc
calls look unnatural and it's a leaking abstraction for us.fetch
getspath
as an argument and forwards it tocallcc
.callcc
then stops evaluation and asksfetcher
function to handlec
andcerr
. Whenfetcher
calls any of those two continuations, the value ofcallcc(...)
call becomes what was provided byfetcher
,[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:
getCurrentCallstack()
getThisFunctionClosure()
- your own
Proxy
object - your own
await
function - your own generator function with
yield
support
lifting
TODO