I think this writeup underplays the extent to which both approaches have had their partisans even within the Lisp tradition. The author’s example of a characteristic Common Lisp function, remove-if-not, is actually deprecated (see X3J13 Cleanup Issues TEST-NOT-IF-NOT and FUNCTION-COMPOSITION) primarily because the spec’s authors felt that function composition was a better way to achieve the same ends. Likewise, loop has always been considered “un-Lispy,” and I don’t think it’s wise to extrapolate from its quirks to the language in general (except inasmuch as loop, like format, exemplifies (Common) Lisp’s willingness to traffic in un-Lispy DSLs when it seems useful to do so).
There is certainly a strain of Common Lisp practice which heavily favors configurability (the canonical example for me is the http-request function in Edi Weitz’s widely-used Drakma library, which supports no fewer than thirty-seven keyword arguments modifying its behavior). But there’s also always been a strain favoring function composition, and in Lisps with more functional programming influence (e.g. Clojure), the function-composition style is arguably dominant, certainly for the kind of sequence manipulation shown in the examples.
To compliment your second parragraph this is a library from a lisper that prefers function composition.
Also agreed, but while reading this it occurred to me that in more “modern” lisps … well in clojure, anyway … there seems to be more of a tendency to have smaller, composable functions, more like the Haskell examples the author gave. Maybe clojure started out with lessons learned from decades of lisp, or swiss-army-knife functions had something to do with performance of machines decades ago, I’m not sure. But I sure do prefer using small composable functions and it’s one of my favorite things about Haskell.
[Comment removed by author]
Thats what I thought as well, those lisp examples looked nothing like what I read in sicp.
Agreed, this is an interesting and important difference between the languages. It was certainly a deliberate choice by both, although it’s worth noting that for Haskell to imitate the Lisp style would be tedious since the language has no equivalent to CL’s keyword parameters.
I appreciate the author not trying to draw further conclusions from this point, which would certainly have weakened it. It’s hard to say whether either is better in some absolute sense, but they’re different choices.
This difference exists but I’m not sure that I’d attribute it to specific languages. Also, Unix is arguably closer to the Lisp design insofar as its (numerous) optional command-line arguments are essentially the same concept of keyword arguments, which are hard to implement in Haskell. (There’s a pattern for it, which is to use record update syntax with a default value, passing in one record argument, but it’s fairly clunky and rarely used.) Also, languages are converging. The Haskell worldview is great for building infrastructure, while the one attributed to Lisp is better for user experience. (Swiss army knives have great UX but are poor infrastructure, because you want foundations to be simple.) Haskell is improving its UX and tackling more front-end/web programs, and Clojure is taking tips from Haskell on how to make reliable infrastructure. Both languages can be used well toward either sort of purpose, and I’d generally cite them as positive examples because, in fact, one needs both UX and infrastructural integrity.
Negative examples at the extremes would be R and Java. R has a lot of libraries and features, but the infrastructure and design of the language are poor. It’s a great UX for data science, but a terrible language for infrastructure. Java, at the other extreme, is designed as a language where everything is a class (i.e. infrastructure) and the UX and culture are awful: programming in the language effectively mandates an IDE. People forget that they can write programs and just write Visitors and Factories that are so far from the problem they’re supposed to solve that you get 21st-century spaghetti code. Because the language is so ugly and uninspiring, and because there are so many terrible patterns that have become standard Java, even the infrastructure itself isn’t as good as one might think (the fact that Haskell “doesn’t have JVM” is not as severe as it’s made out to be by the enterprise). So those would be examples of what to avoid, but the good news is that modern language communities (e.g. Clojure, Haskell) recognize both need sets as valuable (and not after several years of irreversible, core, design decisions have been made).
This seems to be a fundamental distinction between imperative and functional code. For what it’s worth, Clojure programs tend to be a blend of the two styles depending on the problem domain. I’m not using the word “imperative” to mean bad, in fact, I think that there’s a lot of problems out there for which imperative solutions are ideal. However, imperative programs and problems tend to prefer parameterization, where as functional programs and problems tend to prefer decomposition. Choosing the right approach can lead to orders of magnitude simpler programs, in either direction, with either style, in either category of language, with some variable degree of swimming up stream. It’s also worth noting that the functional style tends to produce more reusable code, which is why Unix embeds imperative programs in to the reusable functional piping process. It’s doubly worth noting that “reusable” isn’t always a good thing either.
Copying my comment from HN here:
Some of the Lisp and Haskell code examples aren’t doing the same thing, or what his text description describes. For example, these two do completely different things:
(remove-if-not #'p xs :count 5 :start 3)
take 5 . filter p . drop 3
One way of writing the Haskell version in CL would be:
(loop for x in (subseq xs 3)
when (p x) collect x into result
until (= (length result) 5)
finally (return result))
Another option would be to use subseq and remove-if-not, etc, but without lazy evaluation, the loop version will be more efficient. And though some Lisp people dislike LOOP, I like that it’s easy to read, if not always easy to write ;-)
About the topic of the article, though, to me this seems like less a philosophical difference than a result of Haskell not having easy to use default and optional parameters. There’s currying, but it’s not a great substitute, and it’s a little awkward to use.
I like the Common Lisp way, even if it’s crufty at times, because the keyword arguments to functions like remove-if-not and sort are easier for me to use than chaining a half dozen functions. I don’t have to think about whether I need to call filter before or after take or drop, etc. At the end of the day, it’s a personal preference, though.
Another advantage is that I don’t have to “roll my own” for common idioms.
I thought function composition in Lisps was take(filter(drop(x, 3), p),5). I agree it’s less readable, but it’s more like Math.
I don’t have a lot of experience with common lisp, but the first
example seems weird to me as someone who’s started looking at
Clojure. I don’t recognise it at all. A straight-forward
implementation of this function:
take all elements from the list–except the first three–that satisfy
predicate p, and take only the first five of those
Would look like this:
(defn f [p coll]
(take 5 (filter p (drop 3 coll))))
However, this would probably be more idiomatic to rewrite it using the
macro. This has the benefit that the wording in the spec matches the code
(defn f'[p coll]
I’m not sure what’s going on with the second problem:
get all elements greater than 5, then just the even ones of that set.
It looks to me like neither his lisp nor his Haskell solution works, as they
both get only the even numbers below 5. So, I’ll show both. I would solve
the problem as stated in Clojure like this:
(filter even? (iterate inc 5))
Rewritten with ->>:
(->> (iterate inc 5)
The problem that his code examples solve I would do like this:
Ok, that was cheeky, so let’s show it with code too:
(filter even? (range 5))
or, if 0 is not desired:
(filter even? (range 1 5))
Range has a step option too, but I’d hardly call it a kitchen sink function:
user> (doc range)
( [end] [start end] [start end step])
Returns a lazy seq of nums from start (inclusive) to end
(exclusive), by step, where start defaults to 0, step to 1, and end to
infinity. When step is equal to 0, returns an infinite sequence of
start. When start is equal to end, returns empty list.
;; => nil
So we could solve this like this too:
(range 2 5 2)