1. 33

  2. 22

    As much as I love lisps, ‘totalitarian’ and ‘policing’ is a bit of an odd spin to put on languages with a more constrained operational model. You know what’s a really totalitarian move? Your program crashing. The CPU, that dictator, doing exactly what you told it and shitting the bed as a result.

    When a language enforces strong, static typing, memory safety, or immutability, it doesn’t feel like I’m being policed; it feels like I’m using the computing resources in front of me to improve my chances that my program will work once it’s running.

    1. 44

      You know what’s a really totalitarian move? Your program crashing. The CPU, that dictator, doing exactly what you told it and shitting the bed as a result.

      You know what’s really totalitarian? Killing political dissenters. “Totalitarian” is a silly word to apply to programming languages.

      1. 7

        In line with the author’s distinction between Common Lisp’s, erm, adaptability, I suppose? and the “grand ideas” languages, I’d suggest replacing the obviously out-of-place “totalitarian” with the much nicer “hedgehogs-like”.

        I’m nodding at Isiah Berlin’s The Hedgehog and The Fox (itself an allusion to a much older line) here, although nowadays Philip Tetlock’s study about political experts and forecasters may be more widely known nowadays. I’m specifically mentioning Berlin’s essay, though, because Tetlock sort of makes a value judgement in his study, whereas Berlin is far less categorical in this regard, and it’s sort of what I’m trying to do here, too.

        By analogy with Berlin’s classification, “hedgehog” languages, like Haskell or Rust, or more generally, hedgehog systems, like Unix (“everything is a file”) have this single, elegant, unifying philosophy, a sort of lens through which they view everything, whereas “fox-like” languages like Perl, Python, Common Lisp or C# have no single grand idea that permeates their entire design. (One could argue that C#‘s grand idea is OOP but there are plenty of functional idioms in it, for example; I guess C++ would’ve been an even better example?)

        Both have good and bad sides – this is why I specifically cautioned against following Tetlock’s analogy too much (in his study, foxes are better at forecasting than hedgehogs, but this is really not about forecasting).

        Lots of “hedgehog” languages have a well-earned reputation for elegance and solid design, and they push all sorts of things forward. But grand unifying theories fare no better in computer science than they do in physics: oftentimes, reality just won’t bloody fit their elegant design, and elegant designs often get a bloody nose when they run into the real world. This is further complicated by the human factor, as people tend to get attached to their tools more than it’s healthy, and the results are often baffling.

        “Fox” languages may feel more pragmatic, and the fact that they’re not attached to a particular world model means that sometimes they really are substantially better at handling things that just don’t fit the damn model. But lack of a “higher” unifying idea in a language often descends upon the programs written in that language, too, and they can end up messy. This, too, is further complicated by the human factor: just because they have multiple tools at their disposal doesn’t mean people will end up using the right one, and all this flexibility sometimes encourages them to do things just because they can, without wondering if they should, too.

        Also, hedgehogs and foxes are really cute and that alone would suffice, just sayin’ :-D.

        (Edit: obviously, this is an informal classification :P. One could argue, for example, that Common Lisp belongs in the “hedgehog” category because homoiconicity is the grand idea around which the entire language is built. Like any classification that isn’t based on numbers, it’s hard to say exactly where the border is, but that doesn’t make it any less useful, because most of the time, it’s pretty easy to figure out when two languages (say, Rust and C++, or Haskell and Common Lisp) are on different sides of the border.

        1. 6

          I reject the fox/hedgehog classification. Haskell and Rust are every bit as general purpose as Common Lisp. They can be used for all of the same problems.

          In addition to the syntax, Lisp’s grand unifying idea is that everything can be represented by (mostly) mutable, garbage collected objects, with a notion of object identity that you can test using ‘eq’. No other language worked this way when Lisp was introduced.

          The fact that this idea has become mainstream does not change the fact that Lisp’s “everything is a garbage collected mutable object” is not a good fit for every problem. Haskell and Rust stand out by the fact that they don’t use this model. Haskell is based on pure functions and immutable values. Rust also has elevated support for immutability, and is not garbage collected.

          Lisp’s grand idea works great for single-core CPU programming. It’s terrible and unusable for GPU programming. Shared mutable objects are not great for concurrent programming in general. That’s where programming with immutable values instead of mutable objects has clear benefits. There are also lots of contexts where garbage collection is a liability, and where Rust is moving in.

          1. 1

            The point of the classification is not general purpose vs. special purpose, just how many different, sometimes conflicting ideas a language accommodates. For example, Haskell is obviously just as general-purpose as Common Lisp, but while Common Lisp happily includes both imperative and functional idioms, Haskell is far less willing to accommodate the former. Similarly, C++‘s memory model, far less constrained that Rust’s, will accommodate plenty of things that Rust won’t. Not that it’s a good thing.

        2. 5

          I suppose a CS forum would be among the tougher crowds for metaphor.

          1. 11

            Calling a language “totalitarian” is like me calling you a Qanon supporter: while there might be some technical denotative merit to the analogy, it’s so hyperbolic to completely ruin the point you’re trying to make. There’s a difference between a metaphor and a good metaphor.

            (You’re just like a Qaon supporter in that I disagree with you.)

            1. 2

              I can’t get behind this totalitarian language policing, even if it weren’t a good metaphor, which I think it is.

              1. 6

                Calling a metaphor bad isn’t totalitarian language policing, it’s criticizing the quality of the article. Which is a totally normal and reasonable thing for writers to do. Anyway, this is now pretty off-topic for the OP, feel free to DM if you want to continue chatting aesthetics.

        3. 4

          also both of these languages do allow you to do whatever nonsense you would like, with unsafePerformIO in haskell and unsafe blocks in Rust.

          1. 2

            Yeah this seems like the same bad argument against static typing - you can just use Object/Any types and voila you have dynamic typing. As you say, these constructs are a superset of what was possible before and arguing that they limit you is silly

            1. 4

              I don’t think this is a great response. Sure, you can emulate some features of dynamic types with Any everywhere, but the language isn’t holistically designed around dynamic types, so you’re swimming upstream when you want to do anything. Kinda like how you can emulate HOFs with the strategy pattern, but it’s not the same as having HOFs as a primitive.

              1. 2

                You say emulate some features - are there any features of dynamically typed languages that you can’t emulate in a statically typed language? Sure, additional boilerplate of Any is annoying. But the article talks about certain things being impossible to do in languages, not just annoying.

                1. 2

                  Sure, some dynamically typed scripting languages can have programs rewrite their own code as strings and immediately interpret it without losing state. You can technically do that in Haskell by writing a dynamic Haskell interpreter and running that, but I’d call that a whole new language.

                  Side note, I think Haskell has some dynamic typing features that I haven’t seen in actual dynamic languages, like witness types. You can have a value become untyped and later cast it (or a computation result) back to the right type because it was witnessed.

              2. 2

                Technologies have affordances, because technologies are opinionated. So, yes, you can reproduce certain alien patterns in various languages, but you’re going to be fighting uphill the whole time.

                1. 2

                  Sure, but the article frames them as being impossible to do “Encouraging you to avoid something, however, is quite different from banning it outright.”

            2. 3

              The CPU, that dictator, doing exactly what you told it and shitting the bed as a result.

              Dictator being defined as something that does exactly what I tell it sounds a bit odd.

            3. 13

              It’s funny, the one thing that hooked me on Erlang/Elixir was exactly the ‘hotfix tiny portions of code in a live system’ workflow it encourages. I love it, you see how the new code responds to the existing state – within a second of saving your changes. You do this 100 times in a few minutes, and in terms of understanding your system, you feel like you’ve saved an hour compared to a traditional “save, compile, rerun, look at error” workflow.

              This “truly interactive” is great, we need more of it, and clearly you can get a “truly interactive” experience without being Common Lisp!

              1. 9

                “True interactive development” is also called “live coding”. I would like to see more languages support this.

                The first section could also be titled “Hell is other languages”. Rust and Haskell are totalitarian because they don’t let you write code in the Lisp style. Lisp is not totalitarian, because it fully supports the Lisp style of coding, which is the only style of coding that anybody should use. If you don’t program in Lisp style, then you are yielding to the external pressures of your favourite language to adopt false values and disown your innate freedom as a sentient human being. (WTF!) These are the words of someone who really likes Lisp, and feels no need to think outside of that box. I personally dislike common lisp because it is designed to inflict infinite pain on people who prefer to program in a pure functional style.

                1. 3

                  I fully recognize that cl is not intended as a pure functional language. In what way does it actively prevent pure functional programming? (Honest question from a long ago sometime cl user)

                  1. 10

                    There’s no support in the standard for proper tail recursion (as there is in Scheme). So implementations don’t need to support it. But this is the primitive form of iteration in pure functional programming. (ABCL doesn’t support it at all. Some CLs support it in the compiler but not the interpreter, which is a problem if you want to enjoy “true interactive development”. Even if you make it work, you can’t really share your code with the CL community.)

                    Higher order functions and combinator style programming are a key part of pure functional programming. Although this is possible in CL, the syntax punishes you and reminds you that you are doing things wrong if you try to do this. The fact that functions and data use different namespaces is a barrier to use, because you need to use special ugly syntax to convert a function name to an expression, or to call a function stored in a variable.

                    Curried functions are not natural in CL syntax.

                    There isn’t good support for working with immutable data. See Clojure for a Lisp that does support this.

                    I haven’t tried to make CL work as a pure functional language, but given the evident lack of support for this programming style, I expect that if I were to use pure FP idioms to write CL code, then the code would likely have terrible performance compared to the same code compiled by a pure FP compiler, since compilers and runtimes are optimized for the idioms that people actually use.

                    1. 3

                      Yeah actually that sounds spot on. Although cl weenies would probably argue that y combinator, trampolining, and macros is a complete replacement for tail recursion.

                      1. 4

                        Clojure weenies seem to argue that trampolining (i.e. the recur special form) is a complete replacement for tail recursion ;)

                        edit: To be fair, they have something of a point - tail recursion requires careful construction of your code and you can’t see when you’re breaking tail recursion. So having an explicit special form like recur makes it visually obvious that you’re using tail recursion (and if you’re not, the compiler can complain about it). As a Schemer, the problem I find with that is that it’s less elegant, and (right now?) it doesn’t support mutual tail recursion.

                        Even if it were possible to have mutual tail recursion with a special form, it is by definition cooperative as in that all involved procedures need to be written in a special way - with implicit tail recursion, two completely unrelated procedures (even from different libraries) can call eachother (for example, if you pass one to the other, as with a HOF) without knowing about eachother. You can also get “accidental” or “serendipitous” tail recursion where the programmer might get it without having asked for it, which provides a nice performance bonus across the entire program, instead of just the hand-optimized bits.

                        1. 2

                          Clojure weenies seem to argue that trampolining (i.e. the recur special form) is a complete replacement for tail recursion ;)

                          Some Clojure people make that argument, but the better argument is that lazy sequences are used to solve 95% of the problems that Schemers use TCO to solve.

                          1. 1

                            I don’t understand how the two relate. In Scheme we have force/delay in the standard and SRFI 41 (streams), which seem to me to be the Scheme equivalent of lazy sequences. I don’t see how streams can be used to replace TCO or vice versa.

                            1. 1

                              It’s not replacing TCO in all its applications, it’s replacing the use of TCO to accomplish iteration over a sequence of elements which could in some cases hypothetically be unlimited. Since laziness is the default for sequence operations, Clojure encourages you to frame all kinds of problems in terms of sequences which would in other languages be framed in terms of recursion or imperative for loops.

                              For example, playing thru a board game would be structured as a reduce over an infinite lazy sequence of player moves in Clojure, but it might be structured as a recursive function in Scheme.

                              But it doesn’t help for, say, that trick where you use TCO to implement a state machine.

                          2. 2

                            Imagine updating a library and accidentally disabling tail recursion where you were risking the stack.

                            Serendipitous tail recursion sounds like a footgun.

                            1. 1

                              In Schemes where continuations are fully first class, there is no “stack”. The (mutual) recursion would just eat more memory, and you’d notice that your program was getting slower or eating more memory over time, and treat it like any other performance regression.

                  2. 8

                    As someone whose written Lisp, Ruby, and Node on one side, and Go and Haskell on the other, I’ve always been curious about this apparent split between strongly typed languages and languages with really good interactive connect-to-production drop-into-a-useful-debugger-on-crash REPLs. Is this intrinsic to the language, or just a result of differing focuses on the tooling?

                    I’m currently leaning towards “this is just a side effect of tooling focuses”, although I have yet to see even a really compelling UX sketch for a debug-it-in-prod REPL for a typed language. (For example, what would it mean to rebind a name to a value of a different type while paused computations may still refer to that name after resuming?)

                    1. 5

                      It depends what you mean by “debug”. If you want to inspect values, execute functions, modify value of same type - that’s supported by a lot of languages. GDB will let you do all three.

                      But the idea of using a value of a different type is… interesting. I don’t see why would you ever want to do that in production. You can of course replace an object with a vtable-compatible one. (Or however your language does dispatch) But a completely different type? Technically: You’d need to also replace all the following functions to match the change and all places which produce that value. Practically: Why not take a core dump and debug this outside of production instead?

                      (An incompatible type also doesn’t make sense really - the code won’t do the right thing with it in about the same way a dynamically typed language wouldn’t)

                      And finally as a devopsy person I’d say: “if you feel the need to drop into a debugger in production that means we should improve tracing / logging / automated coredump extraction instead.”

                      1. 2

                        I’ve always been curious about this apparent split between strongly typed languages and languages with really good interactive connect-to-production drop-into-a-useful-debugger-on-crash REPLs

                        This split is indeed interesting and it seems deep and hard to overcome. Languages emphasizing static typing provide a certain level of confidence, if it typechecks it is likely correct. But this requires an enforcement of type constraints across the program otherwise that promise is broken.

                        Interactive languages are more pragmatic. In Smalltalk, modifying a method in the debugger at runtime will compile and install it in the class, but existing instances of that method on the callstack will refer to the old one until they are unwound. There basically is no distinction in runtime / compile time which in practice works very well but is not really compatible with the confidence guarantees of more statically typed approaches.

                        I always wondered what a more nuanced compile time / runtime model would look like, basically introducing the notion of first-class behavior updates like Erlang. In different generations of code different type constraints apply. Which in turn means that for soundness the generations wouldn’t really be able to interact.

                      2. 4

                        This person doesnt understand what Bad Faith actually means. Many people including GP are now using this term as a simple perjorative just to describe things that they don’t like or disagree with. Something similar has occured with the term ‘gaslighting’.

                        1. 2

                          You should take that up with Albert Camus

                            1. 2

                              I’m aware.

                        2. 2

                          You could say most of the same things about Python.

                          1. 10

                            Python’s error recovery and auto-reload is nowhere near as sophisticated as Common Lisp’s. Yes, there’s postmortem debugging (with ipdb and %debug) and %autoreload in IPython, but these are both hacked-on extensions that aren’t as well integrated into the language. A Common Lisp REPL will automatically pause the execution of the program at the point where it died. This allows you to actually fix the state of the object, and continue the computation. Python remembers the callstack when an exception is thrown, sure, but it doesn’t allow you to pick up where it left off directly before throwing the exception. You can place a breakpoint and fix the state before the exception is thrown, but that’s not the same thing.

                            1. 3

                              Run your program with python -m pdb and you’ll drop into a debugger where you can inspect stuff, change it, and restart the computation. All part of stock cpython.

                              1. 1

                                It’s slow, you loose state (=> not truly interactive as in image-based development, the program only stops and restarts) and you are not inside your editor. It is not a first-class experience, but the presence of this switch shows it’s a good thing to have. Moreover, CL has some interactive tools (interactive debugger, stepper, inspector, tracer…).

                                1. 1

                                  Image based development is orthogonal to interactive/restart debugging. Image based development is also not a part of the cl standard. For example abcl doesn’t support it.

                              2. 1

                                While not practical, you could implement this in theory with approach similar to pyrasite. (It can do shell injection into a running script) It would be another hacked-on solution, but mentioning it just in case someone wants to have fun.

                              3. 5

                                Sorta, but the interactiveness of Python is more hacky which leaks from the seams sometimes. In Common Lisp I believe it’s a significant part of the core design.

                                1. 1

                                  What would you say is the leakiness of pdb? Or the missing strength of the cl restart system?

                                  I’ve used pdb a lot and cl only a little. In particular I find nested restarts confusing.

                                  1. 3

                                    I have to do something to have pdb enabled, and then I’m in a rather different environment from a standard python interpreter. Whereas in common lisp, the debugger/environment is always there to fall on and it’s not a separate part of the system.

                                    Can pdb freeze execution during a raised exception, let you change the code and then rerun the same piece again?

                                    Granted, even though I code in python quite a lot (possibly more than any other language), I’ve never used pdb – so I’m probably somewhat myopic to its possibilities. I should really learn to use it some day, but so far print debugging has been too easy and effective.

                                    1. 2

                                      Can pdb freeze execution during a raised exception, let you change the code and then rerun the same piece again?

                                      Yes, although execution will start from the point where the exception was raised

                              4. 2

                                For me this excellently highlights how great lisps are at meeting the needs of an individual, no matter what they’re creating. On the other hand it seems to ignore the collaborative side of development where you’re working on a team of 10 or 20, or maybe even 100+. As the team grows, I think it’s beneficial to have some built in rules (static types, beam error handling, etc) to help guide the team to a common goal. I think it’s also beneficial to use multiple languages in a project, where appropriate. Sometimes thinking in a different language gives you a whole new perspective on the problem(s) you’re solving.

                                1. 3

                                  Python has been extremely popular without any static type checking. People went along with unit tests and production errors. So it feels strange to read that Lisps are ignoring something. Moreover, SBCL throws many static type errors and warnings at compile time (when we compile one function with a keystroke). It is already super useful and we could super charge our program with even more compile time checks.

                                  1. 3

                                    I meant the post ignores collaborative development, not lisp ignores. The post focuses on the individual developer yet ignores that there’s also a lot of collaborative development and sometimes a tool that is excels one area is deficient in another.

                                2. 2

                                  Like this post from Paul Graham, I don’t think the article here is wrong so much as out of date. Even though Lisp had a lot of this stuff early, there’s nothing unique in Common Lisp, and for the most part even the combination of features isn’t unique.

                                  python -m pdb does everything this describes (it doesn’t have recursive restarts, not discussed here). I’d go so far as to say Python has everything Common Lisp has except for procedural, syntactic metaprogramming. Every other feature exists in multiple other non-lisp languages.


                                  1. 4

                                    Ruby has an edge in this area. In particular Ruby has good support for making domain specific languages, which still have the full power of Ruby available. It’s clunky and not as easy as lisp, but it can be used to great effect (see rspec).

                                    On the other hand, Ruby’s implement of meta programming is a massive landmine to new Rubyists who haven’t learned the painful lesson of using it in moderation.

                                    1. 3

                                      I’d go so far as to say Python has everything Common Lisp has except for procedural, syntactic metaprogramming.

                                      Does modern Python have multimethods? Lisp enables one to specialise methods on more than their first argument. Does it have update-instance-for-redefined-class? Lisp enables the programmer to update existing instances when a class is changed (I remember this biting me a lot back when I programmed in Python). Does it permit one to change the class of an object?

                                      Maybe it does — it’s been awhile since I programmed professionally in Python. But I suspect that the capabilities above are still pretty rare.

                                      1. 1

                                        Incidentally what do you use class changing for? I’ve only ever found it useful in deserialization code.

                                        1. 2

                                          It is handy when one realises that an object needs to be changed, without wanting to have to track down every single reference to it. The most common instance when I change an object’s class is when I want to make it into and instance of a super-, sub- or adjacent class.

                                          It is unlikely I would ever need to change the class of an object from, say, WEB-SERVER to USER, but it is more likely than I might change one from WEB-SERVER to TLS-WEB-SERVER.

                                          (I assume you mean changing the class of instances, not updating instances for a new class definition)

                                          1. 1

                                            That makes some sense. Thank you!

                                            In other languages we would use some kind of interface or hook based pattern to handle that kind of change.

                                            1. 1

                                              That’s what Lisp does: the hook is the UPDATE-INSTANCE-FOR-DIFFERENT-CLASS generic function (called by CHANGE-CLASS).

                                              As a user, you define a method on it, specialised on the old and the new classes, and it will automatically be called. It is that simple!

                                              This sort of thing is really useful for long-running, interactive systems. Not so useful for scripts which run and then are done.

                                        2. 1

                                          Multi methods? No. Change class - possible in 2.7 iirc. Patching the class will automatically cascade to instances where there isn’t a shadowing instance variable. If you need more you could easily set a meta class to provide that.

                                          1. 1

                                            Multi methods? No. Change class - possible in 2.7 iirc. Patching the class will automatically cascade to instances where there isn’t a shadowing instance variable. If you need more you could easily set a meta class to provide that.

                                        3. 1

                                          Lost me at “existentialist embrace of the hoplessness of programming complexity”… Sounds like a losing philosophy, to me.