Hacker News new | ask | show | jobs
by Jach 362 days ago
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.)