Skip to content

Instantly share code, notes, and snippets.

@enaeher
Last active August 29, 2015 14:24
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save enaeher/96b2e6ec000618289d7b to your computer and use it in GitHub Desktop.
Save enaeher/96b2e6ec000618289d7b to your computer and use it in GitHub Desktop.
;; * Lisp as an interactive environment
;; The Lisp dream has always been about the control that is possible with
;; a unified system, modifiable interactively at runtime.
;; When it's "Lisp all the way down," as the Lisp Machine designers
;; envisioned, a debugger becomes more than a programmer's tool; instead,
;; in combination with the inspector, it's a basic mode of interaction
;; with (and exploration of) the system. Perhaps it is for this reason
;; that Lisps have always had excellent facilities for debugging and
;; error-handling.
;; * More than just a stacktrace
;; The Lisp debugger is a live system with which you can interact freely:
(in-package :cl-user)
(ql:quickload 'drakma)
(drakma:http-request "http://example.bad" :connection-timeout nil)
;; Using this debugger does not require setting any breakpoints,
;; using any special build options, or enabling any special
;; mode. This is the default response to unhandled exceptions in the
;; Lisp environment.
;; * You have access to the full environment in any stack frame
;; Including the lexical environment (e.g., closures):
(let ((counter 0))
(defun adder (n)
(incf counter n)))
(adder 5)
(adder 10)
(adder 'green)
;; * Exceptions while in the debugger
;; You can go up and down levels in the debugger without losing your
;; state
;; * Philosophical differences with modern exception handling
;; The conventional wisdom today is: "use exceptions only for
;; exceptional cases," or only for unrecoverable failures. In an
;; environment where the debugger was so integral to the system, this
;; hasn't historically been the approach taken by Lisp
;; developers. Even the nomenclature reflects this: we have
;; conditions, not exceptions.
;; * Restarts
;; Notice the list of restarts. What is a restart?
;; In other languages, throwing an exception allows you to separate
;; the location where an error is thrown from from the location where
;; we decide what to do about it. This is helpful, since we often
;; lack the information necessary to make that decision at the point
;; of failure. However, there is a problem: the stack is unwound to
;; the point where you make that handling decision (the location of
;; the try/catch block), and execution must resume from there.
;; If you were Rich Hickey, you might say that Java-style exception
;; handling complects (*drink*!) the decision-making and control-flow
;; aspects of error handling. Restarts let us separate these.
;; * An example
;; That's pretty abstract, so let's look at a concrete example:
;; Suppose we are parsing ASCII representations of bank account
;; numbers which look like this:
;; _ _ _ _ _ _ _ _
;; |_||_ ||_ | ||_|| || || |
;; | _| | _||_||_||_||_||_|
(in-package :ocr)
(parse-file "/Users/eli/work/bank-ocr-kata/sample-input")
;; Returning nils for the error case? What is this, Go? Let's define
;; a condition we can signal:
(defun digit->string (digit)
(format nil "~{~a~%~}~" (mapcar 'list->string (digit->character-list digit))))
(define-condition unparseable-digit (error)
((digit :initarg :digit
:reader digit
:type list))
(:report (lambda (condition stream)
(format stream "Couldn't make heads or tails of this:~%~a"
(digit->string (digit condition))))))
; Now we need to modify the character parser to throw this instead
;; of returning nil
(defun %lookup-digit (digit)
(or (position digit *digits* :test #'equal)
(error 'unparseable-digit :digit digit)))
;; * The call to %lookup-digit is where we will add our first restart
(defun %parse-character-list (character-list)
(loop
;; take sets of three elements
:for (first second third)
;; rotate the lists so that the first three elements are the first three columns
:on (rotate character-list)
;; jump three elements in each iteration
:by #'cdddr
;; rotate them back so that we again have a list of rows to
;; compare against the dictionary
:for digit = (rotate (list first second third))
:collecting (%lookup-digit digit)))
;; * Mimic the old behavior
(defun %parse-character-list (character-list)
(loop
;; take sets of three elements
:for (first second third)
;; rotate the lists so that the first three elements are the first three columns
:on (rotate character-list)
;; jump three elements in each iteration
:by #'cdddr
;; rotate them back so that we again have a list of rows to
;; compare against the dictionary
:for digit = (rotate (list first second third))
:collecting (restart-case
(%lookup-digit digit)
(skip-digit () nil))))
;; * An interactive restart
;; Now we can add a restart which lets you specify the right digit
(defun read-digit ()
(princ "Enter a new value: ")
(multiple-value-list (eval (read))))
(defun %parse-character-list (character-list)
(loop
;; take sets of three elements
:for (first second third)
;; rotate the lists so that the first three elements are the first three columns
:on (rotate character-list)
;; jump three elements in each iteration
:by #'cdddr
;; rotate them back so that we again have a list of rows to
;; compare against the dictionary
:for digit = (rotate (list first second third))
:collecting (restart-case
(%lookup-digit digit)
(skip-digit () nil)
(use-value (value)
:report "Specify a digit to use (0-9)"
:interactive read-digit
value))))
;; * Decomplecting! (*drink*)
;; Restarts don't need to be defined right where we throw the
;; error. They can be defined anywhere in the call stack between
;; where the error is signaled and where it is handled (in this case,
;; at the toplevel by the debugger).
;; The power of the restart facility is the way that it lets you
;; choose to what point you want to unwind the stack, independently
;; of where you it is thrown and where you decide how to handle
;; it.
;; * Skipping the whole account number
;; Let's say you decide if there's a bad digit in an account number,
;; you just want to skip the whole thing rather than trying to figure
;; out the right digit or return a useless account number with nils
;; in it:
(defun parse-entry (stream)
"Reads four lines from STREAM, ignoring the fourth, and returns a
list of integers representing the parsed digits read from the first
three lines. If any digit is illegible, nil will appear in the list
of digits. If it's not possible to read three lines from the stream,
parse-entry will return nil."
(let* ((strings (loop :for i :from 1 :upto 3 :collecting (mapcar #'character->boolean (string->list (read-line stream nil)))))
(parsed-account-number (when (every 'identity strings)
(restart-case
(%parse-character-list strings)
(skip-account-number () nil)))))
;; ignore blank line between entries
(read-line stream nil)
(values parsed-account-number strings)))
;; * Restarts can also be invoked programmatically
;; For example, in a batch process we may want to skip and log bad
;; account numbers:
(handler-bind ((unparseable-digit
(lambda (e)
(format t "Error while processing batch, skipping: ~a" e)
(invoke-restart 'skip-account-number))))
(parse-file "/Users/eli/work/bank-ocr-kata/sample-input"))
;; In another context we might want to use the same parse-file
;; function but see the specific missing digits for auditing:
(handler-bind ((unparseable-digit
(lambda (e)
(declare (ignore e))
(invoke-restart 'skip-digit))))
(parse-file "/Users/eli/work/bank-ocr-kata/sample-input"))
;; Consider the usefulness of the built-in RETRY restart here. You
;; could attempt a database operation and handle connection errors by
;; starting the database and invoking the retry restart.
;; * Fun with threads
(ql:quickload 'lparallel)
(lparallel:task-handler-bind ((unparseable-digit
(lambda (e)
(declare (ignore e))
(format t "~%Invoking restart in thread ~a" sb-thread:*current-thread*)
(invoke-restart 'skip-digit))))
(lparallel:plet ((output-1 (progn (sleep 1) (parse-file "/Users/eli/work/bank-ocr-kata/sample-input")))
(output-2 (progn (sleep 1) (parse-file "/Users/eli/work/bank-ocr-kata/sample-input-2"))))
(list output-1 output-2)))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment