https://www.oilshell.org/blog/2022/03/backlog-arch.html
Why Sponsor Oils? | blog | oilshell.org
A Sketch of the Biggest Idea in Software Architecture
2022-03-12 (Last updated 2022-05-25)
This post was called Backlog: Software Architecture until I edited it
and saw a coherent theme emerge.
It elaborates on narrow waists: an idea in #software-architecture
that relates to networking, operating systems, language design,
compilers, and distributed systems.
Another title I considered is An Overview of Software Composition at
Runtime. That is, you can contrast these two styles of building
software out of parts:
1. Fine-grained static types, build-time composition, static
linking, APIs, and version numbers
2. Coarse-grained "waists", runtime composition, ABIs, IPC, and
versionless protocols
Many programmers are familiar with the first style. This post is
about the second style, which you see at large scales and long time
horizons.
This post is long and dense with links, so you may want to read it in
multiple sittings. Let me know what you think in the comments! I
especially welcome references to similar material.
Table of Contents
Background
Motivating Design Questions
What Is a Narrow Waist?
Precisely Defining "The Unix Philosophy"
Characteristics of Narrow Waists
Fallacies
Related Ideas
Examples and Elaboration
The Web Evolved In A Versionless Manner
Bytes and Text Are Essential Narrow Waists
Slogan: Text Is The Only Thing You Can Agree On
CSV, JSON, HTML - Tables, Records, Documents
Tradeoffs Between Dynamic and Static Types (FAQ)
Refinements
Projection to Waists
Emulation of Waists
Extension of Waists
Composition Between Waists
Addition of Waists
Hierarchy Among Waists
Call to Action
Jon Postel Made the Internet's Waist Narrow
Conclusion
Appendices
The Lambda Calculus Is a Narrow Waist
Wiki, Zulip
Background
I'm happy that there was great discussion on the last post, The
Internet Was Designed With a Narrow Waist:
* Hacker News
* lobste.rs
* Reddit /r/linux and /r/ProgrammingLanguages
I wrote about abstract ideas, but readers understood and applied
them. And a reader answered my question about the history of the
narrow waist, which I repeat in the Call to Action below.
On the other hand, there were a few responses that exhibited exactly
the misconceptions I want to push back on. In particular, the lack of
consideration for tradeoffs:
* Local convenience vs. global economy, flexibility, generality,
and extension
* The code perspective vs. the system perspective
* Fine-grained types vs. coarse-grained "waists"
To be convincing about this, I would dive into examples: show code,
analyze existing designs, and propose new designs. I collected a
great deal of material on the wiki and in Zulip.
But I probably shouldn't spend months writing and arguing about #
software-architecture. It's better to build something with the
principles I'm espousing.
So I'm squeezing many topics into this single post. I state the main
points, with some justification.
Update: I want to expand the project, as mentioned in November. We
now have a Github Sponsors account, and I'm waiting for a response to
a NLnet grant application.
Motivating Design Questions
To be concrete, here are some questions that these ideas will help us
with:
1. Should shells have two tiers?
+ Both external processes and internal "functions"? Both
pipelines of bytes and pipelines of structured data?
+ I argue that processes and byte streams should still be
"primary" because it makes the shell more interoperable and
useful. They are both fundamental narrow waists. Last
January: "Shells Should Shell Out".
2. Is JSON the new narrow waist for shell?
+ It's a narrow waist, but it's not as universal as text or
byte streams. For example, HTML and CSV are not the same as
JSON, and they shouldn't be.
3. How can we design a better distributed OS?
+ We need a simple, extensible OS designed around a few narrow
waists. This is the claim in Kubernetes is Our Generation's
Multics.
+ For example, this diagram indicates several O(M x N) code
explosions. Narrow waists would make the system
simultaneously smaller and more powerful.
+ This problem is out of scope for Oil, but these ideas may be
useful to others.
4. Is Docker designed well? How could it be improved?
+ I bring up this example because I've seen the claim that "the
Unix philosophy is obvious" and has been absorbed into
standard practice.
+ This couldn't be further from the truth: Docker is a recent
design, and its style is profoundly anti-Unix. (Oil's build
now uses podman, which is a nice, compatible improvement.)
+ But despite its design, Docker solved a real problem, and has
notable innovations.
So I believe the ideas below are relevant to the biggest forces and
developments in the industry. I'm glad that Docker is being
"refactored away" into something more Unix-y on two fronts: into OCI
by Red Hat and others, and out of Kubernetes. (Related: Docker's
Second Death)
What Is a Narrow Waist?
Most readers understood the last post: I borrowed the narrow waist
term from networking and extended it to software.
But it's become clear to me that not all narrow waists are alike.
It's worth distinguishing these categories, and more:
1. Small, simple mechanisms like the Internet Protocol, UTF-8, and
JSON.
+ These waists are "narrow" in a strong sense. It's not a
coincidence that Jon Postel, Ken Thompson, and Doug Crockford
were their "editors" or creators.
2. Language standards like POSIX shell, JavaScript, and C++.
+ These are big and hard to reimplement. I know this first hand
from working on Oil!
+ These are narrow waists because they solve the
interoperability problem of {user programs ...} x {language
implementations ...}
3. "Accidental" waists like Win32 and x86.
+ Their evolution isn't guided by a standards body. They're
also big and hard to reimplement.
4. APIs like LLVM. As the home page says, LLVM isn't a virtual
machine. It's really a software library that changes with each
release, requiring consumers to change their code. This makes it
different than the other narrow waists, which are more about
runtime composition.
5. ... ?
So it's worth being more specific, and the posts below will refine
definitions and explore related concepts.
The clearest objection I see to the narrow waist idea is that a
narrow waist is simply a standard! Standards enable interoperability.
But standards have to come from somewhere. A narrow waist may or may
not become a standard. Also, LLVM is not a standard, and isn't
intended to be one.
The hourglass metaphor also suggests why the idea is powerful, and
what to aim for. You want something small that interfaces with many
other things.
Precisely Defining "The Unix Philosophy"
I spent a week drafting a post called Diagrams of Three Narrow Waists
in Unix. The first sentence is:
Have you heard vague claims about "the Unix Philosophy", and are
you confused or skeptical about it?
This is a valuable post, because surprisingly the narrow waist idea
says something new and more specific about Unix! I justify this with
references, including the classic ones on this Wikipedia page.
I have diagrams of these 3 narrow waists:
1. Processes
+ {native code, shebang script, shell function, ...} x { start,
kill, pipe, redirect, run with privileges, ... }
2. File Descriptors
+ {file, pipe, terminal, socket, ... } x {read, write, ioctl,
... }
3. Tree-Shaped Namespaces of unstructured data (file systems)
+ {disk, SSD, memory with tmpfs, file with loopback, ... } x {
ls, mount, version with git, serve over HTTP, ... }
The diagrams show that Unix uses multiple narrow waists to achieve
dynamic and extensible polymorphism.
The file descriptor case shows both sides of the tradeoff. You don't
statically know what syscalls are valid on a descriptor. You also
don't know what errors you'll get! I re-learned this lesson with:
* A bug in Oil 0.9.6: write() can fail with EISDIR if the
descriptor returned by open() points to a directory.
* Bugs in Hello World: write() can fail with ENOSPC if the
descriptor points to a disk file. Python 2 has the bug but Python
3 fixed it.
Nevertheless, the polymorphic design of file descriptors makes Unix
compose, and is one reason why shell is powerful! I give examples in
the post.
Go addresses this problem with single function interfaces like Reader
and Writer, and more generally the -er pattern. Here's an interesting
quote:
It would be nice if Haskell had [open polymorphism], possibly
using Go as a model.
-- Philip Wadler: Featherweight Go
More:
* I mention the relationship to Plan 9 (fixing the composition bugs
in Unix) and REST (the uniform interface constraint).
* I link to two important academic papers, and related analysis of
Unix.
* I also noticed that Lines of Text is distinct narrow waist from
Text, which the last post depicted.
+ In fact Oil's QSN format takes advantage of this narrow
waist, while the GNU's NUL-delimited format doesn't. (This
the format xargs -0 accepts , mentioned in the xargs post.)
+ In particular, wc -l, head, tail, and tail -f work "for free"
with QSN, but you need more code like to support the
NUL-delimited format, like head -z and tail -z.
This post isn't done, but it's the one I want to publish the most.
Characteristics of Narrow Waists
In software, the most important characteristic of a narrow waist is
that it reduces an O(M x N) code explosion, allowing interoperability
and code reuse.
I also realized that there are two distinct senses of the word
"narrow":
1. In terms of architectural connection (topology).
+ For example, applications and physical networks are decoupled
by the the Internet's narrow waist. They don't interface
directly with each other.
2. In terms of the size of the concept.
+ IP is a small concept, and Unix is a small handful of
concepts.
+ But the web is a large set of concepts (HTTP, HTML, SVG,
etc.). C++ and shell are also large.
So this issue deserves some more thought, and perhaps more
terminology.
Here are more ways to characterize narrow waists:
1. They are compromises. They make systems economical and possible,
not necessarily optimal.
+ If you have a small or specialized network, you can design
something more efficient than the Internet.
2. They arise through a mix of explicit design and implicit
evolution.
+ Both the Internet and the web were designed and subject to
evolution. But I'd say the web evolved more.
3. The design can be done well or poorly. The evolution can be
guided or haphazard.
+ JSON was an explicit design, and it's much better than CSV.
+ We should try to do better at design, because the resulting
network effects mean we often get "stuck" with bad designs.
Regarding evolution:
4. Narrow waists can last for decades, usually evolving in a
versionless manner.
+ For example, Unix shell is one of the oldest languages in
wide use, and there's continuous compatibility between
Thompson shell, Bourne Shell, Korn shell, bash, OSH, and Oil.
+ A narrow waist has an amount of inertia that's proportional
to the amount of functionality that hinges on it.
5. But narrow waists can also move!
+ TCP/IP - HTTP
+ POSIX C APIs - Linux x86 ABI
6. They're subject to extreme economic pressure and network effects.
For example:
+ Windows 10 emulates Linux with WSL. (Windows also had a
different kind of Unix emulation decades earlier.)
+ Linux emulates Windows with WINE.
7. The downside of inertia is that narrow waists can inhibit
innovation.
+ For example, hardware-software co-design is inhibited because
of decades-old ISAs.
8. Narrow waists are often overextended to new applications.
+ The web was arguably overextended from a network of
hyperlinked documents to an application delivery platform (
single-page apps in JavaScript)
+ It was also extended to a desktop UI framework via Electron.
+ Linux was arguably overextended to embedded devices,
especially those with real-time requirements.
Some recent narrow waists include Docker / OCI, the Language Server
Protocol, and WebAssembly. I should be more specific about their
varying degrees of success with respect to design and user adoption.
For example, I think WebAssembly is useful, but less general than
what's been recently claimed. It's a deep compromise which involves
winners and losers.
Fallacies
Here are some common objections to the idea.
(1) Textual data is hard to manipulate with programs.
This is not an objection to the narrow waist principle! The main
claims of the principle are about interoperability and economy of
implementation.
I want to make a Simple vs. Easy argument. Narrow waists are simple
in Rich Hickey's terms (not "complected"), but not necessarily easy
to use. For example, Unix shell can be hard to learn, but its power
results in a small, extensible operating system.
(2) The web is really messy, and thus unreliable.
I make a strong Messy vs. Stable distinction. Messy systems aren't
necessarily unreliable. Quite the contrary -- the need for stability
is the cause of the mess! Continuous backward compatibility (like the
the many iterations of CSS) makes a mess, but keeps the system
working.
This relates to another concept I've been having a hard time
describing: versionless evolution, which I describe below.
Related Ideas
We can understand the narrow waist more precisely by relating it to
these ideas:
1. Metcalfe's Law states that the value of a network is proportional
to N^2, where N is the number of nodes.
+ This is related to, but distinct from, the M x N
architectural connections of a narrow waist. Architectural
connections are not network node connections.
+ Thinking about the architectural hierarchy of narrow waists
may clarify this. For example, CSV, JSON, and HTML are narrow
waists at "level 1". And each of them is literally text,
which is at "level 0". Generic operations are "inherited",
which makes the system smaller. (This idea really needs
diagrams.)
2. The Internet Protocol follows the End-to-End Principle and it's a
narrow waist.
+ This doesn't mean the two principles are the same. I view the
narrow waist as more descriptive and predictive when applied
to software.
Examples and Elaboration
Let's apply these principles to real world systems. Again, I claim
the narrow waist is the most important idea in software architecture,
because it describes the biggest and longest-lived systems.
The Web Evolved In A Versionless Manner
I'd like to elaborate on the "versionless" property of many narrow
waists. You can contrast two philosophies of versioning:
1. Version numbers that indicate breaking changes, e.g. Semantic
Versioning.
+ Linux distros and package managers like NPM like often pair
semantic versioning with ad hoc constraint solvers to find a
set of compatible versions for dependencies. This model can
be brittle because you may end up running a set of versions
that's never been tested together.
2. Continuous backward compatibility, i.e. versionless evolution.
+ There are also version numbers here, but they indicate
feature additions rather than breaking changes.
For example, the web doesn't have incompatible versions, and JSON was
explicitly designed by Doug Crockford to be versionless.
History proves this rule. In Don't Break X, I mentioned that XHTML
and ECMAScript 4 both tried to break the web with radical changes,
but they failed because of the inertia of narrow waists.
In contrast, HTML5 and ECMAScript 5 evolved the web in a compatible
way. We should study and disseminate the history of the web avoid
repeating mistakes we get "stuck with".
---------------------------------------------------------------------
Here's a good way of thinking about versionless evolution:
Relaxing a requirement should be a compatible change.
Strengthening a promise should be a compatible change.
-- Rich Hickey in Maybe Not (2018, YouTube)
Examples:
* HTML5 defined
and
to mean the same thing, whereas
previous versions of HTML were stricter. (This is the
self-closing tag issue.) So HTML5 relaxes a requirement on web
page authors, which is a compatible change.
* Adding a new feature strengthens the promise that the browser
makes to the web page author. For example, HTML5 added a