The awesome example we will read comes from a comment by user lispm inside a discussion on this reddit thread: https://www.reddit.com/r/programming/comments/65ct5j/a_pythonist_finds_a_new_home_at_clojure_land/.

The article it discusses is a “Not a monad tutorial” post, where the interviewee is experienced in C++, Java, Javascript and Python and turns into Clojure. He wrote about his first impressions with Common Lisp here, where he raises usual concerns that I agree with but IMO that stay supercifial (“not readable” because of stuff like (format t "~{~{~a:~10t~a~%~}~%~}" *db*), “huge operators set”, “macros look promising”…).

Here starts the discussion.


dzecniv On Common Lisp, I agree with the criticisms except

the code was very difficult to read

I find it very easy, always well expressed, with concise functions. And I find Clojure’s harder, with more [, { and the same number of other symbols (#, *).

Anyway, I’m in the process of trying to go from python to CL. The CL ecosystem is quite good nowadays (equivalents of pip, venvs, pyenv, implementations (even for the JVM or iOS), CI, sphinx, readthedocs, wsgi, setup.py,,…), it’s moving, we can do quite a lot (awesome list), it has unique features but yeah, the ecosystem is tiny compared to clojure’s…

ps: interested ? http://lisp-lang.org/ !

lispm

tiny compared to clojure

In many ways is it much broader than Clojure, since there is much more choice. Interpreters, compilers, native code compilers, batch compilers, compilers targeting C/LLVM/JVM/ARM/ARM64/x86/x86-64/SPARC64/POWER/…

Clojure on the JVM uses a relatively simple and not very user-friendly compiler to the JVM. No Interpreter. No mixed use of interpreted and compiled code. Functions need to be declared before used. Error messages are exposing the underlying JVM. No TCO. No images. Slow startup.

The Roomba cleans your home with a CL program.

MagicMurderBagYT

No Interpreter.

Hol up. What about the REPL?

lispm

That’s not an interpreter. A REPL is not the same as a Lisp interpreter. REPL means read eval print loop. EVAL can be implemented by a compiler or an interpreter. Common Lisp has both and mixed implementations with both compiler and interpreter.

A Lisp interpreter is executing Lisp code directly. Clojure does not have an Interpreter.

https://clojure.org/reference/evaluation

Clojure has no interpreter.

Example in LispWorks, which uses the Interpreter in the REPL:

CL-USER 29 > (let ((f (lambda (a b)
                         (+ (prog1 2 (break))  ; we have a break here
                            (* a b)))))
              (funcall f 2 3))

Break.
  1 (continue) Return from break.
  2 (abort) Return to level 0.
  3 Return to top loop level 0.

Type :b for backtrace or :c <option number> to proceed.
Type :bug-form "<subject>" for a bug report template or :? for other options.

As you see Lisp comes with a sub-repl in the break. The sub-repl is just another repl, but in the context of the break. The break could be done by the debugger when it sees an error or by user code - as above.

Now we ask the interpreter for the current lambda expression:

CL-USER 30 : 1 > :lambda
(LAMBDA (A B) (+ (PROG1 2 (BREAK)) (* A B)))

Above is actually Lisp data. Code as data.

Now I’m changing the + function in the code to be expt, exponentiation. To be clear: I’m changing in the debugger the current executed Lisp function on the Lisp level. We take the third element of the list, and then the first one of that. This is the + symbol. We change it to be expt. * holds the last evaluation result of the REPL.

CL-USER 31 : 1 > (setf (first (third *)) 'expt)
EXPT

Then I’m restarting the current stack frame:

CL-USER 32 : 1 > :res

We get another break, which we just continue from:

Break.
  1 (continue) Return from break.
  2 (abort) Return to level 0.
  3 Return to top loop level 0.

Type :b for backtrace or :c <option number> to proceed.
Type :bug-form "<subject>" for a bug report template or :? for other options.

CL-USER 33 : 1 > :c 1
64                                   ; we computed 2^(2*3)  instead of 2+(2*3)

What did we see? We saw that the interpreter uses actual Lisp code. Lisp code we can change with Lisp code in the debugger.

A second example.

What can we do with that for debugging? Well, we can for example write our own evaluation tracer. The Evaluator prints each expression and its result nicely indented, while walking the expression tree and evaluating subexpressions. Remember: this is now user-level code. The example is from CLtL2. You will also see that LispWorks can freely mix compiled and interpreted functions. The function COMPILE takes a function name and compiles its Lisp code to machine code.

CL-USER 1 > (defvar *hooklevel* 0)
*HOOKLEVEL*

CL-USER 2 > (defun hook (x)
              (let ((*evalhook* 'eval-hook-function))
                (eval x)))
HOOK

CL-USER 3 > (compile 'hook)
HOOK
NIL
NIL

CL-USER 4 > (defun eval-hook-function (form &rest env)
              (let ((*hooklevel* (+ *hooklevel* 1)))
                (format *trace-output* "~%~V@TForm:  ~S"
                        (* *hooklevel* 2) form)
                (let ((values (multiple-value-list
                               (evalhook form
                                         #'eval-hook-function
                                         nil
                                         env))))
                  (format *trace-output* "~%~V@TValue:~{ ~S~}"
                          (* *hooklevel* 2) values)
                  (values-list values))))
EVAL-HOOK-FUNCTION

CL-USER 5 > (compile 'eval-hook-function)
EVAL-HOOK-FUNCTION
NIL
NIL

Now we can trace the evaluation of expressions on the Lisp level:

CL-USER 6 > (hook '(cons (floor *print-base* 2) 'b))

  Form:  (CONS (FLOOR *PRINT-BASE* 2) (QUOTE B))
    Form:  (FLOOR *PRINT-BASE* 2)
      Form:  *PRINT-BASE*
      Value: 10
      Form:  2
      Value: 2
    Value: 5 0
    Form:  (QUOTE B)
    Value: B
  Value: (5 . B)
(5 . B)

dzecniv

that’s an awesome example and tutorial that I’d love to see on a blog post or just a gist or something for further reference and better archiving, this will be buried too quickly on reddit !


So here it is.

Epilogue: the Roomba robot vacuums.