Latest Posts (20 found)
Corrode Today

Migrating from Go to Rust

Out of all the migrations I help teams with, Go to Rust is a bit of an outlier. It’s not a question of “is Rust faster?” or “does Rust have types?”, Go already gets you most of the way there. The discussion is mostly about correctness guarantees , runtime tradeoffs , and developer ergonomics . A quick disclaimer before we start: this guide is heavily backend-focused . Backend services are where Go is strongest, small static binaries, a standard library focused on networking, and an ecosystem of libraries for HTTP servers, gRPC, databases, etc. That’s also where most teams considering Rust are coming from (at least the ones who reach out to me), so I think that’s the comparison that’s actually useful in practice. If you’re writing CLI tools, embedded firmware, or game engines, some of this still applies, but to be honest, I’m afraid this is not the best resource for you. For context, I’ve written about Go and Rust before: “Go vs Rust? Choose Go.” back in 2017, and later the “Rust vs Go: A Hands-On Comparison” with the Shuttle team, which walks through a small backend service in both languages. What you will learn in this article I’ll be upfront: I’m not a fan of Go. I think it’s a badly designed language, even if a very successful one. It confuses easiness with simplicity , and several of its core design tradeoffs ( everywhere, error handling as a discipline rule rather than a type, the long absence of generics) point in a direction I disagree with. That said, success matters! Go has captured a real and persistent share of working developers, hovering around 17–19% in the JetBrains Developer Ecosystem Survey. Rust is growing steadily but is still a smaller slice: Go is clearly working for a lot of people, and a guide that pretends otherwise isn’t helpful. So I’ll do my very best to be objective in this guide rather than relitigate old arguments. But you should know my priors so you can calibrate. The other prior worth disclosing: I run a Rust consultancy; of course I’m biased! More people using Rust is good for my business. But I’ve also worked in both languages professionally and shipped Go services to production. This guide is for Go developers who want an honest, side-by-side look at what changes when you move to Rust. For a deliberately opposite take, I recommend reading “Just Fucking Use Go” by Blain Smith. Holding both views in your head at once is more useful than either one alone. If you prefer to watch rather than read, here’s a video from the Shuttle article above, read and commented by the Primeagen: Go developers already have one of the cleanest toolchains in the industry. Back in the day, it started off a trend of “batteries included” toolchains that give you a single, consistent interface for building, testing, formatting, linting, and managing dependencies. I’m glad that Rust followed suit, because it’s a great model. It’s one of my favorite parts about both ecosystems. has even more built-in: The big difference is that in Go you typically reach for third-party tools ( , , , ) to fill gaps. In Rust, the first-party ecosystem covers more out of the box. Things that do require external crates (e.g. , ) install with one command and feel native, e.g. gives you right away. Both communities have converged on the same insight about formatters: a single canonical style, even an imperfect one, is worth more than the bikeshedding it eliminates. Gofmt’s style is no one’s favorite, yet gofmt is everyone’s favorite. — Rob Pike, Go Proverbs The same is true of : not everyone likes every detail, but the absence of style debates in code review is worth far more than the occasional formatting preference you’d have made differently. The headline is that Go and Rust are both compiled, statically typed, single-binary-deploy languages with strong concurrency stories. The differences are about what guarantees you get from the compiler and how much control you have over runtime behaviour . Go developers don’t usually come to Rust because Go is “too slow.” For most backend workloads, Go is plenty fast. People are generally a bit frustrated with Go’s verbose error handling, the danger of segmentation faults from pointers, and the lack of generics (for a long time) or any sophisticated type system features, such as enums or traits. Interfaces are not a worthy replacement for traits, and the Go standard library has some weird gaps, such as the lack of a type. I call it my billion-dollar mistake. It was the invention of the null reference in 1965 … This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years. — Tony Hoare, inventor of , QCon London 2009 This is the one I hear most often. You ship a Go service, it runs fine for months, and then one Tuesday at 3 a.m. a code path runs where someone forgot to check whether a pointer was , and the goroutine panics. Go’s compiler does not force you to consider the absence case. Rust’s does: You literally cannot dereference an without acknowledging the case. Whole categories of pager-duty incidents disappear. is a great tool, but it’s a runtime detector, it only finds races that actually execute during your tests. Mutating a map from two goroutines without a lock compiles fine in Go and only blows up in production under load. In Rust, sharing mutable state across threads requires types that implement and . Try to share a plain between threads and the program does not compile . You’re forced to wrap it in an , an , or use a channel. That race condition becomes a type error. 1 is fine for a while. After a few years, you notice three things: It’s worth being honest about the counter-argument here, since it came up in the Lobste.rs thread on my Shuttle article: experienced Go developers point out that and catch most of the “forgot to handle the error” cases in practice, and that explicit is easier to read than dense chains. Both points are fair, and the explicit style is a deliberate cultural value, not an accident: I think that error handling should be explicit, this should be a core value of the language. — Peter Bourgon, GoTime #91 , quoted in Dave Cheney’s Zen of Go My take is that lints are an opt-in safety net you have to remember to set up, while Rust’s is the type signature itself, there’s no way to forget. The boilerplate-vs-readability tradeoff is more genuinely subjective. The operator handles propagation; handles wrapping; and a on is exhaustively checked . Add a new variant tomorrow and the compiler shows you every place that needs updating. Go got generics in 1.18, and they’re useful, but the implementation has constraints (no methods with type parameters, GC shape stenciling, occasional surprising performance characteristics). Rust generics monomorphize, each instantiation produces specialized code with zero runtime cost. Combined with traits, this gives you real zero-cost abstractions. This matters less in handler code and more in shared infrastructure (middleware, generic repositories, decoders, parsers), where Go often pushes you back to / plus type assertions. Go’s GC is excellent, concurrent, low-pause, well-tuned for typical service workloads. But “low-pause” is not “no-pause.” Under heavy allocation, P99 latency tails are noticeably worse than a Rust equivalent that simply doesn’t allocate on the hot path. I won’t oversell this, for the vast majority of services, Go’s GC is a non-issue. But for latency-sensitive systems (trading, real-time bidding, network proxies, high-throughput ingestion), the lack of GC pauses is a genuine selling point. Go is death by a thousand paper cuts. It is a very pragmatic language and if you are willing to glance over the above issues, you can be very productive in it. But at a certain codebase size, the problems start to compound. There is no single moment when Go loses its appeal, but teams find themselves wishing for more (more safety, more control, more expressiveness) and that’s when they start looking around for alternatives. The fastest way to feel comfortable in Rust is to map patterns you already know. For a longer, fully-worked example of building the same backend service in both languages, see the Shuttle comparison , the section below focuses on the patterns that come up most often. The operator does the dance for you, including type conversion if is implemented (idiomatic with ’s ). There is no in safe Rust. References can’t be null. Pointers can be, but you almost never use raw pointers in application code. Go’s interfaces are structural, a type satisfies an interface implicitly: Rust’s traits are nominal, you implement them explicitly: The Go style is great for ad-hoc duck typing. The Rust style is great for refactoring and discoverability, you can grep for every implementer of a trait. The closest equivalent of / in Rust is , but you almost never want it. The Go community knows the cost of reaching for too: interface{} says nothing. — Rob Pike, Go Proverbs Generic functions with trait bounds ( ) cover the vast majority of cases and give you monomorphization with no runtime dispatch. Where Go pre-1.18 would have forced you back to plus a type assertion, Rust’s traits + generics let you stay specific. When you do want runtime dispatch (e.g. heterogeneous storage of different implementers), reach for or . That’s the direct Rust analog of holding an value in Go. Go’s concurrency model is famously simple: Goroutines are cheap, the runtime schedules them across OS threads, and channels ( ) are the primary coordination primitive. The Go proverb captures the philosophy: Don’t communicate by sharing memory; share memory by communicating. — Rob Pike, Go Proverbs This is the area where Go genuinely shines, several commenters in the Lobste.rs discussion made the point that goroutines “just disappear” into normal-looking blocking code, and that’s worth giving Go credit for. Rust async is more powerful, but it’s also more visible in your code. Rust uses / on top of an executor (almost always for backend services): The shape is similar. The differences: For most backend code, the day-to-day feel is similar: spawn a task, communicate via channels, use timeouts liberally. In Go, you plumb a through every blocking call: Rust has no built-in . The closest equivalent for cancellation is : For timeouts, wraps any future. For deadlines/values, you typically pass them as explicit arguments or via spans rather than a single context object. Some Go developers miss the implicit-feel of . In practice, the explicit Rust style is easier to reason about, you always know exactly what’s cancellable and what isn’t. The deeper point is that neither language gives you cancellation for free, the discipline just shows up at different layers: Go doesn’t have a way to tell a goroutine to exit. There is no stop or kill function, for good reason. If we cannot command a goroutine to stop, we must instead ask it, politely. — Dave Cheney, The Zen of Go In Go that “asking politely” is a plumbed through every call site by convention. In Rust it’s a (or a channel) plumbed through every call site, but the compiler can actually tell you when you forgot. Both languages have channels. The translation is direct: Rust’s channels distinguish sender and receiver as separate types, which makes ownership and -ness explicit at the type level. Rust’s is the equivalent of a Go value receiver; is a pointer receiver with mutation. Owned (consuming the value) has no Go analog and is occasionally very useful (typestate, builders). Go’s is a UTF-8 byte slice with copy-on-assign semantics (the header is copied, the underlying bytes are shared and immutable). Rust splits this into two types: As a rule of thumb, take in arguments, return when you produce new data. This is mostly painless once you internalize it. The vs split is a microcosm of Rust’s broader “borrow vs own” model. Go got generics in 1.18 (March 2022), thirteen years after the language shipped. They are useful, but they feel tacked on, and in practice they have most of the downsides of a generic type system without delivering the upsides you’d expect coming from Rust, Haskell, or even modern C++. This is a strong claim, so let me back it up. The most telling signal is that three years after generics landed, Go’s own standard library still mostly avoids them. still takes a closure instead of a constraint. is still typed as / . The generic helpers that do exist live in a small handful of packages: , , , and a few entries under . Compare that to Rust, where generics permeate the standard library from day one: , , , , , / , , , every collection, every smart pointer. You cannot write idiomatic Rust without using generics, because the standard library is generic. In Go, generics are an opt-in feature for library authors who really need them. In Rust, they’re the substrate everything else is built on. Rust’s generics are tied to traits, which double as the language’s mechanism for ad-hoc polymorphism, supertraits, associated types, blanket impls, and coherence. Go’s constraints are just interfaces with an extra operator for type-set membership. There are no: The practical consequence is that the moment your abstraction needs more than “a function that works for any with these few operations,” Go pushes you back to plus type assertions, code generation, or runtime reflection. Rust uses a Hindley-Milner-style inference engine that propagates type information through entire expressions, including across closures, iterator chains, and operators. You routinely write: and the compiler figures out is from the range, and is from the target. Go’s inference is much shallower. It can usually infer type parameters from function arguments, but it cannot infer from return-position context , cannot chain inference through generic builders the way Rust does, and frequently forces explicit type arguments at call sites: In Rust this is the exception; in Go it’s still common. Rust monomorphizes: every and produces specialized machine code with zero runtime dispatch. Go uses GCShape stenciling with dictionaries , where types that share a “GC shape” share the same compiled function and dispatch through a dictionary at runtime. The result is a compile-time/runtime tradeoff that often surprises people: generic Go code can be measurably slower than the equivalent hand-written non-generic version, because every method call on a type parameter goes through an indirection. There’s a well-known PlanetScale post showing exactly this. In Rust, generic code is the fast path. Reaching for (the equivalent of Go’s interface dispatch) is a deliberate choice you make when you want runtime polymorphism. This is the part that bothers me most. A good generics system removes reasons to fall back to escape hatches. In Rust, generics + traits eliminate most of what you’d otherwise need or runtime reflection for. The type system gets stronger. In Go, generics did not remove , did not remove , did not remove code generation as the dominant pattern for things like ORMs, decoders, and mocks. still uses reflection. still uses . still generates code. The places where a real generics system would shine are the same places Go reaches for runtime mechanisms it had before 1.18. Generics in Go feel additive, a new tool in the box that’s useful in narrow cases. Generics in Rust feel foundational; remove them and the language collapses. That’s the difference, and it’s why generic Go code, in my experience, doesn’t read better than the -based code it replaced; it just reads differently, with more punctuation. If you’re already opinionated in Go, the Rust ecosystem has converged to a similar level of “default picks.” For a typical backend service: + + + + + covers 90% of what you need. I want to be straightforward here. Coming from Go, you will hit a wall . The wall has a name. Go’s runtime handles memory and aliasing for you. Rust pushes that decision into the type system. The first few weeks you’ll write code that “should obviously work” and the compiler will refuse it. The patterns that bite Go developers most often: With all of these rules, the borrow checker truly sounds like a “gatekeeper” of sorts, which keeps getting in the way and is just overall frustrating to deal with. That is not the mental mindset you should have when learning Rust. The borrow checker truly uncovers real and very existing bugs in your code, and if you don’t address them, your program will deal with safety issues. So whenever you get a compiler error from , take a step back and think how your code could break. A few questions you can ask yourself: That is the mindset you need to understand the borrow checker. Humans are genuinely bad at reasoning about memory. We forget that pointers can be null, that old references can outlive the data they point to, and that multiple threads can touch the same data at the same time. We tend to have a “linear” mental model of how data flows through a program, but in reality it’s closer to a complex graph with many paths and interactions. Every condition forces you to consider what happens in both branches. Every loop forces you to consider what happens on every iteration. That is exactly the kind of reasoning the borrow checker is designed to do for you! It enforces best practices at compile time, and it can feel annoying when your own mental model disagrees with the borrow checker’s (which is the more accurate one 99% of the time). There are cases where the borrow checker is genuinely too strict, but they are rare, and as a beginner you’ll almost never run into them. I got memory management wrong plenty of times in my early days, but I approached it with a learner’s mindset , which helped me ask “what’s wrong with my code?” instead of “what’s wrong with the compiler?”, a reaction I see a lot in trainings. The good news is that once you internalize borrowing, it stops fighting you. Most experienced Rust developers will tell you the borrow checker became an ally somewhere between weeks 4 and 12. The first month is the hardest. Be honest with your team, Rust compile times are a real downgrade from Go’s. A clean release build of a medium service can take minutes in comparison to Go’s near-instantaneous compiles. Incremental builds and are reasonable and compile times have gotten much better over the years, but you’ll feel the difference. To mitigate, use in your edit loop, split into a workspace once it pays off, and keep proc-macro-heavy crates in their own crate so they only recompile when they change. See tips for faster Rust compile times for a deeper dive. Go’s “one type of function, sync everywhere, the runtime handles concurrency” is genuinely simpler than Rust’s split between and . You’ll need to think about which of your functions are async, where you , and how that interacts with traits. Async traits (stable since Rust 1.75) help a lot, but there are still rough edges (especially around with async methods). Rust’s crate ecosystem is growing and libraries are high-quality across the board, but Go has a head start in some backend-adjacent domains: Kubernetes operators, cloud-provider SDKs, database drivers for certain niche stores. Before you commit, spend a day checking that the libraries you depend on have Rust equivalents you’re willing to use. Teams I help often have to hand-roll at least one or two core libraries themselves. For example, they might have to update an abandoned crate for XML schema validation, or write their own client for a lesser-known protocol. You don’t have to rewrite everything in one go. The strategies that work best, in order of how I usually recommend them: If one specific service in your fleet is the perpetual problem child (high CPU, latency-sensitive, or constantly hit with reliability issues), rewrite just that one in Rust, behind the same API contract. This is the lowest-risk migration. Other Go services keep talking to it via HTTP/gRPC, oblivious to the underlying language. Background workers, queue consumers, ingestion pipelines, and CPU-bound batch jobs are excellent first targets. They typically have a clear input/output boundary (a queue, a topic) and no shared in-process state with the rest of the system. You can call Rust from Go via cgo, and there are good guides on how to do it . (Reach out if you’d be interested in a guide on this from me.) In practice, I rarely recommend it for backend services. The build complexity and FFI overhead usually outweigh the benefits compared to “just stand up a Rust service and put it behind a network call.” For libraries and CLI tools, it’s more viable. If you have an API gateway or reverse proxy, you can route specific endpoints to a new Rust service while the rest stays in Go. This works particularly well when one bounded context (auth, search, billing) is the right unit to migrate. The pattern is often called “strangler fig,” because the new service grows around the old one until it eventually replaces it entirely. Start with a service that has a clear boundary. Don’t pick the most central, most-deployed service in your fleet. Pick the one where the contract with the rest of the system is well-defined and the blast radius is small. Keep the same API contract. If your Go service exposes a REST API, your Rust service should too: same paths, same JSON shapes, same error envelope. The migration is invisible to clients, and you can swap traffic incrementally with a gateway. Don’t translate idioms verbatim. Resist the urge to write Go-flavoured Rust. becomes . Goroutine-per-request becomes only when you actually need it (axum already concurrently handles requests). Interfaces with one method usually become trait bounds on a generic, not . Use the compiler as a pair programmer. Rust’s compiler errors are usually pretty good. Read them slowly. They almost always tell you the right answer. The team members who struggle longest are the ones who fight the compiler instead of treating it as a collaborator. Invest in training early. I’ve seen teams try to do a Rust migration “on the side,” learning as they go. It rarely ends well. It’s a bit like training for a marathon by signing up for the race and then trying to run it without any prior training. You can do it, but it’s going to be painful and you might not finish. Block off real time for learning: a workshop, an online course , paired sessions on real code. The upfront investment pays back many times over once the team is fluent. (Hey, if you want to talk about training options, I’m happy to chat .) Not everything should be migrated. Go is excellent for: A hybrid strategy is fine and common. Many of the teams I work with end up with a polyglot backend: Go for the “boring” services, Rust for the ones where reliability and performance pay back the extra effort. Numbers vary wildly by workload, so take these as rough guidance. Not promises! But here are some ballpark numbers, based on Go-to-Rust migrations I’ve helped with: Honestly, you’re unlikely to get a 10x throughput improvement going from Go to Rust the way you might from Python. What you get is fewer “silly errors” and flatter latency tails, plus the ability to expand into other domains like embedded development or systems programming while still using the same language. That’s often the most surprising side-effect of a migration: there’s a lot of opportunity for code-sharing across teams that previously had to use different stacks. You can use Rust for everything. Going from Go to Rust is a different kind of migration than coming from Python or TypeScript . Coming from Go, you know the benefits of a statically-typed, compiled language. So you’re not trading away dynamic typing or a slow runtime, you’re trading away in exchange for a more robust codebase with fewer footguns, and a stricter compiler that catches more mistakes at compile time. There is a steeper learning curve, however. For foundational services (services that your organization relies on, that have high uptime requirements, that are critical to your business), that trade is obviously worth it. For others, Go remains the right answer. The point of a migration is to put each problem in the language that solves it best. Ready to Make the Move to Rust? I help backend teams evaluate, plan, and execute Go-to-Rust migrations. Whether you need an architecture review, training, or hands-on help porting a critical service, let’s talk about your needs . Rust’s type system doesn’t catch all data races, but types that truly can’t be shared between threads without synchronization won’t compile. You can still have logic bugs in your synchronization, but you won’t have the kind of “oh no, I forgot to lock this” that often leads to silent data corruption. ↩ Where Go and Rust overlap, and where they diverge. How Go patterns map to Rust. What you gain from the borrow checker. Where I tell people to keep Go and where Rust is worth the migration cost. How to migrate Go services incrementally. The boilerplate dilutes the actual logic of your function. Wrapping with is a discipline rule, not a compiler rule. It’s easy to drop context on the floor. Sentinel errors via / work, but the compiler doesn’t tell you when you forgot to handle a new variant. Rust async functions return s. They don’t run until awaited or spawned. The compiler tracks / across points. If you hold a non- value across an await, you get a compile error explaining exactly why. There’s no built-in goroutine-style preemption. Long CPU-bound work in an async task starves the executor; you offload to or instead. Channels ( , , ) are first-class but live in libraries, not the language. , owned, heap-allocated, growable. Equivalent to you intend to mutate. , a borrowed view into someone else’s string data. Equivalent to a Go parameter most of the time. Supertraits / constraint hierarchies. In Rust you write , and any automatically satisfies and . Go has no equivalent; you stack interface embeddings, but the constraint solver doesn’t reason about hierarchies the way Rust’s trait system does. Associated types. Rust’s has , so is a first-class thing you can name in bounds. Go’s closest equivalent is a second type parameter, which leaks into every signature. Blanket impls. In Rust, automatically gives every type a method. Go has no way to add methods to a type from outside its defining package, generic or not. Methods with their own type parameters. This is an explicit, documented non-feature in Go. You cannot write . In Rust, generic methods on generic types are routine. Long-lived references. In Go, you’d happily hold a from a map for as long as you want. In Rust, that borrow blocks mutation of the map for its whole lifetime. The fix is usually to clone, or to scope the borrow tighter. Self-referential structs. Common in Go (a struct holding both data and an iterator over it). In Rust, this requires , , or a redesign. Almost always: redesign. Sharing mutable state across goroutines. What you’d write as becomes . Slightly more verbose, much more checked. Returning references from functions. Lifetime annotations show up. They’re not as bad as their reputation, but they’re new. If a value got moved from one place to another, what would happen if the original place tried to use it again? If a value is shared across threads, what would happen if one thread modified it while another thread is using it? If a pointer is dereferenced , what would happen if it was null or dangling? When a value goes out of scope , what would happen if it was still being used somewhere else? Kubernetes-native tooling : operators, controllers, CRDs. The ecosystem is overwhelmingly in Go. CLI utilities and dev tooling : fast compiles, easy cross-compilation, simple deployment. Glue services : thin API layers, proxies, format converters. The boilerplate ratio in Rust isn’t worth it here. Anywhere your team velocity matters more than absolute correctness guarantees . CPU usage: 20–60% reduction. Less dramatic than Python-to-Rust, because Go is already efficient. The wins come from no GC and tighter loops. Memory: 30–50% reduction, mostly from the absence of GC overhead and a smaller runtime. P99 latency: significantly more consistent. Rust services tend to flatline where Go services have visible GC-induced jitter. (This has gotten much better on the Go-side ever since they introduced their low-latency GC, but the difference is still there under heavy load.) Production incidents: this is the one teams report most enthusiastically. The classes of bugs that survive and reach production (data races, nil dereferences, missed error paths) just don’t compile in Rust. Oncall rotations are typically very boring after a Rust migration. Rust’s type system doesn’t catch all data races, but types that truly can’t be shared between threads without synchronization won’t compile. You can still have logic bugs in your synchronization, but you won’t have the kind of “oh no, I forgot to lock this” that often leads to silent data corruption. ↩

0 views

Shoppy

Meet Shoppy ! It's a helper app for my recently revived shopping list , with which I'm hoping to grow the dataset for categories prediction. In fact, even early beta tests have made Shoppy significantly more savvy about alcoholic drinks (the initial data comes from my own shopping, and my entire family happens to be non-drinkers). See if you can confuse it about something it doesn't know! But besides that, there's a few deeper philosophical and technical notes I wanted to share. It's a very, very simple Django app . When I first had the idea to build it I entertained some thoughts about trying some front-end based technology, because, you know, it's an "app"… But then after actually thinking about what it's going to be — a handful of static screens and a couple of forms — I decided to go the familiar way. Now I have a small, view-source 'able HTML app which I'm proud to offer as an example of how you can build something interactive without the layers of modern front-end technology. If you're new here, simplicity is kind of my thing in software engineering. Although it's really hard to convince people to do simple. Trying modern CSS after a long break felt really exciting! Nested blocks, variables, complete control over the box model, new useful units (like ), and niceties like — all of these made my life much simpler. I was especially impressed with which allowed me to make speech and form bubbles flexible. Without it, trying to make text of variable length look nice in a fixed-size bubble caused me a lot of frustration. For layout, I tried flexbox and grid, but they didn't really work for me. It's my own fault, really. You see, ever since I bought into the idea of separating the roles of markup and style, I dislike adding extra structure to markup purely for styling convenience. Markup needs to mean something! And the one thing that grids and flexboxes really like is having straightforward container s with stuff inside of them. But what I have is a which consists of naked , , and , in this order — and that's just not enough structure to say "this goes here, and that goes there". So I ended up with good old absolute positioning and some paddings around Shoppy's avatar. CSS variables really do shine for things like this. And! It was my first time making a responsive layout that looks nice both on mobile and desktop! Tell me if something is broken on your particular setup. The model is a mapping from "terms" to categories . I learned to build such things while working on the Search team at Shutterstock, and their simplicity still amazes me! Here's how it works: You get a search query, like "Honeycrisp apples". You split it into words, stem them and sort them, which gives you — a predictable set of keys independent of morphology and the input order (they're called unigrams). Then you generate all two-word combinations (called bigrams) from this set, which in this case gives you just , and add them to unigrams. And then you look up each of the search terms in the dataset and pick the entry that comes the earliest. In this case, there's only one: . But there's a few non-obvious tricks it lets you do: You don't need to list all the apple varieties, unknown words are simply ignored, and you just recognize any apple as produce. But what of "apple juice"? For that it has an entry , which is deliberately placed before the apples, so it gets picked up instead. In fact, what it means is that "any kind of juice is a drink, regardless of what it's made of". Same goes for "oat milk " (drink), " diced tomatoes" (canned products), etc. Now think of "apple sauce". "Apple" is produce, "sauce" is (usually) a condiment. But "apple sauce" is a snack! This is where bigrams come into play: the bigram entry comes before both and , which resolves the conundrum. (In fact, all of the bigrams must come before all the unigrams, because they're always more specific.) There's some more to it all, and there are downsides, but I won't go any deeper right now. It's 2026, so I can't not talk about it, can I? Generative AI happened to the world right in between of me first coming up with the idea of category prediction and having a chance to actually implement it. And I admit of having thoughts that may be there's no point in building your own model for such a thing now. After all, just ask any LLM "which grocery category is dill weed" and it will tell you… a lot of text with several variants, which you can't really use in a precise manner :-) So of course I went back to my own idea, because it's much, much simpler. And local. And free. And ethical. Luckily, the simpler solution doesn't really lose on feeling magical and intelligent. I've seen people play with the app and really engage with it, and be impressed! One of the testers, when trying to come up with a random grocery item for the first time, said, "There's probably a million of them!" It doesn't matter that my entire model is just around 500 entries, it still feels like it knows much more simply because people overestimate the size of the problem :-) You see, I can process photos, I can do business graphics, and I'm known to have put together a few toolbar icons in my time… but for the life of me I can't draw! And even if I could, I'm particularly hopeless at coming up with what to draw. So I commissioned the graphics from an artist , who also introduced me to the concept of "object shows" and the whole OSC fandom . Not sure I'm joining as a fan yet, but I'm definitely very happy with the original character of Shoppy! Oh, and the background. You get a search query, like "Honeycrisp apples". You split it into words, stem them and sort them, which gives you — a predictable set of keys independent of morphology and the input order (they're called unigrams). Then you generate all two-word combinations (called bigrams) from this set, which in this case gives you just , and add them to unigrams. And then you look up each of the search terms in the dataset and pick the entry that comes the earliest. In this case, there's only one: . You don't need to list all the apple varieties, unknown words are simply ignored, and you just recognize any apple as produce. But what of "apple juice"? For that it has an entry , which is deliberately placed before the apples, so it gets picked up instead. In fact, what it means is that "any kind of juice is a drink, regardless of what it's made of". Same goes for "oat milk " (drink), " diced tomatoes" (canned products), etc. Now think of "apple sauce". "Apple" is produce, "sauce" is (usually) a condiment. But "apple sauce" is a snack! This is where bigrams come into play: the bigram entry comes before both and , which resolves the conundrum. (In fact, all of the bigrams must come before all the unigrams, because they're always more specific.)

0 views

Something’s Rotten in the State of macOS Icon Design

This is an iconic observation : If you put the Apple icons in reverse it looks like the portfolio of someone getting really really good at icon design This isn’t, however, just the story of Apple’s Creator Studio icons. It’s the unfolding story of icon design across the entire macOS platform. For example, take a look at some of Apple’s other apps like iMovie : Or Remote Desktop : Apple sets the standard (and the rules) for how icons look on the Mac. Wherever they go, so goes the ecosystem — and they’re taking the entire ecosystem along down with them. It’s fast becoming the case that if you put any Mac app’s icons in reverse, it looks like the portfolio of someone getting really, really good at icon design. Even Microsoft — not exactly a bastion of design — starts to look pretty decent with their icons the further back you go. For example, with OneNote , the app icon’s progression looks like it went something like this: Some 3rd-party apps continue to fight a good fight, even as Apple’s definition of what an icon should be — or what’s even possible — shrinks all around them. Apps like Capo (remember, these are reverse chronological ): Or BBEdit : Or Fantastical : Or Cot Editor : Everyone’s being put in a box squircle. The imposition is real. I don’t blame any of the 3rd-party app makers. Their designs have to play by Apple’s rules (or end up in icon jail ). World-class designers like Matthew Skiles or The Iconfactory are still out there striving for excellence, even as they’re hamstrung by the Mac’s latest rules. When it comes to icon design on the Mac, the sky is no longer the limit: Apple’s icon design sensibilities are. They set the examples of what world-class icon design should look like, but what do you do when the examples are no longer exemplary? Reply via: Email · Mastodon · Bluesky “I made this with AI” “I tried to make the AI one, but by hand myself” “I don’t need to be constrained by this squircle” “Hey, I’m getting better at this”

0 views
iDiallo Today

Don't call yourself a Software Engineer, you are an AI Enabled Engineer.

I can only imagine what it's like learning the skill of programming in this day and age. What does an average college class look like? What is the CS professor teaching? And students, how do you reconcile what you are learning vs the current job market? I know if I was a student today, I would at least attempt to make connections on LinkedIn to prepare for a future where I would need those connections to get a job . But LinkedIn is not a real place. It might have been at one point. But it certainly isn't today. Everybody is an AI Engineer/Leverager/Prompter/Professional. If I wasn't in the industry, it would be real confusing. I see my ex-colleagues right there on LinkedIn, gainfully employed, yet still felt the need to update their titles. We've worked together, I know their current role. Or at least I thought I knew. Now, that same role leverages AI in a way that has "enabled" them. The software engineers though, their role has remained the same while the entire marketing department has switched to AI first. The software engineer is being left behind . The whole company is moving fast, yet the person writing the code using Claude, Cursor, Codex, Copilot, that person remains a mere Software Engineer. In some cases, this person even calls himself a Software Developer. It reminds me of the days we used to call ourselves programmers. We reluctantly accepted the title of software engineer, feeling that imposter syndrome knowing that our code isn't as reliable as it looks. Patrick Mckenzie wrote something short of a manifesto to convince us to market ourselves as Software Engineers . Yes, we were just building CRUD applications for our employers, maybe an expense tracker and accounting software here and there. It didn't feel very impressive. But companies didn't care about the technical elegance of our code. They were just happy to get such a valuable product that helped them save money and increase revenue. It's not hard to see how an engineer can create business value in the real world. Software is eating the world after all, and everything runs on software today. When you call yourself a programmer, you undersell your competence and the value you create. You are a problem solver, you are a business contributor, and you directly affect the company's bottom line. We don't need convincing anymore. We are Software Engineers. But I think we missed the point of Patrick's essay. Maybe we stopped reading a little too early. Maybe we didn't read anything past the title. While I think his argument for not calling ourselves programmers is convincing, I think the meat of it was in the career advice he offered. What he was trying to convince us is that the outcome of our work is what's important, not the form. To grow in this field of tech, the programming language and framework we used had little relevance. Instead, he talked about attending conferences and meetups, blogging and participating online, helping people, building professional relationships. These are things software engineers still avoid today and then wonder why they have a hard time getting the next job. He spoke on the value of communication as a skill, salary negotiation, navigating politics at work. Only those who read past the title benefitted from this advice. I know I did! But Patrick is nowhere to be found in this new landscape we find ourselves in. AI is eating the world now. The workplace is transformed and there are no signs of ever going back. What is the career advice for the year 2026? If you are on LinkedIn, you must have noticed everyone is changing their work title. Is that what we are supposed to do? Did I miss the memo? I didn't read Patrick's latest manifesto, if there are any. But let me get ahead and suggest a few AI enabled titles to get you started. If you were a backend engineer, you are now an AI Platform engineer. If you worked as DRE, you switch to Decision Intelligence Pipeline. If you ever updated the cron job, well, my friend, now you know about Autonomous Agents. If you had the word blockchain in your title, it's even easier. You can make a one to one replacement from blockchain to AI. I'm still working on that list, but I don't think it will have the same impact as going from programmer to software engineer. The title was just semantics. The real value remains in the career advice. And I think Patrick's advice remains just as effective today as it did so many years ago. If you want to grow as a software engineer, don't worry about mastering React. Instead learn the fundamentals of programming and you will have no trouble navigating the different environments. Experience comes from working anyway . Don't stop there. Meet people. Join those conferences, blog about your experience, help others, make those connections with people. In fact, send emails and reply to emails. Hop on a call. You will be surprised how much easier it is to get an interview when you aren't application #234 in a list of one thousand. In fact, you can close this tab and read Patrick's essay. Pretend that the title is "Don't call yourself a Software Engineer." Ps: Patrick was not harmed in the making of this article. He wasn’t even consulted. Ps2: I understand many won't read past this title as well, but that's just filtering at work.

0 views

Data Center Discontent, Understanding the Opposition, Fixing the Problem

There are understandable reasons for people to oppose data centers; the only solution that will work is simply paying them off.

0 views

Web whetstones

How do you stay sharp as a web developer and/or designer? I’ll share my advice below. I’m also looking for front-end folk to advise me too. What are your whetstones? That is to say: sources of news and knowledge to level up professionally. Does that metaphor work? We’re sharpening our minds, and I suppose the web too with our minds… are minds the whetstone here? Moving swiftly on, in rough order of preference: People love to declare “RSS is dead” because they’ve chosen the likes of Google to gate-keep their web access. Interesting choice, but RSS remains alive and well. When I discover a new blog and like what I read, I’ll subscribe. There’s a good chance that person will write something useful again one day! Funny how that works. I don’t flood my reader with big sites that exist to generate content. I collect personal blogs that may only post once a year. That’s still plenty of unique insights as the list grows. I won’t share my list because I feel for RSS to work you have to curate it yourself. Shop Talk Show has been number one forever. Syntax remains a decent source if you’re deft with the fast-forward button (it’s a little ‘sloppy’ these days.) Igalia Chats is packed with wisdom. For a Better Web is Bruce Lawson in your ears. Wonders of Web Weaving from James is new and hopefully a regular listen. I’ve unsubscribed from too many podcasts that pivoted to AI servitude which is disheartening. I’m not adverse to such discussion but the level of mindless platitudes and gigglefests about what their wacky chat boxes said ain’t my cup of tea. Mastodon and Bluesky is where I follow folk in the web industry. Socials can be a great whetstone if you manage your follow list carefully. Everyone uses these platforms for different reasons which can be difficult to balance. Personally I stick to shop talk and mute politics for example. I follow individuals and rarely organisations to avoid “brand engagement”. Email newsletters are useful to catch stuff I’ve missed. Many exist in RSS form too. My favourites are typically link dumps with a side of commentary. Current favourites: Sidebar still has the odd gem if I care to sift through the “AI” links. Newsletters are a declining category for me. Perhaps because I keep getting unsubscribed by those with failed tracking pixels. Email costs money to send so I’ll accept my loss. Lobste.rs , Hacker News , Reddit (e.g. web dev , experienced devs , frontend etc). Does dev.to have any humans left? These forums are a good source of links — if you can filter the bot spam and avoid the cesspit of comments. Toxicity spreads and it’s all too easy to get dragged in. Sometimes you just have to let people be wrong on the internet. I’ve heard these still happen! I only leave my house now to scavenge for essentials so I don’t have much to say. Clearleft events are guaranteed value if you’re in the UK. Some conferences have online tickets but I find the in-person socialising to be the main benefit. Everything listed above is (or has) a website. I’m poor at organising and utilising bookmarks. I’ll manually visit bigger blogs like CSS-Tricks and Smashing Magazine once a month to see if anything interests me. I bookmark a handful of YouTube channels like Kevin Powell because I have no Google account to “smash that subscribe button” . YouTube isn’t my thing though. I have an allergic reaction to algorithm driven content. I don’t use Discord but I hear it get promoted often. Are these communities lively or are they a ghost town? That’s my problem with Discord. It’s a blackhole for information; antithetical to an open web! Am I missing out? Not sure I care. For no particular reason I’ll end with this quote from Seth Rogen. “I don’t understand what it’s supposed to do. Every time I see a video on Instagram that’s like, ‘Hollywood is cooked,’ what follows is, like, the most stupid dog shit I’ve ever seen in my life,” he said. “And if your instinct is to use AI and not go through that process, you shouldn’t be a writer, because then you’re not writing.” Seth Rogen Says If “Your Instinct Is to Use AI” to Write Scripts, “You Shouldn’t Be a Writer” - The Hollywood Reporter P.S. no more blog posts until June. I’m due a holiday! Thanks for reading! Follow me on Mastodon and Bluesky . Subscribe to my Blog and Notes or Combined feeds. Frontend Focus Design Systems News

0 views

many people's business is really not that impressive

I've recently participated in an entrepreneurship workshop hosted by my university, which focused on idea generation for a startup. I have absolutely no plans to do that, and I think I am the wrong person for this, but I wanted a brief glimpse into the mind of people in that space. We tend to be complete opposites, and I wanted to know and understand how they think and what makes them tick. I didn't last longer than 30-40 minutes in it; the language around all of this just sounds very cringeworthy to me. Anything very hypercapitalist, neoliberal, sales-y just gives me the ick. All I saw was very naive people misled by the idealization of "being your own boss" our society has, without a plan or a problem to solve. It was still impossible for me to empathize and see what they see. Once in a while, I think about how many businesses are just a step between you and the source and just take over a single step for you (if at all) and it baffles me. It weirdly seems illegal when it’s not! As if someone’s gonna come along and say: Why would anyone pay if they can just buy directly? But that's not how it works; that's just the child view of it. As a kid, I had a view of all of these jobs like doctor, vet, police man, fireman, builder etc., who are the source and/or who do a lot of the process. The person looking to build a house on their plot of land can usually not just build a house themselves, it makes sense to hire a company to do it to source all the materials, experts and workers. I understand wedding planners for really complex weddings. Then there’s the grocery store, that gives you easy access and presents it all well for you, factories that produce the products from different raw materials, experts that can interpret the numbers/laws/etc. better than you can as a layperson, and so on. Later on, you realize lots of companies just buy the finished product for cheap and slap their logo onto it, or wrap their own packaging around it, or just dropship. They do practically zero work, aside from offering more of the same. Which is fine, I guess, but I can’t really put it into words how different it all used to seem to me. As if everyone produced things themselves and is a great innovator and problem solver. They still try to sell that idea to these wannabe startup people, meanwhile they will probably just slap their own logo onto a pair of pantyhose that is produced in the same factory as all the other ones, or take an app idea from a competitor and change the interface slightly. Now when you read about someone’s business venture, they tend to just get someone else to do everything; some coders brag that they get clients, just to subcontract it out to some poor person on Fiverr or just let AI do it, and get paid for that anyway by unsuspecting customers. So often, it seems to be more about getting inbetween another company/freelancer and their (potential) customers to get a piece of the pie, instead of creating your own solution. Not even necessarily about making things easier, or creating a new product, but being the one that is found quicker (via socials, search engine etc.) and mediating. Sometimes, you can still catch me being disillusioned about this; like when some girl online announced she bought a matcha farm, and my stupid ass imagined her actually on the farm coordinating the growth, harvests and all, taking care of machinery; meanwhile in the end, all she bought was a piece of the harvest to be turned into matcha in her own branded tin. She doesn't run the farm, she at best invested into it as a shareholder. I cannot be the only one that is caught off guard sometimes when you see what someone's "own company" ends up being many times, and how overstated it all is? Just ask women on dating apps; 9 times out of 10, the dude that's telling them about being the boss of their own company is just a crypto bro, reseller/scalper or creating sloppy AI wrappers. They'll go on about having made something of themselves and created something "from scratch" and it ends up providing zero value to the world, piggybacks directly off of someone else's work and is neither new or impressive. What gives? Reply via email Published 18 May, 2026

0 views
Unsung Today

Less doesn’t need more

A simple rule for overflow logic that will prevent your app looking a bit stupid … = 3x)" srcset="https://unsung.aresluna.org/_media/less-doesnt-need-more/1-framed.1600w.avif" type="image/avif"> = 3x)" srcset="https://unsung.aresluna.org/_media/less-doesnt-need-more/2-framed.1600w.avif" type="image/avif"> …is to avoid “more” expanding to just one item. If the expansion takes up as much room as the UI to show the expansion, why not show it in the first place? #details #interface design

0 views
Unsung Today

“Easy to use,” the hard parts

A reader asked me this, and I thought I will answer here: One bit of challenge with [where I work] is that my audience isn’t already almost-guaranteed to be into design! They’re hopefully interested in making good software in general, though, and probably curious about the app [I’m building] they could be using. I’m also afraid that developers sometimes confuse “easy to use” with “beginner-only, limiting”, which makes it harder to write about streamlining UIs; there’s only so many times you can invoke “reducing mental load.” I love this question because it gets to the core of why I started this blog. I’m perennially unhappy with the conflation of “craft” with “delight,” and the subsequent narrowing of “delight” into “cute strings and slow animations.” In the famous words of Steve Jobs, “Design is not how it looks. Design is how it works. What’s tricky is that they’re sometimes related, and even if you learn how to tell the difference, your exec team probably never will.” I am quoting from memory. Anyway, I hope spending time on Unsung – please like and subscribe – helps with examples of what to talk about and how to talk about them. But, just to list some alternatives to “reduces mental load” for well-made software that come to my mind: There’s more, and I am curious what comes to your mind and how you all connect with developers! But maybe just going through a list like this will provoke some ideas. (Of course, if you cannot honestly claim some of these about software you’re working on, and you think they’re important – I guess you have some work to do.) #storytelling is more efficient gets you home earlier will allow you to spend more time on things you enjoy will allow you to choose which parts of the problem to spend time on reduces tedium understands and practices progressive disclosure understands you speaks your language learns your preferences meets you where you are is made by people like you respects you will make you better at what you do rewards mastery doesn’t dumb things down will teach you concepts helpful in other software never takes control away from you is easy to customize adapts to you doesn’t disobey you will make you look good in front of others respects history and legacy of the space is built well is conceptually/​systemically beautiful is well-considered or thoughtful

0 views

A phone battery experiment

I’ve done all sorts of experiments over the years when it comes to my phone usage. From cutting down my screen time as low as possible, to not using the phone at all, to running it in black-and-white mode, and many others. But this morning I woke up, unplugged my phone from the charger, and I thought «I wonder if I can only charge my phone once a week» . That was a thought half-asleep me had without realizing that what I was actually thinking about was charging it twice a week, not once. So starting the week with a fully charged phone and only plugging it in once until the next Sunday night. I believe it can easily be done, and it might even be doable to use one full charge for the whole week, so not plugging my phone at all for the next seven days. Experiments are fun, and there's only one way to find out, so I’m going for it. I have a Pro Max with a healthy battery that is currently sitting at 100%, and I have put it in low power mode to give myself the best chance. We’ll see how far into the week I’ll make it before I have to charge it again. Thank you for keeping RSS alive. You're awesome. Email me :: Sign my guestbook :: Support for 1$/month :: See my generous supporters :: Subscribe to People and Blogs

0 views

Ironman Training Diary - May 18, 2026

A lot of pushups and dialing back my run pace

0 views
Max Bernstein Yesterday

Travel notes: RubyKaigi Hakodate

I just got back from a three and a half week trip to Japan. It was the longest trip I have ever been on (aside from studying abroad in Germany, which felt different). I made the following wild circuit with only a backpack and a duffel: This trip was split into three parts: time with my immediate family, going to a conference, and then time with my partner. They were all great and also I am glad to be home. I’ll post my abbreviated travel notes here, including activity and food recommendations. We started in Tokyo but we were only there for about 40 hours. We focused our time mostly on arts and crafts: we did a kintsugi workshop, spent time at an artists cooperative, and then did a lot of walking around. This was a good intro to the trip, because everyone kept waking up at 4am and crashing at 7pm due to the jet lag. 4am wakeup makes for nice morning walks to 7-Eleven. I brought my family to T’s Tantan in Tokyo Station because I’m vegetarian and it’s otherwise hard to find ramen that approaches kosher in Japan. It continues to be great and I really appreciate having a steady vegetarian option available. Many years ago when I visited Tokyo there was a place that served a delicious tomato-based vegetarian ramen, but I hear it has since permanently closed. Bummer. We took the shinkansen to Kanazawa. I love the train. It’s fast. It’s quiet. You can eat your snacks on board and gaze out the window as the world whizzes by. It’s nice. We toured a soy sauce factory (meh; they don’t let you in the room where the magic happens) and the old town (pretty!) before finally eventually ending up at our small hotel in Toyama: Satoyama Auberge Maki No Oto. I highly recommend this hotel. It is beautiful, the staff is lovely, the food was excellent, and they were very accomodating of me being vegetarian. We continued on to Toyama, which is a port town. We got to talking with an older local guy who told us all about his favorite local spots. We learned after leaving that this guy has extraordinarily fancy taste and they were all either Michelin starred or at least Michelin rated and with a lead time of months. We opted to instead go to a local brewery, which had a ghost pepper beer (!) and pizza. We then moved on via train to Osaka, where we transferred to a car to head (eventually) to our hotel in the hills near Nara. We toured the Daimon sake brewery. They explained every little thing about the process, which was especially interesting to me, as I’ve done some small amount of homebrewing and I bake. They sounded similar. We had a tasting and even got to talk to Daimon-san. I recommend going. I also recommend the Akame 48 waterfalls walk/hike, which has some exquisite falls, and Murou Art Forest. They had some really wonderful installations. My brother and I parted ways from the rest of my family in Osaka: they headed further west and we headed north to Itō on the Izu peninsula. We got a surprise perfectly clear view of Fuji along the way. It’s beautiful there. They don’t seem to welcome foreigners in a lot of their restaurants (we were turned away several times) but one place had a guy who enthusiastically welcomed us in. We ended that evening enjoying a some food and a beer while also being stared at by a 300lb completely tattooed guy. It was a little unsettling but we left without incident. My brother and I made our way to Tokyo for the day before his flight and before my train north to Hakodate for RubyKaigi. I once again did that thing where I walked around in humid 80F heat with a large backpack and pants and was extraordinarily warm toward the end of the day. After about a liter of Aquarius on the train north I felt better. I stayed at Yunokawa Prince Hotel Nagisatei which I would like to especially call out for having an enormous, diverse, and very vegetarian friendly breakfast. Every morning I got to try new and tasty things and even feel full after. It was great. Hakodate is beautiful in the spring. I arrived at peak cherry blossom season and Goryokaku, their star shaped fort, is absolutely decked out in cherry blossoms. It is also moderately swarmed by tourists (in this case, three cruise ships). It didn’t feel over-crowded though. I enjoyed eating at The Bear King which had a vegetarian friendly option. The next day was the committer meeting. I don’t remember a ton from it other than people talking at length about the semantics of deep freezing an object (do you freeze its class? its class’s superclass? …?). I picked up my badge and also got to check out my colleague Chris Salzberg’s bar SOLENOID ! It’s a neat spot. I headed out to go find some dinner. This is about when I got a message on my phone that there was going to be an earthquake, so I walked back into the bar and said “hey, did you get this?” just before everything started shaking. It was the biggest earthquake I’ve experienced, but I was metaphorically not too shaken up. Then we got the tsunami warning. Chris’s bar is already something like 8 meters above sea level and at the foot of Mt Hakodate. With the city sirens going off and the police directing traffic with batons, though, I decided my best bet was just to march directly up the mountain to get more elevation. Since the tsunami wasn’t scheduled to arrive for about 20 or 30 minutes and my hotel was across the sea-level part of town, I parked myself on a little concrete post. Chris found me eventually. Someone told us that there was a middle school offering refuge, so we went and hung out on the side of the gymnasium. They were really nice about it. On Wednesday, the conference started. It was really well signed and organized. My usual complaint with conferences is that there’s nothing to eat for vegetarians (or that we get mashed with the gluten-free people and each group only gets a salad and bad bread) but that did not happen! They had really stellar vegetarian bento. They had a lot of leftovers toward the end of lunch so I even went and got a second. This was about when I started freaking out because my speaking slot was approaching and I wasn’t yet feeling my talk. Normally when I give a talk, I get up in front of people and I pace and gesticulate and productively complain and throw in some fun anecdotes and the audience, one way or another, ends up learning about JITs at scale, or Scheme semantics, or something. It’s what I’d done for my little lunch talk at Brown two weeks prior. I even titled that talk One must imagine compiler engineers happy so there was plenty of room for educational complaining. But this RubyKaigi talk was in front of an enormous crowd and toward a more general audience than I was used to addressing. The slides did not feel like they were flowing until about twenty minutes before my talk. In the end it went alright. I realized about 40 seconds in that I had way too much content so I ended up speaking rapidly for 30 minutes straight, completely unaware of the audience (which you can’t see anyway because of the lights). I only really noticed people when I made a dumb six-seven joke and Aaron laughed. The rest of the conference I was able to relax and enjoy other people’s talks. I got some good hallway track in, too. I think there’s a good group of people who are interested in Ruby tracing (for example, Perfetto in ZJIT ) so maybe we will make something happen. We had a nice small dinner at Yasai Bar Miruya , which was vegan (!) and had some nice sake. The host was very friendly, too. I nerd-sniped John and J into implementing a VM for the Universal Machine . This was a daunting homework assignment back in undergrad but it was a fun project later in life. S joined toward the end of the conference. She’s also vegetarian so we got some really excellent vegetarian ramen at MAIDO Ramen . Finally, S and I headed south on the shinkansen for Nikko. Nikko is small, beautiful, and a tourist day-trip town. Dinner closes early. Shops close earlier. Since we were staying there we had to make sure to track down and visit the one or two vegetarian places before they shuttered. S and I, along with J and J, took the bus up from Nikko, up the windiest switchbacks, to the Kegon Falls. We were going to take a boat across the lake, but the water level was too low for the dock on the other side, so we ended up half hiking and half taking a bus. Then we continued our hike through the Senjōgahara Marshland (beautiful), to the Yudaki Cascades (lovely), which also had a surprise restaurant and ice cream shop at the base! It’s called Yutaki Rest House . After some great (vegetarian friendly!! wow!!) udon, we marched up the waterfall and around Yuno Lake at the top to Yumoto Onsen. In order to make the last reasonable bus back to town, we just enjoyed putting our feet in the foot bath. One day was rainy. In the evening, J and I thought it would be fun to continue our Universal Machine implementations. As Norman Ramsey would say, “my implementation is 90 lines long and runs sandmark in under six seconds.” We also enjoyed doing a tour of the shrines right above Nikko. The shrines are resplendent against the backdrop of forest. Pro bus tip: you can either pay by IC card or credit card. No need to grab a ticket if you do that. S and I shipped our bags (thanks, Yamato) before continuing on to the small town of Moka, the staging area for our big pottery festival day. Unfortunately, there was no good way to get there: there was no reasonable series of trains and no taxi would take us. Ultimately we ended up taking the train to Utsonomiya and catching the long local bus to Moka. About twenty minutes into this ride, in the middle of nowhere, bus nearly empty, the bus driver pulled over and ran over to us looking kind of panicked. He asked where we were going and was visibly relieved when we said Moka. I suppose we are not the usual riders. Very nice of him. Upon arrival, S introduced me to CoCo ICHIBANYA, which is also super vegetarian friendly. I loved it. We ate really well before walking to our tiny hotel. We did not really know what to expect from the Mashiko pottery festival. The internet said it would be crowded and to arrive early, so we got up at 6:30am for estimated 7am departure on the tiny train from Moka to Mashiko. On most trains you can pay with an IC card but we were out in the sticks so we asked the only other guy on the platform how to pay for the train. He said he had no idea and that this was his first time here. When the train showed up completely packed to the gills and we had to (politely) push onto it, we started to realize that this was The Event and it was going to be mayhem. Also, fun fact: the way the Moka train payment works is that you grab a little ticket from the train, and, upon arrival, wait in line to present your ticket to two very overwhelmed looking people at a table, who charge you, and you pay in cash. Onto Mashiko: the festival was packed . There’s pottery everywhere the eye can see. There are tents and there are full buildings. It varies in quality and artistry from fine to jaw-droppingly spectacular. You could completely stock your kitchen from this fair alone and it would even be cost-effective. The main bummer for us is that we had to get pottery safely back home. We limited ourselves to a reasonable assortment but we really wanted to buy a beautiful painted 20 inch plate with a bird on a branch. After a ton of walking around, we took another long long bus back to Utsonomiya and continued onto Karuizawa. We didn’t know what to expect from Karuizawa but, having been, I could probably concisely describe it as “Aspen for people from Tokyo”. It was… fine. We loved our hotel, Tsuruya Ryokan. The manager was very excited when we borrowed a Studio Ghibli DVD from their collection. We continued on to Tokyo, our final stop. We our usual tour of stationery stores and bakeries—the bread was something to write home about (har har). We enjoyed a (vegetarian!! friendly!!) kaiseki meal at Hyoki Shabu-shabu Ginza before enjoying some live music at Rocky Top . We also recommend Jikasei MENSHO for vegetarian ramen. Bakery checklist: We had an uneventful and reasonably easy trip home. Whew. Long post for a long trip. See you next year in Miyazaki! BOUL’ANGE NIHONBASHI (check! good croissants) Bricolage bread & co (check! good everything) Brasserie Viron Marunouchi Beaver Bread Bricolage bread & co. Bartizan Bread Factory Gontran Cherrier Tokyo Aoyama Shop Comme’N Tokyo Shiomi Bakery The Little BAKERY <!– https://www.jocjapantravel.com/kanto-tokyo-bakeries/ –>

0 views
Phil Eaton Yesterday

Serving files over HTTP three ways: synchronous, epoll, and io_uring

This is an external post of mine. Click here if you are not redirected.

0 views
Sean Goedecke Yesterday

The just-say-no engineer was a ZIRP phenomenon

The engineer who says no all the time is a real archetype among senior and staff engineers. Their role is to slow things down, to block the development of features that add complexity, and to ensure that as little code gets written as possible (since code is a liability). We can think of this as the just-say-no engineer 1 , as opposed to the just-say-yes engineer. The just-say-yes engineer is obsessed with moving fast, approves code changes by default, values MTTR over MTBF , and tends to ship a lot of code. The just-say-no engineer is obsessed with quality, is happy to move slowly, and blocks code changes by default. Most engineers are somewhere in the middle of the spectrum. By “just-say-no engineer”, I’m talking about the group of engineers who most strongly identify with that archetype. The just-say-no engineer is having a hard time in the era of AI. It used to be that they only had to say no to more junior engineers’ handwritten PRs, but now they have to say no to a barrage of AI-generated code, some of it generated by managers and VPs who are politically difficult to say no to. For the first time in their careers, they’re under a lot of pressure to lower their standards and start saying yes. However, this isn’t because of AI. It’s because of the end of ZIRP. ZIRP, or the “zero interest rate policy”, is a shorthand for the era of software development between 2008 and 2022 when banks were allowing companies to borrow money at near-zero interest rates. During this period, investors were throwing borrowed money at anything , which meant that tech companies were incentivized to constantly hire engineers for low-risk high-reward projects 2 . Successful companies would routinely grow from tens of engineers to thousands, who would go and work on all kinds of things: tangential open-source projects, endless technology migrations, rewrites into other languages, and so on. It was a great time to be a software engineer. We had a lot of bargaining power, and could get paid top dollar to do almost anything. The bosses largely didn’t care, because (a) teams were growing so fast they couldn’t pay attention, and (b) just having more engineers around was beneficial to the stock price, which was the main thing they cared about. But tech companies did have one problem: with so many engineers running wild, how would they keep their systems from becoming completely unmanageable? Enter the just-say-no engineer. In this environment, having a very senior engineer whose only job is to say no to things was actually quite valuable to the company. There are a few reasons for this: When banks hiked interest rates, almost every tech company immediately laid off 5-20% of their engineers. It was just no longer profitable to keep a bloated engineering staff around to boost the stock price. Instead, companies had to actually make money 3 . However, that wasn’t a good public explanation for the layoffs, since it sounds weak to admit that you were paying hundreds of engineers to do unprofitable work. Fortunately, the end of ZIRP coincided roughly with the rise of ChatGPT, so tech companies were able to to blame their layoffs on the power of AI. Saying “with this transformative new technology, we’re able to deliver 10x the value with half the engineers” is a much stronger message, even though it doesn’t make much sense (if this is true, why not keep your engineers and deliver 20x the value?) Something like this dynamic has been happening to the just-say-no engineer. Tech companies are now more focused than at any time in the past two decades. They are not doing a bunch of random crap anymore; instead they’re desperately chasing new capabilities and features that can make money (mostly built on AI, for obvious reasons). This new environment is actively inimical to the just-say-no engineer. It’s as if a shark got pulled out of the deep ocean and dropped into a fast-flowing river: what was once a powerful apex predator is now disoriented and flailing. This kind of engineer used to enjoy implicit (albeit distant) support from their management. If someone complained, they’d often get told “that engineer knows what they’re doing, if they said no, then I trust them”. Now that support is gone. The just-say-no engineer is now being criticized and actively overruled by their management. They’re being told to be more of a team player, to find a way to say yes, or are simply no longer being consulted (with the company’s blessing) on key decisions. They’re getting bad reviews for the exact same behavior that’s been rewarded pre-2022 4 . None of this depends upon AI. If LLMs had not taken off this decade, we would still be seeing the same cultural shifts in the industry. Companies would still be laying off engineers, and the engineers whose job has been to say no to things would still be upset and confused about why they’re now being punished for saying no. Ironically, if ZIRP had not ended, this would be a glorious moment for the just-say-no engineers. LLMs would have thrown fuel on the “engineers running wild” problem that the just-say-no engineers were empowered to solve. Tech companies, unable to publicly or privately cast doubt on AI-assisted coding 5 , would have relied heavily on these engineers to prevent the tsunami of AI code from swamping the entire company. They would have been paid even better and celebrated like kings. Instead, LLMs are adding insult to injury for the just-say-no engineer. They’re forced to watch while other engineers merge AI-generated PRs that would previously have been blocked, and are told to use the tools themselves: to become the kind of engineer they’ve spent their entire careers battling against. Worse still, the AI tooling mostly works . It’s not (yet) causing any kind of catastrophe 6 . The code isn’t quite as clean, and it’s a bit less well-understood, but it’s good enough (particularly in a world where companies are trying lots of new things and abandoning the ones that fail). So the just-say-no engineer faces not just a threat to their livelihood, but to their entire self-identity: they have to either insist that the apocalypse is right around the corner, or accept that their technical role was contingent on a really weird economic environment in the tech industry. Will the just-say-no engineer go extinct? No. They don’t fit well into every single tech company anymore, but there are domains where they’re needed. In Pure and impure software engineering I drew a distinction between “pure” engineering, which has a well-scoped, largely technical goal (like building a compiler or a language runtime) and “impure engineering”, which has a poorly-scoped, largely customer-driven goal (like trying out a new feature you’re not sure will work). During the ZIRP era, tech companies did a lot more pure work (for instance, building React ), and tended to treat even impure work like pure work. The just-say-no engineer is great for pure work, because pure codebases have to have a much higher bar for quality and can tolerate slower development cycles. Most tech companies are still doing some kind of pure work, typically in their core infrastructure pieces. This is essential work, but it doesn’t require a huge engineering team, and it’s rarely in the spotlight . If you’re a just-say-no engineer and you want to stay that way, I would recommend trying to move into one of these roles (and accepting that you’ll have a more limited scope than you did in the 2010s). This was a critical role during ZIRP, because: Part of the appeal here is the lure of the guru. In kung fu films, those who know martial arts perform furious acrobatics, but the true expert barely needs to move at all. For the same reasons, it sounds profound to say something like “junior engineers produce tons of code, seniors very little, and staff engineers remove code”. Of course this is false. Staff engineers are expected to be able to produce a lot of working code very quickly, when they need to. I wrote about this a lot more in The good times in tech are over . Not necessarily make a profit , but at least bring in revenue. Or pre-2023, or even pre-2024 or 2025. Cultural change lags behind economic incentives, sometimes by several years. For fear of killing the vibe (and thus the stock price). If you think there have been more incidents recently, consider that (a) you might be wrong , or (b) that other end-of-ZIRP factors (like increased velocity or layoffs) might be primarily responsible. Having half of the company’s engineers enmeshed in an endless loop of proposing changes and being told no was totally fine - they didn’t need to be productive anyway, and this way they weren’t impacting business-critical systems. It also solved the problem of the 5% of engineers who would get drunk on their technical freedom and make wild proposals like migrating to a hand-rolled database. Having a reputation for a very high technical bar is a positive for hiring (and remember, during ZIRP every tech company was always hiring) Some senior and staff engineers operate as gatekeepers, slowing down development and saying no to most things This was a critical role during ZIRP, because: Tech companies had thousands of engineers who were empowered to do basically whatever they wanted, so without gatekeeping the systems would have fallen apart Tech companies didn’t care that much if they got anything done When ZIRP ended, the environment for this kind of engineer became much worse, since tech companies were now actually focused on accomplishing things and the “do whatever you want” era was over Like with layoffs, this shift is often blamed on AI, but it would have happened even if powerful LLMs had not emerged at all. It’s an end-of-ZIRP phenomenon Part of the appeal here is the lure of the guru. In kung fu films, those who know martial arts perform furious acrobatics, but the true expert barely needs to move at all. For the same reasons, it sounds profound to say something like “junior engineers produce tons of code, seniors very little, and staff engineers remove code”. Of course this is false. Staff engineers are expected to be able to produce a lot of working code very quickly, when they need to. ↩ I wrote about this a lot more in The good times in tech are over . ↩ Not necessarily make a profit , but at least bring in revenue. ↩ Or pre-2023, or even pre-2024 or 2025. Cultural change lags behind economic incentives, sometimes by several years. ↩ For fear of killing the vibe (and thus the stock price). ↩ If you think there have been more incidents recently, consider that (a) you might be wrong , or (b) that other end-of-ZIRP factors (like increased velocity or layoffs) might be primarily responsible. ↩

0 views
Blog System/5 Yesterday

A Markdown-based test suite

This article is not about AI and it is not written with AI, but the work that I’m about to present was definitely motivated by AI. And because I generally like telling stories, I have to give you that background. Do with that whatever you want, but… it’d be a pity if you left just because the AI word showed up in the first paragraph! I think the technical explanation that follows is at the very least entertaining and also interesting independently of AI. Back in December, I started toying with coding agents. One thing I tried, and for which I didn’t expect a lot of success, was to point an AI agent to the EndBASIC public documentation and ask it to write games like Space Invaders or Mario from scratch. And even though the results weren’t perfect and they didn’t work on the first try, they did work with a few tiny tweaks. Combining that with a bunch of hand-written rules, I had an agent producing EndBASIC demos with ease. This experiment was impressive because I did not expect an agent to be able to write EndBASIC code… and because it worked, it fueled my interest to pick EndBASIC’s own development back up. Three thoughts came to mind: Increase EndBASIC’s “self-documenting” aspects so that an AI agent can learn about its idiosyncrasies unsupervised. Speed up EndBASIC so that it can run more elaborate games. Extend EndBASIC with long-desired primitives like sprites and sound, to finally realize the vision behind the project. These thoughts combined sparked the rewrite of EndBASIC’s core that I’ve been pursuing since January and which should see the light of day in the upcoming release. But before that happens, I want to talk to you about just one of the cool pieces behind the new core: namely, its approach to testing. I’ve stopped writing unit tests for the compiler and VM in Rust and I’ve switched to writing them in Markdown. And I believe this has turned out to be a pretty nice approach. One of the things I had to do to convince an AI agent to write proper EndBASIC code was to hand-craft a bunch of rules to tell it how EndBASIC differs from other, more traditional BASIC dialects. That worked OK, but writing these rules by hand was error-prone and difficult to make exhaustive. So I wanted to let LLMs extract that information directly from EndBASIC. The idea was simple: if I wrote the integration tests for the new core in Markdown, the lingua franca of AI, the tests would serve as the canonical and correct documentation demonstrating language behaviors. LLMs are great at summarizing information, so if I unleashed them over a large set of these hands-on “examples”, they would probably figure stuff out, right? And they actually do! I gave the following prompt to GPT 5.4: Based on your pre-existing knowledge of BASIC dialects, I want you to read all of the files, analyze how the EndBASIC dialect differs from your knowledge, and come up with a bunch of rules for yourself to know how to write EndBASIC code later on. You can ignore the Disassembly sections. Beware that all functions and commands in these integration tests are test-only: the real functions and commands that you can use are documented in , so read those too to learn what functionality is available. Write your findings to a file. And this produced a very comprehensive file with spot-on rules: here, take a look . But leaving that aside, let’s peek into the internals of this new Markdown-based test suite. All cool so far? Want to see more similar content in the future? Subscribe now to demonstrate your interest! It’s a collection of Markdown files: Where each file acts as a container of one or more test cases : Every test case has a section title describing what the test is about and various subsections to define the test scenario: A Source code block that is the input to the compiler. If compilation fails, a Compilation errors section with the error messages and nothing else afterwards. If compilation succeeds: A Disassembly section that contains the compiled bytecode. An optional Exit code section showing the program’s exit code, if different from zero. An Output section that contains any messages printed to the console by the executed program. A Runtime errors section that contains any errors from the executed program. Here is a simple example validating the command: There is no section to validate the lexer nor parser internals right now but I’m considering to further extend the format and dump the AST too in order to simplify the tests for these components. The driver for this test suite enumerates all Markdown files in the tests directory and processes them one at a time. For each file, the driver extracts all test case titles and their Source subsections to compute all the test cases to execute. Once the driver has this subset of information from the Markdown files, the driver feeds each individual test case to the compiler and, if compilation succeeds, to the VM. All side-effects are captured and the driver emits a new Markdown file from scratch with the results of the test. Once the driver has terminated producing a new version of the Markdown file for a test, the driver compares the produced file (actual) against the pre-recorded, checked-in version (golden). If they differ, the test fails and the driver uses the tool to print the differences. And that’s it. Easy peasy, right? This keeps the driver super-simple as the only thing it has to do is parse a minimal subset of Markdown, and the diffs it produces are trivial to understand to a human. There are currently 448 test cases and 13k lines of Markdown in this test suite so maintaining them “by hand” is not an option. You wouldn’t want to implement an optimization to the compiler and then have to rewrite hundreds of disassembly chunks in the golden files to reflect the changes, would you? The thing is that, due to the design described earlier, regenerating the golden files after a core change is easy: the driver is already doing exactly that to execute the tests! The trick is, simply put, to ask the driver to rewrite the golden file instead of producing an actual file by setting the environment variable. And voila: all golden files are regenerated in place. I can then use Git to validate the changes and commit them along with the actual code change. Let’s start with the pros of this Markdown-based test suite framework: It is much easier to work with than what I had before. I used to dread touching the compiler and VM of the previous EndBASIC core implementation because tweaking tens of tests was painful. Changes required me to fiddle with positions and deeply nested types, and now the tests are trivial to tweak and diff against previous state. Pretty much any decent text editor has Markdown support, including formatting fenced code blocks. This makes it easy to skim through the test suite and modify the files and is actually the primary reason I used Markdown instead of a bespoke textual format. LLMs can “learn” with ease. OK, fair, this is just a guess: I did not try the same prompt at the beginning of this article against the old core with its Rust-based tests, and maybe the LLMs would have done a good job at reverse-engineering the rules. But because the Markdown tests are so much easier to read by humans, I have to assume that they also are for LLMs. And now, of course, some cons: Regenerating the output of a test, or all tests, is way too easy . With the older Rust-based tests, I was forced to manually punch in things like line numbers and nested AST trees. This process forced me to think through the changes in detail. With the new approach… regenerating the golden files is trivial, so it’s easy to miss little mistakes in source positions or disassembled code. Differences in disassembly are usually noisy and hard to review because every line carries an address and thus any new or deleted instruction will introduce offsets into all other addresses. I could of course choose to not include the instruction addresses in the dump, but they come in handy when manually validating jump targets, so it felt better to keep them around. Rust cannot generate first-class test cases on the fly which means that the various test cases within a Markdown file are “invisible” to the driver: I can run them all or none, but regular test filtering via doesn’t apply. I was able to “expose” the different Markdown files as different Rust-native test cases, but this involves a hardcoded list of test files—which must be kept in sync with the files on disk, and so I mitigated the chances of divergence by adding a test that cross-references the two. This idea does not generalize well. The Markdown-based test suite presented here works well for components where end-to-end testing is favorable and, more importantly, cheap , but I wouldn’t recommend it for other scenarios. Keeping tests fast is a must for quick iteration. And I think that’s about it. If the above feels too abstract, I encourage you to take a look at the driver , its helper code , and the directory with test suites . Now that you have this new trick up your sleeve, what do you think? Back in December, I started toying with coding agents. One thing I tried, and for which I didn’t expect a lot of success, was to point an AI agent to the EndBASIC public documentation and ask it to write games like Space Invaders or Mario from scratch. And even though the results weren’t perfect and they didn’t work on the first try, they did work with a few tiny tweaks. Combining that with a bunch of hand-written rules, I had an agent producing EndBASIC demos with ease. This experiment was impressive because I did not expect an agent to be able to write EndBASIC code… and because it worked, it fueled my interest to pick EndBASIC’s own development back up. Three thoughts came to mind: Increase EndBASIC’s “self-documenting” aspects so that an AI agent can learn about its idiosyncrasies unsupervised. Speed up EndBASIC so that it can run more elaborate games. Extend EndBASIC with long-desired primitives like sprites and sound, to finally realize the vision behind the project. A Source code block that is the input to the compiler. If compilation fails, a Compilation errors section with the error messages and nothing else afterwards. If compilation succeeds: A Disassembly section that contains the compiled bytecode. An optional Exit code section showing the program’s exit code, if different from zero. An Output section that contains any messages printed to the console by the executed program. A Runtime errors section that contains any errors from the executed program. It is much easier to work with than what I had before. I used to dread touching the compiler and VM of the previous EndBASIC core implementation because tweaking tens of tests was painful. Changes required me to fiddle with positions and deeply nested types, and now the tests are trivial to tweak and diff against previous state. Pretty much any decent text editor has Markdown support, including formatting fenced code blocks. This makes it easy to skim through the test suite and modify the files and is actually the primary reason I used Markdown instead of a bespoke textual format. LLMs can “learn” with ease. OK, fair, this is just a guess: I did not try the same prompt at the beginning of this article against the old core with its Rust-based tests, and maybe the LLMs would have done a good job at reverse-engineering the rules. But because the Markdown tests are so much easier to read by humans, I have to assume that they also are for LLMs. Regenerating the output of a test, or all tests, is way too easy . With the older Rust-based tests, I was forced to manually punch in things like line numbers and nested AST trees. This process forced me to think through the changes in detail. With the new approach… regenerating the golden files is trivial, so it’s easy to miss little mistakes in source positions or disassembled code. Differences in disassembly are usually noisy and hard to review because every line carries an address and thus any new or deleted instruction will introduce offsets into all other addresses. I could of course choose to not include the instruction addresses in the dump, but they come in handy when manually validating jump targets, so it felt better to keep them around. Rust cannot generate first-class test cases on the fly which means that the various test cases within a Markdown file are “invisible” to the driver: I can run them all or none, but regular test filtering via doesn’t apply. I was able to “expose” the different Markdown files as different Rust-native test cases, but this involves a hardcoded list of test files—which must be kept in sync with the files on disk, and so I mitigated the chances of divergence by adding a test that cross-references the two. This idea does not generalize well. The Markdown-based test suite presented here works well for components where end-to-end testing is favorable and, more importantly, cheap , but I wouldn’t recommend it for other scenarios. Keeping tests fast is a must for quick iteration.

0 views
annie's blog Yesterday

It’s either a poem or a piece of cheese // Week 20 — 2026

Are these weeknotes? Yes they are! Will I do them again next week? Who knows! Sunday 10 May: Got home from hospital shift around 7:30pm. Exhausted, hangry. Walked into a clean tidy home, flowers and cards, and the kids cooking dinner (spring roll bowls which were so so so good). Plus! a NEW CHAIR for the balcony. We ate and talked and did that thing where you laugh so hard you cry. Then I sat on my new balcony chair & had some nice bourbon while they cleaned everything up. Anyway it was a great Mother's Night 💗 More spaces in my life for uncensored unfettered thinking. Less platform, more workshop. Less stage, more garage. Less producing, more tinkering. Tuesday 12 May: Took a sick day. Felt off, sore throat, achy yesterday. Woke up with the full experience. This was to be an uncomfortably busy day and instead I am canceling all the things I can. Left with a couple of items to do from the comfort of the couch. Hot tea. Window open. Cats sitting in the sun. Breeze and blue sky outside. If I feel enough energy I’ll take a slow walk later. Dreamed about being evicted. Felt very real. Woke up panicked. Relieved to realize it was a dream and I have a two-year lease. Wednesday 13 May: Took my chemistry final. Not as difficult as anticipated! A relief, since I didn’t study as much as planned. “I want you to see all kinds,” he would say to her. “I want you to realize that this whole thing is just a grand adventure. A fine show. The trick is to play in it and look at it at the same time.” “What whole thing?” “Living. All mixed up. The more kinds of people you see, and the more things you do, and the more things that happen to you, the richer you are. Even if they’re not pleasant things. That’s living. Remember, no matter what happens, good or bad, it’s just so much” — he used the gambler’s term, unconsciously — “just so much velvet.” —from So Big by Edna Ferber Denial and suffering may be good methods for undoing the old / destructing but they are not good methods for creating / constructing what you actually wish to build. Thursday 14 May: Still sick. Tried to do a bit of work. Mostly just rested. Feeling somewhat better but end of day. Friday 15 May: Mara’s college graduation day. Those two years have flown by. Many feelings! So proud of her. Saturday 16 May: Lily’s birthday! A weekend full of celebrations. Took her and a group of friends to one of those combo bowling / laser tag / arcade / overstimulation places. They did all the things & had fun. I got some studying done. But is it doable? Sunday 17 May: Hiking church. Warm today, 70℉ when we started. Chubb Trail from West Tyson. It is a painful confession but the art of poetry carries its own power without having to break them down into critical listings. I do not mean that poetry should be raffish and irresponsible clown tossing off words into the void. But the very feeling of a good poem carries its own reason for being.  …primarily Art is its own excuse, and it’s either Art or it’s something else. It’s either a poem or a piece of cheese. —from On Writing , Charles Bukowski 💪 One gym session (Monday) before the sickness took me out Tues-Thurs, then it was A Weekend of Events. Back to our regularly scheduled program next week, I hope. 👟 A few short walks, and a nice hike. 📺 Unfamiliar (loved it) and season 1 of The Thaw (liked it, will watch the rest). Lots of tv time with sick days. 📚 So Big by Edna Ferber (finished) and On Writing by Charles Bukowski. 🔗 The old world of tech is dying and the new cannot be born // Baldur Bjarnason No matter the flavour of Christianity, a core idea baked into every aspect of the religion is that singular revelatory events can fundamentally change the world. There’s the “before”. Then the “event”. Then an “after” that has been completely transformed. In Christianity itself this is usually associated with Christ’s chaotic transit schedule –  “He is here! He has left! He is about to arrive again! Now he’s leaving again! But he’s also somehow always been here! And not.”  – but the mode of thinking is common throughout literature, philosophy, and storytelling in the Christian west. 🔗 Letting things build // Tracy Durnell The way I often read non-fiction — snatches of twenty pages here, twenty pages there, putting a book down for two months (or two years) at a time — is  not conducive to *finishing* books, but I do find it conducive to thinking . Rich texts can take a while to sink in, so I’ll jump to another book while I let the first one marinate. 🔗 You are here // Sebastian As I approach my topics and ideas through writing—whether in the form of brief notes or by looking back when I pick up the journal and flip through its pages—a process of contextualization takes place. And that is important. For me, this is a form of metacognition: observing myself as I think and being able to analyze and categorize my thoughts “from the outside.” It doesn’t completely solve the black box problem of self-perception, nor does it eliminate the blind spot of the mind that seeks to explain itself from within itself, but it does make things a lot easier and more accessible.

0 views

Another rant about web browsing

Yes, I’m writing again about my ongoing experiment with blocking JavaScript on a per-site basis. This time, I’m not here to explain how I operate in detail , but to complain about the work needed to maintain this web browsing hygiene. In short, the web is a mess, and while messy things can be fun , I’ve recently grown very frustrated with the need to dance around my extensions every time I visit a new site where displaying simple text apparently requires JavaScript, or where scrolling requires dismissing a cookie modal that is only visible if content blockers are turned off first. I’ve come to the conclusion that blocking JavaScript by default on all websites, as I’ve been doing lately, is a source of frustration. Yes, the web is light as a feather and my browser feels very fast when it doesn’t have to deal with all the JavaScript, I do love that. But this “strategy” breaks too many websites, pushing me to take detours so often that they can barely be called detours any more. You see, I can’t be bothered to manage an efficient “allow” list in the long run, so web browsing often feels like a series of new obstacles, as if every day is the first day of this setup. *1 This strategy is therefore a bad one. Just as bad as the other strategy I tried before, the one where I only block JavaScript after visiting the site, if it feels necessary. My discipline with that second strategy tends to fade away as days go by, and I end up barely ever blocking anything, even forgetting that this is something I can do. This strategy often encourages me to download a proper content blocker or use a filtering DNS. Not only are these strategies inefficient in their initial goal of making my web browsing experience better , but they are also only work on the Mac. On iOS, due to the way Safari extensions work — which is a bit shitty — neither of the two strategies for blocking JavaScript on a per-site basis is practical to use, pushing me to adopt another strategy just for my phone (which, in turn, makes everything feel so much more complex than it needs to be). On the iPhone, accessing the settings for each Safari extension is already complicated, but there seems to be no way to manage a per-site setting if the extension is not recognised as a content blocker and if it is set to “allow on all websites”. With StopTheScript for instance, I can only manage the per-site setting if I set the extension to “ask”. Also, per-site settings only seem to sync between the phone and the Mac if the extension is a content blocker. *2 So, if I were to rate my JavaScript-off web browsing strategies, taking into account the browsing experience itself (the way the websites look and behave), the impact on my computer’s CPU (if the fan turns on or not, if it lags), and the amount of maintenance required (having to manage exception lists): JavaScript-off by default, allowing a few selected sites permanently, visiting others temporarily in a private tab (where the extension is inactive): 8/20 JavaScript-on by default, managing the JS-off list extensively but facing the terribleness of raw webpages: 6/20 Both are bad strategies, but the truth is that none of the alternatives I’m thinking of are better. For example, using a full content blocker like Wipr is a frustrating experience in itself. Having to manage another list of sites and constantly refreshing pages with or without content blockers is a pain. That, and the fact that it seems to be a heavier workload for my old Mac, as are third-party browsers. Content-blocker-enabled Safari, managing the exception list and dealing with a laggy computer: 7/20 Third-party browser , like Helium, Quiche Browser, or Orion, combining content blocking and a neat JavaScript toggle (uBlock Origin is pretty great at both): 7/20 Naked browser, meaning no content blockers, no JavaScript limitation, no list to manage, nothing to do, just the natural web: 1/20 I think the best setup is the following, even if I’ll stick with strategy 1 for a while: The main issue with strategy 6 is that I’ve had issues with these DNS resolvers, like not being able to access common websites for hours, even my own website, resulting in a quick investigation only to realise that everything was working fine and that the issue was with the DNS resolver. This is the state of web browsing in 2026, terrible at best. Allowing JavaScript, blocking JavaScript, whatever; either way the experience is bad. Most websites are stuffed with invasive ads, surveillance tracking, dickpanels , noise, and junk. Nothing we can do really works, unless one spends hours fine-tuning everything and therefore adds extra layers of complexity. The more effort I put into filtering the filth, the more ready I am to give up at the first little hiccup. It doesn’t feel right to reload a webpage three times to view it properly and to take the time to ensure it’s properly set up for future visits. While content blockers and JS toggle tricks are improving things drastically, the added amount of work required is a pain in itself. The browser on one side, the extensions on the other. The more we consume websites as the filling in a sort of software sandwich, the more they resist. The thicker our bread, the more sauce they add. The more bread we bring to absorb it, the more junk they add to the filling. At what point does it become too disgusting to eat? Meanwhile, reading articles outside the web browser , via email newsletters or within my RSS reader, is pure bliss; a delightful, gourmet, delicious cuisine that stimulates my appetite instead of making me want to throw up. It’s so good that I don’t even need extra bread. *3 It just works. Just like it’s increasingly better to search for an answer using an A.I chatbot rather than a traditional search engine, it’s now better to read articles from websites by using apps that are not traditional web browsers. It feels wrong, like driving on the smooth cycle lane rather than a pothole-filled road. How long can this situation last? Between difficult business models  — the source of most problems, driving us to use content blockers in the first place — and new A.I. chatbot intermediaries , I don’t know what will happen to the web in the next three or four years. Some web browsers are already in a weird spot . In the meantime, I will keep overthinking this, as I want my next laptop to inherit a “final” and well-thought-out setup, developed on this early 2020 MacBook Air. Its lack of a powerful chip and its limited memory forces me to face the inefficiency of overloaded webpages and third-party browsers. Maybe I’m obsessing a little too much about this. Or maybe I need to sleep more . “Allow” or “deny” list, depending on whether we talk about JavaScript being on or off, or the extension blocking it. The vocabulary around content blockers and extensions like StopTheScript confuses me in terms of negation.  ^ Some extensions like StopTheMadness can be configured without relying on Safari per-site settings, but decentralising and maintaining two competing lists is pretty much the opposite of what I want.  ^ I do disable JavaScript in NetNewsWire though, just to be safe.  ^ JavaScript-off by default, allowing a few selected sites permanently, visiting others temporarily in a private tab (where the extension is inactive): 8/20 JavaScript-on by default, managing the JS-off list extensively but facing the terribleness of raw webpages: 6/20 Content-blocker-enabled Safari, managing the exception list and dealing with a laggy computer: 7/20 Third-party browser , like Helium, Quiche Browser, or Orion, combining content blocking and a neat JavaScript toggle (uBlock Origin is pretty great at both): 7/20 Naked browser, meaning no content blockers, no JavaScript limitation, no list to manage, nothing to do, just the natural web: 1/20 Strategy 2, with a DNS resolver like Mullvad or NextDNS, effectively blocking most crap without making my laptop choke. 9/20 “Allow” or “deny” list, depending on whether we talk about JavaScript being on or off, or the extension blocking it. The vocabulary around content blockers and extensions like StopTheScript confuses me in terms of negation.  ^ Some extensions like StopTheMadness can be configured without relying on Safari per-site settings, but decentralising and maintaining two competing lists is pretty much the opposite of what I want.  ^ I do disable JavaScript in NetNewsWire though, just to be safe.  ^

0 views
Lalit Maganti Yesterday

Which country voted the best at Eurovision?

Eurovision was on yesterday. I’ve never been interested much in the musical side but the weird political dynamics of Eurovision voting have always fascinated me; I tune in each year just for them and somewhat snarky commentary of Graham Norton, the UK commentator. As I was watching the jury votes come in, a question popped into my head: Which country has voted the best in Eurovision? That is, which country was best at picking the eventual top 10 and in the right order? Strangely enough, while there’s plenty of work on voting blocs and bilateral biases at Eurovision, most of it asks who votes for whom ; I wanted to ask who votes accurately . I couldn’t find anyone asking the question that way, so I decided to do some data analysis myself. To begin to answer this question, I first needed to formalize what “best” even means. That is, some mathematical notion of “good” and “bad”.

0 views
ava's blog Yesterday

i suspect there is a new uber data leak

For the last 2 weeks, I have received 2FA codes for my UberEats account that I did not request roughly every couple days. The account was only created a year ago and used twice, then never again. The email address and password were created specifically for it and are not used elsewhere. No other accounts and services I use are affected. A quick search shows others have been dealing with the same very recently (~3 weeks). I logged in and tried to start the account deletion process. It ends in a white screen with no confirmation, and you get no “Sad to see you go” email or anything else. If you’re lucky, you’re forcibly logged out and take it as a sign that it worked. That’s a shitty process. The info says they deactivate for 30 days and then fully delete, and I had hoped that would stop the 2FA requests, but alas, it did not. They still let you attempt log in and send a code, and I have no idea if that stops the deletion process or resets the 30 days. As I did not want a random person thwarting my account deletion, I once again logged in, changed all personal information and the password, and started the account deletion again (same bullshit). I have contacted their privacy team to let them know. We’ll see what they say. I also requested them to delete my account in case the deletion process failed. I haven’t yet seen any notice about this on their website or in the media. 🤷🏻‍♀️ If you have an Uber/UberEats account, consider changing your password. Reply via email Published 17 May, 2026

0 views
Karboosx Yesterday

How to actually handle database transactions (and why your ORM fails at it)

Wrapping database calls in a simple try-catch block is just asking for trouble. Here is a practical look at how to handle transactions properly, avoid the dreaded ORM lost update, and keep your data actually safe.

0 views