Full Width [alt+shift+f] Shortcuts [alt+shift+k]
Sign Up [alt+shift+s] Log In [alt+shift+l]
1

Humanities Crash Course Week 44: Modernist Art

from Jorge Arango [alt+shift+b] in technology

Week 44 of the humanities crash course had me delving into three modernist classics by two Irish authors: Samuel Beckett’s absurdist play Waiting for Godot and selections from James Joyce’s Dubliners and Ulysses. I’d long been intimidated by the latter, but enjoyed the first episode. (I’ll likely finish the novel next year, after I’m done with the course.) I also watched a movie that was both absurd (in the best sense) and appropriate to Halloween. Onwards! Readings Per Gioia’s recommendation, I only read the first episode of Ulysses. I won’t summarize it here, since it didn’t give me a sense for the overall story. I’ll just say I loved how Joyce played with language. While it’s clearly prose, it has the musicality and color of verse. I’m actually looking forward to reading the rest of the novel. I also only read the last story in Dubliners, titled The Dead. A couple — Gabriel and Gretta Conroy — attend a dinner party hosted by the Gabriel’s aunt. After many sentimental reminiscences, they go home. Before turning in, Gretta emotionally recalls her first love: a young man who died many years before. This triggers in Gabriel an epiphany about love, memory, and mortality. Waiting for Godot is the most famous modernist play. It consists of two acts and has a sparse minimalist setting and only five characters. The main ones are Vladimir (“Didi”) and Estragon (“Gogo“), two drifters. They meet under a bare tree in a road to wait for someone called Godot. They’re unsure why they’re waiting or who Godot is, but expect he’ll help them. Eventually, two other men arrive: Pozzo and Lucky. The former has enslaved the latter, driving him leashed like a dog and grossly mistreating him. We learn Pozzo is on the way to a market to sell Lucky. Lucky says nothing until Pozzo orders him to dance and think. He dons a hat and a long stream of academic-sounding gibberish pours from him. After Pozzo and Lucky leave, a boy arrives. He claims to be a messenger from Godot, and he tells...
2nd Nov 2025

Stay updated

Get a weekly newsletter with the top 5 articles worth reading every week.

More from Jorge Arango

Traction Heroes Ep. 44: WEIRD People

Are you more of an analytical or a holistic thinker? The answer might depend on the culture in which you were raised — and will impact your ability to gain traction. In episode 44 of Traction Heroes, Harry read a short segment from Joseph Henrich’s The WEIRDest People in the World, which argues that people in “WEIRD” societies — Western, Educated, Industrialized, Rich, and Democratic — tend to think more analytically than holistically. I haven’t read the book, but do believe our culture influences our worldview — including how we think of timeframes and outcomes. If we’re driven by quarterly results, we’ll have a very different orientation than if we have a 10-year outlook. Learning how culture affects our perspective changes how we understand ourselves and how we communicate with others — and both affect our ability to gain traction. Traction Heroes episode 44: WEIRD People

2 weeks ago 1 votes
What’s the Purpose of Information Architecture?

It’s been fifty years since information architecture’s coming out party in Philadelphia. Alas, the discipline is still widely misunderstood. The main obstacle? The very thing that brought it to many people’s attention: the World Wide Web. I’d venture most people who’ve heard the phrase ‘information architecture’ have done so in the context of designing website navigation systems. That’s understandable: The web is a huge deal and IA was exactly what it needed early in its development. But IA has more to offer than its contributions to UX design. To understand why, consider its ultimate purpose. This is how I describe it: The purpose of information architecture is increasing agency by making systems more legible. Let’s unpack this statement. The word agency is loaded now. We talk of agentic systems to mean those powered by AI agents. But here, I mean ‘agency’ in its original sense: giving an actor (human or otherwise) scope to decide and act independently. Systems are the scope we’re acting on. What is a system? A collection of parts that relate to one another in particular ways so that the whole can serve a purpose. A website is a system. So is a business. Systems can have subsystems. Many websites are subsystems in service of broader business systems, which are in service of broader social systems. Architects are always aware of the broader context; we operate holistically. As Eliel Saarinen put it, Always design a thing by considering it in its next larger context — a chair in a room, a room in a house, a house in an environment, an environment in a city plan. IA makes systems more legible. That is, the actor who’ll use the system will be better able understand what to do with it to accomplish their goals. Think back to a time when you had to use a complex, unfamiliar product. You scanned its user interface for recognizable labels and symbols, looking for the smooth handle. If you’re like me, the system’s illegibility led you to YouTube, the web, or an LLM for an explanation. IA fixes that — or at least aims to make the process less onerous. To recap, the ultimate purpose of IA is increasing agency — enabling actors to make good choices — by making systems more legible. Let’s stress-test this claim against some real-world applications: A website’s navigation structure should give users recognizable labels that allow them to find their way to the part of the website that has the information they need. The user is the agent; the labels give them clear choices that allow them to make the right decisions about where to click. A spreadsheet gives an executive the information they need to understand how their part of the business is functioning. The executive doesn’t need all the data; that might drown the signal in noise. But the right data shown at the right time and place will allow them to make good business decisions. Your car’s speedometer tells you how fast you’re going, allowing you to remain compliant with traffic laws. Again, a car generates lots of data. Modern dashboards are carefully designed to put the most important front and center: speed, fuel/energy levels, etc. — ‘most important’ being the ones you need to make critical decisions. A map makes physical environments more understandable by collapsing their scale and details into a set of abstractions you can hold in your hand. Reading the map lets you make better decisions about which roads to take to get to your destination. A carefully structured prompt allows a large language model to work properly. You could point the LLM to a broad corpus, but that wouldn’t improve its performance. The point of ‘context engineering’ (which is awfully close to IA) is giving the LLM the right information at the right time with the right instructions so it produces useful outcomes. One of the most common questions I’ve gotten post-LLMs is, “Why do we need information architecture now that we have chatbots?” This is old-school thinking. “For the world wide web” was a phase of information architecture’s development — just a phase. It’s never been more important to re-embrace the discipline’s broader origins and aspirations.

7th Aug 2026 2 votes
After Forty Years, Still No Silver Bullet

Forty years ago, computer scientist Fred Brooks published a paper called No Silver Bullet: Essence and Accident in Software Engineering. As its title implies, the paper argues there are no technological shortcuts to making software radically easier, simpler, or more reliable. You may think AI is the ultimate silver bullet. It isn’t. Moore’s law was in full force in 1986. Hardware was getting more powerful, faster, and cheaper. Surely, some technology would come along to do the same for software. Brooks argued this wasn’t in the cards, since software is fundamentally different from hardware. For one thing, it’s of a different order: The essence of a software entity is a construct of interlocking concepts: data sets, relationships among data items, algorithms, and invocations of functions. This essence is abstract, in that the conceptual construct is the same under many different representations. It is nonetheless highly precise and richly detailed. Specifying, designing, and testing this construct is difficult. The challenge isn’t implementation but design: “We still make syntax errors, to be sure; but they are fuzz compared to the conceptual errors in most systems.” Technical advances usually make development easier. No Silver Bullet traces the history of time sharing, unified programming environments, and high-level languages. Object-oriented programming was a promising new technology at the time and there were even rudimentary AIs in the form of expert systems. Brooks examines them and concludes they’re not enough. Why? Because coding isn’t the hardest part of making software. Instead, the hard part is knowing what to build: The hardest single part of building a software system is deciding precisely what to build. No other part of the conceptual work is so difficult as establishing the detailed requirements, including all the interfaces to people, to machines, and to other software systems. No other part of the work so cripples the resulting system if done wrong. No other part is more difficult to rectify later. What will the system do? How will it serve strategic objectives? How will it enable better judgment and allow people to derive meaning from data? These aren’t implementation questions, they’re design questions. Somebody must define the “construct of interlocking concepts” that define the system, aiming for good fit between the system and the context it serves. LLMs can help, but they can’t replace human understanding and judgment, at least not yet. Brooks calls out four inherent properties of modern software systems: Complexity: Software systems are among the most complex human constructs. They’ve only gotten more so as computers and operating systems have grown more powerful and capable. Conformity: Software solutions must conform to the goals, needs, constraints, and interfaces of the organizations that bring them forth. This is true whether it’s bought off-the-shelf or developed bespoke. Changeability: Anything that lasts does so because it’s able to adapt to changing conditions. Software is inherently more malleable than other complex designed systems, such as buildings. Invisibility: Whereas complex physical systems (again, think of buildings) can be represented with mechanical drawings, software specs are inherently abstract. This makes them hard to design. There’s been progress in the last four decades, but these properties remain fixed. LLMs haven’t changed that. Non-deterministic components add immense complexity and unpredictability to software systems. The ease, speed, and volume of code generation make software more malleable and opaque than ever. And LLMs promise to ease bespoke development, tempting orgs away from one-size-fits-all solutions. Which is to say, LLMs haven’t changed the nature of software. Instead, they’ve made it more so. So far, the technology’s killer application is developing software: teams can now produce more software, faster. (It’s unclear yet whether it’ll ultimately be cheaper, especially when you consider maintenance costs.) What LLMs haven’t done yet is replace software wholesale, at least not for tasks that require predictable behavior. And as their true costs and constraints become evident, it’s increasingly doubtful they will. Instead, LLMs will likely become part of systems that include traditional deterministic components — both inside the systems and as part of the development process. The resulting systems will be more complex, malleable, and abstract than prior ones. They’ll also be better fit to purpose than off-the-shelf solutions. But that requires design, which remains primarily a human challenge. And it’s hard: it is really impossible for clients, even those working with software engineers, to specify completely, precisely, and correctly the exact requirements of a modern software product before having built and tried some versions of the product they are specifying. Replace “software engineers” with LLMs, and this sentence still stands. But it also hints at where LLMs come closest to being a silver bullet: in their ability to spin up rapid prototypes. Good software is grown, not built. That is, it evolves from an initial core to a more complex system through an organic approach that respects Gall’s law: The building metaphor has outlived its usefulness. It is time to change again. If, as I believe, the conceptual structures we construct today are too complicated to be accurately specified in advance, and too complex to be built faultlessly, then we must take a radically different approach. Let us turn to nature and study complexity in living things, instead of just the dead works of man. Here we find constructs whose complexities thrill us with awe. The brain alone is intricate beyond mapping, powerful beyond imitation, rich in diversity, self-protecting, and self-renewing. The secret is that it is grown, not built. So it must be with our software systems. What was true then is true now: technology moves the bottleneck from production to orientation. LLMs make coding easier, much like high-level languages, IDEs, and compilers did in the past. But without shared models, structured context, feedback loops, governance, and clear interfaces, they won’t provide the results leaders expect. As always, how to build gets easier — knowing what to build doesn’t. AI can help with that too — but it needs steering. The question isn’t “Which systems can we replace with AI?” Rather, it’s “How can AI help us grow systems that better fit our needs?” The answer will consider AI as a system component and a production tool. But forty years on, there’s still no silver bullet — just better ways to find good fit, faster.

10th Jul 2026 2 votes
Traction Heroes Ep. 37: Legibility

Many teams are being measured for the wrong things: tokens used, agents deployed, etc. Their orgs have focused on tech adoption rather than value creation. It’s a mistake. I wanted to discuss this with Harry, so I read a passage from one of my favorite books, James C. Scott’s Seeing Like a State. To my surprise, he’d read it too. I won’t cite the whole passage, but it kicks off with a familiar distinction: Isaiah Berlin, in his study of Tolstoy, compared the hedgehog, who knew “one big thing,” to the fox, who knew many things. The scientific forester and the cadastral official are like the hedgehog. The sharply focused interest of the scientific foresters in commercial lumber and that of the cadastral officials in land revenue constrain them to finding clear-cut answers to one question. The naturalist and the farmer, on the other hand, are like the fox. They know a great many things about forests and cultivable land. Although the forester’s and cadastral official’s range of knowledge is far narrower, we should not forget that their knowledge is systematic and synoptic, allowing them to see and understand things a fox would not grasp. Scott then unpacks how flattening an ecosystem to a few legible variables leads to a kind of myopia. This is assuming the variables are meaningful, as with land productivity for cadastral purposes. Token maxxing, on the other hand, is folly. Legibility — instrumenting processes so we can track progress — is essential for traction. But we shouldn’t focus on things we can measure (e.g., tokens used, numbers of agents created) rather than those that matter to the business. It’s harder to focus on the right measures when we’re acting urgently and/or from fear, as is the case for many teams now. How can we measure what really matters? That’s what Harry and I explore in this episode. Traction Heroes episode 37: Legibility

1st Jun 2026 1 votes
Traction Heroes Ep. 33: Perceptions

Here’s a tricky situation: you start reading someone through a negative lens, which changes how you interact with them. They respond in kind, which seems to confirm your negative views. Cue vicious cycle. In any situation, you are both observer and participant, whether you realize it or not. And often, you’re responding not just to the person in front of you, but to your story about them. This mind-bending topic was the subject of episode 33 of Traction Heroes. Harry brought a reading from Nir Eyal’s Beyond Belief to set up the conversation. Here’s one of the key bits: More often, it’s our brains creating problems because none exist. Since perception follows belief, we perceive the problems we look to find and if we can’t find them, our brain skews the data to fit the brief. If you believe your partner is constantly criticizing you, innocent comments transform into attacks. If you believe your boss doesn’t value you, any feedback becomes proof of your perceived inadequacy. This cycle becomes dangerous when it reinforces our negative beliefs, locking us into a belief-driven feedback loop that distorts reality and quietly builds a prison of our own making. I’ve been there, and I’m sure you have too. You may have even unwittingly flipped someone’s “bozo bit,” leading to a strain in the relationship that can be hard to undo. The question is: what can you do about it? As with so many other topics we’ve discussed in the podcast, it comes down to self-awareness: having the wherewithal to step back and realize you’re layering meaning onto situations. Easier said than done! For one thing, you want to perceive clearly to avoid misreadings. But you don’t want to lapse into paranoia, which can also cast a negative valence. Often, our misperceptions become obstacles to gaining traction. Surfacing them is a start, but we also explored practical suggestions in the podcast. Check it out: Traction Heroes episode 33: Perceptions

6th Apr 2026 1 votes

More in technology

FLIP Fluid on Flip Dots

[Hardware] Electromechanical Fluid Simulation

2 hours ago 1 votes
Is This A Joke? In The Auth Header? (F5 BIG-IP UnAuth Heap-Overflow to RCE CVE-2026-94127)

Well, well, well, well, well, well, well, well, well, well, well, well, well, well, well. We're back. Sorry. We've been watching the onslaught of vulnerabilities flood the internet. Every man, dog, and their grandmas (apparently?) are now using LLMs to find and reproduce vulnerabilities - it’

18 hours ago 1 votes
The Reason You Prohibit Things

You want less of them. That’s the reason. You may find that it’s too hard to stop people from doing the thing, literally blood, sweat, and tears trying to prosecute people, but that’s a different thing.

19 hours ago
Solitaire Alone Together

Solitaire Alone Together I made a new game. It's called Solitaire Alone Together. It's Windows 98 solitaire, but you can play with everyone else on the internet. Read the full post on my blog! Here's a raw link, if you need it: https://eieio.games/blog/solitaire-alone-together

3 days ago 1 votes
All The Ways I Broke My Website

This post is a living diary of all the times I messed up something with my website in a funny way. I value those who have the confidence to own their mistakes and share the learning with others, and so this is me doing just that! That Time I Accidentally Made a Tarpit That Time I Accidentally Made Really Large Headers That Time I Accidentally Made a Tarpit Back to Top A "tarpit" is an unofficial term used in computing to describe an intentionally slow response to a request. In these modern times many people are using tarpits as a way to combat the relentless theft of data by AI companies, although there's little to no evidence of that actually being in any way effective. I don't use tarpits, at least not intentionally, but there was that one time when I accidentally created a tarpit and trapped all visitors in it. As I've shared previously, I refuse connections from IP addresses that are blocked or belong to a blocked subnet, and I enforce this firewall during the TCP handshake. The logic here is straightforward: there's no reason to waste resources doing a TLS handshake, accepting an HTTP request, and then rejecting the connection if I already know I'm going to reject it at the earliest step. At the time, the code worked like this: the HTTP server would repeatedly call the Accept() function below expecting a new connection. I've added some comments to help explain the logic. func (l *firewallListener) Accept() (net.Conn, error) { // Accept the connection from the TCP listener. This blocks until there is a connection to accept or the listner was closed. conn, err := l.l.AcceptTCP() if err != nil { return conn, err } // Separate the IP address out from the remote address (which includes the port) ip := utils.SocketStringToIPAddress(conn.RemoteAddr().String()) if ip == nil { return nil, nil } // Check if it's blocked, if so close the connection and return a refuseError if IsBlocked(ip, true) { conn.Close() return nil, &refuseError{} } // Otherwise return the connection on to the HTTP server return conn, nil } If the incoming connection was from a blocked IP then I'd return a refuseError. I need to use a specific error interface because the HTTP server will halt if it encounters a non-temporary error from the call to Accept(), so I need to return an error that satisfies the definition of a temporary error. I defined refuseError like this: type refuseError struct{} func (e *refuseError) Error() string { return "." } func (e *refuseError) Timeout() bool { return true } func (e *refuseError) Temporary() bool { return true } func (e *refuseError) Is(err error) bool { return err == context.DeadlineExceeded } This did accomplish the goal of rejecting connections before the TLS handshake for blocked addresses, but it had one really unintended and difficult to track down side-effect. Accepting connections is done serially, after which servers typically then process that request on a dedicated thread (or in Go's case a goroutine). This means that any delays during the accept loop will block all incoming connection. What I had missed while reviewing the code for Go's HTTP server is that when it receives a temporary error from Accept() is that while it doesn't abort, it does sleep for up to a maximum of 1 second. This sleep blocks the entire server for all incoming connections. You can see a trimmed copy of the code that does this below, with some marks I've added which I will explain. // src/net/http/server.go // Copyright 2009 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. for { // (1) rw, err := l.Accept() if err != nil { if s.shuttingDown() { return ErrServerClosed } // (2) if ne, ok := err.(net.Error); ok && ne.Temporary() { if tempDelay == 0 { tempDelay = 5 * time.Millisecond } else { tempDelay *= 2 } if max := 1 * time.Second; tempDelay > max { tempDelay = max } s.logf("http: Accept error: %v; retrying in %v", err, tempDelay) // (3) time.Sleep(tempDelay) continue } return err } connCtx := ctx if cc := s.ConnContext; cc != nil { connCtx = cc(connCtx, rw) if connCtx == nil { panic("ConnContext returned nil") } } tempDelay = 0 c := s.newConn(rw) c.setState(c.rwc, StateNew, runHooks) // before Serve can return // (4) go c.serve(connCtx) } At mark 1 the server calls the Accept() function, this is the exact function that I defined above where I might return a temporary error. At mark 2 it checks if an error was returned, and if so if that error is temporary. If there was a temporary error, at mark 3 it sleeps for an increasing amount of time up-to 1 second, otherwise, at mark 4 it processes the connection on a dedicated goroutine, which allows the server to accept the next connection. I'm not entirely sure why the Go developers added this sleep delay and the change when it was introduced doesn't provide any meaningful insight. Regardless, it caused significant latency connecting to my website when a flood of rejected requests was coming in. It just goes to show how important it is to write meaningful commit messages, because you never know when somebody might come back years later wondering "why was this done?". I sure home I don't come to eat those words later. Coincidentally, you can actually see this happening if you look carefully at one of the metric graphs I shared in my first post about my server's security model: Securing My Web Infrastructure. This is the graph I shared in that blog post and while I didn't know it at the time, the fact that these request spikes all cap-out at around 60 requests per minute was not a coincidence. These requests were not being made with a limit in mind, attackers rarely ever care about things like that, instead it the accidental tarpit I had created. The downside to this was that while the malicious requests were being rate-limited, all requests were being rate-limited, up to a point of taking so long they timed out. The Fix Fixing the issue was relatively straightforward enough. Instead of returning a temporary error to the HTTP server during the accept loop, just don't return anything at all and wait for the next valid connection. func (l *firewallListener) Accept() (net.Conn, error) { for { conn, err := l.l.AcceptTCP() if err != nil { return conn, err } ip := utils.SocketStringToIPAddress(conn.RemoteAddr().String()) if ip == nil { return nil, nil } if IsBlocked(ip, true) { conn.SetLinger(0) conn.Close() continue } return conn, nil } } Now, when the HTTP server calls Accept(), the only time it returns is with a connection from an IP that isn't blocked, or if there genuinely is an error. No more sleep delays, no more excessive timeouts. That Time I Accidentally Made Really Large Headers Back to Top For about 10 years now all major browsers have support for a security feature known as a Content Security Policy or CSP. A CSP is an HTTP header provided by the server that instructs the browser on where it can load assets from, this could be scripts, images, stylesheets, fonts, etc. The objective of using a CSP is to prevent against injected HTML that tries to load assets, such as a malicious Javascript file, from a remote source. With so much user-provided content being available online, it's very possible for this to happen without an attacker compromising the entire web server. CSP protects against that by saying "scripts can only be loaded from these domains". That's a really simplified way of looking at it, anyways. My web server supports injecting the CSP header automatically, but before I go on I need to explain a little bit about the structure of my web server. When an incoming HTTP request is accepted (having passed all firewall checks and assertions), we look at the destination host for the request. This can either be the value of the Host header or as specified during the TLS handshake. We then look at a map of hosts to apps. Apps are just an interface that accept a few methods: type App interface { Cleanup() ReloadConfig() ServeHTTP(rw http.ResponseWriter, r *http.Request) Setup(dataDir string) error Shutdown() } One of the apps is the Proxy app, which is a reverse proxy - it accepts the incoming HTTP request and then proxies it on to another host. This is a very common design, especially with increasingly complex TLS setups. Because each app is unique to a host, and different hosts have different requirements for CSP rules, the proxy app includes a CSP preset that we use to build the header value, or skip it entirely. When the proxy app was going to copy an HTTP request to the downstream host, it would build the CSP header, however there was a slight bug... func (a *App) ServeHTTP(rw http.ResponseWriter, inRequest *ht2.Request) { // --snip -- if a.CSP != nil { a.CSP.ConnectSrc += " " + inRequest.Origin } CopyHttpRequest(inRequest, outRequest, rw, CopyHttpRequestOptions{ Origin: inRequest.Origin, Csp: a.CSP, Cors: a.CORS, AddHeaders: !a.SkipHeaders, UseHTTP3: a.UseHTTP3, InsecureTLS: a.InsecureTLS, }) } I'm really unsure as to what I was doing with the line to append to the ConnectSrc, but the impact is that I'm appending to a variable that lives on the App, rather than a variable that is per-request. This meant that every time there was a request to the app, any request at all, the origin would be appended to the header value. This went on for quite a long time unnoticed and unresolved, largely because I am constantly tweaking and tinkering with my web server, after all, it's how I made having a website fun again. Each time I restarted the server process, the header value would be reset, but only for it to continue to grow and grow. Eventually, after a period of being busy with other matters, the server process stayed running for long enough that the header value grew too large and HTTP clients began to reject it. There is no defined maximum for an HTTP header value, however most HTTP clients use 100KiB, which is perfectly reasonable, and this header value would continue to grow well beyond that. Diagnosing this issue turned out to be difficult as tools like Curl would fail with errors relating to entities being too large, but stopped short of saying what specifically. I eventually used openssl s_client to send an HTTP request by hand and observed my terminal window being filled with a domain name repeated thousands of times. Looking at the commit history, it was really unclear why I added the culprit lines of code. The commit message just says "Improved CSP support". It just goes to show how important it is to write - hey look it's those words I'm now having to eat! The Fix The fix was to just delete those three lines of code. Yup, it really was that simple, and fixing this bug actually made a larger positive impact than I had expected, as it was immediately clear when I fixed the bug by looking at outbound network bytes: So much traffic was being wasted on excessive header sizes. You might look at these mistakes I've made and think "wow, Ian, these are some obvious mistakes, I never would have made them!" to which I say "good for you!" with the utmost sarcasm and disdain. I enjoy making and refining software, and making anything means making mistakes along the way. Each time I make mistakes such as the ones above, I improve my skills of investigation, diagnosing, and repair. Skills that, judging by my peers in the industry, seemingly everyone is quickly willing to throw away because a robot does it "better" than you. Header Image: "Car accident on the Ffestiniog to Bala road. Nobody was hurt" by Geoff Charles, CC BY-SA 4.0, via Wikimedia Commons.

5 days ago
📚 BoredReading

You seem to be enjoying this.

Join free to unlock everything.

Create free account

Already have an account? Sign in