Understanding algebraic effects

Viewed 255

I came across Effekt language and the idea of algebraic effects. But I'm not sure if I understood the concept correctly. I tried to re-implement the basic "hello world" example in Python and came up with this:

import contextlib
import contextvars

handlers = contextvars.ContextVar('handlers', default={})

def effect(name, *args, **kwargs):
    try:
        return handlers.get()[name](*args, **kwargs)
    except KeyError:
        raise RuntimeError(f'No handler for effect {name}')

@contextlib.contextmanager
def handle(**kwargs):
    token = handlers.set(dict(**handlers.get(), **kwargs))
    try:
        yield
    finally:
        handlers.reset(token)

def example():
    n = 0
    produce = True
    while produce:
        produce = effect('Yield', n)
        n += 1
    return n

with handle(Yield=lambda n: n < 3):
    example()

My question is, can this snippet qualify as a proper algebraic effect? If not, where am I wrong?

1 Answers

algebraic effects are unique because they enable deeply nested handlers, as opposed to something like python generator which can only be directly manipulated by the immediate parent.

an example is in the tests of Effects lib

def test_deep_context():
    sent1 = object()
    sent2 = object()
    with eff.Effects(sent=sent1) as e1:
        assert e1.sent is sent1
        assert eff.Effects.sent is sent1

        with eff.Effects(sent=sent2) as e2:
            assert e1.sent is sent1
            assert e2.sent is sent2
            assert eff.Effects.sent is sent2

        assert e1.sent is sent1
        assert eff.Effects.sent is sent1

here we can see an effect can be masked/overridden in a child scope without affecting the parent scope.

one thing "typed" algebraic effects has, which this python library does not is the ability for a compile time error to indicate a child using, in this example eff.Effects.sent, would fail to compile if "sent" was not provided in a parent scope.

back to your main question and example... I think it would only allow for "flat" handlers, but not hierarchical ones. if the same handler name were defined twice the last one would always win for all levels of depth at all invocations

Related