Arc Forumnew | comments | leaders | submitlogin
Exceptions and protect
2 points by dido 4701 days ago | 7 comments
While reconsidering implementation of protect / dynamic-wind for Arcueid, I got to thinking of a corner case involving the interaction of protect and exceptions. What exception should this code return?

    (on-err (fn (ex) ex)
            (fn ()
                (protect (fn ()
                             (err "foo"))
                         (fn () (err "bar")))))
In Arc 3.1, this code returns the the "bar" exception raised by the protect handler. The original "foo" exception raised by the protected code is lost. I suppose that is as it should be, but I do believe that there should be some way to retrieve the original exception, rather than the last one raised by protect handlers. What do other languages with similar mechanisms do in this case?


1 point by Pauan 4701 days ago | link

"I suppose that is as it should be"

That's right. The point of protect is to do something even if an exception is raised (or continuations are used). It's used for things like closing files. It's analogous to try ... finally in other languages[1], which don't let you grab the exception either.

---

"but I do believe that there should be some way to retrieve the original exception"

If you want try ... catch, then you should use on-err, which you do in the example above. Is there some use case where on-err doesn't work but protect might?

---

* [1]: To clarify:

  JavaScript
    try {
        ...
    } catch (e) {
        ...
    }

  Arc 3.1
    (on-err (fn (e) ...)
            (fn ()  ...))
---

  JavaScript
    try {
        ...
    } finally {
        ...
    }

  Arc 3.1
    (protect (fn () ...)
             (fn () ...))
---

Looking at the above, assuming you didn't care about Arc 3.1 compatibility, it would be possible to merge the two together, into a single operator:

  (protect (fn ()  ...)
           (fn (e) ...))
I'm not sure what the variable "e" should mean in that context, though.

-----

2 points by Pauan 4701 days ago | link

Ah, right, you can't actually merge them together, because they do different things. The code in the second function to protect is always run, regardless of an exception in the first function. But with on-err, the first function is only run if there's an exception... so they truly are analogous to try ... finally and try ... catch, respectively.

So what you're saying is that protect should be extended to pass an argument to the second function in the case of an exception being thrown in the first function...? That doesn't sound like a bad idea, but... what's the use case? What benefit would it give? And could we obtain that same benefit with the existing tools?

-----

1 point by rocketnia 4701 days ago | link

Sure they can be merged. If the input to the catcher function is a data structure indicating what kind of jump is being intercepted and the output is a similar data structure indicating what kind of jump to continue into, it can even be a pure function (if the user so chooses; they could also raise an error or call a continuation manually). The before part of a dynamic wind could work this way too.

The catch is that this reflective kind of tool may not be very efficient... but fortunately, it doesn't have to be the lowest-level tool. In fact, with 'dynamic-wind around, it might not even need to be an axiom:

  ; TODO: Test this code. It's big and confusing and therefore almost
  ; certainly buggy.
  
  ; We're using '$ (as seen in Anarki) to get things from Racket.
  (= raise $.raise)
  (= dynamic-wind $.dynamic-wind)
  (mac b-and-a (bef . rest)
    (let (aft . body) rev.rest
      `(dynamic-wind (fn () ,bef) (fn () ,@body) (fn () ,aft))))
  (mac before (bef . body)
    `(b-and-a ,bef ,@body nil))
  
  ; The "refl" is for "reflective."
  (def refl-dynamic-wind (bef body aft)
    (with (resume-unknown-ccc-exit nil normalentry t)
      (before
        (let normal normalentry
          (wipe resume-unknown-ccc-exit normalentry)
          (let entry (bef:if normal '(enter) '(unknown-ccc))
            (case entry.0
              enter        (unless normal
                             (err "Can't enter normally"))
              raise        (raise entry.1)
              unknown-ccc  (when normal
                             (err "Can't enter using unknown-ccc"))
              known-ccc    (entry.1)
                (err:+ "Unknown entry " (tostring:write entry)))
        (let exit (aft:catch:let normalexit nil
                    (b-and-a
                      wipe.normalexit
                      (do1 (on-err [do `(raise ,_)]
                                   (fn () `(return ,(body))))
                           (= normalexit t))
                      (unless normalexit
                        (when (ccc [= resume-unknown-ccc-exit _])
                          (throw `(unknown-ccc))))))
           (case exit.0
             return       exit.1
             raise        (raise exit.1)
             unknown-ccc  (if resume-unknown-ccc-exit
                            resume-unknown-ccc-exit.nil
                            (err "Can't exit using unknown-ccc"))
             known-ccc    (exit.1)
               (err:+ "Unknown exit " (tostring:write exit))))))
  
  (mac refl-b-and-a (bvar bef . rest)
    (let (aft avar . body) rev.rest
      `(refl-dynamic-wind
         (fn (,bvar) ,bef) (fn () ,@body) (fn (,avar) ,aft))))))
  (mac refl-before (bvar bef . body)
    `(refl-b-and-a ,bvar ,bef ,@body nil nil))
  
  ; NOTE: Arc's 'after uses all sub-s-expressions but the first as the
  : catcher, while this uses only the last two.
  (mac refl-after rest
    (let (aft avar . body) rev.rest
      `(refl-b-and-a nil nil ,@body ,avar ,aft)))
  
  
  ; Example usage:
  
  (def reimplemented-on-err (catcher body)
    (refl-after (body)
      exit (case exit.0 raise
             `(return ,(catcher exit.1))
             exit))
  
  (def reimplemented-protect (body aft)
    (refl-after (body)
      exit (do (aft) exit)))
  
  (def reimplemented-dynamic-wind (bef body aft)
    (refl-b-and-a
      entry (do (bef) entry)
      (body)
      exit (do (aft) exit)))
  
  (def reimplemented-raise (error)
    (refl-after nil
      exit `(raise ,error)))
---

JavaScript's catch and finally aren't so different either:

  function myFinally1( body, aft ) {
      try { body(); }
      finally { aft(); }
  }
  
  function myFinally2( body, aft ) {
      try { body(); }
      catch ( e ) {
          aft();
          throw e;
      }
      aft();
  }
However, things like myFinally2() are pretty annoying when trying to use Chrome's debugger with "Break on uncaught exceptions," so I've stopped treating these as equivalent in practice.

-----

1 point by Pauan 4701 days ago | link

"JavaScript's catch and finally aren't so different either"

I realized afterwards that you can define protect in terms of on-err, but that isn't quite the same as using the same operator for both use cases. And, uh, that code looks awfully intimidating, so please forgive me for not looking deeply into it and trying to understand it.

In any case, my question still hasn't been answered: is there any use case for this at all?

-----

2 points by rocketnia 4700 days ago | link

"I realized afterwards that you can define protect in terms of on-err"

Nah. While that's a straightforward analogy from catch and finally, 'protect deals with continuation early exits (not seen in JavaScript), whereas 'on-err isn't supposed to. Jarc's 'on-err did for a while, but I seem to remember that being fixed.

---

"And, uh, that code looks awfully intimidating, so please forgive me for not looking deeply into it and trying to understand it."

Fair enough. ^_^

Just in case anyone wants to use that monstrous code, feel free.

---

"In any case, my question still hasn't been answered: is there any use case for this at all?"

Inasmuch as a language designer is trying to expose as much as possible to the programmer, even things they don't expect to be useful, that's not a question that needs an answer. The real question is "Why not?"

But dido spelled out at least one goal: "I do believe that there should be some way to retrieve the original exception, rather than the last one raised by protect handlers"

I'm guessing dido's point is that a buggy protect handler shouldn't keep the programmer from discovering bugs under its protection. (It's being overprotective? :-p ) For instance, dido might like to see both errors at the command line.

---

For you and anyone else who didn't read the 'refl-dynamic-wind code, here's an outline of the guards' inputs and outputs, so you can get a sense of the flexibility it gives to the programmer:

  ; Possible inputs to the entry guard:
  (enter)
  (unknown-ccc)
  
  ; Possible results of the entry guard:
  (enter)          ; only if (enter) was the input
  (raise <error>)
  (unknown-ccc)    ; only if (unknown-ccc) was the input
  (known-ccc <destination continuation>)
  
  ; Possible inputs to the exit guard:
  (return <value>)
  (raise <error>)
  (unknown-ccc)
  
  ; Possible results of the exit guard:
  (return <value>)
  (raise <error>)
  (unknown-ccc)
  (known-ccc <destination continuation>)
(It could also help to read the examples I posted, which are at the bottom of that giant code block.)

Come to think of it, it's probably possible to keep hacking on 'refl-dynamic-wind until the interface is like this:

  ; Possible inputs to the entry guard:
  (enter)
  (continue <destination>)
  
  ; Possible inputs to the exit guard:
  (return <value>)
  (raise <error>)
  (continue <destination>)
  
  ; Possible results of either guard:
  (enter)
  (return <value>)
  (raise <error>)
  (continue <destination>)
I'd like to point out that this interface isn't the full extent of what the language designer might like to expose to the programmer.

For instance, in Kernel, continuation guards have access to both the destination continuation and the value being passed to it. If we implemented 'refl-dynamic-wind there, every one of the (continue <destination>) cases above could become (continue <destination> <value>).

If an Arc implementation provided that extended interface, it would make for a more flexible language from a dynamic standpoint and a less predictable language from a static standpoint.

-----

1 point by akkartik 4701 days ago | link

On a tangent, I have somehow never come across protect before. How is it different from after?

-----

3 points by dido 4701 days ago | link

after is a macro that is defined in terms of protect. Protect is the primitive form. Arc 3.1 defines after as:

    (mac after (x . ys)
      `(protect (fn () ,x) (fn () ,@ys)))
after is just syntactic sugar for protect.

-----