6 minutes
LR: Why Lisp is horrible
This is a first post in LR category–Language Review–where I will factually opine on various programming languages. And today’s victim is Lisp.
Or first, let me say the emperor has no clothes, or Scheme is an awful language.
I mean, not only it is an awful first language to teach beginners, or to self-learn on your own, but also in general, as a programming language in 2023, it is not as great as it’s cracked up to be. I will not go through all the other arguments made by others. Those are all (still) valid. I will approach this from a different angle, and given this angle, my rant applies to both Scheme and all other Lisps, including Common Lisp and Racket.
And that angle is the simple and useful and elegant aspects of readability and features. Programs are meant to be read more often than written, and as a programmer, I like to visually “see” my program in a way that matches how my brain “sees” the abstractness of the program. In other words, a programming language I will use to learn something should match how my brain works, rather than to force my brain to become like that language. Additionally, I would want my language to have all the standard features I need, right out of the box.
Whether a language become popular in the industry is directly based on this, in my opinion.
In this post I will only mention my first beef (regarding visual appeal and readability and conciseness) with Lisps including Scheme. Interestingly, my beef is not a mere rant as it is backed by a heavyweight like Wadler (Wadler, 1987) where he uses a simple example to deride Lisp’s visual appeal:
(define (sum xs)
(if null? xs)
0
(+ (car xs) (sum cdr xs)))))
Phew!
All that ceremony to just do this:
(At the top right corner of this page, switch to dark mode in order to see the code-snippets more clearly.)
sum [] = 0
sum (x:xs) = x + sum xs
Wasn’t that much better?
And:
som [] = 0
som (x:xs) = x + som xs
Renaming sum
to som
here to avoid conflict with a Haskell primitive named sum
(Newbie tip: that is, Haskell already has a built-in function of this name.).
Exactly the same, right?
To fully illustrate the problem, let’s write a larger program to demonstrate how quickly you will get lost in the parenthetical jungle. We write a function that counts how many days have passed from a given date. If you don’t know Lisp, look at the other solutions below first, before coming back to the Lisp one. Look at this mess (and this is as compact as it can be written):
(define (leap-year? year)
(if (or (not (zero? (remainder year 4)))
(and (zero? (remainder year 100))
(not (zero? (remainder year 400)))))
#f
#t))
(define (days-since year month day)
(define month-days (list 31 28 31 30 31 30 31 31 30 31 30 31))
(let ((total-days (* (1- year) 365)))
(set! total-days (+ total-days (quotient (1- year) 4)))
(set! total-days (- total-days (quotient (1- year) 100)))
(set! total-days (+ total-days (quotient (1- year) 400)))
(do ((i 0 (add1 i)))
((= i (1- month)) total-days)
(set! total-days (+ total-days (list-ref month-days i))))
(if (and (leap-year? year) (> month 2))
(set! total-days (add1 total-days)))
(set! total-days (+ total-days day))
total-days))
That’s horrible. Imagine reading 20000 lines of this. No, thank you, but no means no.
Let’s see the same in the elegant language that I have grown to love. In Swift, it is simply a matter of:
func daysSince(year: Int, month: Int, day: Int) -> Int {
// Array to store days in each month
let monthDays = [31,28,31,30,31,30,31,31,30,31,30,31]
// Calculate the total number of days in the years before the given year
var totalDays = (year - 1) * 365
// Add one extra day for each leap year
totalDays += ((year - 1) / 4) - ((year - 1) / 100) + ((year - 1) / 400)
// Calculate the total number of days in the given year
for i in 0..<(month-1) {
totalDays += monthDays[i]
}
// Add one extra day if it's a leap year and we're past February
if month > 2 && ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0) {
totalDays += 1
}
// Add the number of days in the current month
totalDays += day
return totalDays
}
The annotations make it wordy, but the program logic is visually appealing and clear even without comments (which were added for the benefit of newbies reading this blog). Here is the un-annotated version:
func daysSince(year: Int, month: Int, day: Int) -> Int {
let monthDays = [31,28,31,30,31,30,31,31,30,31,30,31]
var totalDays = (year - 1) * 365
totalDays += ((year - 1) / 4) - ((year - 1) / 100) + ((year - 1) / 400)
for i in 0..<(month-1) {
totalDays += monthDays[i]
}
if month > 2 && ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0) {
totalDays += 1
}
totalDays += day
return totalDays
}
Actually, the clearest programming language is Hope, which is no longer alive, except it is, because Swift seems like modern Hope. Here is the same in Hope:
program DaysSince
function days-since(year: int, month: int, day: int) -> int
var totalDays: int;
let monthDays: vector<int> = [31,28,31,30,31,30,31,31,30,31,30,31];
func is-leap-year(_ year: int) -> bool
return (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
end
totalDays = (year - 1) * 365;
totalDays += (year - 1) / 4 - (year - 1) / 100 + (year - 1) / 400;
for i in 0..<(month - 1)
totalDays += monthDays[i];
if (is-leap-year(year) && month > 2)
totalDays += 1;
totalDays += day;
return totalDays;
end
It’s even clearer than Swift, but as it is a dead language, the second best choice is Swift. This is not to say Swift is the only coolest kid on the block. Let’s look at a few more examples, this one in F#:
let rec daysSince year month day =
let monthDays = [|31; 28; 31; 30; 31; 30; 31; 31; 30; 31; 30; 31|]
let isLeapYear year =
year % 4 = 0 && (year % 100 <> 0 || year % 400 = 0)
let numberOfLeapYears year =
year / 4 - year / 100 + year / 400
let mutable totalDays = (year - 1) * 365 + numberOfLeapYears year
for i in 0..month-2 do
totalDays <- totalDays + monthDays.[i]
if isLeapYear year && month > 2 then
totalDays <- totalDays + 1
totalDays + day
Pretty neat, although somewhat clanky. Scala also claims to be highly readable:
def daysSince(year: Int, month: Int, day: Int): Int = {
val monthDays = Array(31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
def isLeapYear(year: Int): Boolean =
year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)
val numberOfLeapYears = year / 4 - year / 100 + year / 400
var totalDays = (year - 1) * 365 + numberOfLeapYears
for (i <- 0 until month - 1)
totalDays += monthDays(i)
if (isLeapYear(year) && month > 2)
totalDays += 1
totalDays + day
}
That’s good enough for me, although I dislike its for
loop.
Anyway, give me Swift and Haskell any day, over any Lisp. Life is too short for parentheses hell.
1260 Words
2023-03-21 08:23