The Brutalist Programming Manifesto


    T H E   B R U T A L I S T   P R O G R A M M I N G   M A N I F E S T O



I. Simplicity is essential

    We always prefer the simple and short over the complex and long.
    When we have the choice between simple or concise, we choose
    the simpler solution as long as we come nearer to our goal. It
    is easy to pay lip service to "simplicity", but few have the
    courage to really embrace it, as we fear our fellow programmer's
    verdict.  When you have the impression that an adequate solution
    to a problem is beyond your capabilities, simplify the problem.
    Complex code is not an achievement, nor does it make us better
    programmers.  Code that can not be thrown away is automatically
    technical debt and a burden. If implementation artifacts influence
    the external appearance or usage of a program, then this is
    acceptable as long as it doesn't impede the program's usefulness.
    Don't underestimate the effort to write simple code. Complex
    technology can never be simplified by adding more complex
    technology.  Creating the computational equivalent of a
    Rube-Goldberg device is something that you should consider a
    practical joke or something to be ashamed of, never something
    to take pride in. If you can't explain the internal structure
    of a software system to someone in a day, then you have a
    complexity problem. Some of the complexity may be necessary,
    but it is still a serious defect.

II. Solve problems instead of creating them

    We want to solve concrete problems, not anticipate the tasks
    others might have in the future, so we create applications
    instead of frameworks. We write editors, not text-editing
    toolkits.  games instead of engines. We do not generalize
    unnecessarily, as we will never be able to fully comprehend how
    our code might be used. Moreover, we often evade our responsibilty
    to solve actual problems by procrastinating and conceiving
    one-size-fits-all pseudo solutions for future generations that
    will probably never use them. So, we instead identify a problem
    that might be addressed by a computerized solution and do nothing
    but working towards that solution. We do not create abstractions
    for abstraction's sake but to simplify our current task.

III. We are not smarter than others, others are usually not smarter

    We do not think of how others might judge our code: they struggle
    as much as we do and they don't have any more answers to our
    problems than we have. Experience is important and necessary,
    but creativity is, too. Only beginners think they know everything.
    Experience can lead to cynicism, but cynicism leads to emptyness.
    Doing something for a long time does not automatically lead to
    experience, but failing often does.  True mastery transcendends
    problems and works by intuition trained on making mistakes.  We
    accept that we will be journeymen eternally and that mastership
    is something that is given to us by grace, not effort. So we
    don't attempt to be wizards, we just try to solve problems.
    If a programmer seems to be 10x more productive as us, then perhaps
    because he or she masters her programming environment more than 
    we do, doesn't bother with unnecessary things or has just a 
    different measure for productivity.

IV. Do everything yourself

    We do not use libraries, frameworks or third party packages
    unless we are absolutely forced to do so. Code that we didn't
    write we do not understand.  Code that we do not understand we
    can not maintain. Code that we can not maintain may change from
    version to version or may not work with another version of a
    dependency of that code or the underlying platform that it runs
    on. Other people's code is a liability and we do not want to
    take responsibility for it unless we are certain that we can
    reimplement it ourselves, if required. Forcing others do upgrade
    a piece of software only to make our code work is insulting and
    disrespectful. If you need additional libraries beyond what is
    provided by default, then build them from sources.

V. Strive for robustness

    We follow the +/- 10 year rule: write your software so that it
    can be made to work on 10 year old hardware and operating systems
    and on systems that will exist 10 years from now, with reasonable
    effort, unless your project is highly specific to brand new or
    obsolescent hardware. That means we will not be able to ride
    the newest hype in programming languages, software tools or
    libraries.  Be grateful for that. This also means you will have
    to write your code in C, or in something built on top of C.
    That's fine, because computers were designed to run C efficiently
    and will do so in the future (don't listen to the evangelists,
    they still use C, they just added restrictions and gave it a
    different name). Why should you let yourself be restrained in
    what you can express? Shouldn't you try to learn to be more
    thoughtful and disciplined instead? Designing robust software
    means you know what you are doing and doing it in a responsible
    manner.

VI. Do not think you can make computing "secure"

    We are humble enough to understand that computing will never
    be fully secure.  Buggy hardware, side channel attacks and the
    $5 wrench will always be with us, so don't fool yourself that
    you can change that with clever programming.  Avoid cryptography,
    if possible, as you should write all the code yourself and doing
    your own cryptography is a well known mistake. If you need
    privacy, do not use browsers.  If you want to hide something,
    do not put it on the internet. We accept that we can never be
    sure a communication channel is safe. Beware of security
    consultants, their agenda is a different one.  Most useful
    computing consists of handling key presses and mouse clicks
    modifying local state on your computer.

VII. Use input devices when they make the most sense

    The easiest and most efficient way of designating a point on
    the screen is by using the mouse. Use it. If changing between
    keyboard and mouse is annoying to you, get a smaller keyboard.
    Solely keyboard-driven user interfaces are often hardly
    distinguishable from ideology. Solely mouse-driven user interfaces
    can be awkward. Consider adjusting your mouse parameters,
    consider getting a better mouse and use common sense. If a UI
    feature is hard to understand or cumbersome to use, it may be
    pointless, regardless of how sophisticated and aesthetically
    attractive it may seem. If a control in a user interface is
    both neither obvious in its use nor clearly documented then it
    is pointless and should not exist.

VIII. Avoid all ornaments

    We eschew all visual gimmicks, animations and eye-candy. They
    do not increase the usefulness of our software, they just add
    complexity and show that we wasted our time and energy.  In
    user interfaces use the most basic default and stick to it. Use
    black text on white background, it is easy to read and reduces
    the strain on the eyes in a well-lit environment.  Others will
    try to convince you of the opposite, but they just try to
    rationalize their personal taste. Think of visually impaired
    users.  Don't burden the user with needless configurations. The
    aesthetics of simplicity almost always turn out to be more
    pleasing than pretty graphics, animations or subtle color
    schemes. If you need graphics, you probably do not need a full
    graphical user interface toolkit. If you need a complex graphical
    user interface, then simplify it. User interfaces do not need
    to look nice, they just should be obvious and effortless.

IX. Tools are just tools

    We use tools, we replace them, we ignore them, and we shouldn't
    be dependent on them. They may become obsolete, non-portable,
    broken, but we still have to go on.  Careful thought and "printf"
    debugging is still the best method to find bugs.  When you are
    really stuck, take a long walk and think about it. You will be
    surprised how often that works. We avoid all-powerful intermediate
    data formats, as plain text with clear structure is universal,
    easy to debug, portable and extensible. If you think you need
    a database consider first whether the file system is really not
    sufficient for your needs. The only truly crucial tool is
    between your ears.

X. Be humble

    We are not Google. We will never need the scalability that we
    so often think is what makes software "robust". Machines are
    fast enough for quite some time, now.  Making outlandish demands
    for scalability or performance is often confused with
    professionalism.  We measure before we optimize and we never
    trust benchmarks that others have done.  Hardware is cheaper
    than software. Algorithms of linear complexity, linked lists,
    statically allocated memory and global state each have their
    place and use. Think about how much of your decision to dismiss
    the straightforward aproach is based on folklore, insecurity
    or delusion.  Portability is overrated, we don't fool ourselves
    that we can manage to maintain our code on all possible platforms.
    We maintain only what we can test on real hardware. We are not
    tempted into thinking we will revolutionize the world of computing
    - we are just tool-making primates. The sharpened flint-stone
    and the wheel probably had a bigger impact on civilization than
    our sophisticated compiler or overengineered high-performance
    database.

XI. Don't work for free if you do not enjoy it

    When we create software for others to use, we are doing them a
    service (unless we expect to be compensated in one form or the
    other). When we provide a solution to a particular software
    problem, we are free to do it in the way we find adequate.  If
    a proprietary software platforms forces us to use broken software
    tools and programming interfaces, we should consider to not
    write software for that platform, unless we are employed by the
    vendor or compensated in another way. If the interface with
    which our software has to communicate is weak, then we should
    think hard before putting any effort into overcoming the
    obstacles. If the interface causes us mental or physical pain,
    we stop programming against it. Too many programmers have been
    worn out by bad languages, tools and APIs. How can we trust
    platforms and enrich their ecosystem which sucked the life out
    of us and our fellow programmers?

XII. Do not listen to others

    We never take a software methodology, school of programming or
    some random internet dude's "manifesto" at face value. Rules
    must be broken, when necessary.  You decide in the end what is
    right and what is wrong, depending on circumstances. Every rule
    has its exception, Cargo cults lurk everywhere and every promise
    that something makes your life as a programmer easier while not
    acknowledging that you will have to pay for it in one way or
    another is a lie. "Computer science" is to the largest part
    religion, propaganda and hype.  Principles are important, but
    should only guide and not control you.  Only psychopaths and
    monks are able to live up to them to their final consequences.
    "Best practices" just formalize mediocrity. Innovation means
    diverging from the mainstream. Art means creating something
    that didn't exist before.

By Felix Winkelmann

2 Likes

I wouldn’t say my software is Brutalist, but it is pretty brutal.

Usually to me. At about 3am. Just wham, ?SYNTAX ERROR REDO FROM START. Merciless.

While I don’t agree with all of these ideas (you’ll only take my white-on-black-text CMD.EXE window away from me at the point of a $5 wrench), I’m finding this resonating as I’m reading Dan Ingalls’ “The Evolution of Smalltalk” (http://worrydream.com/refs/Ingalls%20-%20The%20Evolution%20of%20Smalltalk.pdf) and staring at this from page 36:

image

“The operation of the virtual machine can be understood by examining the lower half of Figure 15” Ummmmm can it? Can it really? By me? At 3am?

Afghanistan-powerpoint-gr-006

( The McChrystal Afghanistan PowerPoint slide: can you do any better? | Stanley McChrystal | The Guardian )

And indeed, on page 42:

A virtual machine design amounts to a specification of how each step of a computation shall
be carried out. In Smalltalk-72, Alan’s original design laid out the steps required to evaluate an
expression. In Smalltalk-76, my design centered around a set of bytecodes that would carry out
the data movement and communication functions that would achieve the same global goal of
computation by sending messages. The Smalltalk-72 evaluation mechanism was simple enough that I could fairly reliably write assembly code from Alan’s specification, even where it required some further clarification. By contrast, the Smalltalk-76 interpreter was a complex virtual machine. While
it was as simple as I could make it, it nonetheless involved picking variable-length instructions
from a byte stream, loading and storing data from numerous sources, looking up methods in an
inheritance hierarchy of hash tables, etc. I don’t recall ever considering writing a native code
interpreter straight from such an architecture.

I managed to write a UXNTAL assembler in one afternoon. Just by reading the spec. (And, uh, the C code). And in Javascript, okay, but still. It felt so good! Even Dan Ingalls couldn’t imagine writing his own Smalltalk-76 kernel, which he designed himself, right in the white heat of designing it, from scratch. That’s… that’s a difference.

Yes ok he didn’t consider writing it from scratch in Alto microcode because he already had a running Smalltalk-74 he could write it in, which isn’t the same as not being able to do it, but still… being able to do a from-scratch rewrite, whenever you want, multiple times over, is a really freeing thing, and there’s a certain complexity threshold where… you lose that. I feel like 1976 is maybe when Smalltalk lost that. It got fast, yes. But…

By a very strange coincidence, nobody from other companies who witnessed Smalltalk in operation at PARC was at all interested in writing a Smalltalk kernel of their own from scratch. Instead they did pretty much the computing equivalent of a trapped animal chewing off their own limbs (ie, writing entire GUI operating systems in raw assembler, and then C++) in order to avoid writing a Smalltalk kernel. (Or licensing one, which was probably the actual problem.)

And then there’s the problem of inheritance. Ingalls (and this is in 2020) writes blissfully about “the remarkable synergy of messages and inheritance”, but Kay (in 2023) writes:

“Inheritance” needs to be really controlled in order to not turn into a nightmare (this is why I left it out of the first Smalltalk — Simula I didn’t have it, and Simula 67 did)… We got away with using inheritance in the later Smalltalks at Parc because we — and especially Dan Ingalls — were very careful with it. A later experience with Morphic in Squeak was not so happy.

Anyway, how about them big angular concrete buildings, huh. I do love concrete.

2 Likes

Huh. Winkelmann is into Scheme, Forth… and concurrent Prolog. That’s an interesting mix!

http://www.call-with-current-continuation.org/articles/articles.html

(The feeling that there really ought to be a way to make Prolog more like Forth - ie, not having variables, whether this is good or not, it just seems like it should be a thing - has burned in my brain for nearly 20 years now without managing to manifest in any usable form. If Prolog is Lisp, then $x is Forth. Solve for $x, but without using any named variables! [1] The reason for me thinking this might be interesting is reading about concurrent Prologs just like Winkelmann is talking about, and going “this whole concept of variable in Prolog seems very odd and ill-defined, especially when going concurrent, so is there any way to just bypass it entirely? If we’re going to write a Prolog on Forth, couldn’t we just… do it in Forth?”)

[1] literally in SWI Prolog:
PROGRAM:
logicversionof(lisp,prolog).
logicversionof(forth,fooblah).
logicversionof(forth,fooblaz).

QUERY:
logicversionof(forth,X).

Presumably what I want is something like:

’ forth logicversionof

(assuming ’ generates some kind of Forth equivalent of Lisp/Prolog symbols)

But! Bear in mind that that one word call, “logicversionof”, could return two values, and we don’t want then all at once. So what does it actually return, a value or something I need to step through to get the actual value? And that’s not the worst. What if I want to call the stack version of

normalversionof(X,forth).

and so I want … how do I call “normalversionof”, anyway, if I don’t have a logic variable I can put on the stack and have it operate on? Do I need logic variables but just not names for them? What calculus even is this, anyway?

Sorry about that. It’s just, it’s in my head and I’d like it out.

1 Like

I agree, it gets fragile if the language ends at being a “program” and has stopped being a specification. I’ve tried implementing the smalltalk vm from the bluebook a while back, it’s not too hard, the result is very slow and a bit convoluted for most of it, then you get at the last few opcodes and GC it gets really hairy. In that part of the book it says that it takes usually a person-year of work. That’s a big investment, and I think this sort of endavour might play against software preservation for that specific platform.

Uxn is quite complicated as far as an assembler goes, there’s lots of different token types, and lambda scopes, but instead of a year’s worth of work, it might be something more like you said, a day or two. There’s at least 5 uxntal assemblers that are maintained at the moment, in 5 different languages, I think a variety of implementations is a good thing, it decentralizes things a bit.

Some assemblers have quirks, but I noticed that for as long as they emit the same symbols files, you can move from one assembler to the next by reassembling from the rom and symbols files.

Have you seen Felix’s implementation of Strand? It’s a parallel loglang in forth :slight_smile: I’m not sure how it can interop with the forth backend, I’ve only used Fleng.
http://www.call-with-current-continuation.org/strand/strand.html

2 Likes

In that part of the book it says that it takes usually a person-year of work.

!!! Ouch.

Uxn is quite complicated as far as an assembler goes, there’s lots of different token types, and lambda scopes, but instead of a year’s worth of work, it might be something more like you said, a day or two.

Lambda scopes? Uh-oh. Did I miss something important? I just did the bare minimum to port from C to JS (because compiling Linux-flavoured C on Windows is Very Not Fun) and it really didn’t seem that complicated to me, but I haven’t written an assembler before.

I literally only did this amount of work because I couldn’t get the other Javascript one I found online to work, I couldn’t get Drifblim to run on Webuxn, and I’m on Windows and couldn’t get the Linux-flavoured C to compile. So I just wanted something to generate some bytecode that I could shove into Webuxn.

And yet it was doable, even for me.

Have you seen Felix’s implementation of Strand? It’s a parallel loglang in forth

I glanced at the start of the page about it and that’s what triggered my obsession. But I should take a closer look at what it’s actually doing, yeah.

600 lines! that’s really neat.

Uxntal Lambdas are not too hard to implement, but it’s an extra complexity, it’s a matter of keep a stack of depths, and push/pop so these can be nested into each other:

https://git.sr.ht/~rabbits/drifblim/tree/main/item/src/drifblim.tal#L326

It allows to define labels without giving them names, which is the biggest advantage really. It makes programming in Uxn a lot nicer all around.

@abs2 ( a* -- f )
	DUP2 #0f SFT2 EQU ?{ #0000 SWP2 SUB2 }
	JMP2r

Strand is worth trying out, it’s really hard to program with to me because it’s very different than how I normally think about programs, but it’s unique, it has a sort of array lang to it in how non-deterministic of the evaluation is. I think Joe Armstrong said Strand was TOO PARALLEL haha

1 Like

Here is an implementation of the blue-book specification, and its README tells a bit of its story:

2 Likes

Yup :slight_smile: I used that emulator quite a bit too, I’ve even made changes to it. I wrote a little quick guide on how to use it here. The book Dealers Of Lightning is pretty interesting for anyone interested in the story of Xerox PARC.

2 Likes

So lambdas are a bit like RetroForth’s quotations, maybe? That sounds cool. I’ll have to try to add them.

One other thing I would like to add as an extended string quote syntax, so I could put whole lines of text in and not just a word at a time. For implementing eg adventure games or old BASIC stuff.

Neat! I have to mention though, that Alan Kay (going by his 2020-2024 Quora posts) seems to really not like that book, saying it’s inaccurate. The one book about PARC that he does like is “The Dream Machine” by Mitchell Waldrop.

https://www.amazon.com.au/Dream-Machine-M-Mitchell-Waldrop-ebook/dp/B07GBCX7YC/ref=tmm_kin_swatch_0?_encoding=UTF8&qid=&sr=

I’m wondering if there’s a way to use UXN’s symmetric stack handling to make a lightweight Smalltalk-like message-sending kernel - preferably something about as light as a Forth inner loop. It feels like there’s a bit of unexplored design space there, with being able to use both stacks at the same time. Maybe, object and method selector go to the return stack, parameters to the data stack, finally run object+selector as a lookup. Everything happens in clean left-to-right FIFO order, no complicated compiler needed. Maybe. This sort of thing is why I love UXN, it offers ideas for doing little mad science experiments.

1 Like

Yes it’s complicated but it was designed to be fast and efficient on very slow, limited hardware while running an image that was meant for a microcoded machine and couldn’t be reworked at the time.

I would hope a modern design where the VM and the image format could be designed together would be a lot simpler.

1 Like

You may find the Id part of Ian Piumarta’s COLA design interesting. It’s a full object system in three objects and five methods.

We show that three object types and five methods are sufficient to boot-strap an extensible object model and messaging semantics that are described entirely in terms of those same objects and messages

from here.

1 Like

I think I remember reading that paper several years ago. The trick of putting the vtable pointer at relative address -1 to allow interop with arbitrary system data structures is clever. It all sounds great and very convincing as the first step into a new future! I wonder though if Piumarta and Wurth solved so many problems, why Viewpoints Institute then shut down and nothing else ever seemed to come of this line of research?

I’m not even sure if it’s OOP that I want. I just want something that’s fast on a tiny stack machine, but doesn’t smash RAM or the stack when I make typos.

I’ll make a different thread on this topic specifically, but you might enjoy A sketch of a minimalist bytecode for object-oriented languages which defined a simple VM to run a smalltalk like system.

3 Likes