Why I Program in Lisp
278 comments
·April 11, 2025discmonkey
eadmund
> It's not the parens persay, it's the fact that I'm used to reading up to down and left to right. Lisp without something like the clojure macro ->, means that I am reading from right to left, bottom to top - from inside out.
I’m not certain how true that really is. This:
foo(bar(x), quux(y), z);
looks pretty much identical to: (foo (bar x) (quux y) z)
And of course if you want to assign them all to variables: int bar_x = bar(x);
char quux_y = quux(y);
return foo(bar_x, quux_y, z);
is pretty much the same as: (let ((bar-x (bar x))
(quux-y (quux y)))
(foo bar-x quux-y z))
FWIW, ‘per se’ comes from the Latin for ‘by itself.’AdieuToLogic
One of the awesome things about LISP is it encourages a developer to think of programs as an AST[0].
One of the things that sucks about LISP is - master it and every programming language is nothing more than an AST[0].
:-D
almostgotcaught
> encourages a developer to think of programs as an AST
can you imagine saying something like
> The fradlis language encourages your average reader to think of essays as syntax [instead of content].
and thinking it reflects well on the language................
all2
The lisp is harder to read, for me. The first double paren is confusing.
(let (bar-x (bar x))
(quux-y (quux y)))
(foo bar-x quux-y z)
Why is the second set of parens necessary?The nesting makes sense to an interpreter, I'm sure, but it doesn't make sense to me.
Is each top-level set of parens a 'statement' that executes? Or does everything have to be embedded in a single list?
This is all semantics, but for my python-addled brain these are the things I get stuck on.
kazinator
The let construct in Common Lisp and Scheme supports imperative programming, meaning that you have this:
(let variable-bindings statment1 statement2 ... statementN)
If statementN is reached and evaluates to completion, then its value(s) will be the result value(s) of let.The variable-bindings occupy one argument position in let. This argument position has to be a list, so we can have multiple variables:
(let (...) ...)
Within the list we have about two design choices: just interleave the variables and their initializing expressions: (let (var1 value1
var2 value2
var3 value3)
...)
Or pair them together: (let ((var1 value1)
(var2 value2)
(var3 value3)
...)
There is some value in pairing them together in that if something is missing, you know what. Like where is the error here? (let (a b c d e) ...)
we can't tell at a glance which variable is missing its initializer.Another aspect to this is that Common Lisp allows a variable binding to be expressed in three ways:
var
(var)
(var init-form)
For instance (let (i j k (l) (m 9)) ...)
binds i, j and k to an initial value of nil, and m to 9.Interleaved vars and initforms would make initforms mandatory. Which is not a bad thing.
Now suppose we have a form of let which evaluates only one expression (let variable-bindings expr), which is mandatory. Then there is no ambiguity; we know that the last item is the expr, and everything before that is variables. We can contemplate the following syntax:
(let a 2 b 3 (+ a b)) -> 5
This is doable with a macro. If you would prefer to write your Lisp code like this, you can have that today and never look back. (Just don't call it let; pick another name like le!)If I have to work with your code, I will grok that instantly and not have any problems.
In the wild, I've seen a let1 macro which binds one variable:
(let1 var init-form statement1 statement2 ... statementn)
MayeulC
I am not a Lisp expert by any stretch, but let's clarify a few things:
1. Just for the sake of other readers, we agree that the code you quoted does not compile, right?
2. `let` is analogous to a scope in other languages (an extra set of {} in C), I like using it to keep my variables in the local scope.
3. `let` is structured much like other function calls. Here the first argument is a list of assignments, hence the first double parenthesis (you can declare without assigning,in which case the double parenthesis disappears since it's a list of variables, or `(variable value)` pairs).
4. The rest of the `let` arguments can be seen as the body of the scope, you can put any number of statements there. Usually these are function calls, so (func args) and it is parenthesis time again.
I get that the parenthesis can get confusing, especially at first. One adjusts quickly though, using proper indentation helps.
I mostly know lisp trough guix, and... SKILL, which is a proprietary derivative from Cadence, they added a few things like inline math, SI suffixes (I like that one), and... C "calling convention", which I just find weird: the compiler interprets foo(something) as (foo something). As I understand it, this just moves the opening parenthesis before the preceding word prior to evaluation, if there is no space before it.
I don't particularly like it, as that messes with my C instincts, respectively when it comes to spotting the scope. I find the syntax more convoluted with it, so harder to parse (not everything is a function, so parenthesis placement becomes arbitrary):
let( (bar-x(bar(x))
quux-y(quux(y)))
foo(bar-x quux-y z)
)
jasbrg
> Why is the second set of parens necessary?
it distinguishes the bindings from the body.
strictly speaking there's a more direct translation using `setq` which is more analogous to variable assignment in C/Python than the `let` binding, but `let` is idiomatic in lisps and closures in C/Python aren't really distinguished from functions.
krferriter
The code is written the same way it is logically structured. `let` takes 1+ arguments: a set of symbol bindings to values, and 0 or more additional statements which can use those symbols. In the example you are replying to, `bar-x` and `quux-y` are symbols whose values are set to the result of `(bar x)` and `(quux y)`. After the binding statement, additional statements can follow. If the bindings aren't kept together in a `[]` or `()` you can't tell them apart from the code within the `let`.
Koshkin
I prefer that to this (valid) C++ syntax:
[](){}
kibwen
The tragedy of Lisp is that postfix-esque method notation just plain looks better, especially for people with the expectation of reading left-to-right.
let bar_x = x.bar()
let quux_y = y.quux()
return (bar_x, quux_y, z).foo()
wk_end
Looks better is subjective, but it has its advantages both for actual autocomplete - as soon as I hit the dot key my IDE can tell me the useful operations for the obejct - and also for "mental autocomplete" - I know exactly where to look to find useful operations on the particular object because they're organized "underneath" it in the conceptual hierarchy. In Lisps (or other languages/codebases that aren't structured in a non-OOP-ish way) this is often a pain point for me, especially when I'm first trying to make my way into some code/library.
As a bit of a digression:
The ML languages, as with most things, get this (mostly) right, in that by convention types are encapsulated in modules that know how to operate on them - although I can't help but think there ought to be more than convention enforcing that, at the language level.
There is the problem that it's unclear - if you can Frobnicate a Foo and a Baz together to make a Bar, is that an operation on Foos, on Bazes, or on Bars? Or maybe you want a separate Frobnicator to do it? (Pure) OOP languages force you to make an arbitrary choice, Lisp and co. just kind of shrug, the ML languages let you take your take your pick, for better or worse.
tmtvl
De gustibus non disputandum est, I personally find the C++/Java/Rust/... style postfix notation (foo.bar()) to be appalling.
ssivark
And what about when `bar` takes several inputs? Postfix seems like an ugly hack that hyper-fixates on functions of a single argument to the detriment of everything else.
MarceColl
I think it really depends, in Common Lisp for example I don't think that's the case:
(progn
(do-something)
(do-something-else)
(do-a-third-thing))
The only case where it's a bit different and took some time for me to adjust was that adding bindings adds an indent level. (let ((a 12)
(b 14))
(do-something a)
(do-something-else b)
(setf b (do-third-thing a b)))
It's still mostly top-bottom, left to right. Clojure is quite a bit different, but it's not a property of lisps itself I'd say. I have a hard time coming up with examples usually so I'm open to examples of being wrong here.fc417fc802
Your example isn't a very functional code style though so I don't know that I'd consider it to be idiomatic. Generally code written in a functional style ends up indented many layers deep. Below is a quick (and quite tame) example from one of the introductory guides for Racket. My code often ends up much deeper. Consider what it would look like if one of the cond branches contained a nested cond.
(define (start request)
(define a-blog
(cond [(can-parse-post? (request-bindings request))
(cons (parse-post (request-bindings request))
BLOG)]
[else
BLOG]))
(render-blog-page a-blog request))
https://docs.racket-lang.org/continue/index.htmlMarceColl
Common Lisp, which is what I use, is not really a functional oriented language. I'd say the above is okay in CL.
pfdietz
It's easy enough to add -> (and related arrow operators) to Common Lisp as macros.
https://github.com/hipeta/arrow-macros
The common complaint that Common Lisp lacks some feature is often addressed by noting how easy it is to add that feature.
tmtvl
Besides arrow-macros there's also cl-arrows, which is basically exactly the same thing, and Serapeum also has arrow macros (though the -> macro in Serapeum is for type definitions, the Clojure-style arrow macro is hence relegated to ~>).
drob518
Been programming in Lisp for a while. The parents disappear very quickly. One trick to accelerate it is to use a good editor with structural editing (e.g., paredit in Emacs or something similar). All you editing is done on balanced expressions. When you type “(“, the editor automatically inserts “)” with your cursor right in between. If you try to delete a “)”, the editor ignores you until you delete everything inside and the “(“. Basically, you start editing at the expression level, not so much at the character or even line level. You just notice the indentation/shape of the code, but you never spend time counting parentheses or trying to balance anything. Everything is balanced all the time and you just write code.
buttercraft
> reading from right to left, bottom to top - from inside out
I don't understand why you think this. Can you give an example?
fc417fc802
Does this example help? https://github.com/ghollisjr/cl-ana/blob/master/binary-tree/...
pjmlp
I know Lisp since I read the little lister around 1996, and was an XEmacs user until around 2005.
The parenthesis do really disappear, just like the hieroglyphics on C influenced languages, it is a matter of habit.
At least it was for me.
eternityforest
To me any kind of deep nesting is an issue. It goes against the idea of reducing the amount of mental context window needed to understand something.
Plus, if syntax errors can easily take several minutes to fix, because if the syntax is wrong, auto format doesn't work right, and then you have to read a wall of text to find out where the missing close paren should have been.
AdieuToLogic
> Good article. Funnily enough the throw away line "I don't see parentheses anymore". Is my greatest deterrent with lisp. It's not the parens persay, it's the fact that I'm used to reading up to down and left to right.
Language shapes the way we think, and determines what we can think about.
- Benjamin Lee Whorf[0]
From the comments in the post: Ask 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)))))
Let's see how we can get from the LISP version to something akin to the C version.First, let's "modernize" the LISP version by replacing parentheses with "curly braces" and add some commas and newlines just for fun:
{
defun factorial { x },
{
if { zerop x },
1 {
*,
x {
factorial {
- { x, 1 }
}
}
}
}
}
This kinda looks like a JSON object. Let's make it into one and add some assumed labels while we're at it. {
"defun" : {
"factorial" : { "argument" : "x" },
"body" : {
"if" : { "zerop" : "x" },
"then" : "1",
"else" : {
"*" : {
"lhs" : "x",
"rhs" : {
"factorial" : {
"-" : {
"lhs" : "x",
"rhs" : "1"
}
}
}
}
}
}
}
}
Now, if we replace "defun" with the return type, replace some of the curlies with parentheses, get rid of the labels we added, use infix operator notation, and not worry about it being a valid JSON object, we get: int
factorial ( x )
{
if ( zerop ( x ) )
1
else
x * factorial ( x - 1 )
}
Reformat this a bit, add some C keywords and statement delimiters, and Bob's your uncle.0 - https://www.goodreads.com/quotes/573737-language-shapes-the-...
db48x
Whorf was an idiot. It’s not worth quoting him.
AdieuToLogic
> Whorf was an idiot. It’s not worth quoting him.
The citation is relevant to this topic, therefore use and attribution warranted.
lukaslalinsky
Whenever I hear someone talking about purely functional programming, no side effects, I wonder what kind of programs they are writing. Pretty much anything I've written over the last 30 years, the main purpose was to do I/O, it doesn't matter whether it's disk, network, or display. And that's where the most complications come from, these devices you are communicating with have quirks that need you need to deal with. Purely functional programming is very nice in theory, but how far can you actually get away with it?
jcranmer
The idea of pure functional programming is that you can really go quite far if you think of your program as a pure function f(input) -> outputs with a messy impure thing that calls f and does the necessary I/O before/after that.
Batch programs are easy to fit in this model generally. A compiler is pretty clearly a pure function f(program source code) -> list of instructions, with just a very thin layer to read/write the input/output to files.
Web servers can often fit this model well too: a web server is an f(request, database snapshot) -> (response, database update). Making that work well is going to be gnarly in the impure side of things, but it's going to be quite doable for a lot of basic CRUD servers--probably every web server I've ever written (which is a lot of tiny stuff, to be fair) could be done purely functional without much issue.
Display also can be made work: it's f(input event, state) -> (display frame, new state). Building the display frame here is something like an immediate mode GUI, where instead of mutating the state of widgets, you're building the entire widget tree from scratch each time.
In many cases, the limitations of purely functional isn't that somebody somewhere has to do I/O, but rather the impracticality of faking immutability if the state is too complicated.
lukaslalinsky
I guess my point is that you actually have to write the impure code somehow and it's hard, external world has tendencies to fail, needs to be retried, coordinated with other things. You have to fake all these issues. In your web server examples, if you need to a cache layer for certain part of the data, you really can't without encoding it to the state management tooling. And this point you are writing a lot of non-functional code in order to glue it together with pure functions and maybe do some simple transformation in the middle. Is it worth it?
I have respect for OCaml, but that's mostly because it allows you to write mutable code fairly easily.
Roc codifies the world vs core split, but I'm skeptical how much of the world logic can be actually reused across multiple instances of FP applications.
eduction
There's a spectrum of FP languages with Haskell near the "pure" end where it truly becomes a pain to do things like io and Clojure at the more pragmatic end where not only is it accepted that you'll need to do non functional things but specific facilities are provided to help you do them well and in a way that can be readily brought into the functional parts of the language.
(I'm biased though as I am immersed in Clojure and have never coded in Haskell. But the creator of Clojure has gone out of his way to praise Haskell a bunch and openly admits where he looked at or borrowed ideas from it.)
mrkeen
> external world has tendencies to fail, needs to be retried, coordinated with other things.
This is exactly why I'm so aggressive in splitting IO from non-IO.
A pure function generally has no need to raise an exception, so if you see one, you know you need to fix your algorithm not handle the exception.
Whereas every IO action can succeed or fail, so those exceptions need to be handled, not fixed.
> You have to fake all these issues.
You've hit the nail on the head. Every programmer at some point writes code that depends on a clock, and tries to write a test for it. Those tests should not take seconds to run!
In some code bases the full time is taken.
handle <- startProcess
while handle.notDone
sleep 1000ms
check handle.result
In other code-bases, some refactoring is done, and fake clock is invented. fakeClock <- new FakeClock(10:00am)
handle <- startProcess(fakeClock);
fakeClock.setTime(10:05am)
waitForProcess handle
Why not go even further and just pass in a time, not a clock? let result = process(start=10:00am, stop=10:05)
Typically my colleagues are pretty accepting of doing the work to fake clocks, but don't generalise that solution to faking other things, or even skipping the fakes, and operating directly on the inputs or outputs.Does your algorithm need to upload a file to S3? No it doesn't, it needs to produce some bytes and a url where those bytes should go. That can be done in unit-test land without any IO or even a mocking framework. Then some trivial one-liner higher up the call-chain can call your algorithm and do the real S3 upload.
mrkeen
Think of it like other features:
* Encapsulation? What's the point of having it if's perfectly sealed off from the world? Just dead-code eliminate it.
* Private? It's not really private if I can Get() to it. I want access to that variable, so why hide it from myself? Private adds nothing because I can just choose not to use that variable.
* Const? A constant variable is an oxymoron. All the programs I write change variables. If I want a variable to remain the same, I just wont update it.
Of course I don't believe in any of the framings above, but it's how arguments against FP typically sound.
Anyway, the above features are small potatoes compared to the big hammer that is functional purity: you (and the compiler) will know and agree upon whether the same input will yield the same output.
Where am I using it right now?
I'm doing some record linkage - matching old transactions with new transactions, where some details may have shifted. I say "shifted", but what really happened was that upstream decided to mutate its data in-place. If they'd had an FPer on the team, they would not have mutated shared state, and I wouldn't even need to do this work. But I digress.
Now I'm trying out Dijkstra's algorithm, to most efficiently match pairs of transactions. It's a search algorithm, which tries out different alternatives, so it can never mutate things in-place - mutating inside one alternative will silently break another alternative. I'm in C#, and was pleasantly surprised that ImmutableList etc actually exist. But I wish I didn't have to be so vigilant. I really miss Haskell doing that part of my carefulness for me.
DeathArrow
>I'm in C#, and was pleasantly surprised that ImmutableList etc actually exist.
C# has introduced many functional concepts. Records, pattern matching, lambda functions, LINQ.
The only thing I am missing and will come later is discriminated unions.
Of course, F# is more fited for the job if you want a mostly functional workflow.
mrkeen
I don't want functional-flavoured programming, I want functional programming.
Back when I was more into pushing Haskell on my team (10+ years ago), I pitched the idea something like:
You get: the knowledge that your function's output will only depend on its input.
You pay: you gotta stop using those for-loops and [i]ndexes, and start using maps, folds, filters etc.
Those higher-order functions are a tough sell for programmers who only ever want to do things the way they've always done them.But 5 years after that, in Java-land everyone was using maps, folds and filters like crazy (Or in C# land, Selects and Wheres and SelectManys etc,) with some half-thought-out bullshit reasoning like "it's functional, so it must good!"
So we paid the price, but didn't get the reward.
pfdietz
Those starred rhetorical questions initially looked to me like a critique of Lisp! Because that's how Lisp (particularly Common Lisp) works. All those things are softish. You can see unexported symbols even if you're not supposed to use them. There is no actual privacy unless you do something special like unintern then recreate a symbol.
bobbylarrybobby
> you (and the compiler) will know and agree upon whether the same input will yield the same output
What exactly does this mean? Haskell has plenty of non-deterministic functions — everything involving IO, for instance. I know that IO is non-deterministic, but how is that expressed within the language?
mrkeen
Functions which use IO are tagged as such in the type system. IO can call non-IO, but not vice-versa.
perrygeo
Not even the most fanatical functional programming zealots would claim that programs can be 100% functional. By definition, a program requires inputs and outputs, otherwise there is literally no reason to run it.
Functional programming simply says: separate the IO from the computation.
> Pretty much anything I've written over the last 30 years, the main purpose was to do I/O, it doesn't matter whether it's disk, network, or display.
Every useful program ever written takes inputs and produces outputs. The interesting part is what you actually do in the middle to transforms inputs -> outputs. And that can be entirely functional.
sync13298
> Every useful program ever written takes inputs and produces outputs. The interesting part is what you actually do in the middle to transforms inputs -> outputs. And that can be entirely functional.
My work needs pseudorandom numbers throughout the big middle, for example, drawing samples from probability distributions and running randomized algorithms. That's pretty messy in a FP setting, particularly when the PRNGs get generated within deeply nested libraries.
zelphirkalt
At what point does this get messy?
DeathArrow
>Not even the most fanatical functional programming zealots would claim that programs can be 100% functional. By definition, a program requires inputs and outputs, otherwise there is literally no reason to run it.
So a program it's a function that transforms the input to the output.
DeathArrow
>separate the IO from the computation.
What about managing state? I think that is an important part and it's easy to mess it.
roxolotl
Each step calculates the next state and returns it. You can then compose those state calculators. If you need to save the state that’s IO and you have a bit specifically for it.
skydhash
It takes a bit of discipline, but generally all state additions should be scoped to the current context. Meaning, when you enter a subcontext, it has become input and treated as holy, and when you leave to the parent context, only the result matters.
But that particular context has become inpure and decried as such in the documentation, so that carefulness is increased when interacting with it.
markus_zhang
> separate the IO from the computation.
Can you please elaborate on this point? I read it as this web page (https://wiki.c2.com/?SeparateIoFromCalculation) describes, but I fail to see why it is a functional programming concept.
bmacho
> but I fail to see why it is a functional programming concept.
"Functional programming" means that you primarily use functions (not C functions, but mathematical pure functions) to solve your problems.
This means you won't do IO in your computation because you can't do that. It also means you won't modify data, because you can't do that either. Also you might have access to first class functions, and can pass them around as values.
If you do procedural programming in C++ but your functions don't do IO or modify (not local) values, then congrats, you're doing functional programming.
mrkeen
> I fail to see why it is a functional programming concept.
Excellent! You will encounter 0 friction in using an FP then.
To the extent that programmers find friction using Haskell, it's usually because their computations unintentionally update the state of the world, and the compiler tells them off for it.
AstroBen
Think about this: if a function calls another function that produces a side effect, both functions become impure (non-functional). Simply separating them isn't enough. That's the difference when thinking of it in functional terms
Normally what functional programmers will do is pull their state and side effects up as high as they can so that most of their program is functional
teddyh
Having functions which do nothing but computation is core functional programming. I/O should be delegated to the edges of your program, where it is necessary.
whatnow37373
> The interesting part is what you actually do in the middle to transforms inputs -> outputs.
Can you actually name something? The only thing I can come up with is working with interesting algorithms or datastructures, but that kind of fundamental work is very rare in my experience. Even if you do, it's quite often a very small part of the entire project.
skydhash
A whole web app. The IO are generally user facing network connections (request and response), IPC and RPC (databases, other services), and files interaction. Anything else is logic. An FP programs is a collection of pipes, and IO are the endpoints. With FP the blob of data passes cleanly from one section to another while in imperative, some of it sticks. In OOP, there’s a lot of blob, that flings stuff at each other and in the process create more blobs.
perrygeo
> Even if you do, it's quite often a very small part of the entire project.
So your projects are only moving bits from one place to another? I've literally never seen that in 20 years of programming professionally. Even network systems that are seen as "dumb pipes" need to parse and interpret packet headers, apply validation rules, maintain BGP routing tables, add their own headers etc.
Surely the program calculates something, otherwise why would you need to run the program at all if the output is just a copy of the input?
hansvm
It's a matter of framing. Think of any of the following:
- Refreshing daily "points" in some mobile app (handling the clock running backward, network connectivity lapses, ...)
- Deciding whether to send an marketing e-mail (have you been unsubscribed, how recently did you send one, have you sent the same one, should you fail open or closed, is this person receptive to marketing, ...)
- How do you represent a person's name and transform it into the things your system needs (different name fields, capitalization rules, max characters, what it you try to put it on an envelope and it doesn't fit, ...)
- Authorization logic (it's not enough to "just use a framework" no matter your programming style; you'll still have important business logic about who can access what when and how the whole thing works together)
And so on. Everything you're doing is mapping inputs to outputs, and it's important that you at least get it kind of close to correct. Some people think functional programming helps with that.
phlakaton
You can name almost anything (these are general-purpose languages, after all), but I'll just throw a couple of things out there:
1. A compiler. The actual algorithms and datastructures might not be all that interesting (or they might be if you're really interested in that sort of thing), but the kinds of transformations you're doing from stage to stage are sophisticated.
2. An analytics pipeline. If you're working in the Spark/Scala world, you're writing high-level functional code that represents the transformation of data from input to output, and the framework is compiling it into a distributed program that loads your data across a cluster of nodes, executes the necessary transformations, and assembles the results. In this case there is a ton of stateful I/O involved, all interleaved with your code, but the framework abstracts it away from you.
larve
It's always hard to parse if people mean functional programming when bringing up Lisp. Common Lisp certainly is anything but a functional language. Sure, you have first order functions, but you in a way have that in pretty much all programming languages (including C!).
But most functions in Common Lisp do mutate things, there is an extensive OO system and the most hideous macros like LOOP.
I certainly never felt constrained writing Common Lisp.
That said, there are pretty effective patterns for dealing with IO that allow you to stay in a mostly functional / compositional flow (dare I say monads? but that sounds way more clever than it is in practice).
behnamoh
> It's always hard to parse if people mean functional programming when bringing up Lisp. Common Lisp certainly is anything but a functional language. Sure, you have first order functions, but you in a way have that in pretty much all programming languages (including C!).
It's less about what the language "allows" you to do and more about how the ecosystem and libraries "encourage" you to do.
mejutoco
Any useful program has side-effects. IMHO the point is to isolate the part of the code that has the side-effects as much as possible, and keep the rest purely functionsl. That makes it easier to debug, test, and create good abstractions. Long term it is a very good approach.
throw0101d
> Pretty much anything I've written over the last 30 years, the main purpose was to do I/O, it doesn't matter whether it's disk, network, or display.
Erlang is a strictly (?) a functional language, and the reason why it was invented was to do network-y stuff in the telco space. So I'm not sure why I/O and functional programming would be opposed to each other like you imply.
troupo
> Erlang is a strictly (?) a functional language,
First and foremost Erlang is a pragmatic programming language :)
fulafel
This is discussing Common Lisp which is not even a mostly-functional language, and far from purely functional.
mikelevins
He says Lisp, rather than Common Lisp. Sure, given the context he's writing in now, maybe he means Common Lisp, but Joe Marshall was a Lisp programmer before Common Lisp existed, so he may not mean Common Lisp specifically.
vkazanov
Somehow haskell and friends shifted the discussion around functional programming to pure vs non-pure! I am pretty sure it started with functions as first order objects as differentiator in schemes, lisps and ml family languages. Thus functional, but that's just a guess.
mrkeen
> Somehow haskell and friends shifted the discussion around functional programming to pure vs non-pure
In direct response every other language in the mid 2010s saying, "Look, we're functional too, we can pass functions to other functions, see?"
foo.bar()
.map(x => fireTheMissiles())
.collect();
C's had that forever: void qsort(void *base, size_t nmemb, size_t size,
int (*compar)(const void *, const void *))
djha-skin
I wrote a recursive descent parser in Lisp for a YAML replacement language[1]. It wasn't difficult. Lisp makes it easy to write I/O, but also easy to separate logic from I/O. This made it easy for me to write unit tests without mocking.
I also wrote a toy resource scheduler at an HTTP endpoint in Haskell[2]. Writing I/O in Haskell was a learning curve but was ultimately fine. Keeping logic separate from I/O was the easy thing to do.
DadBase
Had a PalmPilot taped to a modem that did our auth. Lisp made the glue code feel like play. No types barking, no ceremony—just `(lambda (x) (tinker x))`. We didn’t debug, we conversed. Swapped thoughts with the REPL like it was an old friend.
no_wizard
Though these are minor complaints, there is a couple things I'd like to change about a Lisp language.
One is its the implicit function calls. For example, you'll usually see calls like this: `(+ 1 2)` which translates to 1 + 2, but I would find it more clear if it was `(+(1,2))` where you have a certain explicitness to it.
It doesn't stop me from using Lisp languages (Racket is fun, and I been investigating Clojure) but it took way too long for the implicit function stuff to grok in my brain.
My other complain is how the character `'` can have overloaded meaning, though I'm not entirely sure if this is implementation dependent or not
MarceColl
It's not really implicit though, the first element of a list that is evaluated is always a function. So (FUN 1 2) is an explicit function call. The problem is that it doesn't look like C-like languages, not that it's not explicit.
In theory ' just means QUOTE, it should not be overloaded (although I've mostly done Common Lisp, so no idea if in other impl that changes). Can you show an example of overloaded meaning?
dapperdrake
Still unsure, whether a naked single quote character for (quote …) is really a good idea.
Got used to it by typing out (quote …)-forms explicitly for a year. The shorthand is useful at the REPL but really painful in backquote-templates until you type it out.
CL:QUOTE is a special operator and (CL:QUOTE …) is a very important special form. Especially for returning symbols and other code from macros. (Read: Especially for producing code from templates with macros.)
Aside: Lisp macros solve the C-fopen() fclose() dance for good. It even closes the file handle on error, see WITH-OPEN-FILE. That alone is worth it. And the language designers provided the entire toolset for building stuff like this, for free.
No matter how unusual it seems, it really is worth getting used to.
no_wizard
There's an example I saw where `'` was used as a way to denote a symbol, but I can't find that explicit example. It wasn't SBCL, I believe it may have been Clojure. Its possible I'm misremembering.
That said, since I work in C-like languages during the day, I suppose my minor complaint has to do with ease of transition, it always takes me a minute to get acquainted to Lisp syntax and read Lisp code any time I work with it.
Its really a minor complaint and one I probably wouldn't have if I worked with a Lisp language all day.
panzagl
First person to ask for more parenthesis in Lisp.
DadBase
First time I saw (+ 1 2), I thought it was a typo. Spent an hour trying to “fix” it into (1 + 2). My professor let me. Then he pointed at the REPL and said, “That’s not math—it’s music.” Never forgot that. The '? That’s the silent note.
no_wizard
It's due to Polish notation[0] as far as I understand it. This is how that notation for mathematics works.
I suppose my suggestion would break those semantics.
obi2kenobi
Beautiful. I wish I had more professors who expressed concepts poetically back when I was in school. That's the kind of line that sticks in your head.
wdkrnls
R works exactly as you describe. You can type `+`(1, 2) and get 3 because in R everything that happens is a function call even if a few binary functions get special sugar so you can type 1 + 2 for them as well. The user can of course make their own of these if they wrap them in precents. For example: `%plus%` = function(a, b) { `+`(a, b)}. A few computer algebra systems languages provide even more expressivity like yacas and fricas. The later even has a type system.
seanw444
Similar in Nim as well.
Reefersleep
It's not implicit in this case, it's explicit. + is the function you're calling. And there's power in having mathematical operations be functions that you can manipulate and compose like all other functions, instead of some special case of infix implicit (to me, yeah) function calling, like 1 + 2, where it's no longer similar to other functions.
eyelidlessness
How is it implicit? The open parenthesis is before the function name rather than after, but the function isn’t called without both parentheses.
If you want to use commas, you can in Lisp dialects I’m familiar with—they’re optional because they’re treated as whitespace, but nothing is stopping you if you find them more readable!
apgwoz
, is typically “unquote.” Clojure is the only “mainstream” Lisp that allows , as whitespace. Has meaning in CL and Scheme.
bcrosby95
func(a, b) is basically the same as (func a b). You're just moving the parens around. '+' is extra 'strange' because in most languages it isn't used like other functions: imagine if you had to write +(1, 2) in every C-like.
zelphirkalt
Surely you mean (+ (, 1 2))
;)
fud101
[flagged]
DadBase
Wat’s the sound of meeting something older than you thought possible.
Lisp listened. Modem sang. PalmPilot sweated. We talked, not debugged.
owlstuffing
> No types barking
No thanks
tmtvl
The reason I switched from Scheme to Common Lisp was because I could say...
(defun foo (x)
(declare (type (Integer 0 100) x))
(* x
(get-some-value-from-somewhere-else)))
And then do a (describe 'foo) in the REPL to get Lisp to tell me that it wants an integer from 0 to 100.codr7
Common Lisp supports gradual typing and will (from my experience) do a much better job of analyzing code and pointing out errors than your typical scripting language.
shadowgovt
The most impressive thing, to me, about LISP is how the very, very small distance between the abstract syntax tree and the textual representation of the program allows for some very powerful extensions to the language with relatively little change.
Take default values for function arguments. In most languages, that's a careful consideration of the nuances of the parser, how the various symbols nest and prioritize, whether a given symbol might have been co-opted for another purpose... In LISP, it's "You know how you can have a list of symbols that are the arguments for the function? Some of those symbols can be lists now, and if they are, the first element is the symbolic argument name and the second element is a default value."
bgitarts
Always read from experienced developers praising lisps, but why is it so rare in production applications?
gpcz
Looking for a nice, solid, well-documented library to do something is difficult for most stuff. There are some real gems out there, but usually you end up having to roll your own thing. And Lisp generally encourages rolling your own thing.
geor9e
People smart enough to read and write it are rare.
Capricorn2481
Is it about intelligence or just not being used to/having time for learning a different paradigm?
I personally have used LISP a lot. It was a little rough at first, but I got it. Despite having used a lot of languages, it felt like learning programming again.
I don't think there's something special about me that allowed me to grok it. And if that were the case, that's a horrible quality in a language. They're not supposed to be difficult to use.
noisy_boy
I think it allows very clever stuff, which I don't think is done routinely, but that's what gets talked about. I try to write clean functional style code in other languages which in my book means separation of things that have side effects and things that don't. I don't think I'll have difficulty writing standard stuff in Lisp with that approach.
Just because it allows intricate wizardry doesn't mean it is inherently hard to get/use. I think the bigger issue would be ecosystem and shortage of talent pool.
tmtvl
I have hardly any issues reading and writing CL and I am so stupid even bags of bricks take pity on me. Intelligence is not a factor here.
Rendello
The only software I use that I know runs a lisp is Hacker News.
wdkrnls
When I was in high school I learned AutoCAD and I remember that back then it was scripted in LISP. I'm not sure if that is still true.
grandempire
JavaScript and Python have adopted almost every feature that differentiated Lisp from other languages. So in comparison Lisp is just more academic, esoteric, and advanced.
int_19h
This is only true if you define "Lisp" as the common subset. If you look specifically at Common Lisp, neither Python nor JS come close in terms of raw power.
dangus
It's pretty rare because when it was being originally designed the main intended use case was for bragging about the cool unique niche programming language you use on your blog posts. While it's a very good language for being able to recursively get itself onto the front page, it's not so good at conformist normie use cases.
mikedelago
In my case, my boss won't let me.
p0w3n3d
Terry Pratchett's quote in one of his books (in fact I think this is a running gag, and appeared in multiple books):
Five exclamation marks, a sure sign of an insane mind
That's what I think about five closing parentheses too... But tbh I am also jealous, because I can't program in lisp at alldjha-skin
I agree with some statements OP makes but not others. Ultimately, I write in lisp because it's fun to write in Lisp due to its expressive power, ease of refactoring, and the Lisp Discord[1].
> Lisp is easier to remember,
I don't feel this way. I'm always consulting the HyperSpec or googling the function names. It's the same as any other dynamically typed language, such as Python, this way to me.
> has fewer limitations and hoops you have to jump through,
Lisp as a language has incredibly powerful features find nowhere else, but there are plenty of hoops. The CLOS truly feels like a superpower. That said, there is a huge dearth of libraries. So in that sense, there's usually lots of hoops to jump through to write an app. It's just I like jumping through them because I like writing code as a hobby. So fewer limitations, more hoops (supporting libraries I feel the need to write).
> has lower “friction” between my thoughts and my program,
Unfortunately I often think in Python or Bash because those are my day job languages, so there's often friction between how I think and what I need to write. Also AI is allegedly bad at lisp due to reduced training corpus. Copilot works, sorta.
> is easily customizable,
Yup, that's its defining feature. Easy to add to the language with macros. This can be very bad, but also very good, depending on its use. It can be very worth it both to implementer and user to add to the language as part of a library if documented well and done right, or it can make code hard to read or use. It must be used with care.
> and, frankly, more fun.
This is the true reason I actually use Lisp. I don't know why. I think it's because it's really fun to write it. There are no limitations. It's super expressive. The article goes into the substitution principle, and this makes it easy to refactor. It just feels good having a REPL that makes it easy to try new ideas and a syntax that makes refactoring a piece of cake. The Lisp Discord[1] has some of the best programmers on the planet in it, all easy to talk to, with many channels spanning a wide range of programming interests. It just feels good to do lisp.
jll29
As much as I sympathize with this post and similar ones, and as much I personally like functional thinking, LISP environments are not nearly as advanced anymore as they used to be.
Which Common LISP or Scheme environment (that runs on, say Ubuntu Linux on a typical machine from today) gets even close to the past's LISP machines, for example? And which could compete with IntelliJ IDEA or PyCharm or Microsoft Code?
vindarel
Common Lisp can compete with Python no problem, that's what matters to me. You get:
- truly interactive development (never wait for something to restart, resume bugs from any stack frame after you fixed them),
- self-contained binaries (easy deployment, my web app with all the dependencies, HTML and CSS is ±35MB)
- useful compile-time warnings and errors, a keystroke away, for Haskell levels see Coalton (so better than Python),
- fast programs compiled to machine code,
- no GIL
- connect to, inspect or update running programs (Slime/Swank),
- good debugging tools (interactive debugger, trace, stepper, watcher (on some impls)…)
- stable language and libraries (although the implementations improve),
- CLOS and MOP,
- etc
- good editor support: Emacs, Vim, Atom/Pulsar (SLIMA), VScode (ALIVE), Jetbrains (SLT), Jupyter kernel, Lem, and more: https://lispcookbook.github.io/cl-cookbook/editor-support.ht...
What we might not get:
- advanced refactoring tools -also because we need them less, thanks to the REPL and language features (macros, multiple return values…).
---
For a lisp machine of yesterday running on Ubuntu or the browser: https://interlisp.org/
Capricorn2481
> self-contained binaries
But Lispworks is the only one that makes actual tree-shaken binaries, whereas SBCL just throws everything in a pot and makes it executable, right?
> good editor support: Emacs, Vim, Atom/Pulsar (SLIMA), VScode (ALIVE)
I can't speak for those other editors, but my experience with Alive has been pretty bad. I can't imagine anyone recommending it has used it. It doesn't do what slime does, and because of that, you're forced to use Emacs.
Calva for Clojure, however, is very good. I don't know why it can't be this way for CL.
vindarel
Maybe you tried some time ago? This experience report concludes by "all in all, a great rig": https://blog.djhaskin.com/blog/experience-report-using-vs-co...
> The usage experience was very ergonomic, much more ergonomic than I'm used to with my personal CL set-up. Still, the inability to inspect stack frame variables would bother me, personally.
I don't use them, but I'd recommend Pulsar's SLIMA over the VSCode plugin, because it's older and based on Slime, where ALIVE is based on LSP.
> But Lispworks is the only one that makes actual tree-shaken binaries, whereas SBCL just throws everything in a pot and makes it executable, right?
right. SBCL has core compression, so as I said a web app with dozens of dependencies and all static assets is ±35MB, that includes the compiler and debugger (that allow to connect and update a running image, whereas this wouldn't be possible with LispWorks' stripped down binary). 35MB for a non-trivial app is good IMO (and in the ballparks of a growing Go app right?)
There's also ECL, if you rely on libecl you can get very small binaries (I didn't explore this yet, see example in https://github.com/fosskers/vend)
jrapdx3
Of course you're right. I've written non-trivial programs in Scheme, Emacs is a good tool for it, but certainly don't know of an environment that matches the Lisp machines.
IDEs provide such environments for the most common languages but major IDEs offer meager functionality for Lisp/Scheme (and other "obscure" languages). With a concerted effort it's possible an IDE could be configured to do more for Lisp. Thing is the amount of effort required is quite large. Since AFAIK no one has taken up the challenge, we can only conclude it's not worth the time and energy to go there.
The workflow I've used for Scheme programming is pretty simple. I can keep as many Emacs windows ("frames") open as necessary with different views of one or several modules/libraries, a browser for documentation, terminals with REPL/compiler, etc. Sort of a deconstructed IDE. Likely it does take a bit more cognitive effort to work this way, but it gets the job done.
efitz
This is the first article I’ve ever read that made me want to go learn Lisp.
cutler
Watch Rich Hickey's early Clojure videos and be blown away.
laurent_du
Got any specific suggestion?
spicybbq
"Simple Made Easy" is pretty popular, there is a transcription with slides:
https://github.com/matthiasn/talk-transcripts/blob/master/Hi...
cess11
It's tangentially relevant, but I've enjoyed this one, about hammock driven programming.
dutchblacksmith
Lispworks has a free editition with lots of examples. Look into PAIP from Peter Norvig.
larve
Even putting the common lisp aside, PAIP is my favourite book about programming in general, by FAR. Norvig's programming style is so clear and expressive, the book touches on more "pedestrian" parts of programming: building tools / performance / debugging, but also walks you through a serious set of algorithms that are actually practical and that I use regularly (and they shape your thinking): search, pattern matching, to some extent unification, building interpreters and compilers, manipulating code as data.
It's also extremely fun, you go from building Eliza to a full pattern matcher to a planning agent to a prolog compiler.
phlakaton
Paul Graham's On Lisp is also a powerful argument to try the language, even if some of the stuff it presents is totally bonkers. :-D
__MatrixMan__
Next time you see a HN post on a lisp-centric topic, click into the comments. I'll bet you a nickel that they'll be happier than most. Instead of phrases like "dumpster fire" they're using words like "joyful".
That's why I keep rekindling my learn-lisp effort. It feels like I'm just scratching the surface re: the fun that can be had.
dutchblacksmith
Never been happier since building an Erp system in pure lisp and postgresql.
smckk
Could a not-too trivial example like the difference between a Java sudoko solver and a lisp version with all the bells and whistles of FP such as functions as data and return values, recursion and macros be used to illustrate the benefits?
tikotus
Here's one in Clojure using its core.logic library. I'd say it's pretty neat. You can do something similar in something like Prolog, but a Java implementation would look very different.
https://github.com/sideshowcoder/core-logic-sudoku-solver/bl...
terminalbraid
> Other general purpose languages are more popular and ultimately can do everything that Lisp can (if Church and Turing are correct).
I find these types of comments extremely odd and I very much support lisp and lisp-likes (I'm a particular fan of clojure). I can only see adding the parenthetical qualifier as a strange bias of throwing some kind of doubt into other languages which is unwarranted considering lisp at its base is usually implemented in those "other general purpose languages".
If you can implement lisp in a particular language then that particular language can de facto do (at least!) everything lisp can do.
russellbeattie
One doesn't have to invoke Turing or Church to show all languages can do the same things.
Any code that runs on a computer (using the von Neumann architecture) boils down to just a few basic operations: Read/write data, arithmetic (add/subtract/etc.), logic (and/or/not/etc.), bit-shifting, branches and jumps. The rest is basically syntactic sugar or macros.
If your preferred programming language is a pre-compiled type-safe object oriented monster with polymorphic message passing via multi-process co-routines, or high-level interpreted purely functional archetype of computing perfection with just two reserved keywords, or even just COBOL, it's all going to break down eventually to the ops above.
Capricorn2481
Sometimes, when people say one language can't do what another does, they aren't talking about outputs. Nobody is arguing that lisp programs can do arithmetic and others can't, they're arguing that there are ergonomics to lisp you can't approach in other languages.
But even so
> it's all going to break down eventually to the ops above.
That's not true either. Different runtimes will break down into a completely different version of the above. C is going to boil down to a different set of instructions than Ruby. That would make Ruby incapable of doing some tasks, even with a JIT. And writing performance sensitive parts in C only proves the point.
"Any language can do anything" is something we tell juniors who have decision paralysis on what to learn. That's good for them, but it's not true. I'm not going to tell a client we're going to target a microcontroller with PHP, even if someone has technically done it.
terminalbraid
You can trivially devise a language that doesn't, though? Let's say I have a language that can return 0 and only 0. It cannot reproduce lisp.
pgwhalen
Isn’t this just a cheeky joke? I.e. “if Einstein is right about this whole theory of relatively thing”
zachbeane
Common Lisp at its base is usually written in Common Lisp.
terminalbraid
I'm sure you are aware there is ultimately a chicken and egg problem here. Even given the case you presented, it doesn't invalidate the point that if it can implement lisp it must be able to do everything lisp can do. In fact given lisp's simplicity, I'd be hard pressed to call a language that couldn't implement lisp "general purpose".
greydius
"You're a very clever man, Mr. James, and that's a very good question," replied the little old lady, "but I have an answer to it. And it's this: The first turtle stands on the back of a second, far larger, turtle, who stands directly under him."
"But what does this second turtle stand on?" persisted James patiently.
To this, the little old lady crowed triumphantly,
"It's no use, Mr. James—it's turtles all the way down."
grandempire
> I'm sure you are aware there is ultimately a chicken and egg problem here.
You should learn more about compilers. There is a really cool idea waiting for you.
varjag
There are several Lisp implementations (including fully-fledged operating systems) which are implemented in Lisp top to bottom.
taeric
This is conflating slightly different things, though? One is that you can build a program that does the same thing. The other is that you can do the same things with the language.
There are special forms in LISP, but that is a far cry from the amount of magic that can only be done in the compiler or at runtime for many languages out there.
thethimble
Brainfuck is also Turing complete but that isn’t an argument that it’s a good replacement for LISP or any other language.
f1shy
That has a name: Turing tarpit.
bitwize
Yes, but sometimes doing the things Lisp can do in another language as easily and flexibly as they are done in Lisp has, as a first step, implementing Lisp in the target language.
For a famous example, see Clasp: https://github.com/clasp-developers/clasp
volemo
I believe ‘twas a joke.
0xTJ
I've never programmed in a Lisp, but I'd love to learn, it feels like one of those languages like Perl that are just good to know. I do have a job where getting better with SKILL would be useful.
Good article. Funnily enough the throw away line "I don't see parentheses anymore". Is my greatest deterrent with lisp. It's not the parens persay, it's the fact that I'm used to reading up to down and left to right. Lisp without something like the clojure macro ->, means that I am reading from right to left, bottom to top - from inside out.
If i programmed enough in lisp I think my brain would adjust to this, but it's almost like I can't full appreciate the language because it reads in the "wrong order".