When I was a kid, my favorite video game was Ultimate Mortal Kombat 3. When the game loaded, there was a quote:
There is no knowledge that is not power
I always found that quote interesting, so it stuck with me even though I didn’t fully understand it. It sounded cool 🙂
The original quote is from Ralph Waldo Emerson, who according to Wikipedia was an American essayist, lecturer, philosopher, abolitionist, and poet who led the Transcendentalist movement of the mid-19th century.
I’m not really sure what the context is for what Mr. Emerson wrote, but it certainly applies to programming. I’ve always been fascinated by programming languages, and how different languages provide different ways to solve problems, even to think about problems, which is the craziest part to me.
Even knowing a language that could be considered useless, because you’ll never use it for work or actual software projects, can teach you things that will actually be useful. I think that’s one of the things I like the most about programming in general. Everything is related, even things that seem completely specific to a particular problem or tool.
The idea behind this series is to show off a few weird programming languages, and some of the things we can learn from them.
Lisp
People say learning Lisp will make you a better programmer, and I agree. Of course, learning any language can help make you a better programmer, but Lisp is so different, it forces you to think a different way than you are used to.
Lisp is a little bit more of a set of standards than an implementation. I’d say one of the most popular versions of Lisp is Common Lisp. And one of the most popular implementations of Common Lisp is SBCL.
There are other languages which also call themselves Lisp besides Common Lisp, such as Scheme (which also has several implementations, with Racket being the most popular) and Clojure.
It’s certainly not easy to get started with Lisp, not only because there’s no single implementation, but also because it’s quite old, there isn’t much support and not many people use it. That said, a modern implementation like Clojure is much more welcoming, as it works on top of the JVM and the Java ecosystem.
Lisp is a functional language based on processing lists. A list is simply a bunch of elements wrapped in parentheses:
(+ 2 3)
The code above is Common Lisp for a list with 3 elements (or atoms): +, 2 and 3. When Lisp sees a list, it will process it by assuming the first atom is a function, and the rest are parameters to that function. In this case, the code above will apply the function + to the parameters 2 and 3, returning 5.
Notice that the above code is the same as doing 2 + 3 in most other languages. When the operator (+) comes first it’s called prefix notation, or Reverse Polish Notation (RPN).
RPN Advantages
The concept for Reverse Polish Notation was introduced by Polish logician Jan Łukasiewicz in the early 20th century. He developed a notation where operators preceded operands, known as Polish notation.
It looked like this: 1 2 +. Similar to Lisp, right?
Australian philosopher and computer scientist Charles L. Hamblin later reversed the order of operators and operands, creating RPN. Hewlett-Packard (HP) calculators in the 1970s and 1980s brought RPN to a wider audience. Their calculators, renowned for their efficiency and power, relied heavily on RPN.
One of the main advantages of this notation is that it can be represented with a stack, which is a very efficient and computer-friendly way.
This is why it was used in calculators and now in programming languages. A lot of modern virtual machines (such as the JVM, Ruby, CPython and Web Assembly) are stack-based.
Another advantage of the RPN is that we don’t need parentheses to know the order of operations. For example:
(3 + 4) × (5 + 6)
Becomes:
3 4 + 5 6 + ×
In Lisp, we inherit that quality. We don’t need to worry about the order of precedence, functions are just executed in order:
(* (+ 3 4) (+ 5 6))
Because we can dynamically define the order of precedence, we can also define our own “binary operators”, for example, we could create a function >> that will concatenate two numbers, and use it as any other operator:
(>> 2 (+ 3 1))
The above expression will return 24. Another nice property of this is that we can mentally execute this function by replacing the function call with the returned body. In Lisp, arguments are executed before the function so the first thing we have to do is replace the innermost arguments, and go from there up to the initial function call:
(>> 2 (+ 3 1))
(>> 2 4)
24
We replace (+ 3 1) first, because it’s an argument. Then we can replace the >> function, and end up with 24. We can also do this for more complex operations, such as the previous example:
(+ 2 (/ (* 3 4) 2))
(+ 2 (/ 12 2))
(+ 2 6)
8
We end up with 8. In most other languages, you can’t modify binary operators (eg: redefine what + does, or create your own), but in Lisp, there are no binary operators, just lists and list processing.
Homoiconicity
That’s a fancy word. Quoting Wikipedia:
In computer programming, homoiconicity (from the Greek words homo- meaning “the same” and icon meaning “representation”) is a property of some programming languages. A language is homoiconic if a program written in it can be manipulated as data using the language.
What that all means is that
- You have a program written in Lisp
- You can modify that program using Lisp
Consider the code below:
(defun square (x)
(* x x))
This will create a function named square, taking a single parameter (x) and it will return the square of the given parameter.
But if you look closely, this is still a list with four elements:
-
defun– a symbol representing the function definition keyword -
square– a symbol representing the function name -
(x)– a list representing the function’s parameter list -
(* x x)– a list representing the function body
To Lisp, that’s a function definition, because it knows that defun is what’s called a “special form”, meaning it won’t process it as it normally does (by calling a function), instead, it knows that we want to define a function.
But also, we can have access to this list, modify it, and then execute it in any way we want, or pass it to other functions. It’s just data, like an array in other programming languages.
Lisp was created with AI in mind, so this is a feature they were particularly interested in 🙂
Macros
One of Lisps most iconic features are macros. A macro is like a function, but instead of being executed at runtime, it executes at compile-time, and then whatever your macro returns replaces the actual macro call.
This is why it’s said that Lisp is a language that allows you to define your own language to solve your particular problem. You can create your own syntax rules!
To illustrate this, I will use JavaScript as I think it’s a much more common programming language to know and it’s also easier to understand.
Imagine you have a macro MAKE_PERSON that returns this code:
const param1 = {
name: param2,
likes: param3
}
Macros can receive parameters, and we can interpolate those parameters in the generated code. In the snippet above we use param1, param2 and param3. Now, using the macro would look like this:
MAKE_PERSON('person', 'Fede', 'cats');
console.log(person.name, 'likes', person.likes);
It might seem like a regular function call. But under the hood, before the code executes, it gets replaced to this:
const person = {
name: 'Fede',
likes: 'cats'
}
console.log(person.name, 'likes', person.likes);
Macros are like a shortcut that allows us to re-utilize the same snippets of code across different places. Because they are not function calls (evaluated at runtime), but rather inlined at compile time, they are quite performant. They can also be very useful for removing boilerplate and DRY up the code in ways it’s just impossible to do in other languages. It even allows you to create your own syntax for the language! For example, some minimal Lisps might not include a switch operator, but you can create it yourself using macros and if statements.
That being said, they can feel like a bit of black magic, making the codebase harder to read as you’ll need to learn what macros do in a particular program before you can understand what the program is doing.
REPL-based development
Another thing that makes Lisp unique is the heavy usage of the REPL, or READ-EVAL-PRINT-LOOP. Simply meaning that while you are writing code, you’ll have a terminal open somewhere (most likely in your editor) where you can call the function you just wrote, and see what it returns with different outputs.
You can compose several functions and make sure your program does what’s expected by just playing around at the REPL.
This is a bit like TDD but not really, because when you are in the REPL you are testing your functions but the REPL session is not saved anywhere, it’s just a place for you to try things.
That being said, you can always copy things from the REPL session into actual tests, and this kind of development is said to be quite solid and fast. Also, it does not replace tests, it’s just an easy way to sanity check your code as you go.
That’s it!
That’s it for Lisp, but if you liked this post, check out the following parts, where I’ll talk about languages such as SmallTalk, Forth and OCaml.
If you are interested in learning more about Lisp, I recommend learning Clojure, which would be the most usable and also it has good learning resources. If you don’t like the JVM, then CommonLisp with SBCL would be my recommendation, just because it’s one of the biggest implementations out there, making it easier for you to get help.
