I have come to regard the cult of simplicity with some suspicion myself. For one thing, a simple tool that fails to manage its problem completely often seems to beget a recapitulation of the problem in the layer above. For instance, we have disk partitioning below, and then wind up with volume management above. ZFS and BtrFS show declarative solutions to the underlying problems of balancing performance against robustness to error. But they are not simple tools.
It also often seems like we are trading complexity of one system for a constellation of simple things that, taken together, are as or more complicated as a whole.
As a card-carrying member of the cult of simplicity, I’d say this is a good observation, but also that it conflates two things.
Yes, if you try to make a problem simpler than its minimum essential complexity, you will achieve nothing more than trading one kind of complexity for another, and likely overall make the problem worse. Classic example here is deciding that “functions more than 5 lines long are a code smell”, and producing 100 five line functions rather than 10 fifty line functions, and patting yourself on the back, failing to realize that navigating 100 functions is much harder than navigating 10 of them.
But that’s not what the cult’s typical gripe is. The gripe is about heavy tooling and frameworks being used for medium sized, quite manageable problems, where the tooling itself become the lion’s share of the complexity. So… say you have a fairly simple web application which requires some JS. The kind of thing a competent JS developer could write in, say, a week or less without any frameworks or build tools. And now you walk into a project that has “solved” this problem with Angular, webpack, a utility library, 2 test frameworks, docker, and some bash scripts for glue. This is not hyperbole – I have seen this.
Yeah I feel like the article (and most discussion on this topic) spends a lot of time talking past each other. People complain about overblown solutions being applied to problems that don’t warrant that complexity and others reply with answers describing situations where the same solutions were applied to problems where the complexity may have been justified.
All of this brings me and I hope you to the counter-intuitive conclusion that simple tools do not necessarily do better at helping users manage complexity than more complex tools.
Outside the pedagogical space, no one’s really arguing that you should never use a solution above N kloc for any problem. The real arguments (the ones worth paying attention to) are more like “hey, I don’t think this problem calls for a 20kloc thing with a mental surface area of 150 api calls when the same results could be achieved with 5kloc”. The trick is identifying accidental vs essential complexity a la Out of the Tarpit, but such classifications are always so, so context-dependent.
That said, when it comes to a language, I absolutely appreciate when stuff is left out so I can bring it in specifically when I need it. For instance, Lua doesn’t have a class system, but it has tables which can be used to build a class system in userspace. I’m very glad that I don’t have to pay the complexity cost of thinking about classes for problems which aren’t a good fit for classes, but other people appreciate the ability to write code using classes in the way they’re familiar with. You get to pick for yourself how much conceptual overhead you want, and it’s great.
I honestly wish Clojure had done that with defrecord/defprotocol because right now at work I’m dealing with people using it for stuff it’s absolutely not a good fit for, and it’s frustrating. Working in Lua where similar features are opt-in is a breath of fresh air.
A while back, I chanced upon a blog post that as of writing remains frustratingly ungooglable. It offered the hot take that being self-hosted is not necessarily the holy grail of a general purpose programming language, and that, if anything, acts as a forcing function in which a language feature that makes it easier to implement a compiler or an interpreter ends up more prized and valued than one that does not. This assertion is built on a broader, yet often easily forgotten notion in discussions about PL design: there is a wide gulf between the viewpoint of a PL designer / implementer and the viewpoint of a PL user.
Now, a lot of folks might think, “pffft, that’s obviously true,” but what does this gulf actually do to the differing perceptions of complexity? We can look towards a similar parallel: linguists and language learners. A lot of language learners, especially ones that start out monolingual, often express frustration at linguistic features that exist in a language they’re learning but not in a language they already know. For example, many anglophones learning Spanish often bemoan the sheer number of verb conjugations that exist, in comparison to those of English; Sinophones learning English are often flabbergasted by not just plurality, but the number of irregular plural forms in English.
Linguists, on the other hand, are routinely exposed to linguistic features, even those of languages they don’t speak with fluency. So what happens when you task these two demographics with making a conlang, a constructed language? In the wild, I’ve observed that in their initial iterations, language learners tend to be tempted by the urge to create a language with less linguistic features, e.g. forgoing grammatical gender, cases, plurality, etc. That is, until you give them fables to translate into this conlang of theirs. Schleicher’s fable is one such example, and it’s an exercise to test whether a conlang has developed a big enough vocabulary and syntax to the point where it can express a short tale.
It turns out that to be capable of conveying at least a rudimentary human idea, a language, even a constructed one, must possess a minimum amount of complexity. To project this back into the land of programming: most users of a general purpose PL don’t use the majority of the surface area provided by said PL, and that usually even applies to the maintainers and spec writers of said PL.
Which brings us back to Steele’s point:
To summarize, Steele posits that a language must BOTH be “large enough” to be “useful”, and yet be “small enough” to learn. The conceit of the talk and core premise being that being of “small” size and being “simple” conflate.
For general purpose PLs, there is a surprisingly high floor for complexity. One can hide some of that by not offering certain language features, but the end result is akin to shoving stuff under a rug, with all the userland solutions bulging out incongruently. An example that comes to mind is that prior to Go 1.18, there was no way for a user-defined type to be iterable using the range clause of a for statement. This resulted in a proliferation of ForEach methods in many Go libraries that offered its own data structures.
Steele suggests the trick in balancing size against utility is that a language must empower users to extend (if not change) the language by adding words and maybe by adding new rules of meaning.
I don’t buy this. Authors generally don’t extend e.g. English with new words or grammar in order to write a novel. Programmers generally don’t need to extend a programming language with new words or rules in order to write a program.
A programming language, like a spoken/written language, establishes a shared lexicon in which ideas can be expressed by authors and widely understood by consumers. It’s an abstraction boundary. If you allow authors to mutate the rules of a language as they use it, then you break that abstraction. The language itself is no longer a shared lexicon, it’s just a set of rules for arbitrarily many possible lexicons. That kind of defeats the purpose of the thing! It’s IMO very rare that a given work, a given program, benefits from the value this provides to its authors, compared to the costs it incurs on its consumers.
Programmers generally don’t need to extend a programming language with new words or rules in order to write a program.
Maybe you would disagree, but I would argue that functions are essentially new words that are added to your program. New rules do seem to be a bit less common though.
This would be my interpretation as well. Steele defines a language to be approximately “a vocabulary and rules of meaning”, and clearly treats defining types and functions as to be adding to that vocabulary throughout his talk. My (broad) interpretation of a generalized language is based on that idea that the “language” itself is actually just the rules of meaning and all “vocabulary” or libraries are equal whether they be “standard” or “prelude” or defined by the user.
I understand the perspective that functions (or classes, or APIs, or etc.) define new words, or collectively a new grammar, or perhaps a DSL. But functions (or classes, or APIs, or etc.) are obliged to follow the rules of the underlying language(s). So I don’t think they’re new words, I think they’re more like sentences, or paragraphs.
Authors generally don’t extend e.g. English with new words or grammar in order to
FWIW a thing that shitposters on Tumblr have in common with William Shakespeare is the act of coining new vocabulary and novel sentence structure all the time. :)
The author is saying that simple systems need this extensibility to be useful. English is far from small. Even most conventional languages are larger than the kernel languages under discussion, but chief argument for kernel languages is their smallness and simplicity. And those languages are usually extended in various ways to make them useful.
I would say, and I think that the author might go there too, that certain libraries that rely heavily on “magic” (typically ORMs) also count to some degree as language extensions. ActiveRecord and Hibernate, for instance, use functionality that is uncommonly used even by other practitioners in their respective languages.
The article is about languages, right? Languages are a subset of systems. The abstraction established by a language is by definition a shared context. A language that needs to be extended in order to provide value doesn’t establish a shared context and so isn’t really a language, it’s a, like, meta-language.
It’s about using languages as a case study in how systems can be of adequate or inadequate complexity to the tasks they enable their users to address, and how if a tool is “simple” or perhaps inadequately complex that the net result is not that the resulting system is “simple” but as @dkl speculates above that the complexity which a tool or system failed to address has to live somewhere else and becomes user friction or – unaddressed – lurking unsuitability.
This creates a nice concept of “leverage” being how a language or system allows users to address an adequate level of complexity (or fails to do so), and begs how you can measure and compare complexity in more meaningful and practical terms than making aesthetic assessments both of which I want to say more about later.
I suppose I see languages as systems that need to have well-defined and immutable “expressibility” in order to satisfy their fundamental purpose, which isn’t measured in terms of expressive power for authors, but rather in terms of general comprehension by consumers.
And, consequently, that if a language doesn’t provide enough expressivity for you to express your higher-order system effectively, the solution should be to use a different language.
If you consider that each project (or team) has slightly different needs, but there aren’t that many slightly different language dialects out there that add just one or two little features (thankfully!) that these projects happen to need. Sometimes people build preprocessors to work around one particular lack of expressibility in a language (for one well-known example that’s not an in-house only development, see yacc/bison). That’s a lot of effort and produces its own headaches.
Isn’t it better to take a good language that doesn’t need all that many extensions, but allows enough metaprogramming to allow what your team needs for their particular project? This allows one to re-use the 99% existing knowledge about the language and the 1% that’s different can be taught in a good onboarding process.
Nobody in their right mind is suggesting that projects would be 50% stock language, 50% custom extensions. That way lies madness. And indeed, teams with mostly juniors working in extensible languages like CL will almost inevitably veer towards metaprogramming abuse. But an experienced team with a good grasp of architecture that’s eloquent in the language certainly benefits from metaprogrammability, if even it’s just to reduce the amount of boilerplate code. In some inherently complex projects it might even be the difference between succeeding and failing.
I do think the industry tends to be dominated by junior-friendly programming languages, as if the main concern was more about increasing headcount than it was about expressing computation succinctly and clearly.
Nobody in their right mind is suggesting that projects would be 50% stock language, 50% custom extensions. That way lies madness.
Paul Graham made roughly that claim when he wrote about why viaweb could only have been written in common lisp, but I think his numbers were more like 70/30. But you did qualify it with only people in their right mind, so maybe that doesn’t count.
Isn’t it better to take a good language that doesn’t need all that many extensions, but allows enough metaprogramming to allow what your team needs for their particular project?
YMMV, but I don’t think so. My experience with metaprogramming in industry has been negative. I find it usually adds more complexity than it removes. And I don’t really agree with the framing that a language should be extensible, at the ·language· level, by its users. I see that as an abstraction leak.
My experience with metaprogramming in industry has been negative. I find it usually adds more complexity than it removes.
I’d put that in the “hell is other people” basket - indeed, very often metaprogramming gets abused in ways that make code more complicated than it has to be. So you definitely have a point there. But then I’ve seen horror shows of many other kinds in “industrial” codebases in languages without metaprogramming. In the wrong hands, metaprogramming can certainly wreak more havoc, though!
Still, I wouldn’t want to go back to languages without metaprogramming features, as I feel they allow me to express myself more eloquently; I prefer to say things more precisely than in a roundabout way.
And I don’t really agree with the framing that a language should be extensible, at the ·language· level, by its users. I see that as an abstraction leak.
I never considered that perspective, but it makes sense to view it that way. In my neck of the woods (I’m a Schemer), having an extensible language is typically considered empowering - it’s considered elitist to assume that the developer of the language knows everything perfectly and is the only one who should be allowed to dictate in which direction the language grows. After all, the language developer is just another fallible programmer, just like the language’s users are.
As a counterpoint, look at the featuritis that languages like Python and even Java have spawned in recent years. They grew lots of features that don’t even really fit the language’s style. Just because something’s gotten popular IMHO doesn’t necessarily mean it ought to be put in a language. It burdens everyone with these extra features. Think about C++ (or even Common Lisp), where everyone programs in a different subset of the language because the full language is just too large to comprehend. But when you want to leverage a useful library, it might make use of features that you’ve “outlawed” in your codebase, forcing you to use them too.
There’s also several examples of the Ruby and Python standard libraries having features which have been surpassed by community libraries, making them effectively deprecated. Sometimes they are indeed removed from the standard library, which generates extra churn for projects that were using them.
I’d rather have a modestly-sized language which can be extended with libraries. But I readily admit that this has drawbacks too: let’s say you choose one of many object/class libraries in an extensible language which doesn’t supply its own. Then if you want to use another library which depends on a different class library, you end up with a strange mismatch where you have entirely different class types, depending on what part of the system you’re talking to.
I’ve seen horror shows of many other kinds in “industrial” codebases in languages without metaprogramming. In the wrong hands, metaprogramming can certainly wreak more havoc, though!
Sure! In pure quantitative terms, I agree: I’ve seen way more awful Java (or whatever) than I have awful metaprogramming stuff. But the bad Java was a small subset of the overall Java I’ve witnessed, whereas the bad metaprogramming was practically 100% of the overall metaprogramming I’ve witnessed.
I fully acknowledge that I’m biased by my experience, but with that caveat, I just haven’t seen metaprogramming used effectively in industry.
This is an important point. You are allowing authors to add new syntax, not requiring it. You can do most of the same tricks in Common Lisp or Dylan that you can do in Javascript or Kotlin, by passing funargs etc., it’s just that you also have the ability to introduce new syntax, within certain clearly defined bounds that the language sets.
Just as you have to learn the order of arguments or the available keywords to a function, Lisp/Dylan programmers are aware that they have to learn the order of arguments and which arguments are evaluated for any given macro call. Many of them look exactly like functions so there’s no difference. (I like that in Julia, as oppose to Common Lisp and Dylan, macro calls must start with the special character “@”, since it makes a clear distinction between function calls and macro calls. But I don’t know much about Julia macros.)
A programming language, like a spoken/written language, establishes a shared lexicon…
Yes, and I don’t believe macros change this significantly, although this depends to a certain extent on the macro author have a modicum of good taste. The bulk of Common Lisp and Dylan macros are one of two flavors:
“defining macros” – In Common Lisp these are usually named “defsomething” and in Dylan “define [adjectives] something”. When you see define frame <chess-board> (<frame>) ... end you know you will encounter special syntax because define frame isn’t part of the core language. So you go look it up just as you would lookup a function for which you don’t know the arguments.
“with…” or “…ing” macros like “with-open-file(…)” or “timing(…)”
These don’t change the complexity of the language appreciably in my experience and they do make it much more expressive. (Think 1/10th to 1/15th the LOC of Java here.)
Where I believe there is an issue is with tooling. Macros create problems for tooling, such as including the right line number in error messages (since macros can expand to arbitrarily many lines of code), stepping or tracing through macro calls, recording cross references correctly for code browsing tools, etc.
This seems founded in the idea that if you just know the language, you can look at code and understand what it does. But this is always convention. For instance, consider the classic C issue of “who owns the pointer passed to a function?” Even with an incredibly simple language, there’s ambiguity about what conventions surround a library call - do you need to call free()? Will the function call free()? Can you pass a stack pointer, or does it have to be heap allocated? And so on. More powerful type systems can move more information into the type itself, but more powerful types tend to be included in more powerful languages; for instance, in D, if you pass an expression to a function, if the parameter is marked as lazy, the expression may actually be evaluated any number of times, though it’s usually zero or one, and you have no idea when the evaluation takes place. So just from looking at a function call, foo(bar), it may be that bar is evaluated before foo, or during foo, or multiple times during foo, or never.
Now a macro could do worse, sure, but to me it’s a difference of degree, not kind. There’s always a spectrum, and there’s always ambiguity, and you always need to know the conventions. Every library is a grammar.
This seems founded in the idea that if you just know the language, you can look at code and understand what it does. But this is always convention. For instance, consider the classic C issue of “who owns the pointer passed to a function?” Even with an incredibly simple language, there’s ambiguity about what conventions surround a library call - do you need to call free()? Will the function call free()? Can you pass a stack pointer, or does it have to be heap allocated? And so on.
The devil’s in the details. But when I’m answering questions like “who calls free()?” I don’t need to re-evaluate my understanding of language keywords or sigils or operator semantics. I see this as a categorical difference. I suppose other people may not.
I think it’s worth addressing the elephant in the room: there’s language as in formal language (mathematics) and then there’s language as reasoned by linguistics, which also acknowledges and studies the sociopolitical and anthropological aspects of language. It may be useful to analyze PLs through the lens of the former, but programming languages also do an awful lot of things that human languages do: give rise to dialects, have governing bodies, have speakers that defy governing bodies, borrow from each other, develop conventions, nurture communities, play host to ecosystems, and so forth.
We now arrive at the core of your assertion, which is that there is a hard line between syntax that comes for free and syntax that is defined by the userland programmer. This is part of the very same line that the author is asking us to imagine blurring:
Building on Steele’s deliberate blurring of the line between a “language” and a “library”, I suggest this train of thought applies to libraries as well. Libraries – language extensions really – help us say and do more but can fail to help us manage complexity or impose costs on their users in exactly the same ways as language features.
It is furthermore dangerous to equate the malleability of syntax with the extensibility of a language. Macros are a form of extensibility, yet conversely, extensibility does not require a support for macros. Take Python, a language that does not support macros, for example. Python allows you to engage in so much operator overloading that you could read the expression (a + b) << c and still have no clue what it actually does without looking up how the types of a, b, and c define such operations. Ultimately, it is conventions — be they implicit or documented, be they cultural or tool-enforced — that dictate the readability of a language, just as @FeepingCreature has demonstrated with multiple examples.
The author covers a great deal of the triumphs and tragedies of language extensibility in the “Convenience? Or simplicity through design?” section. The monstrosity of the Common Lisp LOOP macro, as well as userland async / await engines, are brought up because they brutally defeat static analysis, whereas the .use method in Kotlin is shown as an example that manages to accomplish all of the needs of WITH-FOO macros with none of the ill effects of macros. In fact, there is another commonly used language that manages to achieve this the same way Kotlin does: Ruby. In Ruby, any method call can take an additional block of code, which gets materialized into a Proc object, which is a first-class function value. This means in Ruby it is routine to write code that looks like File.open("~/scratch.txt") { |f| f.write("hello, world") }. This is an example straight out of the Ruby standard library, and it is the convention for resource management across the entire Ruby ecosystem.
Now, even though Ruby — like Python — does not support macros, and even though it got resource management “right”, just like Kotlin did, it has nevertheless managed to dole out some of the most debilitating effects of metaprogramming. Ask anyone who’s worked on a Rails project, myself included, and they will recount how they were victimized by runtime-defined methods whose names were generated by concatenating strings and therefore remain nigh unsearchable and ungreppable. A great deal of research and literature have been dedicated to making Ruby more statically analyzable, and they all converge towards the uncomfortable conclusion that its capability of evaluating strings as code at runtime and its Smalltalk-like object system both make it incredibly difficult for machines to reason with Ruby code without actually running it.
there’s language as in formal language (mathematics) and then there’s language as reasoned by linguistics, which also acknowledges and studies the sociopolitical and anthropological aspects of language.
Programming languages can be analyzed in the formal/mathematical sense, but when they are used, they are used by humans, and humans necessarily don’t understand languages through this lens. I’m not saying a programming language is strictly e.g. linguistic, but I am saying that it cannot be effectively described/modeled purely via formal or mathematical means. A language isn’t an equation, it’s an interface between humanity and logic.
Authors generally don’t extend e.g. English with new words or grammar in order to write a novel.
Well, no, but it helps that English language already has innumerable – legion, one might say – words, an English dictionary is a bountiful, abundant, plentiful universe teeming with sundry words which others have already invented, not always specifically for writing a whole novel, often just in order to get around IRL. It’s less obvious with English because it’s not very agglutinative but it has been considerably extended in time.
Language changes not only through the addition of new words and rules but also through other words and rules falling out of use (e.g. in English, the plural present form of verbs lost its inflection by the 17th century or so), so it’s not quite fair to say that modern English is “bigger” than some of its older forms. But extension is a process that happens with spoken languages as well.
It’s also super frequent in languages that aren’t used for novels and everyday communication, too. At some point I got to work on some EM problems with a colleague from a Physics department and found that a major obstacle was that us engineers and them physicists use a lot of different notations, conventions, and sometimes even words, for exactly the same phenomenon. Authors of more theoretical works regularly developed their own (very APL-like…) languages that required quite some translation effort in order to render them into a comprehensible (and extremely verbose) math that us simpletons could speak.
(Edit: this, BTW, is in addition to all that stuff below, which @agent281 is pointing out, I think the context of the original talk is relevant here).
If I had to guess I would think that this was a nudge to Scheme programming language from Steel’s point of view (or Common Lisp, but this one is quite large in comparison). Quite easy to extend Scheme that way and he does have a history with lisp-family languages. But that’s just a guess.
I think the limitation has to do with humans’ limited cognitive ability and the necessity of abstraction. I don’t know exactly what that limit is or even how you’d measure it, but for the sake of argument, let’s call it seven plus-or-minus two items.
The ideal toolkit contains seven plus-or-minus two tools which can be combined arbitrarily, following simple rules (like Unix pipes, or C function composition, or LEGO bricks), to solve different problems. Let’s say I have Problem A, and a kit of tools I can easily combine and rearrange until I find Solution A. This will almost invariable reveal the larger, looming Problem B, and Solution A is just one of the tools I’ll need to solve it. What should I do with Solution A?
If I keep Solution A as just an arrangement of tools from my toolkit, solving Problem B becomes more difficult, because my tools are operating at the wrong level of abstraction: I want to be combining and rearranging Solution A and other tools like it, not combining and rearranging the components of Solution A. For example, it’s possible to examine a problem in a Python program by inspecting the Python interpreter in gdb, but it’s a whole lot easier if you can use a Python debugger instead.
If I add a new tool to my toolkit that specifically implements Solution A, then my toolkit is bigger (it takes longer to find the tool I want) and the tools can no longer be arbitrarily recombined - one of them only does a special, specific thing. For example, tar fits neatly into shell pipelines because it’s a streaming format, but you can’t efficiently access a specific file in a tarball. zip solves that problem, but since it’s not a streaming format, it no longer fits neatly into shell pipelines.
If I create a whole new toolkit of seven plus-or-minus two tools at the correct abstraction level to solve Problem B, I’ll be able to solve it much more quickly… but it’ll be very expensive to create such a toolkit, and expensive to teach people a new, unrelated toolkit. For example, there’s nothing can do with Docker that you couldn’t do with sysvinit, but Docker does it much more quickly… if you can spare the time to learn it.
Some people go as far as trying to create a new toolkit that’s flexible enough to solve Problem A and Problem B at the same time, and some of them manage to craft an elegant, perfect crystal of a toolkit… then it shatters to pieces when Problem C comes along. For example, the Plan 9 operating system elegantly solves most of the problems faced by Unix-like operating systems in the 1980s, but the modern concept of a web-browser doesn’t fit in that paradigm.
Most of the hardline “software simplicity” communities I’ve seen online achieve that simplicity by calling Problem B “out of scope”. In the healthier communities, somebody who actually wants to solve Problem B is directed to more complex tools; in the less healthy ones, trying to solve Problem B is taken as a sign of ignorance or moral weakness.
That said, I still think software simplicity is desirable, just within a limited scope. All the layers below the one I’m working at should be rigorously, carefully crafted by experts, and I don’t care if they’re too complex for me to understand or debug (I’m not exactly about to whip out an electron microscope to debug my CPU). The layer I’m actually on should have seven plus-or-minus two tools I can freely combine, and when Problem B inevitably arises, by definition I’m the problem domain expert who needs to rigorously, carefully craft a new layer for somebody else to build on.
Defining person to be man or woman is obviously false and needlessly exclusionary. The talk is itself quite good and there’s a reason I’m working from it, but that production probably wouldn’t fly today.
This is not false, exclusionary, or transphobic or any other sort of “problematic”. It was simply an illustrative example of defining a concept as a union of existing concepts, that everybody in the intended audience could easily understand. It is not, and is not pretending to, say anything at all about sex, gender, personhood, or anything else. Pretending otherwise is disingenuous at best.
It’s worth noting that the production isn’t great, and Steele says so himself in the typed notes. There’s no need to accuse anyone of anything that the original author hasn’t already conceded.
This choice, to start with just words of one syllable, was why I had to define the word
“woman” but could take the word “man” for granted. I wanted to define “machine” in
terms of the word “person”, and it seemed least hard to define “person” as “a man or a
woman.” (I know, I know: this does not quite give the true core meaning of the word
“person”; but, please, cut me some slack here.) By chance, the word “man” has one
syllable and so is a primitive, but the word “woman” has two syllables and so I had to
define it. In a language other than English, the words that mean “man” and “woman”
might each have one syllable—or might each have two syllables, in which case one would
have to take some other tack.
I’m flagging this reply because I find it kind of dismissive. I also think that style of rhetoric will invite a low standard of discussion.
What do I see here? An abridged transcript:
Commenter 1: Author, in your article you alluded to an opinion. Could you please elaborate?
Author: Sure, here's my opinion.
Commenter 2: Author, your opinion is wrong.
I see this as penalising the author, who so far has contributed positively to this thread.
If it’s very important to you to express this opinion, we can help you do it in a way that’s clearly positive sum
If we can’t disagree with someone, even with someone who has contributed positively, then we can’t have discourse. Your flag is against free discourse.
I’m not trying to discourage disagreement itself; I’m trying to advocate for a style of discussion that leads to the flourishing of all participants.
I clearly have not conveyed myself as well as I intended, and I don’t have the motivation to improve my original comment further. You should probably just ignore it.
Indeed. I wonder if we can deconstruct why that happened and fix it. I think that the main reason that the talk needs to define “man” and “woman” is so that it can talk about great men and great women of history. A modern reconstruction might go through the motions of humorously defining “person”, a two-syllable word, instead.
I find this article challenging because it seems to butcher one of my sacred cows. Perhaps the issue is that simplicity can’t really exist for a truly general purpose language. Maybe we ultimately have to pick two between expressivity, performance, and simplicity. Hence all languages seem to grow without bound over time. (Or maybe it’s just that “Change gives the illusion of progress.”)
The real question – the unanswered question – is what tools effectively help users manage “complexity”, how and why.
Fundamentally I believe this is not a matter of building the right tools, but of gaining experience that can be applied differently to every specific situation.
As the author says, there will always be this tradeoff between:
a small set of building blocks that leaves managing complexity to the user, versus
a large set of building blocks that is itself complex
I suggest this train of thought applies to libraries as well.
I believe it applies to a language, to its libraries, and to all layers of abstraction defined on top of these libraries in any given software architecture.
A general-purpose tool can never answer how to manage complexity because the answer is fully dependent on the situation. Every situation requires a different balance between these two.
This is especially true in software development. When building houses, or even cities, the needs don’t change between projects, so it makes sense to refine tools that manage the recurring complexity of these projects. In software, when a specific challenge has been solved once, it doesn’t need solving again. The next project will be completely different, with different needs, with different types of complexity.
So in software development we can never rely on rules or tools that tell us how to best manage complexity. What we need instead as engineers is to train ourselves to develop a deep understanding of and an intuition for managing complexity.
This is what makes software engineering hard and fun and a form of art.
As a consumer, how can I get the most bang for my buck? As a programmer, how can I get the most functionality for the least complexity? As a library author, how can I give the most leverage with the simplest API?
I have a fairly strong feeling that the definite answer to complexity, in pretty much any domain, is a generalisation of John Ousterhout’s “classes should be deep”: to the extent possible we want something packed with usefulness, that nevertheless remains simple enough that we can learn to use it.
It’s a cost/benefit analysis: each problem will have a cost associated with it not being solved, and each solution will have a cost associated to its implementation and use. Problems are worth solving when we can find a solution that costs less than the problem itself. Ideally we want solutions that are much cheaper than the problem.
When we get to programming, this cost/benefit business translates to a permanent tension between eventual efficiency and ease of learning: basically, the time you save by using something must exceed the time you took to learn it. I believe much of the Not Invented Here Syndrome comes from the perception that writing what we need from scratch will be cheaper (or at least more enjoyable) than taking on a dependency that does the job for us. Sometimes, typically when what we need is but a small fraction of the kitchen sink that masquerades as a library, this perception is even correct.
This cost of learning vs benefit of usage puts programming languages and libraries in sharply different spots: languages are used for a long time. Getting familiar with one will let you reap benefits for as long as you program in it. libraries however tend to be used much less often. Some standard stuff like hash tables or dynamic arrays will come up all the time, but how often do you compress .zip files or serve HTTP requests? Many libraries are only used once in a while, sometimes once period. Unlike languages, the cost of learning their API is not amortised over many uses. Thus, the simplicity of APIs is paramount. To the point that sometimes, pushing parts of a problem down to users can be cheaper than requiring them to learn a more complex API that solves the whole problem.
This need for API simplicity may have contributed to C’s long standing survival as an Interface Definition Language. Why so many ABIs are C ABIs, why all languages talk to C and not much else. That’s because C is weak. We have a few basic data types and few ways to compose them, first order functions, and not much else. This forces APIs to make their complexity explicit. Such explicit APIs are more cumbersome to use, but they’re also easier to learn. For libraries I use only once, that’s just perfect.
I agree. When it comes down to it, the thing that lets you not know how something works is trust (in the abstraction or person or whatever). If we had probabilistic estimates of trust and we’d share them with others then we could maybe start building measures of legitimacy. This is something I want to do but I’ve been having a lot of real world interfering with my computer use recently.
I have come to regard the cult of simplicity with some suspicion myself. For one thing, a simple tool that fails to manage its problem completely often seems to beget a recapitulation of the problem in the layer above. For instance, we have disk partitioning below, and then wind up with volume management above. ZFS and BtrFS show declarative solutions to the underlying problems of balancing performance against robustness to error. But they are not simple tools.
It also often seems like we are trading complexity of one system for a constellation of simple things that, taken together, are as or more complicated as a whole.
As a card-carrying member of the cult of simplicity, I’d say this is a good observation, but also that it conflates two things.
Yes, if you try to make a problem simpler than its minimum essential complexity, you will achieve nothing more than trading one kind of complexity for another, and likely overall make the problem worse. Classic example here is deciding that “functions more than 5 lines long are a code smell”, and producing 100 five line functions rather than 10 fifty line functions, and patting yourself on the back, failing to realize that navigating 100 functions is much harder than navigating 10 of them.
But that’s not what the cult’s typical gripe is. The gripe is about heavy tooling and frameworks being used for medium sized, quite manageable problems, where the tooling itself become the lion’s share of the complexity. So… say you have a fairly simple web application which requires some JS. The kind of thing a competent JS developer could write in, say, a week or less without any frameworks or build tools. And now you walk into a project that has “solved” this problem with Angular, webpack, a utility library, 2 test frameworks, docker, and some bash scripts for glue. This is not hyperbole – I have seen this.
That is what the cult of simplicity is on about.
Yeah I feel like the article (and most discussion on this topic) spends a lot of time talking past each other. People complain about overblown solutions being applied to problems that don’t warrant that complexity and others reply with answers describing situations where the same solutions were applied to problems where the complexity may have been justified.
Outside the pedagogical space, no one’s really arguing that you should never use a solution above N kloc for any problem. The real arguments (the ones worth paying attention to) are more like “hey, I don’t think this problem calls for a 20kloc thing with a mental surface area of 150 api calls when the same results could be achieved with 5kloc”. The trick is identifying accidental vs essential complexity a la Out of the Tarpit, but such classifications are always so, so context-dependent.
That said, when it comes to a language, I absolutely appreciate when stuff is left out so I can bring it in specifically when I need it. For instance, Lua doesn’t have a class system, but it has tables which can be used to build a class system in userspace. I’m very glad that I don’t have to pay the complexity cost of thinking about classes for problems which aren’t a good fit for classes, but other people appreciate the ability to write code using classes in the way they’re familiar with. You get to pick for yourself how much conceptual overhead you want, and it’s great.
I honestly wish Clojure had done that with defrecord/defprotocol because right now at work I’m dealing with people using it for stuff it’s absolutely not a good fit for, and it’s frustrating. Working in Lua where similar features are opt-in is a breath of fresh air.
It sounds like you and I have nearly identical takes on the subject.
A while back, I chanced upon a blog post that as of writing remains frustratingly ungooglable. It offered the hot take that being self-hosted is not necessarily the holy grail of a general purpose programming language, and that, if anything, acts as a forcing function in which a language feature that makes it easier to implement a compiler or an interpreter ends up more prized and valued than one that does not. This assertion is built on a broader, yet often easily forgotten notion in discussions about PL design: there is a wide gulf between the viewpoint of a PL designer / implementer and the viewpoint of a PL user.
Now, a lot of folks might think, “pffft, that’s obviously true,” but what does this gulf actually do to the differing perceptions of complexity? We can look towards a similar parallel: linguists and language learners. A lot of language learners, especially ones that start out monolingual, often express frustration at linguistic features that exist in a language they’re learning but not in a language they already know. For example, many anglophones learning Spanish often bemoan the sheer number of verb conjugations that exist, in comparison to those of English; Sinophones learning English are often flabbergasted by not just plurality, but the number of irregular plural forms in English.
Linguists, on the other hand, are routinely exposed to linguistic features, even those of languages they don’t speak with fluency. So what happens when you task these two demographics with making a conlang, a constructed language? In the wild, I’ve observed that in their initial iterations, language learners tend to be tempted by the urge to create a language with less linguistic features, e.g. forgoing grammatical gender, cases, plurality, etc. That is, until you give them fables to translate into this conlang of theirs. Schleicher’s fable is one such example, and it’s an exercise to test whether a conlang has developed a big enough vocabulary and syntax to the point where it can express a short tale.
It turns out that to be capable of conveying at least a rudimentary human idea, a language, even a constructed one, must possess a minimum amount of complexity. To project this back into the land of programming: most users of a general purpose PL don’t use the majority of the surface area provided by said PL, and that usually even applies to the maintainers and spec writers of said PL.
Which brings us back to Steele’s point:
For general purpose PLs, there is a surprisingly high floor for complexity. One can hide some of that by not offering certain language features, but the end result is akin to shoving stuff under a rug, with all the userland solutions bulging out incongruently. An example that comes to mind is that prior to Go 1.18, there was no way for a user-defined type to be iterable using the
range
clause of afor
statement. This resulted in a proliferation ofForEach
methods in many Go libraries that offered its own data structures.Here is the post
Oh this is excellent. Thanks!
I don’t buy this. Authors generally don’t extend e.g. English with new words or grammar in order to write a novel. Programmers generally don’t need to extend a programming language with new words or rules in order to write a program.
A programming language, like a spoken/written language, establishes a shared lexicon in which ideas can be expressed by authors and widely understood by consumers. It’s an abstraction boundary. If you allow authors to mutate the rules of a language as they use it, then you break that abstraction. The language itself is no longer a shared lexicon, it’s just a set of rules for arbitrarily many possible lexicons. That kind of defeats the purpose of the thing! It’s IMO very rare that a given work, a given program, benefits from the value this provides to its authors, compared to the costs it incurs on its consumers.
Maybe you would disagree, but I would argue that functions are essentially new words that are added to your program. New rules do seem to be a bit less common though.
This would be my interpretation as well. Steele defines a language to be approximately “a vocabulary and rules of meaning”, and clearly treats defining types and functions as to be adding to that vocabulary throughout his talk. My (broad) interpretation of a generalized language is based on that idea that the “language” itself is actually just the rules of meaning and all “vocabulary” or libraries are equal whether they be “standard” or “prelude” or defined by the user.
I understand the perspective that functions (or classes, or APIs, or etc.) define new words, or collectively a new grammar, or perhaps a DSL. But functions (or classes, or APIs, or etc.) are obliged to follow the rules of the underlying language(s). So I don’t think they’re new words, I think they’re more like sentences, or paragraphs.
FWIW a thing that shitposters on Tumblr have in common with William Shakespeare is the act of coining new vocabulary and novel sentence structure all the time. :)
See: 1984, full of this
Also, Ulysses no?
The author is saying that simple systems need this extensibility to be useful. English is far from small. Even most conventional languages are larger than the kernel languages under discussion, but chief argument for kernel languages is their smallness and simplicity. And those languages are usually extended in various ways to make them useful.
I would say, and I think that the author might go there too, that certain libraries that rely heavily on “magic” (typically ORMs) also count to some degree as language extensions. ActiveRecord and Hibernate, for instance, use functionality that is uncommonly used even by other practitioners in their respective languages.
The article is about languages, right? Languages are a subset of systems. The abstraction established by a language is by definition a shared context. A language that needs to be extended in order to provide value doesn’t establish a shared context and so isn’t really a language, it’s a, like, meta-language.
Not really.
It’s about using languages as a case study in how systems can be of adequate or inadequate complexity to the tasks they enable their users to address, and how if a tool is “simple” or perhaps inadequately complex that the net result is not that the resulting system is “simple” but as @dkl speculates above that the complexity which a tool or system failed to address has to live somewhere else and becomes user friction or – unaddressed – lurking unsuitability.
This creates a nice concept of “leverage” being how a language or system allows users to address an adequate level of complexity (or fails to do so), and begs how you can measure and compare complexity in more meaningful and practical terms than making aesthetic assessments both of which I want to say more about later.
Right.
I suppose I see languages as systems that need to have well-defined and immutable “expressibility” in order to satisfy their fundamental purpose, which isn’t measured in terms of expressive power for authors, but rather in terms of general comprehension by consumers.
And, consequently, that if a language doesn’t provide enough expressivity for you to express your higher-order system effectively, the solution should be to use a different language.
Reasonable people may disagree.
If you consider that each project (or team) has slightly different needs, but there aren’t that many slightly different language dialects out there that add just one or two little features (thankfully!) that these projects happen to need. Sometimes people build preprocessors to work around one particular lack of expressibility in a language (for one well-known example that’s not an in-house only development, see yacc/bison). That’s a lot of effort and produces its own headaches.
Isn’t it better to take a good language that doesn’t need all that many extensions, but allows enough metaprogramming to allow what your team needs for their particular project? This allows one to re-use the 99% existing knowledge about the language and the 1% that’s different can be taught in a good onboarding process.
Nobody in their right mind is suggesting that projects would be 50% stock language, 50% custom extensions. That way lies madness. And indeed, teams with mostly juniors working in extensible languages like CL will almost inevitably veer towards metaprogramming abuse. But an experienced team with a good grasp of architecture that’s eloquent in the language certainly benefits from metaprogrammability, if even it’s just to reduce the amount of boilerplate code. In some inherently complex projects it might even be the difference between succeeding and failing.
I do think the industry tends to be dominated by junior-friendly programming languages, as if the main concern was more about increasing headcount than it was about expressing computation succinctly and clearly.
Paul Graham made roughly that claim when he wrote about why viaweb could only have been written in common lisp, but I think his numbers were more like 70/30. But you did qualify it with only people in their right mind, so maybe that doesn’t count.
YMMV, but I don’t think so. My experience with metaprogramming in industry has been negative. I find it usually adds more complexity than it removes. And I don’t really agree with the framing that a language should be extensible, at the ·language· level, by its users. I see that as an abstraction leak.
I’d put that in the “hell is other people” basket - indeed, very often metaprogramming gets abused in ways that make code more complicated than it has to be. So you definitely have a point there. But then I’ve seen horror shows of many other kinds in “industrial” codebases in languages without metaprogramming. In the wrong hands, metaprogramming can certainly wreak more havoc, though!
Still, I wouldn’t want to go back to languages without metaprogramming features, as I feel they allow me to express myself more eloquently; I prefer to say things more precisely than in a roundabout way.
I never considered that perspective, but it makes sense to view it that way. In my neck of the woods (I’m a Schemer), having an extensible language is typically considered empowering - it’s considered elitist to assume that the developer of the language knows everything perfectly and is the only one who should be allowed to dictate in which direction the language grows. After all, the language developer is just another fallible programmer, just like the language’s users are.
As a counterpoint, look at the featuritis that languages like Python and even Java have spawned in recent years. They grew lots of features that don’t even really fit the language’s style. Just because something’s gotten popular IMHO doesn’t necessarily mean it ought to be put in a language. It burdens everyone with these extra features. Think about C++ (or even Common Lisp), where everyone programs in a different subset of the language because the full language is just too large to comprehend. But when you want to leverage a useful library, it might make use of features that you’ve “outlawed” in your codebase, forcing you to use them too.
There’s also several examples of the Ruby and Python standard libraries having features which have been surpassed by community libraries, making them effectively deprecated. Sometimes they are indeed removed from the standard library, which generates extra churn for projects that were using them.
I’d rather have a modestly-sized language which can be extended with libraries. But I readily admit that this has drawbacks too: let’s say you choose one of many object/class libraries in an extensible language which doesn’t supply its own. Then if you want to use another library which depends on a different class library, you end up with a strange mismatch where you have entirely different class types, depending on what part of the system you’re talking to.
Sure! In pure quantitative terms, I agree: I’ve seen way more awful Java (or whatever) than I have awful metaprogramming stuff. But the bad Java was a small subset of the overall Java I’ve witnessed, whereas the bad metaprogramming was practically 100% of the overall metaprogramming I’ve witnessed.
I fully acknowledge that I’m biased by my experience, but with that caveat, I just haven’t seen metaprogramming used effectively in industry.
This is an important point. You are allowing authors to add new syntax, not requiring it. You can do most of the same tricks in Common Lisp or Dylan that you can do in Javascript or Kotlin, by passing funargs etc., it’s just that you also have the ability to introduce new syntax, within certain clearly defined bounds that the language sets.
Just as you have to learn the order of arguments or the available keywords to a function, Lisp/Dylan programmers are aware that they have to learn the order of arguments and which arguments are evaluated for any given macro call. Many of them look exactly like functions so there’s no difference. (I like that in Julia, as oppose to Common Lisp and Dylan, macro calls must start with the special character “@”, since it makes a clear distinction between function calls and macro calls. But I don’t know much about Julia macros.)
Yes, and I don’t believe macros change this significantly, although this depends to a certain extent on the macro author have a modicum of good taste. The bulk of Common Lisp and Dylan macros are one of two flavors:
“defining macros” – In Common Lisp these are usually named “defsomething” and in Dylan “define [adjectives] something”. When you see
define frame <chess-board> (<frame>) ... end
you know you will encounter special syntax becausedefine frame
isn’t part of the core language. So you go look it up just as you would lookup a function for which you don’t know the arguments.“with…” or “…ing” macros like “with-open-file(…)” or “timing(…)”
These don’t change the complexity of the language appreciably in my experience and they do make it much more expressive. (Think 1/10th to 1/15th the LOC of Java here.)
Where I believe there is an issue is with tooling. Macros create problems for tooling, such as including the right line number in error messages (since macros can expand to arbitrarily many lines of code), stepping or tracing through macro calls, recording cross references correctly for code browsing tools, etc.
Do you not see code that defines new syntax as categorically different than code which uses defined syntax?
What is a language if not a well-defined grammar and syntax?
I don’t see how something which permits user modification of its grammar/syntax can be called a language. It’s a language construction set, maybe?
This seems founded in the idea that if you just know the language, you can look at code and understand what it does. But this is always convention. For instance, consider the classic C issue of “who owns the pointer passed to a function?” Even with an incredibly simple language, there’s ambiguity about what conventions surround a library call - do you need to call free()? Will the function call free()? Can you pass a stack pointer, or does it have to be heap allocated? And so on. More powerful type systems can move more information into the type itself, but more powerful types tend to be included in more powerful languages; for instance, in D, if you pass an expression to a function, if the parameter is marked as lazy, the expression may actually be evaluated any number of times, though it’s usually zero or one, and you have no idea when the evaluation takes place. So just from looking at a function call,
foo(bar)
, it may be that bar is evaluated before foo, or during foo, or multiple times during foo, or never.Now a macro could do worse, sure, but to me it’s a difference of degree, not kind. There’s always a spectrum, and there’s always ambiguity, and you always need to know the conventions. Every library is a grammar.
The devil’s in the details. But when I’m answering questions like “who calls
free()
?” I don’t need to re-evaluate my understanding of language keywords or sigils or operator semantics. I see this as a categorical difference. I suppose other people may not.I think it’s worth addressing the elephant in the room: there’s language as in formal language (mathematics) and then there’s language as reasoned by linguistics, which also acknowledges and studies the sociopolitical and anthropological aspects of language. It may be useful to analyze PLs through the lens of the former, but programming languages also do an awful lot of things that human languages do: give rise to dialects, have governing bodies, have speakers that defy governing bodies, borrow from each other, develop conventions, nurture communities, play host to ecosystems, and so forth.
We now arrive at the core of your assertion, which is that there is a hard line between syntax that comes for free and syntax that is defined by the userland programmer. This is part of the very same line that the author is asking us to imagine blurring:
It is furthermore dangerous to equate the malleability of syntax with the extensibility of a language. Macros are a form of extensibility, yet conversely, extensibility does not require a support for macros. Take Python, a language that does not support macros, for example. Python allows you to engage in so much operator overloading that you could read the expression
(a + b) << c
and still have no clue what it actually does without looking up how the types ofa
,b
, andc
define such operations. Ultimately, it is conventions — be they implicit or documented, be they cultural or tool-enforced — that dictate the readability of a language, just as @FeepingCreature has demonstrated with multiple examples.The author covers a great deal of the triumphs and tragedies of language extensibility in the “Convenience? Or simplicity through design?” section. The monstrosity of the Common Lisp
LOOP
macro, as well as userlandasync
/await
engines, are brought up because they brutally defeat static analysis, whereas the.use
method in Kotlin is shown as an example that manages to accomplish all of the needs ofWITH-FOO
macros with none of the ill effects of macros. In fact, there is another commonly used language that manages to achieve this the same way Kotlin does: Ruby. In Ruby, any method call can take an additional block of code, which gets materialized into aProc
object, which is a first-class function value. This means in Ruby it is routine to write code that looks likeFile.open("~/scratch.txt") { |f| f.write("hello, world") }
. This is an example straight out of the Ruby standard library, and it is the convention for resource management across the entire Ruby ecosystem.Now, even though Ruby — like Python — does not support macros, and even though it got resource management “right”, just like Kotlin did, it has nevertheless managed to dole out some of the most debilitating effects of metaprogramming. Ask anyone who’s worked on a Rails project, myself included, and they will recount how they were victimized by runtime-defined methods whose names were generated by concatenating strings and therefore remain nigh unsearchable and ungreppable. A great deal of research and literature have been dedicated to making Ruby more statically analyzable, and they all converge towards the uncomfortable conclusion that its capability of evaluating strings as code at runtime and its Smalltalk-like object system both make it incredibly difficult for machines to reason with Ruby code without actually running it.
Programming languages can be analyzed in the formal/mathematical sense, but when they are used, they are used by humans, and humans necessarily don’t understand languages through this lens. I’m not saying a programming language is strictly e.g. linguistic, but I am saying that it cannot be effectively described/modeled purely via formal or mathematical means. A language isn’t an equation, it’s an interface between humanity and logic.
I would accept that Common Lisp can be called a language construction set but I don’t see how it’s useful or accurate to say it’s not a language.
Well, no, but it helps that English language already has innumerable – legion, one might say – words, an English dictionary is a bountiful, abundant, plentiful universe teeming with sundry words which others have already invented, not always specifically for writing a whole novel, often just in order to get around IRL. It’s less obvious with English because it’s not very agglutinative but it has been considerably extended in time.
Language changes not only through the addition of new words and rules but also through other words and rules falling out of use (e.g. in English, the plural present form of verbs lost its inflection by the 17th century or so), so it’s not quite fair to say that modern English is “bigger” than some of its older forms. But extension is a process that happens with spoken languages as well.
It’s also super frequent in languages that aren’t used for novels and everyday communication, too. At some point I got to work on some EM problems with a colleague from a Physics department and found that a major obstacle was that us engineers and them physicists use a lot of different notations, conventions, and sometimes even words, for exactly the same phenomenon. Authors of more theoretical works regularly developed their own (very APL-like…) languages that required quite some translation effort in order to render them into a comprehensible (and extremely verbose) math that us simpletons could speak.
(Edit: this, BTW, is in addition to all that stuff below, which @agent281 is pointing out, I think the context of the original talk is relevant here).
If I had to guess I would think that this was a nudge to Scheme programming language from Steel’s point of view (or Common Lisp, but this one is quite large in comparison). Quite easy to extend Scheme that way and he does have a history with lisp-family languages. But that’s just a guess.
I think the limitation has to do with humans’ limited cognitive ability and the necessity of abstraction. I don’t know exactly what that limit is or even how you’d measure it, but for the sake of argument, let’s call it seven plus-or-minus two items.
The ideal toolkit contains seven plus-or-minus two tools which can be combined arbitrarily, following simple rules (like Unix pipes, or C function composition, or LEGO bricks), to solve different problems. Let’s say I have Problem A, and a kit of tools I can easily combine and rearrange until I find Solution A. This will almost invariable reveal the larger, looming Problem B, and Solution A is just one of the tools I’ll need to solve it. What should I do with Solution A?
If I keep Solution A as just an arrangement of tools from my toolkit, solving Problem B becomes more difficult, because my tools are operating at the wrong level of abstraction: I want to be combining and rearranging Solution A and other tools like it, not combining and rearranging the components of Solution A. For example, it’s possible to examine a problem in a Python program by inspecting the Python interpreter in gdb, but it’s a whole lot easier if you can use a Python debugger instead.
If I add a new tool to my toolkit that specifically implements Solution A, then my toolkit is bigger (it takes longer to find the tool I want) and the tools can no longer be arbitrarily recombined - one of them only does a special, specific thing. For example,
tar
fits neatly into shell pipelines because it’s a streaming format, but you can’t efficiently access a specific file in a tarball.zip
solves that problem, but since it’s not a streaming format, it no longer fits neatly into shell pipelines.If I create a whole new toolkit of seven plus-or-minus two tools at the correct abstraction level to solve Problem B, I’ll be able to solve it much more quickly… but it’ll be very expensive to create such a toolkit, and expensive to teach people a new, unrelated toolkit. For example, there’s nothing can do with Docker that you couldn’t do with sysvinit, but Docker does it much more quickly… if you can spare the time to learn it.
Some people go as far as trying to create a new toolkit that’s flexible enough to solve Problem A and Problem B at the same time, and some of them manage to craft an elegant, perfect crystal of a toolkit… then it shatters to pieces when Problem C comes along. For example, the Plan 9 operating system elegantly solves most of the problems faced by Unix-like operating systems in the 1980s, but the modern concept of a web-browser doesn’t fit in that paradigm.
Most of the hardline “software simplicity” communities I’ve seen online achieve that simplicity by calling Problem B “out of scope”. In the healthier communities, somebody who actually wants to solve Problem B is directed to more complex tools; in the less healthy ones, trying to solve Problem B is taken as a sign of ignorance or moral weakness.
That said, I still think software simplicity is desirable, just within a limited scope. All the layers below the one I’m working at should be rigorously, carefully crafted by experts, and I don’t care if they’re too complex for me to understand or debug (I’m not exactly about to whip out an electron microscope to debug my CPU). The layer I’m actually on should have seven plus-or-minus two tools I can freely combine, and when Problem B inevitably arises, by definition I’m the problem domain expert who needs to rigorously, carefully craft a new layer for somebody else to build on.
How so? I did not find the presentation lacking in any such respects.
Defining person to be man or woman is obviously false and needlessly exclusionary. The talk is itself quite good and there’s a reason I’m working from it, but that production probably wouldn’t fly today.
This is not false, exclusionary, or transphobic or any other sort of “problematic”. It was simply an illustrative example of defining a concept as a union of existing concepts, that everybody in the intended audience could easily understand. It is not, and is not pretending to, say anything at all about sex, gender, personhood, or anything else. Pretending otherwise is disingenuous at best.
It’s worth noting that the production isn’t great, and Steele says so himself in the typed notes. There’s no need to accuse anyone of anything that the original author hasn’t already conceded.
From the typed version:
[Comment removed by moderator pushcx: Leaps into an off-topic rant against trans people.]
[Comment removed by moderator pushcx: Pruning an off-topic thread.]
[Comment removed by moderator pushcx: Pruning an off-topic thread.]
[Comment removed by moderator pushcx: Pruning an off-topic thread.]
I’m flagging this reply because I find it kind of dismissive. I also think that style of rhetoric will invite a low standard of discussion.
What do I see here? An abridged transcript:
I see this as penalising the author, who so far has contributed positively to this thread.
If it’s very important to you to express this opinion, we can help you do it in a way that’s clearly positive sum
If we can’t disagree with someone, even with someone who has contributed positively, then we can’t have discourse. Your flag is against free discourse.
I’m not trying to discourage disagreement itself; I’m trying to advocate for a style of discussion that leads to the flourishing of all participants.
I clearly have not conveyed myself as well as I intended, and I don’t have the motivation to improve my original comment further. You should probably just ignore it.
[Comment removed by author]
Indeed. I wonder if we can deconstruct why that happened and fix it. I think that the main reason that the talk needs to define “man” and “woman” is so that it can talk about great men and great women of history. A modern reconstruction might go through the motions of humorously defining “person”, a two-syllable word, instead.
I find this article challenging because it seems to butcher one of my sacred cows. Perhaps the issue is that simplicity can’t really exist for a truly general purpose language. Maybe we ultimately have to pick two between expressivity, performance, and simplicity. Hence all languages seem to grow without bound over time. (Or maybe it’s just that “Change gives the illusion of progress.”)
Fundamentally I believe this is not a matter of building the right tools, but of gaining experience that can be applied differently to every specific situation.
As the author says, there will always be this tradeoff between:
I believe it applies to a language, to its libraries, and to all layers of abstraction defined on top of these libraries in any given software architecture.
A general-purpose tool can never answer how to manage complexity because the answer is fully dependent on the situation. Every situation requires a different balance between these two.
This is especially true in software development. When building houses, or even cities, the needs don’t change between projects, so it makes sense to refine tools that manage the recurring complexity of these projects. In software, when a specific challenge has been solved once, it doesn’t need solving again. The next project will be completely different, with different needs, with different types of complexity.
So in software development we can never rely on rules or tools that tell us how to best manage complexity. What we need instead as engineers is to train ourselves to develop a deep understanding of and an intuition for managing complexity.
This is what makes software engineering hard and fun and a form of art.
As a consumer, how can I get the most bang for my buck? As a programmer, how can I get the most functionality for the least complexity? As a library author, how can I give the most leverage with the simplest API?
I have a fairly strong feeling that the definite answer to complexity, in pretty much any domain, is a generalisation of John Ousterhout’s “classes should be deep”: to the extent possible we want something packed with usefulness, that nevertheless remains simple enough that we can learn to use it.
It’s a cost/benefit analysis: each problem will have a cost associated with it not being solved, and each solution will have a cost associated to its implementation and use. Problems are worth solving when we can find a solution that costs less than the problem itself. Ideally we want solutions that are much cheaper than the problem.
When we get to programming, this cost/benefit business translates to a permanent tension between eventual efficiency and ease of learning: basically, the time you save by using something must exceed the time you took to learn it. I believe much of the Not Invented Here Syndrome comes from the perception that writing what we need from scratch will be cheaper (or at least more enjoyable) than taking on a dependency that does the job for us. Sometimes, typically when what we need is but a small fraction of the kitchen sink that masquerades as a library, this perception is even correct.
This cost of learning vs benefit of usage puts programming languages and libraries in sharply different spots: languages are used for a long time. Getting familiar with one will let you reap benefits for as long as you program in it. libraries however tend to be used much less often. Some standard stuff like hash tables or dynamic arrays will come up all the time, but how often do you compress .zip files or serve HTTP requests? Many libraries are only used once in a while, sometimes once period. Unlike languages, the cost of learning their API is not amortised over many uses. Thus, the simplicity of APIs is paramount. To the point that sometimes, pushing parts of a problem down to users can be cheaper than requiring them to learn a more complex API that solves the whole problem.
This need for API simplicity may have contributed to C’s long standing survival as an Interface Definition Language. Why so many ABIs are C ABIs, why all languages talk to C and not much else. That’s because C is weak. We have a few basic data types and few ways to compose them, first order functions, and not much else. This forces APIs to make their complexity explicit. Such explicit APIs are more cumbersome to use, but they’re also easier to learn. For libraries I use only once, that’s just perfect.
We are lacking good measurements, not to mention repeatable exercises, for assessing size (of anything) vs. utility (for anyone).
I agree. When it comes down to it, the thing that lets you not know how something works is trust (in the abstraction or person or whatever). If we had probabilistic estimates of trust and we’d share them with others then we could maybe start building measures of legitimacy. This is something I want to do but I’ve been having a lot of real world interfering with my computer use recently.