I'm not much of a Pythonist, but I have been doing CL since before ANSI CL.
To me the thing that CL has that has yet to be matched by any other languages, REPL-based or not, static or not, is the amazing condition system and dynamic restarts for handling problems. For building and debugging systems this is an utterly amazing thing. It also allows composing exceptional conditions in a reasonable way.
Every other language I use (e.g., Clojure these days a lot) I constantly miss this amazing stack-walking ability and conditional restarts.
Every time I boot my old Symbolics computers and something happens and I get a GUI restart, I'm just amazed at what we have lost through two decades of non-CL living.
The sound of it makes me think or Erlang's OTP system. I've just been really digging into it lately. Though the stack walking seems different, the entire OTP system is wonderful in letting you deal with errors by composing restartable processes and supervisors.
Heck, it's easier to handle recovering from an error by just restarting a process and letting it follow the normal unit logic.
Do have any knowledge of how CL and Erlang/Elixir/OTP are similar or vary on error handling?
Common Lisp's restarts are not restartable processes in the Erlang sense. They are part of a facility for suspending execution of a thread of control when it encounters an error, modifying the dynamic state of the process during the suspension, and resuming execution afterward.
When an error occurs in a Common Lisp program the runtime signals a __condition__. A condition is an instance of a class that represents unexpected or otherwise exceptional situations that can arise during execution. When a condition is signaled, control searches for a registered handler that matches the type of the signaled condition. If one is found then control passes to the matching handler. If none is found then control passes to the default handler, which by default starts an interactive repl called a breakloop__.
The breakloop has access to the full dynamic state of the suspended computation, including all local and dynamic variables and all pending functions on the stack. From the breakloop a programmer can alter variable values and redefine functions and classes. For example, if you conclude that the error happened because of an incorrect function definition, you can redefine the function and tell the breakloop to resume execution as if the new definition had been called instead of the original one.
A __restart__ in this context is an option for continuing execution. Common Lisp's condition system offers the programmer the ability to choose from among available restarts in a breakloop, or to write a handler that will make the choice automatically when a condition is signaled, and it also offers the ability to define custom restarts.
I've been wanting to experiment with this. There are some libraries in Clojure that try, but it seems that the real value is the interactivity of it while developing? I haven't found the exception handlers to really add much value over normal exception handling at least in Clojure.
As an interesting example, conditions and restarts can be used to implement an interactive system that is composable with other programs. For instance, you can implement a simple y/n question handler as follows:
[yes-or-no-p](http://clhs.lisp.se/Body/f_y_or_n.htm) is an interactive function that reads from the stdin. However, you cannot programatically answer "yes" to fn1, as other functions in the call stack (fn2) has no way to know that fn1 halts because it waits for the input from the stdin. Instead, a condition system allows this:
When you call fn2-automated, an error is signaled, handled by the handler, which invokes a restart 'yes, then :yes is printed. Interactivity is still maintained by fn2-interactive.
It enters the debugger due to the error. In the debugger menu you see additional entries YES and NO.
yes or no?
[Condition of type SIMPLE-ERROR]
Restarts:
0: [YES] YES
1: [NO] NO
2: [RETRY] Retry SLIME REPL evaluation request.
3: [*ABORT] Return to SLIME's top level.
4: [ABORT] abort thread (#<THREAD "repl-thread" RUNNING {1003167FA3}>)
See? All debugger menus are actually implemented as the restarts, set up at various call stacks. In this implementation of the debugger the way to select YES is to enter 0 or click it on the emacs buffer. In another debugger, it could be entering ":r 0" to the stdin. You can also implement your debugger function, which `read-line` the input stream and only recognizes a specific string, y or n, and set your function to `debugger-hook` to use it (I forgot to mention this in the above example).
Ok right. That's why I feel in Clojure it doesn't provide as much, because it doesn't hook into a debugger.
Having said that, practically, you wouldn't release a tool which has users interact at the debugger, at least for any level of more serious commercial work no?
CL's debugger is just a function, and the key is not to be distracted by the name "debugger".
As said, you can have a custom "debugger" which does not look like a debugger, but a general purpose user interface in the text terminal. For example, being "custom", it can also hide the restarts which are only meaningful for true debugging (like restart 2,3,4). (see detail [1])
It is possible for a debugger to even invoke a GUI (imagine a debugger hook which pops up a window with an OK button) or a web UI (sends a Websocket message to the client, wait for the user to click something on the browser, receive a reply and invoke the restart). In fact, SLIME is written this way: it overloads the debugger hook, and it sends and receives messages to the Emacs TCP client (thus you can click on the menu on an Emacs buffer to restart -- this action is notified back to the underlying lisp process via TCP, which then calls invoke-restart).
[1] You can obtain the list of restarts with COMPUTE-RESTARTS and filter unassociated restarts (similar to a method dispatch but is temporary / has dynamic extent -- see http://clhs.lisp.se/Body/09_adbd.htm).
To me the thing that CL has that has yet to be matched by any other languages, REPL-based or not, static or not, is the amazing condition system and dynamic restarts for handling problems. For building and debugging systems this is an utterly amazing thing. It also allows composing exceptional conditions in a reasonable way.
Every other language I use (e.g., Clojure these days a lot) I constantly miss this amazing stack-walking ability and conditional restarts.
Every time I boot my old Symbolics computers and something happens and I get a GUI restart, I'm just amazed at what we have lost through two decades of non-CL living.