I also asked this question on clojurians, but I’ll ask it here as I suspect it may lead to a more indepth and opinionated discussion.
I wrote the following function, inspired by a similar function I previously implemented in Scala. However, certain parts of the Scala function are not implemented yet in my clojure version.
(defn call-with-return
"A functional interface to CL block/return.
The caller of call-with-return provides a unary function.
The body of that function may call the function passed as argument.
to cause call-with-return to return. E.g.,
(call-with-return
(fn [ret1]
;; now ret1 is a unary function, callint ret1 with argument x
;; causes call-with-return to immediately return x
(if something
(ret1 42) ;; return 42 from call-with-return
...)))"
[unary]
(letfn [(ret [v] ;; TODO, need to somehow set NoStackTrace on the exception
(throw (ex-info "" {:data v
:ident ret})))]
(try (unary ret)
(catch clojure.lang.ExceptionInfo e
(if (= ret (:ident (ex-data e)))
(:data (ex-data e))
(throw e))))))
The function, call-with-return
creates a block which can be escaped from. Such escape can be anywhere within the calling stack. The function is also reentrant, in the sense that I can nest calls to call-with-return
, and the escaping protocol won’t get confused.
Moreover, since the escape function is just a lexical variable, it can be passed along to other functions allowing them to escape as well.
Usage example:
(call-with-return
(fn [ret1]
(call-with-return
(fn [ret2]
(ret1 42))) ;; return from outer call-with-return
43))
---> 42
(call-with-return
(fn [ret1]
(call-with-return
(fn [ret2]
(ret2 42))) ;; return from inner call-with-return
43))
43
So what’s the problem? Well the call to ex-info
is expensive, and the underlying java implementation does a lot of work to generate stacktrace information. My Scala implementation of this function obviates this problem by creating a local subclass of Exception, overriding the java method which generates such expensive stacktrace information. I don’t know how to do this in Clojure, but I’m pretty sure it’s possible as the Scala code doesn’t do much other than propagate the information on to the java exception generator.
def block[A](body:(A=>Nothing)=>A):A = {
// CL like block/return, the name of the return() function is provided
// by the caller.
// Usage: block{ ret => ... ret(someValue) ...}
// extending Exception with NoStackTrace prevents throwing the
// exception from computing the stacktrace and storing the information.
// We don't need a stacktrace because the purpose of this exception
// is simply to perform a non-local exit.
import scala.util.control.NoStackTrace
class NonLocalExit(val data:A) extends Exception with NoStackTrace {}
def ret(data:A):Nothing = {
throw new NonLocalExit(data)
}
try{
body(ret)
}
catch{
case nonLocalExit: NonLocalExit => nonLocalExit.data
}
}