It’s a good piece that does say a lot of stuff I agree with about why blameless culture is important.
My summary of blameless culture is: when there is an outage, incident, or escaped bug in your service, assume the individuals involved had the best of intentions, and either they did not have the correct information to make a better decision, or the tools allowed them to make a mistake.
This, though… this I have issues with.
I take the blameless attitude even when I know for a fact that there’s a specific individual who meant to cause harm. Like, I might fire them if that was within my authority and it seemed warranted, but the investigation and other steps would ignore that - it’s the responsibility of their manager to take corrective action, not of technical leadership.
Why leave blame out even when blame is clear? Because I still need to analyze how they were able to do that and how to stop the next person! Focusing on blame derails that conversation and takes everything in a more personal, back-and-forth, accusatory direction which accomplishes nothing. When the conversation is about blame, it damages the credibility of everyone involved, and it just does not go anywhere useful.
There’s also this thing where “assume good intent” is a longstanding line that management tends to use to discourage discussion of hostile work environments. That’s not a technical context at all, but I prefer not to put my weight behind rhetoric that is going to be used to hurt people, even indirectly like that.
I take the blameless attitude even when I know for a fact that there’s a specific individual who meant to cause harm. Like, I might fire them if that was within my authority and it seemed warranted, but the investigation and other steps would ignore that - it’s the responsibility of their manager to take corrective action, not of technical leadership.
The latter half of this paragraph is antithetical to the intent of blameless culture. The goal of blameless culture is to solve the problem and prevent it happening again. If there’s an expectation or even a possibility of punitive action, people will tend to cover up failures and act out of self interest.
@scraps posted Etsy’s Blameless postmortems above, which does a better job than I of describing it, but this bolded quote covers it:
…engineers whose actions have contributed to an accident can give a detailed account of … and that they can give this detailed account without fear of punishment or retribution.
Yeah, I mean, I would only do that in a truly egregious situation. Not as a first resort, not as a common case, but I’m talking about scenarios where there was malice - which is itself quite rare of course, but it does happen, I’ve seen it. If somebody’s conscious intention was to hurt the company and the users, and they succeeded in doing that, I think that’s a corner case that the general principles around blamelessness aren’t really meant for.
In any situation short of that I agree with you, people need to feel safe talking about their mistakes! Trust is so hard to earn and so easy to lose, and cultivating a safe workplace for that kind of thing is really hard. I didn’t mean to over-index on the malice case, because it really is quite rare.
I appreciate you calling me on this though. It’s important that there be back and forth on this stuff.
I was pretty amused by that remark, too. It feels a bit petty to talk about my take-aways from it, so I won’t, but… well, suffice to say I prefer things here.
Yeah, I probably should’ve refrained from even posting this. But I really like this place, and find the content so much more focused, and the discussions so much better (the irony of saying this while posting the above comment is not lost on me).
I don’t think it’s necessarily wrong. I’m not a frequent HN reader, but I’ve read the occasional interesting post there. The key difference is signal to noise ratio. If someone sends me a link to an interesting HN comment and I zoom out to the whole thread, I typically see a hundred uninformed and hostile posts. If I do the same here, I typically see far fewer comments in total but very few that are aggressively uninformed and none that are abusive. That makes lobste.rs a much worse source of procrastination but a much better source of information.
I think I agree that HN has a lot more noise. Here feels a lot more focused. A lot more, dare I say, “hacker”, than “hackernews”.
HN has a nice variety of stuff, not just tech. But with the variety comes a lot of noise, low effort content, and with a broader user base the comments are prone to the same.
I only maintain a couple packages, but I was really pleased to get a Tenacity upgrade packaged right in time for this release. :) The package counts in the release notes are really something, Nix did not used to be so comprehensive!
To quote Iliana Etaoin “There is no “software supply chain” > This is where the supply chain metaphor — and it is just that, a metaphor — breaks down. If a microchip vendor enters an agreement and fails to uphold it, the vendor’s customers have recourse. If an open source maintainer leaves a project unmaintained for whatever reason, that’s not the maintainer’s fault, and the companies that relied on their work are the ones who get to solve their problems in the future. Using the term “supply chain” here dehumanizes the labor involved in developing and maintaining software as a hobby.” https://iliana.fyi/blog/software-supply-chain/
I was just thinking the other day: why don’t browsers work this way? The HTTP caching headers kind of provide this ability but pages are encouraged to operate as live, always-fresh applications.
In the context of reading information, news sources have a common issue where they’ll report some facts about a local story and then find out the information is not-factual. They will then rewrite the story. There isn’t an audit trail or a way to compare/contrast against what was previously written. I think humans need this kind of audit-ability as we are naturally bound to observing change which in turn is our only way to discern credibility.
I agree with some of this, not all of it. In particular I really want to formulate a version of this that loses the deontology but keeps the anarchy, but I think I have a lot of reading ahead of me if I want to do that.
I really love the replacing answers with questions aspect of it though.
Plus it is a much-needed exposition of some serious problems that have been around for a long time. A lot of gendery people in tech have had to grapple with this stuff but I haven’t seen anyone attempt to actually convince a general audience of it before, let alone this well.
Ah! Belatedly, thanks very much for the citation. I do safety stuff in my work and am slowly becoming familiar with the field, but I didn’t know that author.
Always happy to discuss things here. And I do mean discuss. This is an area of the world and research that is open to a lot of diverse opinions and point of views for good reasons. Arguing is rarely that useful there even when we disagree.
I see the point being made here and it’s a good point, the heuristics by which compilers decide what kinds of transformations are okay are not the same as the heuristics by which databases should decide that. I think that honestly I come down on the side of treating databases more like compilers rather than less, but it’s a legit topic to discuss
but the example doesn’t shine light on that; if anything it obscures it
for a start, neither of those transformations is valid and not for a deep reason but because they do have different semantics
the semantics of an ON clause are clear and well-defined. the “when” of it is not, so the article’s technically right, but that’s only because time is a bad metaphor for SQL’s execution model
the author suggests that the confusion is caused by random() not being a pure function, ie. depending on things other than its inputs. I think that’s a red herring. I think the confusion is because random() doesn’t care about the values in the rows at all!
therefore, I’d like to work through an example with a function that does care about the values, and is also pure. I think when doing that, it’s obvious that these transformations are disallowed. consider:
SELECT count(*) FROM one_thousand AS a
INNER JOIN one_thousand AS b ON gcd(a.id, b.id) = 1
(this is a close match to the original query in the article, I just changed random to gcd and broke it into two lines for readability)
it should be obvious that there’s no way to turn that into something in the form
SELECT
count(*)
FROM
(SELECT * FROM one_thousand AS a WHERE gcd(a.id, ????) = 1)
INNER JOIN
one_thousand AS b ON true
because the join “hasn’t happened yet” in the subquery, so b isn’t visible to it. that’s clearly defined from the spec.
similarly, you also can’t transform it to something with structure analogous to
SELECT
count(*)
FROM
(SELECT * FROM one_thousand AS a WHERE gcd(a.id, ???) = 1)
INNER JOIN
(SELECT * FROM one_thousand AS b WHERE gcd(???, b.id) = 1)
ON true
because, again, neither subquery can see the other. the semantics here ARE well-defined.
I don’t have time to go through it in detail right now but I am pretty sure from my math background, and from working through it in my head, that if you were to actually run the various versions of the query with random() a few thousand times, all three of the author’s queries would give results drawn from different possibility distributions. (I can’t quite prove to my own satisfaction that the third one is different from the first, but it feels different. the second definitely is not the same as either of them.)
probability math is its own complex subject which trips people up a lot, but the weird behavior of the author’s examples is precisely due to the semantics of where in the process the operation happens - the part that SQL specifies quite explicitly! it’s exactly analogous to why my three gcd examples don’t mean the same thing.
now, if the value of random() were constant - depending neither on other values in the query nor on side-effects outside the query’s control - then the transformations would be valid. for example, if the original ON clause were just ON true, all three queries would mean the same thing, both to anyone observing their output and to the formal semantics of SQL. but that’s not the case, so the transformation is disallowed.
hope this actually helps, it can be hard to tell with these things whether I’m making it better or worse :)
just replying to myself to add: I figured out the probability distributions, more or less. I think they’re normal distributions but I couldn’t swear to that; at any rate…
the first one gives values centered around 1000*1000*.05, ie. 50,000.
the second one also gives values centered around 1000*1000*.05 but, interestingly, it only gives values that are divisible by a thousand. this is because rows are discrete, you can’t have part of a row, so you get a spreading-out effect like when you’ve gotten too aggressive with the levels tool in photoshop.
the third one gives values centered around (1000*.05)*(1000*.05), ie. 2,500
again, the intuition for this is that in both the random() example and the gcd() example, it matters whether you’re making a choice about a pair of rows that have already been put into correspondence, or whether you’re going to put them into correspondence afterwards at some point.
This is a neat demo of the custom “diff” feature in Git, which I hadn’t seen before.
I built my own tooling to solve this problem: https://datasette.io/tools/sqlite-diffable - which outputs a “diffable” copy of the data in a SQLite database, precisely so you can store it in Git and look at the differences later.
I’ve been running that for a couple of years in this repo: https://github.com/simonw/simonwillisonblog-backup - which provides a backup of my blog’s PostgreSQL Django database (first converted to SQLite and then dumped out using sqlite-diffable).
Simon, just today I went down the rabbit hole that is your blog and, by extension, Datasette. I’m honored to hear from you!
I’m curious: what advantage does this data format have over plain SQL dumps? It’s hard to justify any additional dependency, but I’m sure you had a compelling reason to build a custom format.
There is one huge benefit of storing the data in a textual format: repository size.
Git’s storage strategy is notoriously bad at handling frequent changes to binary files. Though, if someone’s already made this choice and you have to deal with it, the strategy you mention here is a fantastic one.
Git’s storage strategy is notoriously bad at handling frequent changes to binary files.
Although I’m sure git could have better binary deltas the main reason for this is that binary formats are very poor at information preserving. When everything gets scrambled on every change there’s not much that can be done.
A conversion process is an opportunity to re-structure the data into a more regular format, thus giving deltas the opportunity to do something. You should get similar results with a more regular binary format, and similar issue with scrambled text.
Although I’m sure git could have better binary deltas the main reason for this is that binary formats are very poor at information preserving. When everything gets scrambled on every change there’s not much that can be done.
I think that’s not quite true. The problem is in semantics-unaware comparison.
In text, this is less obvious because there is very little embedded semantics. I work around git’s foibles in text by putting one sentence on each line and so changing a sentence changes a line. Git understands this and works well. I’ve worked on other projects that want to line-wrap text at 80 column boundaries. If you make a one-word correction near the start of a long paragraph and re-wrap it, git doesn’t see this as ‘word changed and rewrap’, it just sees that a load of newline characters have changed. It isn’t aware that the newlines are not semantic markers (if it were, it would strip all single newlines, capture double newlines, and re-wrap on checkout).
If I have cross references in a text format, I will express them symbolically with a target marker and a reference to that target and so it’s fine to treat them as just a range of bytes. In a binary format, I will have a structured notion of records and cross references will either point to a specific record number or to an entry in an indirection table. Moving the target record causes something else to be updated elsewhere.
The CODA distributed filesystem dealt with this by having plugins that could extract semantic changes. For an SQLite database, this might be a SQL statement that drops records that have gone away and inserts ones that are added. For something like a Word document, it might be the track-changes record that describes the set of modifications that are applied. These changes could capture higher-level changes and were often smaller than a text or binary diff.
Some years ago, I had a student build a system that did tree transforms on ASTs to effectively run refactoring tools in reverse: given a before and after snapshot of source code, track what refactoring has been applied. The tool was able to distill multi-thousand-line diffs into short summaries like ‘the foo function was renamed to bar and all callers were updated except the one in mistake.c’ (and, when you saw something like that, you’d know what the next commit was going to be). If git were able to capture things like that, it would handle diffs on source code more densely (though I doubt anyone would care because source code changes are limited by the speed at which people can write code and so tend to be small even if you store a complete copy of the source file per diff). It’s more noticeable in files where there is not a linear relationship between typing speed and data size.
A VCS can not stake its storage layer on a boondoggle.
On the other hand you can design your binary format with sync-ability in mind.
In text, this is less obvious because there is very little embedded semantics. I work around git’s foibles in text by putting one sentence on each line and so changing a sentence changes a line. Git understands this and works well.
Git does not actually care a whit, delta-ification does not deal with lines. The diffs you see in your terminal are generated on the fly, it’s not how git stores things.
Git’s delta-ification is quite literally copy and paste between objects. A closer simile would be something like LZ77, except between a base object and a new object instead of having an intra-object window (there is also an intra-object window because the literal or delta-ified objects are DEFLATE-ed).
If you make a one-word correction near the start of a long paragraph and re-wrap it, git doesn’t see this as ‘word changed and rewrap’, it just sees that a load of newline characters have changed.
It sees a bunch of stuff has changed. Not especially newlines. Git doesn’t see newlines as anything more than a byte value.
It isn’t aware that the newlines are not semantic markers
Thankfully, since they routinely are.
(if it were, it would strip all single newlines, capture double newlines, and re-wrap on checkout).
If I have cross references in a text format, I will express them symbolically with a target marker and a reference to that target and so it’s fine to treat them as just a range of bytes. In a binary format, I will have a structured notion of records and cross references will either point to a specific record number or to an entry in an indirection table. Moving the target record causes something else to be updated elsewhere.
And that is an issue with how the binary format is designed. You could encode records and cross references to limit cascading changes as you do in your text document.
It’s more noticeable in files where there is not a linear relationship between typing speed and data size.
It’s more noticeable in files where there is a non linear relationship between the number of changes and the amount of change. It’s not “typing speed” and “data size” which cause the entirety of a png file to get rewritten and scrambled when you change a few pixels at the start of the image.
Do the same with text, and you’ll have the same issue.
First of all, SQL, from the get-go, doesn’t have a notion of the execution order of things. Impure functions mess with that notion a bit, but the problem here is the impure functions—they’re the culprit here.
Ok, we can blame random() instead of the optimizer or the SQL language. But then what behavior should we expect from random()?
Evaluate once for the whole query.
Evaluate once per batch of rows.
Evaluate once per partition.
Evaluate once if there are fewer than 1000 rows; otherwise evaluate once per row.
Always return 0.4.
It wouldn’t be useful to permit all of these, so how do we define it in a way that’s both useful and permits optimizations?
I think the argument in this post is dubious at best. Obviously the optimizations that a database query engine performs should be semantics-preserving in the same way that compiler optimizers are. In the case of an ON clause in a join, the semantics are that the expression is evaluated on the cartesian product of the rows from the tables that are being joined. In the previous article, SQLite and CockroachDB optimize incorrectly because they assume that a function that doesn’t depend on the contents of one row is effectively constant for any row from that table. This isn’t true for random(), but I don’t think there are many functions that are weird in a similar to random() so it’s not a good example from which to generalize like this article does. The date_part() example is not an example of a side effect: it’s a straightforward dependency on the contents of a row, which is the thing that random() lacked, so it won’t break the push-down optimization in the same way.
If you push down date_part you might evaluate it on rows that it would not get evaluated on if you do not push it down because the other side of the join might be empty.
Oh right, now I get the point about empty joins - I thought it was performance, I missed the effect on correctness. There are lots of partial functions … I am dismayed that the Postgres docs for mathematical functions don’t appear to say what happens if you divide by zero or take the logarithm of a negative number (maybe I am looking in the wrong place?). Anyway, I was wondering if they raise an error or return NULL. In the context of a join, it would make the planner’s job easier if errors became NULL; maybe there’s a wrapper function to NULLify errors.
They raise errors. You could easily write your own safe_div and safe_log functions, and more importantly declare them immutable the same as would be for PG-native versions, but in a where context you could even more easily check in another predicate before proceeding to divide or take the log since ((x <> 0) and (n / x > 1)) only needs to fail the first branch.
Obviously the optimizations that a database query engine performs should be semantics-preserving in the same way that compiler optimizers are.
If the semantics aren’t defined (different databases don’t agree on the them), what’s there to preserve? This is essentially undefined behaviour. It’s not as harmful as UB in C, but very similar in that the optimizer can produce different results because the results you got the first time around weren’t guaranteed anyway.
Similarly, if you use LIMIT 1 without an explicit ordering, you might get different results at different times, even with the same query and the same data because the optimizer might select a different plan (after a VACUUM or an ANALYZE, for example).
One could say these are cracks in the declarational facade that databases try to put up. The abstraction leaks and the underlying implementation/execution strategy shines through.
You can have a nondeterministic semantics. That is a perfectly fine thing to do. The specification describes a range of legal behaviours and intermediate states, instead of a single legal behaviour; and a well-formed program should ensure that, under all legal behaviours, it ends up in the same final state (or, rather, that all allowable final states maintain application invariants; for example, two newly added rows could get added in different orders and so have different ids, but that probably doesn’t matter).
I guess my point there, that I should have made more explicitly, was that I think answering this question in a coherent way is incompatible with a declarative language. Or at least the ones we are stuck with today. I could imagine a language designed to have a sane answer to this question and still have a lot of the good properties that a language like SQL has, but I don’t know what it would look like and I’m not sure SQL is it.
Yeah, I agree random() doesn’t fit well into the language. :(
But I bet you could rescue it! You could find a definition that explains the behaviors people expect, but is still declarative.
For example here are some things I expect about random():
multiple random() calls in the same expression are independent
random() calls on separate rows are independent
When a subquery uses random(), the subquery result for different rows of the outer query are independent.
multiple requests are independent (even though the query text is the same).
So you could define random() as a function that takes a whole bag of rows, and marks each row with a value such that the overall distribution comes out right. Or you could think of it as a function that maps each unique row ID to an independent random value–even though these dependencies aren’t visible in the user syntax.
oh, good. this is a frequent topic of confusion when ML people need to communicate with non-ML engineers and it’s really nice to have a resource to point people at.
It has never been possible to “just copy a program from one computer to
another,” except in the special case where the two computers are nearly
identical in hardware layout. If anything, our lives in the Wintel era gave us
an unprecedented level of compatibility, and the situation has only improved
since then.
Similarly, most hardware doesn’t actually have a primitive approach to putting
“pixels on the screen.” It only takes one adventure with a breadboard to
understand this, but for some reason, he thinks that we should be able to
gloss over the nature of the hardware. It’s yet another instance of
modern-day vaxocentrism. (And
of course, this is all beside the idea that “every frame is a painting,” and
that screen access should be done per-draw-call instead of per-pixel, as in Wayland.)
Video games used to be about hacking the target platform. The typical emulator
has to not just emulate the hardware, but also all of the timing quirks and
errata, because programmers of yore depended on the platform’s
weirdness and not just its API
or ABI.
It is almost humorous how he cannot define simplicity, even as he exhorts us
to simplify. “It only requires will,” he claims! Really? And yet there is no
roadmap to simplicity.
This is complete and utter lunacy. Never before in the history of computing has it been more difficult to draw a triangle. I know OpenGL and a few years ago, I wanted to expand my knowledge and learn Vulkan, but when I saw this, I completely stopped caring about low level graphics.
And another point I’d like to make: He’s completely right about how difficult it has become to just draw pixels on the screen. I’m not sure how experienced you are with graphics programming, but even well-established, battle-hardened libraries like SDL2 have difficulties drawing pixel-perfect lines unless you write your own shaders. You go through such ridiculous, complex pipelines that you literally cannot control which pixel has which color. This is new, it didn’t use to be like this.
Drawing pixels directly on the screen is fundamentally not part of the modern hardware. I think that Blow imagines hardware where VRAM is memory-mapped such that the CPU can directly alter pixel values in VRAM. However, modern hardware usually works with texels instead of pixels, and prefers managed DMA for VRAM updates; to draw pixels on the screen, we modify a texture in main memory and then arrange for it to be copied to VRAM by a co-processor. This facilitates repeated draw calls which produce complete frames with double-buffering; it is complicated, but we do this so that videos (and video games) will render smoothly.
I don’t know. I don’t think Blow wants to go back to racing the beam or fullscreen tearing; I don’t think that that meets his definition of simplicity. I think instead that Blow’s simplicity is about what he finds familiar and primitive, and his mistake is assuming that his environment is universal.
For what it’s worth, having written a couple GPU drivers, I think that it’s not worth trying to control every single pixel directly. In GL, the programmer doesn’t really have control over the viewport, and it’s already hard enough to ensure that a scene renders properly when the viewport is stretched in awkward ways. Sometimes I think that GLUT got it right and most other toolkits got it wrong; GLUT is awful, but it has a good approach to variable frame rates and lag.
Thanks Corbin, I think you said the actually important stuff, and I agree.
I want to weigh in to point out also that the audience of Vulkan is not game programmers, it’s game engine developers. The complexity in actually rendering anything is because the API isn’t trying to provide facilities to render things, it’s trying to not get in the way of people who are building those facilities. It turned out that OpenGL did get in the way, because all the well-trodden paths that it provided for common functionality became compatibility and performance hassles that engine devs would have to reverse-engineer and work around.
I have no idea whether this is a good thing or a bad one, even after chewing on it for years. There’s an argument to be made that it makes game programming less accessible to individuals and small teams by enshrining the large, corporate model of doing things in the standard. There’s also an argument to be made that individuals also benefit from clearly specified semantics without unpleasant performance surprises.
Which argument “wins”? I don’t claim to know, but I know that advocating for a return to a past that never existed isn’t productive.
I don’t really see an argument in your story that I can disprove, so I’ll just state my experience: I disagree.
There are more programmers than ever before. There is more hardware than ever before. Both are getting more advanced, which means better but also more complicated.
Give me a terminal with QBASIC and I can fill the screen with pixels in 5 minutes despite literally not having touched the language in a decade. On a modern computer, I don’t even know where to start. Maybe I should use OpenGL, or a Windows Forms App, or make a website with a canvas. I think it would take me at least ten times as long to get some pixels on the screen.
(For hardware it’s the same. VGA, an analog protocol, is piss-easy. The “modern” alternative, HDMI, requires a much higher frequency (10x), and a special encoding (TMDS). Same for keyboards, PS/2 is piss-easy, compared to USB.)
Now, I will not argue that things are equivalent, because they are not. The options I listed use hardware and a window manager, and have much better graphics (e.g. better resolution, 16M colors instead of 256). But, if you squint your eyes a bit, you could make a reasonable argument that people are 10 times as productive in QBASIC. This is not objectively true, of course, the modern variant is doing more. But nowadays, people also expect more. If you make a website, people expect it to be accessible, responsive, reactive, dynamic, and pretty. So the bar is constantly raised.
There are also more tools, but due to the increased complexity we (being mere mortals) are often forced to use them as black boxes. Not every combination of tools work well together. So we’re stuck in this loop where expectations, the number of options, and the complexity (both essential and accidental) are constantly increasing, while the tools were are using get more opaque.
See sibling response for graphics considerations. One could still use QBASIC on modern hardware, but there will be underlying shims to paper over how VRAM is actually accessed.
PS/2 is a great example of how hardware design has moved on. The peripherals on PS/2 ports aren’t hot-pluggable, and if incorrectly implemented, they can impair or even damage the computer. This all was considered in USB’s design. I suppose that there could be some notion of simplicity vs. isolation, although it’s not obvious to me.
I don’t know. I understand your points, but I’m also not sure whether we’re moving backwards or forwards in terms of simplicity just because we are moving forwards in terms of safety and throughput.
That we would get from other people. My knee-jerk reaction every time I hear about simplicity is to link to John Ousterhout (and his book, A Philosophy of Software Design, that I would recommend to any beginner).
Thanks for the link. I find it quite interesting that he says that what we need in computer science is problem decomposition, but then he spends most of his time talking about software engineering and productivity. I suspect that this is a sort of memetic blindness, since I’d be surprised if he’s been working on this for half a century without ever hearing about category theory.
Very much agreed. I absolutely would not be where I am in life if I’d treated programming as a curriculum to follow rather than something to play around with. No chance.
Among the maxims on Lord Naoshige’s wall there was this one: ‘Matters of great concern should be treated lightly.’ …Among one’s affairs there should not be more than two or three matters of what one would call great concern. If these are deliberated upon during ordinary times, they can be understood.
“Deliberated upon during ordinary times” is key, here. It means: think deeply about matters of great importance in advance, so your decision making on the spot will be easier.
This is some of the best advice I’ve ever read, and has helped me weather exceptional crises particularly well.
I can’t help but think this may all have turned out differently if the engineers involved had followed a similar practice.
for SURE, and I fully co-sign this advice. I like to say that if you don’t do your moral thinking in advance, the big, high-impact decisions don’t feel like big decisions at all, they feel like one more work item to get through on a Friday afternoon before going home for the weekend.
It’s surprisingly easy for a culture to turn toxic / dangerous / criminal in small increments, such that each individual increment can be rationalised.
I’d guess this is the way most good people turn bad. They don’t wake up one morning and decide “time to join the dark side of the force!”. Rather they wake up one morning and realise, to their horror, that it happened without them ever really noticing it.
your proposed set of features sounds good to me. also I think things like issue trackers, pull requests, discussion threads etc are better done as separate tools rather than as part of the main tool.
Oooooh, that’s very cool! I was not aware. Even better reason to give it a go. :-)
I distinctly remember the fun moment when I finally rebooted into a working system to realize that the only tools I have available to “get online” and build further are ping, telnet and ftp.
On the other hand, this might be a risky endeavor for you. Since you like building such alternatives, you might be tempted to cook yourself a custom package manager. And that’s a deep rabbit hole to fall into. I mean, trying to automatically detect shell script dependencies…
I have a copy of the Postscript language reference manual from 1985, and it has an arcto operator for drawing proper circles. You can see it in the PLRM 3rd ed. on page 191.
It’s still an issue in PDF. It only supports straight line segments and cubic Bezier curves. PS is still around but it seem to be on the way out. For one, macOS removed a native feature to view PS files. And even before that PS were converted to PDF. This is an issue with PDF/E specifically conceived as a format for exchange of engineering data
I no longer have my copy of the reference manual but I believe the goal with PDF was to construct a minimal subset of PS functionality, to make it simpler to implement. from that mindset it made sense to take out circles, since they were also taking out most of the flow control to avoid having to deal with DoS issues
the original goal, I mean. today of course it has JavaScript support, clickable URLs, network features for advertising, and all manner of other crap. they haven’t really revisited the actual drawing operators in a long time though
That doesn’t mean it’s a perfect circle. I would strongly suspect it’s made of Bézier curves, just because those are the primitive shape PS uses. An arc isn’t going to be rendered as-is — it gets combined with any other path segments, then possibly stroked (which turns it into another shape, an outline of the stroked area) or filled.
This seems like a great talk for beginners. My feedback is not aimed at beginners, but at Nix evangelists.
Because there were no other options at the time, Nix uses its own programming language also named Nix to define packages.
This doesn’t quite sound right to me. While Eelco’s thesis doesn’t directly refute this claim (say, on p69), it does explain that the Nix expression language is the simplest possible language which has all of the necessary features, and it also explains why each individual feature (laziness, recursive definitions, attrsets, functions, etc.) are necessary for defining packages. The thesis also explains why languages like Make are unsuited to the task.
A NixOS module is a function that takes the current state of the world and returns things to change in it.
This is perhaps too imperative to be true. NixOS configuration is generally monoidal, and often commutative (or configuring underlying systems which commute, e.g. networking.firewall.allowedTCPPorts which is sorted by the iptables subsystem.) It’s important to not give folks the false impression that adding a NixOS module is like adding a SysV init script.
yeah a lot of good language design is saying no to features or syntactic decisions because of concerns that seem very abstract, because past experience has shown they have far-reaching negative impact
with is in that category and should be removed, imo. I find it super convenient but that does not outweigh the loss of referential transparency. there have been a few proposed replacements, some of which are kind of okay although nothing perfect, but at the end of the day it’s a damaging feature and even just removing it entirely ought to be on the table
it’s hard to do that when there’s existing code, but it’s possible, and it does pay dividends down the line
Right, I mean, I’m a known nix partisan as it were, but in my view I’d say that there is no better expression language for the thing nix is doing today, and there also wasn’t at the time it was new. :)
Sure, but these are written once somewhere deep inside lib, and then used declaratively. You won’t really be writing functions as you would in a regular PL, and I would argue that most package descriptions can actually be read by humans if you parse them more or less as you would a JSON, which is the important mental model.
Nix seems like a worthwhile investment to learn. Though (as mentioned with the comic sans slide) the ecosystem is somewhat confusing.
Among the things that add a barrier to entry is needing to learn yet another custom configuration language (funny that the post starts off by complaining that NGINX has one). I’m wondering if a wrapper in a lang I might already know, like Ruby or Rust, might ease that one small burden so I could focus on the outcome and not the syntax.
If you already know a programming language with some functional elements (Rust definitely fits that bill), I’d say Nix the language can be learned in an hour by taking the time to read Nix pills 4 and 5.
The language is definitely quirky, but it is also extremely small. Warts are an annoyance but not a complete blocker.
Then however you have to find out what all the functions in the lib thingy do (I think it’s provided by nixpgs and not Nix the language but I am still not sure), and this is IMO the big barrier to entry.
funny that the post starts off by complaining that NGINX has one
NixOS is surprisingly old: Wikipedia tells me that the initial release of NixOS was in 2003, while nginx was in 2004. Maybe nginx should have used nix the language for configuration from the beginning :P
That’s correct, lib is part of nixpkgs, not part of the language. Strictly speaking there’s one lib that’s used when writing packages and another that’s used when writing modules, but you’ll rarely need to worry about that distinction.
I really want guix to succeed, but fear it’s lack of network effect will continue to hamper it. nixpkgs just has so many more packages and people involved. Sadly, guix will continue to be even more niche than the niche nix as long as it’s tied to GNU. In the real world, purity doesn’t really work for operating systems any more than programming languages.
Eh, “pure” is a very overloaded term that has no meaning to the general computing public. I think it’s safer to say that they are more strictly focused on being repeatable (same inputs -> same outputs).
Yep. Only that one simple thing with nix, that I can just replicate this exact environment perfectly in another machine every single time is, for me, so valuable I kind of don’t mind some of the quirks in nix/nixpkgs.
I think you two interpret “pure” differently: Guix is pure in the FSF/GNU sense: free of proprietary software in its repositories, to the point where it is using linux-libre (a kernel stripped of binary blobs). This has the major disadvantage of not working very well on a lot of hardware that does require those binary blobs.
nixpkgs has plenty of unfree software in it, and even though you can’t install them without going through additional hoops, they’re there, thus, it’s not pure in the FSF/GNU sense. Like, GNU considers the linux kernel impure to begin with, and you can install that from nixos without any additional hoops.
Well, Nix is simply a decade older than Guix, so it’s understandable. For my personal use, I’ve found https://toys.whereis.みんな/ to be a nice package search across many channels, and you can add any channels you want as and when you find something that’s not packaged in the main repository. As for the last sentence, Nix is a pure programming language ;)
Scheme is just not doing it for me. I just had a look at how to create something nix-shell like in GUIX, and found this reference. Unfortunately the examples (see the --expression=expr section) is exactly the sort of Perl-style line noise I can’t abide. Like, I don’t want to have to remember which complex LISP thing @ or % stand for. What’s wrong with words?
Hmm, I have to say I vehemently disagree! I’m sorry if what follows feels like flamebait, tone is hard to convey over text and I just want to address some issues with what you said. You are of course entitled to any opinions you hold. The funny thing about what you said, that % is arcane, is that %has no special meaning, that’s just part of the variable name! @ is just splat, like in javascript or python, and neither of those are words either (whereas in scheme you can express it as a word, but using @ is shorter and spending a tiny amount of time is enough to understand that it’s shorthand). The examples are extremely clear and there is a 1-1 mapping between nix-shell and guix shell until you get to the one about --expression, which you would only use if you actually want to evaluate code to do something, which you can’t do with nix-shell.
Of course someone used to Scheme would disagree ;) I tried learning Scheme many years ago, and just got the worst cognitive dissonance ever from every single token being abbreviated to death, and from using obtuse technical jargon like car and cdr instead of short, simple, everyday synonyms like first and second, or even initial and subsequent. LISP clones have a massive usability issue in that it’s not just a new programming language, but an entirely new set of words which other programming languages have moved away from.
I would love to learn a LISP clone which chose usability over trying to look like 1960s code.
Wouldn’t a Nix vs. Guix syntax debate just go back to the split between ML & LISP? That’s been one of the main syntax dividing lines in FP for decades. Having used many MLs (PureScript, Elm, OCaml, Haskell) Nix feels cozy (albeit quirky) & my limited time with Clojure(Script) or Chicken Scheme for Tree-sitter feels alien & unergonomic.
It’s a good piece that does say a lot of stuff I agree with about why blameless culture is important.
This, though… this I have issues with.
I take the blameless attitude even when I know for a fact that there’s a specific individual who meant to cause harm. Like, I might fire them if that was within my authority and it seemed warranted, but the investigation and other steps would ignore that - it’s the responsibility of their manager to take corrective action, not of technical leadership.
Why leave blame out even when blame is clear? Because I still need to analyze how they were able to do that and how to stop the next person! Focusing on blame derails that conversation and takes everything in a more personal, back-and-forth, accusatory direction which accomplishes nothing. When the conversation is about blame, it damages the credibility of everyone involved, and it just does not go anywhere useful.
There’s also this thing where “assume good intent” is a longstanding line that management tends to use to discourage discussion of hostile work environments. That’s not a technical context at all, but I prefer not to put my weight behind rhetoric that is going to be used to hurt people, even indirectly like that.
The latter half of this paragraph is antithetical to the intent of blameless culture. The goal of blameless culture is to solve the problem and prevent it happening again. If there’s an expectation or even a possibility of punitive action, people will tend to cover up failures and act out of self interest.
@scraps posted Etsy’s Blameless postmortems above, which does a better job than I of describing it, but this bolded quote covers it:
Yeah, I mean, I would only do that in a truly egregious situation. Not as a first resort, not as a common case, but I’m talking about scenarios where there was malice - which is itself quite rare of course, but it does happen, I’ve seen it. If somebody’s conscious intention was to hurt the company and the users, and they succeeded in doing that, I think that’s a corner case that the general principles around blamelessness aren’t really meant for.
In any situation short of that I agree with you, people need to feel safe talking about their mistakes! Trust is so hard to earn and so easy to lose, and cultivating a safe workplace for that kind of thing is really hard. I didn’t mean to over-index on the malice case, because it really is quite rare.
I appreciate you calling me on this though. It’s important that there be back and forth on this stuff.
Yeah that would be a super tricky situation to deal with, and I’m glad I haven’t had to!
…LOL
I was pretty amused by that remark, too. It feels a bit petty to talk about my take-aways from it, so I won’t, but… well, suffice to say I prefer things here.
Yeah, I probably should’ve refrained from even posting this. But I really like this place, and find the content so much more focused, and the discussions so much better (the irony of saying this while posting the above comment is not lost on me).
I don’t think it’s necessarily wrong. I’m not a frequent HN reader, but I’ve read the occasional interesting post there. The key difference is signal to noise ratio. If someone sends me a link to an interesting HN comment and I zoom out to the whole thread, I typically see a hundred uninformed and hostile posts. If I do the same here, I typically see far fewer comments in total but very few that are aggressively uninformed and none that are abusive. That makes lobste.rs a much worse source of procrastination but a much better source of information.
Oh I don’t know. I find myself procrastinating a lot here.
I think I agree that HN has a lot more noise. Here feels a lot more focused. A lot more, dare I say, “hacker”, than “hackernews”.
HN has a nice variety of stuff, not just tech. But with the variety comes a lot of noise, low effort content, and with a broader user base the comments are prone to the same.
I look forward to a future where these are expected features of any piece of hardware!
same, for sure
I only maintain a couple packages, but I was really pleased to get a Tenacity upgrade packaged right in time for this release. :) The package counts in the release notes are really something, Nix did not used to be so comprehensive!
To quote Iliana Etaoin “There is no “software supply chain” > This is where the supply chain metaphor — and it is just that, a metaphor — breaks down. If a microchip vendor enters an agreement and fails to uphold it, the vendor’s customers have recourse. If an open source maintainer leaves a project unmaintained for whatever reason, that’s not the maintainer’s fault, and the companies that relied on their work are the ones who get to solve their problems in the future. Using the term “supply chain” here dehumanizes the labor involved in developing and maintaining software as a hobby.” https://iliana.fyi/blog/software-supply-chain/
very much agreed, and Iliana says it well also
I was just thinking the other day: why don’t browsers work this way? The HTTP caching headers kind of provide this ability but pages are encouraged to operate as live, always-fresh applications.
In the context of reading information, news sources have a common issue where they’ll report some facts about a local story and then find out the information is not-factual. They will then rewrite the story. There isn’t an audit trail or a way to compare/contrast against what was previously written. I think humans need this kind of audit-ability as we are naturally bound to observing change which in turn is our only way to discern credibility.
I agree.
I agree with some of this, not all of it. In particular I really want to formulate a version of this that loses the deontology but keeps the anarchy, but I think I have a lot of reading ahead of me if I want to do that.
I really love the replacing answers with questions aspect of it though.
Plus it is a much-needed exposition of some serious problems that have been around for a long time. A lot of gendery people in tech have had to grapple with this stuff but I haven’t seen anyone attempt to actually convince a general audience of it before, let alone this well.
You may find some of the work from Safety science, particularly Resilience Engineering, to be useful.
In particular, Steven Shorrock has great blogposts like this one
https://humanisticsystems.com/2016/12/05/the-varieties-of-human-work/
Ah! Belatedly, thanks very much for the citation. I do safety stuff in my work and am slowly becoming familiar with the field, but I didn’t know that author.
You may find this list and repo useful
https://github.com/lorin/resilience-engineering
Always happy to discuss things here. And I do mean discuss. This is an area of the world and research that is open to a lot of diverse opinions and point of views for good reasons. Arguing is rarely that useful there even when we disagree.
Very much agreed.
but… wait…
I see the point being made here and it’s a good point, the heuristics by which compilers decide what kinds of transformations are okay are not the same as the heuristics by which databases should decide that. I think that honestly I come down on the side of treating databases more like compilers rather than less, but it’s a legit topic to discuss
but the example doesn’t shine light on that; if anything it obscures it
for a start, neither of those transformations is valid and not for a deep reason but because they do have different semantics
the semantics of an ON clause are clear and well-defined. the “when” of it is not, so the article’s technically right, but that’s only because time is a bad metaphor for SQL’s execution model
the author suggests that the confusion is caused by random() not being a pure function, ie. depending on things other than its inputs. I think that’s a red herring. I think the confusion is because random() doesn’t care about the values in the rows at all!
therefore, I’d like to work through an example with a function that does care about the values, and is also pure. I think when doing that, it’s obvious that these transformations are disallowed. consider:
(this is a close match to the original query in the article, I just changed random to gcd and broke it into two lines for readability)
it should be obvious that there’s no way to turn that into something in the form
because the join “hasn’t happened yet” in the subquery, so b isn’t visible to it. that’s clearly defined from the spec.
similarly, you also can’t transform it to something with structure analogous to
because, again, neither subquery can see the other. the semantics here ARE well-defined.
I don’t have time to go through it in detail right now but I am pretty sure from my math background, and from working through it in my head, that if you were to actually run the various versions of the query with random() a few thousand times, all three of the author’s queries would give results drawn from different possibility distributions. (I can’t quite prove to my own satisfaction that the third one is different from the first, but it feels different. the second definitely is not the same as either of them.)
probability math is its own complex subject which trips people up a lot, but the weird behavior of the author’s examples is precisely due to the semantics of where in the process the operation happens - the part that SQL specifies quite explicitly! it’s exactly analogous to why my three gcd examples don’t mean the same thing.
now, if the value of random() were constant - depending neither on other values in the query nor on side-effects outside the query’s control - then the transformations would be valid. for example, if the original ON clause were just
ON true
, all three queries would mean the same thing, both to anyone observing their output and to the formal semantics of SQL. but that’s not the case, so the transformation is disallowed.hope this actually helps, it can be hard to tell with these things whether I’m making it better or worse :)
just replying to myself to add: I figured out the probability distributions, more or less. I think they’re normal distributions but I couldn’t swear to that; at any rate…
the first one gives values centered around
1000*1000*.05
, ie. 50,000.the second one also gives values centered around
1000*1000*.05
but, interestingly, it only gives values that are divisible by a thousand. this is because rows are discrete, you can’t have part of a row, so you get a spreading-out effect like when you’ve gotten too aggressive with the levels tool in photoshop.the third one gives values centered around
(1000*.05)*(1000*.05)
, ie. 2,500again, the intuition for this is that in both the random() example and the gcd() example, it matters whether you’re making a choice about a pair of rows that have already been put into correspondence, or whether you’re going to put them into correspondence afterwards at some point.
(edit: inadvertent markdown syntax)
oh cool!!!!!
This is a neat demo of the custom “diff” feature in Git, which I hadn’t seen before.
I built my own tooling to solve this problem: https://datasette.io/tools/sqlite-diffable - which outputs a “diffable” copy of the data in a SQLite database, precisely so you can store it in Git and look at the differences later.
I’ve been running that for a couple of years in this repo: https://github.com/simonw/simonwillisonblog-backup - which provides a backup of my blog’s PostgreSQL Django database (first converted to SQLite and then dumped out using sqlite-diffable).
Here’s an example diff: https://github.com/simonw/simonwillisonblog-backup/commit/72e73b2cdd714fb1f3cd87d6a752971fc6398890
Simon, just today I went down the rabbit hole that is your blog and, by extension, Datasette. I’m honored to hear from you!
I’m curious: what advantage does this data format have over plain SQL dumps? It’s hard to justify any additional dependency, but I’m sure you had a compelling reason to build a custom format.
Mainly I wanted a format that’s somewhat database independent. Every entry on my blog is available in this single newline-delimited JSON file: https://github.com/simonw/simonwillisonblog-backup/blob/main/simonwillisonblog/blog_entry.ndjson
I don’t need SQLite to work with that data - I can pull it into anything that speaks JSON.
I also really like the diffs I get back from that - there’s no confusion with different formats of INSERT statement etc.
There is one huge benefit of storing the data in a textual format: repository size.
Git’s storage strategy is notoriously bad at handling frequent changes to binary files. Though, if someone’s already made this choice and you have to deal with it, the strategy you mention here is a fantastic one.
Although I’m sure git could have better binary deltas the main reason for this is that binary formats are very poor at information preserving. When everything gets scrambled on every change there’s not much that can be done.
A conversion process is an opportunity to re-structure the data into a more regular format, thus giving deltas the opportunity to do something. You should get similar results with a more regular binary format, and similar issue with scrambled text.
I think that’s not quite true. The problem is in semantics-unaware comparison.
In text, this is less obvious because there is very little embedded semantics. I work around git’s foibles in text by putting one sentence on each line and so changing a sentence changes a line. Git understands this and works well. I’ve worked on other projects that want to line-wrap text at 80 column boundaries. If you make a one-word correction near the start of a long paragraph and re-wrap it, git doesn’t see this as ‘word changed and rewrap’, it just sees that a load of newline characters have changed. It isn’t aware that the newlines are not semantic markers (if it were, it would strip all single newlines, capture double newlines, and re-wrap on checkout).
If I have cross references in a text format, I will express them symbolically with a target marker and a reference to that target and so it’s fine to treat them as just a range of bytes. In a binary format, I will have a structured notion of records and cross references will either point to a specific record number or to an entry in an indirection table. Moving the target record causes something else to be updated elsewhere.
The CODA distributed filesystem dealt with this by having plugins that could extract semantic changes. For an SQLite database, this might be a SQL statement that drops records that have gone away and inserts ones that are added. For something like a Word document, it might be the track-changes record that describes the set of modifications that are applied. These changes could capture higher-level changes and were often smaller than a text or binary diff.
Some years ago, I had a student build a system that did tree transforms on ASTs to effectively run refactoring tools in reverse: given a before and after snapshot of source code, track what refactoring has been applied. The tool was able to distill multi-thousand-line diffs into short summaries like ‘the foo function was renamed to bar and all callers were updated except the one in mistake.c’ (and, when you saw something like that, you’d know what the next commit was going to be). If git were able to capture things like that, it would handle diffs on source code more densely (though I doubt anyone would care because source code changes are limited by the speed at which people can write code and so tend to be small even if you store a complete copy of the source file per diff). It’s more noticeable in files where there is not a linear relationship between typing speed and data size.
A VCS can not stake its storage layer on a boondoggle.
On the other hand you can design your binary format with sync-ability in mind.
Git does not actually care a whit, delta-ification does not deal with lines. The diffs you see in your terminal are generated on the fly, it’s not how git stores things.
Git’s delta-ification is quite literally copy and paste between objects. A closer simile would be something like LZ77, except between a base object and a new object instead of having an intra-object window (there is also an intra-object window because the literal or delta-ified objects are DEFLATE-ed).
It sees a bunch of stuff has changed. Not especially newlines. Git doesn’t see newlines as anything more than a byte value.
Thankfully, since they routinely are.
https://git-scm.com/book/en/v2/Customizing-Git-Git-Attributes#filters_a
And that is an issue with how the binary format is designed. You could encode records and cross references to limit cascading changes as you do in your text document.
It’s more noticeable in files where there is a non linear relationship between the number of changes and the amount of change. It’s not “typing speed” and “data size” which cause the entirety of a png file to get rewritten and scrambled when you change a few pixels at the start of the image.
Do the same with text, and you’ll have the same issue.
ah! thanks for that
Ok, we can blame
random()
instead of the optimizer or the SQL language. But then what behavior should we expect fromrandom()
?It wouldn’t be useful to permit all of these, so how do we define it in a way that’s both useful and permits optimizations?
I think the argument in this post is dubious at best. Obviously the optimizations that a database query engine performs should be semantics-preserving in the same way that compiler optimizers are. In the case of an ON clause in a join, the semantics are that the expression is evaluated on the cartesian product of the rows from the tables that are being joined. In the previous article, SQLite and CockroachDB optimize incorrectly because they assume that a function that doesn’t depend on the contents of one row is effectively constant for any row from that table. This isn’t true for random(), but I don’t think there are many functions that are weird in a similar to random() so it’s not a good example from which to generalize like this article does. The date_part() example is not an example of a side effect: it’s a straightforward dependency on the contents of a row, which is the thing that random() lacked, so it won’t break the push-down optimization in the same way.
If you push down date_part you might evaluate it on rows that it would not get evaluated on if you do not push it down because the other side of the join might be empty.
Oh right, now I get the point about empty joins - I thought it was performance, I missed the effect on correctness. There are lots of partial functions … I am dismayed that the Postgres docs for mathematical functions don’t appear to say what happens if you divide by zero or take the logarithm of a negative number (maybe I am looking in the wrong place?). Anyway, I was wondering if they raise an error or return NULL. In the context of a join, it would make the planner’s job easier if errors became NULL; maybe there’s a wrapper function to NULLify errors.
They raise errors. You could easily write your own
safe_div
andsafe_log
functions, and more importantly declare themimmutable
the same as would be for PG-native versions, but in awhere
context you could even more easily check in another predicate before proceeding to divide or take the log since((x <> 0) and (n / x > 1))
only needs to fail the first branch.If the semantics aren’t defined (different databases don’t agree on the them), what’s there to preserve? This is essentially undefined behaviour. It’s not as harmful as UB in C, but very similar in that the optimizer can produce different results because the results you got the first time around weren’t guaranteed anyway.
Similarly, if you use
LIMIT 1
without an explicit ordering, you might get different results at different times, even with the same query and the same data because the optimizer might select a different plan (after aVACUUM
or anANALYZE
, for example).One could say these are cracks in the declarational facade that databases try to put up. The abstraction leaks and the underlying implementation/execution strategy shines through.
You can have a nondeterministic semantics. That is a perfectly fine thing to do. The specification describes a range of legal behaviours and intermediate states, instead of a single legal behaviour; and a well-formed program should ensure that, under all legal behaviours, it ends up in the same final state (or, rather, that all allowable final states maintain application invariants; for example, two newly added rows could get added in different orders and so have different ids, but that probably doesn’t matter).
I genuinely think it’s clearly defined as-is, per my long post elsewhere in the thread.
I guess my point there, that I should have made more explicitly, was that I think answering this question in a coherent way is incompatible with a declarative language. Or at least the ones we are stuck with today. I could imagine a language designed to have a sane answer to this question and still have a lot of the good properties that a language like SQL has, but I don’t know what it would look like and I’m not sure SQL is it.
Yeah, I agree random() doesn’t fit well into the language. :(
But I bet you could rescue it! You could find a definition that explains the behaviors people expect, but is still declarative.
For example here are some things I expect about random():
So you could define random() as a function that takes a whole bag of rows, and marks each row with a value such that the overall distribution comes out right. Or you could think of it as a function that maps each unique row ID to an independent random value–even though these dependencies aren’t visible in the user syntax.
oh, good. this is a frequent topic of confusion when ML people need to communicate with non-ML engineers and it’s really nice to have a resource to point people at.
This talk seems mired in a mythic past.
It has never been possible to “just copy a program from one computer to another,” except in the special case where the two computers are nearly identical in hardware layout. If anything, our lives in the Wintel era gave us an unprecedented level of compatibility, and the situation has only improved since then.
Similarly, most hardware doesn’t actually have a primitive approach to putting “pixels on the screen.” It only takes one adventure with a breadboard to understand this, but for some reason, he thinks that we should be able to gloss over the nature of the hardware. It’s yet another instance of modern-day vaxocentrism. (And of course, this is all beside the idea that “every frame is a painting,” and that screen access should be done per-draw-call instead of per-pixel, as in Wayland.)
Video games used to be about hacking the target platform. The typical emulator has to not just emulate the hardware, but also all of the timing quirks and errata, because programmers of yore depended on the platform’s weirdness and not just its API or ABI.
It is almost humorous how he cannot define simplicity, even as he exhorts us to simplify. “It only requires will,” he claims! Really? And yet there is no roadmap to simplicity.
Just to steelman Jonathan Blow’s argument: This is the code required to draw a triangle with Vulkan:
https://github.com/SaschaWillems/Vulkan/blob/master/examples/triangle/triangle.cpp
This is complete and utter lunacy. Never before in the history of computing has it been more difficult to draw a triangle. I know OpenGL and a few years ago, I wanted to expand my knowledge and learn Vulkan, but when I saw this, I completely stopped caring about low level graphics.
And another point I’d like to make: He’s completely right about how difficult it has become to just draw pixels on the screen. I’m not sure how experienced you are with graphics programming, but even well-established, battle-hardened libraries like SDL2 have difficulties drawing pixel-perfect lines unless you write your own shaders. You go through such ridiculous, complex pipelines that you literally cannot control which pixel has which color. This is new, it didn’t use to be like this.
First, I agree that Vulkan is ridiculous.
Drawing pixels directly on the screen is fundamentally not part of the modern hardware. I think that Blow imagines hardware where VRAM is memory-mapped such that the CPU can directly alter pixel values in VRAM. However, modern hardware usually works with texels instead of pixels, and prefers managed DMA for VRAM updates; to draw pixels on the screen, we modify a texture in main memory and then arrange for it to be copied to VRAM by a co-processor. This facilitates repeated draw calls which produce complete frames with double-buffering; it is complicated, but we do this so that videos (and video games) will render smoothly.
I don’t know. I don’t think Blow wants to go back to racing the beam or fullscreen tearing; I don’t think that that meets his definition of simplicity. I think instead that Blow’s simplicity is about what he finds familiar and primitive, and his mistake is assuming that his environment is universal.
For what it’s worth, having written a couple GPU drivers, I think that it’s not worth trying to control every single pixel directly. In GL, the programmer doesn’t really have control over the viewport, and it’s already hard enough to ensure that a scene renders properly when the viewport is stretched in awkward ways. Sometimes I think that GLUT got it right and most other toolkits got it wrong; GLUT is awful, but it has a good approach to variable frame rates and lag.
Thanks Corbin, I think you said the actually important stuff, and I agree.
I want to weigh in to point out also that the audience of Vulkan is not game programmers, it’s game engine developers. The complexity in actually rendering anything is because the API isn’t trying to provide facilities to render things, it’s trying to not get in the way of people who are building those facilities. It turned out that OpenGL did get in the way, because all the well-trodden paths that it provided for common functionality became compatibility and performance hassles that engine devs would have to reverse-engineer and work around.
I have no idea whether this is a good thing or a bad one, even after chewing on it for years. There’s an argument to be made that it makes game programming less accessible to individuals and small teams by enshrining the large, corporate model of doing things in the standard. There’s also an argument to be made that individuals also benefit from clearly specified semantics without unpleasant performance surprises.
Which argument “wins”? I don’t claim to know, but I know that advocating for a return to a past that never existed isn’t productive.
I don’t really see an argument in your story that I can disprove, so I’ll just state my experience: I disagree.
There are more programmers than ever before. There is more hardware than ever before. Both are getting more advanced, which means better but also more complicated.
Give me a terminal with QBASIC and I can fill the screen with pixels in 5 minutes despite literally not having touched the language in a decade. On a modern computer, I don’t even know where to start. Maybe I should use OpenGL, or a Windows Forms App, or make a website with a canvas. I think it would take me at least ten times as long to get some pixels on the screen.
(For hardware it’s the same. VGA, an analog protocol, is piss-easy. The “modern” alternative, HDMI, requires a much higher frequency (10x), and a special encoding (TMDS). Same for keyboards, PS/2 is piss-easy, compared to USB.)
Now, I will not argue that things are equivalent, because they are not. The options I listed use hardware and a window manager, and have much better graphics (e.g. better resolution, 16M colors instead of 256). But, if you squint your eyes a bit, you could make a reasonable argument that people are 10 times as productive in QBASIC. This is not objectively true, of course, the modern variant is doing more. But nowadays, people also expect more. If you make a website, people expect it to be accessible, responsive, reactive, dynamic, and pretty. So the bar is constantly raised.
There are also more tools, but due to the increased complexity we (being mere mortals) are often forced to use them as black boxes. Not every combination of tools work well together. So we’re stuck in this loop where expectations, the number of options, and the complexity (both essential and accidental) are constantly increasing, while the tools were are using get more opaque.
See sibling response for graphics considerations. One could still use QBASIC on modern hardware, but there will be underlying shims to paper over how VRAM is actually accessed.
PS/2 is a great example of how hardware design has moved on. The peripherals on PS/2 ports aren’t hot-pluggable, and if incorrectly implemented, they can impair or even damage the computer. This all was considered in USB’s design. I suppose that there could be some notion of simplicity vs. isolation, although it’s not obvious to me.
I don’t know. I understand your points, but I’m also not sure whether we’re moving backwards or forwards in terms of simplicity just because we are moving forwards in terms of safety and throughput.
That we would get from other people. My knee-jerk reaction every time I hear about simplicity is to link to John Ousterhout (and his book, A Philosophy of Software Design, that I would recommend to any beginner).
Thanks for the link. I find it quite interesting that he says that what we need in computer science is problem decomposition, but then he spends most of his time talking about software engineering and productivity. I suspect that this is a sort of memetic blindness, since I’d be surprised if he’s been working on this for half a century without ever hearing about category theory.
Very much agreed. I absolutely would not be where I am in life if I’d treated programming as a curriculum to follow rather than something to play around with. No chance.
Some advice from the Hagakure:
“Deliberated upon during ordinary times” is key, here. It means: think deeply about matters of great importance in advance, so your decision making on the spot will be easier.
This is some of the best advice I’ve ever read, and has helped me weather exceptional crises particularly well.
I can’t help but think this may all have turned out differently if the engineers involved had followed a similar practice.
for SURE, and I fully co-sign this advice. I like to say that if you don’t do your moral thinking in advance, the big, high-impact decisions don’t feel like big decisions at all, they feel like one more work item to get through on a Friday afternoon before going home for the weekend.
https://web.archive.org/web/20090430082450/https://rentzsch.com/notes/imNotGoingToHellFor35Cents is a blog post I think about a lot.
…deliberating… fraud??
Yeah.
It’s surprisingly easy for a culture to turn toxic / dangerous / criminal in small increments, such that each individual increment can be rationalised.
https://en.m.wikipedia.org/wiki/Normalization_of_deviance
I’d guess this is the way most good people turn bad. They don’t wake up one morning and decide “time to join the dark side of the force!”. Rather they wake up one morning and realise, to their horror, that it happened without them ever really noticing it.
your proposed set of features sounds good to me. also I think things like issue trackers, pull requests, discussion threads etc are better done as separate tools rather than as part of the main tool.
And to be fair that’s what SourceHut does from what I understand.
ah, certainly
For the last 20 years, I’ve whispered: “one day, I will read you. One day, i will install you on a computer. One day…”
Me too…I have also tought the same about installing Gentoo from stage 1
I have to admit, that matches my own experience with it. It feels good to say so out loud.
You should absolutely do that! I have very fond memories of it. Also,
w3m
+gpm
for the win!Already coded my own alternative to w3m ;-)
Oooooh, that’s very cool! I was not aware. Even better reason to give it a go. :-)
I distinctly remember the fun moment when I finally rebooted into a working system to realize that the only tools I have available to “get online” and build further are
ping
,telnet
andftp
.On the other hand, this might be a risky endeavor for you. Since you like building such alternatives, you might be tempted to cook yourself a custom package manager. And that’s a deep rabbit hole to fall into. I mean, trying to automatically detect shell script dependencies…
That’s exactly why I don’t want to enter that rabbit hole. I’ve only one life and I sadly need to make hard choices.
I think that LFS will be for my next reincarnation.
Is it public? 👀
yep: https://sr.ht/~lioploum/offpunk/
V2.0 is finished and “only” need to be packaged for release.
I have a copy of the Postscript language reference manual from 1985, and it has an
arcto
operator for drawing proper circles. You can see it in the PLRM 3rd ed. on page 191.It’s still an issue in PDF. It only supports straight line segments and cubic Bezier curves. PS is still around but it seem to be on the way out. For one, macOS removed a native feature to view PS files. And even before that PS were converted to PDF. This is an issue with PDF/E specifically conceived as a format for exchange of engineering data
Huh, that’s a surprising omission since most other vector graphics implementations have arcs. (eg SVG, web canvas - tho they postdate PDF)
I no longer have my copy of the reference manual but I believe the goal with PDF was to construct a minimal subset of PS functionality, to make it simpler to implement. from that mindset it made sense to take out circles, since they were also taking out most of the flow control to avoid having to deal with DoS issues
the original goal, I mean. today of course it has JavaScript support, clickable URLs, network features for advertising, and all manner of other crap. they haven’t really revisited the actual drawing operators in a long time though
That doesn’t mean it’s a perfect circle. I would strongly suspect it’s made of Bézier curves, just because those are the primitive shape PS uses. An arc isn’t going to be rendered as-is — it gets combined with any other path segments, then possibly stroked (which turns it into another shape, an outline of the stroked area) or filled.
PDF is the same rendering model as PS, basically.
This seems like a great talk for beginners. My feedback is not aimed at beginners, but at Nix evangelists.
This doesn’t quite sound right to me. While Eelco’s thesis doesn’t directly refute this claim (say, on p69), it does explain that the Nix expression language is the simplest possible language which has all of the necessary features, and it also explains why each individual feature (laziness, recursive definitions, attrsets, functions, etc.) are necessary for defining packages. The thesis also explains why languages like Make are unsuited to the task.
This is perhaps too imperative to be true. NixOS configuration is generally monoidal, and often commutative (or configuring underlying systems which commute, e.g.
networking.firewall.allowedTCPPorts
which is sorted by theiptables
subsystem.) It’s important to not give folks the false impression that adding a NixOS module is like adding a SysV init script.The
with
keyword certainly could’ve been removed from the language without removing expressiveness. I’d even say the language would be better for it.yeah a lot of good language design is saying no to features or syntactic decisions because of concerns that seem very abstract, because past experience has shown they have far-reaching negative impact
with
is in that category and should be removed, imo. I find it super convenient but that does not outweigh the loss of referential transparency. there have been a few proposed replacements, some of which are kind of okay although nothing perfect, but at the end of the day it’s a damaging feature and even just removing it entirely ought to be on the tableit’s hard to do that when there’s existing code, but it’s possible, and it does pay dividends down the line
Right, I mean, I’m a known nix partisan as it were, but in my view I’d say that there is no better expression language for the thing nix is doing today, and there also wasn’t at the time it was new. :)
TBH if I was remaking Nix today, I’d probably use JavaScript as the expression language to make people’s lives easier
It’s basically JSON for all practical purposes. Also, you want to keep computations to a minimal, it is more of a data description language.
Functions are a super important part. Recursion is used heavily to do things like overrides. It’s not really JSON.
Sure, but these are written once somewhere deep inside lib, and then used declaratively. You won’t really be writing functions as you would in a regular PL, and I would argue that most package descriptions can actually be read by humans if you parse them more or less as you would a JSON, which is the important mental model.
Try it and see what happens. There’s several projects like Nickel and Zilch already exploring the space, too.
I understand the impulse but I still feel it would be the wrong call. it’s not like it’s something really exotic, it uses infix and curly braces, heh.
Thanks for pointing this out!
Nix seems like a worthwhile investment to learn. Though (as mentioned with the comic sans slide) the ecosystem is somewhat confusing.
Among the things that add a barrier to entry is needing to learn yet another custom configuration language (funny that the post starts off by complaining that NGINX has one). I’m wondering if a wrapper in a lang I might already know, like Ruby or Rust, might ease that one small burden so I could focus on the outcome and not the syntax.
If you already know a programming language with some functional elements (Rust definitely fits that bill), I’d say Nix the language can be learned in an hour by taking the time to read Nix pills 4 and 5. The language is definitely quirky, but it is also extremely small. Warts are an annoyance but not a complete blocker.
Then however you have to find out what all the functions in the
lib
thingy do (I think it’s provided by nixpgs and not Nix the language but I am still not sure), and this is IMO the big barrier to entry.Also about this bit:
NixOS is surprisingly old: Wikipedia tells me that the initial release of NixOS was in 2003, while nginx was in 2004. Maybe nginx should have used nix the language for configuration from the beginning :P
That’s correct,
lib
is part of nixpkgs, not part of the language. Strictly speaking there’s onelib
that’s used when writing packages and another that’s used when writing modules, but you’ll rarely need to worry about that distinction.Thanks for clarifying!
sure thing!
Guix isn’t quite as widely adopted as Nix, but uses scheme which is a big win for it. Anecdotally, it’s also much easier to use.
I really want guix to succeed, but fear it’s lack of network effect will continue to hamper it. nixpkgs just has so many more packages and people involved. Sadly, guix will continue to be even more niche than the niche nix as long as it’s tied to GNU. In the real world, purity doesn’t really work for operating systems any more than programming languages.
Nix and NixOS are pure, so that’s a very weird to say!
Eh, “pure” is a very overloaded term that has no meaning to the general computing public. I think it’s safer to say that they are more strictly focused on being repeatable (same inputs -> same outputs).
Same inputs, same outputs is purity.
Yes, but if you call it purity you start making people’s eyes glaze over because it starts to sound like Haskell ivory tower bullshit lol
That seems like “oh this isn’t pure, because that’s ivory tower, this is just strictly focused on being repeatable”
I prefer to say “oh purity is super practical, look”
Yep. Only that one simple thing with nix, that I can just replicate this exact environment perfectly in another machine every single time is, for me, so valuable I kind of don’t mind some of the quirks in nix/nixpkgs.
I think you two interpret “pure” differently: Guix is pure in the FSF/GNU sense: free of proprietary software in its repositories, to the point where it is using linux-libre (a kernel stripped of binary blobs). This has the major disadvantage of not working very well on a lot of hardware that does require those binary blobs.
nixpkgs has plenty of unfree software in it, and even though you can’t install them without going through additional hoops, they’re there, thus, it’s not pure in the FSF/GNU sense. Like, GNU considers the linux kernel impure to begin with, and you can install that from nixos without any additional hoops.
I can’t remember the FSF using the term “pure” in this sense (complete absence of non-free software). It’s possible other FLOSS projects do, however.
What’s the relevance to programming languages?
Well, Nix is simply a decade older than Guix, so it’s understandable. For my personal use, I’ve found https://toys.whereis.みんな/ to be a nice package search across many channels, and you can add any channels you want as and when you find something that’s not packaged in the main repository. As for the last sentence, Nix is a pure programming language ;)
Scheme is just not doing it for me. I just had a look at how to create something nix-shell like in GUIX, and found this reference. Unfortunately the examples (see the
--expression=expr
section) is exactly the sort of Perl-style line noise I can’t abide. Like, I don’t want to have to remember which complex LISP thing@
or%
stand for. What’s wrong with words?Hmm, I have to say I vehemently disagree! I’m sorry if what follows feels like flamebait, tone is hard to convey over text and I just want to address some issues with what you said. You are of course entitled to any opinions you hold. The funny thing about what you said, that % is arcane, is that
%
has no special meaning, that’s just part of the variable name!@
is just splat, like in javascript or python, and neither of those are words either (whereas in scheme you can express it as a word, but using @ is shorter and spending a tiny amount of time is enough to understand that it’s shorthand). The examples are extremely clear and there is a 1-1 mapping between nix-shell and guix shell until you get to the one about--expression
, which you would only use if you actually want to evaluate code to do something, which you can’t do with nix-shell.Of course someone used to Scheme would disagree ;) I tried learning Scheme many years ago, and just got the worst cognitive dissonance ever from every single token being abbreviated to death, and from using obtuse technical jargon like
car
andcdr
instead of short, simple, everyday synonyms likefirst
andsecond
, or eveninitial
andsubsequent
. LISP clones have a massive usability issue in that it’s not just a new programming language, but an entirely new set of words which other programming languages have moved away from.I would love to learn a LISP clone which chose usability over trying to look like 1960s code.
maybe you’d find clojure more amenable, it is very much a modern language and (I think) uses head and tail instead of car and cdr.
Wouldn’t a Nix vs. Guix syntax debate just go back to the split between ML & LISP? That’s been one of the main syntax dividing lines in FP for decades. Having used many MLs (PureScript, Elm, OCaml, Haskell) Nix feels cozy (albeit quirky) & my limited time with Clojure(Script) or Chicken Scheme for Tree-sitter feels alien & unergonomic.
I think of a wrapper language for Nix occasionally too, but I’ve been scared away by how difficult it would be to debug the errors.