top | item 44398490

(no title)

smcameron | 8 months ago

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

discuss

order

molteanu|8 months ago

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.

lproven|8 months ago

> This is less true in C, I think, because it doesn't have the power

It may be, but even so, C is famously unreadable to the extent that it's a common observation that people can't read their own code from a few months earlier.

I think there is an argument -- one that would be very unpopular among Lisp folks, I suspect -- that says that for languages to enforce some forms of simplicity would help their uptake and use.

Most language advocacy revolves around poorly-defined notions of "power" where more power is seen as desirable.

I think Niklaus Wirth was on to something when he took the ideas of Pascal and kept simplifying as well as improving: Modula, Modula-2, Oberon, Oberon 2, and finally Oberon-07. (There are more -- Active Oberon, Zonnon -- but Wirth had retired by then.)

Jach|8 months 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.)