[HN Gopher] A Lisp adventure on the calm waters of the dead C (2...
___________________________________________________________________
A Lisp adventure on the calm waters of the dead C (2021)
Author : caned
Score : 72 points
Date : 2025-06-23 20:00 UTC (4 days ago)
(HTM) web link (mihaiolteanu.me)
(TXT) w3m dump (mihaiolteanu.me)
| lproven wrote:
| Previously:
|
| https://news.ycombinator.com/item?id=28851992
|
| https://news.ycombinator.com/item?id=44359454
|
| No comments on any of them.
|
| It sounded of interest to me, but I read it and closed the tab
| within a page or so as it wandered off into tech arcana. Shame.
| There may be an interesting idea in here but it's phrased in
| terms I think few will be able to follow and understand.
|
| I did not finish it but I saw no mention of the lambda calculus
| or of currying, both of which -- from my _very_ meagre
| understanding -- seem _directly_ relevant to what I understood to
| be the core point, which seems to be about anonymous functions.
| tmtvl wrote:
| The core point, to me, seemed to be about limiting factors in
| language extension. To allow something like:
| my_if (points <= 100, printf ("%D", points), error ("Invalid
| point total"));
|
| Where the various parameters are lazily evaluated. Or like:
| frobnicate (frazzle: foo, frozzle: bar, frizzle: baz);
|
| Where frazzle, frozzle, and frizzle are position-independent
| keyword variables.
|
| Allowing those in C would require a modicum of effort, while
| other languages make these kinds of syntax extension fairly
| easy.
| PaulHoule wrote:
| In languages like Java (or C) you can build S-expression like
| structures like so Variable<Integer> x =
| newVariable(); Expression<Integer> =
| add(x,literal(5)); x.set(15);
| System.out.println(eval(x)) // prints "20"
|
| and it is not that hard to either serialize these to code or
| run them in a tree-walking interpreter where quote() and
| eval() imply an extended language where you can write
| functions that work on Expression<Expression<X>>. Type
| erasure causes some problems in Java that make you sometimes
| write a type you shouldn't have to and you do have to unerase
| types in method names which is a little ugly but it works.
|
| I did some experiments towards this to convince myself it
| would work
|
| https://github.com/paulhoule/ferocity/blob/main/ferocity0/sr.
| ..
|
| had I really kept at it I would have bootstrapped by
| developing a ferocity0 which was sufficient to write a code
| generator that could generate stubs for the Java stdlib + a
| persistent collections library and then write a ferocity1 in
| ferocity0, and if necessary ferocity(N+1) in ferocityN until
| it supported "all" of Java, though "all" might have omitted
| some features like "var" that are both sugar and use type
| inference that ferocity would struggle with -- if you need
| sugar in this system you implement it with metaprogramming.
|
| The idea is that certain projects would benefit from balls-
| to-the-walls metaprogramming and the code compression you get
| would compensate for the code getting puffed up. My guess is
| a lot of people would see it as an unholy mating of the worst
| of Java and Common Lisp. However, I'm certain it would be
| good for writing code generators.
| molteanu wrote:
| The solution, which I often seen in practice, is to
| eventually write code generators, which is what Lisp macros
| are, after all. I've seen it in C and wrote a big piece
| about it that was posted here some time ago[1], about the
| extra tools, code generators, special formats and standards
| employed and needed to make up for C's deficiencies (in
| respect to meta-programming, at least).
|
| Everywhere I see code generators it means a feature is
| lacking in the main language used for the project. Then you
| bring in other tools to make up for that deficiency. Only,
| usually, we don't call that deficiency, since we are used
| to things being that way. It is called day-to-day business.
| I think that's what I've tried to convey in the article.
|
| [1] https://news.ycombinator.com/item?id=41066544
| PaulHoule wrote:
| I don't see code generation as a bad smell at all.
|
| At my job we use the JooQ code generator which is well
| integrated with maven and either IntellJ IDEA or Eclipse
| so autcompletion "just works". In modern Java you can
| pack up a code generator as a maven plugin [1] and put
| something in your POM that runs the generator. It's easy.
| There are other ways to hook the compiler too, see the
| controversial
|
| https://projectlombok.org/
|
| Lisp does come closest to a "language construction set"
| that lets you bend a language to your will. I think a
| compiler could be built for a language that looks more
| conventional that would be just as malleable, maybe even
| more malleable, but a generation of system programmers
| were traumatized by slow C++ builds and want to have
| nothing to do with a compiler which could be slow, even
| if you could make up for the slowness by having
| dramatically less code.
|
| [1] A maven plugin is just a Java class which can do
| everything in the ordinary Java way which gets
| dependencies injected by the maven runtime. It's a common
| rookie mistake to try to solve problems by writing XML. I
| mean, if you can write a POM that makes existing plugins
| do what you want go right ahead, but if you can't just
| write your own plugin.
| molteanu wrote:
| > I don't see code generation as a bad smell at all.
|
| Well, exactly!
|
| > At my job we use the JooQ code generator...
|
| And in Lisp one would use the...Lisp code generator. That
| is, the macro. And the beauty of it is that it doesn't
| work with pure strings but 'understands', parses,
| manipulates the code as expressions in its own language.
| That is, it has at its disposal the entire language for
| manipulating those expressions.
|
| And I think that is one of the "aha" moments. At least,
| it was for me. When you realize the reason for having
| those code generators, regardless of the project and
| language, in the first place. That is, something missing
| from the language. Some extra feature that can't be
| implemented. Some solution that works really close to
| 99%, but not beyond. Something that one would like to
| express, but can't. Some piece of code that you want to
| be parameterized like you would a function, some piece of
| code that you want to use in multiple places but you
| don't want to write the same boilerplate or copy/paste it
| all over the place with the risk that when you modify
| something, you'll need to modify in all those places. Or
| some piece of code that you want to be auto-generated
| when you build/deploy/etc.
|
| The examples are countless. The world of meta-programming
| offers enough of them. The article gives the control
| statements as an example, as a hint to build the
| appetite, as is suggested in the intro, in fact.
| qsort wrote:
| I have done something exactly like this in production for a
| system that turned natural language into SQL. This was pre-
| LLM, so we had models that produced intent and keywords as
| structured output and we had to turn it into queries for
| several backends. The project didn't work out for a variety
| of reasons, but technically it was beautiful: it produced
| query plans that in many cases were identical to those from
| the queries analysts wrote by hand. So yeah, I accidentally
| wrote a compiler. Does it still count?
| PaulHoule wrote:
| Two good examples of "builders to SQL" are
|
| https://www.jooq.org/
|
| and
|
| https://www.sqlalchemy.org/
|
| JooQ isn't everybody's taste but I use it for my job and
| I think it's great particularly in that you can reuse
| expressions and write generators for complex queries. We
| have a powerful search interface that combines full-text
| with other kinds of queries ("Is about topic T", "Data
| was collected between S and E") that is beautiful. I
| think it's funny how JooQ has that lispy f(a,b) style (no
| accident it is like ferocity) and how Sqlalchemy is
| really fluent and takes advantage of operator
| overloading.
| dreamcompiler wrote:
| Currying is done automatically in Haskell but not in Lisp. If
| you wanted currying in Lisp you could write it, but Lisp
| programmers don't depend on or talk about currying as much as
| Haskell programmers do.
| mrbluecoat wrote:
| At least "the dead C" was a nice pun :D
| molteanu wrote:
| I find it is a real challenge to come up with a good title.
| On the one hand it should, probably, convey to the potential
| reader something about the contents of the article, on the
| other hand it should be something to differentiate it from
| the rest of the articles published on the same subject.
|
| I like those that read something like a punch-line, that come
| across as something different that just a summary of the
| article. But these maybe work best for literature, prose,
| movies, etc.
| Jach wrote:
| I don't think you're missing much. Yeah, the main point seems
| to be that if your language has closures, you suddenly can
| express a lot of things that were out of reach before. Not a
| new insight. But there's another point I think that is hinted
| at on the topic of control abstractions. Or at least I'm
| reminded of the topic. It's better and more succinctly and
| explicitly talked about in an early chapter of the free book
| _Patterns of Software_ :
| https://dreamsongs.com/Files/PatternsOfSoftware.pdf
|
| The extra point might be that more languages should facilitate
| defining your own control abstractions just as they support
| defining your own data abstractions. Functions are one way of
| making data abstractions, but languages often provide multiple
| ways. Closures are one way of doing a type of control
| abstraction (involving such things as delayed or multiple
| evaluation), but there are other ways too. For some reason we
| see value and a need for defining our own data abstractions,
| but not so much for control abstractions, even though
| (according to the book) once they were often co-designed, like
| Fortran's arrays and DO loop. And for some reason even in the
| few languages that do support making your own control
| abstractions, like Lisp, you'll still find users who disapprove
| of doing so, claiming all you need are the standard existing
| methods like looping, map/reduce style functions, and some non-
| local exits.
| PaulHoule wrote:
| There's closures and there's being able to transform the
| expression tree.
|
| Graham's _On Lisp_ is a really interesting book
|
| https://paulgraham.com/onlisptext.html
|
| which is allegedly about programming with macros but I'd say
| 80% of the time he implements something with closures and
| then makes a macro-based implementation that peforms better.
| That 80% can be done in Python and the other 20% you wouldn't
| want to do in Python because Python already has those
| features... And if you wanted to implement meta-objects in
| Python you would do it Pythonically.
|
| Graham unfortunately doesn't work any examples that involve
| complex transformations on the expression trees because these
| are hard and if you want to work that hard you're better off
| looking at the Dragon book.
|
| You can work almost all the examples in Norvig's Common Lisp
| book
|
| https://www.amazon.com/Paradigms-Artificial-Intelligence-
| Pro...
|
| in Python and today Norvig would advocate that you do.
| Jtsummers wrote:
| Norvig put PAIP up on Github a while ago:
| https://github.com/norvig/paip-lisp
| smcameron wrote:
| There's an upside to C's limitations. Several times I've seen
| expressed the notion that "every big Lisp project uses its
| own dialect of Lisp". This is less true in C, I think,
| because it doesn't have the power (ignoring preprocessor
| abuse, which does happen, see Bourne shell source[1]). C++
| has a bit more power, and there we see projects tend to use
| their own subset of C++.
|
| In both Lisp, and C++, taking some isolated snippet from a
| codebase, you can't quite really be sure what it's doing
| without reading the rest of the program because of what might
| be called the "excessive" power of the languages.
|
| In C, it is much more likely that you _can_ look at an
| isolated snippet of code from some codebase and be reasonably
| sure about what it is doing, and be able to extract this
| snippet more or less as-is, and re-use it in some other,
| unrelated code base. At least, this has been my experience,
| ymmv.
|
| [1] https://www.tuhs.org/cgi-
| bin/utree.pl?file=V7/usr/src/cmd/sh
| molteanu wrote:
| Well, yes, an often heard, and reasonable argument.
|
| But then what happens in a reasonably sized C project is
| that other tools are brought in to fill the gaps. Tools
| like code generators (see some other comments on this
| post), for example, which are necessarily written in a
| different language and, most importantly, come with their
| own terminology and way of doing things. Which, in the end,
| becomes an extra mental load, the kind that you've rightly
| mentioned. Only you've partitioned or spread that load
| horizontally in multiple little pieces as opposed to
| building abstractions on top of each other.
|
| Or, each piece of code is understandable by itself, but one
| has to read dozens of such "little islands" to make sense
| of what the code is achieving since, by necessity, you need
| to have many such pieces to achieve something meaningful.
| The alternative being the abstraction pyramid which does
| more with less code. The downside being, there is an
| upfront investment in understanding those abstraction
| layers before one can juggle with them, so to speak. In
| your example, the upfront investment is less so, but every-
| time you must do something meaningful, including reading
| the code, you pay the penalty of not having powerful enough
| tools. As an example: engineers go to universities to study
| calculus, physics and material science so that, with these,
| they can better plan and build a highway (let's say). There
| is a huge upfront investment in that, let's say 16 years of
| study. Versus, use our shovels and buckets and start
| building right away. That is easy to learn. You learn it in
| a day. You start building right away. Everyone can
| supervise and, thus, understand the work because it is
| straightforward. 1 day upfront investment. But the later
| will advance at a horrible pace. There is no way to
| visualize what you're doing. There is no study beforehand.
| Maybe the hill will collapse and you need to start over.
| Maybe the two ends of the road will not meet where you'd
| expect to. Maybe the materials crack after a week. Maybe
| the road is not even, so you start over. Etc, etc.
| Jach wrote:
| It's sometimes expressed and repeated but I think usually
| by people who don't do much or any Lisp programming. Like
| they hear Lisp is great for making DSLs and interpret that
| as every interesting program must have created its own
| wacky DSL and is basically incomprehensible outside of the
| program. That's not the case. At least in Common Lisp.
| Similarly I don't tend to see much (any?) "CL is _too_
| expressive and big and complex, thus we define a language
| subset and if you contribute /work here you must use said
| subset" attitude which is more prevalent in C++. Certain
| people and companies have their own stylistic preferences,
| sure. I could believe there are some out there that go all
| the way to requiring fset/immutable collections everywhere
| (Clojure-esque without switching languages to Clojure), or
| outright banning CLOS or lists or using conditions or ever
| writing your own defmacro or something (to try and put an
| analog on "no virtual methods, no linked lists, no
| exceptions, no preprocessor beyond #include" that I've seen
| for C++ guides). But none come to mind.
|
| What did just come to mind is Coalton, a project I like
| though haven't tried using seriously yet, which more says:
| stock CL is too _inexpressive_ for programs we want to
| write that make use of static algebraic data types with
| more compile-time guarantees. So here 's a macro wrapped in
| a library that's effectively a new language, but we didn't
| have to go build a whole new language, we could just build
| on top of CL, and we kept the syntax tasteful and
| familiarly lispy. The interop is great. If you see a
| project using coalton and you want to use their code in CL,
| you can. And vice versa, using CL from coalton, it's just a
| library. And it's transparently done, obvious what is
| happening / what you're doing. CL allows this flexibility.
| Most projects do not birth a new language to support
| something.
|
| Pick a random file from some larger projects that do come
| to mind: Kandria (commercial game), SHOP3 (a Hierarchical
| Task Network planner), Mezzano (an OS), Open Genera (an
| ancient OS), Maxima (computer algebra software that still
| uses code from the 80s), and it's pretty much all just...
| normal Lisp code. There are style differences, sure, but
| it's normal, easy to understand what it's doing
| mechanically. (i.e. the same as C -- even if you have no
| idea what/why it's doing at a more meaningful level, like
| what's a bessel function, there's some value just in
| knowing that a chunk of code idiomatically isn't depending
| on or doing too much crazy stuff outside the context.)
| Random short snippet: (defun lookup-
| reduction-label (obj) (let ((task
| (etypecase obj (primitive-node
| (operator-task obj)) (list obj))))
| (gethash task *reduction-labels*)))
|
| Yup, pretty normal looking to me. Might be better as a pair
| of defmethods. Easily understandable mechanically even if I
| don't yet know what a primitive-node is, or any of the
| application specific concepts really. I don't fault C if I
| don't know what a vma->vm_page_prot is, I just know it's a
| field on a vma struct that's getting dereferenced, and
| nothing else super crazy because -> can't be overloaded in
| C.
|
| You'll find the occasional macro in such lisp projects, but
| it's unlikely to be an impressive display of macrology that
| requires sitting down to study it. Same thing with most
| popular libraries. Most Lisp code doesn't actually have
| much "spooky action at a distance stuff" an otherwise
| innocent looking line of C++ can be known for like implicit
| conversions, copies, confusing precedence, or overloaded
| operators. Even Lisp's take on exception handling (the
| condition system) isn't so spooky despite allowing more
| control flexibility, simply because it doesn't
| _automatically_ unwind the stack and allows for restarts --
| you can resume execution from where the condition was
| signaled. The worst you 'll tend to see somewhat frequently
| are method calls that may have surprising
| :before/:after/:around dynamics. But now here's my Java
| apologist talking: Lisp can be thought of as more of a
| programming system than a language, and as such in practice
| the context of working with Lisp is with a Lisp-aware
| editor, so it's not exactly fair to compare it (or modern
| Java) with another language if your arena of choice is
| black and white paper print-outs on a desk rather than a
| typical working environment. This means, to address that
| spooky action possibility, you can resolve your curiosity
| at any time by calling compute-applicable-methods and
| compute-effective-method. In addition, you have things like
| intellisense on demand for symbol/function/macro
| documentation, ability to jump-to-source (even of the lisp
| implementation's functions), ability to cross-reference
| callers and callees of something, ability to macro-expand
| any macro call, ability to disassemble a function, ability
| to compile changes to functions, and so on and so on. This
| is also all pretty much built in to the language itself
| ("compile-file" is a function available at runtime) and
| just more conveniently exposed via Lisp-aware editors.
|
| I don't do much C anymore. Three projects that came to
| mind: sqlite, the linux kernel, and atril (pdf viewer
| bundled with mate desktop environments, forked from the old
| gnome2 evince). Take random files from these projects. I
| don't think you can really claim that you can just take
| snippets more or less as-is and use them in some other
| project. Part of the problem again stems from C's lack of
| expressive power along with other weaknesses like an
| impoverished runtime -- which larger projects are going to
| feel the pain of even more acutely, and thus use different
| and incompatible methods of solving that. But the other
| part of the problem is just a nature of any project in any
| language: it's made up of its own data abstractions that
| are highly relevant to that project. Re-use at the snippet
| level is rare. You aren't going to gain anything from (dart
| at wall) sqlite's mutex.c file if you try to cherry pick
| snippets. Everything depends on its own struct (which is
| different for three platforms -- hey, that's useful to
| _study_ and maybe copy, but there 's enough specific sqlite
| stuff even in the short definitions that you can't
| literally just copy the code over), you can't even re-use
| anything that allocates because it calls their own sqlite
| malloc that does whatever differently from a system
| malloc... Picking a random linux kernel file,
| io_uring/cancel.c, what are you going to be able to extract
| from this? It's straightforward C code, yes, but it's
| intimately tied to the context. There are references to
| mutexes in it -- how much do you want to bet you can't just
| use sqlite's notion of a mutex in place, or vice versa?
| (Sqlite uses a pthread mutex; needless to say the struct
| layout is not compatible with the kernel's mutex.)
|
| Atril came to mind because last year I hacked it so it
| would show me a time estimate of how long it'd take to
| reach the end of a book when I have it auto-scrolling. I
| dived in and noticed types like gboolean and gint. Oh
| great, custom typedefs over basic things, like so many
| other C projects, what do I have in store... at least
| they're sensible.
| https://github.com/GNOME/glib/blob/main/glib/gtypes.h#L56
| But yeah, to make a long story short, it uses glib. glib is
| an amazing and kind of cursed library to bring a lot of
| higher level language features to C. Except (at least to
| me, encountering it with the goal of understanding code for
| the first time) without the normal tools of high level
| languages that aid in development and understanding. I
| pieced what I needed to piece together and made my hack
| work, but it involved quite a few printfs. (Incidentally,
| Common Lisp includes the standard function "trace", which
| you can call at any time, applied to a function, and from
| then on when that function is invoked its input args and
| output values will be printed. You can "untrace" it later.
| Editors have hot keys.)
| fifticon wrote:
| I see mixed comments, so let me add some praise. I am one of
| countless, who match his intro-filter: repeatedly hearing
| 'enlightened' people lament that the vast masses don't "get" lisp
| and FP, and repeatedly attempting/failing to pick up the red
| string myself.
|
| background - I am a computer science major with 30+ years
| experience. I did do a mandatory class of 'implement your own
| lisp' many eons ago. It just never really 'clicked' for me. I do,
| by accident, assimilation and lazyness,employ FP style designs in
| my software. And I guess fp techniques gradually rub off on me
| from e.g. javascript, lambdas,closures, and map-filter-reduce. in
| particular, lambdas are useful to me. But I am one of the guys
| who continue to read the "let me tell you what monads really
| are", and every time I fall off the bicycle. So, well, I
| appreciated this 'Xfor 5year olds" :-)
| jmkr wrote:
| I think Lisp is more on the liberal arts side of programming
| languages.
|
| That the "enlightenment" of Lisp is that you can use functions
| everywhere. Write macros that look like functions and modify
| behavior, and build your code as a language.
|
| Things like monads are more on the evolution of functional
| languages, and I also fall off the bike. It's as difficult as
| you want it to be, and I find scheme and lisp to be easier high
| level languages than javascript or python and makes more sense.
|
| The forward and preface to SICP is good reading.
|
| https://mitp-content-server.mit.edu/books/content/sectbyfn/b...
|
| The Dan Friedman books are pretty good in general: "The Little
| Schemer," and the sequel "The Seasoned Schemer" which are both
| more "recursion" books. He also has another book "Scheme and
| the Art of Programming." Which I think is a great comp sci book
| that's not too difficult and doesn't seem too well known.
|
| How to Design Programs is supposed to be a pretty good comp sci
| intro:
|
| https://htdp.org/2024-11-6/Book/index.html
| molteanu wrote:
| If we're naming names, for me personally, Lisp in Small
| Pieces by Christian Queinnec tops my Lisp books list. But,
| yes, only after perusing the SICP and The Little Schemer
| first.
|
| "Liberal arts," nice :)
| msla wrote:
| Lisp is "functional" in a 1970s sense in that it has functions
| as first-class objects you can pass as parameters to other
| functions, but those functions are basically subroutines which
| can have side-effects and un-functional behavior. As you allude
| to, this is pretty much par for the course in procedural
| languages now, and OO As She Is Spoke is procedural with some
| extra stuff added. Even garbage collection is common enough now
| that languages which don't have it trumpet the fact and make it
| their whole personality. Lisp is heady and revolutionary if
| your baseline is FORTRAN and maybe C, in other words, unless
| you actually do begin to write your own macros, at which point
| the C people begin to look at you funny.
|
| Haskell is functional in that it demands its functions be
| functions, not subroutines. A function has inputs mapped to
| outputs and no side-effects. Functions can be composed and
| composition always works. Haskell uses monads to represent the
| regrettable fact that having an impact on the outside world is,
| in a very real sense, a side-effect, so it marks all side-
| effecting functions with an indelible stain. Haskell requires a
| different mode of thought from Python, or even from C++, and
| it's definitely not another Lisp.
| baq wrote:
| my epiphany with lisp was that it is not a functional language
| in the modern sense, i.e. mutability is fine, loops are fine,
| etc. it's primarily a list processor, not lambda calculus.
| timewizard wrote:
| typedef int fn_t(int, int); int iff(bool cond, fn_t
| a, fn_t b) { if (cond) return(a());
| else return(b()); }
|
| Now just write the implementation in terms of a() and b(). I
| don't get it. C doesn't have convenient syntax but this is
| compiled and not an evaluated language. This argument didn't make
| sense to me.
| dawnofdusk wrote:
| The author of this blog is clearly eloquent and, as per their
| interspersed quotations of David Hume and others, it is
| refreshing to see someone so well-read in the software/tech
| blogosphere.
|
| I love Lisp. The last few paragraphs are a pretty good
| description. It's nice to have a very flexible set of tools,
| instead of being forced to conform to object-oriented design or
| whatever paradigm. IMO the only legitimate reason in sticking
| steadfast to a design paradigm is for performance reasons, but of
| course this can only really justify array programming/imperative
| programming. But at the point where you want some flexible
| abstractions, it's nice to have the power to do introspection,
| delayed evaluation, and so on. Disclaimer: my background is
| physics/math, so function abstractions are much more intuitive to
| me than objects, or whatever other structures are taught to CS
| students.
| kazinator wrote:
| The author may be eloquent, but unfortunately calls imperative
| operators, like while, "functions".
| layer8 wrote:
| Imperative code is a function from one program state to
| another.
| int_19h wrote:
| Coincidentally R is one language in which `if` and `while` can be
| written as functions, because all function arguments are lazily
| evaluated, and one can get access to the underlying lambda for
| repeated re-evaluation. In fact, `if` and `while` _are_ functions
| in R, and you can call them as such if you properly quote the
| keyword so that it 's treated as an identifier. And then the
| familiar C-style syntactic forms are just syntactic sugar for
| function calls.
|
| R takes it up a notch though by making _all_ syntactic constructs
| boil down to a function call. Function definitions are themselves
| calls, for example, and so are assignments and even curly braces.
___________________________________________________________________
(page generated 2025-06-27 23:01 UTC)