> How would you use call/cc or the rest with arbitrary callers?
For example, with shift/reset operators (a form of delimited continuations) you can do something like this pseudo-JS code:
const cancelled = reset {
arr.sort((a, b) => {
if (aborted()) {
shift (k) {
// k is a continuation from shift to reset,
// but we want early return so we discard k
// and replace the reset expr with false.
return false;
}
}
return compareTo(a, b);
});
// this will replace the reset expr if shift is not triggered.
return true;
};
Every example I've mentioned can be used to implement exceptions after all, so if you know something can be done with exceptions, you can do the same with call/cc and so on as well.
This is basically a functional version of setjmp and longjmp right? Meaning it has the same problems? How do you ensure the caller's cleanups etc. still occur so you don't mess up the program state?
I'm not sure about "messing up the program state". Do you mean the memory error like segfault, or the `finally` block? My answer would depend on the answer:
Memory error: Instead of the register states for setjmp, call/cc and so on give you a well-behaved function of the continuation. I will spare what the continuation is but basically it's possible to convert any code into a code with the explicit continuation available as a function at any time (Continuation-Passing Style). So unlike setjmp/longjmp you can be sure that everything will be a normal function call in the end. (The actual implementation can of course vary.)
`finally` block: You are kinda right. They are used to implement things like more readable control structures like exceptions and cumbersome to use directly. There are things like `dynamic-wind` in the Scheme world, but it's true that if you weren't aware you can easily screw up. My point stands though, what you need is a non-local jump and not the exception proper. It just happens that the most widespread non-local jump construct is an exception.
I meant finally blocks... freeing memory, reverting changes, etc... which are things you'd use destructors for (and hence why you can't use setjmp/longjmp with them). If you don't do them then you mess up your program's state and hence have no way to ensure correct behavior afterward. This is why I mentioned arbitrary callers: you have no idea what callers have done or what they still need to do before returning. Heck, even sort() could have allocated memory that you'd be leaking, and not all destructors are constrained to resource leaks either.
I don't know what you consider "exceptions proper" (do you have to inherit from Exception? do you even have to pass along extra information?) but I don't think I agree. My point is actually precisely that what you want is not a non-local jump, but actual unwinding. Semantically, this is what exceptions are: synchronous dynamic unwinding. If you have a way to stop control flow and unwind arbitrary callers in a synchronous manner, what you have is exceptions, regardless of the particular syntax or other details. (e.g., it's beside my point you can "throw" extra information alongside this mechanism, or whether it's part of the language or a library, or whether you do the unwinding on the heap vs. the stack, or whether it's functional or procedural, or what have you.)
And my overall point is that this kind of dynamic unwinding is necessary for you to be able to do certain things like canceling routines in the middle in a safe and well-defined manner. (Especially that Result<>/Optional<>/whatever explicit localized mechanism is in fashion these days is clearly an insufficient replacement for exceptions.)
I'm in principle in agreement. You do need unwinding. Specifically I meant an explicitly triggered unwinding by "exceptions proper". I just wanted to point out that unwinding is much broader than exceptions. (For example generators will implicitly unwind upon its cleanup. Of course it requires callee's cooperation and I couldn't use it as an example.) I also don't buy the claim that explicit Result types are incompatible with unwinding---e.g. RAII works fine with result types.
For example, with shift/reset operators (a form of delimited continuations) you can do something like this pseudo-JS code:
Every example I've mentioned can be used to implement exceptions after all, so if you know something can be done with exceptions, you can do the same with call/cc and so on as well.