I have definitely had a very good experience with KDE over the past year. It definitely does not look as nice as GNOME, but I can do the stuff I need to do with it, and the plasma desktop environment is mostly things that make sense (screenshot tool in KDE is quite nice and functional, if pretty ugly. GNOME’s one looks way nicer and improves on every release, of course)
We had a very unfortunate thing happen with desktop environments, with both major DE going through very painful transitions at the same time. Gnome is doing a lot of good work improving things, but so has KDE.
We had a very unfortunate thing happen with desktop environments,
It wasn’t just the DEs, either. “Desktop Linux” just generally seemed to get worse for a long time after 2010-ish. And by “worse” I’m not talking about nerdy philosophical holy wars (systemd, etc), but I just mean getting things to work quasi-correctly. Some of it was “growing pains”, like when I had to do arcane rituals to simultaneously appease PulseAudio apps and ALSA apps, but some of it was just too much stuff all breaking in a short timespan. We had NetworkManager always doing something funny, Xorg changes + Wayland stuff, libinput with weird acceleration, polkit, udev, PAM, AppArmor, etc, etc, all changing or being rewritten or replacing each other.
Desktop Linux was nuts for a while. And, honestly, with the whole Snap vs Flatpack vs whatever ecosystems, I feel like we’re still doomed. Mostly because all of the app-containerizations absolutely suck. Badly. They all take up way more space, eat up way more resources when running, take longer to launch, etc. I know and understand that maintaining a massive repository for all supported software for an OS is kind of crazy and seems unsustainable, but these technologies are just not the answer. When will software devs learn that “computers are fast” is actually a lie used to justify lazy programmers? </pedestal>
It wasn’t just the DEs, either. “Desktop Linux” just generally seemed to get worse for a long time after 2010-ish. And by “worse” I’m not talking about nerdy philosophical holy wars (systemd, etc), but I just mean getting things to work quasi-correctly.
Some two years ago, when I was still going through my inner “fuck it, I’m getting a Mac – holy fuck I’m not spending that kind of money on a computer!!!! – but I should really… – LOOK AT THE PRICE TAG MAN!!” debates, a friend of mine pointed out that, you know, we all look at the past through rose-tinted glasses, lots of things broke all the time way back, too.
A few days afterwards we were digging through my shovelware CDs and, just for shits and giggles, I produced a Slackware 10 CD, which we proceeded to install on an old x86 PC I keep for nostalgia reasons. Slackware 10 shipped with KDE 3.2.3, which was still pretty buggy and not quite the “golden” 3.5 standards yet.
Man, it’s not all rose-tinted glasses, that thing was pretty solid. Two years ago I could still break Plasma desktop just by staring at it mencingly – like, you could fiddle with the widgets on the panel for a bit and have it crash or resize them incorrectly, drag a network-mounted folder to the panel to iconify it and then get it to freeze at login by unplugging the network cable, or get System Settings and/or Kwin to grind to a halt or outright crash just by installing a handful of window decoration themes.
Then again, the tech stack underneath all that has grown tremendously since then. Plasma 5 has the same goals on paper but it takes a lot more work to achieve them than it took back in 2004 or whatever.
I love this anecdote, and I’ve had similar experiences.
I’m a software dev these days, myself, and I’ve always been a free software fan/advocate, so I don’t want to shit on anyone’s hard work–especially when they are mostly doing it for free and releasing it to the world for free. But, I do wonder where things went wrong in the Desktop Linux world.
Is it that the “modern” underlying technologies (wayland, libinput, systemd, auth/security systems, etc) are harder to work with than the older stuff?
Is it that modern hardware is harder to work with (different sleep levels, proprietary driver APIs, etc)?
Is it just that there’s so much MORE of both of the above to support, and therefore the maintenance burden increases monotonically over time?
Or is it just the age-old software problem of trying to include the kitchen sink while never breaking backwards compatibility so that everyone is happy (which usually ends up with nobody happy)?
Again, I appreciate the work the KDE devs do, and I’m really glad that KDE and Plasma exist and that many people use their stuff and are happy with it… But…, I will state my uninformed speculation as a fellow software dev: I suspect that the vast majority of bugs in Plasma today are a direct result of trying to make the desktop too modular and too configurable. The truth is that the desktop pieces generally need to know about each other, so that the desktop can avoid being configured into a bad state, or so that widgets can adapt themselves when something else changes, e.g., containing panel resizes, screen size changes, etc. Obviously Plasma does have mechanisms in place for these things, and I don’t know what those mechanisms are (other than that it probably uses DBUS to publish event messages), so this is just speculation, but I imagine that the system for coordinating changes and alerting all of the different desktop parts is simultaneously more complex and more limited than it would be if the whole desktop were more tightly integrated. I strongly suspect that Plasma architected itself with a kind of traditional, Alan Kay-ish, “object oriented” philosophy: everything is an independent actor that communicate via asynchronous messages, and can be added and removed dynamically at runtime. I’m sure that the idea was to maximize flexibility and extensibility, but I also think that the cost to that approach is more complexity and that it’s basically impossible to figure out what will actually happen in response to a change. Not to mention that most of this stuff is (or was the last time I checked, at least) written in C++, which is not the easiest language to do dynamic stuff in.
I suspect that the vast majority of bugs in Plasma today are a direct result of trying to make the desktop too modular and too configurable.
I hear this a lot but, looking back, I really don’t think it’s the case. KDE 3.x-era was surprisingly close to modern Plasma and KDE Applications releases in terms of features and configurability – not on the same level but also not barebones at all, and was developed by fewer people over a much shorter period of time. A lot of it got rewritten from the ground up – there was a lot of architecture astronautics in the 4.x series, so a couple of Plasma components actually lost some featyres on the way. And this was all happening back when the whole KDE series was a big unhappy bunch of naked C++ – it happened way before QtQuick & co..
IMHO it’s just a symptom of too few eyes looking over code that uses technology developed primarily for other purposes. Back in the early ‘00s there was money to be made in the desktop space, so all the cool kids were writing window managers and whatnot, and there was substantial (by FOSS standards of the age) commercial backing for the development of commercially-viable solutions, paying customers and all. This is no longer the case. Most developers in the current generations are interested in other things, and even the big players in the desktop space are mostly looking elsewhere. Much of the modern Linux tech stack has been developed for things other than desktops, too, so there’s a lot of effort to be duplicated at the desktop end (eh, Wayland?), and modern hardware is itself a lot more complex, so it just takes a lot more effort to do the same things well.
Some of the loss in quality is just inherent to looking the wrong way for inspiration – people in FOSS love to sneer at closed platforms, but they seek to emulate them without much discrimination, including the bad parts (app stores, ineffective UX).
But I think most of it is just the result of too few smart people having to do too much work. FOSS platforms were deliberately written without any care for backwards compatibility, so we can’t even reap the benefits of 20+ years of maintenance and application development the way Windows (and, to some extent, macOS) can.
I hear this a lot but, looking back, I really don’t think it’s the case. KDE 3.x-era was surprisingly close to modern Plasma and KDE Applications releases in terms of features and configurability
It was very configurable, yes. But, I was speaking less from the lens of the user of the product, and more from the software architecture (as I came to understand it from blog posts, etc). I don’t know what the KDE 3.x code was like, but my impression for KDE/Plasma 4+ was that the code architecture was totally reorganized for maximum modularity.
Here’s a small example of what I mean from some KDE 4 documentation page: https://techbase.kde.org/Development/Architecture/KDE4/KParts. This idea of writing the terminal, text editor, etc as modular components that could be embedded into other stuff is an example of that kind of thinking, IMO. It sounds awesome, but there’s always something that ends up either constraining the component’s functionality in order to stay embeddable, or causing the component to not work quite right when embedded into something the author didn’t expect to be embedded in.
Back in the early ‘00s there was money to be made in the desktop space, so all the cool kids were writing window managers and whatnot, and there was substantial (by FOSS standards of the age) commercial backing for the development of commercially-viable solutions, paying customers and all. This is no longer the case.
Is that correct? My understanding was that a good chunk of the GNOME leadership were employed by Red Hat. Is that no longer the case? I don’t know the history of KDE and its stewardship, but if Novell or SUSE were contributing financially to it and now no longer are, I could see how that would hurt the person-power of the project.
Some of the loss in quality is just inherent to looking the wrong way for inspiration – people in FOSS love to sneer at closed platforms, but they seek to emulate them without much discrimination, including the bad parts (app stores, ineffective UX).
I definitely agree with this. That’s actually one reason why I tune out the GNOME Shell haters. It’s not that I don’t have some of my own criticisms about the UI/UX of it, but I really appreciate that they tried something different. Aside: And as someone who has worked on Macs for 10 years, it blows my mind when people say that GNOME Shell is at all mac-like; the workflow and UX has almost nothing in common with macOS except for the app-oriented super-tab switcher.
Here’s a small example of what I mean from some KDE 4 documentation page: https://techbase.kde.org/Development/Architecture/KDE4/KParts. This idea of writing the terminal, text editor, etc as modular components that could be embedded into other stuff is an example of that kind of thinking, IMO.
Uhh… it’s been a while so I don’t remember the details very well but KDE 3 was definitely very modular as well. In fact KParts dates from the 3.x series, not 4.x: https://techbase.kde.org/Development/Architecture/KDE3/KParts . KDE 4.x introduced a whole bunch of new things that, uh, didn’t work out well for a while, like Nepomuk, and changed the desktop shell model pretty radically (IIRC that’s when (what would eventually become) Plasma Shell came up). Some frameworks and applications probably went through some rewrites, some were abandoned, and things like DCOP were buried, but the overall approach to designing reusable frameworks definitely stayed.
Is that correct? My understanding was that a good chunk of the GNOME leadership were employed by Red Hat. Is that no longer the case? I don’t know the history of KDE and its stewardship, but if Novell or SUSE were contributing financially to it and now no longer are, I could see how that would hurt the person-power of the project.
I think Red Hat still employs some Gnome developers. But Canonical no longer has a desktop team IIRC, Ximian is pretty much gone, Nokia isn’t pouring money into desktop/mobile Linux technologies etc.. Pretty much all the big Linux players are mostly working on server-side technologies or embedded deployments.
I definitely agree with this. That’s actually one reason why I tune out the GNOME Shell haters.
I don’t really mind Gnome Shell, Linux always had all sorts of whacky “desktop shell” thingies. However, I really started to hate my Linux boxes starting with GTK3.
I dropped most of the GTK3 applications I was using and got a trip down the memory lane compiling Emacs with the Lucid toolkit. But it wasn’t really avoidable on account of Firefox. That meant I had to deal with its asinine file finding dialog, the touch-sized widgets on a non-touch screen, and that awful font rendering on a daily basis. Not having to deal with that more than justifies the money I spent on my Mac, hell I’d pay twice that money just to never see those barely-readable Adwaita-themed windows again *sigh*.
Uhh… it’s been a while so I don’t remember the details very well but KDE 3 was definitely very modular as well.
Fair enough. I definitely used KDE 3 a bit back in the day, but I don’t remember knowing anything about the development side of it. I could very well be mistaken about KDE 4 being a significant push toward modularity.
Oof, I echo all of this so much.
I was a desktop linux user from about 2003 until 2010 or so, going through a variety of distros (Slackware, Gentoo, Arch) and sticking with Ubuntu since 2006ish.
At one point I got tired of how worse things were getting, specially for laptop users and switched to a Mac. I’ve used macs for my main working computers since then and mostly only used Linux in servers/rPis, etc.
About 3 years back, before the new Arm based macs came out I was a bit fed up with my work computer at the time, an Intel based Mac, being so sluggish, so I decided to try out desktop Linux again (with whatever the current Ubuntu was at the time) on my Windows Desktop PC which is mostly just a gaming PC.
I could replicate my usual workflow, specially because I never depended too much on Mac specific apps, but then even on a desktop machine with two screens, the overall experience was just…not great.
The number one thing that irked me was dealing with my two screens which have different resolutions and different DPIs. The desktop UI for it just literally didn’t work and I had to deal with xrandr commands that ran on desktop start and apparently this is “normal” and everyone accepted this as it being ok. And even then I could never get it exactly right and sometimes it would just mess up to a point that the whole display server would need a restart.
Other than that, the way many of these modern web based desktop apps just have all sorts of issues with different DPIs and font rendering.
I thought, how were all of these things still such a massive issue? Specially the whole screen thing with laptops being the norm over the last 15 years and people often using external screens that probably have a different DPI from their laptop anyway?
Last year I decided to acquire a personal laptop again (for many years I only had work laptops) and I thought I’d have a go at a Framework laptop, and this time I thought I’d start with Kubuntu and KDE, as I’d also briefly tried modern KDE on an Asahi Linux installation and loved it.
KDE seems to handle the whole multiple display/DPI thing a lot better but still not perfectly. The web based desktop app and font rendering issues were somehow still there but not as bad (I did read about some Electron bugs that got fixed in the meanwhile).
And then I dove into the whole Snap/Flatpack thing which I was kind of unfamiliar with as not having used desktop linux for so many years. And what a mess! At multiple points I had multiple instances of Firefox running and it took me a while to understand why. Some apps would open the system Firefox, others would go for the containerized one.
I get why these containerized app ecosystems exist, but in my limited experience with it the interoperability between these apps seems terrible and it makes for a terrible user experience. It feels like a major step back for all the improvements and ease of use Desktop Linux made over the years.
I did also briefly try the latest Ubuntu with Gnome and the whole dual screen DPI situation was just as bad as before, I’m guessing related to the whole fractional scaling thing. Running stuff at 100% was fine but too small, 200% fine but too bug, 150%? A blurry mess. KDE deals fine with all those in between scales.
My other spicy opinion on breakage is that Ubuntu not doing rolling releases holds back everything, because bug fixes take too long to get in front of users.
“I don’t want updates to break things” OK well now every bug fix takes at least 6 months to get released. And is bundled with 10 other ones.
I understand the trade offs being made but imagine a world in which bug fixes show up “immediately”
I understand the trade offs being made but imagine a world in which bug fixes show up “immediately”
… that was the world back in the day.
“Oh, $SHIT is broken. I see the patch landed last night. I’ll just grab the source and rebuild.”
And it still is, depending on the distro (obviously, you can manually compile/manage packages with any distro, but some distros make that an officially supported approach).
I agree that Ubuntu’s release philosophy isn’t great, but in its defense, bug fixes are not blocked from being pushed out as regular updates in between major releases.
What I do think is the big problem with Ubuntu’s releases is that there used to be no real distinction between “system” stuff and “user applications”. It’s one thing to say “Ubuntu version X.Y has bash version A.B, so write your scripts targeting that version.” It’s another to say “Ubuntu version X.Y has Firefox version ABC.” Why the hell wouldn’t “apps” always just be rolling release style? I do understand that the line between a “system thing” and a “user-space thing” is blurry and somewhat arbitrary, but that doesn’t mean that giving up is the right call.
To be fair, I guess that Ubuntu’s push toward “snap” packages for these things does kind of solve the issue, since I think snaps can update independently.
It wasn’t just the DEs, either. “Desktop Linux” just generally seemed to get worse for a long time after 2010-ish.
That’s part of why I landed on StumpWM and stayed. It’s small, simple, works well for my use cases, and hasn’t experienced the sort of churn and CADT rewrites that have plagued others.
Moving to FreeBSD as my daily driver also helped there, because it allowed me to nope out of a lot of the general desktop Linux churn.
I switched to Plasma from GNOME because I was tired of my customizations getting obliterated all the time. I also like the fact I can mess with the key combinations in many more apps, since my muscle memory uses Command, not Control. Combined with a couple add-ons and global menu, I’ve never looked back.
We had a very unfortunate thing happen with desktop environments, with both major DE going through very painful transitions at the same time.
The reasons were entirely clear, but people’s memories are short, and there is a tonne of politics.
Microsoft threatened to sue. Red Hat and Ubuntu *2 bigger GNOME backers) refused to cooperate (including with each other) and built new desktops.
SUSE and Linspire (2 of the biggest KDE backers) cooperated.
I detailed it all here.
https://liam-on-linux.dreamwidth.org/85359.html
Someone stuck it on HN and senior GNOME folk denied everything. I don’t believe them. Of course they deny it, but it was no accident.
This is all a matter of historical record.
These had a different name in the past, Fakes. The concept is important though and more people should be aware of it. I can’t even count the number of times that I debugged problems that weren’t caught by a test because it was testing interactions instead of logic/implementations. The converse is also true. Tests that break after refactors even though no bug was introduced.
Fakes or Nullables as you call them keep your tests from being too flaky and also catch more real breakages.
Came here to post basically the same thing. “Nullables” are basically just Fakes or Stubs.
Though, I can understand why one might want to call it something besides “Fake”, because the spirit of the technique is that the implementation is “real” as far as it can possibly be, and only “nulled” or stubbed at the point of interacting with the outside world.
The other difference with “Nullables”, which I’ve gathered from other posts on the subject, is that the Nullable implementation actually exists in the production code, and can often be one and the same as the production implementation. This as opposed to Mocks/Fakes/Stubs which tend to be written as part of the test code, and therefore are maintained separately from the production implementation code.
Yes, at the lowest level, a Nullable is a production module with an embedded Fake or Stub. (Usually a stub, because they’re easier to write and maintain.) Some people call Nullables “production doubles” in contrast to Mocks/Spies/Fakes/Stubs, which are “test doubles.”
Nullables have two big advantages over traditional Fakes and Stubs. First is that you stub the third-party library, not your own code. Fakes and stubs are usually written as a replacement for your code. The Nullable approach means that your tests are running all the code you wrote, so you catch more errors when things change.
(Fakes are more likely to be written as a replacement for the third-party code, but they’re harder to write than stubs. You have to match a lot of existing behavior.)
The other advantage of Nullables over traditional Fakes and Stubs is the emphasis on encapsulation. Although they’re implemented with an embedded Fake/Stub at the lowest level, the conception of the Nullable is that it’s “production code with an infrastructure off switch.” This applies at every level of the call stack. So your low-level infrastructure wrapper is Nullable, but so is your high-level infrastructure wrapper and your application code. With a Fake/Stub, you typically have to worry about low-level details in tests of your high-level code. With Nullables, you only worry about the direct dependencies of your current unit under test.
This emphasis on encapsulation makes a lot of little things easier. You don’t have to worry about deep dependency injection, usability is better, there’s production use cases, reuse is simple. Implementing Nullables is easy, too, because you’re typically just delegating to lower-level Nullables. The lowest-level Nullables—the ones with the embedded Stub/Fake—are highly reusable.
These benefits do come at a cost—your production code now has a createNull() factories that may not be used in production, and your low-level infrastructure has an embedded fake or stub that may not be used in production. For some people, that’s a deal-breaker, but I think the tradeoffs are worth it.
Nullables have two big advantages over traditional Fakes and Stubs. First is that you stub the third-party library, not your own code. Fakes and stubs are usually written as a replacement for your code. The Nullable approach means that your tests are running all the code you wrote, so you catch more errors when things change.
That’s a good point, and I’m not sure if I realized that this distinction is an explicit part of Nullables or not.
It so happens that I’ve been writing my Stubs like this already for a couple of years now. I haven’t used Mocks in a long time because I believe Mocks almost always test the wrong thing: you should be testing that your code is producing the correct result, not that its implementation happens to call some other code a certain way. I also don’t use Fakes because then how do you know that your Fake is implemented correctly? Do you test your Fake somehow before you use your Fake to test something else?
So, I only use Stubs if I can help it, and I only Stub out code that communicates with the outside world.
With your approach to Nullables, what do you do for interfaces that are supposed to return a value, like an HTTP API? Do you just hard code one response in your Nullable, or do you somehow make your Nullable configurable via the factory functions? When I’m working with APIs like that, I use boring old inversion-of-control (a.k.a. dependency-injection without a “DI framework”) so that I can write a custom Stub for a given test case. The downside is, of course, that the inversion-of-control causes the dependency to propagate through all of the code that uses the interface.
The createNull() factory is configurable, and exists (at varying levels of abstraction) in every intermediate dependency. It decomposes the configuration for the next level down. This prevents the problem of having to propagate a low-level Stub in a high-level test, because the createNull() factories do the propagation for you.
This feels like a straw man. I use C/C++ when I’m talking about properties common to both languages, most often about the abstract machine, for example:
C++ has a lot of nice syntactic sugar over C but, with the exception of exceptions, you can write C code that compiles down to exactly that same machine code as C++. I vastly prefer writing the C++ because it is more concise and does a lot more checks at compile time, but both languages live in the C/C++ abstract machine.
Agreed. Even as someone with definite feelings about the difference between the two (and with the opposite preference to yours!), it seems pretty silly to pretend they don’t have plenty in common that their overlap is a meaningful, useful thing to talk about.
The second paragraph in particular struck me as pretty disingenuous, totally (willfully?) missing the point. C and C++ have enough in common that many programs are compilable (and semantically equivalent) in both languages. None of the other languages mentioned have that property; if you’re bringing FFIs into the mix you’re talking about something very different.
Honestly, I’m struggling to think of any other pair of languages that are more similar – all the other potential candidates I can come up with seem more like dialects than distinct languages (though that’s of course a pretty grey area).
Honestly, I’m struggling to think of any other pair of languages that are more similar
The obvious example would be C and Objective-C, since the latter is a superset of the former (every valid C program is the same valid program in Objective-C), but the fact that I often write [Objective-]C[++], just highlights the point. Perhaps equally importantly, they are typically compiled with the same compilers. For all four languages, clang uses the same parser with a few tweaks to determine which variant it’s using and builds a common AST that can express all four languages.
There are probably some managed languages that have a similar property. For example, I think F# and C# can be mechanically translated to the other (at least, in theory) and can use each other’s data types, but favour different idioms. The only reason that people don’t tend to write C#/F# is that ‘the CLR’ or ‘.NET languages’ typically captures what they mean a bit better.
True, I forgot about Objective-C – though while it does (unlike C++) have the strict-superset relation, IMO what it adds on top of C seems significantly more divergent from C’s basic principles than what C++ adds, so ultimately I guess I still feel like C++ is (handwave handwave) “more similar” as a language.
IMO what it adds on top of C seems significantly more divergent from C’s basic principles than what C++ adds
On the other hand, I feel like there should be some kind of variant of Greenspun’s tenth rule about major C and C++ projects eventually reimplementing a bunch of Objective-C features. (Reference counting, reflection systems, object serialisation, dynamic dispatch registration similar to passing selectors, etc.)
I have some biases here (I still maintain the GNUstep Objective-C runtime), but I think there’s a counter rule in here somewhat, which is that for any sufficiently flexible abstraction, users will try to build systems with a small subset of it. Objective-C’s dynamic dispatch comes with some overhead and, in particular, the reflection everywhere makes dead code elimination impossible and prohibits inlining (well, not quite. I did implement speculative inlining for Objective-C but I don’t think anyone ever enabled it in production). It’s great for things like KVC/KVO and for being able to connect things up for serialisation in Interface Builder, but I care about those things in less than 10% of the objects in a typical program, yet pay for the, everywhere.
This is part of the reason why I’m so interested in structural typing. If you never cast a concrete type to a structural type then you can do static dispatch for it at all call sites and dead-code eliminate a lot of unused bits. Reachability analysis gives you a fairly good overapproximation of the called methods on types that are cast to interface types and lets you do write code that is as flexible as Smalltalk where it needs to be but as efficient to compile as C++ everywhere else.
Oh, don’t get me wrong, I was by no means advocating for Objective-C. I was merely making the observation that Objective-C’s additions to C are not as esoteric and as much of a departure from de facto practical use of C as the GGP poster was suggesting.
I’ve had the misfortune of being tasked to improve perf on a throughput-critical Objective-C code base (client for a particular network protocol in an iOS & Mac app) and after a few easy low-level wins, method call and heap object lifetime management overhead rapidly started dominating profiles. (Unfortunately one of very few engagements that ended in bad blood, as the client wanted big improvements on a shoestring budget and without major architectural changes to the code base. For once, my mistake was not to push for a de-facto rewrite.)
I once came up with a harebrained monomorphising idea for Objective-C method dispatch: at monomorphic-suspected call sites, keep a cached isa/IMP
pair of the most recent target; check the incoming object’s isa against the cache and call straight to the cached IMP on hit. No idea if this would make up for the inflated code size however. (You’d also need a nil check where nonnullness couldn’t be statically proven.) I certainly lack the general compiler hacking and specific clang experience to try it out as a casual project. (The other aspect to this is that I wonder if call sites would be better as a 2-stage IMP lookup and indirect call at each site rather than objc_msgSend()
; but maybe modern branch predictors are smart enough to also look at the return pointer for predicting indirect [tail] calls, to take into account the actual call site rather than the unpredictable objc_msgSend
.)
I once came up with a harebrained monomorphising idea for Objective-C method dispatch: at monomorphic-suspected call sites, keep a cached isa/IMP pair of the most recent target; check the incoming object’s isa against the cache and call straight to the cached IMP on hit.
The GNUstep runtime was designed to support this, with an approach that was simplified in the v2 ABI. There is a global 64-bit counter that is incremented every time a dispatch table is updated (this turns out to be quite rare - there was originally one per dispatch table). This means that you can cache a method as a triple of the class, this counter value and the IMP that was the result from the lookup.
A couple of things make it annoying to do a simple compare. The first is that nil can be an object, the second is that objects may not have an isa pointer, they may have the class encoded in the low 1-3 bits. This means that you need some masking and a branch to tell if this really is an pointer with an isa pointer. This starts to look almost as complex as the fast-path lookup. If you do this early though, you can also add a check that is ‘if the IMP is a pointer to the function I expect, call that’ and then you can inline that call. This lets you inline small methods in a bunch of places. The other place where it seemed to be a pretty big win was for superclass messages, where you know the target class rather than having to look it up, so you’re really just checking the counter and redoing the loopup if it changes (i.e. rarely).
I did write some LLVM transforms to do these optimisations. I hoped in the early days of LLVM that the API (and ABIs) needed for passes would become stable enough that libraries could ship passes that optimised for their specific idioms but this never happened (largely because Google doesn’t understand the concept of code that is not in their monorepo).
The GNUstep runtime was designed to support this, with an approach that was simplified in the v2 ABI. There is a global 64-bit counter that is incremented every time a dispatch table is updated (this turns out to be quite rare - there was originally one per dispatch table). This means that you can cache a method as a triple of the class, this counter value and the IMP that was the result from the lookup
Sounds similar to the approach used by sicl (§16.4), though missing one refinement: in sicl, not only do different versions of the same class have different counters, different classes have different counters too, so you can just dispatch on the counter.
If you do this early though, you can also add a check that is ‘if the IMP is a pointer to the function I expect, call that’ and then you can inline that call.
You can do it late, too, because of the dependency-cutting effect of branches.
The V1 ABI had a counter per slot (method), but in Objective-C method replacement is sufficiently rare that the overhead from checking and maintaining individual counters reduced the number of cache misses by almost nothing relative to a global counter and the global counter significantly simplified the logic.
all the other potential candidates I can come up with seem more like dialects than distinct languages (though that’s of course a pretty grey area)
A language is a dialect with an army and a navy.
Honestly, I’m struggling to think of any other pair of languages that are more similar – all the other potential candidates I can come up with seem more like dialects than distinct languages (though that’s of course a pretty grey area).
Kotlin and Java are kind of like C++ and C to my mind. Kotlin basically has the exact same semantics as Java, just as C++ has the same semantics as C. When I say “semantics”, I’m talking about things like whether function arguments are passed by reference or copy, runtime type information, polymorphism/inheritance resolution, etc.
But, at the same time, Kotlin and Java are definitely different languages in that idiomatic Kotlin reads pretty differently to idiomatic Java: Kotlin having expression-oriented syntax is a big part of that, while top-level functions and variables, and the cleaner syntax around higher-order functions and/or closures being another big part.
At the same time, I would say that Scala and Java are not quite as good an example, because it’s not always quite as obvious what the equivalent Java code would be for given Scala code, whereas I find that Kotlin-to-Java is much more obvious, like figuring out the equivalent C from C++ code.
I’m sure you use C/C++ only to refer to both or common features, but for a lot of people, C/C++ usually just means C++.
I have to admit that I’m a little bit of a Plasma “hater”. I haven’t tried it in three or four years at this point (I remember when I thought keeping the same desktop settings for six months was a long time!).
But, every time I try Plasma, I end up disappointed at how many paper cuts there always are. There were always too many configuration options–and I don’t say that as a “user”, I say that as a developer. They obviously cannot do good QA on all of the different combinations of settings, so there was always something that didn’t work right the minute you deviated from the default configuration. I remember trying to do a vertical panel, but some of the (official, first-party) panel widgets didn’t resize/reorient correctly. I remember the main panel disappearing fairly unpredictably when I’d un/plug my laptop from my two monitors, which would also screw up the hacky super-key-to-open-main-menu feature. Sometimes the KRunner pop up would be in the wrong place if I moved the panel away from its default place at the bottom of the screen. And there were always countless other things. Every time I tried it, I hoped that the experience would be more polished, but it never was over the course of years (I tried every version from 4.0 to 5.10-ish).
Again, the last time I tried was several years ago now, so I guess it’s time to try again with an open mind. But, it’s really hard to not be skeptical/cynical.
One other thing that always bothered the hell out of me was how much it barfed config files all over our XDG_CONFIG_HOME directory. I get that the KDE applications are their own things, but why does every single Plasma desktop component also need its own top-level config directory? I hope they changed that, but I doubt it.
I remember the main panel disappearing fairly unpredictably when I’d un/plug my laptop from my two monitors,
This one got fixed, I definitely used to get that a while back, but now the panel stays put.
One other thing that always bothered the hell out of me was how much it barfed config files all over our XDG_CONFIG_HOME directory.
I think that’s still the case, there’s hodge-podge of a bunch of unintuitively named confit files. What’s more, configs are still a mix of actual configuration and volatile state, so they aren’t easily stored in a git repo. Hope
https://bugs.kde.org/show_bug.cgi?id=444974
is addressed at some point (and saddened that this seemingly didn’t get into plasma 6 roadmap).
There were always too many configuration options–and I don’t say that as a “user”, I say that as a developer.
Entirely agree. The customisations are only what some KDE dev somewhere wants, and the UI on them is terrible.
I want real deep customisation, and I want it by direct interaction not dialog boxes.
If I want to move my panel, and I do, I want to just drag it, not pick an option or try to click on a little target.
I want deep customisation. I want lots of vertical space because everyone has widescreens now. That means a vertical panel but with horizontal contents. That means setting the size of the start button. That means vertical window title bars, like wm2. KDE either can’t do this or buries it down 1/2 dozen levels of dialog boxes.
KDE copied the design of Win98, badly.
I want a simple copy of Win95, without all the Active Desktop garbage.
My prefered way to spell the following
let mut xs: Vec<u32> = Vec::new();
xs.push(1);
xs.push(2);
let xs = xs; // no longer can be changed!
would be
let longer_name = {
let mut xs = Vec::new();
xs.push(1);
xs.push(2);
xs
};
as it makes initialization bit more visible, and also hides away any extraneous temporaries should you need them.
As for the general thoughts on shadowing, I don’t know… I had bugs both ways:
I guess the only reasonable choice is to ban locals (and parameters, while we are at it), as they are too error prone!
I also often use blocks for initialization of a local variable (especially HashMaps).
But, I don’t use the block approach in order to have an “immutable” binding at the end. I only use it for the reasons you cite: it clearly separates the initialization of a local from the rest of the logic of the function, and it keeps temporaries from polluting the outer-local scope.
I would never write the first version (let mut xs = Vec::new(); [...] let xs = xs;
) because I find that to be extra line noise for very little gain. Sure, it might give a slight hint to future readers that xs
isn’t mutated anymore past that line, but it’s truly only a slight hint because you can easily write let mut xs = xs;
later in the function and go back to mutating it. The important thing in Rust is not that you have little-to-no mutation, it’s that you have concurrency-safe mutation, which you have whether xs
is a mut
binding or not.
As for shadowing, I love it, but I only use it for one scenario that I can think of: when you immediately transform an input argument and don’t want to ever refer to the “raw” value again. This happens especially frequently in Rust with things like Into<>
. If you have a function like fn do_stuff<S: Into<String>>(text: S)
, it’s really nice to have the first line of the function be let text = text.into()
. Similar for things like trimming whitespace from input params, etc.
I’ve been writing Rust full-time with a small team for over a year now.
It sounds you have been building an application, rather than a library with semver-guarded API. This explains the differences:
(Personally, I’d am not a fan of using From even in applications, as it makes it harder to grep for origins of errors)
I’ve been building both applications and libraries. And I 100% agree with both of your points wrt. libraries.
But, I don’t think the application / library binary is … well … binary. It’s a spectrum.
In my experience, any application of significant complexity will have internal library-like things. In that kind of situation, protecting their abstractions is vital but a slightly more complex compilation graph less so. (👀 tokio and friends)
Conversely, a library might exist to make hard things easier. One common case is an abstraction that intentionally exposes its internals to eliminate complexity for the callers.
(wrt. From
in applications and grepping for the origins of errors, I blame Rust’s error handling ecosystem’s anemic support for backtraces and locations. We shouldn’t have to grep! I touch on that at the end of the gist. I’ve daydreamed about thiserror supporting a location
field that does something similar to this hack.)
(Personally, I’d am not a fan of using From even in applications, as it makes it harder to grep for origins of errors)
Wow. I thought I was the only one. I’m very skeptical of implementing From for error types. At least as a general pattern.
The main reason I’m skeptical of it is because (obviously) From works based on the type of the value, but not on the context. As a hypothetical example, your code might encounter an std::io::Error during many different operations; some may be a fatal error that calls for a panic, some may be a recoverable/retryable operation, and some may be a business logic error that should be signaled to the caller via a domain-specific error type. When you implement From for a bunch of third-party error types and 99% of your “error handling” is just adding a ?
at the end of every function call, it’s really easy to forget that some errors need to actually be handled or inspected.
in a library, taking a proc macro dependency significantly restructures compilation graph for your consumer, and you, as a library, don’t want to force that.
hmmm I’m not sure what the issue is here. Dependencies have dependencies, I don’t think people find that surprising. For me it’s more of a question of how many dependencies and how does that set of dependencies change my tooling?
A proc macro that only depends on the built-in proc_macro crate is pretty different from a proc macro that depends on syn, quote, and proc_macro2.
proc macro deps are different, for two reasons:
And yes, this mostly doesn’t apply if the proc macro itself is very small and doesn’t depend on parsing Rust code/syn, but thiserror uses syn.
In general, macros that avoid parsing Rust and instead use their own DSL. See eg this bench: https://github.com/matklad/xshell/blob/af75dd49c3bb78887aa29626b1db0b9d0a202946/tests/compile_time.rs#L6
ah. I personally don’t like syn so I get the gripe. I think “avoid proc macros in libraries” is casting a wide net, but “you probably don’t need syn to write that proc macro” is something I very much agree with.
Branded types are extremely powerful for security-critical systems, like finance apps, which require a lot of validations and checks before actions can be performed.
This part made be break into a sweat.
I know it’s not fashionable to be a JavaScript (TypeScript) hater anymore, but while TypeScript’s type system has a lot of interesting power, it also misses extremely basic static soundness and type safety.
In this specific case with so-called branded types and the advice to use type predicate functions, it’s extremely easy to mess up a predicate function for a non-trivial type. For example, imagine you want to use a branded type for a NonEmptyArray type (which I’ve done). The following type + predicate function is wrong. Can you spot why?
declare const NonEmptyArray: unique symbol
type NonEmptyArray<T> = readonly T[] & {
readonly [NonEmptyArray]: unique symbol
}
const isNonEmptyArray = <T>(arr: readonly T[]): arr is NonEmptyArray<T> => arr.length > 0
The reason it’s wrong is because any mutable array that happens to be non-empty at the moment you call isNonEmptyArray
will pass the test and the compiler will treat is as a non-empty array even though it could become empty. Here’s an example:
declare function useNonEmptyArray(arr: NonEmptyArray<number>)
const x = [1] // Not empty
if (isNonEmptyArray(x)) {
x.pop() // x is empty now
// Compiler still thinks x is a NonEmptyArray
useNonEmptyArray(x) // Compiler doesn't complain
}
And this is ignoring that your type predicate could just be wrong by forgetting to recursively test the types of an object type’s fields.
I used branded types extensively in my TypeScript code, but TypeScript honestly provides so little type safety that we need to be really careful to not let it lull us into a false sense of security.
I’m using a Rust backend web framework (actix-web), and I really don’t care about the speed because … it’s always the database. What I do like is strong type safety end to end, so I get compile time checking of SQL queries (in fact that shows up in the lsp output as I type!) and templates (IDE integration isn’t as good, but still), which means by the time I’ve compiled there’s whole categories of bugs that don’t exist.
Downsides: lack of libraries means I had to write some infrastructure myself.
I’m using a Rust backend web framework (actix-web), and I really don’t care about the speed because … it’s always the database.
Not always. I’ve worked on a couple of simple CRUDy apps where a significant amount of time was spent in JavaScript-land processing and validating incoming data, and then ORMifying it. In one particular case, I was able to increase the request-response cycle on the local network by several times by simply replacing the JS/Node endpoints with ones written in Rust. I initially just did it to see how much Rust web frameworks had matured and whether I’d like it, but when the Rust code actually improved the speed enough that it made a difference to the human users, we migrated the services to Rust for real.
it’s always the database
… or, rather, the ORM, which trades constructing giant blobs of SQL in your code for terrible performance with any but the most trivial schema. Unless you’re willing to create ORM code which is at least as complex as the resulting SQL in order to bend it towards performance.
Hear hear! I can’t believe that ORMs are still so popular. Granted, so-called “query builders” have been gaining in popularity over the last several years. But, there’s not nearly enough ORM hate, especially considering how much OOP hate there is- do people not realize they are spiritual siblings? I suspect we’ll collectively be laughing at ORMs in the next decade the way we are laughing at BeanFactoryAdapterVisitorFactory OOP today.
ORMs are the worst abstraction. It takes the parts of SQL that are already simple and makes them super-duper-trivial, while making the challenging parts of SQL even more challenging and error-prone. It’s the worst trade-off. And it’s a terribly leaky abstraction even in the good cases.
Coming from Ruby I’m laughing all the way to the bank every time someone scareposts about minimizing heap allocations. If Ruby is fast enough for the web (and I believe it is) Rust is stupid fast enough.
I don’t know who the quote is attributed to, but I remember reading once that “there is not such thing as fast code, just fast enough code.”
However, when it comes to “fast enough” for a web server, it obviously depends a whole lot on what we mean by “fast enough”. If “fast enough” means that it responds to the user within X number of milliseconds, then yeah, Ruby or Python or anything else is fine. BUT, what if meeting that goal requires your fancy AWS load balancer to spin up instances and scale out sooner and more frequently than some other language/stack? Does “fast enough” include consideration for our AWS bill? Of course there are competing tensions there, too. Maybe the AWS bill difference is less than the extra cost (salary) of waiting for Rust to compile all day while you work.
I’ve made a few side projects in rust now and I decided to give it a whirl with a recent web project:
What I disliked:
* Compilation/reloading.
* Traits as API is an extra cognitive tax with libraries.
* Stringifying types to make template work undoes type safety.
* Strings.
* Weirdly I found rust's error messages/suggestions to be frustrating.
What I liked:
* Type safety.
* Just copy a binary and some files to deploy. So nice.
At the end of the day I switched to sigh rails (queue “oF CoUrSe RUst iS WRoNg FOr THAT” replies). I just didn’t have enough time to make rust work for this particular project.
What I really want is for someone to write a nice framework for Terra. I feel like it hits the sweet spot for a language that would work really well for the web:
* small
* types
* very simple syntax (mostly Lua)
* great C interop
* static and dynamic (jit) compilation
* repl
* compile time code (roll your own generics, checks, etc)
* speed
It doesn’t give you anything that rust does but just throwing it out there to someone who might have more time on their hands.
I’m really enjoying compiling Go to C++ lately (https://github.com/nikki93/gx). Getting the benefits of Go tooling and package system, along with C/C++ interop and portability. Also metaprogramming because hacking on the compiler is easy (it’s about ~1800 lines atm written over Go’s builtin parser and typechecker). It also outputs GLSL because I’m using it for gamedev. Compile times are about one second (both the Go and C++ pass) on my game project.
Def kind of just a personal tool and not setting it up for wide use at the moment, but maybe it would give some ideas / inspire for doing something similar. Go is a great starting point I’ve found.
Very cool! I watched your video at the bottom. The edit-compile-run cycle was quite reasonable, and I particularly liked how easy it was to expose component fields to the editor via tags - that was a nice QoL touch.
I’m not sure if you’ve already explored it, but one suggestion that might help with debugging C++ compiler failures (and any C/C++ runtime assertions that use __FILE__
/__LINE__
), is to use the #line directive in the generated code.
Thanks for checking it out! And yeah #line
would be useful indeed. Probably a generalization of that is to get sourcemaps in some form so that it works in debuggers too. So far I’ve been fine with looking at the generated C++ because it generates it in formatted / readable form. You can see an example here, with the input code further down below – https://gist.github.com/nikki93/458852c50cd4822f2c9935ce0d41a2bc#file-example-gx-cc-L583.
I’ve been keeping an eye on Terra for many years now, it’s one of the most exciting technologies out there for me along the system-level/high performance programming axis. I wish it were a Terra-like language and not Rust that captured all the C++ refugees.
That said, the last time I played with it (a couple of years ago), it felt very experimental. I kept getting segfaults while trying to run examples from the docs etc. To be fair, I was trying to play with its first-class jit features, but still…
I’d love to see it get some traction and its rough edges smoothed out.
It’s a small project and it finally hit 1.0 in June. There are some more popular languages that get close to it but ultimately what I’d like is something much simpler which Terra really nails.
I wish it were a Terra-like language and not Rust that captured all the C++ refugees.
From my experience this seems like the group most actively against using rust. Maybe it’s just gamedevs?
What makes terra good versus other jinja-like template engines? I’ve used it and liked it but have little else to compare to in the rust ecosystem.
I share your frustrations. I absolutely adore Rust, but the web frameworks in particular are painful.
As far as your dislikes, compilation and Rust’s string types/APIs are things that are unfortunate and can’t really be helped, considering Rust’s design and use-case goals.
But, I tend to agree that Rust devs often times lean too heavily into traits and just having tons and tons of types where it’s not always worth the added cognitive burden. There has to be a decent way to write a low-overhead web framework in/for Rust that isn’t so damn painful to e.g., write a middleware handler for, etc.
I am tired of Rust users claiming their stuff is fast without providing any benchmarks comparing their tools/frameworks with the best-performing alternatives. The first version of https://this-week-in-neovim.org/latest was written in one of these Rust-in-the-browser frameworks (Yew, I think?) and was painfully slow (minutes to load on my computer). Having magic Rust dust in your software does not make it automatically fast.
There’s a long tradition of claiming to be fast at handling HTTP requests, and I think most people have known for decades that the implementation language is rarely the bottleneck, compared to the data model / database powering it.
I think a more interesting question is what the recurring pattern (optimizing for nonsensical benchmarks) says about our industry.
I don’t know. We had a service in Python that was taking a protobuf request with an id, then joining 4-5 tables in mysql and sending the result back as a protobuf.
Everyone thought mysql was the bottleneck, but it turned out to be cpu because (de)serializing protobuf is slow.
Historically the Python protobuf libraries are unfortunately slow, mainly because C++ and other statically typed languages are the main use case at google. They just get more attention
HTTP parsing is pretty much solved in Python, but you can definitely run into very slow libraries here and there
Protobuf de-/serializing was slow? I mean, it’s Python, so it’s conceivable, but do you have any code/numbers to back that up?
No, not anymore. It was a year ago and when we realized, I just rewrote it in Rust and that was that (I just checked in on it. It’s serving 30-40k req/min and hasn’t been touched for months. No err logs the last month either).
I think @andyc is correct though. Parsing protobuf in Python is fairly slow because of lack of love. I forget what we use for it, but looking at the generated Python code I always get a feeling that it’s a bit much. The generated Rust I can read and understand. The generated Python is like 10x and completely impossible to parse for me (granted, Python is not my first language). Certainly not just a class with some typed fields as I would’ve liked.
I strongly agree.
Another thing this might cause is caring a lot of application side scalability (Cloud/Kubernetes/…), when the bottleneck tends to be the database or more precisely how it is used.
I echo this sentiment from a different angle: fast to run is equally important as fast to write. I can throw together a webapp with React because the tooling is top notch and everyone uses it.
Whenever I try one of these Rust libraries that’s ridden with macros there’s virtually zero feedback loop because the macros are just… macros. Sure, I can write some HTML-like tree structure into the macro and make it feel like React but if I make a typo or pass the wrong type and that’s not immediately obvious, that feedback loop will compound over the days, weeks or months I spend building a product and suddenly the value proposition isn’t as attractive and I’d rather spend a few more CPU cycles than a few hundred more human-hours that I could otherwise use doing fun things away from a computer screen.
fast to run is equally important as fast to write.
To me that Depends.
If it’s just for myself, maybe. If I’m going to subject many people to it, many times, then absolutely not.
Like everything in the software world, “it depends.”
I wish I wouldn’t sound like a raving, unobjective, fanboy when I say this, but it’s 100% true: The code I’ve written in Rust tends to have far fewer defects than code I’ve written in other languages, including PHP, Kotlin, Java, JavaScript, TypeScript, and a few others. The amount of time I spend on bug-fixes on my Rust projects is at least an order of magnitude less than the other projects of similar size/scope. Rust saves me human-hours overall– without a doubt.
Granted, writing async Rust with these frameworks is way more painful than it feels like it should be. I’ll give you that. In fact, every time I want to sit down and write a “middleware” for an Actix-Web project, I end up wanting to pull my hair out. And I also loathe macros 99% of the time.
Everyone is different, of course, and we have different expertise in different languages and frameworks. But, I’m slow in every language–there’s no way I’m launching anything from scratch in an afternoon or two no matter the language or framework. Some people swear up and down that they can get a “prototype” up in a day or two, and I don’t know if I can honestly believe them because I can’t even usually finish naming my project, setting up a repository, and configuring my linter in a day…
Of course it depends, there’s a lot to consider and it’s likely impossible to meaningfully quantify.
That initial chunk of time required to get a project going likely pays off in the long run. But there’s also things like hiring and diagnostics to consider, if I can’t find any competent Rust engineers to bring on to a fast-growing product then that’s a real big problem.
That being said I am bullish, I think it’ll get there, async Rust will get easier to write and there will be more people in the job market. But in its current state, I wouldn’t pick it for anything other than a side project right now unless it was something that was really the bread and butter of what Rust is great at: low level systems programming (which it’s very good at)
WASM tax is real. As of now the same code in JavaScript and WASM, JS will win. Also a fast lang can’t escape a poor paradigm.
I listened to Rustaceans station podcast about Leptos and he talked a bit about why the approach used by yew and others is slower as it fundamentally has to do much more for each change in state.
Sorry, but it’s silly to call something a “unit test” which “only dependencies being the operating system”.
I define a unit test for functions only where the same input always provides the same output with no side effects (“pure” functions). If you need to mock something (file, os, I/O, whatever), or a test that has side effects, that’s an integration test (it integrates with the system/storage/network/whatever).
Why is this important? The naming is not that important, but if you think about this way, maybe you can realize how important is it to split code and abstractions on I/O boundaries, so you can test main logic without ever touching disk or network.
I’ve never seen this definition of “unit test”, and it seems unnecessarily restrictive. What about a very simple class that holds a single state variable which is manipulated through a getter and setter? Most people would definitely call it a unit test when you’re verifying that the getter can read what the setter set.
Also, AFAIK all web frameworks call ORM model tests “unit tests”, even though they go through a database. It’s about the smallest unit you can meaningfully test in a web framework.
I define a unit test for functions only where the same input always provides the same output with no side effects (“pure” functions). If you need to mock something (file, os, I/O, whatever), or a test that has side effects, that’s an integration test (it integrates with the system/storage/network/whatever).
That’s probably approximately what “unit test” means in common parlance today, but that’s not the original definition of the term, as far as I know. To the best of my knowledge, a “unit” originally was just a “unit of functionality” in a piece of software. So, if you had a piece of software that had an “export to CSV” button, a unit test might provide some hand-made data to the export_to_csv function and see that it actually writes a valid csv file to disk with the data represented correctly.
Not that I’m arguing for one definition over the other, just that I think your opening statement of calling one definition “silly” is probably misguided, especially considering that I think you’re calling the original definition silly.
Do you have any links for the original definition? Couple of days ago I looked at the docs for JUnit 1.0, but they didn’t define the unit.
Nothing authoritative, unfortunately. It’s just something I’ve picked up over the years, which is why I used such uncertain language in my comment (“To the best of my knowledge”, etc). It’s mostly just something I picked up from people who had been programming much longer than me.
However, I do have a fairly old post from Martin Fowler in my bookmarks that discusses object mocking in the context of Test Driven Development and the two “schools” of TDD: London and Classical. The post doesn’t directly set out to define “unit test”, but his explanations and examples kind of hint at the idea that a unit test isn’t necessarily a single-class or single-function affair, and the post is from 2007: https://martinfowler.com/articles/mocksArentStubs.html. See, for example, the section titled “Classical and Mockist Testing.”
I find this distinction a bit confusing since there’s lots of cross cutting concerns which may be io boundaries, but also seem pure. For example anything that includes logging. In an extreme case add(a,b,logger) { logger.log("adding"); a+b }
- I’d argue a test for this is not an integration test, but it may be just one frame away from invoking io and requires effectively mocking the logger.
What a great writeup. Here’s why it hits the nail on the head. TDD on its own is great. It gets people talking about the design of their code. It gets people caring about correctness. But the “TDD maximalist” position is unhelpful, arrogant, annoying, and flat out incorrect.
Uncle Bob’s “TDD is double entry bookkeeping” doesn’t make any sense. The reason double entry bookkeeping works is because accounting is zero-sum. Money must move between accounts, because it’s a finite resource. If you write a test, code can be written to pass it in an infinite number of ways. Computation is not a finite resource. The analogy is not good.
The TDD maximalist position that “TDD improves design because the design ends up more testable that way” is the definition of circular reasoning. I loved the point about long functions sometimes being the better abstraction - this is absolutely true in my experience. Artificially separating a concept that’s just inherently complex doesn’t improve anything objectively, it just gives someone with an obsession over small functions an endorphin hit. The same is true of dependency injection and other TDD-inspired design patterns.
I know tons of people who have done TDD for years and years and years. As mentioned in the article, it has a lot of great benefits, and I’ll always use it to some degree. But in those years working with these other people, we have still always had bug reports. The TDD maximalist position says we were just doing it wrong.
Well, if a methodology requires walking a tightrope for 5 years to get the return on investment, maybe it’s not a perfect methodology?
I don’t think this is right: while Freud has largely been discarded in the details, his analytical approach was utterly new and created much of modernity. Uncle Bob … well, the less said, the better.
While I generally agree with your take, I’m going to quibble with part of it:
The TDD maximalist position that “TDD improves design because the design ends up more testable that way” is the definition of circular reasoning.
This is not circular reasoning, though the inexact phrasing probably contributes to that perception. A more exact phrasing may help make it clear that this isn’t circular:
Put this way, this may be less controversial: it allows room for both those who subscribe to TDD, and for those who object to it. (Possible objections include “all else can’t be equal” – I think DHH’s comments on Test-induced Design Damage fall in this category – or that other practices can be as good or better at generating comprehensive tests, like TFA’s discussion of property-based testing.)
- All else equal (and I’m not sure what that might mean when discussing design), a more testable design can be considered a better design.
This is why the reasoning is circular. This point right here can be debated, but it’s presented as an axiom.
- TDD is a practice which generates comprehensive tests, and which rejects designs that cannot be comprehensively tested.
TDD does nothing inherently, this is another false claim. It does not reject any design. In practice, programmers just continuously copy the same test setup code over and over and do not care about the signal that the tests are giving them. Because TDD does not force them to. It happily admits terrible designs.
It does reject designs: designs that cannot be tested cannot be generated when following strict TDD, and designs which are difficult to test are discouraged by the practice. (Edit to add: In practice, this requires developers to listen to their tests, to make sure they’re “natural” to work with. When this doesn’t happen, sure, blindly following TDD probably does more harm than good.)
Sure, there’s some design out there that won’t even support an end to end test. The “naturalness” of a test though can’t be measured, so the path of least resistance is to just make your tests closer and closer to end to end tests. TDD itself will not prevent that natural slippage, it’s only prevented by developer intervention.
That’s the TDD maximalist point of view is a nutshell - if you don’t get the alleged benefits of it, you did it wrong.
For what it’s worth, I’m not a TDD maximalist, I try to be more pragmatic. I’m trying to have this come across in some of the language I’m using: non-absolute terms, like “encourage” or “discourage”, instead of absolutes like “allow” or “deny”. If you’re working with a code base that purports to follow TDD, and you’re letting your unit tests drift into end-to-ends by another name, then I’d argue that you aren’t listening to your tests (this, by the way, is the sense in which I think “naturalness” can be measured – informally, as a feeling about the code), and that (I’m sorry to say, but this does sometimes happen) you’re doing it wrong.
Doing it right doesn’t necessarily mean using more TDD (though it might!), it’s just as easy to imagine that you’re working in a domain where property-based tests are a better fit (“more natural”) than the style of tests generated via TDD, or that there are contextual constraints (poorly-fitting application framework, or project management that won’t allow time to refactor) that prevent TDD from working well.
I’m mostly with you. I’m definitely what Hillel refers to in this article as a “test-first” person, meaning I use tests to drive my work, but I don’t do strict TDD. The way that people shut their brains off when talking about TDD kills me though.
It does reject designs: designs that cannot be tested cannot be generated when following strict TDD, and designs which are difficult to test are discouraged by the practice.
Well, yeah, that’s why “maximalist TDD” has such a mixed reputation…
First, there are plenty of cases where you’re stuck with various aspects of a design. For example, if you’re writing a driver for a device connected over a shared bus, you can’t really wish the bus out of existence, so you’re stuck with timing bugs, race conditions and whatever. In these cases, as is often the case with metrics, the circular thing happens in reverse: you don’t get a substantially more testable design (you literally can’t – the design is fixed), developers just write tests for the parts that are easily tested.
Second, there are many other things that determine whether a design is appropriate for a problem or not, besides how easy it is to implement it by incrementally passing automatic unit tests written before the code itself. Many of them outweigh this metric, too – a program’s usefulness is rarely correlated in any way to how easy it is to test it. If you stick to things that The Practice considers appropriate, you miss out on writing a lot of quality software that’s neither bad nor useless, just philosophically unfit.
For example, if you’re writing a driver for a device connected over a shared bus, you can’t really wish the bus out of existence, so you’re stuck with timing bugs, race conditions and whatever. In these cases, as is often the case with metrics, the circular thing happens in reverse: you don’t get a substantially more testable design (you literally can’t – the design is fixed), developers just write tests for the parts that are easily tested.
I’ve written device drivers and firmware code that involved shared buses, and part of the approach involved creating a HAL for which I could sub out a simulated implementation, allowing our team to validate a great deal of the logic using tests. I used an actor-model approach for as much of the code as possible, to make race conditions easier to characterize and test explicitly (“what happens if we get a shutdown request while a transaction is in progress?”). Some things can’t be tested perfectly – for example we had hardware bugs that we couldn’t easily mimic in the simulated environment – but the proportion of what we could test was kept high, and the result was one of the most reliable and maintainable systems I’ve ever worked on.
I’m not trying to advocate against DFT, that’d be ridiculous. What I’m pointing out is that, unless DFT is specifically a goal at every level of the design process – in these cases, hardware and software – mandating TDD in software without any deliberation higher up the design chain tends to skew the testability metric. Instead of coming up with a testable design from the ground up, developers come up with one that satisfies other constraints, and then just write the tests that are straightforward to write.
Avoiding that is one of the reasons why there’s so much money and research being poured into (hardware) simulators. Basic hiccups can be reproduced with pretty generic RTL models (oftentimes you don’t even need device-specific logic at the other end), or even just in software. But the kind of scenarios that you can’t even contemplate during reviews – the ones that depend on timing constraints, or specific behavior from other devices on the bus – need a more detailed model. Without one, you wind up not writing tests for the parts that would benefit the most from testing. And it’s what you often end up with, because it’s hard to come up with reliable test models for COTS parts (some manufacturers do publish these, but most don’t). That’s not to say the tests you still get to write are useless, but it doesn’t have much of an impact over how testable the design is, nor that there aren’t cheaper and more efficient ways to attain the same kind of correctness.
My experience with TDD development in this field has been mixed. Design teams that emphasize testability throughout their process, in all departments, not just software, tend to benefit from it, especially as it fits right in with how some of the hardware is made. But I’ve also seen TDD-ed designs with supposedly comprehensive tests that crashed if you so much as stared at the test board menacingly.
This is why the reasoning is circular. This point right here can be debated, but it’s presented as an axiom.
The way they phrased that does make it kind of circular, but we can phrase it differently to avoid being circular:
Therefore, designs that lend to better test coverage are more likely to be correct than designs that don’t.
End to end testing has more coverage per test case though. So, by this logic, you would just write only end to end tests, which is in contrast to the isolated unit testing philosophy.
So, by this logic, you would just write only end to end tests, which is in contrast to the isolated unit testing philosophy.
I’m not sure that actually is in opposition to TDD philosophy, though. My understanding of TDD is that it’s a pretty “top-down” approach, so I imagine you would start with a test that is akin to e2e/integration style before starting work on a feature. A “fundamentalist” would leave just about all of the implementation code “private” and not unit test those individual private functions unless they became part of the exposed public API of the software. A more pragmatic practitioner might still write unit tests for functions that are non-trivial even if they are not public, but I still think that TDD would encourage us to shy away from lots of unit tests and the “traditional” test pyramid.
I could be totally off base with my understanding of TDD, but that’s what I’ve come away with from reading blogs and essays from TDD advocates.
End to end tests can have difficulty with “testability”, IMO:
Edit to add: This is probably giving the wrong impression. I like E2E tests, whole system behavior is important to preserve, or to understand when it changes, and E2E’s are a good way of getting to that outcome. But more narrowly targeted tests have their own benefits, which follow from the fact that they’re narrowly targeted.
I was going to object along similar lines - but I agree with the sibling - your 1) does not make a good argument.
I’d say more something like: A testable design is better, because it allows for writing tests for bugs, helping document fixes and prevent regressions. Tests can also help document the business logic, and can improve the overall system that way too.
Just saying tests are good; let’s have more tests! - Doesn’t say why tests are good - and we do indeed get circular reasoning.
I’ve inherited a number of legacy systems with no tests - and in the few I’ve shoe-horned in tests with new features and bug fixes - those have always cought some regressions later. And even with the “wasted” hours wrangling tests to work at all in an old code base; I’d say tests have “paid their way”.
If those code bases had had tests to begin with, I think a) the overall designs would probably have been better, but perhaps more importantly b) writing tests while fixing bugs would have been easier.
I also think that for many systems, just having unit tests will help a great deal with adding integration tests - because some of the tooling and requirements for passing in state/environment (as function arguments, mocks, stubs etc) is similar.
I do consider point 1 nearly axiomatic. What does “testable” mean? Trivially, it means something like “easy to test”, which suggests properties like:
Achieving these requires that the interface under test be pretty easy to understand. All else equal (whatever that might mean when talking about design), having these qualities is better than not having them. (Note, also, that I talk more about “testability”, not “tests”: I value testability more than I value tests, though TDD does strongly encourage testability.)
Still, I don’t believe this is circular. Point 1 is presented axiomatically, but the remaining points that attempt to characterize the meaning only depend on earlier points, without cycles.
The first part: about changing the “shape” of the object messing up the optimization of functions being called with similar arguments, kind of galvanizes something I’ve been arguing for the last year or two.
(This is slightly off-topic and really has almost nothing to do with Elm, itself)
To be clear, I’m not saying that this is “proof” that my opinion is right, or that this kind of problem is unsolvable, etc. I’m merely claiming that I came to have an opinion (which I’ll express in a second), and that this information seems to be consistent with that opinion.
I’ve been saying for a while now that trying to transplant functional programming styles and techniques into languages that are not designed for functional programming is a mistake. Especially ones with “heavy” runtimes that do runtime optimizations/JITing, etc.
JavaScript’s design is clearly expecting programmers to mutate data in-place. That’s the reason that the standard array and map types’ APIs have mutable APIs, and it’s the reason there hasn’t been a way to “deep copy” things until just recently (or in the near future?). It’s why “records” and “tuples” are just now being added, etc.
As someone who fell deep into the FP rabbit hole, I love functional programming languages. I like Scala, OCaml, even Clojure despite my strong preference for static types. But, FP is not the only programming style that can offer correct/safe code, despite the hype.
My advice is to go with the flow. Sure, minimizing mutation is naturally going to make your code easier to understand, but minimizing mutation by taking a copy of something and never using the old copy ever again is effectively the same thing as mutating it when it comes to a human trying to understand the interaction of the system. In fact, your inner mental model as you’re skimming the code is probably thinking something like: “Okay, then we take the Foo object and update its bar field…”
There’s also a contradiction I see in the online programming communities. I see people (rightly) complain about how slow and bloated software has become; whether it’s web-apps or even local apps, yet I also see people repeat the advice that computers are fast and it doesn’t matter if you make umpteen copies of an array in your function by calling arr.map().filter().reduce()
over and over. I’m not sure we can reconcile those two viewpoints without some mental gymnastics (e.g., “It’s not the code, it’s just that the software is doing more stuff.”).
Now, after rambling about all that, I want to add that I think my “advice” here doesn’t apply (as much) to people who are writing programming languages, like Elm. It’s more about people writing libraries and applications in a non-functional language trying to force it to be functional. But, it does apply a little bit to Elm, too, because it’s always going to be an uphill battle to write a language to target a runtime that somewhat hates your language’s programming paradigm…
I see people (rightly) complain about how slow and bloated software has become
I’d say it’s mostly dependent on who says it. The developers who have to optimize their code will say that it’s fine to make copies of things (until they measure it and find out it’s making the app slow), and the rest who just notice that the app is slow.
it’s always going to be an uphill battle to write a language to target a runtime that somewhat hates your language’s programming paradigm
At very low levels (machine code) the paradigm is very imperative and non-functional. Since languages always build to lower-level languages (in some sense, not saying JS is lower-level than Elm), meaning that there will necessarily be a level where the paradigms don’t .
JavaScript is actually not such a bad target, because it allows passing functions as arguments and things like that which can be harder in some other targets. In fact, you can kind of see Elm as a subset of JavaScript, so there’s not really much that is too hard to translate. And the performance is usually very good for a JavaScript app (because the language already optimizes plenty of things, including the keys that I mentioned). It’s just that because we always want the language to be faster than it is today that we try to find new optimizations, and they’re hard to benchmark.
I’d say it’s mostly dependent on who says it. The developers who have to optimize their code will say that it’s fine to make copies of things (until they measure it and find out it’s making the app slow), and the rest who just notice that the app is slow.
I agree and disagree. I agree because a lot of times the people who are commenting on the perceived speed or memory consumption of a piece of software don’t know enough to be informed about whether it’s good or bad engineering that went into making that way.
On the other hand, I think it’s also valid for even a non-tech-savvy individual to sit down at a computer today and wonder why it takes longer to load the god-damned calculator app than it took in the 90s on their Pentium II PC.
Obviously there’s more to it than making extra copies of objects and arrays in whatever programming language- that was just an obvious/simple example I chose to pick on. But, as a developer myself, I can’t help but notice in my own work that there is an attitude that there’s almost no limit to how algorithmically poor your code can be, because “Computers are fast and IO dwarfs everything,” (which fails to account for the fact that many code paths have very few IO calls and potentially very many allocations and GC garbage).
As a specific example, I challenge anyone here to find a typical Java project that uses Hibernate to talk to a SQL database, and compare that to a version where you take out Hibernate completely and just shoot raw query strings and use the (horrible) ResultSet API to extract the data returned from the DB call. Again, this is just a random concrete example, but I would bet real currency that the speed up will be statistically significant, even including the time for the IO.
Admittedly, this has nothing to do with the original topic. But my point here is that we, as developers, keep doing things that are slow and then there’s a contingency of us that are gaslighting us into believing that what we’re doing isn’t actually making our software slower.
Also, benchmarking/measuring isn’t a silver bullet. If all of your code is 10% slower than it should be, then your measurements won’t find any bottlenecks because it’s all relative.
At very low levels (machine code) the paradigm is very imperative and non-functional. Since languages always build to lower-level languages (in some sense, not saying JS is lower-level than Elm), meaning that there will necessarily be a level where the paradigms don’t .
Definitely true. But it’s also true that adding more layers means more “friction”. So we already have the machine code layer and we’ll lose some maximum performance by translating a functional language to machine code. But then we have a virtual machine on top of the machine code, so that VM loses some performance, but has its own stuff that it optimizes for. Then we have Elm which targets the VM which target the machine code, so there will be losses at every level.
The/your article points out another example of it: the addition operator. Since JS doesn’t expose type-specific addition functions, every time we add two numbers in Elm, the runtime is checking if our numbers are Strings or BigIntegers or whatever else, even though we know for sure that they’re numbers, already. That’s the cost of targeting a runtime that’s designed for more-or-less the opposite of what you’re trying to do. I’m not saying that you shouldn’t do it. Goodness knows I rather never have to write actual JavaScript ever again if I can help it…
Yes, very much this. The way I like to conceptualize this is “test features, not code”. Another good one is “test at the system boundary”: https://www.tedinski.com/2019/03/19/testing-at-the-boundaries.html.
One neat tactical trick here is that, if you want to unit test something deep in the guts of the system to exercise all corner cases which are hard to reach from the outside, you can create “it, that won’t change” yourself. Rather than writing each test directly against the API of the internal component, write a check
driver function, which takes some data structure as an input, feeds it to the component, and then checks against expected results. Each test then just calls this check with specific inputs.
As a results, tests are shielded from components API changes. The API has only two usages: one production, and one in the check function. This is in contrast to typical situation, where you have one prod and ten test usages.
Can you elaborate on your check
function approach? I’m having trouble imagining how that changes anything about testing an internal component.
If the API of your internal component changes, don’t you have to change the way you call this check
function? Either the input data structure or the expected result values?
See an illustrative example here: https://matklad.github.io/2021/05/31/how-to-test.html#test-driven-design-ossification.
And here are test for tree-diffing functionality in rust-analyzer as a real-world example:
Note how all tests just call check-diff
The example in the blog post seems very similar to https://github.com/golang/go/wiki/TableDrivenTests
https://matklad.github.io/2021/05/31/how-to-test.html#test-driven-design-ossification.
This is great - I think I actually read this blog a while ago but couldn’t find it in my history to put in the further reading section, which explains how mine ended up starting with a similar anecdote about testing as a junior dev 😂.
Ah! Thank you. That clarifies it. So, this check function approach is more of a “buffer” against API changes- it’s going to reduce the tedium of mechanically updating many tests after somewhat-trivial API signature changes.
That’s clever. I assume that you must still use this approach, since you just posted here about it. Have you developed any kind of “rules of thumb” in your experience for when this approach works best or might not be worth it (e.g., “I know that any change to this signature will mean I need to actually rethink all my tests anyway”)?
That’s just default approach I use. The driving rule of thumb is perhaps “the number of usages of an API in tests should not exceed the number of usages in prod”.
If I expect “I need to rethink all my tests anyway” situation, I add expectation testing into the mix: https://matklad.github.io/2021/05/31/how-to-test.html#expect-tests
It’s interesting what’s being included under the umbrella of “Typescript” in this post. As someone who’s been writing JS for the better part of 20 years, it’s generally been clear to me what’s JS, what’s Node vs browser, and how Typescript is a layer over JS. But if I was coming in today and trying to learn Typescript I would totally conflate all these things, too.
Some concrete examples:
The ecosystem is also pretty impressive. With over 1.3M npm packages available.
I would bet that the vast majority of the packages on npm are pure JS with no Typescript support. Factor in popularity and you’ll see a lot more TS, but npm is a Javascript package repo, it predates Typescript by several years. It’s where you get Typescript-supporting packages from, but it’s not of Typescript.
Typescript is also very async-friendly, and everything is single threaded.
Javascript is also very async-friendly …
But if I had to choose the thing I disliked the most… that would be the module system
100% agree, and this is a place where adding in Typescript does makes things more confusing. It is massively frustrating when you run into module issues, and the sooner the ecosystem can get on the same page, the better. Though I suspect this pain will stick around for years. But the module issue also isn’t really of Typescript.
I accept that these are probably all pointless distinctions to someone who’s trying to write some Typescript and deploy their code!
Aside, since the author is here: I’m not sure if the bit about checking for a successful request is just an example for the blog post, but in case it’s not: Rather than checking the status code, from fetch
you probably have a Response object, so you likely want the Response.ok property
I’ve noticed this as well, similarly having used JS for 20+ years. It’s actually somewhat frustrating to me to hear people argue for TS using JS features because it shows a fundamental misunderstanding or lack of knowledge of the ecosystem and language itself.
Not that this blog is arguing these points, but seeing TypeScript thrown around where it’s really just JavaScript is still difficult for me to see. JS is demonized while people sing the praises of TS, but quite frankly, if you could only use JS, you wouldn’t really notice that much of a difference.
Yeah. I am actually aware of some of those distinctions. But you captured my point correctly! As a newcomer, as much as I am aware of those distinctions now, the post documents my journey learning through all of that, so I included some things even though today I know better (And I wish I had taken more notes!)
All in all I am pretty happy: having started my career in the kernel, and now writing Javascript/Typescript… maybe I am finally full-stack?
Thanks a lot for the Response
tip! I’m learning stuff every day.
Personally I found it hilarious, the bit with the response code. Here comes a hard-core systems-language, low-level guy, complains about types and stuff, then assumes that 2.03 equals 2. Like, I thought you guys are precise!
Anyway, nice post for people trying to deal with TypeScript.
I accept that these are probably all pointless distinctions to someone who’s trying to write some Typescript and deploy their code!
Yes, but I’m going to pick on you a bit even though you “admit” this.
The way you describe your examples as “That’s JavaScript, not TypeScript” or by distinguishing between JS, a browser engine, and Node.js is very similar to a scenario in which someone said “I’m switching from Rust to Java” and I chimed in and said “Actually, you’re writing Java on the Oracle JVM.”
“Switching from Rust to TypeScript” could only possibly mean that the code they are authoring is written in TypeScript and that it’s running in whatever runtime matches the use-case that the Rust code would’ve been used for (so, likely Node.js).
Is there anyone who doesn’t know that TypeScript is just a broken type system on top of JavaScript?
100% agree, and this is a place where adding in Typescript does makes things more confusing. It is massively frustrating when you run into module issues, and the sooner the ecosystem can get on the same page, the better.
One thing that helps in this regard is that libraries are starting to ship ESM-only, meaning if you’re still on the old CommonJS system or you’re using an older version of Node (pre-12), you can’t use that library anymore. This is going to incentivize a lot of people to upgrade, or rethink how they’re approaching JavaScript.
that’s what one would logically assume would happen. But then look at python2 vs python3, which did a similar thing… Took at least 10 years to resolve.
I definitely get that, and as someone who dealt with that whole transition in my own software, I distinctly remember the pain being caused by accidental upgrades to Python 3 or downgrades to Python 2.
But I think the JavaScript package ecosystem is a bit different for a couple reasons:
left-pad
would have never existed in Python (you know, assuming Python didn’t have a proper standard library), because it just goes against the culture of packaging in that language. Instead, you might find a library that does all of the string manipulation functions. This is great for a language like Python where you are typically running it on a server, but not so great when you are thinking about pulling dependencies from all sorts of places on the web, essentially trusting the network with your package management.node_modules/
is denser than a black hole or whatever, but the truth to that is due to all of the tiny packages in the JS ecosystem, just a single one updating to be ESM-only can cause some real peril in your dependency tree. This is why most libraries are doing CJS/ESM these days, but there are a few that are only distributing as ESM in order to help speed this transition along.I’m not saying that “oh this time it’s different trust me bro”, but I am saying that while there are similarities here, I think the incentive here for not only application developers but also library developers to upgrade is much greater than it was in the Python world when they switched to v3. It also feels a lot easier to me, as someone who had to go from Python 2->3 and has also seen JavaScript go from a language without basic shit like conditionals into the juggernaut that it is today, than Python was because there are tools to compile your code for different environments, like you couldn’t e.g. write a Python 3 program, compile it down to Python 2, and then distribute both versions in the same package. (or can you? if so that’s pretty cool but I had no idea it existed back then)
that’s very helpful context, thank you!
Having this being simpler would definitely be of benefit to everybody, so I am rooting for this
I buy the argument that REST now means the opposite of the intended meaning. I also think that if we used the original definition, approximately no one needs a “REST API” because they’re not made for programmatic use, and most people just want JSON RPC over HTTP instead.
I think RPC with JSON encodings is what most people want; it’s what’s compatible with the thick-client architecture web apps are written with today.
On the other hand, I kind of think they ought to want a REST API, because over the last 10 years, the web dev community has been tying itself in absolute knots over the issue of state management, which each attempted solution to the problem adding another layer of intractable complexity. Whereas with a REST API designed around HATEOAS, state management is just not a problem, because every response is sending you the application state.
A big frustration with HATEOAS is that it can be incompatible with some useful deployment models. Exposing an API application via two hostnames, ports, URL path prefixes, etc. The need for response generation to understand how to send a requestor back through the same network route used to reach the API in the first place can be similarly complex. It’s not an intractable problem but it feels like initial implementations deemed API response generation as the purview of the application. If we had middle boxes (very present in the REST literature) that transformed responses to/from a form imbued with global identify versus logically isolated it would make HATEOAS feel a lot more maintainable.
Using relative urls helps a lot here, though it can still get a bit complicated in some cases. But interestingly, the utility of relative uris are why I actually disagree with the common “REST” advice of never using a trailing slash. Indeed, I say you should ALWAYS use a trailing slash on resources.
Imagine if you go to /users/me/ and have links on them like “subresource/” and “child”. The relative link there now works, without the generated thing needing to know that it live at /users/me/. You can go to other users by linking to “../someone-else/” so even if you were mounted somewhere else on a new path prefix it can still work.
they’re not made for programmatic use
I’d argue that, on the contrary, they’re better suited for programmatic use. Having hypermedia formats like html or json-ld+hydra is what enables programs to understand an otherwise opaque ad-hoc vocabulary, forcing a human to hardcode the knowledge in yet another client. The inverse of programmable.
I don’t understand what that means. Humans have to program computers. Computers cannot program themselves. Computers can share vocabularies, schemas, endpoints, etc. with each other, but at the end of the day, a human being has to make the decision to call some API or not. How does hypermedia change any of that? You can standardize a DELETE verb, but you can’t standardize “do delete spam; don’t delete user data.” It’s just the wrong level for standardization.
Here’s an example problem I solved recently: there was a spam page that listed spam messages in an inbox for me. I got so much spam that clicking each message was time consuming, but the page had no “select all” option. I worked around this by sniffing the network traffic, learning what endpoints it was calling, and just writing a CLI to call those endpoints myself.
How would that process be different or easier with real REST?
You wouldn’t have to sniff anything since the endpoints it calls are declared with on the ui you use. You can form.submit() each delete button.
I mean this in the most literal sense: How? What would that look like in the real world? How is it easier than what I did?
As it was the network calls were just GET which returned a JSON list of messages and a DELETE sent to an endpoint that had the message ID in it. Why would that be simpler in a world where REST won?
document.querySelectorAll(".spam input[type=checkbox]").forEach(function(e) { e.checked = true; });
(you might not even need the .spam parent and then this little thing would just select all on the page, whatever fits your actual thing)
That’s your user agent extension to select all. Then you can click delete. It isn’t all that different, since you’re still looking for the container element or whatever rather than watching network traffic but since the hypertext format is relatively standardized you can do pretty generic extensions too.
PS kinda hard to say REST lost considering the massive success of the world wide web, including generic user agent extensions.
That was my first approach, BTW, but whatever React does to the elements makes it not work. 🙃
I personally agree that Plain Old Webpages With Forms are pretty good and people use React and SPAs etc. too much. But making a CLI to work with a POWWFs (as opposed to a browser extension) would not be easier and in many cases would be harder than making one to work with JSON over HTTP RPC. And the reason people overuse SPAs isn’t just that they’re trendy, it’s because it solves a business problem to have specialized frontend and backend engineers who communicate over formally defined interfaces. The end product isn’t as nice in some ways as a well made MPA, but it’s hard to blame business for wanting to decouple their teams even if it makes the product strictly worse.
One of the missing pieces to make HATEOAS work in practice is that it can be difficult to trust that your application wants to make those API calls. A mis-configured node that redirected an staging app to a production API would be annoying in the conventional model and potentially catastrophic in the HATEOAS model. Similarly for MITM attacks.
I think the difference is is that you’d teach the client to interpret a document type once, and re-use it in multiple places across your application (assuming you have a problem amenable to that kind of re-use).
real REST
if they used a standard vocabulary (eg: a kind of IMAP encoding a-la JMAP) then you’d be able to pull a client off the shelf. If not, then you’d still need to write some logic to interpret and act based on the documents you get. But assuming that this is an alternative universe where people actually took hypertext applications and ran with them, you might even have some framework that handles a lot of the state management for you. But this is not that universe.
Maybe another example might be Jim Webber’s REST-bucks example. Although again, it very much lives in a alternate reality where coffee vendors have standardized on a common set of base document formats.
What did you mean by “not made for programmatic use” then?
but the page had no “select all” option
Good example of when hypermedia could have been used to help the agent discover the “delete all spam” action and help him drive to a new state, all that without adding more coupling than having to know what “delete all spam” means.
It’s an internal API for a website. They don’t want me to program against it. I can’t imagine them choosing to document it. I also still am extremely unclear what form you imagine this documentation taking. Is it just HTML? Is REST just another name for web scraping? Because people do that every day.
Is REST just another name for web scraping?
Well, sort of, yes. REST is a formalization of what Fielding observed in the wild web 1.0 days. A traditional www site is a REST system.
people do that every day
and one big reason why people can do that is thanks to hypermedia’s discoverability (generic hypermedia formats and Uniform Interface), allowing the same spider bot (or any hypermedia agent really, like you+firefox) to traverse the whole web with a single client.
Though probably not JSONRPC because of course that exists. It looks like an alternative to XML-RPC using JSON as the transport.
approximately no one needs a “REST API” because they’re not made for programmatic use, and most people just want JSON RPC over HTTP instead.
I’ve had this thought several times in the last few years. I feel like most of the web APIs I’ve worked on were really not intended to be navigated ad-hoc by a client, so why bother limiting ourselves to the 6 or so HTTP “verbs” and then having to contort our business concepts into noun-ified words that go with the HTTP verbs?
I mean, it’s nice that a programmer can go from job to job and have a common industry language/pattern to get up to speed quicker. So, there’s definitely a social advantage to being REST-ish just for the sake of convention.
The “it would be too easy to misuse” line is pretty exhausting to hear for something that isn’t actually unsafe. Result
propagation is fantastic and I use it all the time, but it isn’t always appropriate, such as for invariant violation.
Sometimes I just want to unwrap something I believe strongly should not fail; e.g., truncating a usize
with what I believe must have a small value into something like a u32
. If there was no unwrap()
I would have to write expect("superfluous message")
which is unlikely to help me, or anybody else, more than the core file or stack trace will. It’s not going to provide the user anything actionable.
Not a rustacean, but it sounds like these uses of unwrap results in undocumented assumptions. One could address this using comments, but those are subject to decay by various means.
A good “why-oriented” design could have unwrap
act like a debug-only expect
, making it require a string literal that is ignored when compiling for Release. To facilitate productivity, it could also be made to accept being called without arguments when compiling in Debug mode, falling back to the current unwrap behaviour.
it sounds like these uses of unwrap results in undocumented assumptions
Sort of? It’s true that there isn’t prose as an argument to the call, but sometimes that’s actually fine. The fact that you’ve unwrapped it instead of propagating it as a Result means you’re literally asserting it is invariant for correct operation. You don’t always need more than that; e.g., for converting from a usize to a u32:
let v: u32 = some_usize.try_into().unwrap();
You’re making it clear you believe the usize value will fit inside a u32. Maybe that needs a comment, but sometimes it’s just obvious from the structure of the program why that’s the case (e.g., you assembled a list of five things above, and the usize came from len()
of a Vec
).
You can write unintelligible or error-prone code in any language. My point is mostly just that, as long as it isn’t demonstrably unsafe, the “if we give it to people they might use it!” justification for not having nice things is pretty weak.
Not a rustacean, but it sounds like these uses of unwrap results in undocumented assumptions. One could address this using comments, but those are subject to decay by various means.
Not really. At the end of the day, if someone writes a helpful function, they don’t know how you’re going to call it, so they have to write it in a general way. But, as the caller, you have more information than the author does, so it’s entirely possible that you’re just calling the function in such a way that you know it can’t fail.
As a not-real example, imagine an integer division function that returns a Result because dividing by zero is an error. If you call that function with a non-zero literal value, you’re not introducing an “undocumented assumption” beyond “I’m assuming that 4 is not 0”.
There’s absolutely no problem with unwrap() in Rust code. I think that people new-and-intermediate to Rust just get over enthusiastic about not having to deal with unchecked exceptions being used for all errors- domain-related and otherwise.
I agree, see also my above comment for another real-world example.
I think this blog post gives a very good overview of the issues around unwrap. I still think it should be in std. The reason here is a structural one: if it weren’t available, people would write similar ad-hoc macros or functions for it.
Defining it as a std lib method enables quite a number of fine grained lints around it.
The most basic one is the one banning it altogether: https://rust-lang.github.io/rust-clippy/master/#unwrap_used
(the search box gives more)
I still think it should be in std. The reason here is a structural one: if it weren’t available, people would write similar ad-hoc macros or functions for it.
This is what I think as well. I agree with the blog post author that expect
should be used instead of unwrap
, but unwrap
is useful for prototyping, and without it in the standard library, some people would just do .expect("")
or as you said define their own function/macro.
I think that the unwrap_used
rule should be enabled by default though and that there should be more documentation and guidelines strongly advising against unwrap
.
I’m a bit of a Rust noob, but what do you do for a function call that should never fail? For example, I have a static site generator that parses a base URL out of a configuration file (validating that it’s correct). Later, that URL is joined with a constant string: base_url.join("index.html").unwrap()
. What do I do here other than unwrap()
? Should I use .expect(format!("{}/index.html", base_url))
or similar? It doesn’t make sense to return this as an error via ?
because this is an exceptional case (a programmer error) if it happens.
You should just use unwrap()
.
I think of it like an assertion, if my assumption about this code is wrong, the program will crash. That’s perfectly acceptable in many situations.
or expect(...)
which is identical except for making grepping for the issue later easier if it does come up
Doesn’t an unwrap()-induced crash always print the line number in addition to the default error message?
Yes. For a quite a while it would be the line number of the panic call inside unwrap though, so not super useful unless you were running with the RUST_BACKTRACE
env var set. This was remedied in Rust 1.42.0.
One easy fix would be to augment cargo publish
so that it scans for unwrap
. Either disallow the upload or add a tag to the published package, with a link to how to fix this.
There‘s quite a few legitimate uses of unwrap, particularly in cases where the error case is impossible, but is needed e.g. because of a trait interface. Such a solution would be very heavy-handed and I think the practical issues with unwrap are overrated - I rarely see them popping up in projects.
If an error is impossible, you should use std::convert::Infallible
(which will be eventually be deprecated in favour the unstable !
(never)) as the error type. Then you could do
fn safe_unwrap<T>(result: Result<T, Infallible>) -> T {
result.unwrap_or_else(|e| match e {})
}
Or with #[feature = "unwrap_infallible"]
you can use .into_ok()
:
fn safe_unwrap<T>(result: Result<T, !>) -> T {
result.into_ok()
}
In the “Why PHP?” Section:
The answer is simple: because it was there. I’m self-taught, and I don’t have much in the way of formal training. Except maybe for the occasional online course I’ve taken, I have no piece of paper with a stamp on it from a prestigious university that says I can tell computers what to do.
This is the crux of it, and there’s a lot of implicit things going on in these sentences. First off, there is a clear jab at people who do have degrees and formal training. “Prestigious” is used pejoratively and sarcastically here. This wasn’t the author’s path, so they resent people who did take that path. Of course when you are self taught, you skew towards any tool that can get you up and running the easiest and quickest. Note how I said “up and running” - it’s not the tool that is best in the long run, it’s the tool that gets you a picture on the screen the quickest. By the way, there’s value in that too, but I wouldn’t base all of the dimensions of my evaluation just on something “being there.” Availability is valuable, but it’s not the only valuable quality.
This is a viewpoint that’s very common in the industry, I personally meet a lot of people who share this mindset. I don’t think it’s entirely wrong, but I think it’s a very limited way of thinking, and the people who hold it tend to be self-righteous like this. I understand that PHP might have been your path, and it might have worked for you. But a tone like this reeks of criticizing and minimizing other people’s path. I get that they feel defensive because people are attacking PHP, but I don’t think that’s causing this philosophy, I think this is many people’s true philosophy under the hood, this was just an excuse to write about it.
Where does this philosophy come from though - can anyone name any popular programming language that was designed for “CS graduates?” Python? Java? Javascript? These are the most pragmatic and un-academic languages on Earth. The pragmatists and proudly un-educated have won, why are they claiming to be the ones that are being persecuted?
Btw, if it’s important to anyone, I don’t have a CS degree, I studied Electrical Engineering. I definitely took CS electives, but I also consider myself mostly self-taught in terms of actual programming and CS. But I don’t knock the academic side of CS, on the contrary I think it’s responsible for every single good idea that makes its way into “practical” programming languages.
I don’t necessarily have a problem with people using something that gets content on a screen quickly. But I don’t accept the excuse that self-taught means you can’t or shouldn’t grow beyond that. I’m self taught. I don’t even have a college degree and yet I learned Haskell. I can write Rust code. I started out in PHP but I outgrew it eventually. Anyone who can become an expert in PHP can do the same.
It’s fine to get payed to write PHP. There is code out there that needs maintaining in PHP. But PHP earned it’s reputation as a deeply unsafe language to develop in and even with the improvements the language has made much of that unsafe core still remains.
While the author of the article is being contemptuous to those with more educated backgrounds, I think you’re doing a bit of the converse here. Programming is as wide as humanity itself; if there’s a way for a computer (for some definition of computer) to accept input and provide output, I can guarantee you that someone will have probably programmed it. There doesn’t need to be a single, good path to programming. Whether your path involves writing PHP, unsafe C, or Haskell, it doesn’t really matter.
It’s difficult in a comment forum to give an appropriately nuanced take on stuff like this. I didn’t intend to come off as contemptuous. If you get hired to help maintain a PHP codebase then the responsible appropriate thing to do is to work on PHP code. There is no shame or condemnation for it.
Sometimes though I think people get stuck or pigeonholed as “PHP developer” or “Python developer” and never learn other tools or approaches. I want to encourage those people that they can be more than just $LANG developer. There are better tools than PHP out there that you can use when you get the opportunity. Learning to protect yourself from the flaws of a given language is a valuable skillset. There is no shortage of work for people who became experts in avoiding the pitfalls.
But ,when you have the opportunity, it is hugely valuable to be able to choose a language with less pitfalls. Where the defensive programming is less about defending against the language itself and more about defending against the environment your software has to run in. Being able to choose those languages is also a valuable skillset that no one should feel is out of their reach.
But ,when you have the opportunity, it is hugely valuable to be able to choose a language with less pitfalls. Where the defensive programming is less about defending against the language itself and more about defending against the environment your software has to run in. Being able to choose those languages is also a valuable skillset that no one should feel is out of their reach.
Great explanation of this idea.
It’s difficult in a comment forum to give an appropriately nuanced take on stuff like this. I didn’t intend to come off as contemptuous. If you get hired to help maintain a PHP codebase then the responsible appropriate thing to do is to work on PHP code. There is no shame or condemnation for it.
I figured which is why I tried to keep my reply soft. I agree with everything you just said.
Also, a CS degree doesn’t teach you programming anyway. It’s not meant to. It teaches you CS (or at least tries to). You probably self teach some programming along the way but it’s harly a focus of coursework.
I don’t think that’s universally true. The first two years of required classes for a CS degree at the universities around me (US) were heavily focused on programming (Java… C++…), and failing any of those would have meant no CS degree.
But a tone like this reeks of criticizing and minimizing other people’s path.
I don’t entirely agree with your interpretation, but I will note for sake of irony that this is more or less how un-credentialed (in the sense of not having a degree in CS or other “relevant” field) developers feel for pretty much their entire careers. There’s a huge and powerful trend in tech hiring of prioritizing people who have a degree from one of the handful of trendy top universities, and a feedback loop wherein people who work at major companies help develop and teach “how to pass our interview” courses at those universities. The result is that if you are not someone who has a CS (or other “relevant”) degree you are constantly the odd one out and near-constantly being reminded of it.
I still feel this coming up on 20 years in to the industry and with a résumé that largely lets me avoid a lot of the BS in interviewing/hiring.
I still feel this coming up on 20 years in to the industry and with a résumé that largely lets me avoid a lot of the BS in interviewing/hiring.
(I’m hoping this comment isn’t too off-topic.) I certainly agree. I myself come from one of those trendy elite CS universities (though it’s been a good while at this point) and am well credentialed, but I’ve started to use large numbers of junior engineers out of good schools to usually be a light negative signal when applying for a company. The culture of hiring in software is such that companies often overselect for credentials while ignoring effectiveness, at least in my opinion.
There’s a huge and powerful trend in tech hiring of prioritizing people who have a degree from one of the handful of trendy top universities
This depends entirely on your experience. I’ve never seen this trend, I’ve seen quite the opposite - a large chunk of people I work with don’t come from a CS or engineering background. Nor do I see anyone being hired over someone else because of where they went to school.
The fact that you’ve never seen FAANGs go on-campus at certain universities (but not others) to recruit, help develop “how to pass the interview” curriculum to be taught at certain universities (but not others), etc., doesn’t mean that it doesn’t happen or that it doesn’t have an effect both on their resulting workforce/company culture and on everyone who emulates their hiring (which is unfortunately a large chunk of the industry).
can anyone name any popular programming language that was designed for “CS graduates?”
My glib answer: yes. Go. From Rob Pike, the creator: “The key point here is our programmers are Googlers, they’re not researchers. They’re typically, fairly young, fresh out of school, probably learned Java, maybe learned C or C++, probably learned Python. They’re not capable of understanding a brilliant language but we want to use them to build good software. So, the language that we give them has to be easy for them to understand and easy to adopt.”
More seriously, I think this “self-taught persecution” attitude is related to imposter syndrome and the increasing credentialism in society. A former coworker of mine tended towards a self-defense attitude and I chalk that up to his not having a CS degree as a developer (he did, however, have formal training as a jazz musician).
It’s also not as if PHP is alone in this hate—C is probably hated just as much, if not more, than PHP.
Yet if you know C well, you’re probably seen as much better “programmer” then if you knew PHP well. But yeah, perceptions.
Rent seeking isn’t hate. C dominates everything. So if you invent a new programming language, there’s only two paths to being successful. You can either (1) appeal to the new generation of young people to learn your language instead of C. That’s how PHP and Node did it. Or (2) you can run PR campaigns vilifying C until you’ve convinced enough VPs to convert their departments. That’s how Java did it.
I agree with everything you said here. I even share your perception that many people seem to be insecure and/or defensive about not having a CS background or not really understanding some of the popular concepts we read about in blog posts, etc. Like you said: The “pragmatists” won- what are they worrying about?
In some ways, computer programming is very much like engineering– engineering jobs/tasks/people fall on a wide spectrum of “how academic” from “glorified assembly line work” all the way to “indistinguishable from pure science research.” And attitudes similarly span that spectrum. That’s not a bad thing–just an observation.
What’s extra frustrating to me is that it’s now turned a corner where if you do criticize a language like PHP, you’re seen as an irrational troll who just thinks you’re smarter than everyone else. It’s become anti-cool to shit on PHP, even though it’s still literally the least capable backend language being used today besides maybe Python (no async, no threads, no generics in its tacked-on type system, object equality is not customizable and will crash your program if you have a ref cycle, etc).
As a developer that has experience building web backends in PHP but also some experience with Go, Python, Ruby (w/ Rails) and Rust, I wonder what language you deem capable as a backend language that still provides the same level of abstraction as, say, Laravel or Symfony. Rails obviously comes to mind, but what else is there (if Python w/ Django is out of the question)?
Keep in mind that I’m not trying to evangelize “the one true backend language” or anything like that. My main point was just to gripe that there are still (what I consider to be) legitimate criticisms of PHP as a (backend) programming language, but whenever I point them out as reasons that I might suggest someone avoid starting a project in PHP, I’m met with more skepticism and defensiveness than I think is justified.
The secondary point is that I truly believe that PHP is strictly inferior to other backend programming languages. What I mean by that is that we all work differently and have different preferences: some people will like dynamically typed languages, some will like statically typed; some like OOP, some like FP. That’s all great. Scala is a very different language than Go, which is a very different language than Clojure. But if you look at “modern” PHP projects and “best practices” advice, it’s almost literally the same thing as the “best practices” for Java 7 in 2010, except that PHP doesn’t have good data structures in its standard library, has no threading or async, no generics, etc. And if you compare modern PHP with a recent version of Java… oh, boy! Java now has ADTs, Records, Streams, and a few other really nice additions that make writing code a little less painful.
So, it’s not just that I think PHP is a bad language, it’s that it’s, IMO, a less capable subset of another language. If it were actually different, then I might just shrug it off as a preference thing. I mean, PHP even stole the class semantics right from Java (single inheritance, interfaces, static methods, abstract classes, final keyword, etc).
I know people love Laravel, but my contention isn’t that Laravel isn’t good (I don’t honestly have much experience with Laravel, but I have worked with Symfony). But Laravel is not PHP. I’m talking about the language. If there’s something written in PHP that’s so valuable to your project that you choose to work with it, that’s great. But in my opinion, the sane attitude should be one of “Well, I guess it’ll be worth it to deal with PHP so that we can use XYZ.” and not “PHP is a good language and @ragnese is just an anti-PHP troll. What backend language could possibly want async IO or easy fire-and-forget tasks, anyway?”
I don’t like rails either for slightly different reasons. I would rather develop in Go if I’m creating a backend web service than Python, Ruby, or PHP. I don’t find that I usually need much more abstraction than say gorilla most of the time. And Go doesn’t suddenly surprise me with strange behavior as often.
The person they’re asking definitely doesn’t use Go though, because of:
no generics in its tacked-on type system
Phoenix in Elixir is pretty good — very batteries-included, high-velocity, and extremely good with async/concurrency/parallelism. I don’t know if “object equality is not customizable” applies, it’s immutable with value-based equality at all points, so…
The tacked-on type system is pretty awful (Dialyzer is well worse than, say TypeScript, mypy, or Sorbet) but I don’t think it’s strictly worse, considering what you get from the framework/library.
Based on their complaints (“no async, no threads, no generics in its tacked-on type system, object equality is not customizable and will crash your program if you have a ref cycle, etc”), and assuming they’re not using a really esoteric language, I’m willing to bet they use Scala, Java, C#, or Rust. I’m definitely curious about this too.
Whether or not those have a web framework that competes with Laravel, I’m not sure. That’s also not the measure of success though, for example if people are working with a lot of services a monolithic web framework isn’t as important or even desired. That’s one other thing to consider here - I think it’s accurate to say that PHP is synonymous with building monolithic web apps, and not everyone builds monolithic web apps.
Elm 0.19 nuked any good thoughts I had towards the language. Limiting native modules to a small group of people was a bad design decision, and the lack of leadership has imo doomed the language. elm-janitor is a project that exists to cleanup the mess, and they link a useful but sad spreadsheet of all the fixes that have yet to be merged.
If I were to stick to a functional language today, I would bet on Roc succeeding where Elm couldn’t.
I think ReScript has pretty much taken that niche already. It did what Elm didn’t–provide a pragmatic, interoperable integration with the rest of the JS ecosystem while also providing type safety and fast compiles.
mm this does look like a proper replacement, whereas my Roc evangelism is probably premature. my first question would be how it differs from reason ml which also fills the functional-web niche
My understanding is that ReScript is essentially a rebranding of ReasonML which has mostly been abandoned.
ReScript is a continuation of the ReasonML functional-web effort. It split off from the ‘ReasonML’ branding to create a more integrated, turnkey experience for typed functional web dev.
This may sound nit-picky or unreasonable, but the immediate blocker for me adopting ReScript is its take on
Option
/undefined
/null
.I actually do use
undefined
andnull
to mean semantically different things in my JavaScipt/TypeScript code. For example, if I’m sending a POST request to my backend API, it will treat a missing field (undefined
) as unset and needing the default value (if it is intended to be optional), and it will treat a presentnull
to mean that the client is explicitly denoting “emptiness” for the value and it will NOT be overwritten with a default value.Likewise, in languages with a generic Option type (Swift, Rust, Kotlin-when-I-write-it), I do sometimes represent the above semantics similarly by having an
Option<Option<T>>
(orOption<T?>
in Kotlin) for an “optional nullable” value.I realize that ReScript has escape hatches for these things, but I feel like “escape hatches” are for uncommon situations; I don’t want to have these “escape hatches” all over my code and have to fight against the grain of the language.
As much as I loathe TypeScript, I feel like I’m stuck with it.
ReScript can deal with
null
andundefined
separately using its provided APIs:Right, I know. It’s just that the standard library functions all use
option<'a>
, so I worry about needing to do a bunch of fiddling between three kinds of “maybe present” types instead of the usual two. Maybe in practice it isn’t bad, but I’ve dug myself into plenty of holes before while trying to go against the grain of the language I was using.I don’t see any big problems, you just define your form fields with strong types like
myField: option<string>
and convert the weaker-typed values received over the network into strongly-typed internal values with checks and converter functions. It’s conceptually very similar to TypeScript except it doesn’t suffer from the ‘wrapping’ problem because the option type has special compiler support.Wow that spreadsheet is really depressing.