I'm truly sympathetic. The simplicity of the microcomputer era has a certain romance to it, especially if you never lived through the reality of it. There's a sense of freedom to the model of computing where only your program is running and the entirety of the machine is your own little playground. So I get why people make stuff like PICO-8 and uxn.
I agree with the criticism of Jon Blow's rhetoric, even though the tone is harsher than strictly necessary. Blow's criticisms and proposed solutions seem overly opinionated to me, and he's throwing out the baby with the bath water. He describes things like compile time and runtime hygienic macros like they're a new invention that hasn't existed in Lisp since before he was born.
However, I think targeting uxn is unfair. Uxn would be viewed better as an experiment in minimalism and portability. I think of it more like an art project.
It's unfair because the author is comparing mainstream languages that benefit from countless users' input and innovations over the course of 60 years to a niche system with a small handful of developers that has existed for maybe 2 years or so. That's a strawman if I ever heard one.
Having actually experienced it, all the way back to writing business applications in mainframe assembler, I am not nostalgic for it. Today I write mostly in Rust.
[1]: https://mwcampbell.us/a11y-history/echo.mp3 This file has a real Echo followed by an emulator. Unfortunately, I forgot where I got it, so I can't provide proper attribution.
He explained to me what a “patch” actually means, or meant back then. He was talking about debugging and implementing loaders for new hardware and such, then he mentioned a “patching area”, I asked wtf that means and apparently the release cycles then where so slow, that a binary left some dedicated space for you to directly hack in a patch. He then would change the machine code directly to jump to the patched area so he could fix a bug or extend the behavior.
Contrast this to the arguably wasteful CI/CD setups we have now.
For example, when I scroll a two-page document in Google Docs my CPU usage on an M1 Mac spikes to 20%. For an app with overall functionality that is probably less than that of a Word 95.
And at the end it's basically nostalgia, which is a really old trap, so old that the warnings are well posted for many centuries. If you're stepping into that trap, as Blow seemingly has, you have specifically ignored warnings telling you what's about to happen, and you should - I think - be judged accordingly.
Whether he has any real solutions to the decay of modern software is, of course, another question. It makes intuitive sense that, since previous generations were able to develop efficient and pleasant-to-use software for much more constrained computers than the ones we now have, we can gain something by looking back to the past. But those previous generations of software also lacked things that we're no longer willing to give up -- security, accessibility, internationalization, etc. That doesn't mean we have to settle for the excesses of Electron, React, etc. But it does mean that, at least for software that needs qualities like the ones I listed above, we can't just go back to the ways software used to be developed for older computers. So, I think you're right about the danger of nostalgia.
I suspect that this is where some of the inspiration comes from, because he mentioned being a fan of Scheme at a young age. At the same time he wants strong static typing, fine grained control and power (non restrictive).
People say this with a straight face, and I don't know if it's an elaborate joke of some kind or not.
We're building a tower of babel that requires supercomputers to barely run, and somehow end up defending it.
The comment you're replying to is not defending the explosion of complexity but pointing out that we resent it precisely because we find ourselves dependent on it. The article is pointing out that we tend to take bad or counterproductive paths when we try to free ourselves from that complexity though.
These days, we call that little playground a 'sandbox'. But I think OP's point is that sandboxes can be a lot more efficient that what they see w/ uxn. It's not exactly a secret that the whole FORTH model does not easily scale to larger systems: that's why other languages exist!
My name is Devine, and I'm to blame for the uxn disaster.. I'm getting this link thrown at me from all sides right now, so I figured I might as well chip in.
I'm a bit more on the design-y side of things, and all these fancy words to talk about computers and retro computing, are a bit beyond me, and reference an era of computing which I didn't really had a chance to experience first hand.
But let's talk about "the uxn machine has quite the opposite effect, due to inefficient implementations and a poorly designed virtual machine, which does not lend itself to writing an efficient implementation easily."
This is meaningful to me and I'd love to have your opinion on this. Before setting on the journey to build uxn, I looked around at the options that were out there, that would solve our specific problems, and I'd like to know what you would recommend.
Obviously, I take it that the author is not advocating that we simply return to Electron, and I take it they understand that re-compiling C applications after each change is not viable with our setup, that Rust is manyfolds too slow for us to make any use of it, and that they understand that our using Plan 9, C is not a good candidate for writing cross-compatible(libdraw) applications anyway.
So, what should we have done differently?
I'm genuinely looking for suggestions, even if one suggestion might not be compatible with our specific position, many folks come to us asking for existing solutions in that space, and I would like to furnish our answers with things I might have not tried.
Here are some directions we tried prior to Uxn:
https://wiki.xxiivv.com/site/devlog.html
Ah, one last thing, this is how you fib the first 65k shorts in uxntal:
https://git.sr.ht/~rabbits/uxn/blob/main/projects/examples/e...
The article suggests that Uxn programs can write to arbitrary locations on the filesystem. If that is the case, it seems like it would be really easy to change that in the interpreter. Then Uxn's security model would be essentially identical to how the article describes WebAssembly: the interpreter serves as a "sandbox" that prevents programs from touching anything outside their own virtual memory. This is a good security model and it likely makes a lot of sense for Uxn.
Otherwise, the article seems to be more bluster than substance. Uxn is probably not going to be the fastest way to compute the Fibonacci sequence, nor the most secure way to protect a telecommunications network from foreign cyber-attacks, but it doesn't need to be either of those things to be useful and valuable as a way to write the kind of UI-focused personal computer software you want to write.
Some of the emulator sandbox the file device, it is likely to be the way that all the emulators will work eventually.
Having said that, IMO, if you're having fun with uxn and its retro 8-bit aesthetic, by all means keep going with that.
It's neat, but I don't remember seeing a graphical API for it, I'll have a look :)
> For a time, I thought I ought to be building software for the NES to ensure their survival over the influx of disposable modern platforms — So, I did. . Sadly, most of the software that I care to write and use require slightly more than an 8-button controller.
Seems to me you could have saved a lot of effort by changing gears slightly to target other ubiquitous 6502 machines such as the apple 2 or c64.
I did a few C64 test applications both in plain 6502 and via cc65(which works very poorly on ARM btw), but that didn't really work out for us. I ran into issues porting VICE to Plan 9, and I had all sorts of issues with c64 sound emulation.
And "don't let the turkeys get you down". :)
What setup is that? Or, where can I read more about it? I realize this is kind of irrelevant, since the original article criticizes C and similar languages. But I'm curious.
My understanding of Jon Blow's argument is not that he is against certain classes of "safe" languages, or even formal verification. It is that software, self-evidently, does not work well -- or at least not as well as it should. And a big reason for that is indeed layers of unnecessary complexity that allow people to pretend they are being thoughtful, but serve no useful purpose in the end. The meta-reason being that there is a distinct lack of care in the industry -- that the kind of meticulousness one would associate with something like formal verification (or more visibly, UI design and performance) isn't present in most software. It is, in fact, this kind of care and dedication that he is arguing for.
His language is an attempt to express that. That said, I'm not so sure it will be successful at it. I have some reservations that are sort of similar to the authors of this piece -- but I do appreciate how it makes an attempt, and I think is successful in certain parts, that I hope others borrow from (and I think some already have).
John isn’t right about everything: he criticizes LSP in the cited talk, and I think the jury is in that we’re living in a golden age of rich language support (largely thanks to the huge success of VSCode). I think he was wrong on that.
But the guy takes his craft seriously, he demonstrably builds high-quality software that many, many people happily pay money for, and generally knows his stuff.
Even Rust gives up some developer affordances for performance, and while it’s quite fast used properly, there are still places where you want to go to a less-safe language because you’re counting every clock. Rust strikes a good balance, and some of my favorite software is written in it, but C++ isn’t obsolete.
I think Jai is looking like kind of what golang is advertised as: a modern C benefitting from decades of both experience and a new hardware landscape. I have no idea if it’s going to work out, but it bothers me when people dismiss ambitious projects from what sounds like a fairly uninformed perspective.
HN can’t make up its mind right now: is the hero the founder of the big YC-funded company that cuts some corners? Is it the lone contrarian self-funding ambitious software long after he didn’t need to work anymore?
He has made a few good games, but how has he done anything that would paint him as a competent language designer? Frankly, Blow has done very little (up to and including being a non-asshole) that would make me terribly interested in what he's up to.
The common thing in PL is to publish something written or code. So don’t be surprised when some people don’t feel like they have the time to go through an unconventional format.
This isn't a good argument for C++. If you can't get where you need to go in Rust because you are "counting every clock" you need to go down, which means writing assembler -- not sideways to C++. Once you're counting every clock, none of the high level languages can help you, because you're at the mercy of their compiler's internal representation of your program and their optimisers. If you care whether we use CPU instruction A or CPU instruction B, you need the assembler, which likewise cares, and not a high level language.
Both C++ and Rust provide inline assembler if that's what you resort to.
There are things to like about C++ but "counting every clock" isn't one of them.
It's also very hard, for me at least, to interpret the sum of his arguments in the talk as anything except "If you're not managing memory, you're not a real programmer."
Add to this that John Blow is such an exemplar of the entrepreneurial spirit that this site has basically its foundational value.
The guy worked in the rat race for awhile, saved a modest amount to self-fund building Braid. Braid smashed every record both commercially and critically for what one guy in a garage could do in games.
He took all that money, hired a few people carefully, and built The Witness, not on fucking Unity or something, but from the shaders up so that it would have a unique look and not be a clone of something else. The Witness was also a huge commercial and critical success.
His most recent project is, uh, ambitious. I don’t know if it’s going to prove feasible. But I’m sure as hell rooting for success rather than failure.
Now mostly this is directed at the auth or the post, but you’ve kind of signed up for a little of this: what the fuck is your CV?
I've watched people live coding audio software. That's a performance, like jamming or rap battling where you make music spontaneously for an audience. It's a distinct skill from being able to polish stuff in a studio, just like playing a guitar or singing live is a distinct skill, it's even distinct from working with a loop sampler (like Marc Rebillet) although it's often related.
But for a compiler, what's "live"? Somebody writes some code and you... tokenize it in real time, transform it into some intermediate representation, optimise that and then spit out machine code? No? Then it's not "live coding", you're just talking about how he got paid to stream on Twitch or whatever. Loads of people do that. Ketroc streamed his last minute strategies for the recent SC2 AI tournament, he's not even a "professional" programmer, half his audience haven't seen Java before.
There is no such thing as "safe language".
The author has clearly never seen what real safety critical code look like.
When safety/robustness really is critical, there are ways to achieve it with high confidence, and it can be done in any language as long as the team doing it is qualified and applying stringent rules.
Of course this is time consuming and costly but luckily we've known how to do it for decades.
The kind of rules I am talking about are more stringent than most people who never worked on those fields realize.
In the most extreme case I've seen (aerospace code) the rules were : no dynamic memory allocation, no loop, no compiler. Yeah, pure hand-rolled assembly. Not for speed, for safety and predictability.
that said this isn't an essay about safety, it's about the emotional appeal of a sort of false simplicity that some programmers are prone to falling for and pointing out the inherent inability of a couple projects (in synecdoche for a whole shit ton of other projects) to live up to promises of that mirage.
See https://cs.stackexchange.com/questions/93798/what-is-a-safe-... for a decent overview.
The article is likely referring to memory safety.
I write slow, half-assed Python scripts every single day, because “good enough” is in fact good enough for some things.
But over-extrapolating the “good enough” mindset to everything is lazy and makes computers less fun.
In particular:
> The most significant performance issue is that all uxn implementations we have found use naïve interpretation. A typical estimation is that an interpreter is between one and two magnitudes slower than a good compiler. We instead propose the use of dynamic translation to generate native code, which should eliminate the overhead associated with interpreting instructions.
Okay, go for it. Literally nothing is stopping you from implementing an optimizing compiler for uxn bytecode.
Meanwhile, zero mention in this "analysis" of program size, or memory consumption, or the fact that uxn implementations exist even for microcontrollers. Interpreters are slow, but they're also straightforward to implement and they can be pretty dang compact, and so can the interpreted bytecode be much smaller than its translated-to-native-machine-code equivalent.
(Interpreters also don't need to be all that slow; I guess this guy's never heard of Forth? Or SQLite?)
> Writing a decent big-integer implementation ourselves requires some design which we would rather not perform, if possible. Instead, we can use a library for simulated 32-bit arithmetic. […] The resulting program takes 1.95 seconds to run in the "official" uxn implementation, or 390 times slower than native code! This amount of overhead greatly restricts what sort of computations can be done at an acceptable speed on modern computers; and using older computers would be out of the question.
Well yeah, no shit, Sherlock. Doing 32-bit arithmetic on an 8-bit machine is gonna be slow. Go try that on some Z80 or 6502 or whatever and get back to us.
And no, using older computers ain't "out of the question". There are literally uxn ports to DOS, to the Raspberry Pi Pico, to the goddamn Gameboy Advance, to all sorts of tiny constrained systems. The author would know this if one had done even the slightest bit of investigation before deciding to shit all over uxn and the folks making it.
> The uxn system could also be a sandbox, and prevent damage outside of its memory, but a filesystem device is specified for the platform, and is often implemented, and so a uxn program can destroy information outside its virtual machine. Any isolation would have to be performed outside of the virtual machine; thus running a random uxn program is as insecure as running a random native executable.
Absolutely nothing in the uxn spec suggests that the "filesystem" in question should be the host's filesystem; it could very well be some loopback device or a sandbox or what have you. If security/isolation is desired, then that's pretty trivial to implement in a way that a tiny bytecode stack machine would have a very hard time escaping. Either the author is incapable of actually reading the documentation of the thing being analyzed, or one is being blatantly intellectually dishonest.
----
Like, I don't know what the author's beef is - maybe Rek and Devine ran over the author's dog with their sailboat, or maybe the Bytecode Interpreter Mafia smashed the author's kneecaps with hammers - but the article reads more like bone-pickery than some objective analysis.
Well, for one thing, once you get to subroutine-threaded code (which is often the most efficient way to implement it on modern architectures), it's arguable whether still counts as "interpreted".
But even then, FORTH is still several times slower than equivalent native code. Which is better than most naive bytecode interpreter, but it's also why industrial FORTH compilers do native code inlining and other optimizations to reach the desired degree of performance. At which point, what's the fundamental difference with C?
They do mention disregarding uxn's default choice to optimize for size and change it instead to optimize for speed (-OS vs -O2) to give a more level comparison (for speed specifically). But it helps to show that the comparison is a bit stretched.
Perhaps the greatest of all programmers produce redundancy while depending on very little of it in their own code. For example, Richard Hipp created SQLite, the ubiquitous and amazingly high-quality embedded database, in C. Thinking about that makes me feel like I ought to be using C in my own, much more modest attempt at a reusable infrastructure project [1]. Cue the cliches about younger generations (like mine) being soft compared to our predecessors.
[1] https://github.com/AccessKit/accesskit (it's too late now, I'm committed to doing it in Rust)
Before you do that, read https://www.sqlite.org/testing.html
SQLite has a crazy amount of verification and testing. There are something like 640x the code for tests as for the actual implementation.
If you are looking to SQLite as an inspiration to write in C, you should also consider the verification and testing that makes it work.
And that’s a self-own.
> Simplicity needs to be pervasive through the system, and further in the context it resides in, for one to benefit from it
This should have been the opening text of the essay.
I'd like to ask the author, how does he think his computer works? The hardware is rather complex and it has to work near perfectly all the time. It should be impossible according to his premise, but it's clearning happening.
Setting the content aside, when an author starts off their with with this kind of tone I'm immediately turned off.
There's no need to be so antagonistic. Jonathan Blow "said", "claimed", "discussed", or any of the infinite other neutral and non-insulting ways to refer to another person's work.
I would agree with Jonathon Blow here that it is very common to use abstractions without there being any understanding of or even an interest to discover the underlining implementation of that abstraction.
For example, why else would the question, “Do you use multithreading in NodeJS?” be a common interview question?
It is commonly known that NodeJS “runs code asynchronously” but how often would it be that an engineer could accurately explain how this is done?
While some may find the benefit in understanding the system in which they work in completion I believe a majority is content with living with assumptions.
You can write a pure-functional RPN calculator in a handful of lines of D, you can also write D's entire implementation in D including the "low-level" aspects.
I also think the distinction between low and high level code in languages that are not built around some theoretically abstract model of execution (e.g. the lambda calculus) to be rather pointless and mostly useful only for making poor arguments.
I'm also sceptical of simplicity as an unqualified idea. I have read "simple" code with no abstractions that makes it extremely hard to read, I have read "simple" code that can be considered as such because the abstractions are actually abstractions: "Abstraction is the removal of irrelevant detail".
We all want less sloppy code and less sloppy abstractions, but it's hard to do in the real world with tens of millions of developers placed under different constraints.
"I was not given time to do this correctly, I'll just use a library that adds 200mb RES but basically works for now."
The Internet Architecture Board, W3 working groups, OWASP, Open telemetry, and hundreds of other groups are working hard to standardize things so we don't have to repeat the same mistakes in a problem area. Heck even community sites like leetcode help raise awareness about sub-optimal solutions to problem spaces.
Also, the example about dynamic dispatch isn't really as persuasive as the author might think. Even if everyone benefits from these abstractions, what's the point when that abstraction is fundamentally slow on hardware in the first place, no matter how much optimization you can do? I mean, the Apple engineers have done everything they can do to optimize obj_msgSend() down to the assembly level, but you're still discouraged to use Obj-C virtual method calls in a tight loop because of performance problems. And we know in both principle and practice that languages which heavily rely on dynamic polymorphism (like Smalltalk) tends to perform much worse than languages like C/(non-OOP)C++/Rust which (usually) doesn't rely heavily on dynamic polymorphism. In these languages, when performance matters devs often use the good ol' switch statement (with enums / tagged unions / ADTs) instead to specify different kinds of behavior, since they are easier to inline for the compiler and the hardware is made to run switch statements faster than virtual calls. (Or to go even further you can just put different types of objects in separate contiguous arrays, if you are frequently iterating and querying these objects...) The problem I think for most programmers is that they don't know they can actually make these design choices in the first place, since they've learned "use virtual polymorphism to model objects" in OOP classes as a dogma that they must always adhere to, whereas a switch statement could have been better in most cases (both in terms of performance and code readability/maintainability. Virtual calls may be a good abstraction in some cases, but in most cases there are multiple abstractions competing with it that are more performant (and arguably, can actually be simpler).
The point Jon is trying to make (although maybe not that clear enough from the talk), is that we simply need better abstractions for the hardware that we have. And C/C++ doesn't really cut it for him, so that's why he's creating his own abstractions from scratch by writing a new language. He has often said that he dislikes "big ideas programming", which believes that if every programmer believes in a core "idea" of programming then everything will magically get better. He instead opts towards a more pragmatic approach to writing software, which is writing for the hardware and the design constraints we have right now. Maybe he may seem a bit grumpy from the perspective of people outside of OS/compiler/game development (since he also lets out some personal developer grievances in the talk), but I think his sentiment make sense in a big picture, that we have continuously churned out heaps of abstractions that have gone too far from the actual inner workings of the hardware, up to the point that desktop software has generally become too slow compared to the features it provides to users (Looking at you, Electron...)
A term I like to use for this phenonemon is "Inner Platform Effect".
https://en.wikipedia.org/wiki/Inner-platform_effect
The prime example is that in the 90's, Java thought they were going to make Windows irrelevant (a pile of poorly debugged device drivers or something like that).
But now Java is just Windows/Unix process.
Same with all these "alternative computing stacks" -- in the end they will almost certainly be just Unix processes.
The only situation I can think of where they wouldn't be is a revolution in hardware like the IBM PC producing MS-DOS, etc. And even not then, because we already had the mobile revolution starting ~15 years ago, and both platforms are based on Unix (iOS/Android).
----
I do think people should carefully consider whether they want to create an "inner platform" or make the platforms we ACTUALLY USE better.
That is the purpose of https://www.oilshell.org/.
Evolving working systems is harder than creating clean slates. So clean slates are fun for that reason: you get to play God without much consequence.
Sometimes clean slates introduce new ideas, and that's the best case. But a common pattern is that they are better along certain dimensions (the "pet peeves" of their creator -- small size, etc.), but they are WORSE than their predecessors along all the other dimensions!
So that is the logic of Oil being compatible and painstakingly cleaning up bash -- to not be worse than the state of the art along any dimension! I've experimented writing a clean slate shell in the past, but there are surprising number of tradeoffs, and a surprising number of things that Unix shell got right.
-----
But Oil can ALSO be viewed as a clean slate, which is sometimes hard to understand. https://www.oilshell.org/release/latest/doc/oil-language-tou...
I just drafted this doc, and surprisingly little breaks even in the non compatible mode:
What Breaks When You Upgrade to Oil https://www.oilshell.org/preview/doc/upgrade-breakage.html
So I would like to see more systems take a DUAL view -- compatible, but also envision the ideal end state, and not just pile on hacks (e.g. Linux cgroups and Docker are a prime example of this.)
You will learn the wisdom of the system that way, and the work will be more impactful. For example, it forced me to put into words what Unix (and the web) got right: https://www.oilshell.org/blog/2022/03/backlog-arch.html