I try to avoid languages that are oriented around the REPL. That is, anything that is not compiled but interpretted. The equation didn’t make any sense to me at first: Why should I use a language that adds X amount of delay to the start of my production executable? What about shipping software and finding the right version of the interpretter? Why should I provide /dev/stdin when I will not use it? On and on, the arguments I had against REPL were solid.

Keyword “were” because after using Common Lisp, I’m sold.

A meme about my transition

Now, Common Lisp’s depth is something that no amateur blog writer can do justice to. Even the best of writers cannot encompass it, one should experience Common Lisp to grasp its depth.

I’ll try to illustrate each reason why Common Lisp’s REPL is simply built different and how these features work even when Common Lisp is compiled.

macros

Macros in Common Lisp are functions that return syntax that is evaluated. These macros are only different in that aspect only, meaning:

  1. Macros can call other macros
  2. Macros can call any functions
  3. Macros have access to all variables, data structures, classes and other things.

Therefore, these macros have much more room to play with and can be a lot more powerful than, for example, C’s macros. How powerful? Very very powerful. Let’s do some simple illustration with the languages with the simplest syntax, Golang.

func verboseFunction() error {
	result, err := failableFunction()
	if err != nil {
		return err
	}

	result2, err := failableFunction2(result)
	if err != nil {
		return err
	}

	result3, err := failableFunction3(result)
	if err != nil {
		return err
	}

	return nil
}

Very verbose, right? This is because Go cannot try/catch errors like other languages. If Go had the power of Common Lisp’s macros, something like this would be possible:

func quietFunction() error {
	withErrorMacro(func() error {
		result := failableFunction()
		result2 := failableFunction2(result)
		result3 := failableFunction3(result)

		return nil
	})
}

Then, withErrorMacro would return contents of equivalence to verboseFunction. You might ask, what about non failable functions? Common Lisp’s answer is that you can examine any function’s contents, arguments and return values(with their types). Therefore, you could examine which functions are failable and return them accordingly.

Furthermore, you could examine and transform the syntax to strings to do some really cool things:

func quietFunction() error {
	withErrorMacro(func() error {
		result := failableFunction()
		result2 := failableFunction2(result)
		result3 := failableFunction3(result)

		return nil
	})
}
// would return this
func verboseFunction() error {
	result, err := failableFunction()
	if err != nil {
		return fmt.Errorf("failableFunction: %w", err)
	}

	result2, err := failableFunction2(result)
	if err != nil {
		return fmt.Errorf("failableFunction2: %w", err)
	}

	result3, err := failableFunction3(result)
	if err != nil {
		return fmt.Errorf("failableFunction3: %w", err)
	}

	return nil
}

Because Common Lisp’s internal data type is very loose, one can transform symbols(the name of a variable or function) into strings and automate returning customized errors. Also that’s without having to write that verbose function not even once. Therefore, any function can silence its statements.

The effect of this is very subtle but extremely profound because the user(a Common Lisp developer) essentially changes the mold of the language as they use it.

A potter can mold clay into shapes that a carpetner can only dream of.

To illustrate further, here’s an example for one of my closed source projects:

(defun /user/settings (env)
  (with-user
    (->page (fmt "My Project - ~a: Settings" (user-name *user*)) ""
      `((h1 "Settings")
        (form :method "post" :action "/user/settings"
          (input :type "text" :value ,(user-name *user*))
          (button "Save"))))))

It is implicit that with-user, user-name and *user* are defined in the above code. That is because with-user is a macro that wraps the body in a function that fetches the user from the database by reading the user cookie and defines it temporarily in *user* and user-name is a function that reads the user name from the user.

The difference between this feature and other features that are implemented by language developers is although it is implicit, it is the user that created it which makes him/her aware of it. While some users of other languages maybe puzzled of why some syntax is buggy, a Common Lisp user doesn’t because they added that syntactic themselves.

If that’s not enough, consider that Common Lisp allows you to see the result of a macro before evaluating it. This is only possible under a REPL. As mentioned before, this strengths the experimentation aspect of Common Lisp and comes in handy often. Read MACROEXPAND for more information.

reader-syntax

What’s more concise?

type Measurement struct {
	value float64
	name string
}

func createMeasurement() Measurement {
	return Measurement{1.45, "kg"}
}

func createMeasurement2() Measurement {
	return 1.45kg
}

It doesn’t matter which one you pick in Common Lisp because you are able choose between either. That is, you can modify the syntax, unmodify it, to your liking. While doing it, you still have access to the environment so you can call macros, functions or read variables.

How’s this useful? I can imagine a multitude of ways: String interpolation, reading formats like YAML, HTML or JSON, creating shortcuts for classes and data structures.

Once a user modifies the reader macros, they can attain the values that they defined to be read through different functions such as READ or READ-FROM-STRING.

<CL-USER> (read-from-string "1.45kg")
;; #<MEASUREMENT {1002B3B7D3}>
;; It returns an instance of the class MEASUREMENT
<CL-USER> 1.45KG
;; #<MEASUREMENT {1002B3B7DA}>

By the way, I actually implemented such a syntax in Common Lisp. It took about an hour and it was worth it.

the debugger

Whenever an error happens in Common Lisp, you don’t see the generic Error Report but the Common Lisp Debugger.

Common Lisp Debugger

The Common Lisp Debugger is a debugger implemented in Common Lisp Fashion. That means you can:

  • Unknown variables can be defined temporarily as a response to the error
  • Return any other value if you want to
  • Provide the user with other programmed responses
  • Retry the form
  • Find out where the error occurred at and in which form
  • Or just ignore the error…

This is just different to other implementations. When using a compiled language, one has to put debug printf across the functions. When errors happen, they just happen and cannot be overriden unless previously programmed.

In Common Lisp, you get a good amount of default options to deal with errors. If the error is of type unbound-variable, you can return a specific value for that variable, define a global variable and use it or return a result all-together from the expression.

You could also just retry that expression again. Also, where the error occured is printed and at which Line/Column. Furthermore, one can define custom options to deal with certain errors.

constant experimentation: the common denominator

The Common Denominator between all these features is constant experimentation. One does not plan in Common Lisp but implement and re-iterate; the effect of the REPL is so deep, it changes the development process.

There are other facets to the Common Lisp REPL that I haven’t covered but only because I cannot summarize them with words. Try out the language if you to discover them.

common lisp compilation

A cool part of Common Lisp’s REPL is its dynamic nature. To the point where a Common Lisp user can literally compile parts of the program they’re writing through running a function called COMPILE.

;; this function is interpretted
(defun my-function ()
  'some-result)
;; now, it is compiled
(compile 'my-function)
;; running this function returns whether a function is compiled or not
(compile-function-p #'my-function) ;; returns true

What does it mean when a function is compiled in Common Lisp? That it is reduced down to either byte code or assembly in the case of some implementations.

Compiled code is always faster than normal code. Heck, one can even inspect the assembly using DISASSEMBLE.

(disassemble 'my-function)

Which returns

; disassembly for MY-FUNCTION
; Size: 23 bytes. Origin: #x535E96FB                          ; MY-FUNCTION
; 6FB:       498B4510         MOV RAX, [R13+16]               ; thread.binding-stack-pointer
; 6FF:       488945F8         MOV [RBP-8], RAX
; 703:       488B15C6FFFFFF   MOV RDX, [RIP-58]               ; 'SOME-RESULT
; 70A:       488BE5           MOV RSP, RBP
; 70D:       F8               CLC
; 70E:       5D               POP RBP
; 70F:       C3               RET
; 710:       CC10             INT3 16                         ; Invalid argument count trap
NIL

I actually have used this for the css-lexer I wrote for a recent project. It was a good way to measure how complex my functions and what optimizations I am missing.

You could also compile entire files to a .fasl file and compile a Common Lisp program to a single executable.