C++ Coroutines Three: More Than Asynchrony
If you haven't read my previous blogposts on C++ coroutines, I would recommend doing so.
This post isn't an explanation of implementing something, as much as it is an explanation of something cool you can do in C++ using coroutines. Warning: this is very esoteric from a C++ perspective, and you're probably going to have problems understanding this post without knowing what algebraic effects are from other languages (mostly research ones), despite my best attempts to explain them.
In short, you can implement a rudimentary version of algebraic effects using C++ coroutines.
But what is an algebraic effect?
The simplest way to describe them is “resumable exceptions that can return a value”.
Koka explains it well with an example:
effect ask<a> {
control ask() : a
}
This defines a generic effect ask<T>
with an operation ask
that returns a value of type T
.
If a function wants to use this effect, it has to declare that it does, much in the same way that some languages require you to declare that your function can throw an exception.
fun ask-and-add-twice() : ask<int> int {
return ask() + ask()
}
When you call this function, the Koka compiler checks that you handle ask somewhere in enclosing scopes, the same way that some languages check that your exceptions are being caught in a try
/catch
block.
Defining a handler for an effect in Koka looks like this:
fun ask-random() : random int {
with control ask(){ resume(random-int()) }
add-twice()
}
fun ask-const() : int {
with control ask(){ resume(21) }
add-twice()
}
So what does this have to do w/ C++ and coroutines?
C++ coroutines allow you to pause a function, and resume it, optionally passing in a value. This is the same thing that algebraic effects do: pause a function, and resume it, optionally passing in a value.
As a result, you can implement much of the functionality of algebraic effects in C++ using coroutines.
This is an example snippet of Crouton's rudimentary implementation of algebraic effects:
using Log = EffectVoidFun<std::function<void(QString)>>;
Effect<void> contextDependentLogging() {
perform Log("hi");
co_return;
}
void log() {
{
auto handler = Log::handler([](const QString& it) -> int {
qDebug() << it;
});
contextDependentLogging();
}
{
auto handler = Log::handler([](const QString& it) -> int {
qWarning() << it;
});
contextDependentLogging();
}
}
Algebraic effects are powerful in that they let your functions generic over behaviour, much in the same way that templates allow functions to be generic over types.
For example, say you're authoring a Matrix client. You can define an effect fetch
that GETs/POSTs a URL. Without touching your code that uses fetch
, you can transparently replace the handler for fetch
in different parts of your application. Your live application could use a handler that uses the actual internet, while unit tests could use a handler that returns dummy data. Compared to #ifdef-ing different implementations, this allows different parts of an application to use different handlers at runtime, and allows library users to provide their own handlers without needing to modify the library.
There's a lot of stuff you can do with algebraic effects, and this only scratches the tip of the iceberg.
That's all for this blog post. Stay tuned for more C++-related shenanigans.
Contact Me
If you didn't understand anything here, please feel free to come to me and ask for clarification. This blog post is probably particularly confusing since it's deep in the realm of programming language geekery in the same way monads are.
Or, want to talk to me about other coroutine stuff I haven't discussed in these blog posts (or anything else you might want to talk about)?
Contact me here:
Telegram: https://t.me/pontaoski Matrix: #pontaoski:tchncs.de (prefer unencrypted DMs)
Tags: #libre