I’ve been fascinated by this topic for a little while now, and there doesn’t seem to be much discussion around it aside from occasional mentions. I also want to discuss how other languages in trying to remove friction end up encouraging worse practices.
What I mean by friction is essentially making something more difficult or less convenient to do in order to steer users away from it without resorting to restricting that thing entirely. Andrew explains a little bit here. I really love how intentional friction is used in Zig to guide users towards better solutions to problems. Two of the bigger examples of intentional friction in Zig are vtable based interfaces and generic APIs. Another example would be anonymous functions, but I’m too lazy to write about that one right now.
Vtable interfaces
Zig doesn’t have a language-level feature for vtable interfaces, so you’re required to implement them manually. This isn’t difficult but it’s certainly less convenient compared to a language with interfaces built in. This lack of convenience is actually a good thing! Vtable based interfaces have some very serious performance implications: indirect function calls are more expensive and can’t always be inlined by the compiler, they require one more level of indirection to access, they’re horrific for data locality, and they complicate memory management. Interfaces tend to be overused by programmers used to higher level languages where runtime polymorphism is heavily encouraged, and the downsides of this approach are less impactful in a garbage collected language where everything is a reference anyway.
Generic APIs
Another instance of friction in Zig is writing generic APIs. Generics cause slower compile times and makes code harder to reason about in any language, so we should prefer to limit the amount of generic code in use as much as possible. In Zig, duck typing and anytype
make generic APIs feel really “loose”, and as a result it doesn’t “feel nice” to write very generic APIs in Zig. I couldn’t come up with a better way to explain that, but I think if you’ve used Zig before you’ll know what I mean. This friction discourages proliferation of overly generic code.
And now for the opposite effect…
Lubing up
Yeah… it’s a gross term*, but it conveys the concept well. That is, causing harm by reducing friction in the wrong places. What I previously talked about was “making the bad things hard”, and this is essentially the opposite: “making the bad things easy.” This is a trap that seems easy to fall into if you aren’t carefully considering the design and incentives of your language features. Zig does an excellent job of not falling into this trap, other languages not so much. I have two examples.
RAII
Oh boy! HN boomers, avert your eyes. Allocating heap memory is expensive. Good code batches memory allocations. In good code, there are very few points in the program where memory is actually freed. In good code, RAII is almost completely superfluous and useless. RAII is for bad code, where every single object gets its own malloc/free and pointers “own” their memory. This is a horribly outdated, slow, and Java-esque way of managing memory that introduces massive amounts of complexity and performance issues. RAII is a bandaid “solution” to manage manufactured complexity, when the real problem is much more fundamental. When you bring this up the reply is always, “Don’t like it? Don’t use it.” But the problem comes back to incentives. RAII makes it easier to write terrible code, so its existence actively incentivizes people to write worse code. When the bad things are easy to do, people are going to do them more!
Map vs forEach
I have seen JS code in the wild that used array.map for in place operations, and I have a feeling it was entirely because using forEach or writing a for loop would take more characters to type and look slightly uglier. I wonder how prevalent this is, and how many CPU cycles have been wasted in aggregate because there was the tiniest bit more friction involved in using forEach instead of map. The fact that it’s non-zero makes me sad. Preferable solutions like forEach should be easier to reach for than worse solutions like map, so I imagine a little bit of intentional friction could be introduced here to discourage code like this. Of course, JS is full of terrible design decisions like having class based runtime polymorphism through inheritance but not having f*cking enums or a good switch statement, so changing that one thing would not stop JS programmers from torturing CPUs with bad code.
End
Do you have any more examples of intentional friction in Zig, other languages, or software in general? I suppose a large downside of having this kind of friction is that there’s users who encounter it and instead of going “maybe I could solve this problem in a better way”, they instead go make an issue on the issue tracker demanding you to lube up.
I’ve been thinking about maybe turning this into a blog post which would elaborate more, so that’s why I want to see what other people here have to say about this.
* Literally making it easier to get f*cked, hence “lubing up”