1. 34
    1. 10

      Excellent post, I strongly agree with this. One of the reasons I enjoy Elm is its radical simplicity (and, relatedly, its stability). I’ve been using Elixir more recently, and there’s a clear overhead to learning it and using it in comparison.

      Some of its complexity seems particularly unnecessary - eg there shouldn’t really be special syntax for calling anonymous functions, it’s not great to have different ways to access fields in structs vs maps, and so on. Possibly this highlights the “vertical” aspect of complexity - everything sits on top of a deep stack of other stuff these days. Perhaps the design of Elixir was constrained by the peculiarities of Erlang and OTP.

      Elixir still takes a commendably conservative approach to features in comparison to other languages like Rust, TypeScript and C++.

      In general, I feel like there’s an extremely cavalier approach to complexity at all levels of our industry, whether it’s designing PLs or writing software using them. That’s probably why, no matter how much tools and technologies seem to improve (on paper), it still takes a long time to create even an ostensibly simple piece of software.

      1. 14

        I’m finding that the most complex part of learning yet another language isn’t the language, it’s the tooling and ecosystem.

        TypeScript itself was fun and easy … but figuring out that config file the translator uses was annoying, and ditto for TSLint and Jester, and don’t get me started on the seventeen flavors of JS modules. Stuff like Grunt can die in a fire. (I now understand why some folks go nuts for radical simplicity and end up living in a cave running Gopher on their 6502-based retro computer: Node and the web ecosystem drove them to it. So much worse than C++, even with CMake.)

        1. 3

          Yes, that is true but I think there’s a compounding effect on complexity which means complexity at all levels is important to minimise. As an example, a more complex language requires more complex tools and more attempts at getting the tools right, which leads to the proliferation you mention. Some features can instead lead to proliferation of libraries (Smart pointers or regular pointers? Which of fifteen flavours of strings? Macros or regular functions? Etc.)

          Putting languages aside, isn’t it the same kind of disregard for complexity in all other areas (protocols, APIs, libraries etc.) that ultimately results in unwieldy ecosystems?

          We tend to underestimate how small bits of complexity compound and snowball, I think.

        2. 3

          I strongly agree with this. Coming from Clojure on the JVM, I found it much more difficult to work on a project written in ClojureScript (supposedly the “same” language on a different runtime) than it was to learn Mirah (completely different language on the JVM).

          Of course there are other factors like completely different paradigms that come out of left field (logic programming, category theory, etc) to make things difficult, but other than that, once you’ve got thee or four languages under your belt, learning a new one is much easier than learning a new runtime.

      2. 4

        Some of its complexity seems particularly unnecessary - eg there shouldn’t really be special syntax for calling anonymous functions, it’s not great to have different ways to access fields in structs vs maps, and so on. Possibly this highlights the “vertical” aspect of complexity - everything sits on top of a deep stack of other stuff these days. Perhaps the design of Elixir was constrained by the peculiarities of Erlang and OTP.

        Joe Armstrong discussed this in an article.

        It’s because there is a bug in Erlang. Modules in Erlang are sequences of FORMS. The Erlang shell evaluates a sequence of EXPRESSIONS. In Erlang FORMS are not EXPRESSIONS.

        The two are not the same. This bit of silliness has been Erlang forever but we didn’t notice it and we learned to live with it.

        https://joearms.github.io/published/2013-05-31-a-week-with-elixir.html

        1. 2

          My Erlang is super rusty so I’m a bit fuzzy on this, but it looks to me like “forms” in this context basically means what other languages call statements; is that accurate?

          It’s refreshing to see someone admitting their mistakes so openly.

      3. 2

        In general, I feel like there’s an extremely cavalier approach to complexity at all levels of our industry, whether it’s designing PLs or writing software using them.

        This has been frustrating me to no end, especially in the last ~4 years. My pet theory is that the industry has been overtaken by a “tech consumerist” mindset, where the impact of incorporating new, shiny “tech products” into your codebase is greatly overestimated, while the cost of the added complexity is underestimated.. Curious to hear if you have a take on how this cavalier attitude became so prevalent.

        1. 2

          Very few major projects are considered minimalist. The knock on effect of this is that steers the imagination away from minimalism. From a maximalist viewpoint, PLs such as Lisp, Smalltalk, or ML look like toys because there seemingly isn’t enough stuff to learn.

        2. 2

          It’s often a question of choosing where to put complexity. In Verona, we’d really like to have Smalltalk-like user-defined flow control (though with static typing so that it can all be inlined in an ahead-of-time compiler with no overhead), where a match expression and a function call / lambda, and (checked) exception throwing / catching are the only things in the language and everything else is part of the standard library. This is a great thing for having a minimal language and also means that complex control flow constructs such as a range-based for loop for iterating over every other element of a collection in reverse order are as first-class as while loops.

          We pushed quite a long way on this, but in a language that has linear types they add a huge amount of complexity in the type system. For example, consider this trivial case:

          var x : iso; // Linear pointer to the entry point of a region.
          if (a)
          {
            doSomethingWith(x);
          }
          else
          {
            doSomethingDifferentWith(x);
          }
          

          The two lambdas (braces are used to define zero-argument lambdas in Verona) both need to capture x, but x is a linear type and so they can’t both capture it. There are some details here around the fact that else is actually an infix operator applied to the result of if here that I’m going to ignore for now but even without that, we need to be able to express in the type system that:

          • The if...else construct takes two lambda that take zero arguments.
          • Each lambda must be callable once.
          • We don’t need to be able to call both lambdas together and so it’s fine for calling one to implicitly invalidate the other.
          • The construct will definitely call one or the other, and so any properties that are true at the end of both (e.g. that x is still valid and was not consumed) can be assumed for type checking the surrounding block.

          This is incredibly complex to make work at all, making it work and be something that mere mortals can use is basically impossible, so we’ve punted on it for our MVP and will build some control-flow constructs into the language.

    2. 3

      I suspect the most radical minimalism is not to design a new one to start with.

      1. 4

        Where’s the fun in that?

      2. 3

        Ding ding ding!

        every new language makes things locally better, but globally worse. Because you never have a system in just one language. If you think you do, then your view of the system is too narrow.

        https://news.ycombinator.com/item?id=16741043

        1. 3

          Glad you mentioned COM there.

          I believe it’s time has almost come (again). The amount of resources squandered on writing yet another HTTP library is probably enormous.

          1. 1

            Yeah to me we lost something when CGI fell out of favor – that was Unix style reuse.

            It suffers from interpreter/JIT startup time, but I still use FastCGI! I think FastCGI is quite complex but nowhere near as complex as HTTP.


            MIcrosoft did have some incentive to put code/libraries in the operating system, in polyglot fashion, rather than in languages.

            I agree with that but for different reasons. Related philosophical paper:

            https://lobste.rs/s/fw5ubn/unix_plan_9_lurking_smalltalk

            which re-read a few weeks ago. It’s good even though I come at it from a somewhat opposite view.

            The funny thing is that he starts off from a language-oriented position – the claim that “there shouldn’t be and OS”.

            Then the last paragraph is basically stating what’s obvious to “OS people”.

            Languages themselves may then become “views” onto a space of objects that is managed by the operating system, rather than by a per-language VM. Ingalls’s per- spective, as a language designer looking on operating systems, saw “things that don’t fit in to a language”, and concluded that “there shouldn’t be one”. The cul- mination of our relatively postmodern perspective on operating systems provides a converse view of languages themselves. A language is a collection of concepts that can be found and recognised within a larger system; there will be many.

      3. 2

        Since I’m not radical enough to follow this path, I found an interesting middle-ground: a language which does all its work at compile time and delegates 100% of the runtime behavior to another language’s VM. Languages compiling to another language’s runtime are very common nowadays (perhaps even in the majority, for very good reason: Clojure, Kotlin, LFE, Typescript, etc) but most of them bring along some form of standard library or other overhead at runtime.

        Sometimes the constraints you get can be awkward, but it gives you an incredible focus and lets you ignore huge swaths of very difficult language-building work.

        The compiler I work on is https://fennel-lang.org but I first encountered this approach with Mirah, a language created by the JRuby maintainer which has no standard library and compiles to zero-overhead JVM bytecode while fixing a lot of warts in Java. The idea was to incorporate it into JRuby’s own implementation to make certain bits less tedious, but it appears to have been abandoned 5 years ago, possibly because improvements to Java made it difficult to justify continuing investment: https://github.com/mirah/mirah

        1. 1

          Yes and no. On the one hand I like that approach and on the other hand stuff like the whole TS -> JS transpile (please excuse the vagueness, I don’t want to discuss it anymore) is a huge part of what people don’t like.

          So I guess it still kinda depends on which runtime/intermediate you use and how the tooling reacts. Maybe the JS ecosystem is an outlier here because everything was a big ball of whatever anyway and then people also started writing stuff in another language (although coffeescript.. I don’t remember there being such a fuss over it not being direct JS…).

          1. 1

            I’ve never used JS, Typescript, ClojureScript, or Coffeescript so I don’t really understand what you mean here. It does sound like an outlier.

            There’s only so much you can do with a compiler; you can’t fix ecosystem-level problems.

            1. 1

              My (maybe wrong) perception is that people really, really disliked the “get JS out of TS” step for all those years before ES6 modules especially. So in that case I was talking about TS/coffeescript as having JS as a base (or compile target, as you will).

              On the other hand there are JVM and BEAM languages and maybe also the ones based on LLVM, in this case it’s seen as a pro (and I agree). Maybe ‘target JS’ is just a necessity and everyone only wants it to run in the browser, and doesn’t actually like the runtime (unsurprisingly).

    3. 3

      Imperative programming languages typically make a grammatical distinction between statements and expression. Functional languages instead tend to be structured in a way that everything inside a function body is an expression. The latter is more minimalistic, and also imposes less constraints on the programmer

      When I was first reading about Rust, I read in their documentation that they have statements and expressions. My first impression was that this was a red flag; if the designer really understood what statements are, they would not have included them in the language, and thus their inclusion demonstrated a lack of understanding of the nuance of language design.

      Later I came to understand (and I’m not a Rust expert so I could have gotten this wrong) that their documentation just used a creative definition of the word “statement”: it is in fact, just an expression which returns a Unit type. Not what most people would call a statement at all.

      1. 3

        You are partially correct. Things like if or for are expressions, but let is not. See an example: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=43fb87f7b27c2199001b2d9678fc23b4