Lisp is not the most popular language. It never was. Other general purpose languages are more popular and ultimately can do everything that Lisp can (if Church and Turing are correct). They have more libraries and a larger user community than Lisp does. They are more likely to be installed on a machine than Lisp is.
Yet I prefer to program in Lisp. I keep a Lisp REPL open at all times, and I write prototypes and exploratory code in Lisp. Why do I do this? Lisp is easier to remember, has fewer limitations and hoops you have to jump through, has lower “friction” between my thoughts and my program, is easily customizable, and, frankly, more fun.
Lisp's dreaded Cambridge Polish notation is uniform and universal. I don't have to remember whether a form takes curly braces or square brackets or what the operator precedency is or some weird punctuated syntax that was invented for no good reason. It is (operator operands ...) for everything. Nothing to remember. I basically stopped noticing the parenthesis 40 years ago. I can indent how I please.
I program mostly functionally, and Lisp has three features that help out tremendously here. First, if you avoid side effects, it directly supports the substitution model. You can tell Lisp that when it sees this simple form, it can just replace it with that more complex one. Lisp isn't constantly pushing you into thinking imperatively. Second, since the syntax is uniform and doesn't depend on the context, you can refactor and move code around at will. Just move things in balanced parenthesis and you'll pretty much be ok.
Third, in most computer languages, you can abstract a specific value by
replacing it with a variable that names a value. But you can perform a further
abstraction by replacing a variable that names a quantity with a
function that computes a quantity. In functional programming, you often downplay
the distinction between a value and a function that produces that
value. After all, the difference is only one of time spent waiting for the
answer. In Lisp, you can change an expression that denotes an
object into an abtraction that computes an object by simply wrapping
a lambda
around it. It's less of a big deal these
days, but properly working lambda
expressions were only
available in Lisp until recently. Even so, lambda
expressions are generally pretty clumsy in other languages.
Functional programming focuses on functions (go figure!). These are the ideal black box abstraction: values go in, answer comes out. What happens inside? Who knows! Who cares! But you can plug little simple functions together and get bigger more complex functions. There is no limit on doing this. If you can frame your problem as "I have this, I want that", then you can code it as a functional program. It is true that functional programming takes a bit of practice to get used to, but it allows you to build complex systems out of very simple parts. Once you get the hang of it, you start seeing everything as a function. (This isn't a limitation. Church's lambda calculus is a model of computation based on functional composition.)
Lisp lets me try out new ideas as quickly as I can come up with them. New programs are indistinguishable from those built in to the language, so I can build upon them just as easily. Lisp's debugger means I don't have to stop everything and restart the world from scratch every time something goes wrong. Lisp's safe memory model means that bugs don't trash my workspace as I explore the problem.
The REPL in lisp evaluates expressions, which are the fundamental fragments of Lisp programs. You can type in part of a Lisp program and see what it does immediately. If it works, you can simply embed the expression in a larger program. Your program takes shape in real time as you explore the problem.
Lisp's dynamic typing gives you virtually automatic ad hoc polymorphism. If you write a program that calls +, it will work on any pair of objects that have a well-defined + operator. Now this can be a problem if you are cavalier about your types, but if you exercise a little discipline (like not defining + on combinations of strings and numbers, for example), and if you avoid automatic type coercion, then you can write very generic code that works on a superset of your data types. (Dynamic typing is a two-edged sword. It allows for fast prototyping, but it can hide bugs that would be caught at compile time in a statically typed language.)
Other languages may share some of these features, but Lisp has them all together. It is a language that was designed to be used as a tool for thinking about problems, and that is the fun part of programming.
Could you please elaborate the last part about polymorphism? What I do not understand is: you mentioning the + (operator). Even if you would define a function named `plus`, in CL it is not possible to automatically dispatch on argument type. You would have to write the dispatching manually (e.g. via `cond`) inside that `plus` function. Only if you use CLOS, generic functions, and methods, the dispatch table is implicitly generated for you. Other languages like C++ (not C!) can do that, I know that feature by the name of `operator overloading`.
ReplyDeleteOk, to be more specific.
DeleteAsk a C programmer to write factorial and you will likely get something like this (excuse the underbars, they are there because blogger doesn't format code in comments):
int factorial (int x) {
____if (x == 0)
________return 1;
____else
________return x * factorial (x - 1);
}
And the Lisp programmer will give you:
(defun factorial (x)
__(if (zerop x)
______1
______(* x (factorial (- x 1)))))
But now let's take the factorial of 54.0
The C program won't do it, it is defined on ints.
The Lisp program returns 2.308436973392414d71
The C program is narrowly defined on the data type that the programmer guessed would be sufficient.
The Lisp program is defined on any type that supports the zerop, *, and - operators.
Now it is true that if you mentioned this to the C programmer they could go back and re-write the program to handle bignums and floats (and rationals, and complexes, and matrices, and whatever weird number-like object that you send its way), but by default they don't do this.
Lisp programmers get this for free.
The C type system is pretty puny, though. In a language with something like typeclasses (e.g. Haskell or Rust) you can constrain the types to the categories you need, with something like
Deletefac :: (Eq a, Num a, Enum a) => a -> a
fac 0 = 1
fac n = n * fac (pred n)
and you can do `fac 54.0` and get `2.308436973392414e71` just fine, but going `fac "a string"` is a compilation error.
Rust _can_ do the same thing, but the constraint gets a lot more verbose and you _do_ have to import some stuff to get arbitrary precision. E.g. running `fac(54.0)` on
fn fac(x: T) -> T
where
____T: Clone + Copy + PartialEq + Sub + Mul + From,
{
____if x == T::from(0) {
________T::from(1)
____} else {
________x * fac(x - T::from(1))
____}
}
will net you an equally precise answer as your lisp example.
You can even add some constraints like making sure the input is non-negative so you don't get into an unending loop at `(factorial -1)`. Good type systems add a lot of power. :)
Why compare Lisp to C? Absolutely no one would argue that C is the nicer language to work with. You use C only when you need it for speed.
DeleteAlmost there with you. @andrew. Languages are tools. Not all tools are good for all tasks. So, writing an os in lisp would pose a number of problems to solve. Doing the same in C would pose much fewer issues. The other way around, likes, to write an inference engine.
DeleteThey _did_ write an OS in Lisp, @Uqbar. The Symbolics Lisp Machines (also referred to as LispM) had their heyday. Maybe now that AI is super popular again they'll come back? :^)
DeleteWhy "almost", @Uqbar? I think we agree entirely. Lisp and C could not more different use cases.
DeleteThe Rust example is missing a <T> in fn fac<T>(x: T) -> T. Guessing blogspot eats HTML-like stuff
ReplyDeleteWonderful write-up! The one thing I’ll add, and honestly I could hear it in your voice, is that Lisp is aesthetically beautiful. It is beautiful in its overt simplicity and appealing to the eye. It is beautiful in its homoiconicity, its innate form and function.
ReplyDeleteIt genuinely makes me happy working in the REPL.
The C# example has become pretty neat after the introduction of numerical constraints on generic type arguments:
ReplyDeleteT Factorial(T n) where T : INumber
=> n == T.One ? T.One : n * Factorial(n - T.One);
(+ < T > in the signature)
ReplyDelete(Trolling) So what I am hearing is
ReplyDelete"A sufficiently sophisticated static type system will allow you to express, with some verbosity, the constraints necessary for a compile time check equivalent to a naïve run time check."
In all seriousness, though, let us conside Euclid's algorithm:
(defun euclid (left right)
__(cond ((= left right) left)
________((> left right) (euclid (- left right) right)))
________(t (euclid left (- right left))))
Now you write it in your favorite statically typed language.
The Common Lisp version of Euclid's algorithm will work on any type that is a Euclidean ring, e.g. univariate polynomials. Does your version handle those?
My point is not that a dynamic type system is more expressive (it isn't), but that you get, without any effort, the more expansive interpretation of the types of the arguments. If you can live with the late-bound, run-time type checks, it is a sweet spot for prototyping. After you've figured out how to make it work, of course it is nice to cast the constraints in stone so that the compiler can make guarantees, and it is unfortunate that Lisp doesn't do that.
> The Common Lisp version of Euclid's algorithm will work on any type that is a Euclidean ring, e.g. univariate polynomials. Does your version handle those?
DeleteSeems easy enough. Haskell:
euclid left right = case left `compare` right of
EQ -> left
GT -> euclid (left - right) right
_ -> euclid left (right - left)
if we load it up with `cabal repl` and do `:t euclid` because I was too lazy to write up the types myself, we'll get
euclid :: (Ord t, Num t) => t -> t -> t
Rust again can do the same thing, but the lack of a GC means it's a bit more fussy
fn euclid(left: T, right: T) -> T
where
T: Sub + Ord + Copy,
{
match left.cmp(&right) {
Ordering::Equal => left,
Ordering::Greater => euclid(left - right, right),
_ => euclid(left, right - left),
}
}
(let's see if non-breaking spaces work for code)
I wonder if Wolfram Language might have replaced Lisp as the best language in which to get things done quickly.
ReplyDeleteEuclid in Haskell, and particularly in Rust is quite overwrought and much less expressive. Perhaps some of the problem is blogger.
ReplyDeleteReally? The Haskell version above looks pretty nice.
DeleteNow the question is: do you use emacs?
ReplyDeleteYou use lisp to configure it.
Author of the initial question here. I didn't expect so many comments. :)
ReplyDeleteI feel the urge to say: I really enjoy your blog, your frequent posts and I like Common LIsp myself.
I think I understand what you meant with the polymorphism paragraph. Though, I think CL lacks in the area of ad hoc polymorphism. You just can't do (+ string-1 string-2) or (+ set-list-1 set-list-2) or other valid use cases. Other languages already showed, that this is useful. And if one would define a `plus` function instead of +, one need to program all that type handling in that function. :(
"Other general purpose languages are more popular and ultimately can do everything that Lisp can (if Church and Turing are correct)."
ReplyDeleteSigh. I suppose this is what can be expected from someone so fuzzy in their thinking as to claim that Lisp programs don't have parentheses. Turing Completeness can be proven (or disproven). The Church-Turing Thesis, which is about whether Turing Completeness (or Church's lambda calculus) completely captures the notion of effective calculation, is not. Two different things aren't the same just because they both have the word "Turing" in them.
I certainly wouldn't read a blog from someone with such fuzzy thoughts. What were you thinking!
DeleteUqbar: "So, writing an os in lisp would pose a number of problems to solve. Doing the same in C would pose much fewer issues."
ReplyDeleteHow do you figure? The Linux kernel contains data structures (like linked lists and queues), a memory manager (including a GC), string formatting and I/O, loadable modules, method dispatch tables, and so on -- all of which are already built-in to every Lisp dialect I've ever used. A C-based OS has to re-implement all of these from scratch.
This is not simply a theoretical argument. We have proof by construction: there exist Lisp-based OSs. What problems, exactly, are you thinking of, which Lisp has but C doesn't?