Recursive Handlers
Effekt has recursive objects (i.e. object methods can call themselves recursively), but it doesn't have recursive handlers. Obviously, a handler method shouldn't be able to directly call itself or its fellow handlers, since its prompt is not present (under shift0 semantics). What I mean is that, bidirectional/higher-order resume should allow you to perform this handler's effects.
This relates to #1176 since (I believe) the type of bidirectional resume is incorrect, so it's the perfect time to introduce this.
Do note that this changes semantics slightly; some code might've relied on do sayHello being handled by an outer handler.
Example:
interface Dummy {}
interface Greet {
def sayHello(): Unit
def sayGoodbye(): Unit / Dummy
}
def helloWorld(): Unit / {} = try {
do sayGoodbye()
} with Greet {
def sayGoodbye = {
resume {
do sayHello() // Effect Greet is not allowed in this context.
}
}
def sayHello() = { println("Hello!"); resume(()) }
} with Dummy {}
In fact, it'd be perfectly reasonable to allow calling "sister" handlers that are on the same try as well:
interface Dummy {}
interface Dummy2 {
def dummy(): Unit
}
interface Greet {
def sayHello(): Unit
def sayGoodbye(): Unit / Dummy
}
def helloWorld(): Unit / {} = try {
do sayGoodbye()
} with Greet {
def sayGoodbye = {
resume {
do dummy() // Effect Dummy2 is not allowed in this context.
}
}
def sayHello() = { println("Hello!"); resume(()) }
} with Dummy2 { def dummy() = resume(()) }
with Dummy { }
This is guaranteed to be safe because, in effekt, handlers are deep, and we're 100% sure that we're pushing the prompt for the try when we call resume, hence, in the local context of the effect call, our handler and its sister handlers are guaranteed to be valid (after all, that's how our handler got called in the first place). Do note that a similar argument is made to justify being able to use outside handlers in local resumptions; it's just that using the same handler (or its sisters) has been forgotten about.
One final note to add: Looking at the first example, I had to add Dummy to force having bidirectional effects so that I can get the special resume. That makes perfect sense currently, since the special local resume is useless without bidirectional/higher-order effects (because of lexical handlers). However, if this gets implemented, then there are legitimate cases for someone to use local resume purely for the purpose of calling their handler (or a sister handler) recursively. That should probably be supported too (although there are likely workarounds by creating an object since object method bodies are local)
Hey, this is a great idea!
It is related to an old ticket: #397
We also had a long discussion about bidirectional effects here, in case you are interested: #395
In general, this will require local mutually recursive object definitions, which is not supported in our core language right now, as it translates to something like:
interface Dummy {}
interface Dummy2 {
def dummy(): Unit
}
interface Greet {
def sayHello(): Unit
def sayGoodbye(): Unit / Dummy
}
def helloWorld(): Unit / {} = reset { p =>
def greet = new Greet {
def sayGoodbye() = shift(p) { k => resume(k) { dummy.dummy() }
...
}
def dummy = new Dummy {
def dummy() = shift(p) { k => resume(k) { return () } }
}
greet.sayGoodbye()
}
Here, the object greet has a forward reference to the object dummy.
I didn't think too hard about the local mutual recursion here, so that's a good thing to point out! It'd be ideal if that could be supported somehow, but even if not, it'd still be okay if one had to reorder Greet and Dummy to get it to compile (although, of course, that rules out some programs that would use mutual recursion, and instead forces it to be a DAG but with self-loops, I think?)