<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Iterative Tangents</title>
  <link href="https://iterativetangents.com/atom.xml" rel="self"/>
  <link href="https://iterativetangents.com/"/>
  <updated>2026-06-09T00:17:47+00:00</updated>
  <id>https://iterativetangents.com/</id>
  <author>
    <name>exupero</name>
  </author>
  <entry>
    <id>https://iterativetangents.com/two-git-scripts-to-encourage-good-development-habits</id>
    <link href="https://iterativetangents.com/two-git-scripts-to-encourage-good-development-habits"/>
    <title>Two Git scripts to encourage good development habits</title>
    <updated>2026-05-29T08:00:00+00:00</updated>
    <content type="html"><![CDATA[<p>I've written about various practices I have when writing or reviewing code, from <a href="/splitting-branches-using-diff-files">splitting branches using diff files</a> and <a href="/drafting-pull-requests">drafting pull requests locally</a> to <a href="/local-pr-review">reviewing code locally</a> and <a href="/exploratory-testing-on-pull-requests">documenting exploratory testing</a>. It's not always easy going those extra miles, so I try to make it as simple, reliable, and fun as possible by automating whatever I can. Here are two scripts I often use, both of which use <code>git</code> and <a href="https://github.com/junegunn/fzf"><code>fzf</code></a>. One helps keep commits clean, the other I use for testing tests.</p><h2>git-fixup</h2><p>I make a lot of small, frequent commits, checking in every time the code works and has a meaningful change I wouldn't want to lose. Then, if new work gets the code in a tangle or breaks something I don't understand, I can clear the mess by resetting the working directory to latest commit. Tiny and always-working commits also helps <code>git bisect</code>.</p><p>Occasionally commits need amending. Some commit in a long list of commits broke a feature or introduced a bug that didn't get noticed. Rather than just add the fix to that long list, I prefer to update the commit that introduced the problem. To do that, I add a commit whose message starts with &quot;fixup&quot;, then do an interactive rebase, which allows re-ordering commits and indicating that the fixup commit should be squashed into the commit before it. Then the original commit is cleaner.</p><p>While branches are often short-lived enough to rebase the whole branch, it's tricker to rebase when developing directly off the main trunk. One has to find the commit hash for the commit that needs to be amended then run <code>git rebase -i &lt;hash&gt;^</code>. To turn the process of finding a commit hash and rebasing into one step, I use this script:</p><pre><code class="bash">git log --oneline -50 \
    | fzf \
    | cut -d' ' -f1 \
    | xargs -I {} git rebase -i {}^
</code></pre><p>That lists the last 50 commits with their short-form SHA and commit message, and pipes them into <code>fzf</code> where you can search for the commit message of the commit you want to rebase onto. Select a commit by moving up and down and/or searching and hitting Enter. Then <code>git rebase -i &lt;hash&gt;^</code> will run with the commit you selected, which opens your default text editor where you can move the fixup commit to where it belongs in the list, mark it with an <code>f</code> for &quot;fixup&quot;, then exit and apply the rebase.</p><h2>git-regress</h2><p>When reviewing code that includes tests, I run the tests on my local machine to surface any missing environment variables, potential OS incompatibilities, test flakiness, and sometimes even tests that aren't picked up by the test runner.</p><p>I also want to see that the tests can fail. Sometimes the tests run but the assertions are broken, such as the case of the <code>.assertTrue</code> that forgot the <code>()</code> after it, preventing the assertion from actually being invoked. The easiest way to see new tests fail is to undo the application changes the tests are checking.</p><p>To undo a branch's logical changes but not its tests, use <code>git diff &lt;some-branch&gt;... -R -- &lt;directories&gt; | patch -p1</code>. That creates a reverse diff of specific files, then applies the changes.</p><p>To simplify finding the code to undo then patching, I use this script that gives <code>fzf</code> all the changed files on a branch, which lets me select the files to revert then applies the reverse diff of those files:</p><pre><code class="bash">mainBranch=$(git remote show origin | grep 'HEAD branch' | cut -d' ' -f5)
toRevert=$(git diff $mainBranch... --name-only \
  | sort -u \
  | fzf --ansi \
      --disabled \
      --bind 'j:down,k:up,q:abort' \
      --preview=&quot;git diff $mainBranch... -- {}&quot; \
      --preview-window=right:60%)

if [ -z &quot;$toRevert&quot; ]; then
  echo &quot;No directories or files selected.&quot;
  exit 1
fi

git diff $mainBranch... -R -- $toRevert | patch -p1
</code></pre><p>You may want to add a step after <code>git diff ...</code> that takes the files changed on the branch and includes their parent directories, so you can revert the changes in a whole directory without having to select every file under it.</p><p>If you have suggestions for improvements to either of these scripts, or if you have your own scripts that help with your workflow, please <a href="mailto:eric@iterativetangents.com?subject=Two Git scripts to encourage good development habits">email me</a>.</p>]]></content>
  </entry>
  <entry>
    <id>https://iterativetangents.com/nice-words</id>
    <link href="https://iterativetangents.com/nice-words"/>
    <title>Nice words</title>
    <updated>2026-05-20T08:00:00+00:00</updated>
    <content type="html"><![CDATA[<p>An old oak in my yard fell a few weeks ago, and after I cleared the brush my kids volunteered to help rake up twigs and bark. My six year-old declared the job somewhat difficult, to which my three year-old replied, &quot;It's not hard for me.&quot; I was a bit snappish and couldn't let that braggadocio go uncorrected, so I told him, &quot;It's easy for you because you're not doing anything, you're just telling everyone else what to do.&quot; He paused for a long moment. Then he said, &quot;But you are doing a good job!&quot;</p><p>The little guy is a born manager. My friend Joe has a term for that kind of response from a leader. Years ago I was pulled off a project to work on a more urgent project, and during it I wrote some convoluted code that I should have refactored before publishing. When a reviewer called me out, I complained about the rush. The next morning, the project manager left a comment: &quot;You’ve been doing a lot of awesome work Eric! Your contributions, insights, and suggestions have really helped the [project] effort. Thanks!&quot; When I told Joe that story, he called it &quot;affirmation bombing&quot;.</p><p>Leaders often address complaints with compliments. But when praise is just a tactic used to assuage frustration, it comes across as disingenuous. Worse, it can to backfire if the receiver sees through such praise and interprets it as, &quot;Your negative emotions have given me negative emotions, so I want them to go away. If this bit of positivity doesn't give you positive emotions, you must either hide your negative emotions or go away yourself.&quot; Praise can, counterintuitively, deepen a negative feeling.</p><p>To keep my praise authentic, I tend to be sparing with it, but as a dad I've had to learn to be more generous. Kind words still have to be spontaneous, not calculated, so I've chosen to get sensitive to what's worth praising. Trying an unfamiliar food. Keeping a screwdriver in the head of a screw.</p><p>I've also learned to give myself some kind words. It's easy for me to discount praise, to see it as motivated, to know it's part of a scheduled review cycle, to wish it was more substantive. But rejecting all praise leads to long, dark moods. It's important to recognize compliments from others even if it's sometimes tactical. Rarely is it outright dishonest. Kindness prompted by a complaint is still kindness.</p><p>At one company I worked for, we emailed positive remarks from a client out to the whole staff with a subject line of &quot;Nice words&quot;. One day I decided to adopt that practice for myself. Whenever someone gives a kind word, even if it was affirmation bombing, or a perfunctory farewell when changing jobs, or an annual review where the boss instructed everyone to review gently, I write it down. Tim Ferriss does something similar with a &quot;<a href="https://tim.blog/2018/01/01/the-tim-ferriss-show-transcripts-how-to-optimize-creative-output-jarvis-vs-ferriss/">Jar of Awesome</a>&quot;. I keep a digital version. Every now and then I come across one I'd forgotten. &quot;You have a nice reading voice.&quot; &quot;We should all be using your terminal setup.&quot; &quot;You've been a huge inspiration to me.&quot; &quot;I love the Eric notes.&quot; &quot;What you did is how I want this department to behave.&quot; &quot;You're probably my favorite writer.&quot; I can quote the project lead's original affirmation bomb word for word because I wrote it down in my nice words file.</p><p></p><figure class="figure-small"> <img src="/images/nice-words.svg" alt="Affirmation bombing into a Jar of Awesome full of nice words"> <figcaption>Even affirmation bombs are nice words.</figcaption> </figure><p></p><p>Some nice words were offhand comments, and while I may still discount a lot of compliments as general politeness, I do write down the things I find myself thinking about later. Fading memory doesn't mean the emotional boost of kind words also has to fade.</p><p>When I was a teenager, my father made me read Sean Covey's <i><a href="https://www.amazon.com/s?k=seven+habits+for+teens">The 7 Habits of Highly Effective Teens</a></i>, which used the metaphor of a relationship bank account: you have to make more deposits than withdrawals, and unlike an actual bank account, in relationships a deposit tends to evaporate and a withdrawal turns to stone. The same happens in our relationships with ourselves. For decades we remember an embarrassing moment, a typo in a tweet, a negative comment on a blog post, but we forget the compliments and nice words. Keeping a jar of awesome or a document of nice words is a way to combat that human weakness. Beyond that, documenting the positive things people say about you shows what others perceive to be your strengths, which can provide insight into how you can be your best self.</p><p>Sure there are bad days and low moods, but you are doing a good job!</p>]]></content>
  </entry>
  <entry>
    <id>https://iterativetangents.com/5-tips-for-sketchnoting</id>
    <link href="https://iterativetangents.com/5-tips-for-sketchnoting"/>
    <title>5 Tips for Sketchnoting</title>
    <updated>2026-05-15T08:00:00+00:00</updated>
    <content type="html"><![CDATA[<p>Back in 2013 I learned about <a href="https://en.wikipedia.org/wiki/Sketchnoting">sketchnoting</a> in a workshop at Midwest UX, and as someone who always doodled in my notes, giving meaning to the doodles felt like an obvious innovation I had overlooked, like putting wheels on luggage. After a few months' practice, I captured <a href="https://spin.atomicobject.com/sketchnoting-doodles-with-meaning/">some brief tips</a>. Since then, I've sketchnoted presentations from Agile Conf, local software meetups, company retreats, Clojure/conj, a handful of local one-day conferences, and a commencement speech. My style is mostly the same as it was ten years ago. And that's fine, because the point was never to get better at drawing, it was to capture notes more memorably. If you're interested in visual note-taking, here are five tips after 12 years and 100 sketchnotes.</p><p></p><figure class="figure-large"> <img src="/images/sketchnotes.jpg" alt="Sketchnotes of a talk about sketchnoting"> <figcaption>Sketchnotes about sketchnoting.</figcaption> </figure><p></p><h2>1. Draw to create focus</h2><p>It's easy to get distracted during a 40-minute or longer talk, but drawing creates focus by distracting the part of the brain that generates distractions. Doodling occupies the hand and the brain's visual processing. Even abstract patterns can serve as memory anchors, but infusing your doodles with content from the presentation locks it even deeper into memory, and it's more fun.</p><p>You don't have to be an artist to make sketchnoting fun or memorable. Simple drawings are great. Elaborate illustrations often detract from focus, rather than enhancing it.</p><p></p><figure class="figure-small"> <img src="/images/creation-of-adam-sketchnote.jpg" alt="Creation of Adam, as a sketchnote"> <figcaption><i>The Creation of Adam</i>, with apologies to Michelangelo.</figcaption> </figure><p></p><h2>2. There are many ways to be visual</h2><p>Sketchnotes frequently translate a presentation's visual elements into pictures, but many presentations are mostly text. Notes without a visual language can still be sketchnotes. Just capture text in visually interesting ways: make lists look like lists, underline important works, use an accent color for some words, write some words in ALL CAPS. Wrap sentences and fragments to leave room around them where you can write related concepts.</p><p></p><figure class="figure-small"> <img src="/images/text-focused-sketchnotes.jpg" alt="Text-focused sketchnotes"> <figcaption>Sketchnotes might be mostly text, but they can still have visual structure.</figcaption> </figure><p></p><p>A presentation is given linearly, one word after another, but the ideas have non-linear relationships that benefit from being captured in two dimensions. Paper allows you to de-linearize the content, even if you don't draw any pictures.</p><h2>3. Use pen and paper</h2><p>Using physical pen and paper is freeing. I've sketchnoted on tablets, but switching pens and colors is distracting. If the app allows editing—which most do because of the imprecision of stylus input—then it's tempting to clean up a drawing instead of listening. Paper has no distracting notifications or apps, and the battery never dies. You might not be able to erase, but mistakes can be memorable too.</p><p></p><figure class="figure-small figure-full-width"> <img src="/images/airplane-sketchnote.jpg" alt="An inked sketchnote with a mistake"> <figcaption>An inked airplane that needed a wing extension.</figcaption> </figure><p></p><p>I take sketchnotes on an 8&quot;x5&quot; index card that's blank on both sides. For pens I use basic utensils: a Pilot V5 and an orange felt-tip Paper Mate. Both provide smooth lines. The one fancier tool I use is a Copic N4 marker with a brush tip, to add some color or shadow to drawings, though when it's used up I'll probably look for a lighter shade of gray.</p><h2>4. Sketchnote for yourself, not others</h2><p>Sketchnotes are never detailed enough to reconstruct a talk, so they tend to have little meaning to those outside the audience. Even for those who did hear the presentation, your personal notations are likely to be opaque. You might share your sketchnotes with the speaker or other members of the audience, but don't expect anyone to use them as a reference. They're for you. The act of making them is the valuable part. Your memory of the event will be embedded in your own physical movements of drawing and writing. Share your notes in recognition of the time and energy the speaker put into their talk, and as a token of the time the audience spent together, but no one will get more out of your notes than you do.</p><p></p><figure class="figure-small"> <img src="/images/inverted-pyramid-sketchnote.jpg" alt="An inverted pyramid with some cryptic icons"> <figcaption>A tiny summary of the visuals on several slides, likely not intelligible to others.</figcaption> </figure><p></p><h2>5. Do it!</h2><p>You can start sketchnoting without experience, without tips, and without a grand reason. Do it for fun! Do it to enhance your memory of the occasion. Do it to create connection with others.</p><p>The purpose of sketchnoting isn't to become a master. You don't need artistic ability or perfect penmanship. The goal is to focus your attention, make you think more deeply about others' ideas, and have and share a memorable experience.</p><p>Sketchnoting is a gift to your future self.</p><p></p><figure class="figure-large"> <img src="/images/horizons-sketchnote.jpg" alt="Sketchnote of a ship facing progressively longer time horizons"> <figcaption>The important part is to start the journey.</figcaption> </figure><p></p><p>(For concrete tips on sketchnoting, see my <a href="https://spin.atomicobject.com/sketchnoting-doodles-with-meaning/">earlier post</a> and Mike Rohde's <i><a href="https://www.amazon.com/Sketchnote-Handbook-illustrated-visual-taking/dp/0321857895/ref=sr_1_1?dib=eyJ2IjoiMSJ9.IsC3ii8wWaWwpoD1z_mf3QJJ8-nMU5Sk2uttSUUyPiCqF1rIzJzPb19_lyIVlPgm4rVC3kDZdURdGfw6MPyt30ckefTDfVvJJ8HWuwlRLxI2ana2y2hkPkqaudABccjQwRcW3BgqbugBdYNQXtFGlwZOcxoWOUVn2zYbEfhEKICDa77vypNrvW0blbDvjhavNROGwHDb8F-EAF9jl-9tHtJHt8INGeM0j3tWX0IT2Fs.ZRobYhbV1DBXQ7v-S3QcbjTWik-TdMeDLjoDY-2NQZ4&amp;dib_tag=se&amp;keywords=the+sketchnote+handbook&amp;qid=1778803680&amp;sr=8-1">The Sketchnote Handbook</a></i>.)</p>]]></content>
  </entry>
  <entry>
    <id>https://iterativetangents.com/on-writing-code-myself</id>
    <link href="https://iterativetangents.com/on-writing-code-myself"/>
    <title>On writing code myself</title>
    <updated>2026-05-05T08:00:00+00:00</updated>
    <content type="html"><![CDATA[<p>Years ago I visited Mystic Seaport in Connecticut, where wooden sailing ships were built to go out on the Atlantic. The hull had to be as strong as possible, so beside making the internal structure from dense hardwoods like oak, shipwrights also used the natural shape of trees to the ship's advantage. Most notably they constructed the arched ribs from the bends in a tree where the trunk joined a root or a branch. The curved grain was far stronger than a man-made joint between two pieces of straight lumber.</p><p></p><figure class="figure-large"> <img src="/images/ships-rib.svg" alt="A ship's rib made from a curved section of a tree"> <figcaption>A ship's rib is stronger when made from a curved section of a tree.</figcaption> </figure><p></p><p>In the <a href="/a-game-loop-in-a-core-async-goroutine">previous post</a> I described a small game and some friction I encountered implementing a basic game loop. The problem was minor, and it would have been easy to ask an LLM to churn out the code. But I didn't, because my goal wasn't really to make a game, it was to learn. The friction created by my approach was instructive. I was cutting against the grain of the problem, rather than working with the grain. I could have forced my way through the quagmire, either myself or with the help of an AI sledgehammer, but I was making the game for fun, and part of the fun is exploring new approaches and adding new tools to my toolbox.</p><p>Experiences like that are why I continue to write code more by hand than by AI. When I do ask an LLM to code, it's so I can learn, not to outsource the effort. I have a relationship with the problem and the potential solutions I've collected, not so different from the way a woodworker has a relationship with the material and his tools. A relationship can't be delegated to a machine, even a thinking machine.</p><p>The craftsman mindset may become a luxury. Code written by hand may be considered &quot;artisanal&quot;. But I love doing more with less, like using curved sections of a tree to make curves in a ship's ribs stronger with less work, and while LLMs are often billed as saving us work, the total amount of work done is usually more. I value craftsmanship, and I still believe technique matters, even when it's slower.</p><p>So while I use AI, I don't use it as a substitute for learning. Notation is a tool of thought, and working hands-on with the code is a form of thinking. I write code myself because, beyond crafting software, I'm also crafting myself.</p>]]></content>
  </entry>
  <entry>
    <id>https://iterativetangents.com/a-game-loop-in-a-core-async-goroutine</id>
    <link href="https://iterativetangents.com/a-game-loop-in-a-core-async-goroutine"/>
    <title>A game loop in a core.async goroutine</title>
    <updated>2026-05-01T08:00:00+00:00</updated>
    <content type="html"><![CDATA[<p>Growing up I had an old Commodore 64 with an assortment of games on 5¼&quot; floppy disks. One of them was an election game, possibly <i><a href="https://en.wikipedia.org/wiki/President_Elect_(video_game)">President Elect</a></i> by Strategic Simulations, Inc, but at the time I was too young to understand it, and I never played much. Last year I wanted to teach my kids about the electoral college, so I made my own election game, one aimed more at a third grader. I omitted real-world parties, issues, and polling data, and focused instead on a tight loop of nudging states' preferences for or against two different candidates. There's a little geography, and a lot of puns. I called it <i>Electravaganza</i>. You can play <a href="https://electravaganza.exupero.org/">here</a>.</p><p></p><figure class="figure-large"> <img src="/images/electravaganza.png" alt="Screenshot of Electravaganza"> <figcaption>A screenshot of Electravaganza.</figcaption> </figure><p></p><p>It's implemented in <a href="https://clojurescript.org/">ClojureScript</a>, the UI is rendered with <a href="https://replicant.fun/">Replicant</a>, events handled with <a href="https://github.com/cjohansen/nexus">Nexus</a>, and game data stored in <a href="https://github.com/tonsky/datascript">DataScript</a>. The interface is mostly a map of the United States; the player's controls are on the left side. In the original implemenation, each component had buttons which triggered a Replicant event when clicked, then Nexus routed the event to an action that produced an effect and updated the DB, often by setting a new UI component in the DB. Then the renderer would update the UI with the new component.</p><p></p><figure class="figure-large"> <img src="/images/electravaganza-simple-loop.svg" alt="A simple Replicant and Nexus event loop"> <figcaption>A simple Replicant and Nexus event loop.</figcaption> </figure><p></p><p>That's a fairly standard rendering loop for Replicant, but putting a UI component in the DB was an experiment. I didn't end up liking it. It made the game loop hard to follow through the code. I had to cross back and forth between code in two different files to trace the event fired by a component's button to the action handler that specified the next UI component, then find that component and its button(s). Even if all the logic had been in action handlers, the code would have been split into small pieces that required lots of jumping around in the file, goto style.</p><p>Also, using Nexus actions for the game loop made it hard to change the rules. The first version of the game gave the player a random hand of action cards to choose from, then chose a random action for the computer opponent, then gave the human player a totally new set of cards. That was good enough to get the basic mechanics working, but it didn't allow for any strategy across turns. I wanted to tinker with the game loop, but having the logic split between components and action handlers meant that changing the sequence of steps required rewriting the actions. That was tedious and error-prone.</p><p>To address both problems, I introduced a proper state machine. While I briefly toyed with a high-level, data-oriented definition of states and transitions, the need for a loop with a turn counter smelled like a programming language, so first I tried a different form of state machine: a <a href="https://github.com/clojure/core.async">core.async</a> goroutine.</p><p>I hadn't used core.async in a while. Though I appreciate Go's model of communicating sequential processes, it's overkill for most of the helper scripts and small apps I write. In this case, I didn't need to execute code in parallel, but for something to serve as a goroutine's namesake, a pausable <a href="https://en.wikipedia.org/wiki/Coroutine">coroutine</a>. After some refactoring, I ended up with a game loop defined like this:</p><pre><code class="clojure"><span>(<span class="hl-keyword">defn</span> basic-game [conn turns]
  (<span class="hl-keyword">let</span> [chans [(async/chan) (async/chan)]]
    (async/go
      (<span class="hl-keyword">let</span> [[player1 player2] (&lt;! (choose-party chans))]
        (&lt;! (choose-hairdo chans @conn player1))
        (&lt;! (choose-hairdo chans @conn player2))
        (<span class="hl-keyword">loop</span> [turns-left turns]
          (&lt;! (set-turns-left chans turns-left))
          (<span class="hl-keyword">if</span> (<span class="hl-built_in">pos?</span> turns-left)
            (<span class="hl-keyword">do</span>
              (&lt;! (choose-action chans @conn player1))
              (&lt;! (choose-action chans @conn player2))
              (<span class="hl-keyword">recur</span> (<span class="hl-built_in">dec</span> turns-left)))
            (&lt;! (results chans))))))
    chans))
</span></code></pre><p>The syntactic gynmastics of core.async <code>chan</code>, <code>go</code>, and <code>&lt;!</code> make it a bit noisy, but it did centralize the logic of the game loop. The helper functions provided a domain-level grammar of the game's sequence.</p><p>The game initializes this goroutine during setup, stores the returned channels in the DB, sends Nexus effects keyed with <code>:effect/send</code> to the input channel, and spawns a second goroutine that listens for output messages and dispatches actions:</p><p></p><figure class="figure-large"> <img src="/images/electravaganza-goroutine-loop.svg" alt="A Replicant event loop with a goroutine loop"> <figcaption>A Replicant, Nexus, and core.async rendering flow, with two intersecting loops.</figcaption> </figure><p></p><p>That requires each of the helper functions used by the goroutine to have some of the same syntactic overhead as the main loop, littered with <code>go</code> and <code>&lt;!</code> and <code>&gt;!</code>. Maybe that boilerplate could be cleaned up with macros. But a perfect DSL wasn't my main concern. I was more interested in having a game loop defined in a compact and comprehensible chunk, as well as one that could be easily swapped out for a different loop. A goroutine gave me that. I could fiddle with the loop so each player started with random cards then discarded played cards and kept the rest for the next turn:</p><pre><code class="clojure"><span>(<span class="hl-keyword">defn</span> hand-of-actions [conn turns actions-per-turn]
  (<span class="hl-keyword">let</span> [chans [(async/chan) (async/chan)]
        hand-limit actions-per-turn]
    (async/go
      (<span class="hl-keyword">let</span> [[player1 player2] (&lt;! (choose-party chans))]
        (&lt;! (choose-hairdo chans @conn player1))
        (&lt;! (choose-hairdo chans @conn player2))
        (<span class="hl-keyword">loop</span> [turns-left turns
               hand1 (db/random-actions @conn hand-limit)
               hand2 (db/random-actions @conn hand-limit)])
	      (&lt;! (set-turns-left chans turns-left))
	      (<span class="hl-keyword">if</span> (<span class="hl-built_in">pos?</span> turns-left)
	        (<span class="hl-keyword">let</span> [[played1 hand1] (&lt;! (choose-actions chans @conn player1 hand1))
	              [played2 hand2] (&lt;! (choose-actions chans @conn player2 hand2))]
	          (<span class="hl-keyword">recur</span> (<span class="hl-built_in">dec</span> turns-left)
	                 (replace-action @conn hand1 hand-limit)
	                 (replace-action @conn hand2 hand-limit)))
	        (&lt;! (results chans)))))
    chans))
</span></code></pre><p>I didn't have to add features to a custom state machine evaluator, but instead used Clojure's built-in language constructs to keep state between passes through the loop. A minimum viable implementation.</p><p>The end result is more complicated, with more moving parts—a game-loop goroutine running in the background, a goroutine listening for output messages, channels saved to the database—but it grouped logic according to its responsibilities and made the individual parts easier to reason about.</p><p>That's the important part. A functional language like Clojure usually avoids the kind of imperative logic above, but sometimes the clearest expression of a solution is step-by-step. Especially for a game, which is inherently imperative. Do this, then do that. The goal isn't to always avoid side effects and only use pure functions. My original Nexus handlers were pure functions. The goal is to make code understandable and to facilitate new ways of using it. Having clusters of tiny pure functions can undermine those goals.</p><p>Both the code and the game have plenty of room for improvement, but using a core.async goroutine allowed me to define different new game loops easily enough to arrive at some rules my son found fun to play, and hopefully one that taught him a little about the electoral college and geography (and wordplay).</p><p>If you have suggestions for the game, or the code, feel free to <a href="mailto:eric@iterativetangents.com?subject=A game loop in a core.async goroutine">email me</a>.</p>]]></content>
  </entry>
  <entry>
    <id>https://iterativetangents.com/talk-on-software-development-as-a-career</id>
    <link href="https://iterativetangents.com/talk-on-software-development-as-a-career"/>
    <title>Talk on software development as a career</title>
    <updated>2026-04-21T08:00:00+00:00</updated>
    <content type="html"><![CDATA[<p>Recently I discussed software engineering as a career with a few high school juniors and seniors. I worked from a loose visual outline, and I drew on the whiteboard quite a bit, so here's my synthesis of the two, with a few added drawings. More details below the sketchnotes.</p><p></p><figure class="figure-large"> <img src="/images/sketchnotes-software-engineering.svg" alt="Sketchnotes on talk about software engineering career"> <figcaption>Sketchnotes on my talk about the role of a principal software engineering.</figcaption> </figure><p></p><p>Originally I planned to walk through the various jobs I've held and compare them to higher and higher parts of the <a href="https://rogermartin.medium.com/heuristics-management-strategy-bdc744acdfab">knowledge funnel</a>, moving from junior-level &quot;code&quot; up to principal-level &quot;mystery&quot;, but it struck even me as dry and not very helpful for guiding career decisions, so I pivoted to talk more about turning ambiguous ideas into concrete realities. To illustrate, I started with an activity. I paired up the kids and asked teams to design an app that would help people eat healthier. Remembering my own high school class, I expected they'd need elaboration or cajoling, but I was pleasantly surprised how readily they jumped into the task without further instruction. Digital natives, I guess.</p><p>After about five minutes each team described their idea. They were all different, though they had the same underlying goal. As a software engineer, my job is often to surface those kinds of differences in how stakeholders, designers, and devs imagine a product or interface.</p><p>When I was fresh out of college, I noticed how different the working world was from school. Rather than being given closed-ended problems and having to find the right answer, I was given open-ended problems which often required asking more questions. And there were no right answers. Instead, every choice had tradeoffs. My job is frequently to identify those tradeoffs and make either a recommendation or a decision.</p><p>I'm frequently an intermediary between parties, translating concepts and language between stakeholders, other developers, the computer, and (now) AI. Contrary to stereotypes of computer programmers, that role requires a lot of communication skills.</p><p>I concluded with some of my personal experiences:</p><p></p><figure class="figure-large"> <img src="/images/sketchnotes-software-engineer.svg" alt="Sketchnotes on talk about being a software engineerer"> <figcaption>Sketchnotes on my personal experience as a software engineer.</figcaption> </figure><p></p><p>I work remotely, and the students were old enough to remember COVID and doing school from home, so they had context for how WFH can be lonely but also less distracting. Personally, I prefer working from home. I can live almost anywhere (thanks now to Starlink), I'm paid much more than I was when I worked for a local software agency (thanks to having more employment options), and I get to spend way more time with my wife, kids, and the house and property that are my favorite place in the world.</p><p>I also explained some of the differences between big companies and smaller startups. Small organizations provide more opportunity to make an impact on the business, but huge FAANG corporations provide more opportunity to make an impact on the world. The compensation is different too. Startups sometimes offer equity to make up for less salary, but beyond a few famous examples, not many people get rich off startup equity, though it can occasionally provide a moderate windfall.</p><p>Job-hopping is common in the software industry. People often struggle to get promotions and raises without changing jobs every couple of years, but my experience is mixed. I've had jobs where I got raises and promotions, but also jobs where I didn't. My bigger gripe is having to change jobs to continue growing professionally, regardless of promotions or pay. It's very easy to get pigeon-holed. The better I've been at a job, the harder it's been for the business to replace me in a given role. Sometimes it's easier to jump into a new role at a new job than to convince your boss to take a gamble on your capabilities.</p><p>One student asked what &quot;bad stories&quot; I had. Overall my career has been reasonably positive, and I don't really have horror stories. Certainly there are experiences I didn't prefer at the time, but those experiences were also stepping stones to where I am now, which is a stepping stone to wherever I'll be in the future. I've had bosses I didn't like and coworkers who annoyed me, but the incidents were all fairly forgettable, and so far I'm free of tales about dropped production databases, unsecured S3 buckets, and expectations of working a <a href="https://en.wikipedia.org/wiki/996_working_hour_system">996 schedule</a>.</p>]]></content>
  </entry>
  <entry>
    <id>https://iterativetangents.com/learning-dvorak</id>
    <link href="https://iterativetangents.com/learning-dvorak"/>
    <title>Learning Dvorak</title>
    <updated>2026-04-13T08:00:00+00:00</updated>
    <content type="html"><![CDATA[<p>I saw a blog post today about someone who's been using Dvorak for 20 years, and a commenter asked how long it took to learn. I learned Dvorak in my first job after college, when I had a lot of downtime and wanted to give it a try. I practiced by transcribing Wikipedia articles, and I remember it as a three-month process.</p><p>In the first month, I typed very slowly and made lots of mistakes, all with the much higher cognitive burden of having to remember where a character was and which finger should press the key before I could type anything. A very frustrating month. In the second month, my fingers began to know where they needed to go, but by then I had developed the habit of thinking about each key before I hit it. Many times a finger found a character before I could remember where it was. At that point, I knew I could drop the mental load, and I spent the third month learning not to think about each key. After that, it just took some time to get back up to my old Qwerty speed.</p><p>I related that three-month learning curve to my grandfather not long afterward, and he thought it felt very similar to his experience learning Morse code in the Navy after he was drafted for World War II.</p><p>I don't know if I'm faster using Dvorak than I was with Qwerty. I don't have any data, but qualitatively I feel like I type about the same speed. What has changed is the amount of effort. At the end of a long day of typing, my hands were less tired using Dvorak than they were using Qwerty. When a coworker found out I used Dvorak, he exclaimed, "Oh, that's why your hands don't move when you type!"</p><p>Despite using Dvorak as my main keyboard for more than 15 years, I can still type Qwerty, though not as fast and I have to look at the keyboard as I type. I can't touch type Qwerty anymore. I knew a guy who used Dvorak when in Emacs and Qwerty everywhere else (or vice versa), and he had learned to switch between layouts fluently, but I never tried. The closest I've come is typing on an iPad, where I still use the default Qwerty layout. An iPad, of course, has no tactile feedback, so I have to look anyway, and I don't feel that I would be as fast looking at a Dvorak keyboard. It's as if my eyes know one layout and my fingers another.</p>]]></content>
  </entry>
  <entry>
    <id>https://iterativetangents.com/synchronized-blinking</id>
    <link href="https://iterativetangents.com/synchronized-blinking"/>
    <title>Synchronized blinking</title>
    <updated>2026-04-10T08:00:00+00:00</updated>
    <content type="html"><![CDATA[
<figure><svg viewBox="0 0 100 20" width="100%"><rect width="100%" height="100%" fill="white"></rect><rect fill="black" height="20" width="100"></rect><g><circle cx="10.0" cy="5.0" fill="red" r="0.1499999977648258"><animate attributeName="fill-opacity" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="11.600000023841858" cy="3.5" fill="red" r="0.2249999988824129"><animate attributeName="fill-opacity" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="13.200000047683716" cy="3.0" fill="red" r="0.24999999925494193"><animate attributeName="fill-opacity" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="14.800000071525574" cy="3.5" fill="red" r="0.2249999988824129"><animate attributeName="fill-opacity" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="16.40000009536743" cy="6.0" fill="red" r="0.09999999701976775"><animate attributeName="fill-opacity" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="18.00000011920929" cy="2.0" fill="red" r="0.3"><animate attributeName="fill-opacity" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="19.600000143051147" cy="6.0" fill="red" r="0.09999999701976775"><animate attributeName="fill-opacity" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="21.200000166893005" cy="6.0" fill="red" r="0.09999999701976775"><animate attributeName="fill-opacity" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="22.800000190734863" cy="6.0" fill="red" r="0.09999999701976775"><animate attributeName="fill-opacity" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="24.40000021457672" cy="5.5" fill="red" r="0.12499999739229678"><animate attributeName="fill-opacity" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="26.00000023841858" cy="3.5" fill="red" r="0.2249999988824129"><animate attributeName="fill-opacity" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="27.600000262260437" cy="3.5" fill="red" r="0.2249999988824129"><animate attributeName="fill-opacity" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="29.200000286102295" cy="3.0" fill="red" r="0.24999999925494193"><animate attributeName="fill-opacity" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="30.800000309944153" cy="4.0" fill="red" r="0.19999999850988387"><animate attributeName="fill-opacity" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="32.40000033378601" cy="2.5" fill="red" r="0.27499999962747096"><animate attributeName="fill-opacity" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="34.00000035762787" cy="5.0" fill="red" r="0.1499999977648258"><animate attributeName="fill-opacity" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="35.60000038146973" cy="4.5" fill="red" r="0.17499999813735484"><animate attributeName="fill-opacity" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="37.200000405311584" cy="2.5" fill="red" r="0.27499999962747096"><animate attributeName="fill-opacity" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="38.80000042915344" cy="3.5" fill="red" r="0.2249999988824129"><animate attributeName="fill-opacity" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="40.4000004529953" cy="2.5" fill="red" r="0.27499999962747096"><animate attributeName="fill-opacity" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="42.00000047683716" cy="5.0" fill="red" r="0.1499999977648258"><animate attributeName="fill-opacity" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="43.600000500679016" cy="5.0" fill="red" r="0.1499999977648258"><animate attributeName="fill-opacity" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="45.200000524520874" cy="2.0" fill="red" r="0.3"><animate attributeName="fill-opacity" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="46.80000054836273" cy="4.0" fill="red" r="0.19999999850988387"><animate attributeName="fill-opacity" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="48.40000057220459" cy="5.0" fill="red" r="0.1499999977648258"><animate attributeName="fill-opacity" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="50.00000059604645" cy="2.5" fill="red" r="0.27499999962747096"><animate attributeName="fill-opacity" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="51.600000619888306" cy="4.0" fill="red" r="0.19999999850988387"><animate attributeName="fill-opacity" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="53.200000643730164" cy="4.0" fill="red" r="0.19999999850988387"><animate attributeName="fill-opacity" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="54.80000066757202" cy="4.5" fill="red" r="0.17499999813735484"><animate attributeName="fill-opacity" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="56.40000069141388" cy="3.0" fill="red" r="0.24999999925494193"><animate attributeName="fill-opacity" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="58.00000071525574" cy="4.5" fill="red" r="0.17499999813735484"><animate attributeName="fill-opacity" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="59.600000739097595" cy="2.5" fill="red" r="0.27499999962747096"><animate attributeName="fill-opacity" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="61.20000076293945" cy="2.0" fill="red" r="0.3"><animate attributeName="fill-opacity" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="62.80000078678131" cy="2.5" fill="red" r="0.27499999962747096"><animate attributeName="fill-opacity" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="64.40000081062317" cy="2.5" fill="red" r="0.27499999962747096"><animate attributeName="fill-opacity" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="66.00000083446503" cy="4.0" fill="red" r="0.19999999850988387"><animate attributeName="fill-opacity" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="67.60000085830688" cy="6.0" fill="red" r="0.09999999701976775"><animate attributeName="fill-opacity" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="69.20000088214874" cy="3.0" fill="red" r="0.24999999925494193"><animate attributeName="fill-opacity" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="70.8000009059906" cy="3.5" fill="red" r="0.2249999988824129"><animate attributeName="fill-opacity" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="72.40000092983246" cy="3.0" fill="red" r="0.24999999925494193"><animate attributeName="fill-opacity" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="74.00000095367432" cy="3.5" fill="red" r="0.2249999988824129"><animate attributeName="fill-opacity" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="75.60000097751617" cy="3.0" fill="red" r="0.24999999925494193"><animate attributeName="fill-opacity" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="77.20000100135803" cy="5.5" fill="red" r="0.12499999739229678"><animate attributeName="fill-opacity" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="78.80000102519989" cy="2.0" fill="red" r="0.3"><animate attributeName="fill-opacity" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="80.40000104904175" cy="4.5" fill="red" r="0.17499999813735484"><animate attributeName="fill-opacity" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="82.0000010728836" cy="4.5" fill="red" r="0.17499999813735484"><animate attributeName="fill-opacity" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="83.60000109672546" cy="2.5" fill="red" r="0.27499999962747096"><animate attributeName="fill-opacity" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="85.20000112056732" cy="4.5" fill="red" r="0.17499999813735484"><animate attributeName="fill-opacity" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="86.80000114440918" cy="6.0" fill="red" r="0.09999999701976775"><animate attributeName="fill-opacity" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="88.40000116825104" cy="3.0" fill="red" r="0.24999999925494193"><animate attributeName="fill-opacity" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle></g></svg><figcaption>Mysterious lights blinking in synchrony on a night drive.</figcaption></figure>
<p>The first time I saw this sight, I was riding across northern Indiana in the early morning hours, awoken by my wife as she drove. Neither of us knew what it was. The eeriness of it is the closest I've felt to a UFO encounter, but now, 15 years later, these sights are common. On that drive across Indiana, she woke me again near dawn to tell me what they were. Windmills.</p><p>The red lights, of course, are safety lights to warn planes, and recently while driving across Kansas in the dark and thinking how glad I was not to have this blinking array out my bedroom window, I supposed it could be important for the lights to all blink together. If each light blinked on its own schedule, it would create a continuous scintillation, which could be even more disruptive:</p>
<figure><svg viewBox="0 0 100 20" width="100%"><rect width="100%" height="100%" fill="white"></rect><rect fill="black" height="20" width="100"></rect><g><circle cx="10.0" cy="5.0" fill="red" r="0.1499999977648258"><animate attributeName="fill-opacity" begin="1.230242434476605s" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="11.600000023841858" cy="3.0" fill="red" r="0.24999999925494193"><animate attributeName="fill-opacity" begin="0.9981511678785335s" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="13.200000047683716" cy="6.0" fill="red" r="0.09999999701976775"><animate attributeName="fill-opacity" begin="0.018351546797283902s" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="14.800000071525574" cy="6.0" fill="red" r="0.09999999701976775"><animate attributeName="fill-opacity" begin="2.8195961663457294s" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="16.40000009536743" cy="6.0" fill="red" r="0.09999999701976775"><animate attributeName="fill-opacity" begin="2.811246446687909s" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="18.00000011920929" cy="3.5" fill="red" r="0.2249999988824129"><animate attributeName="fill-opacity" begin="1.0425540876093309s" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="19.600000143051147" cy="3.0" fill="red" r="0.24999999925494193"><animate attributeName="fill-opacity" begin="1.5194508819787051s" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="21.200000166893005" cy="2.5" fill="red" r="0.27499999962747096"><animate attributeName="fill-opacity" begin="2.311607640237533s" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="22.800000190734863" cy="4.5" fill="red" r="0.17499999813735484"><animate attributeName="fill-opacity" begin="0.47024067170953876s" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="24.40000021457672" cy="3.5" fill="red" r="0.2249999988824129"><animate attributeName="fill-opacity" begin="0.41928804871125347s" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="26.00000023841858" cy="5.0" fill="red" r="0.1499999977648258"><animate attributeName="fill-opacity" begin="2.415683314421141s" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="27.600000262260437" cy="2.0" fill="red" r="0.3"><animate attributeName="fill-opacity" begin="1.569405467364999s" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="29.200000286102295" cy="5.0" fill="red" r="0.1499999977648258"><animate attributeName="fill-opacity" begin="0.4260681096477784s" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="30.800000309944153" cy="4.0" fill="red" r="0.19999999850988387"><animate attributeName="fill-opacity" begin="1.6336644266810212s" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="32.40000033378601" cy="4.5" fill="red" r="0.17499999813735484"><animate attributeName="fill-opacity" begin="0.6147406372756847s" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="34.00000035762787" cy="4.5" fill="red" r="0.17499999813735484"><animate attributeName="fill-opacity" begin="0.5541212708338459s" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="35.60000038146973" cy="2.0" fill="red" r="0.3"><animate attributeName="fill-opacity" begin="0.4831299701151067s" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="37.200000405311584" cy="2.5" fill="red" r="0.27499999962747096"><animate attributeName="fill-opacity" begin="1.6211911943513893s" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="38.80000042915344" cy="6.0" fill="red" r="0.09999999701976775"><animate attributeName="fill-opacity" begin="0.7362796607438868s" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="40.4000004529953" cy="3.5" fill="red" r="0.2249999988824129"><animate attributeName="fill-opacity" begin="0.6528063747230545s" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="42.00000047683716" cy="3.5" fill="red" r="0.2249999988824129"><animate attributeName="fill-opacity" begin="0.6994673842540002s" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="43.600000500679016" cy="5.5" fill="red" r="0.12499999739229678"><animate attributeName="fill-opacity" begin="0.11498067995782624s" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="45.200000524520874" cy="4.5" fill="red" r="0.17499999813735484"><animate attributeName="fill-opacity" begin="1.9655208093882492s" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="46.80000054836273" cy="2.5" fill="red" r="0.27499999962747096"><animate attributeName="fill-opacity" begin="1.9574301206878595s" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="48.40000057220459" cy="6.0" fill="red" r="0.09999999701976775"><animate attributeName="fill-opacity" begin="0.620212522081522s" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="50.00000059604645" cy="3.5" fill="red" r="0.2249999988824129"><animate attributeName="fill-opacity" begin="1.3900493627084272s" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="51.600000619888306" cy="3.5" fill="red" r="0.2249999988824129"><animate attributeName="fill-opacity" begin="1.3296373509518071s" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="53.200000643730164" cy="4.0" fill="red" r="0.19999999850988387"><animate attributeName="fill-opacity" begin="2.9969424443037864s" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="54.80000066757202" cy="4.5" fill="red" r="0.17499999813735484"><animate attributeName="fill-opacity" begin="2.7287521067670726s" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="56.40000069141388" cy="4.0" fill="red" r="0.19999999850988387"><animate attributeName="fill-opacity" begin="1.4743536166848377s" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="58.00000071525574" cy="3.5" fill="red" r="0.2249999988824129"><animate attributeName="fill-opacity" begin="0.9242787456254394s" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="59.600000739097595" cy="5.0" fill="red" r="0.1499999977648258"><animate attributeName="fill-opacity" begin="2.8872437782012392s" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="61.20000076293945" cy="3.0" fill="red" r="0.24999999925494193"><animate attributeName="fill-opacity" begin="0.5179464627420157s" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="62.80000078678131" cy="4.0" fill="red" r="0.19999999850988387"><animate attributeName="fill-opacity" begin="1.6664051462638847s" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="64.40000081062317" cy="4.5" fill="red" r="0.17499999813735484"><animate attributeName="fill-opacity" begin="2.3675719735052874s" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="66.00000083446503" cy="5.0" fill="red" r="0.1499999977648258"><animate attributeName="fill-opacity" begin="0.6139066566170566s" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="67.60000085830688" cy="3.0" fill="red" r="0.24999999925494193"><animate attributeName="fill-opacity" begin="2.3337490785585837s" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="69.20000088214874" cy="3.0" fill="red" r="0.24999999925494193"><animate attributeName="fill-opacity" begin="2.9493095545011094s" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="70.8000009059906" cy="5.0" fill="red" r="0.1499999977648258"><animate attributeName="fill-opacity" begin="2.508944843268986s" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="72.40000092983246" cy="2.5" fill="red" r="0.27499999962747096"><animate attributeName="fill-opacity" begin="1.912492289578219s" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="74.00000095367432" cy="2.0" fill="red" r="0.3"><animate attributeName="fill-opacity" begin="1.8935696649345748s" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="75.60000097751617" cy="3.0" fill="red" r="0.24999999925494193"><animate attributeName="fill-opacity" begin="2.641071854721127s" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="77.20000100135803" cy="5.0" fill="red" r="0.1499999977648258"><animate attributeName="fill-opacity" begin="2.1701149386474077s" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="78.80000102519989" cy="4.0" fill="red" r="0.19999999850988387"><animate attributeName="fill-opacity" begin="2.9613064142888996s" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="80.40000104904175" cy="2.5" fill="red" r="0.27499999962747096"><animate attributeName="fill-opacity" begin="2.1317243254450036s" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="82.0000010728836" cy="5.5" fill="red" r="0.12499999739229678"><animate attributeName="fill-opacity" begin="0.3710136855087727s" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="83.60000109672546" cy="4.5" fill="red" r="0.17499999813735484"><animate attributeName="fill-opacity" begin="1.4455870657817944s" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="85.20000112056732" cy="2.5" fill="red" r="0.27499999962747096"><animate attributeName="fill-opacity" begin="1.8514376695842953s" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="86.80000114440918" cy="2.0" fill="red" r="0.3"><animate attributeName="fill-opacity" begin="1.7743173178639142s" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle><circle cx="88.40000116825104" cy="2.5" fill="red" r="0.27499999962747096"><animate attributeName="fill-opacity" begin="0.22214378182003724s" calcMode="discrete" dur="3s" repeatCount="indefinite" values="1;0;0;1"></animate></circle></g></svg><figcaption>Not-so-mysterious lights blinking out of sync.</figcaption></figure>
<p>So I have some questions:</p><ol><li>Is it a safety consideration for the lights to blink together, or aesthetic?</li><li>Is synchronized blinking required when building a wind farm?</li><li>What's the mechanism for synchronizing the lights?</li></ol><p>If you know any of the answers, I'd be happy to <a href="mailto:eric@iterativetangents.com?subject=Synchronized blinking">hear from you</a>.</p>
]]></content>
  </entry>
  <entry>
    <id>https://iterativetangents.com/annotating-changes-in-diff-files</id>
    <link href="https://iterativetangents.com/annotating-changes-in-diff-files"/>
    <title>Annotating changes in diff files</title>
    <updated>2026-04-07T08:00:00+00:00</updated>
    <content type="html"><![CDATA[<p>Besides <a href="/diff-files-for-troubleshooting-git-branches">troubleshooting branches</a> and <a href="/splitting-branches-using-diff-files">splitting branches</a>, I've also used diff files to annotate changes in a branch. That's a less common analytical task, not a regular part of my workflow, but having already used diff files for other purposes, using one to summarize some repetitive changes was natural.</p><p>In this particular case, I introduced a new vertical slice of functionality, which required adding a lot of new classes. I became curious how many classes had to be added, how many methods, and how many imports, to have a better idea how much similar work might be required for future vertical slices. I tried going through the changes and counting, but the counts were difficult to track. Instead, I added comments to the diff.</p><p>The unified diff format output by Git doesn't natively support comments, but since I wasn't using the diff afterward, it didn't matter. I used <code>#</code> in the first column to denote a comment, then added comments for any changes of interest, such as <code>class added</code>, <code>method changed</code>, <code>import added</code>, <code>external interface changed</code>, <code>unit test added</code>, etc. Afterward, it was simple to tally the changes with a Bash command:</p><pre><code class="bash">cat annotated-changes.diff | grep '^#' | sort | uniq -c | sort -nk1r
</code></pre><p>That finds all the comment lines, sorts them, counts unique lines, then sorts by count, with the most common changes listed first. With those results, I got a quick bird's-eye view of the vertical slice. It took some manual tagging within the diff, but I didn't want to write even a rudimentary parser for a one-off task I could do the hard way in a few minutes. And that annotated diff may be useful in the future, such as if a stakeholder asks why there's so much friction for a new feature, I can provide specifics on how a dozen new classes were needed (for transporting data between different layers) and how three times as many imports were needed to make those classes available in the expected places.</p><p>If you have other suggestions for using diff files as part of your workflow, please <a href="mailto:eric@iterativetangents.com?subject=Annotating changes in diff files">email me</a>.</p>]]></content>
  </entry>
  <entry>
    <id>https://iterativetangents.com/splitting-branches-using-diff-files</id>
    <link href="https://iterativetangents.com/splitting-branches-using-diff-files"/>
    <title>Splitting branches using diff files</title>
    <updated>2026-04-03T08:00:00+00:00</updated>
    <content type="html"><![CDATA[<p>Besides using diff files to <a href='/diff-files-for-troubleshooting-git-branches'>troubleshoot Git branches</a>, I've also used them to split a branch into multiple branches. Often when starting some exploratory work, it's hard to know the full breadth of the changes that are needed, so foreseeing tactical cleanups and refactors can be difficult. I'll often make changes as I explore the code, whether they're relevant or not. But as a reviewer, I appreciate when a pull request is small, focused, and contains only non-functional changes <i>or</i> changes that are visible to the end user, not both, so when I've written a branch that's a big bundle of mixed concerns, I try to take the time to split it up. It can feel like starting over to break working changes into smaller sets, so here's how I use diff files to jumpstart new branches and avoid some rework.</p><p>In my oversized branch, I write the full diff to a file with something like <code>git diff main... &gt; changes.diff</code>. Then I review the changes, looking for what parts are trivial cleanup that could go in a separate branch and be merged without any of the other work I've done. I copy those files out into a new file, say, <code>cleanup.diff</code>, remove irrelevant chunks, and delete from <code>changes.diff</code> the chunks that got copied. Sometimes chunks themselves need to be edited, which can be tricky, since Git's diffs include information about what line changes start at and how many lines were changed, information that has to be updated if I add or remove lines from a chunk.</p><p>Once I've pulled out all the simple cleanup changes, I look for refactors. Those chunks go in another file, e.g. <code>refactor.diff</code>. Sometimes an oversized branch has multiple refactors, and multiple diff files are needed to untangle them into separate threads. When I'm done, the chunks remaining in <code>changes.diff</code> should reflect functional changes.</p><p>Once I have all the diff files, I figure out how to structure my pull requests. I create a new branch for the first set of changes, run <code>cat something.diff | patch -p1</code>, test, and if all is well, open a PR. After that, I create a new branch off the first branch and apply the next diff. It may or may not work. Sometimes the subsequent diff files don't align perfectly with a subset of the original changes, but even if the diff applies cleanly, the code might not function correctly, because often changes are tough to eyeball from a diff. Tweaking is frequently necessary. Once I've tested and gotten the second branch working, I continue on through any other diffs I have, branching from the appropriate base branches. Usually only two branches are necessary; in extreme cases, three. Any more than that and I tend to break out cleanups and refactors before finishing exploratory work, so that exploration is clearer and without as many distractions.</p><p>Whether I create PRs for each branch immediately depends on team culture. For some teams, I've only created branches one at a time, keeping others in reserve until the PRs they depend on get merged. That simplifies the mental landscape for reviewers, since PRs targeting other PRs can be hard to reason about, but it can also be simpler for me as the code author. When reviewers have feedback, I can make changes to the branch and rebase downstream branches without worrying that others have already pulled those branches. If I do open subsequent PRs before the first one is merged, I target the open branch and note in the PR description that it depends on another PR, then I rely on GitHub to automatically retarget a PR at the main development branch when its base branch is merged.</p>
]]></content>
  </entry>
</feed>
