One of my resolutions for 2017 was to learn Lisp. I had recently learned Haskell and Elixir. Lisp was appealing because it was another type of functional language.
Learning Lisp was so helpful for me for the rest of the year, that I decided I wanted to share that experience with others so that maybe they will add Lisp to their resolution list for 2018.
The value of learning Lisp is in the ideas. I don't write any Lisp code. But I use the ideas from Lisp in my code all the time. So, for the new year, I have compiled a list of lessons that I have taken from learning Lisp. I hope this inspires someone else to learn Lisp in 2018!
Labels carry meaning. Labels are powerful. When you affix a label to something, you gain power over it. That is, you gain the power to talk about it. This is the idea of abstraction. When we create a class, we are creating an abstraction to talk about some piece of the system. But this is not where abstraction stops.
When you create a function, you are also creating an abstraction. In Java, classes are the main method for abstracting. In Lisp, the main method for abstracting is by using functions. This is one of the markers of a functional language.
In many programming languages, functions are first-class citizens. That means that functions are a type. They can be the input of a function and the can be the output of a function. Bonus points if your language supports anonymous functions, which many do at this point.
Lisp has functions that are first-class citizens. Lisp has the original anonymous function, called a lambda. The lambda is actually the same thing as a closure, which is an anonymous function that can capture it's environment.
This becomes particularly powerful when you use higher-order functions, like map, filter or fold. Lambdas are so powerful that the entire Lisp interpreter can be implemented using nothing but lambdas. Even the data structures!
Recursion is scary for many developers. There are several reasons why this could be the case. There could be a fear of creating an infinite loop. Maybe you don't like defining something in terms of itself. But there's nothing to be afraid of and you'll find that defining things in terms of themselves sounds natural, so long as certain preconditions are met.
Recursion consists of four parts: a base case, a recursive case, a condition and a reduction of the problem. Let's write some pseudocode for this:
function factorial(n) if n is less than or equal to 0 # condition 1 # base case otherwise n * factorial(n - 1) # recursive case, reduction of the problem
As a matter of fact, the Lisp code is not that different:
(define (factorial n) (if (<= n 0) 1 (* n (factorial (- n 1)))))
The condition determines whether our function ever stops. A condition that is always false will become an infinite loop. Here, our condition is guaranteed to be true at some point as long as our
n is a positive integer. The base case is what happens if the condition is true. Our base case is 1, which is the answer to
factorial(0). The recursive case is what happens if the condition is false. The recursive case should involve a reduction of the problem in terms of itself. Here,
factorial(n) becomes the subproblem
n * factorial(n - 1). The reduction here is very important. If we didn't subtract one from
n, then our condition would never be true. This, again, would cause an infinite loop.
So, to keep your recursive function from running forever you need a condition that is guaranteed to become true based on the initial values of the input and the method of reduction.
You could write a length function that reduces a string by each character until it becomes empty. You could write a sum function that reduces an array by each entry until it becomes empty. These are using the same recursive mechanism, just with different methods of reduction.
Keeping data immutable allows you to use that code in parallel with much fewer worries than if it relied on mutating values. What if the value changes before the loop in the other thread reads it? These problems go away with immutability. But, naturally, the question that follows is: "If we can't change values, then how do we change values?"
The answer is to construct a new value using the old value, rather than changing the old value. This is why
map returns a new list, not a modified one.
Global variables are discouraged in Lisp. Scope is limited by a
let function. This also allows us a way to mutate values within a certain context. Keeping data immutable in general and mutating within scope when necessary will help to keep your data consistent.
In many languages, you code using the constructs of the language directly. But in Lisp, we define a Domain-Specific Language. Then, we write our program in language we defined. This makes our code cleaner and more understandable. It makes it less likely for bugs to enter our code and easier to fix them should they arise.
I hope that you consider learning Lisp in 2018. It's a very worthy language to learn. You probably will not use it to build anything directly, but you will use the ideas that you learn from it to build better programs. Lisp challenges you to think in a new way and expand your horizons. No matter what language you use to code, the most important skill that you have is problem solving. Why not add more tools to your toolbelt?