The Perils of Lisp in Production

I wrote an article a few months ago about Lisp in production environments, specifically Common Lisp. And I still stand by it. However, I have a few comments on the other side of the coin. What are the dangers of using Lisp in production? Here is my opinion as a result of my experience with programming in Lisp in a production environment on non-trivial code.

Types

Right off, I can say that the biggest, most glaring thing that causes problems is the lack of static typing. It happens that when the compiler is unable to detect these issues, the end user is left to find them. This can be very potentially damaging to companies. And it is more prominent when you have 100k lines of code, not all of which one can be familiar with.

Lisp takes some steps to help solve these issues. SBCL has very good type inference capabilities, and can warn or error at compile time with type issues. Additionally, Common Lisp allows one to declare and assert argument types, but there’s no guarantee this information can be used. For example, suppose we have the function

(defun hello (x)
  (declare (type fixnum x))
  (1+ x))

which gets compiled with COMPILE. Then follow with
(compile nil (lambda () (hello "world")))

and we will receive the following from CMU Common Lisp:
; Compiling LAMBDA NIL: 

; In: LAMBDA NIL

;   (HELLO "world")
; Warning: This is not a (VALUES &OPTIONAL FIXNUM &REST T):
;   "world"

But note, this checking is not guaranteed.

Compilers for Standard ML have the huge benefit of type checking. If you have something like a case form dispatching on the different clauses of an algebraic data type, and you happen to miss one of the clauses by accident, SML can warn you. Type mismatches are compile-time errors.

In light of the issue, however, I do not assert that a static type system is better. In fact, I think quite the opposite. Static types are hard (let’s go shopping!). Actually, we can’t do much better than what we have now. Hindley-Milner is sort of at a local optimum; the code is rather simple but the type checks are rather powerful. But who is to say we can’t do better, even if the code is more advanced? Well, we can. Haskell implements a system that is richer than HM. ATS/Agda/etc. implement systems stronger than Haskell. But we have reached diminishing returns with types. We are taking smaller and smaller steps, converging to a point which we are unable to go past.

But, all of that is speaking formally. Informally, type systems can take giant leaps. Let me demonstrate what I mean.

Consider the function $f$ defined by
\begin{align*}
\left[-\tfrac{1}{2},\tfrac{1}{2}\right] &\to S\\
x &\mapsto \frac{1}{\sqrt{1-x^2}}.
\end{align*}
Let’s try to reason what $S$ is. Well, we know $-\tfrac{1}{2}\le x\le \tfrac{1}{2}$. Squaring that (to get $x_1$) means $0 \le x_1 \le \tfrac{1}{4}$. \[x_1 \mapsto \frac{1}{\sqrt{1-x_1}}\] Subtracting that from $1$ gives $\tfrac{3}{4} \le x_2 \le 1$. \[x_2\mapsto\frac{1}{\sqrt{x_2}}\] Square root of that gives $\tfrac{\sqrt{3}}{2} \le x_3 \le 1$. \[x_3\mapsto\frac{1}{x_3}\] Taking the reciprocal gives $1 \le x_4 \le \tfrac{2}{\sqrt{3}}$ or equivalently $1 \le x_4 \le \tfrac{2}{3}\sqrt{3}$. \[x_4\mapsto x_4\] Since we have reached something equivalent to the identity function, we have \[S=\left[1, \tfrac{2}{3}\sqrt{3}\right]\approx [1, 1.1547005].\]

Now, let’s define this function in Lisp.

(defun f (x)
  (declare (type (single-float -0.5 0.5) x))
  (/ (sqrt (- 1 (* x x)))))

Great. Cursory inspection of f tells us
> (type-of #'f)
FUNCTION

Not very impressive. It can’t even tell us the input type? Haskell blows Lisp out of the water here.

Or does it? Let’s dig deeper.

> (describe #'f)

# is a function.
Arguments:
  (x)
Its defined argument types are:
  ((SINGLE-FLOAT -0.5 0.5))
Its result type is:
  (SINGLE-FLOAT 1.0 1.1547005)

This means the input type is a single-precision real number that is between $-0.5$ and $0.5$, and the result is a single-precision real number between $1.0$ and approximately $\tfrac{2}{3}\sqrt{3}$. Coincidence?

In this case, CMUCL did better than any statically typed system that I know of. And it also shows why I think static types aren’t the right way: this kind of system can’t be formalized to be decidable for every function at compile time. But I believe just because we can’t do everything, doesn’t mean we can’t do anything.

However, all in all, the lack of static typing in Lisp does give statically typed languages an advantage, especially for big, commercial software systems.

Programmers

Now, there are indeed a great deal of Lisp programmers. Many of them at varying degrees of familiarity about the Lisp language, and about programming itself. I don’t think we have any shortage of them.

I do think, though, that for a Lisp working environment to be a success, one of two things need to happen:

  1. The Lisp programmers need to know the Lisp language very well, and general software engineering very well.
  2. The Lisp programmers need a very good manager that fits the first item.

Surely this is bad news for recruiters. There is less supply. These kinds of programmers aren’t as replaceable as your average Java undergrad is. And perhaps it is bad news, but let me rationalize.

Lisp is very dynamic. I mean very, very dynamic. Pieces of the language can be reinvented to fit a suitable task. New paradigms can be introduced to the language without any difficulty. The language itself is already chock-full of different paradigms: imperative, functional, object-oriented, reflective, etc. Some Lisps (but not by standard) even come with a Prolog-like mechanism built in, adding “logical”, “declarative”, and “non-deterministic” to the list.

I believe a good Lisp programmer will take advantage of all of these paradigms, and use others. Most average programmers are either average or very good at a single paradigm, or are mediocre with a bunch of paradigms. And, to gain knowledge of other paradigms, it is essential to use other languages. One can easily learn the basics of imperative, object-oriented, and functional style in Lisp alone. But true understanding of each will probably come more aptly from languages like C, Smalltalk, or Standard ML. A lot of programmers are focused on “getting the job done”. Once comfortable with one language, they stick to it (or learn very similar languages: C, C++, C#, Java is one class of examples).

As a result, it’s hard to find someone who is both good at Lisp and at least has more than an hour’s worth of experience in other languages—in turn making him or her more productive in Lisp itself.

Now, even if such programmers can’t be found, there’s an alternative. Namely, #2 above. An employer might hire some Lisp programmers who might not be excellent at the craft, but are at least willing to learn. A good mentor can be a much more powerful force on someone than someone’s own autodidactic drive.

Lisp Code

Sloppy Lisp code is hard to maintain.

“But that’s true anywhere.”

But it’s especially true with Lisp. In Lisp, it is easy to construct a mountain sitting atop cobblestones and sticks. One of the most prized aspects of Lisp is its ability to handle rapid prototyping.

Perhaps this argument fits in with the argument above. However, even very good programmers can be sloppy. Especially under the pressure of short deadlines and a plethora of other tasks from their employers.

An example of my own. I built a system for printing data on (physical, paper) forms. The actual piece of data getting printed was eventually a string or a vector of image data. Me, attempting to follow the KISS philosophy, decided those two data types would be sufficient.

Then a customer wanted centering. So I decided to use a tag to describe this: (:center . "data to print"). Still fine. Now the customer wants check marks. Good, this fits into the scheme fine: (:checkbox . t). All of this happened in a short period of time, and it was fine, and it was well built. However, eight months later, people wanted to be able to print right-justified strings, word-wrapped strings, barcodes (this was fun!), etc. And being eight months later, I added the stuff on—more like bolted on—due to short deadlines. Yet, it had to all fit a cohesive model. And indeed it does, but it is not extensible, and bolting stuff on gets harder and harder. And it has turned into a maintainability nightmare.

In hindsight, given what I know now, I would have implemented the system very differently. I would have made classes of printable objects, etc. But, with my initial knowledge, that would have been overkill—lousy over-generalization.

But in Java, this kind of thing probably wouldn’t have happened. Java gets criticized for its heaping amounts of over-generalization: defining factories and abstract factories and abstract factory factories. In doing this, the expensive is simple code and perhaps time. But it does—given a good programmer—avoid the issue outlined above.

Conclusion

Programming in Lisp is a fantastic choice for “enterprise development”, but it does come with perils. Type errors can be hard to find, good programmers can be hard to find, and code which once made sense for a certain problem can be turned into a maintainability nightmare in a production environment.

Don’t let this deter companies from using Lisp. Rather, let it be a sign of some of the “dangerous bends” when choosing Lisp.

8 comments to The Perils of Lisp in Production

  • I believe you are wrong about the code maintainability… Lisp let’s you do the right thing, construct the code quickly. That doesn’t mean you can ignore that technical debt. You have to pay for it eventually. The great thing about Lisp is that when you pay the technical debt you’ll still be quicker than the Java hacker. Of course, you have to be able to say “hey client, there is this technical debt we made here… we have to pay for that at some point” and I don’t think Lisp programmers have been very good at that.

    • That is a good way to think about it.

      I admit that perhaps my view on Lisp potentially being unmaintainable might be a result of the style of management in the Lisp work I’ve done, and not so much in the Lisp itself. That is, the “forge ahead, get new features in, if it ain’t broke don’t fix it”-style some managers have.

      Anyway, thanks for the comment!

  • Regarding “But it’s especially true with Lisp.” – I’d be interested in hearing why you think that. I understand the point you make about type systems, but what about other languages with dynamic types? Also, the kinds of issues in your example can easily happen in Java – there’s nothing in that language forcing you to create abstractions any more than there is in Lisps. Seems like in either case, knowing when, what, and how to abstract takes thought about things that should be easy to change.

    • I reckon this will be harder to explain than I initially anticipated.

      For the most part, I actually don’t disagree. You can have sloppy abstractions in Java just as in Lisp. There is nothing that stops you in Java from not abstracting well.

      I do think, however, that Java is built up on the principles of abstracting with classes. The first thing you do when writing a Java program is often defining the class holding your main method. Things like design patterns, methods of abstraction, and related practices are closely related to Java.

      Lisp, on the other hand, and in my experience, takes a different approach. It is simply easy to get something working. I find a very prominent qualitative aspect about Lisp that allows for very sloppy code, and is probably a reason why it is called a “hacker’s language” (not to say hackers write sloppy code!). When time is the most important metric for success (e.g., “we need this huge project done by tomorrow morning”), in Lisp, there likely won’t be a problem getting it done. I attribute that to the fact that it’s very easy to just skip past almost any form of data structural abstraction and just use raw structures provided in the language. For example, as a result of the ease in making cons cells in Lisp, that’s often a quick and easy data structure choice. Combine that with first-class symbols. Now you have symbols to give things identity, and conses to combine things. Additionally, Lisp provides native functionality for working on these kinds of structures, like member, assoc, union, etc.

      This combination of data structures makes it easier, I’d argue, than the equivalent in Java. Reading, destructuring, writing/serializing, and editing lists and symbols is easy and fast in Lisp. Furthermore, writing thin abstractions over these is easy.

      On top of that, having enforced return types and argument types is something Lisp simply doesn’t have.

      It may be true that these aspects are true about any dynamic language. However, I don’t think I can fairly speak about those since I haven’t used them in the same way I’ve use Lisp in a production environment.

      All of this is not to say that Lisp is not suitable for abstractions. Quite the contrary. Lisp is extremely suitable for developing powerful, simple, and/or complex abstractions. In a perfect world, every programmer would spend time writing a program with suitable abstractions. Pragmatically, however, I have observed that this is not the case.

  • Your story brings memories about my own job. I had so little specs about what I should implement that there was a great amount of guesswork (because in fact, users themselves have no clue what they need).

    I found that certain Lisp features allowed me to move on without rewriting everything, and the project is still maintenable—it certainly will be difficult to hand it over to a developer who doesn’t know my 50K lines of Lisp/JavaScript/HTML/CSS/Custom template language, but without Lisp this project would simply not exist.

    So.. I’m not sure you’re right here. If I had more time (and incentives) I could clean up the messy parts, but nobody cares about the code mess as long as it works.

    Java maybe works when your target is clearly defined, but when lots of guesswork is involved, my experience is that Lisp wins.

    Of course, management has a problem now, but it’s not because of me or my code. It’s because they fail the second point that you mentioned:

    “2. The Lisp programmers need a very good manager that fits the first item.”

    The manager who hired me, a serious hacker himself, no longer works here. That’s the end of the story.

    • I worked for a start-up that had some very good programmers, but one of the first programmers–one of the programmers I interviewed with, even–left before I was hired; the other head programmer had decided to move on. And this was at a time when the start-up just got some funding, and was thus trying to expand!

      The company struggled; using Java for the backend, and JavaScript for the front end, wasn’t exactly saving them. They decided to switch to Ruby for the front end just before I left, which I think was a good idea–but they also just barely decided they needed a good HTML guy, since the only person good at HTML was the one leaving the company. Somehow, I don’t think the company would have been in a good position, even if they had started everything in Common Lisp, all because of problems with “2. The need of a good manager.”

      I was let go after four months working there, so I don’t know how they are doing now, but I hope they are making progress towards their goals; I have confidence that the programmers can pull together and do this, even if the company itself was in a dysfunctional situation! But it’s a difficult position to be in, and I don’t envy them.

  • “In this case, CMUCL did better than any statically typed system that I know of. And it also shows why I think static types aren’t the right way: this kind of system can’t be formalized to be decidable for every function at compile time. But I believe just because we can’t do everything, doesn’t mean we can’t do anything.”

    That is a static type. You detected it statically, and it’s a type. So this whole paragraph doesn’t mean anything. Whether it’s a “describe”, a warning or an error, it’s a static type.

    But I think I understand the point you were trying to make which is that there are more kinds of static checks than type checks. Variable existance, name shadowing, non-termination, optimization opportunities, things like that. John Carmack echoes this sentiment in as many words.

    • I could and maybe should rephrase the paragraph to say “fixed, mechanically provable static type systems arent the right way:…”. I don’t mean to say static types as an atomic concept are wrong, but a limited system of static types is wrong.

Leave a Reply

  

  

  

You can use these HTML tags

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Before you post, please prove you are sentient.

what is 3 + 5?