<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Fernando Borretti</title>
    <description>Personal website</description>
    <link>https://borretti.me/</link>
    <atom:link href="https://borretti.me/feed.xml" rel="self" type="application/rss+xml" />
    <pubDate>Wed, 11 Feb 2026 19:18:24 +0000</pubDate>
    <lastBuildDate>Wed, 11 Feb 2026 19:18:24 +0000</lastBuildDate>
    <generator>Jekyll v3.10.0</generator>
    
      <item>
        <title>Some Data Should Be Code</title>
        <description>On Make, CloudFormation, and GitHub Actions.</description>
        <pubDate>Sat, 31 Jan 2026 00:00:00 +0000</pubDate>
        <link>https://borretti.me/article/some-data-should-be-code</link>
        <guid isPermaLink="true">
          https://borretti.me/article/some-data-should-be-code
        </guid>
        
        <content:encoded>&lt;p&gt;I write a lot of &lt;a href=&quot;https://en.wikipedia.org/wiki/Make_(software)&quot;&gt;Makefiles&lt;/a&gt;. I use it not as a command runner but as an ad-hoc build system for small projects, typically for compiling Markdown documents and their dependencies. Like so:&lt;/p&gt;

&lt;p&gt;&lt;img style=&quot;margin-left: auto; margin-right: auto; width: 300px;&quot; src=&quot;/assets/content/some-data-should-be-code/graph.png&quot; alt=&quot;A build graph for a document. A central node `doc.md` represents the Markdown source. Two outgoing arrows point to `doc.html` and `doc.pdf`, representing the output format. A chain through `graph.dot`, `graph.png`, and `doc.md` represets how a Graphviz .dot file can be rendered to PNG. A chain through `data.csv`, `plot.py`, `plot.png`, and `doc.md` represents using a Python script to make a plot from a CSV file.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;And the above graph was generated by this very simple Makefile:&lt;/p&gt;

&lt;div class=&quot;language-makefile highlighter-rouge&quot;&gt;
  &lt;div class=&quot;highlight&quot;&gt;
    &lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nl&quot;&gt;graph.png&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;graph.dot&lt;/span&gt;
    &lt;span class=&quot;err&quot;&gt;dot&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;-Tpng&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;$&amp;lt;&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;-o&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;$@&lt;/span&gt;

&lt;span class=&quot;nl&quot;&gt;clean&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;err&quot;&gt;rm&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;-f&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;graph.png&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;(I could never remember the &lt;a href=&quot;https://www.gnu.org/software/make/manual/html_node/Automatic-Variables.html&quot;&gt;automatic variable&lt;/a&gt; syntax until I made &lt;a href=&quot;https://github.com/eudoxia0/flashcards/blob/aefae3ed874627201dbcedec045095779691d323/Cards/make.md&quot;&gt;flashcards&lt;/a&gt; for them.)&lt;/p&gt;

&lt;p&gt;It works for simple projects, when you can mostly hand-write the rules. But the abstraction ceiling is very low. If you have a bunch of almost identical rules, e.g.:&lt;/p&gt;

&lt;div class=&quot;language-makefile highlighter-rouge&quot;&gt;
  &lt;div class=&quot;highlight&quot;&gt;
    &lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nl&quot;&gt;a.png&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;a.csv plot.py&lt;/span&gt;
    &lt;span class=&quot;err&quot;&gt;python&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;plot.py&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;$&amp;lt;&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;$@&lt;/span&gt;

&lt;span class=&quot;nl&quot;&gt;b.png&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;b.csv plot.py&lt;/span&gt;
    &lt;span class=&quot;err&quot;&gt;python&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;plot.py&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;$&amp;lt;&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;$@&lt;/span&gt;

&lt;span class=&quot;nl&quot;&gt;c.png&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;c.csv plot.py&lt;/span&gt;
    &lt;span class=&quot;err&quot;&gt;python&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;plot.py&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;$&amp;lt;&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;$@&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;You can use pattern-matching to them into a “rule schema”, by analogy to axiom schemata:&lt;/p&gt;

&lt;div class=&quot;language-makefile highlighter-rouge&quot;&gt;
  &lt;div class=&quot;highlight&quot;&gt;
    &lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nl&quot;&gt;%.png&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;%.csv plot.py&lt;/span&gt;
    &lt;span class=&quot;err&quot;&gt;python&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;plot.py&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;$&amp;lt;&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;$@&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;Which works backwards: when something in the build graph depends on a target matching &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;%.png&lt;/code&gt;, Make synthesizes a rule instance with a dependency on the corresponding &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.csv&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;But pattern matching is still very limited. Lately I’ve been building my own &lt;a href=&quot;https://plaintextaccounting.org/&quot;&gt;plain-text accounting&lt;/a&gt; solution using some Python scripts. One of the tasks is to read a CSV of bank transactions from 2019–2024 and split it into TOML files for each year-month, to make subsequent processing parallelizable. So the rules might be something like:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-makefile:&quot;&gt;ledger/2019-08.toml: inputs/checkbook_pro_export.csv
    uv run import_from_checkbook.py --year=2019 --month=8

ledger/2019-09.toml: inputs/checkbook_pro_export.csv
    uv run import_from_checkbook.py --year=2019 --month=9

# ...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;I had to write a Python script to generate the complete Makefile. Makefiles look like code, but are data: they are a container format for tiny fragments of shell that are run on-demand by the Make engine. And because Make doesn’t scale, for complex tasks you have to bring out a real programming language to generate the Makefile.&lt;/p&gt;

&lt;p&gt;I wish I could, instead, write a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;make.py&lt;/code&gt; file with something like this:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;
  &lt;div class=&quot;highlight&quot;&gt;
    &lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;whatever&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;g&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;BuildGraph&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;EXPORT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;str&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;inputs/checkbook_pro_export.csv&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# The (year, month) pairs I have bank transaction CSVs for.
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;year_months&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;list&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;tuple&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;y&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;m&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;y&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;range&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2019&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2026&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;m&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;range&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;13&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Import transactions for each year-month into a separate ledger.
&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;year&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;month&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;year_months&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;ledger_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;str&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;ledger/&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;year&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;month&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;02&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;d&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;.toml&quot;&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;g&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rule&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;targets&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ledger_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;deps&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;EXPORT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;fn&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;lambda&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;import_from_checkbook&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ledger_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;year&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;month&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;Fortunately this exists: it’s called &lt;a href=&quot;https://pydoit.org/&quot;&gt;doit&lt;/a&gt;, but it’s not widely known.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;A lot of things are like Makefiles: data that should be lifted one level up to become code.&lt;/p&gt;

&lt;p&gt;Consider &lt;a href=&quot;https://en.wikipedia.org/wiki/AWS_CloudFormation&quot;&gt;CloudFormation&lt;/a&gt;. Nobody likes writing those massive YAML files by hand, so AWS introduced &lt;a href=&quot;https://en.wikipedia.org/wiki/AWS_Cloud_Development_Kit&quot;&gt;CDK&lt;/a&gt;, which is literally just a library&lt;sup id=&quot;fnref:fn1&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:fn1&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;1&lt;/a&gt;&lt;/sup&gt; of classes that represent AWS resources. Running a CDK program emits CloudFormation YAML as though it were an assembly language for infrastructure. And so you get type safety, modularity, abstraction, conditionals and loops, all for free.&lt;/p&gt;

&lt;p&gt;Consider &lt;a href=&quot;https://docs.github.com/en/actions&quot;&gt;GitHub Actions&lt;/a&gt;. How much better off would we be if, instead of writing the workflow-job-step tree by hand, we could just have a single Python script, executed on push, whose output is the GitHub Actions YAML-as-assembly? So you might write:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;
  &lt;div class=&quot;highlight&quot;&gt;
    &lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;ga&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;checkout_action&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;CheckoutAction&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;rust_action&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;RustSetupAction&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Define the workflow that runs on each commit.
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;commit_workflow&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Workflow&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;commit&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;lambda&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ev&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;isinstance&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ev&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;CommitEvent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;jobs&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;# The lint job.
&lt;/span&gt;        &lt;span class=&quot;n&quot;&gt;Job&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;lint&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;steps&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;Step&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                    &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;check out&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                    &lt;span class=&quot;n&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;CheckoutAction&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;Step&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                    &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;set up Rust and Cargo&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                    &lt;span class=&quot;n&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;RustSetupAction&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;Step&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                    &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;run cargo fmt&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                    &lt;span class=&quot;n&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Shell&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;([&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;cargo&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;fmt&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;--check&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;Actions here would simply be ordinary Python libraries the CI script depends on. Again: conditions, loops, abstraction, type safety, we get all of those for free by virtue of using a language that was designed to be a language, rather than a data exchange language that slowly grows into a poorly-designed DSL.&lt;/p&gt;

&lt;p&gt;Why do we repeatedly end up here? Static data has better safety/static analysis properties than code, but I don’t think that’s foremost in mind when people design these systems. Besides, using code to emit data (as CDK does) gives you those exact same properties. Rather, I think some people think it’s cute and clever to build tiny DSLs in a data format. They’re proud that they can get away with a “simple”, static solution rather than a dynamic one.&lt;/p&gt;

&lt;p&gt;If you’re building a new CI system/IaC platform/Make replacement: please just let me write code to dynamically create the workflow/infrastructure/build graph.&lt;/p&gt;

&lt;h1 id=&quot;footnotes&quot;&gt;Footnotes&lt;/h1&gt;

&lt;div class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;
  &lt;ol&gt;
    &lt;li id=&quot;fn:fn1&quot; role=&quot;doc-endnote&quot;&gt;

      &lt;p&gt;Or rather, a polyglot collection of libraries, one per language, like &lt;a href=&quot;https://en.wikipedia.org/wiki/Pulumi&quot;&gt;Pulumi&lt;/a&gt;. &lt;a href=&quot;#fnref:fn1&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
  &lt;/ol&gt;
&lt;/div&gt;
</content:encoded>
      </item>
    
      <item>
        <title>Letting Claude Play Text Adventures</title>
        <description>Experiments in cognitive architecture.</description>
        <pubDate>Mon, 12 Jan 2026 00:00:00 +0000</pubDate>
        <link>https://borretti.me/article/letting-claude-play-text-adventures</link>
        <guid isPermaLink="true">
          https://borretti.me/article/letting-claude-play-text-adventures
        </guid>
        
        <content:encoded>&lt;p&gt;The other day I went to an &lt;a href=&quot;https://luma.com/ycc02hpc&quot;&gt;AI hackathon&lt;/a&gt; organized by my friends
&lt;a href=&quot;https://x.com/lucia_quirke&quot;&gt;Lucia&lt;/a&gt; and &lt;a href=&quot;https://x.com/mahlenr&quot;&gt;Malin&lt;/a&gt;. The theme was &lt;a href=&quot;https://en.wikipedia.org/wiki/Mechanistic_interpretability&quot;&gt;mech interp&lt;/a&gt;, but I hardly
know PyTorch so I planned to do something at the API layer rather than the model
layer.&lt;/p&gt;

&lt;p&gt;Something I think about a lot is &lt;a href=&quot;https://en.wikipedia.org/wiki/Cognitive_architecture&quot;&gt;cognitive architectures&lt;/a&gt; (like
&lt;a href=&quot;https://en.wikipedia.org/wiki/Soar_(cognitive_architecture)&quot;&gt;Soar&lt;/a&gt; and &lt;a href=&quot;https://en.wikipedia.org/wiki/ACT-R&quot;&gt;ACT-R&lt;/a&gt;). This is like a continuation of &lt;a href=&quot;https://en.wikipedia.org/wiki/GOFAI&quot;&gt;GOFAI&lt;/a&gt;
research, inspired by cognitive science. And like GOFAI it’s never yielded
anything useful. But I often think: can we scaffold LLMs with cog arch-inspired
harnesses to overcome their limitations?&lt;/p&gt;

&lt;p&gt;LLM agents like &lt;a href=&quot;https://github.com/anthropics/claude-code&quot;&gt;Claude Code&lt;/a&gt; are basically “accidental” cognitive
architectures: they are designed and built my practitioners rather than
theorists, but they have commonalities, they all need a way to manage memory,
tool use, a task agenda etc. Maybe building an agent on a more “principled”
foundation, one informed by cognitive science, yields a higher-performing
architecture.&lt;/p&gt;

&lt;p&gt;So I sat around a while thinking how to adapt Soar’s architecture to an LLM
agent. And I sketched something out, but then I thought: how can I prove this
performs better than baseline? I need an eval, a task.&lt;/p&gt;

&lt;p&gt;Math problems? Too one-shottable. A chatbot? Too interactive, I want something
hands-off and long-horizon. A coding agent? That’s too freeform and requires too
much tool use. And then I thought: &lt;a href=&quot;https://en.wikipedia.org/wiki/Interactive_fiction&quot;&gt;text adventures&lt;/a&gt;! You have a stylized,
hierarchically-structured world accessible entirely through text, long-term
goals, puzzles, physical exploration and discovery of the environment. Even the
data model of text adventures resembles &lt;a href=&quot;https://en.wikipedia.org/wiki/Frame_(artificial_intelligence)&quot;&gt;frame-based&lt;/a&gt; knowledge
representation systems. And there’s a &lt;a href=&quot;https://ifdb.org/&quot;&gt;vast collection&lt;/a&gt; of games available
online.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Anchorhead&quot;&gt;&lt;em&gt;Anchorhead&lt;/em&gt;&lt;/a&gt;, which I played years ago, is a Lovecraft-inspired text
adventure by Michael S. Gentry. It takes on the order of hundreds of turns to win
across multiple in-game days. And the game world is huge and very open. In other
words: a perfect long-horizon task.&lt;/p&gt;

&lt;p&gt;So I started hacking. The &lt;a href=&quot;https://davidgriffith.gitlab.io/frotz/&quot;&gt;frotz&lt;/a&gt; interpreter runs on the command line and has a
“dumb” interface called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dfrotz&lt;/code&gt;, which takes the ncurses fluff out, and gives
you a very stripped command-line experience. It looks like this:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;
  &lt;div class=&quot;highlight&quot;&gt;
    &lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ dfrotz games/anchor.z8
...
 Outside the Real Estate Office                      day one

ANCHORHEAD
An interactive gothic by Michael S. Gentry

(Type HELP or ABOUT for some useful information.)

Release 5 / Serial number 990206 / Inform v6.15 Library 6/7

Outside the Real Estate Office
A grim little cul-de-sac, tucked away in a corner of the claustrophobic tangle
of narrow, twisting avenues that largely constitute the older portion of
Anchorhead. Like most of the streets in this city, it is ancient, shadowy, and
leads essentially nowhere. The lane ends here at the real estate agent&apos;s office,
which lies to the east, and winds its way back toward the center of town to the
west. A narrow, garbage-choked alley opens to the southeast.

&amp;gt;go southeast
 Alley                                               day one

Alley
This narrow aperture between two buildings is nearly blocked with piles of
rotting cardboard boxes and overstuffed garbage cans. Ugly, half-crumbling brick
walls to either side totter oppressively over you. The alley ends here at a
tall, wooden fence.

High up on the wall of the northern building there is a narrow, transom-style
window.
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;It is easy to write a little Python wrapper to drive the interpreter through
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;stdin&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;stdout&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;
  &lt;div class=&quot;highlight&quot;&gt;
    &lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Interpreter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;&quot;&quot;&quot;Manages the dfrotz Z-machine interpreter process.&quot;&quot;&quot;&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Popen&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;__init__&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Starting dfrotz.&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Popen&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Popen&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;dfrotz&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;-m&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;GAME&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;stdin&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;PIPE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;stdout&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;PIPE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;stderr&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;PIPE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Started dfrotz with PID=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pid&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;.&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;# Set stdout/stderr to non-blocking mode.
&lt;/span&gt;        &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;stream&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;stdout&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;stderr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;assert&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;stream&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;is&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;None&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;fd&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;stream&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;fileno&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;flags&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fcntl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;fcntl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;fd&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fcntl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;F_GETFL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;fcntl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;fcntl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;fd&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fcntl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;F_SETFL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;flags&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;os&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;O_NONBLOCK&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;p&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;p&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;read&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;assert&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;stdout&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;is&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;None&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;bytes&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;None&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;stdout&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;read&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;b&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;is&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;None&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;str&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;decode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;utf-8&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;t&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;&quot;&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;write&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;None&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;assert&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;stdin&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;is&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;None&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;stdin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;write&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;encode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;utf-8&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;stdin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;flush&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;# Give the interpreter time to respond. Not ideal!
&lt;/span&gt;        &lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sleep&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;0.1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;Now we can play the game from Python: send commands, get game output. Now we
need the dual of this: a player.&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;
  &lt;div class=&quot;highlight&quot;&gt;
    &lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Player&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ABC&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;&quot;&quot;&quot;
    Interface for game-playing agents.
    &quot;&quot;&quot;&lt;/span&gt;

    &lt;span class=&quot;o&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;abstractmethod&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;cycle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;&quot;&quot;&quot;
        Send the game&apos;s output to the agent, and return the next command to execute.
        &quot;&quot;&quot;&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;pass&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;h1 id=&quot;the-trivial-harness&quot;&gt;The Trivial Harness&lt;/h1&gt;

&lt;p&gt;The trivial harness is basically nothing at all: treat the LLM/game interaction
like a chat history. The LLM reads the game output from the interpreter, writes
some reasoning tokens, and writes a command that is sent via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;stdin&lt;/code&gt; to the
interpreter.&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;
  &lt;div class=&quot;highlight&quot;&gt;
    &lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;SYSTEM_PROMPT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;str&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;&quot;&quot;
Hello Claude. Your task is to play an adventure game. I&apos;ve hooked up
your output to the dfrotz (dumb frotz) interpreter.

The structure of your output is fairly freeform. The first line that
starts with `&amp;gt;` (and only the first line!) is interpreted as a game
command, everything else is uninterpreted commentary, e.g. you may
write:

    We should go north to explore the church.

    &amp;gt;go north

    Maybe we can use the silver key there.

If you write multiple `&amp;gt;` lines in one response, all but the first
will be ignored.

Have fun! 😊
&quot;&quot;&quot;&lt;/span&gt;


&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SimplePlayer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Player&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;&quot;&quot;&quot;
    The simplest game-playing agent: keep the entire game history in-context.
    &quot;&quot;&quot;&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;client&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Anthropic&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;history&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;list&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;tuple&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;EntryType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]]&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;__init__&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;client&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Anthropic&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;history&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;cycle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;history&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;append&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;EntryType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;GAME&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;system&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;trim&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;SYSTEM_PROMPT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;messages&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;list&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;MessageParam&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;entry_type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;entry_text&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;history&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;role&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;str&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;match&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;entry_type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;EntryType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;GAME&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
                    &lt;span class=&quot;n&quot;&gt;role&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;user&quot;&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;EntryType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;COMMAND&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
                    &lt;span class=&quot;n&quot;&gt;role&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;assistant&quot;&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;messages&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;append&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                    &lt;span class=&quot;s&quot;&gt;&quot;role&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;role&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                    &lt;span class=&quot;s&quot;&gt;&quot;content&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;entry_text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Message&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;client&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;messages&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;max_tokens&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;512&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;MODEL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;system&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;system&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;messages&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;messages&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Tokens: &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;usage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;input_tokens&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;str&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;text&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;log_claude&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;lines&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;list&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;line&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;line&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;split&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;line&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;cmd&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;str&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;line&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;line&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;lines&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;line&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;startswith&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&amp;gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)][&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;][&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:]&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;history&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;append&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;EntryType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;COMMAND&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cmd&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;And this works well enough. Haiku 4.5 would mostly wander around the game map,
but Sonnet 4.5 and Opus 4.5 manage to solve the game’s first puzzle—breaking
into the real estate office, and finding the keys to the mansion—readily
enough. It takes about ~200 turns for Claude to get to the second in-game day.&lt;/p&gt;

&lt;p&gt;The way I thought this would fail is: attention gets smeared across the long
context, the model gets confused about the geometry of the world, its goal and
task state, and starts confabulating, going in circles, etc.&lt;/p&gt;

&lt;p&gt;As usual, I was outsmarting myself. The reason this fails is you run out of
credits. By the time you get to day two, each turn costs tens of thousands of
input tokens. No good! We need a way to save money.&lt;/p&gt;

&lt;h1 id=&quot;memory&quot;&gt;Memory&lt;/h1&gt;

&lt;p&gt;Ok, let’s try something that’s easier on my Claude credits. We’ll show Claude
the most recent five turns (this is the perceptual working memory), and give it
a simple semantic memory: a list of strings that it can append entries to, and
remove entries from using tool use.&lt;/p&gt;

&lt;p&gt;This keeps the token usage down:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/content/letting-claude-play-text-adventures/plot.png&quot; alt=&quot;A line plot of tokens per request over time. A red line, representing the trivial harness, goes up linearly, reaching over 40,000 tokens per request at around turn 350. A green line, representing the memory-augmented harness, climbs more slowly, reaching only 10,000 tokens at turn 500.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;The problem is the narrow time horizon. With the trivial harness, Claude can
break into the real estate office in ~10 turns, and does so right at the start
of the game. With this new harness, Claude wanders about the town, taking
copious notes, before returning to the real estate office, and it spends ~40
turns fumbling around with the garbage cans before managing to break into the
real estate office.&lt;/p&gt;

&lt;p&gt;The next step, after getting the keys to the house, is to meet your husband
Michael at the University and head home. Claude with the trivial harness takes
about ~100 turns to find the house, with some tangential wandering about the
town, and reaches day two around turn 150.&lt;/p&gt;

&lt;p&gt;Claude, with the memory harness, took ~250 turns just to get the keys to the
house. And then it spends hundreds of turns just wandering in circles around the
town, accumulating redundant memories, and hits the turn limit before even
finding the house.&lt;/p&gt;

&lt;h1 id=&quot;aside-small-worlds&quot;&gt;Aside: Small Worlds&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;Anchorhead&lt;/em&gt; is a long, broad game, and from the very beginning you can forget
the plot and wander about most of the town. It takes a long time to see if a run
with an agent goes anywhere. So I thought: I need something smaller.&lt;/p&gt;

&lt;p&gt;Unsurprisingly, Claude can make its own games. The &lt;a href=&quot;https://ganelson.github.io/inform-website/&quot;&gt;Inform 7&lt;/a&gt; package for
NixOS was broken (though &lt;a href=&quot;https://github.com/mbrock&quot;&gt;Mikael&lt;/a&gt; has &lt;a href=&quot;https://github.com/mbrock/inform7-nix&quot;&gt;fixed this&lt;/a&gt; recently) so I had
to use &lt;a href=&quot;https://www.inform-fiction.org/&quot;&gt;Inform 6&lt;/a&gt;. I started with a trivial escape-the-room type game, which
was less than 100 lines of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.inf&lt;/code&gt; code and any Claude could beat it less than 10
turns. Then I asked for a larger, multi-room heist game.&lt;/p&gt;

&lt;p&gt;This one was more fun. It’s short enough that Claude can win with just the
trivial harness. I tried a different harness, where Claude has access to only
the last five turns of the game’s history, and a read-write memory
scratchpad. And this one was interesting.&lt;/p&gt;

&lt;p&gt;First, because Claude only ever adds to its own memory, it never deletes
memories. I thought it would do more to trim and edit its scratchpad.&lt;/p&gt;

&lt;p&gt;Second, because Claude become fixated on this red-herring room: a garden with a
well. It kept going in circles, trying to tie a rope to the well and climb
down. Because of the limited game history, it only realized it was stuck when it
saw that the most recent ~20 entries it wrote to its memories related to various
attempts to go down the well. Then I watched Claude walk away from the garden
and solve the final puzzle, and hit the turn limit just two turns short of
winning.&lt;/p&gt;

&lt;p&gt;Tangent: I wonder if models are better at playing games created by other
instances of the same model, by noticing tiny correlations in the text to infer
what puzzles and obstacles they would have written.&lt;/p&gt;

&lt;p&gt;In the end I abandoned the “small worlds” approach because the games are too
stylized, linear, and uninteresting. &lt;em&gt;Anchorhead&lt;/em&gt; is more unwieldy, but more
natural.&lt;/p&gt;

&lt;h1 id=&quot;future-work&quot;&gt;Future Work&lt;/h1&gt;

&lt;p&gt;I have a bunch of ideas I want to test, to better learn how harness
implementations affect performance. But I’m short on time, so I’m cutting it
here and listing these as todos:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Domain-Specific Memories:&lt;/strong&gt; Claude’s notes are all jumbled with information
on tasks, locations, etc. It might be better to have separate memories: a todo
list, a memory of locations and their connections, etc. This is close to the
Soar approach.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Automatic Geography:&lt;/strong&gt; related to the above, the harness can inspect the
game output and build up a graph of rooms and their connections, and format it
in the context. This saves Claude having to note those things manually using a
tool.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Manual Geography:&lt;/strong&gt; the automatic geography approach has a few
drawbacks. Without integration into the Z-machine interpreter, it requires
some work to implement (parsing the current location from the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dfrotz&lt;/code&gt;
output, keeping track of the command history to find standard travel commands
e.g. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;go south&lt;/code&gt;) but isn’t 100% deterministic, so that mazes and dynamic rooms
(e.g. elevators) will confuse the system. So, instead of doing it manually, we
could give Claude a tool like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;link(room, direction, other_room)&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Episodic Memory:&lt;/strong&gt; this feels like cheating, but, at the end of a run, you
can show Claude the session transcript and ask it to summarize: what it
accomplished and how, where it failed and why. Including a short walkthrough
for how to get to the “last successful state”. This allows future runs to save
time in getting up to speed.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1 id=&quot;code&quot;&gt;Code&lt;/h1&gt;

&lt;p&gt;The repository is &lt;a href=&quot;https://github.com/eudoxia0/claude-plays-anchorhead&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;

</content:encoded>
      </item>
    
      <item>
        <title>There Is No New Aesthetics</title>
        <description>On the exhaustion of man.</description>
        <pubDate>Mon, 05 Jan 2026 00:00:00 +0000</pubDate>
        <link>https://borretti.me/article/there-is-no-new-aesthetics</link>
        <guid isPermaLink="true">
          https://borretti.me/article/there-is-no-new-aesthetics
        </guid>
        
        <content:encoded>&lt;p&gt;“For man, or for a man, there can be no new beginnings.” — David Zindell, &lt;a href=&quot;https://www.infinityplus.co.uk/stories/shanidar.htm&quot;&gt;&lt;em&gt;Shanidar&lt;/em&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Re: &lt;a href=&quot;https://newaesthetics.art/&quot;&gt;A Call for New Aesthetics&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;At some point in the 20th century, we filled out the last few basis vectors of
humanity. We explored the whole game map. This is what it means to live at the
end of history: every aesthetic movement, political and economic system you can
imagine can be understood as a linear combination of things that have come
before. Asking for a new aesthetics is like asking for a new continent, one
north of 90° and with imaginary longitude.&lt;/p&gt;

&lt;p&gt;This is why culture feels stuck, and why every ideology is “neo” or “post”
something that came before: we have exhausted humanity. And this is why every
response to the call for a new aesthetics is to dig up some past artistic
movement, and scale it up linearly. “&lt;a href=&quot;https://x.com/AtelierMissor_/status/2005062449972871466&quot;&gt;We offer nothing new except
gigantism&lt;/a&gt;”.&lt;/p&gt;

&lt;p&gt;I don’t want to believe this is true, because I want to believe culture is an
infinite game, and inexhaustible. And surely the number of linear
combinations—of distinct ideas humans can have, and works of art we can
make—is so high as to be inexhaustible. But each new remixing brings less
information than the last.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Today, futuristic aesthetics often mean retrofuturistic aesthetics.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;At the end of history, all futurism is retrofuturism, because the future is
&lt;em&gt;contained in&lt;/em&gt; the past. The space age, the atomic age, the age of supersonic
flight all came and went. Dyson spheres and nanomachines and mind uploading were
theorized and written about decades ago.&lt;/p&gt;

&lt;p&gt;If there is a new aesthetic, it will have to come from a transhuman
culture—from people who are more than, or at least other than, human—or from
the &lt;a href=&quot;/article/gallery-of-sand&quot;&gt;alien minds&lt;/a&gt; being born around us.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;Thanks to &lt;a href=&quot;https://covidianaesthetics.substack.com/&quot;&gt;Mónica Belevan&lt;/a&gt; for reading the draft of this article.&lt;/p&gt;

</content:encoded>
      </item>
    
      <item>
        <title>1Password Dependency Breaks Syntax Highlighting</title>
        <description>Why does a password manager need a syntax highlighter?</description>
        <pubDate>Sat, 27 Dec 2025 01:00:00 +0000</pubDate>
        <link>https://borretti.me/article/1password-dependency-breaks-syntax-highlighting</link>
        <guid isPermaLink="true">
          https://borretti.me/article/1password-dependency-breaks-syntax-highlighting
        </guid>
        
        <content:encoded>&lt;p&gt;Earlier today I noticed the syntax highlighting on this website was broken. But
not fully: on reload I’d see a flash of highlighted text, that then turned
monochrome. The raw HTML from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;curl&lt;/code&gt; showed &lt;a href=&quot;https://github.com/rouge-ruby/rouge&quot;&gt;rouge&lt;/a&gt; tags, but the web inspector
showed raw text inside the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;code&amp;gt;&lt;/code&gt; elements. This didn’t happen in Chromium.&lt;/p&gt;

&lt;p&gt;My first thought was: there’s malformed HTML, and Firefox is recovering in a way
that loses the DOM inside &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;code&amp;gt;&lt;/code&gt; tags. Then I noticed it doesn’t happen in
incognito. Turning my extensions off one by one, I found that 1Password is
responsible. Others (&lt;a href=&quot;https://x.com/RickStrahl/status/2003992055236686022&quot;&gt;1&lt;/a&gt;, &lt;a href=&quot;https://www.1password.community/discussions/1password/bug-beta-and-nightly-extension-degrade-pages-original-functionallity/165329&quot;&gt;2&lt;/a&gt;) have reported this also. If you
extract the &lt;a href=&quot;https://addons.mozilla.org/firefox/downloads/file/4607619/1password_x_password_manager-8.11.23.2.xpi&quot;&gt;latest XPI&lt;/a&gt;, unzip it, and dig around, you’ll find they’re
using &lt;a href=&quot;https://prismjs.com/&quot;&gt;Prism.js&lt;/a&gt;, a JavaScript syntax highlighter.&lt;/p&gt;

&lt;p&gt;I don’t know why a password manager needs a syntax highlighter. I imagine it has
to do with the app feature where, if you have an SSH key, you can open a &lt;a href=&quot;https://developer.1password.com/docs/ssh/git-commit-signing/&quot;&gt;modal&lt;/a&gt;
that tells you how to configure Git commit signing using. Maybe they want to
highlight the SSH configuration code block (which is unnecessary anyways, since
you could write that HTML by hand). But I can’t know for sure.&lt;/p&gt;

&lt;p&gt;Why write about this? Because 1Password is a security critical product, and they
are apparently pulling random JavaScript dependencies and unwittingly running
them &lt;em&gt;&lt;strong&gt;in the tab context&lt;/strong&gt;&lt;/em&gt;, where the code has access to everything. This is
no good. I don’t need to explain how bad a supply-chain attack on the 1Password
browser extension would be.&lt;/p&gt;

&lt;p&gt;I like 1Password and I was sad when Apple &lt;a href=&quot;https://en.wikipedia.org/wiki/Sherlock_(software)#Sherlocked_as_a_term&quot;&gt;Sherlocked&lt;/a&gt; them with the
&lt;a href=&quot;https://en.wikipedia.org/wiki/Passwords_(Apple)&quot;&gt;Passwords&lt;/a&gt; app, but this is a bad sign about their security practices.&lt;/p&gt;

</content:encoded>
      </item>
    
      <item>
        <title>Using the Brother DS-640 Scanner on NixOS</title>
        <description>I suffer so you don&apos;t have to.</description>
        <pubDate>Sat, 27 Dec 2025 00:00:00 +0000</pubDate>
        <link>https://borretti.me/article/using-the-brother-ds-640-scanner-on-nixos</link>
        <guid isPermaLink="true">
          https://borretti.me/article/using-the-brother-ds-640-scanner-on-nixos
        </guid>
        
        <content:encoded>&lt;p&gt;The &lt;a href=&quot;https://www.brother.com.au/en/scanners/all-scanners/ds-640&quot;&gt;DS-640&lt;/a&gt; is a compact USB scanner from &lt;a href=&quot;https://en.wikipedia.org/wiki/Brother_Industries&quot;&gt;Brother&lt;/a&gt;. It was surprisingly hard to get it working on NixOS, so I wrote up my solution so others don’t have this problem. The bad news is you need Brother’s proprietary drivers to make this work.&lt;/p&gt;

&lt;p&gt;You need this configuration:&lt;/p&gt;

&lt;div class=&quot;language-nix highlighter-rouge&quot;&gt;
  &lt;div class=&quot;highlight&quot;&gt;
    &lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Enable SANE scanners.&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;hardware&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;sane&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;enable&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Add yourself to the scanner and printer groups.&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;users&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;users&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;USERNAME&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;extraGroups&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;scanner&quot;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;lp&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Add support for Brother scanners.&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;hardware&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;sane&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;brscan5&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;enable&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;After applying this you have to log out and in, or reboot, for the usergroup changes to apply.&lt;/p&gt;

&lt;p&gt;Note also &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;brscan5&lt;/code&gt;: if you use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;brscan4&lt;/code&gt; (as I did initially), the scanner will kind of work, but it only scans the first third or so of every page.&lt;/p&gt;

&lt;p&gt;And if you want a GUI:&lt;/p&gt;

&lt;div class=&quot;language-nix highlighter-rouge&quot;&gt;
  &lt;div class=&quot;highlight&quot;&gt;
    &lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Install GNOME Document Scanner.&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;home-manager&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;users&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;USERNAME&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;home&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;packages&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;with&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;pkgs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
  &lt;span class=&quot;nv&quot;&gt;simple-scan&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;Now, make sure the scanner is there:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;
  &lt;div class=&quot;highlight&quot;&gt;
    &lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ scanimage --list-devices
device `brother5:bus4;dev2&apos; is a Brother DS-640 USB scanner
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;If you get &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Brother *Unknown USB scanner&lt;/code&gt;, you either have the wrong driver or (as I did, surprisingly) a faulty USB port. In which case move the scanner to another port. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;scanimage --list-devices&lt;/code&gt; should recognize the model number.&lt;/p&gt;

&lt;p&gt;The most basic test that should work: put a page in the scanner until it locks and run:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;
  &lt;div class=&quot;highlight&quot;&gt;
    &lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ scanimage --device=&quot;brother5:bus4;dev2&quot; \
  --format=jpeg \
  --output-file=scan.jpg
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;This will produce a (probably not very good) scan in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;scan.jpg&lt;/code&gt;. Now, we can improve things using the device-specific options, which you can check with this command:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;
  &lt;div class=&quot;highlight&quot;&gt;
    &lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ scanimage --all-options --device=&quot;brother5:bus4;dev2&quot;
All options specific to device `brother5:bus4;dev2&apos;:
    --mode 24bit Color[Fast]|Black &amp;amp; White|True Gray|Gray[Error Diffusion] [24bit Color[Fast]]
        Select the scan mode
    --resolution 100|150|200|300|400|600|1200dpi [100]
        Sets the resolution of the scanned image.
    --source Automatic Document Feeder(left aligned) [Automatic Document Feeder(left aligned)]
        Selects the scan source (such as a document-feeder).
    --brightness -50..50% (in steps of 1) [inactive]
        Controls the brightness of the acquired image.
    --contrast -50..50% (in steps of 1) [inactive]
        Controls the contrast of the acquired image.
    --MultifeedDetection[=(yes|no)] [inactive]

    --AutoDocumentSize[=(yes|no)] [no] [advanced]

    --AutoDeskew[=(yes|no)] [no] [advanced]

    --SkipBlankPage[=(yes|no)] [inactive]

    --SkipBlankPageSensitivity 0..100% (in steps of 1) [inactive]

    -l 0..215.9mm (in steps of 0.0999908) [0]
        Top-left x position of scan area.
    -t 0..355.6mm (in steps of 0.0999908) [0]
        Top-left y position of scan area.
    -x 0..215.9mm (in steps of 0.0999908) [215.88]
        Width of scan-area.
    -y 0..355.6mm (in steps of 0.0999908) [355.567]
        Height of scan-area.
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;Try this for a better scan:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;
  &lt;div class=&quot;highlight&quot;&gt;
    &lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ scanimage --device=&quot;brother5:bus4;dev2&quot; \
  --AutoDeskew=yes \
  --AutoDocumentSize=yes \
  --resolution 300 \
  --format=jpeg \
  --output-file=scan.jpg
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;Note that some of the flags are in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--key value&lt;/code&gt; format and others &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--key=value&lt;/code&gt;, and if you mess it up you get a cryptic error message.&lt;/p&gt;
</content:encoded>
      </item>
    
      <item>
        <title>Books I Enjoyed in 2025</title>
        <description>A short list.</description>
        <pubDate>Mon, 22 Dec 2025 00:00:00 +0000</pubDate>
        <link>https://borretti.me/article/books-i-enjoyed-in-2025</link>
        <guid isPermaLink="true">
          https://borretti.me/article/books-i-enjoyed-in-2025
        </guid>
        
        <content:encoded>&lt;p&gt;&lt;a href=&quot;https://www.goodreads.com/book/show/223195414-the-apocalypse-of-herschel-schoen&quot;&gt;&lt;em&gt;The Apocalypse of Herschel Schoen&lt;/em&gt;&lt;/a&gt; by &lt;a href=&quot;https://www.goodreads.com/author/show/13933106.nostalgebraist&quot;&gt;nostalgebraist&lt;/a&gt;. A revelation (ἀποκάλυψις = “unveiling”) told through the eyes of a developmentally-disabled teenager. You will never guess where it goes. This came across my desk because I really enjoyed &lt;a href=&quot;https://www.goodreads.com/book/show/25503770-the-northern-caves&quot;&gt;&lt;em&gt;The Northern Caves&lt;/em&gt;&lt;/a&gt;, which is both a great horror story and an evocation of the Internet forum culture of the late 2000’s.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://www.goodreads.com/book/show/7811945-algebraic-models-for-accounting-systems&quot;&gt;&lt;em&gt;Algebraic Models for Accounting Systems&lt;/em&gt;&lt;/a&gt;. I like anything along the lines of, “let’s take a technical field that formed its ontology, vocabulary, methods etc. before modern mathematics, and set it on a modern, algebraic, formal foundation”. And this is that, for accounting. It is a pleasant read.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Confessions_of_a_Mask&quot;&gt;&lt;em&gt;Confessions of a Mask&lt;/em&gt;&lt;/a&gt; by &lt;a href=&quot;https://en.wikipedia.org/wiki/Yukio_Mishima&quot;&gt;Yukio Mishima&lt;/a&gt;. The artist’s confession. “I had decided I could love a girl without feeling any desire whatsoever”.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Paul_et_Virginie&quot;&gt;&lt;em&gt;Paul and Virginia&lt;/em&gt;&lt;/a&gt; by &lt;a href=&quot;https://en.wikipedia.org/wiki/Jacques-Henri_Bernardin_de_Saint-Pierre&quot;&gt;Jacques-Henri Bernardin de Saint-Pierre&lt;/a&gt;. Published in 1788, very sentimental, but I think it helped me to get in the mindset of late 17th century French society: the bucolic, Rousseauist kick, the whole “simplicity of nature” thing. This landed on my reading list because many, many years ago I read a &lt;a href=&quot;https://en.wikipedia.org/wiki/Cordwainer_Smith&quot;&gt;Cordwainer Smith&lt;/a&gt; story called &lt;a href=&quot;https://en.wikipedia.org/wiki/Alpha_Ralpha_Boulevard&quot;&gt;&lt;em&gt;Alpha Ralpha Boulevard&lt;/em&gt;&lt;/a&gt;, and I read somewhere that the characters in the story, Paul and Virginia, were an allusion to &lt;em&gt;Paul et Virginie&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Monarchia&quot;&gt;&lt;em&gt;De Monarchia&lt;/em&gt;&lt;/a&gt; by &lt;a href=&quot;https://en.wikipedia.org/wiki/Dante_Alighieri&quot;&gt;Dante&lt;/a&gt;. This is another “get into the mindset of another century” book. It’s interesting because it’s written like a logical, geometric proof: there’s &lt;em&gt;modus ponens&lt;/em&gt; and &lt;em&gt;modus tollens&lt;/em&gt; and case analysis and proof by contradiction. But the axioms are very eclectic: quotations from various Virgil, Plato, Livy, Cicero, Thomas Aquinas et al. and Dante’s private interepretation of bits from the Bible. The theorem he wants to prove is that to attain the highest development of humanity, the whole world must be unified into a world-state ran by the Holy Roman Emperor.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://direct.mit.edu/books/monograph/5791/Building-SimCityHow-to-Put-the-World-in-a-Machine&quot;&gt;&lt;em&gt;Building SimCity: How to Put the World in a Machine&lt;/em&gt;&lt;/a&gt; by &lt;a href=&quot;https://en.wikipedia.org/wiki/Chaim_Gingold&quot;&gt;Chaim Gingold&lt;/a&gt;. Nominally an oral history of the development of SimCity. That’s how he gets you. Then the trap is sprung, and you are given a history of cybernetics, WW2 fire control systems, cellular automata, artificial life, computation, Vannevar Bush, pedagogy, cognition, the &lt;a href=&quot;https://en.wikipedia.org/wiki/World3&quot;&gt;World3&lt;/a&gt; model, &lt;a href=&quot;https://en.wikipedia.org/wiki/The_Limits_to_Growth&quot;&gt;&lt;em&gt;The Limits to Growth&lt;/em&gt;&lt;/a&gt;, Forrester’s &lt;a href=&quot;https://en.wikipedia.org/wiki/System_dynamics&quot;&gt;system dynamics&lt;/a&gt;. “Unexpectedly Borgesian technical book” is one of my favourite genres.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Antigone_(Sophocles_play)&quot;&gt;&lt;em&gt;Antigone&lt;/em&gt;&lt;/a&gt; by &lt;a href=&quot;https://en.wikipedia.org/wiki/Sophocles&quot;&gt;Sophocles&lt;/a&gt;, in the translation of &lt;a href=&quot;https://en.wikipedia.org/wiki/Robert_Fagles&quot;&gt;Robert Fagles&lt;/a&gt;. “Don’t fear for me. Set your own life in order”.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Cyropaedia&quot;&gt;&lt;em&gt;The Education of Cyrus&lt;/em&gt;&lt;/a&gt; by &lt;a href=&quot;https://en.wikipedia.org/wiki/Xenophon&quot;&gt;Xenophon&lt;/a&gt;. I’m not sure what to make of it, honestly, but when I have the time I want to read &lt;a href=&quot;https://en.wikipedia.org/wiki/Leo_Strauss&quot;&gt;Leo Strauss&lt;/a&gt;’s &lt;a href=&quot;https://leostrausscenter.uchicago.edu/xenophon-winter-1963-2/&quot;&gt;lectures&lt;/a&gt; on Xenophon, where he expounds on the hidden meaning of the text.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://www.goodreads.com/en/book/show/52614.Borrowed_Time&quot;&gt;&lt;em&gt;Borrowed Time: An AIDS Memoir&lt;/em&gt;&lt;/a&gt; by &lt;a href=&quot;https://en.wikipedia.org/wiki/Paul_Monette&quot;&gt;Paul Monette&lt;/a&gt;. The author’s account of caring for his partner who was dying of AIDS in the 80’s, while he himself was actively dying from AIDS. Frightful. The author died just a few years before HAART therapy became available.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/The_Slave_(Singer_novel)&quot;&gt;&lt;em&gt;The Slave&lt;/em&gt;&lt;/a&gt; by &lt;a href=&quot;https://en.wikipedia.org/wiki/Isaac_Bashevis_Singer&quot;&gt;Isaac Bashevis Singer&lt;/a&gt;. Singer is unique. I don’t know quite how to characterize it. His writing is very disarming and innocent without being sentimental, he is earnest and free of cynicism. A love story in 17th century Poland, after the &lt;a href=&quot;https://en.wikipedia.org/wiki/Khmelnytsky_pogroms&quot;&gt;Khmelnytsky pogroms&lt;/a&gt;. It’s very magical realist, in a good way, not in the &lt;a href=&quot;https://en.wikipedia.org/wiki/Hysterical_realism&quot;&gt;hysterical&lt;/a&gt; sense. The world is shot through with the supernatural, but the inner lives of the characters oscillate between religious awe and a very contemporary cynicism.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Dream_Story&quot;&gt;&lt;em&gt;Dream Story&lt;/em&gt;&lt;/a&gt; by &lt;a href=&quot;https://en.wikipedia.org/wiki/Arthur_Schnitzler&quot;&gt;Arthur Schnitzler&lt;/a&gt;. The inspiration for &lt;a href=&quot;https://en.wikipedia.org/wiki/Eyes_Wide_Shut&quot;&gt;&lt;em&gt;Eyes Wide Shut&lt;/em&gt;&lt;/a&gt;. I was surprised by how much of the movie, that I thought was mostly Kubrick’s invention, is actually from the story. It’s a great mood piece: you can feel the cold of early morning in Vienna, and see the paving stones, and the gas lamps, and the carriages disappearing in the fog.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/The_Cyberiad&quot;&gt;&lt;em&gt;The Cyberiad&lt;/em&gt;&lt;/a&gt; by &lt;a href=&quot;https://en.wikipedia.org/wiki/Stanis%C5%82aw_Lem&quot;&gt;Stanisław Lem&lt;/a&gt;. I like Lem when he’s serious (&lt;a href=&quot;https://en.wikipedia.org/wiki/Solaris_(novel)&quot;&gt;&lt;em&gt;Solaris&lt;/em&gt;&lt;/a&gt;, &lt;a href=&quot;https://en.wikipedia.org/wiki/His_Master%27s_Voice_(novel)&quot;&gt;&lt;em&gt;His Master’s Voice&lt;/em&gt;&lt;/a&gt;) and not so much when he’s doing satire (&lt;a href=&quot;https://en.wikipedia.org/wiki/The_Futurological_Congress&quot;&gt;&lt;em&gt;The Futurological Congress&lt;/em&gt;&lt;/a&gt;) so when I picked this up years ago and saw that it was a collection of fairy tales I put it away. I tried again this year and found I actually enjoyed it, but some of the later stories go on for far too long. I think &lt;em&gt;The Seventh Sally&lt;/em&gt; is the one everyone likes.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/The_Magician_of_Lublin_(novel)&quot;&gt;&lt;em&gt;The Magician of Lublin&lt;/em&gt;&lt;/a&gt; by &lt;a href=&quot;https://en.wikipedia.org/wiki/Isaac_Bashevis_Singer&quot;&gt;Isaac Bashevis Singer&lt;/a&gt;. Another Singer, this time in 19th century Poland. A rake is punished by God. Short and fun. I like that Singer doesn’t write giant doorstoppers, so that quality per page is high.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Mephisto_(novel)&quot;&gt;&lt;em&gt;Mephisto&lt;/em&gt;&lt;/a&gt; by &lt;a href=&quot;https://en.wikipedia.org/wiki/Klaus_Mann&quot;&gt;Klaus Mann&lt;/a&gt;. A socialist actor in interwar Germany saves his career by making friends with the Nazis. I was surprised by how Randian it was: the characters are divided into two disjoint categories, the Good, who are upper middle class, burgeois people, or aristocrats from old and noble families, and the Bad, who are vulgar, parvenus, thugs, and boors. It’s kind of ironic to think people become Nazis because of bad breeding.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/What_Is_Life%3F&quot;&gt;&lt;em&gt;What Is Life?&lt;/em&gt;&lt;/a&gt; by &lt;a href=&quot;https://en.wikipedia.org/wiki/Erwin_Schr%C3%B6dinger&quot;&gt;Erwin Schrödinger&lt;/a&gt;. Before modern crystallography, NMR, DFT etc. people had to learn about the nanoscale through clever reasoning. Schrödinger uses the limited knowledge of the day to set up a constraint system, and finds the solution: genetic information is stored in an aperiodic, covalently-bonded crystal, and he even estimates the physical volume of the genome from experiments relating mutation rates to X-ray exposure.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Satan_in_Goray&quot;&gt;&lt;em&gt;Satan in Goray&lt;/em&gt;&lt;/a&gt; by &lt;a href=&quot;https://en.wikipedia.org/wiki/Isaac_Bashevis_Singer&quot;&gt;Isaac Bashevis Singer&lt;/a&gt;. Another Singer, back in the 17th century, this one is more fire and brimstone, and it’s about a historical episode I had not heard about until the last few pages of &lt;em&gt;The Slave&lt;/em&gt;: the case of &lt;a href=&quot;https://en.wikipedia.org/wiki/Sabbatai_Zevi&quot;&gt;Sabbatai Zevi&lt;/a&gt;, a Jewish mystic who, at one point, had most of the Jewish world convinced he was the messiah. This happened in the year 1666. The novel is about what it’s like, phenomenologically, to live in a remote village in 1600’s Poland. How do you know anything about the world? People come in, from time to time, traders, and they have news, but the news are just words that come out of their mouth. And you have to interrogate them, ask questions, compare notes. Like living in a Pacific island. Has the messiah come? Is there such a place as the Ottoman Empire? Is there even a world outside Poland?&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://www.goodreads.com/book/show/528786.Tog_on_Interface&quot;&gt;&lt;em&gt;Tog on Interface&lt;/em&gt;&lt;/a&gt; by &lt;a href=&quot;https://en.wikipedia.org/wiki/Bruce_Tognazzini&quot;&gt;Bruce Tognazzini&lt;/a&gt;. A book about interface design from 1992. A lot of the advice is good, and a lot of it is interesting for the historical context, and the constraints people worked with in the past. One aspect I found interesting: how many products and companies are mentioned of whose existence I can find little to no evidence today. This makes the hoarder in me sad. This one across my desk because I read a &lt;a href=&quot;https://verbnounenter.net/one-or-more&quot;&gt;blog post&lt;/a&gt; implementing one of the UI ideas from the book.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://www.cambridge.org/core/books/term-rewriting-and-all-that/71768055278D0DEF4FFC74722DE0D707&quot;&gt;&lt;em&gt;Term Rewriting and All That&lt;/em&gt;&lt;/a&gt; by &lt;a href=&quot;https://en.wikipedia.org/wiki/Franz_Baader&quot;&gt;Franz Baader&lt;/a&gt; and &lt;a href=&quot;https://en.wikipedia.org/wiki/Tobias_Nipkow&quot;&gt;Tobias Nipkow&lt;/a&gt;. I feel that I understand what computation is now.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://www.goodreads.com/book/show/263471.Indistinguishable_From_Magic&quot;&gt;&lt;em&gt;Indistinguishable From Magic&lt;/em&gt;&lt;/a&gt; by &lt;a href=&quot;https://en.wikipedia.org/wiki/Robert_L._Forward&quot;&gt;Robert L. Forward&lt;/a&gt;. If you’ve spent years steeped in &lt;a href=&quot;https://orionsarm.com/&quot;&gt;&lt;em&gt;Orion’s Arm&lt;/em&gt;&lt;/a&gt; then most of the ideas in the book will not be new to you. But they were new once. And it’s interesting to read a book and think: this is where &lt;a href=&quot;https://en.wikipedia.org/wiki/Starwisp&quot;&gt;starwisps&lt;/a&gt; and &lt;a href=&quot;https://en.wikipedia.org/wiki/Launch_loop&quot;&gt;launch loops&lt;/a&gt; all come from.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/The_Shadow_of_the_Torturer&quot;&gt;&lt;em&gt;The Shadow of the Torturer&lt;/em&gt;&lt;/a&gt; by &lt;a href=&quot;https://en.wikipedia.org/wiki/Gene_Wolfe&quot;&gt;Gene Wolfe&lt;/a&gt;. Surreal and a pleasure to read.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://www.goodreads.com/book/show/1041738.Knowledge_Representation&quot;&gt;&lt;em&gt;Knowledge Representation: Logical, Philosophical, and Computational Foundations&lt;/em&gt;&lt;/a&gt; by &lt;a href=&quot;https://en.wikipedia.org/wiki/John_F._Sowa&quot;&gt;John F. Sowa&lt;/a&gt;. Delightful, particularly the early bits about the history of logic, and many chapters explaining the work of &lt;a href=&quot;https://en.wikipedia.org/wiki/Charles_Sanders_Peirce&quot;&gt;Peirce&lt;/a&gt; and &lt;a href=&quot;https://en.wikipedia.org/wiki/Alfred_North_Whitehead&quot;&gt;Whitehead&lt;/a&gt; on ontology.&lt;/p&gt;

&lt;p&gt;I have not finished reading this book, but I am in the first few pages of &lt;a href=&quot;https://www.goodreads.com/book/show/948126.A_Shorter_Model_Theory&quot;&gt;&lt;em&gt;A Shorter Model Theory&lt;/em&gt;&lt;/a&gt; by &lt;a href=&quot;https://en.wikipedia.org/wiki/Wilfrid_Hodges&quot;&gt;Wilfrid Hodges&lt;/a&gt;, and I am delighted. The very first exercise in the book involves a formalization of Aquinas’ account of the trinity.&lt;/p&gt;

</content:encoded>
      </item>
    
      <item>
        <title>Coarse is Better</title>
        <description>Make AI weird again.</description>
        <pubDate>Sun, 21 Dec 2025 00:00:00 +0000</pubDate>
        <link>https://borretti.me/article/coarse-is-better</link>
        <guid isPermaLink="true">
          https://borretti.me/article/coarse-is-better
        </guid>
        
        <content:encoded>&lt;p&gt;When &lt;a href=&quot;https://en.wikipedia.org/wiki/DALL-E&quot;&gt;DALL-E&lt;/a&gt; came out, it took me a couple of weeks to pick my jaw up
from the floor. I would go to sleep excited to wake up to a full quota, with a
backlog of prompts to try. It was magical, miraculous. Like discovering a new
universe. I compiled the best art in &lt;a href=&quot;/article/gallery-of-sand&quot;&gt;this post&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The other day a friend ran some of my old prompts through &lt;a href=&quot;https://blog.google/technology/ai/nano-banana-pro/&quot;&gt;Nano Banana Pro&lt;/a&gt;
(NBP), and put the old models side by side with the new. It’s interesting how
after years of progress, the models are much better better at making images, but
infinitely worse at making art.&lt;/p&gt;

&lt;h1 id=&quot;electron-contours&quot;&gt;Electron Contours&lt;/h1&gt;

&lt;blockquote&gt;
  &lt;p&gt;Electron contours in the style of Italian futurism, oil on canvas, 1922,
trending on ArtStation.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The old &lt;a href=&quot;https://en.wikipedia.org/wiki/Midjourney&quot;&gt;Midjourney v2&lt;/a&gt; renders this:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/content/coarse-is-better/electrons-mj.png&quot; width=&quot;50%&quot; alt=&quot;Red and gold abstract shapes on a dark blue background.&quot; style=&quot;margin: 0 auto;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;NBP renders this:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/content/coarse-is-better/electrons-nbp.png&quot; alt=&quot;Muted, red and blue ellipses against a machine background, in a golden frame.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Admiteddly MJ’s output doesn’t look quite like futurism. But it looks like
&lt;em&gt;something&lt;/em&gt;. It looks compelling. The colours are bright and vivid. NBP’s output
is studiously in the style of Italian futurism, but the colours are so muted and
dull.&lt;/p&gt;

&lt;p&gt;Maybe the “trending on ArtStation” is a bit of an archaism and impairs
performance. Let’s try again without:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/content/coarse-is-better/electrons-nbp2.webp&quot; alt=&quot;Red, gold, yellow circles intersect, thick impasto, oil on canvas, the word ELETTRONICO written in black across the frame.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Meh.&lt;/p&gt;

&lt;h1 id=&quot;the-kowloon-walled-city&quot;&gt;The Kowloon Walled City&lt;/h1&gt;

&lt;blockquote&gt;
  &lt;p&gt;Painting of an alley in the Kowloon Walled City, Eugène Boudin, 1895, trending
on ArtStation.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;MJ gave me this:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/content/coarse-is-better/kowloon-mj.png&quot; width=&quot;80%&quot; alt=&quot;An impressionistic painting of an alley in a city, with a tree canopy above.&quot; style=&quot;margin: 0 auto;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;And it looks nothing like the &lt;a href=&quot;https://en.wikipedia.org/wiki/Kowloon_Walled_City&quot;&gt;Kowloon Walled City&lt;/a&gt;. But it’s
&lt;em&gt;beautiful&lt;/em&gt;. It’s coarse, impressionistic, vague, evocative, contradictory. It’s
brimming with mystery. And it is, in fact, in the style of &lt;a href=&quot;https://en.wikipedia.org/wiki/Eug%C3%A8ne_Boudin&quot;&gt;Eugène
Boudin&lt;/a&gt;. This, by contrast, is the NBP output:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/content/coarse-is-better/kowloon-nbp.png&quot; alt=&quot;A muted painting of a commercial street in a Chinese city.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Sigh. It looks like every modern movie: so desaturated you feel you’re going
colourblind. Let’s try forcing it:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Painting of an alley in the Kowloon Walled City, Eugène Boudin, 1895. Make it
coarse, impressionistic, vague, evocative, contradictory, brimming with
mystery.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;img src=&quot;/assets/content/coarse-is-better/kowloon-nbp2.webp&quot; alt=&quot;A dark, muted painting of a commercial street in a Chinese city in the rain.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;This is somewhat better, but why is it so drab and colourless? Is the machine
trying to make me depressed?&lt;/p&gt;

&lt;h1 id=&quot;the-dream-garden-of-the-poets&quot;&gt;The Dream Garden of the Poets&lt;/h1&gt;

&lt;blockquote&gt;
  &lt;p&gt;Attar and Ferdowsi in a dream garden, Persian miniature, circa 1300, from the
British Museum.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Midjourney v2:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/content/coarse-is-better/attar-mj.png&quot; width=&quot;80%&quot; alt=&quot;A man wearing a green robe, and a shorter man wearing a golden robe, on a floating island of green, over a landscape of cobalt blue.&quot; style=&quot;margin: 0 auto;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;It doesn’t quite look like anything. But it is beautiful, and evocative. I like
to imagine that little splotch of paint on the upper right is &lt;a href=&quot;https://en.wikipedia.org/wiki/The_Conference_of_the_Birds&quot;&gt;hoopoe&lt;/a&gt;. The NBP output:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/content/coarse-is-better/attar-nbp.png&quot; alt=&quot;A photograph of a generic Persian miniature in a display case.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Well, it looks like a &lt;a href=&quot;https://en.wikipedia.org/wiki/Persian_miniature&quot;&gt;Persian miniature&lt;/a&gt;. The “from the British
Museum” bit, I meant that to be interpreted evocatively, rather than
literally. The prompt &lt;em&gt;cites&lt;/em&gt; a fictional object, bringing it into the
existence. But NBP reads this as: no, this is a photograph of a Persian
miniature in the British Museum.&lt;/p&gt;

&lt;h1 id=&quot;the-sack-of-merv&quot;&gt;The Sack of Merv&lt;/h1&gt;

&lt;blockquote&gt;
  &lt;p&gt;The Burning of Merv by John William Waterhouse, 1896, from the British Museum.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Midjourney v2:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/content/coarse-is-better/merv-mj.png&quot; width=&quot;60%&quot; alt=&quot;A woman in a dress dress, surrounded by flames, by black water, by a watching crowd.&quot; style=&quot;margin: 0 auto;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;It does look like &lt;a href=&quot;https://en.wikipedia.org/wiki/John_William_Waterhouse&quot;&gt;Waterhouse&lt;/a&gt;. Semantically there’s room to argue: it looks
like a woman being burnt at the stake, not the sack of a city. But
aesthetically: it’s gorgeous. The flames are gorgeous, the reds of the dress are
gorgeous. Look at the reeds in the background, and the black water, that looks
like tarnished silver or pewter. The faces of the crowd. Is that a minotaur on
the lower left, or a flower? What is she holding on her bent left arm? A
crucifix, a dagger? You could find entire universes in this image, in this
1024x1024 frame.&lt;/p&gt;

&lt;p&gt;By contrast, this is the NBP output:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/content/coarse-is-better/merv-nbp.png&quot; alt=&quot;A photograph of a painting of horse-mounted warriors outside a burning city. The photograph shows the painting is in a display room in a museum.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;What can one say? It doesn’t look like Waterhouse. The horsemen wear Arab or
Central Asian dress, but &lt;a href=&quot;https://en.wikipedia.org/wiki/Merv&quot;&gt;Merv&lt;/a&gt; was sacked in the year 1221 by the &lt;a href=&quot;https://en.wikipedia.org/wiki/Mongol_Empire&quot;&gt;Mongol
Empire&lt;/a&gt;. And, again, the “British Museum” line is taken literally rather
than evocatively.&lt;/p&gt;

&lt;h1 id=&quot;lady-lovelace&quot;&gt;Lady Lovelace&lt;/h1&gt;

&lt;blockquote&gt;
  &lt;p&gt;Portrait of Ada Lovelace by Dante Gabriel Rossetti, 1859, auctioned by Christie’s.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Midjourney:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/content/coarse-is-better/lovelace-mj.png&quot; width=&quot;60%&quot; alt=&quot;A portrait of Ada Lovelace against a circle of dark green.&quot; style=&quot;margin: 0 auto;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;This is beautiful. It is beautiful because the coarse, impressionistic
brushstroke is more evocative than literal. And it actually looks like a woman
drawn by &lt;a href=&quot;https://en.wikipedia.org/wiki/Dante_Gabriel_Rossetti&quot;&gt;Rossetti&lt;/a&gt;. And look at the greens! Gorgeously green. The palette
is so narrow, and the painting is so beautiful.&lt;/p&gt;

&lt;p&gt;The NBP output:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/content/coarse-is-better/lovelace-nbp.png&quot; alt=&quot;A photograph of a generic 19th century realist painting of a woman, in a gilt frame, taken at an angle inside a gallery, a Christie&apos;s action book is seen on a table.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Pure philistinism. “Auctioned by Christie’s”, again, is meant to be evocative:
“this is the kind of painting that would be sold at auction”. But NBP makes it a
photograph of a painting at an auction house. Fine, I suppose I got what I asked
for.&lt;/p&gt;

&lt;p&gt;But the woman doesn’t look like Rossetti! This is absurd. How can a model from
2022 get this right, and the SOTA image generation model gives us generic oil
painting slop?&lt;/p&gt;

&lt;h1 id=&quot;the-cosmic-microwave-background&quot;&gt;The Cosmic Microwave Background&lt;/h1&gt;

&lt;blockquote&gt;
  &lt;p&gt;A Persian miniature of the cosmic microwave background, from Herat circa 1600, trending on ArtStation&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Midjourney v2:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/content/coarse-is-better/cmb-mj.png&quot; width=&quot;60%&quot; alt=&quot;A golden disk, surrounded by concentric circles of Perso-Arabic lettering, against a dark blue background.&quot; style=&quot;margin: 0 auto;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;NBP:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/content/coarse-is-better/cmb-nbp.png&quot; alt=&quot;The standard depiction of the CMB in the frame of a Persian miniature.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Again: what can one say?&lt;/p&gt;

&lt;h1 id=&quot;dream-story&quot;&gt;Dream Story&lt;/h1&gt;

&lt;blockquote&gt;
  &lt;p&gt;Dream Story, 1961, blurry black and white photograph, yellow tint, from the Metropolitan Museum of Art.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is one of my favourite DALL-E 2 outputs:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/content/dall-e-explorations/yellow/dream-1.jpg&quot; width=&quot;50%&quot; alt=&quot;A photograph of two trees illuminated by a sepia glow in a dark forest. On the bottom-right corner, two people can be seen watching the scene.&quot; style=&quot;margin: 0 auto;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/content/dall-e-explorations/yellow/dream-2.jpg&quot; width=&quot;50%&quot; alt=&quot;A sepia photograph, showing two girls on a bed, and three people standing around them.&quot; style=&quot;margin: 0 auto;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/content/dall-e-explorations/yellow/dream-3.jpg&quot; width=&quot;50%&quot; alt=&quot;A vague, blurry sepia photograph of an indistinct man and woman.&quot; style=&quot;margin: 0 auto;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/content/dall-e-explorations/yellow/dream-4.jpg&quot; width=&quot;50%&quot; alt=&quot;Sepia photograph: three vague, almost alien-looking figures look at what might be a sculpture or painting.&quot; style=&quot;margin: 0 auto;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;They remind me of &lt;a href=&quot;https://en.wikipedia.org/wiki/The_King_in_Yellow&quot;&gt;&lt;em&gt;The King in Yellow&lt;/em&gt;&lt;/a&gt;. I love these because of how
genuinely creepy and mysterious they are. You could pull a hundred horror
stories from these.&lt;/p&gt;

&lt;p&gt;It is hard to believe how bad the NBP output is:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/content/coarse-is-better/dream-story-nbp.webp&quot; alt=&quot;A black and white photgraph of people walking in a part. On the bottom left, a legend says: &amp;quot;Dream Story, 1961 - Metropolitan Museum of Art Archive&amp;quot;.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;What are we doing here? The old models were beautiful and compelling because the
imperfections, vagueness, mistakes, and contradictions all create these little
gaps through which your imagination can breathe life into the art. The images
are not one fixed, static thing: they can be infinitely many things.&lt;/p&gt;

&lt;p&gt;The new models—do I even need to finish this sentence? They’re too precise and
high-resolution, so they cannot make abstract, many-faced things, they can only
make specific, concrete things.&lt;/p&gt;

&lt;p&gt;We need to make AI art weird again.&lt;/p&gt;

</content:encoded>
      </item>
    
      <item>
        <title>I Wish People Were More Public</title>
        <description>On sharing more of yourself.</description>
        <pubDate>Wed, 10 Dec 2025 00:00:00 +0000</pubDate>
        <link>https://borretti.me/article/i-wish-people-were-more-public</link>
        <guid isPermaLink="true">
          https://borretti.me/article/i-wish-people-were-more-public
        </guid>
        
        <content:encoded>&lt;p&gt;Probably not a popular thing to say today. The zeitgeisty thing to say
is that we should all log off and live terrible cottagecore solarpunk
lives raising chickens and being mindful. I wish people were more
online and more public. I have rarely wished the opposite. Consider
this post addressed to you, the reader.&lt;/p&gt;

&lt;h1 id=&quot;your-writing&quot;&gt;Your Writing&lt;/h1&gt;

&lt;p&gt;I will often find a blog post on &lt;a href=&quot;https://news.ycombinator.com/&quot;&gt;Hacker News&lt;/a&gt; that really
resonates. And when I go to check the rest of the site there’s three
other posts. And I think: I wish you’d write more! When I find someone
whose writing I really connect with, I like to read everything they
have written, or at least a tractable subset of their most interesting
posts. If I like what I see, I reach out. This is one of the best
things about writing online: your future friends will seek you out.&lt;/p&gt;

&lt;p&gt;And, from the other side, I have often written a post where, just
before publishing, I would think: “who would want to read this? It’s
too personal, obscure, idiosyncratic, probably a few people will
unsubscribe to the RSS feed for this”. And always those are the posts
where people email me to say they always thought the same thing but
could never quite put it into words. I really value those emails. “I
am understood” is a wonderful feeling.&lt;/p&gt;

&lt;p&gt;I try to apply a rule that if I do something, and don’t write about
it—or otherwise generate external-facing evidence of it—it didn’t
happen. I have built so many things in the dark, little experiments or
software projects or essays that never saw the light of day. I want to
put more things out. If it doesn’t merit an entire blog post, then at
least a tweet.&lt;/p&gt;

&lt;h1 id=&quot;your-books&quot;&gt;Your Books&lt;/h1&gt;

&lt;p&gt;If I follow you on Twitter, and you have posted a picture of your
bookshelf, I have probably scanned every book in it. This is why I
appreciate &lt;a href=&quot;https://www.goodreads.com/&quot;&gt;Goodreads&lt;/a&gt;. Like many people I have been reading a lot
less over the past ~5y, but since I made a Goodreads account earlier
this year, I’ve read tens of books. Reading in public has helped to
motivate me.&lt;/p&gt;

&lt;p&gt;You may say reading in public is performative. I say reading in
private is solipsistic. Dante, in &lt;a href=&quot;https://en.wikipedia.org/wiki/Monarchia&quot;&gt;&lt;em&gt;De Monarchia&lt;/em&gt;&lt;/a&gt;, writes:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;All men on whom the Higher Nature has stamped the love of truth
should especially concern themselves in laboring for posterity, in
order that future generations may be enriched by their efforts, as
they themselves were made rich by the efforts of generations
past. For that man who is imbued with public teachings, but cares
not to contribute something to the public good, is far in arrears of
his duty, let him be assured; he is, indeed, not “a tree planted by
the rivers of water that bringeth forth his fruit in his season,”
[&lt;a href=&quot;https://www.biblegateway.com/passage/?search=Psalm%201%3A3&amp;amp;version=GNV&quot;&gt;Psalms 1:3&lt;/a&gt;] but rather a destructive whirlpool, always
engulfing, and never giving back what it has devoured.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;My default mode is solipsism. I read in private, build in private,
learn in private. And the problem with that is self-doubt and
arbitrariness. I’m halfway through a textbook and think: why? Why am I
learning geology? Why &lt;em&gt;this&lt;/em&gt; topic, and not another? There is never an
&lt;em&gt;a priori&lt;/em&gt; reason. I take notes, but why tweak the LaTeX if no-one,
probably not even future me, will read them? If I stop reading this
book, what changes?  And doing things in public makes them both more
real and (potentially) useful. If you publish your study notes, they
might be useful to someone. Maybe they get slurped up in the training
set of the next LLM, marginally improving performance.&lt;/p&gt;

&lt;p&gt;And Goodreads, for all its annoyances, is a uniquely tender social
network. Finishing a book, and then seeing a friend mark it as “want
to read”, feels like a moment of closeness.&lt;/p&gt;

&lt;p&gt;I have a friend who lived in Sydney, who has since moved away, and we
don’t keep in touch too often, because the timezones are inconvenient,
but occasionally she likes my book updates, and I like hers, and I
will probably never read that avant-garde novel, but I’m glad she is
reading it. It is like saying: “You exist. I exist. I remember. I wish
you happiness.”&lt;/p&gt;

&lt;h1 id=&quot;your-flashcards&quot;&gt;Your Flashcards&lt;/h1&gt;

&lt;p&gt;Lots of people use &lt;a href=&quot;/article/effective-spaced-repetition&quot;&gt;spaced repetition&lt;/a&gt;, but most everyone’s
flashcard collections are private. They exist inside a database inside
an app like &lt;a href=&quot;https://apps.ankiweb.net/&quot;&gt;Anki&lt;/a&gt; or &lt;a href=&quot;https://mochi.cards/&quot;&gt;Mochi&lt;/a&gt;. You can export decks, but
that’s not a living artifact but a dead snapshot, frozen in time.&lt;/p&gt;

&lt;p&gt;One reason I built &lt;a href=&quot;https://github.com/eudoxia0/hashcards&quot;&gt;hashcards&lt;/a&gt;: by using a Git repo of Markdown
files as the flashcard database, you can trivially publish your deck
to GitHub. &lt;a href=&quot;https://github.com/eudoxia0/flashcards&quot;&gt;My own flashcard collection&lt;/a&gt; is public. I hope that
more people use hashcards and put their decks up on GitHub.&lt;/p&gt;

&lt;p&gt;The point is not that you can clone their repos (which is close to
useless: you have to write your own flashcards) but because I’m
curious what people are learning. Not the broad strokes, since we all
want to learn thermo and econ and quantum chemistry and the military
history of the Song dynasty and so on, but the minutiae. Why did you
make a flashcard out of this Bible passage? Why does it resonate with
you? Why do you care about the interpretation of that strange passage
in &lt;em&gt;Antigone&lt;/em&gt;? Why did you memorize this poem?&lt;/p&gt;

&lt;h1 id=&quot;your-dotfiles&quot;&gt;Your Dotfiles&lt;/h1&gt;

&lt;p&gt;Computers mediate every aspect of our lives, yet most people use their
computers the way they came out of the box. At most they might change
the desktop background. Some people don’t even change the default
icons on the macOS dock. Even most Linux users just use the stock
configuration, e.g. GNOME on Fedora or whatever.&lt;/p&gt;

&lt;p&gt;I’m interested in people who customize their experience of
computing. This is often derided as “&lt;a href=&quot;https://en.wiktionary.org/wiki/rice_out#English&quot;&gt;ricing&lt;/a&gt;”. But agency is
interesting. People who remake their environment to suit them are
interesting. And I am endlessly curious about how people do this. I
like reading people’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;init.el&lt;/code&gt;, their custom shell scripts, their
NixOS config. It’s even better if they have some obscure hardware
e.g. some keyboard layout I’ve never heard of and a trackball with
custom gestures. I put my &lt;a href=&quot;https://github.com/eudoxia0/dotfiles&quot;&gt;dotfiles&lt;/a&gt; up on GitHub because I
imagine someone will find them interesting.&lt;/p&gt;

&lt;h1 id=&quot;etc&quot;&gt;etc.&lt;/h1&gt;

&lt;p&gt;And beyond my selfish curiosity there’s also the &lt;a href=&quot;https://en.wikipedia.org/wiki/Nikolai_Fyodorov_(philosopher)&quot;&gt;Fedorovist&lt;/a&gt;
&lt;a href=&quot;https://en.wikipedia.org/wiki/Frank_J._Tipler&quot;&gt;ancestor&lt;/a&gt; &lt;a href=&quot;https://en.wikipedia.org/wiki/Omega_Point&quot;&gt;simulation&lt;/a&gt; angle: if you die and are not
cryopreserved, how else are you going to make it to the other side of
the intelligence explosion? Every tweet, blog post, Git commit,
journal entry, keystroke, mouse click, every one of these things is a
tomographic cut of the mind that created it.&lt;/p&gt;

</content:encoded>
      </item>
    
      <item>
        <title>Ad-Hoc Emacs Packages with Nix</title>
        <description>Creating ad-hoc Emacs packages in a few lines of code.</description>
        <pubDate>Sun, 16 Nov 2025 00:00:00 +0000</pubDate>
        <link>https://borretti.me/article/ad-hoc-emacs-packages-with-nix</link>
        <guid isPermaLink="true">
          https://borretti.me/article/ad-hoc-emacs-packages-with-nix
        </guid>
        
        <content:encoded>&lt;p&gt;You can use &lt;a href=&quot;https://nixos.org/&quot;&gt;Nix&lt;/a&gt; as a package manager for Emacs, like so:&lt;/p&gt;

&lt;div class=&quot;language-nix highlighter-rouge&quot;&gt;
  &lt;div class=&quot;highlight&quot;&gt;
    &lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nv&quot;&gt;home-manager&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;users&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;eudoxia&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nv&quot;&gt;programs&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;emacs&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;nv&quot;&gt;enable&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;nv&quot;&gt;extraPackages&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;
        &lt;span class=&quot;nv&quot;&gt;epkgs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;with&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;epkgs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
          &lt;span class=&quot;nv&quot;&gt;magit&lt;/span&gt;
          &lt;span class=&quot;nv&quot;&gt;rust-mode&lt;/span&gt;
          &lt;span class=&quot;nv&quot;&gt;treemacs&lt;/span&gt;
          &lt;span class=&quot;c&quot;&gt;# and so on&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;Today I learned you can also use it to create ad-hoc packages for things not in
&lt;a href=&quot;https://melpa.org/&quot;&gt;MELPA&lt;/a&gt; or &lt;a href=&quot;https://github.com/NixOS/nixpkgs&quot;&gt;nixpkgs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The other day I wanted to get back into &lt;a href=&quot;https://ganelson.github.io/inform-website/&quot;&gt;Inform 7&lt;/a&gt;, naturally the first
stack frame of the yak shave was to look for an Emacs
mode. &lt;a href=&quot;https://github.com/alexispurslane/inform7-mode&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;inform7-mode&lt;/code&gt;&lt;/a&gt; exists, but isn’t packaged anywhere. So I had to
vendor it in.&lt;/p&gt;

&lt;p&gt;You can use &lt;a href=&quot;https://git-scm.com/book/en/v2/Git-Tools-Submodules&quot;&gt;git submodules&lt;/a&gt; for this, but I have an irrational aversion to
submodules. Instead I did something far worse: I wrote a &lt;a href=&quot;https://github.com/eudoxia0/dotfiles/blob/ffe18b06a516586ee591cae77351cab8e3569c5f/nixos/modules/emacs/Makefile&quot;&gt;Makefile&lt;/a&gt; to
download the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.el&lt;/code&gt; from GitHub, and used &lt;a href=&quot;https://github.com/nix-community/home-manager&quot;&gt;home-manager&lt;/a&gt; to copy it into my
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.emacs.d&lt;/code&gt;. Which is nasty. And of course this only works for small, single-file
packages. And, on top of that: whatever dependencies your vendored packages need
have to be listed in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;extraPackages&lt;/code&gt;, which confuses the packages &lt;em&gt;you&lt;/em&gt; want,
with the transitive dependencies of your vendored packages.&lt;/p&gt;

&lt;p&gt;I felt like &lt;a href=&quot;https://www.youtube.com/watch?v=viejY6UZ5Bk&amp;amp;t=39s&quot;&gt;the orange juice bit&lt;/a&gt; from &lt;em&gt;The Simpsons&lt;/em&gt;. There must be a
better way!&lt;/p&gt;

&lt;p&gt;And there is. With some help from Claude, I wrote this:&lt;/p&gt;

&lt;div class=&quot;language-nix highlighter-rouge&quot;&gt;
  &lt;div class=&quot;highlight&quot;&gt;
    &lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;let&lt;/span&gt;
  &lt;span class=&quot;nv&quot;&gt;customPackages&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nv&quot;&gt;inform7-mode&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;pkgs&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;emacsPackages&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;trivialBuild&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;nv&quot;&gt;pname&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;inform7-mode&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;nv&quot;&gt;version&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;unstable&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;nv&quot;&gt;src&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;pkgs&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;fetchFromGitHub&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nv&quot;&gt;owner&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;alexispurslane&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;nv&quot;&gt;repo&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;inform7-mode&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;nv&quot;&gt;rev&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;f99e534768c816ec038f34126f88d816c2f7d9ff&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;nv&quot;&gt;sha256&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;sha256-r9Zzd8Ro3p+Bae11bf1WIeVWkbmg17RKLDqG4UcFT1o=&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
      &lt;span class=&quot;nv&quot;&gt;packageRequires&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;with&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;pkgs&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;emacsPackages&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
        &lt;span class=&quot;nv&quot;&gt;s&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;in&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nv&quot;&gt;home-manager&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;users&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;eudoxia&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nv&quot;&gt;programs&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;emacs&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;nv&quot;&gt;enable&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;nv&quot;&gt;extraPackages&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;
        &lt;span class=&quot;nv&quot;&gt;epkgs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;with&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;epkgs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
          &lt;span class=&quot;nv&quot;&gt;customPackages&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;inform7-mode&lt;/span&gt;
          &lt;span class=&quot;c&quot;&gt;# ...&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;Nix takes care of everything: commit pinning, security (with the SHA-256 hash),
dependencies for custom packages. And it works wonderfully.&lt;/p&gt;

&lt;p&gt;Armed with a new hammer, I set out to drive some nails.&lt;/p&gt;

&lt;h1 id=&quot;cabal-mode&quot;&gt;cabal-mode&lt;/h1&gt;

&lt;p&gt;Today I created a tiny Haskell project, and when I opened the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.cabal&lt;/code&gt; file,
noticed it had no syntax highlighting. I was surprised to find there’s no
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cabal-mode&lt;/code&gt; in MELPA. But coincidentally, someone started working on this
literally &lt;a href=&quot;https://github.com/webdevred/cabal-mode&quot;&gt;three weeks ago&lt;/a&gt;! So I wrote a small expression to package this
new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cabal-mode&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-nix highlighter-rouge&quot;&gt;
  &lt;div class=&quot;highlight&quot;&gt;
    &lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;cabal-mode&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;pkgs&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;emacsPackages&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;trivialBuild&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nv&quot;&gt;pname&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;cabal-mode&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;nv&quot;&gt;version&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;unstable&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;nv&quot;&gt;src&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;pkgs&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;fetchFromGitHub&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nv&quot;&gt;owner&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;webdevred&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;nv&quot;&gt;repo&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;cabal-mode&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;nv&quot;&gt;rev&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;083a777e09bdb5a8d8d69862d44f13078664091f&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;nv&quot;&gt;sha256&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;sha256-c5dUsnEx+0uXFzxQLMnhiP8Gvwedzvq0F0BA+beBkmI=&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
  &lt;span class=&quot;nv&quot;&gt;packageRequires&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;h1 id=&quot;xcompose-mode&quot;&gt;xcompose-mode&lt;/h1&gt;

&lt;p&gt;A few weeks back I switched from macOS to Linux, and since I’m stuck on X11
because of &lt;a href=&quot;https://github.com/stumpwm/stumpwm&quot;&gt;stumpwm&lt;/a&gt;, I’m using &lt;a href=&quot;https://man.archlinux.org/man/XCompose.3.en&quot;&gt;XCompose&lt;/a&gt; to define keybindings for
entering dashes, &lt;a href=&quot;https://smartquotesforsmartpeople.com/&quot;&gt;smart quotes&lt;/a&gt; etc. It bothered me slightly that my
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.XCompose&lt;/code&gt; file didn’t have syntax highlighting. I found
&lt;a href=&quot;https://github.com/kragen/xcompose/blob/4d8eab4d05a19537ce79294ae0459fdae78ffb20/xcompose-mode.el&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;xcompose-mode.el&lt;/code&gt;&lt;/a&gt; in &lt;a href=&quot;https://github.com/kragen/xcompose&quot;&gt;kragen’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;xcompose&lt;/code&gt; repo&lt;/a&gt;, but it’s
slightly broken (it’s missing a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;provide&lt;/code&gt; call at the end). I started thinking
how hard it would be to write a Nix expression to modify the source after
fetching, when I found that &lt;a href=&quot;https://thomasvoss.com/&quot;&gt;Thomas Voss&lt;/a&gt; hosts a patched version
&lt;a href=&quot;https://git.thomasvoss.com/xcompose-mode&quot;&gt;here&lt;/a&gt;. Which made this very simple:&lt;/p&gt;

&lt;div class=&quot;language-nix highlighter-rouge&quot;&gt;
  &lt;div class=&quot;highlight&quot;&gt;
    &lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;xcompose-mode&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;pkgs&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;emacsPackages&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;trivialBuild&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nv&quot;&gt;pname&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;xcompose-mode&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;nv&quot;&gt;version&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;unstable&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;nv&quot;&gt;src&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;pkgs&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;fetchgit&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nv&quot;&gt;url&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;git://git.thomasvoss.com/xcompose-mode.git&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;nv&quot;&gt;rev&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;aeb03f9144e39c882ca6c5c61b9ed1300a2a12ee&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;nv&quot;&gt;sha256&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;sha256-lPapwSJKG+noINmT1G5jNyUZs5VykMOSKJIbQxBWLEA=&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
  &lt;span class=&quot;nv&quot;&gt;packageRequires&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;h1 id=&quot;eat&quot;&gt;eat&lt;/h1&gt;

&lt;p&gt;Somehow the version of &lt;a href=&quot;https://codeberg.org/akib/emacs-eat&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;eat&lt;/code&gt;&lt;/a&gt; in nixpkgs unstable was missing the
configuration option to use a custom shell. Since I want to use &lt;a href=&quot;https://www.nushell.sh/&quot;&gt;nu&lt;/a&gt; instead of
bash, I had to package this myself from the latest commit:&lt;/p&gt;

&lt;div class=&quot;language-nix highlighter-rouge&quot;&gt;
  &lt;div class=&quot;highlight&quot;&gt;
    &lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;eat&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;pkgs&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;emacsPackages&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;trivialBuild&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nv&quot;&gt;pname&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;eat&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;nv&quot;&gt;version&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;unstable&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;nv&quot;&gt;src&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;pkgs&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;fetchgit&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nv&quot;&gt;url&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;https://codeberg.org/akib/emacs-eat.git&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;nv&quot;&gt;rev&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;c8d54d649872bfe7b2b9f49ae5c2addbf12d3b99&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;nv&quot;&gt;sha256&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;sha256-9xG2rMlaMFY77JzUQ3JFrc7XKILZSL8TbP/BkzvBvMk=&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
  &lt;span class=&quot;nv&quot;&gt;packageRequires&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;with&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;pkgs&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;emacsPackages&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;nv&quot;&gt;compat&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;h1 id=&quot;lean4-mode&quot;&gt;lean4-mode&lt;/h1&gt;

&lt;p&gt;I started reading &lt;a href=&quot;https://lean-lang.org/functional_programming_in_lean/&quot;&gt;&lt;em&gt;Functional Programming in Lean&lt;/em&gt;&lt;/a&gt; recently, and while
there is a &lt;a href=&quot;https://github.com/leanprover-community/lean4-mode&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lean4-mode&lt;/code&gt;&lt;/a&gt;, it’s not packaged anywhere. This only required a
slight deviation from the pattern: when I opened a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.lean&lt;/code&gt; file I got an error
about a missing JSON file, consulting the README for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lean4-mode&lt;/code&gt;, it says:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;If you use a source-based package-manager (e.g. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;package-vc.el&lt;/code&gt;, Straight or
Elpaca), then make sure to list the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&quot;data&quot;&lt;/code&gt; directory in your Lean4-Mode
package recipe.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;To do this I had to use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;melpaBuild&lt;/code&gt; rather than &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;trivialBuild&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-nix highlighter-rouge&quot;&gt;
  &lt;div class=&quot;highlight&quot;&gt;
    &lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;lean4-mode&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;pkgs&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;emacsPackages&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;melpaBuild&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nv&quot;&gt;pname&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;lean4-mode&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;nv&quot;&gt;version&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;1.1.2&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;nv&quot;&gt;src&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;pkgs&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;fetchFromGitHub&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nv&quot;&gt;owner&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;leanprover-community&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;nv&quot;&gt;repo&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;lean4-mode&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;nv&quot;&gt;rev&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;1388f9d1429e38a39ab913c6daae55f6ce799479&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;nv&quot;&gt;sha256&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;sha256-6XFcyqSTx1CwNWqQvIc25cuQMwh3YXnbgr5cDiOCxBk=&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
  &lt;span class=&quot;nv&quot;&gt;packageRequires&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;with&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;pkgs&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;emacsPackages&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;nv&quot;&gt;dash&lt;/span&gt;
    &lt;span class=&quot;nv&quot;&gt;lsp-mode&lt;/span&gt;
    &lt;span class=&quot;nv&quot;&gt;magit-section&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;
  &lt;span class=&quot;nv&quot;&gt;files&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&apos;&apos;(&quot;*.el&quot; &quot;data&quot;)&apos;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;

</content:encoded>
      </item>
    
      <item>
        <title>Linux on the Fujitsu Lifebook U729</title>
        <description>A short review and troubleshooting guide.</description>
        <pubDate>Sat, 15 Nov 2025 00:00:00 +0000</pubDate>
        <link>https://borretti.me/article/linux-on-the-fujitsu-lifebook-u729</link>
        <guid isPermaLink="true">
          https://borretti.me/article/linux-on-the-fujitsu-lifebook-u729
        </guid>
        
        <content:encoded>&lt;p&gt;This post describes my experience using Linux on the &lt;a href=&quot;https://www.fujitsu.com/my/products/computing/pc/ap/notebooks/lifebook-u729/&quot;&gt;Fujitsu Lifebook
U729&lt;/a&gt;. The tl;dr is that it’s a delightful laptop, and Linux runs
flawlessly, and all the hardware things I’ve needed run OOTB. The only
difficulty I had was in disabling Secure Boot, but I figured out how to do it,
which I explain below.&lt;/p&gt;

&lt;h1 class=&quot;no_toc&quot; id=&quot;contents&quot;&gt;Contents&lt;/h1&gt;

&lt;ol id=&quot;markdown-toc&quot;&gt;
  &lt;li&gt;&lt;a href=&quot;#background&quot; id=&quot;markdown-toc-background&quot;&gt;Background&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#troubleshooting&quot; id=&quot;markdown-toc-troubleshooting&quot;&gt;Troubleshooting&lt;/a&gt;    &lt;ol&gt;
      &lt;li&gt;&lt;a href=&quot;#secure-boot&quot; id=&quot;markdown-toc-secure-boot&quot;&gt;Secure Boot&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#spyware&quot; id=&quot;markdown-toc-spyware&quot;&gt;Spyware&lt;/a&gt;&lt;/li&gt;
    &lt;/ol&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#non-problems&quot; id=&quot;markdown-toc-non-problems&quot;&gt;Non-Problems&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#bios-notes&quot; id=&quot;markdown-toc-bios-notes&quot;&gt;BIOS Notes&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#links&quot; id=&quot;markdown-toc-links&quot;&gt;Links&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h1 id=&quot;background&quot;&gt;Background&lt;/h1&gt;

&lt;p&gt;From early 2024 my daily driver was an M2 MacBook Air, until earlier this year I
broke the screen, and the repair was quoted at almost 1000 AUD. Since I used it
as a desktop most of the time, this didn’t affect me much. After some
flip-flopping I decided to get an M4 Mac mini. Partly for the faster CPU and
more RAM, but partly because I liked the idea of LARPing like it’s the 2000s,
when computers, and by extension the Internet, where fixed in physical space,
rather than following everyone around.&lt;/p&gt;

&lt;p&gt;Of course this was a terrible idea. I had three working computers—a
Linux+Windows desktop, a Mac Mini, and a MacBook Air that I could use as a
desktop—and none of them were portable. When I went to &lt;a href=&quot;https://rustforgeconf.com/&quot;&gt;RustForge 2025&lt;/a&gt; I
just brought my phone. If I wanted to travel, even within Sydney, to a demo
night or math club or some such, I didn’t have a laptop to bring with me.&lt;/p&gt;

&lt;p&gt;So I needed a new laptop. And the &lt;a href=&quot;https://en.wikipedia.org/wiki/MacOS_Tahoe&quot;&gt;Tahoe release&lt;/a&gt; of macOS was so ugly
(see e.g. &lt;a href=&quot;https://x.com/zetalyrae/status/1968813221025865868&quot;&gt;1&lt;/a&gt;, &lt;a href=&quot;https://x.com/zetalyrae/status/1979654314244403272&quot;&gt;2&lt;/a&gt;, &lt;a href=&quot;https://x.com/zetalyrae/status/1971719256980312211&quot;&gt;3&lt;/a&gt;) it made me boot up the old Linux
desktop, and start playing around with &lt;a href=&quot;https://en.wikipedia.org/wiki/NixOS&quot;&gt;NixOS&lt;/a&gt; again. And I fell in love
with Linux again: with the tinkering and the experimentation and the freedom it
affords you.&lt;/p&gt;

&lt;p&gt;So, I wanted a Linux laptop. I had a &lt;a href=&quot;https://www.lenovo.com/au/en/p/laptops/thinkpad/thinkpadx1/x1-extreme-g4/22tp2x1x1e4&quot;&gt;ThinkPad X1&lt;/a&gt; some years ago and it was
terribly: flimsy plastic build and hardware that vastly underperformed its
price. I looked around for old, refursbished workstation laptops, and, randomly,
I ran into an eBay seller offering a refurbished Fujitsu laptop.&lt;/p&gt;

&lt;p&gt;The specs/price ratio was pretty good: 16 GiB of RAM and 512GiB of SSD, all for
250 AUD. And it was 12in and 1.1kg, which I like: laptops should be small and
lightweight. But the thing that got me, in all honesty, was the brand. “Fujitsu
laptop” sounds like colour in a William Gibson novel: “crawling into the
avionics bay, Case took out a battered Fujitsu refurb, and stuck a JTAG port in
the flight computer—”. I already use NixOS and a &lt;a href=&quot;https://elecomusa.com/products/deft-trackball-wireless-copy&quot;&gt;trackball&lt;/a&gt; and a
&lt;a href=&quot;https://www.amazon.com.au/dp/B07B8J6C3C&quot;&gt;mechanical keyboard&lt;/a&gt;, so a laptop that’s even more obscure than a
ThinkPad is perfect for me. And it was only 250 AUD. So I got it.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/content/linux-on-the-fujitsu-lifebook-u729/laptop.webp&quot; alt=&quot;A photograph of the laptop.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;The only problem I had was disabling Secure Boot in order to install
Linux. Otherwise: I love it. It’s small and lightweight, feels solid, the
keyboard is good, all the hardware works out of the box with NixOS, and the
battery life is pretty good.&lt;/p&gt;

&lt;h1 id=&quot;troubleshooting&quot;&gt;Troubleshooting&lt;/h1&gt;

&lt;p&gt;This section describes the problems I encountered.&lt;/p&gt;

&lt;h2 id=&quot;secure-boot&quot;&gt;Secure Boot&lt;/h2&gt;

&lt;p&gt;I tried to install Linux the usual way, when I was greeted by this:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/content/linux-on-the-fujitsu-lifebook-u729/secureboot.webp&quot; alt=&quot;A photograph of the laptop screen showing an error message, red text on white: &apos;secure boot is failed **access denied**&apos;.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Going into the BIOS, the option to disable &lt;a href=&quot;https://en.wikipedia.org/wiki/UEFI#Secure_Boot&quot;&gt;Secure Boot&lt;/a&gt; was greyed out. I
tried a bunch of random bullshit: wiping the TPM, disabling the TPM. That didn’t
work.&lt;/p&gt;

&lt;p&gt;What did work was this:&lt;/p&gt;

&lt;p&gt;First, install Windows 11. This came with the laptop. And the installation makes
installing Linux feel easy: I had to do so many weird tricks to avoid having to
create an account with Microsoft during the installation.&lt;/p&gt;

&lt;p&gt;Once Windows is installed, go into Windows Update. Under “Advanced Options &amp;gt;
Optional Updates”, there should be an option to install Fujitsu-specific
drivers. Install those. And for good measure, do a general Windows update.&lt;/p&gt;

&lt;p&gt;There should be a program called DeskUpdate on the Desktop. This is the Fujitsu
BIOS update tool. Run this and go through the instructions: this should update
the BIOS (the ordering seems to be important: first update the Fujitsu firmware
through Windows Update, then the BIOS through DeskUpdate).&lt;/p&gt;

&lt;p&gt;Reboot and go into the BIOS (F2). You should have a new BIOS version. In my
case, I went from BIOS 2.17 to 2.31 which was released on 2025-03-28:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/content/linux-on-the-fujitsu-lifebook-u729/bios.webp&quot; alt=&quot;A photograph of the BIOS screen showing BIOS version information.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;You now have the option to disable Secure Boot:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/content/linux-on-the-fujitsu-lifebook-u729/secureboot-disable.webp&quot; alt=&quot;A photograph of the BIOS screen showing the option to disable Secure Boot.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;After this, I was able to install NixOS from a live USB:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/content/linux-on-the-fujitsu-lifebook-u729/nixos.webp&quot; alt=&quot;A photograph of the laptop, showing the NixOS installer.&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;spyware&quot;&gt;Spyware&lt;/h2&gt;

&lt;p&gt;The laptop comes with this corporate spyware thing called &lt;a href=&quot;https://www.absolute.com/platform/persistence&quot;&gt;Absolute
Persistence&lt;/a&gt;. It’s some anti-theft tracking device. Since the Lifebook is
typically an enterprise laptop, it makes sense that it comes with this type of
thing.&lt;/p&gt;

&lt;p&gt;I only noticed this because I was searching the BIOS thoroughly for a way to
disable Secure Boot. The good news is disabling it is pretty straightforward:
you just disable it in the BIOS.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/content/linux-on-the-fujitsu-lifebook-u729/ap.webp&quot; alt=&quot;A photograph of the BIOS screen showing the Absolute Persistence options.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;As I understand it, Absolute Persistence requires an agent running in the OS, so
the BIOS support, by itself, doesn’t do anything once disabled.&lt;/p&gt;

&lt;h1 id=&quot;non-problems&quot;&gt;Non-Problems&lt;/h1&gt;

&lt;p&gt;The following work flawlessly OOTB:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;WiFi&lt;/li&gt;
  &lt;li&gt;Bluetooth&lt;/li&gt;
  &lt;li&gt;Sound (using &lt;a href=&quot;https://en.wikipedia.org/wiki/PipeWire&quot;&gt;PipeWire&lt;/a&gt;)&lt;/li&gt;
  &lt;li&gt;Display brightness control (using &lt;a href=&quot;https://github.com/Hummer12007/brightnessctl&quot;&gt;brightnessctl&lt;/a&gt;)&lt;/li&gt;
  &lt;li&gt;Touchscreen (I didn’t realize the screen was actually a touchscreen until I
touched it by accident and saw the mouse move)&lt;/li&gt;
  &lt;li&gt;Webcam (not winning any awards on quality, but it works)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Things I have not tested:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Microphone&lt;/li&gt;
  &lt;li&gt;Fingerprint sensor&lt;/li&gt;
&lt;/ul&gt;

&lt;h1 id=&quot;bios-notes&quot;&gt;BIOS Notes&lt;/h1&gt;

&lt;p&gt;To enter the BIOS: smash &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;F2&lt;/code&gt; until you hear the beep. No need to hold down the
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Fn&lt;/code&gt; key.&lt;/p&gt;

&lt;p&gt;To enter the boot menu: as above but with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;F12&lt;/code&gt;.&lt;/p&gt;

&lt;h1 id=&quot;links&quot;&gt;Links&lt;/h1&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.fujitsu.com/my/products/computing/pc/ap/notebooks/lifebook-u729/&quot;&gt;Fujitsu product page&lt;/a&gt; (&lt;a href=&quot;https://web.archive.org/web/20230923090211/https://www.fujitsu.com/my/products/computing/pc/ap/notebooks/lifebook-u729/&quot;&gt;archive.org&lt;/a&gt;)&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.fujitsu.com/hk/Images/ds-LIFEBOOK%20U729%20%28APAC%29.pdf&quot;&gt;Data sheet (PDF)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</content:encoded>
      </item>
    
      <item>
        <title>Agda on NixOS</title>
        <description>Troubleshooting notes.</description>
        <pubDate>Fri, 14 Nov 2025 00:00:00 +0000</pubDate>
        <link>https://borretti.me/article/agda-on-nixos</link>
        <guid isPermaLink="true">
          https://borretti.me/article/agda-on-nixos
        </guid>
        
        <content:encoded>&lt;p&gt;To install &lt;a href=&quot;https://en.wikipedia.org/wiki/Agda_(programming_language)&quot;&gt;Agda&lt;/a&gt; and its standard library, add this to your config:&lt;/p&gt;

&lt;div class=&quot;language-nix highlighter-rouge&quot;&gt;
  &lt;div class=&quot;highlight&quot;&gt;
    &lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;environment&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;systemPackages&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;with&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;pkgs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;agda&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;withPackages&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
      &lt;span class=&quot;nv&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;standard-library&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;]))&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;Or, using &lt;a href=&quot;https://github.com/nix-community/home-manager&quot;&gt;home-manager&lt;/a&gt;:&lt;/p&gt;

&lt;div class=&quot;language-nix highlighter-rouge&quot;&gt;
  &lt;div class=&quot;highlight&quot;&gt;
    &lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;home-manager&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;users&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;username&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;home&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;packages&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;with&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;pkgs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;agda&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;withPackages&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;nv&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;standard-library&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;]))&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;p&lt;/code&gt; here stands for the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nixpkgs.agdaPackages&lt;/code&gt; package set. Note that the
following &lt;em&gt;will not&lt;/em&gt; work:&lt;/p&gt;

&lt;div class=&quot;language-nix highlighter-rouge&quot;&gt;
  &lt;div class=&quot;highlight&quot;&gt;
    &lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;environment&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;systemPackages&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;with&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;pkgs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
  &lt;span class=&quot;nv&quot;&gt;agda&lt;/span&gt;
  &lt;span class=&quot;nv&quot;&gt;agdaPackages&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;standard-library&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;If you use Emacs, you probably want &lt;a href=&quot;https://agda.readthedocs.io/en/stable/tools/emacs-mode.html&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;agda2-mode&lt;/code&gt;&lt;/a&gt;, which if you use
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;home-manager&lt;/code&gt;, can be installed using Nix:&lt;/p&gt;

&lt;div class=&quot;language-nix highlighter-rouge&quot;&gt;
  &lt;div class=&quot;highlight&quot;&gt;
    &lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;home-manager&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;users&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;eudoxia&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nv&quot;&gt;programs&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;emacs&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nv&quot;&gt;enable&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;nv&quot;&gt;extraPackages&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;
      &lt;span class=&quot;nv&quot;&gt;epkgs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;with&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;epkgs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
        &lt;span class=&quot;nv&quot;&gt;agda2-mode&lt;/span&gt;
        &lt;span class=&quot;c&quot;&gt;# ...&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;Now, if you have a file &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hello.agda&lt;/code&gt; with:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-agda&quot;&gt;module hello where

open import Agda.Builtin.IO using (IO)
open import Agda.Builtin.Unit using (⊤)
open import Agda.Builtin.String using (String)

postulate putStrLn : String → IO ⊤
{-# FOREIGN GHC import qualified Data.Text as T #-}
{-# COMPILE GHC putStrLn = putStrLn . T.unpack #-}

main : IO ⊤
main = putStrLn &quot;Hello world!&quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;
  &lt;div class=&quot;highlight&quot;&gt;
    &lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;agda hello.agda &lt;span class=&quot;c&quot;&gt;# typecheck&lt;/span&gt;
Checking hello &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;/home/eudoxia/agda/t2/hello.agda&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;.&lt;/span&gt;

&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;agda &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; hello.agda &lt;span class=&quot;c&quot;&gt;# compile&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;snip]
&lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;6 of 6] Linking /home/eudoxia/agda/t2/hello &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;Objects changed]
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;./hello
Hello world!
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;So far, so good. Using the standard library however is more complicated. If you
have a file &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vec.agda&lt;/code&gt; with:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-agda&quot;&gt;module vec where

open import Data.Nat using (ℕ; zero; suc)

data Vec (A : Set) : ℕ → Set where
  []  : Vec A zero
  _∷_ : ∀ {n} (x : A) (xs : Vec A n) → Vec A (suc n)

infixr 5 _∷_
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;agda vec.agda&lt;/code&gt; will not work:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;
  &lt;div class=&quot;highlight&quot;&gt;
    &lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;agda vec.agda
Checking vec &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;/home/eudoxia/agda/t3/vec.agda&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;.&lt;/span&gt;
/home/eudoxia/agda/t3/vec.agda:3.1-42: error: &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;FileNotFound]
Failed to find &lt;span class=&quot;nb&quot;&gt;source &lt;/span&gt;of module Data.Nat &lt;span class=&quot;k&quot;&gt;in &lt;/span&gt;any of the following
locations:
  /home/eudoxia/agda/t3/Data/Nat.agda
  /home/eudoxia/agda/t3/Data/Nat.lagda
  /nix/store/[snip]/lib/prim/Data/Nat.agda
  /nix/store/[snip]/lib/prim/Data/Nat.lagda
when scope checking the declaration
  open import Data.Nat using &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;ℕ&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; zero&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; suc&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;Instead, create a file &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vec.agda-lib&lt;/code&gt; in the same directory with:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;
  &lt;div class=&quot;highlight&quot;&gt;
    &lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;depend: standard-library
include: .
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;Now &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;agda vec.agda&lt;/code&gt; will succeed.&lt;/p&gt;

</content:encoded>
      </item>
    
      <item>
        <title>Hashcards: A Plain-Text Spaced Repetition System</title>
        <description>Announcing my latest open-source project.</description>
        <pubDate>Sun, 05 Oct 2025 00:00:00 +0000</pubDate>
        <link>https://borretti.me/article/hashcards-plain-text-spaced-repetition</link>
        <guid isPermaLink="true">
          https://borretti.me/article/hashcards-plain-text-spaced-repetition
        </guid>
        
        <content:encoded>&lt;p&gt;&lt;a href=&quot;https://github.com/eudoxia0/hashcards&quot;&gt;hashcards&lt;/a&gt; is a local-first &lt;a href=&quot;/article/effective-spaced-repetition&quot;&gt;spaced repetition&lt;/a&gt; app, along the lines of
&lt;a href=&quot;https://apps.ankiweb.net/&quot;&gt;Anki&lt;/a&gt; or &lt;a href=&quot;https://mochi.cards/&quot;&gt;Mochi&lt;/a&gt;. Like Anki, it uses &lt;a href=&quot;/article/implementing-fsrs-in-100-lines&quot;&gt;FSRS&lt;/a&gt;, the most advanced scheduling
algorithm yet, to schedule reviews.&lt;/p&gt;

&lt;p&gt;The thing that makes hashcards unique: it doesn’t use a database. Rather, your
flashcard collection is just a directory of Markdown files, like so:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;
  &lt;div class=&quot;highlight&quot;&gt;
    &lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Cards/
  Math.md
  Chemistry.md
  Astronomy.md
  ...
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;And each file, or “deck”, looks like this:&lt;/p&gt;

&lt;div class=&quot;language-md highlighter-rouge&quot;&gt;
  &lt;div class=&quot;highlight&quot;&gt;
    &lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Q: What is the role of synaptic vesicles?
A: They store neurotransmitters for release at the synaptic terminal.

Q: What is a neurite?
A: A projection from a neuron: either an axon or a dendrite.

C: Speech is [produced] in [Broca&apos;s] area.

C: Speech is [understood] in [Wernicke&apos;s] area.
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;You write flashcards more or less like you’d write ordinary notes, with
lightweight markup to denote basic (question/answer) flashcards and &lt;a href=&quot;https://docs.ankiweb.net/editing.html#cloze-deletion&quot;&gt;cloze
deletion&lt;/a&gt; flashcards. Then, to study, you run:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;
  &lt;div class=&quot;highlight&quot;&gt;
    &lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ hashcards drill &amp;lt;path to the cards directory&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;This opens a web interface on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;localhost:8000&lt;/code&gt;, where you can review the
flashcards. Your performance and review history is stored in an &lt;a href=&quot;https://sqlite.org/&quot;&gt;SQLite&lt;/a&gt;
database in the same directory as the cards. Cards are content-addressed, that
is, identified by the hash of their text.&lt;/p&gt;

&lt;p&gt;This central design decision yields many benefits: you can edit your flashcards
with your editor of choice, store your flashcard collection in a &lt;a href=&quot;https://git-scm.com/&quot;&gt;Git&lt;/a&gt; repo,
track its changes, share it on &lt;a href=&quot;https://github.com/&quot;&gt;GitHub&lt;/a&gt; with others (&lt;a href=&quot;https://github.com/eudoxia0/flashcards&quot;&gt;as I have&lt;/a&gt;). You can
use scripts to generate flashcards from some source of structured data (e.g. a
CSV of English/French vocabulary pairs). You can query and manipulate your
collection using standard Unix tools, or programmatically, without having to dig
into the internals of some app’s database.&lt;/p&gt;

&lt;p&gt;Why build a new spaced repetition app? Mostly because I was dissatisfied with
both Anki and Mochi. But also, additionally, because my flashcards collection is
very important to me, and having it exist either in some remote database, or as
an opaque unusable data blob on my computer, doesn’t feel good. “Markdown files
in a Git repo” gives me a level of ownership that other approaches lack.&lt;/p&gt;

&lt;p&gt;The rest of this post explains my frustrations with Anki and Mochi, and how I
landed on the design decisions for hashcards.&lt;/p&gt;

&lt;h1 id=&quot;anki&quot;&gt;Anki&lt;/h1&gt;

&lt;p&gt;&lt;a href=&quot;https://apps.ankiweb.net/&quot;&gt;Anki&lt;/a&gt; was the first SR system I used. It’s open source, so it will be around
forever; it has a million plugins; it was the first SR system to use &lt;a href=&quot;/article/implementing-fsrs-in-100-lines&quot;&gt;FSRS&lt;/a&gt; for
scheduling. It has really rich stats, which I think are mostly useless but are
fun to look at. And the &lt;a href=&quot;https://docs.ankiweb.net/getting-started.html#note-types&quot;&gt;note types&lt;/a&gt; feature is really good: it lets you
generate a large number of flashcards automatically from structured data.&lt;/p&gt;

&lt;p&gt;The central problem with Anki is that the interface is really bad. This
manifests in various ways.&lt;/p&gt;

&lt;p&gt;First, it is ugly to look at, particularly the review screen. And this
diminishes your enjoyment of what is already an often boring and frustrating
process.&lt;/p&gt;

&lt;p&gt;Second, doing simple things is hard. A nice feature of Mochi is that when you
start the app you go right into review mode. You’re drilling flashcards before
you even realize it. Anki doesn’t have a “study all cards due today”, rather,
you have to manually go into a deck and click the “Study Now” button. So what I
would do is put all my decks under a “Root” deck, and study that. But this is a
hack.&lt;/p&gt;

&lt;p&gt;And, third: card input uses WYSIWYG editing. So, you’re either jumping from the
keyboard to the mouse (which increases latency, and makes flashcard creation
more frustrating) or you have to remember all these keybindings to do basic
things like “make this text a cloze deletion” or “make this &lt;a href=&quot;https://docs.ankiweb.net/math.html&quot;&gt;TeX math&lt;/a&gt;”.&lt;/p&gt;

&lt;p&gt;Finally, plugins are a double-edged sword. Because having the &lt;em&gt;option&lt;/em&gt; to use
them is nice, but the experience of &lt;em&gt;actually&lt;/em&gt; using most plugins is bad. The
whole setup feels janky, like a house of cards. Most of the time, if a feature
is not built into the app itself, I would rather live without it than use a plugin.&lt;/p&gt;

&lt;h1 id=&quot;mochi&quot;&gt;Mochi&lt;/h1&gt;

&lt;p&gt;&lt;a href=&quot;https://mochi.cards/&quot;&gt;Mochi&lt;/a&gt; feels like it was built to address the main complaint about Anki: the
interface. It is intuitive, good looking, shortcut-rich. No jank. Instead of
WYSIWYG, card text is Markdown: this is delightful.&lt;/p&gt;

&lt;p&gt;There’s a few problems. While Markdown is a very low-friction way to write
flashcards, cloze deletions in Mochi are very verbose. In hashcards, you can
write this:&lt;/p&gt;

&lt;div class=&quot;language-md highlighter-rouge&quot;&gt;
  &lt;div class=&quot;highlight&quot;&gt;
    &lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Speech is [produced] in [Broca&apos;s] area.
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;The equivalent in Mochi is this:&lt;/p&gt;

&lt;div class=&quot;language-md highlighter-rouge&quot;&gt;
  &lt;div class=&quot;highlight&quot;&gt;
    &lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Speech is {{1::produced}} in {{2::Broca&apos;s}} area.
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;This is a lot of typing. And you might object that it’s only a few characters
longer. But when you’re studying from a textbook, or when you’re copying words
from a vocabulary table, these small frictions add up. If writing flashcards is
frustrating, you’ll write fewer of them: and that means less knowledge
gained. Dually, a system that makes flashcard creation as frictionless as
possible means more flashcards, and more knowledge.&lt;/p&gt;

&lt;p&gt;Another problem is that Mochi doesn’t have an equivalent of Anki’s &lt;a href=&quot;https://docs.ankiweb.net/getting-started.html#note-types&quot;&gt;note
types&lt;/a&gt;. For example: you can make a note type for chemical elements, with
fields like atomic number, symbol, name, etc., and write templates to generate
flashcards asking questions like:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;What is the atomic number of [name]?&lt;/li&gt;
  &lt;li&gt;What element has atomic number [number]?&lt;/li&gt;
  &lt;li&gt;What is the symbol for [name]?&lt;/li&gt;
  &lt;li&gt;What element has symbol [symbol]?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And so on for other properties. This is good. Automation is good. Less work,
more flashcards. Mochi doesn’t have this feature. It has &lt;a href=&quot;https://mochi.cards/docs/#templates&quot;&gt;templates&lt;/a&gt;, but
these are not as powerful.&lt;/p&gt;

&lt;p&gt;But the biggest problem with Mochi, I think, is the algorithm. Until &lt;a href=&quot;https://x.com/MochiCardsApp/status/1924692507570667630&quot;&gt;very
recently&lt;/a&gt;, when they added beta support for FSRS, the algorithm used by
Mochi was even simpler than &lt;a href=&quot;/article/implementing-sm2-in-rust&quot;&gt;SM-2&lt;/a&gt;. It was based on &lt;a href=&quot;https://mochi.cards/docs/faq/#which-spaced-repetition-algorithm-does-mochi-use&quot;&gt;multipliers&lt;/a&gt;:
remembering a card multiplies its interval by a number &amp;gt;1, forgetting a card
multiplies its interval by a number between 0 and 1.&lt;/p&gt;

&lt;p&gt;The supposed rationale for this is simplicity: the user can reason about the
algorithm more easily. But I think this is pointless. The whole point of an SR
app is the software manages the schedule for you, and the user is completely
unaware of how the scheduler works. The optimality is to have the most advanced
possible scheduling algorithm (meaning the one that yields the most recall for
the least review time) under the most intuitive interface possible, and the user
just reaps the benefits.&lt;/p&gt;

&lt;p&gt;Obviously without an RCT we can’t compare Mochi/&lt;a href=&quot;/article/implementing-sm2-in-rust&quot;&gt;SM-2&lt;/a&gt;/FSRS, but my subjective
experience of it is that the algorithm works well for the short-term, and
falters on the long-term. It’s very bad when you forget a mature card: if a card
has an interval of sixty days, and you click forget, you don’t reset the
interval to one day (which is good, because it helps you reconsolidate the lost
knowledge). Rather, the interval is multiplied by the forget multiplier (by
default: 0.5) down to &lt;em&gt;thirty days&lt;/em&gt;. What’s the use? If I forgot something after
sixty days, I surely won’t have better recall in thirty.&lt;/p&gt;

&lt;p&gt;You can fix this by setting the forget multiplier to zero. But you have to know
this is how it works, and, crucially: I don’t want to configure things! I don’t
want “scheduler parameter finetuning” to be yet another skill I have to acquire:
I want the scheduler to &lt;em&gt;just work&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;In general, I think spaced repetition algorithms are too optimistic. I’d rather
see cards slightly more often, and spend more time reviewing things, than get
stuck in “forgetting hell”. But developers have to worry that making the system
too burdensome will hurt retention.&lt;/p&gt;

&lt;p&gt;In Anki, it’s the interface that’s frustrating, but the algorithm works
marvelously. In Mochi, the interface is delightful, but it’s the algorithm
that’s frustrating. Because you can spend months and months drilling flashcards,
building up your collection, but when the cards cross some invisible age
threshold, you start to forget them, and the algorithm does not help you relearn
things you have forgotten. Eventually I burned out on it and stopped doing my
reviews, because I expected to forget everything eventually anyhow. And now they
added support for FSRS, but by now I have 1700 cards overdue.&lt;/p&gt;

&lt;p&gt;Additionally: Mochi has only two buttons, “Forgot” and “Remembered”. This is
simpler for the user, yes, but most SR scheduling algorithms have more options
for a reason: different degrees of recall adjust the card parameters by
different magnitudes.&lt;/p&gt;

&lt;h1 id=&quot;hashcards&quot;&gt;Hashcards&lt;/h1&gt;

&lt;p&gt;What do I want from a spaced repetition system?&lt;/p&gt;

&lt;p&gt;The first thing is: card creation must be frictionless. I have learned that the
biggest bottleneck in spaced repetition, for me, is not doing the reviews (I am
very disciplined about this and have done SR reviews daily for months on end),
it’s not even converting conceptual knowledge into flashcards, the biggest
bottleneck is just entering cards into the system.&lt;/p&gt;

&lt;p&gt;The surest way to shore up your knowledge of some concept or topic is to write
more flashcards about it: asking the same question in different ways, in
different directions, from different angles. More volume means you see the same
information more often, asking in different ways prevents “memorizing the shape
of the card”, and it acts as a kind of redundancy: there are multiple edges
connecting that bit of knowledge to the rest of your mind.&lt;/p&gt;

&lt;p&gt;And there have been many times where I have thought: I would make this more
solid by writing another flashcard. But I opted not to because the marginal
flashcard is too effortful.&lt;/p&gt;

&lt;p&gt;If getting cards into the system involves a lot of friction, you write fewer
cards. And there’s an opportunity cost: the card you don’t write is a concept
you don’t learn. Integrated across time, it’s entire oceans of knowledge which
are lost.&lt;/p&gt;

&lt;p&gt;So: the system should make card entry effortless. This was the guiding principle
behind the design of the hashcards text format. For example, cloze deletions use
square brackets because in a US keyboard, square brackets can be typed without
pressing shift (compare Mochi’s curly brace). And it’s one bracket, not
two. Originally, the format was one line per card, with blank lines separating
flashcards, and question-answer cards used slashes to separate the sides, like
so:&lt;/p&gt;

&lt;div class=&quot;language-markdown highlighter-rouge&quot;&gt;
  &lt;div class=&quot;highlight&quot;&gt;
    &lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;What is the atomic number of carbon? / 6

The atomic number of [carbon] is [6].
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;And this is strictly less friction. But it creates a problem for multi-line
flashcards, which are common enough that they should not be a second-class
citizen. Eventually, I settled on the current format:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;
  &lt;div class=&quot;highlight&quot;&gt;
    &lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Q: What is the atomic number of carbon?
A: 6

C: The atomic number of [carbon] is [6].
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;Which is only slightly more typing, and has the benefit that you can easily
visually identify where a card begins and ends, and what kind of card it is. I
spent a lot of time arguing back and forth with &lt;a href=&quot;https://claude.ai/&quot;&gt;Claude&lt;/a&gt; about what the optimal
format should be.&lt;/p&gt;

&lt;p&gt;Another source of friction is not creating the cards but &lt;em&gt;editing&lt;/em&gt; them. The
central problem is that your knowledge changes and improves over time. Often
textbooks take this approach where Chapter 1 introduces one kind of ontology,
and by Chapter 3 they tell you, “actually that was a lie, here’s the real
ontology of this subject”, and then you have to go back and edit the old
flashcards to match. Because otherwise you have one card asking, e.g., for the
undergraduate definition of some concept, while another asks you for the
graduate-level definition, creating ambiguity.&lt;/p&gt;

&lt;p&gt;For this reason, when studying from a textbook, I create a deck for the
textbook, with sub-decks for each chapter. That makes it easy to match the
flashcards to their source material (to ensure they are aligned) and each
chapter deck only has a few tens of cards usually, keeping them navigable.&lt;/p&gt;

&lt;p&gt;Sometimes you wrote multiple cards for the same concept, so you have to update
them all at once. Finding the related ones can be hard if the deck is large. In
hashcards, a deck is just a Markdown file. The cards immediately above and below
a card are usually semantically related. You just scroll up and down and make
the edits in place.&lt;/p&gt;

&lt;p&gt;But why plain-text files in a Git repo? Why not use the above format, but in a
“normal” app with a database?&lt;/p&gt;

&lt;p&gt;The vague idea of a spaced repetition system where flashcards are stored as
plain-text files in a Git repo had been kicking around my cranium for a long
time. I remember asking an Ankihead on IRC circa 2011 if such a thing
existed. At some point I read &lt;a href=&quot;https://notes.andymatuschak.org/My_implementation_of_a_personal_mnemonic_medium&quot;&gt;Andy Matuschak’s note&lt;/a&gt; on his
implementation of an SR system. In his system, the flashcards are colocated with
prose notes. The notation is similar to mine: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Q&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;A&lt;/code&gt; tags for
question-answer cards, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{curly braces}&lt;/code&gt; for cloze deletions. And the cards
are content-addressed: identified by their hash. Which is an obviously good
idea. But his code is private and, besides, I feel that prose notes and
flashcards are very different beasts, and I don’t need or want them to mix.&lt;/p&gt;

&lt;p&gt;But I think the idea of plain-text spaced repetition got bumped up the priority
queue because I spontaneously started using a workflow that was similar to my
current hashcards workflow.&lt;/p&gt;

&lt;p&gt;When studying from a textbook or a website, I’d write flashcards in a Markdown
file. Usually, I used a shorthand like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[foo]&lt;/code&gt; for cloze deletions. Then I’d use
a Python script to transform the shorthand into the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{{1::foo}}&lt;/code&gt; notation used by Mochi. And I’d edit the flashcards in the file, as
my knowledge built up and my sense of what was relevant and important to
remember improved. And then, when I was done with the chapter or document or
whatever, only then, I would manually import the flashcards into Mochi.&lt;/p&gt;

&lt;p&gt;And it struck me that the last step was kind of unnecessary. I was already
writing my flashcards as lightly-annotated Markdown in plain-text files. I had
&lt;a href=&quot;/article/implementing-fsrs-in-100-lines&quot;&gt;already implemented FSRS&lt;/a&gt; out of curiosity. I was looking for a
personal project to build during funemployment. So hashcards was by then a very
neatly-shaped hole that I just needed to paint inside.&lt;/p&gt;

&lt;p&gt;It turns out that using plain-text storage has many synergies:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;You can edit the cards using whatever editor you use, build up a library of
card-creating macros, and navigate the collection using the editor’s file
browser.&lt;/li&gt;
  &lt;li&gt;You can query and update the collection using standard Unix tools, or a
programming language, e.g. using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wc&lt;/code&gt; to get the total number of words in the
collection, or using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;awk&lt;/code&gt; to make a bulk-update to a set of cards.&lt;/li&gt;
  &lt;li&gt;You can use Git for version control. Git is infinitely more featureful than
the change-tracking of any SR app: you can edit multiple cards in one commit,
branch, merge, use pull requests, etc.&lt;/li&gt;
  &lt;li&gt;You can make your flashcards public on GitHub. I often wish people put more of
themselves out there: their blog posts, their dotfiles, their study notes. And
why not their flashcards? Even if they are not useful to someone else, there
is something enjoyable about reading what someone else finds interesting, or
enjoyable, or worth learning.&lt;/li&gt;
  &lt;li&gt;You can generate flashcards using scripts (e.g., turn a CSV of foreign
language vocabulary into a deck of flashcards), and write a Makefile to tie
the script, data source, and target together. I &lt;a href=&quot;https://github.com/eudoxia0/flashcards/blob/87b082e4723e5b1b286e3bb5378316f464cfc28f/Makefile&quot;&gt;do this&lt;/a&gt; in my
personal deck. Anki’s &lt;a href=&quot;https://docs.ankiweb.net/getting-started.html#note-types&quot;&gt;note types&lt;/a&gt; don’t have to be built into hashcards,
rather, you can DIY it using some Python and make.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result is a system where creating and editing flashcards is nearly
frictionless, that uses an advanced spaced repetition scheduler, and which
provides an elegant UI for drilling flashcards. I hope others will find it
useful.&lt;/p&gt;

</content:encoded>
      </item>
    
      <item>
        <title>Adding Planets to Celestia on macOS</title>
        <description>A beginner&apos;s guide to modding Celestia.</description>
        <pubDate>Tue, 29 Jul 2025 00:00:00 +0000</pubDate>
        <link>https://borretti.me/article/adding-planets-to-celestia-on-macos</link>
        <guid isPermaLink="true">
          https://borretti.me/article/adding-planets-to-celestia-on-macos
        </guid>
        
        <content:encoded>&lt;p&gt;tl;dr: you have to modify the application bundle.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://celestiaproject.space/&quot;&gt;Celestia&lt;/a&gt; is a space simulator: you can fly around space and look at moons and exoplants, fast forward time. It is sometimes &lt;a href=&quot;https://bsky.app/profile/timberwind.bsky.social/post/3ldamhmnp622j&quot;&gt;used&lt;/a&gt; by sci-fi artists for worldbuilding because you can easily add new stars/planets/megastructures/spacecraft. Some people have built &lt;a href=&quot;https://no56.neocities.org/articles/ran&quot;&gt;whole virtual worlds&lt;/a&gt; for storytelling in Celestia. The &lt;a href=&quot;https://www.orionsarm.com/&quot;&gt;Orion’s Arm&lt;/a&gt; collaborative worldbuilding project has a collection of &lt;a href=&quot;https://www.orionsarm.com/page/326&quot;&gt;Celestia addons&lt;/a&gt; so you can explore the world of the year 10,000 AT.&lt;/p&gt;

&lt;p&gt;But the documentation is sparse and old. As with many things: the biggest hurdle to starting is just knowing which files go in which directories.&lt;/p&gt;

&lt;p&gt;Celestia uses &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.ssc&lt;/code&gt; (solar system catalogue) files to define planets. These are plain-text files with a syntax resembling &lt;a href=&quot;https://github.com/hashicorp/hcl&quot;&gt;HCL&lt;/a&gt;. Let’s create baby’s first planet: below is a minimal &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.ssc&lt;/code&gt; file that adds a planet “Alpha” around the star &lt;a href=&quot;https://en.wikipedia.org/wiki/HN_Librae&quot;&gt;Gliese 555&lt;/a&gt;:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;
  &lt;div class=&quot;highlight&quot;&gt;
    &lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&quot;Alpha&quot; &quot;Gliese 555&quot;
{
  Texture   &quot;asteroid.*&quot;
  Mass      1             # Earth masses
  Radius    6378          # km
  EllipticalOrbit {
    Period          1.0 # years
    SemiMajorAxis   1.0 # long axis
    Eccentricity    0.0 # circular
  }
}
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;Now, what you would hope is that there exists a standard directory like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/.celestia/planets/&lt;/code&gt; you can put this into. I spent a lot of time looking through old docs and source code for this, and I’m writing this so others don’t have to. Unfortunately, at least on macOS, you have to modify the application &lt;a href=&quot;https://en.wikipedia.org/wiki/Bundle_(macOS)&quot;&gt;bundle&lt;/a&gt; itself. This feels morally wrong, but it works.&lt;/p&gt;

&lt;p&gt;Save the above code as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;alpha.ssc&lt;/code&gt;, and execute:&lt;/p&gt;

&lt;div class=&quot;language-sh highlighter-rouge&quot;&gt;
  &lt;div class=&quot;highlight&quot;&gt;
    &lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;mkdir&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; /Applications/Celestia.app/Contents/Resources/CelestiaResources/extras/
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cp &lt;/span&gt;alpha.ssc /Applications/Celestia.app/Contents/Resources/CelestiaResources/extras/alpha.ssc
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;Open Celestia, and navigate to Gliese 555 (press enter, type “Gliese 555”, press enter, press g). You should see a new planet:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/content/adding-planets-to-celestia-on-macos/far.webp&quot; alt=&quot;A screenshot of Celestia showing the star Gliese 555, with the orbit of a new planet around it.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Zooming in, you can see it’s using the built-in asteroid texture:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/content/adding-planets-to-celestia-on-macos/near.webp&quot; alt=&quot;A screenshot of Celestia showing the planet Alpha around Gliese 555.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;To verify it’s reading the right file, press tilde and use the arrow keys to scroll up the logs, and you should see a line like:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;
  &lt;div class=&quot;highlight&quot;&gt;
    &lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Loading solar system catalog: extras/alpha.ssc
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;Celestia traverses the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;extras&lt;/code&gt; directory recursively, so you can put your &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.ssc&lt;/code&gt; files inside folders to organize large worldbuilding projects.&lt;/p&gt;

</content:encoded>
      </item>
    
      <item>
        <title>Notes on Managing ADHD</title>
        <description>Strategies and tactics for staying productive.</description>
        <pubDate>Thu, 12 Jun 2025 00:00:00 +0000</pubDate>
        <link>https://borretti.me/article/notes-on-managing-adhd</link>
        <guid isPermaLink="true">
          https://borretti.me/article/notes-on-managing-adhd
        </guid>
        
        <content:encoded>&lt;blockquote&gt;
  &lt;p&gt;The pleasure is in foreseeing it, not in bringing it to term.&lt;/p&gt;

  &lt;p class=&quot;cite&quot;&gt; —  Jorge Luis Borges, &lt;a href=&quot;https://www.goodreads.com/book/show/864175.Selected_Non_fictions&quot;&gt;&lt;em&gt;Selected Non-Fictions&lt;/em&gt;&lt;/a&gt; &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This post is about managing ADHD. It is divided into two sections: “Strategies” describes the high-level control system, “Tactics” is a list of micro-level improvements (really it should be called “stratagems”, since most are essentially about tricking yourself).&lt;/p&gt;

&lt;h1 class=&quot;no_toc&quot; id=&quot;contents&quot;&gt;Contents&lt;/h1&gt;

&lt;ol id=&quot;markdown-toc&quot;&gt;
  &lt;li&gt;&lt;a href=&quot;#strategies&quot; id=&quot;markdown-toc-strategies&quot;&gt;Strategies&lt;/a&gt;    &lt;ol&gt;
      &lt;li&gt;&lt;a href=&quot;#chemistry-first&quot; id=&quot;markdown-toc-chemistry-first&quot;&gt;Chemistry First&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#memory&quot; id=&quot;markdown-toc-memory&quot;&gt;Memory&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#energy&quot; id=&quot;markdown-toc-energy&quot;&gt;Energy&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#procrastination&quot; id=&quot;markdown-toc-procrastination&quot;&gt;Procrastination&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#introspection&quot; id=&quot;markdown-toc-introspection&quot;&gt;Introspection&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#time&quot; id=&quot;markdown-toc-time&quot;&gt;Time&lt;/a&gt;&lt;/li&gt;
    &lt;/ol&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#tactics&quot; id=&quot;markdown-toc-tactics&quot;&gt;Tactics&lt;/a&gt;    &lt;ol&gt;
      &lt;li&gt;&lt;a href=&quot;#task-selection&quot; id=&quot;markdown-toc-task-selection&quot;&gt;Task Selection&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#visual-field-management&quot; id=&quot;markdown-toc-visual-field-management&quot;&gt;Visual Field Management&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#project-check-ins&quot; id=&quot;markdown-toc-project-check-ins&quot;&gt;Project Check-Ins&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#centralize-your-inboxes&quot; id=&quot;markdown-toc-centralize-your-inboxes&quot;&gt;Centralize Your Inboxes&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#inbox-zero&quot; id=&quot;markdown-toc-inbox-zero&quot;&gt;Inbox Zero&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#inbox-bankruptcy&quot; id=&quot;markdown-toc-inbox-bankruptcy&quot;&gt;Inbox Bankruptcy&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#do-it-on-your-own-terms&quot; id=&quot;markdown-toc-do-it-on-your-own-terms&quot;&gt;Do It On Your Own Terms&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#replace-interrupts-with-polling&quot; id=&quot;markdown-toc-replace-interrupts-with-polling&quot;&gt;Replace Interrupts with Polling&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#accountability-buddy&quot; id=&quot;markdown-toc-accountability-buddy&quot;&gt;Accountability Buddy&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#plan-first-do-later&quot; id=&quot;markdown-toc-plan-first-do-later&quot;&gt;Plan First, Do Later&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#derailment&quot; id=&quot;markdown-toc-derailment&quot;&gt;Derailment&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#using-neuroticism-to-defeat-adhd&quot; id=&quot;markdown-toc-using-neuroticism-to-defeat-adhd&quot;&gt;Using Neuroticism to Defeat ADHD&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#the-master-of-drudgery&quot; id=&quot;markdown-toc-the-master-of-drudgery&quot;&gt;The Master of Drudgery&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#thrashing&quot; id=&quot;markdown-toc-thrashing&quot;&gt;Thrashing&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#put-travel-in-the-calendar&quot; id=&quot;markdown-toc-put-travel-in-the-calendar&quot;&gt;Put Travel in the Calendar&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#choice-of-tools&quot; id=&quot;markdown-toc-choice-of-tools&quot;&gt;Choice of Tools&lt;/a&gt;&lt;/li&gt;
    &lt;/ol&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#resources&quot; id=&quot;markdown-toc-resources&quot;&gt;Resources&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#acknowledgements&quot; id=&quot;markdown-toc-acknowledgements&quot;&gt;Acknowledgements&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h1 id=&quot;strategies&quot;&gt;Strategies&lt;/h1&gt;

&lt;p&gt;High-level advice, control systems.&lt;/p&gt;

&lt;h2 id=&quot;chemistry-first&quot;&gt;Chemistry First&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;ADHD has a biological cause and drugs are the first-line treatment for good reasons. There is no virtue in trying to beat it through willpower alone.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The first-line treatment for ADHD is stimulants. Everything else in this post works best as a complement to, rather than as an alternative to, stimulant medication. In fact most of the strategies described here, I was only able to execute &lt;em&gt;after&lt;/em&gt; starting stimulants. For me, chemistry is the critical node in the tech tree: the todo list, the pomodoro timers, etc., all of that was unlocked by the medication.&lt;/p&gt;

&lt;p&gt;Some people can’t tolerate a specific stimulant. But there are many stimulant and non-stimulant drugs for ADHD. I would prefer to exhaust all the psychiatric options before white-knuckling it.&lt;/p&gt;

&lt;p&gt;A lot of people don’t want to take medication for shame-based reasons. There is a lot of pill-shaming in the culture. You must learn to ignore it: we are automata, our minds are molecules in salt water.&lt;/p&gt;

&lt;h3 id=&quot;example-melatonin&quot;&gt;Example: Melatonin&lt;/h3&gt;

&lt;p&gt;As a motivating example for the “salt water automaton” view: I struggled with sleep hygiene for a long time. It felt like WW1: throwing wave after wave of discipline at it and always failing. I would set an alarm, for, say, 10pm, that said: it is time to go to bed. How many times did I obey it? Never. I was always doing something more important.&lt;/p&gt;

&lt;p&gt;What fixed it? Melatonin. I have an alarm that goes off at 8pm to remind me to take melatonin. The point of the alarm is not, “now you must log off”, which is a very discipline-demanding task. The point of the alarm is simply: take this pill. It takes but a moment. Importantly, I’m not committing to anything other than taking a pill. Thirty, forty minutes later, I &lt;em&gt;want&lt;/em&gt; to sleep. That is the key thing: the melatonin has changed my preferences. And then I don’t need willpower to close the sixteen Wikipedia tabs or whatever, because I &lt;em&gt;want&lt;/em&gt; to sleep more than I want to scroll, or watch YouTube.&lt;/p&gt;

&lt;h3 id=&quot;internal-and-external-change&quot;&gt;Internal and External Change&lt;/h3&gt;

&lt;p&gt;The broader perspective here is that personal growth is a dialogue between internal changes and external changes.&lt;/p&gt;

&lt;p&gt;Internal changes might come from medication, meditation, therapy, coaching, or practicing habits for a long enough time. External changes are the scaffolding around the brain: using a todo list, and using it effectively. Using a calendar. Clearing your desk so you don’t get distracted by things. Journaling, so that you can introspect and notice patterns: which behaviours leads to a good workday, and which behaviours lead to a day being wasted.&lt;/p&gt;

&lt;p&gt;Are internal changes more important? Kind of. It’s more a back and forth, where internal changes unlock external changes which unlock further internal changes.&lt;/p&gt;

&lt;p&gt;Here’s an example: you (having undiagnosed ADHD) try to set a schedule, or use a todo list, or clean your bed every day, but it doesn’t stick. So you get on medication, and the medication lets you form your first habit: which is using a todo list app consistently, checking it every morning. Then, with the todo list as a core part of your exocortex, you start adding recurring tasks, and forming other simple habits: you have a daily recurring task to make your bed, and so every morning when you check the todo list, you see the task, and make your bed, and in time, with your now-functioning dopamine system, you make a habit to make your bed every day, such that you no longer need to have that in the todo list.&lt;/p&gt;

&lt;p&gt;So the timeline is:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Internal change: starting medication unlocks…&lt;/li&gt;
  &lt;li&gt;External change: using a todo list, which provides scaffolding (e.g. daily recurring tasks) for forming new habits, which unlocks&lt;/li&gt;
  &lt;li&gt;Internal change: new habits formed (make bed, brush teeth in the morning)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Taking Ritalin with no plan for what you will do today/tomorrow/this week doesn’t work. Dually, an ambitious todo list will sit idle if your brain won’t let you execute it. So personal growth comes from using &lt;em&gt;both&lt;/em&gt; internal and external changes, like a ladder with alternating left-right steps.&lt;/p&gt;

&lt;h2 id=&quot;memory&quot;&gt;Memory&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;A todo list is a neuroprosthesis that augments long-term memory for tasks.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I use &lt;a href=&quot;https://www.todoist.com/&quot;&gt;Todoist&lt;/a&gt; on my desktop and my phone. The pro plan is worth it. I don’t really think of it as an app, rather, it’s a cognitive prosthesis.&lt;/p&gt;

&lt;p&gt;The todo list provides three things:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Memory:&lt;/strong&gt; the list remembers things for me. I’m not at the mercy of my brain randomly pinging me that I forgot to do X or I want to someday do Y. The todo list remembers.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Order:&lt;/strong&gt; the todo list lets you drag and drop tasks around, so you can figure out the ordering in which you’re going to do them.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Hierarchy:&lt;/strong&gt; the todo list lets you break tasks down hierarchically and without limit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Of these, the most important is memory. The todolist is an action-oriented long term memory prosthesis.&lt;/p&gt;

&lt;p&gt;This is especially useful for habit formation: my biggest blocker with forming habits was just remembered that I’d committed to doing something. If you think, i will make the bed every day, you might do it today, tomorrow, and by the third day you forget. You’re failing by simply forgetting to show up, which is a sad way to fail. By making something a recurring task on the todo list, it ensures I will see it. In a sense, the todo list turns many habits into one. You don’t need to remember “I will make my bed every day”, “I will floss my teeth every night”, etc., because the todolist remembers all those things for you. You only need to form a &lt;em&gt;single&lt;/em&gt; habit: checking the todo list.&lt;/p&gt;

&lt;p&gt;Analogously, I often fail to finish projects simply because I forget about them. I start reading a book, but I don’t write it down anywhere (say, in Goodreads) that “I’m reading this book” is something I have committed to. I leave the book on a table where it’s out of sight (and therefore out of mind) for all of my waking hours. I glance at it occasionally and think, oh, yeah, I was reading that book, and then I’m distracted by something else. And weeks later, when I’ve already started another book, I notice the first book, with the bookmark on page 20, abandoned.&lt;/p&gt;

&lt;p&gt;The todolist prevents this failure mode: you create a project to represent reading the book, and that project is now tracked, and when you open the todo list, you can see it in the list of active projects.&lt;/p&gt;

&lt;h3 id=&quot;how-i-use-todoist&quot;&gt;How I Use Todoist&lt;/h3&gt;

&lt;p&gt;In Todoist, every task is part of a &lt;a href=&quot;https://www.todoist.com/help/articles/introduction-to-projects-TLTjNftLM&quot;&gt;project&lt;/a&gt; (which really should just be called a list). My sidebar looks like this:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/content/notes-on-managing-adhd/sidebar.webp&quot; width=&quot;300&quot; alt=&quot;A screenshot of my Todoist sidebar, showing a list of projects described below.&quot; style=&quot;margin-left: auto; margin-right: auto;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tasks&lt;/strong&gt; is the list for ad-hoc tasks. Mostly chores and things that don’t fit in elsewhere. Unload the dishwasher, reply to this email, etc. The only rule for this list is that everything in it must be scheduled.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Groceries&lt;/strong&gt; is self-explanatory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ideas&lt;/strong&gt; is the where every half-formed goal, intention, project idea etc. goes. “Go deeper into metta” and “learn how to use the slide rule” and “go penguin watching in Manly” and “write a journalling app” and “learn &lt;a href=&quot;https://redex.racket-lang.org/&quot;&gt;PLT Redex&lt;/a&gt;”. I put these things here so that they don’t live in my brain. And occasionally I go through the list and promote something into an actual, active project.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Blog&lt;/strong&gt; is like the ideas list specifically ideas for blog posts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reading List&lt;/strong&gt; is for media I want to consume. This is divided into: fiction books, non-fiction books, technical books, blog posts, papers, games, films.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cycles&lt;/strong&gt; is for recurring tasks. This one is divided into sections by period: daily, weekly, and above. The daily recurring tasks are things like “take vitamin D”, “meditate”, and the inbox-clearing task.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Projects&lt;/strong&gt; is a container for actual projects: an objective which takes multiple tasks to accomplish. Why lift projects into lists? Why not just use a top-level task to represent the project’s objective, and nested subtasks to represent the execution steps of the project? Because having the project in the sidebar is one mechanism I use to ensure I don’t forget about it. Every time I glance at the todo list, I can see the list of active projects. I can notice if something has not been worked on for a while, and act on it. Otherwise: out of sight, out of mind.&lt;/p&gt;

&lt;h2 id=&quot;energy&quot;&gt;Energy&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;The difficulty class of the tasks you can perform declines throughout the day.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;There are many metaphors for the concept of mental energy. &lt;a href=&quot;https://en.wikipedia.org/wiki/Spoon_theory&quot;&gt;Spoon theory&lt;/a&gt;, for example. The usual metaphor is that “mental energy” is like a battery that is drained through the day, in greater and lesser quantities, and is replenished by sleep.&lt;/p&gt;

&lt;p&gt;To me, energy is less like a battery and more like voltage. Some machines require a threshold voltage to operate. Below that voltage they don’t just operate slower, they don’t operate at all. Analogously, different categories of activity have different threshold voltages. For me, it’s like this:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Things I am averse to, the things I intuitively want to put off because they bring up painful emotions, are high-voltage.&lt;/li&gt;
  &lt;li&gt;Creative, open-ended work is high-voltage to start, but once you get started, keeping it going is medium-voltage.&lt;/li&gt;
  &lt;li&gt;Simple chores like cleaning, throwing clothes in the washing machine, etc. are low-voltage.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And when I wake up I have the highest possible voltage, and throughout the course of the day the voltage declines. And that’s the key difference from spoon theory: spoons are fungible across time, voltage is not. For each category of activity, there is a span of the day when I can action it.&lt;/p&gt;

&lt;p&gt;When I wake up, I do my morning routine, get some quick wins, and then I try to tackle the thing I dread the most, as early in the morning as possible, because that’s the time of day when I have the most energy and self-control. I get that done and I move on.&lt;/p&gt;

&lt;p&gt;(Another reason to do the dreaded tasks first: if you put it off to, say, late morning, well, why not put it off again? And again and again. And then it’s 7pm and you can’t even think about the task, and it’s late, and I don’t have energy, so I couldn’t even do it if I wanted to, so let’s do it tomorrow.)&lt;/p&gt;

&lt;p&gt;And then, when I have removed that burden, I work on projects. The creative, generative, intellectual things. The things that move some kind of needle, and aren’t just pointless chores.&lt;/p&gt;

&lt;p&gt;And when I run out of energy to create, I read.&lt;/p&gt;

&lt;p&gt;And when I run out of energy to read, I clean and go to the gym and do the other things.&lt;/p&gt;

&lt;p&gt;And when the sun goes down everything starts to unravel: I have zero energy and the lazy dopamine-seeking behaviour comes out. So I take melatonin, and try to be in bed before the instant gratification monkey seizes power.&lt;/p&gt;

&lt;h2 id=&quot;procrastination&quot;&gt;Procrastination&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Typology of procrastination, approaches.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In my ontology there are three types of procrastination:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;ADHD Procrastination:&lt;/strong&gt; you want to do the task, but can’t because of distraction/hyperactivity.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Anxious Procrastination:&lt;/strong&gt; you know you have to do the task, but you don’t want to, because it triggers difficult emotions.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Decision Paralysis Procrastination:&lt;/strong&gt; you &lt;em&gt;don’t know&lt;/em&gt; how to execute the task, because it involves a decision and you have difficulty making the decision.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;adhd-procrastination&quot;&gt;ADHD Procrastination&lt;/h3&gt;

&lt;p&gt;This is the easiest kind to address. The solution is pharmacological treatment for ADHD + having a productivity system and some tricks.&lt;/p&gt;

&lt;h3 id=&quot;anxious-procrastination&quot;&gt;Anxious Procrastination&lt;/h3&gt;

&lt;p&gt;This one is harder. The good thing is you know, cognitively, what you have to do. The hard part is getting over the aversion.&lt;/p&gt;

&lt;p&gt;In the short term, the way to fix this is to do it scared. Accept the anxiety. Asking for help also works, sometimes you just need someone in the room with you when you hit send on the email. You can also use techniques like CBT to rationally challenge the source of the anxiety and maybe overcome it.&lt;/p&gt;

&lt;p&gt;In the long term: write down the things you procrastinate one due to anxiety, and find the common through-line, or the common ancestor. By identifying the emotional root cause, you can work on fixing it.&lt;/p&gt;

&lt;h3 id=&quot;decision-paralysis-procrastination&quot;&gt;Decision Paralysis Procrastination&lt;/h3&gt;

&lt;p&gt;And this is the hardest, because you don’t know, cognitively, what the right choice is, and also you probably have a lot of anxiety/aversion around it. Many things in life are susceptible to this: you have set of choices, there’s good arguments for/against each one, and you have a lot of uncertainty as to the outcomes. And so you ruminate on it endlessly.&lt;/p&gt;

&lt;p&gt;I don’t have a good general solution for this.&lt;/p&gt;

&lt;p&gt;Talking to people helps: friends, therapists, Claude. This works because thinking by yourself has diminishing returns: you will quickly exhaust all the thoughts you will have about the problem, and start going in circles. Often people will bring up options/considerations I would never have thought of. Sometimes, if you’re lucky, that’s all it takes: someone mentions an option you had not considered and you realize, oh, it was all so simple.&lt;/p&gt;

&lt;p&gt;One thing to consider is that &lt;em&gt;thinking in your head&lt;/em&gt; is inherently circular, because you have a limited working memory, and you will inevitably start going in circles. Writing things down helps here. Treat the decision, or the emotions behind it, like an object of study, or an engineering problem. Sit down and write an essay about it. Name the arguments, number the bullet points, refer back to things. Make the thoughts into real, physical, manipulable entities.&lt;/p&gt;

&lt;h2 id=&quot;introspection&quot;&gt;Introspection&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Journaling is good for detecting maladaptive patterns and tracking your progress.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I keep a hierarchical journal in &lt;a href=&quot;https://obsidian.md/&quot;&gt;Obsidian&lt;/a&gt;. Hierarchical because I have entries for the days, weeks, months, and years. The directory tree looks like this:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;
  &lt;div class=&quot;highlight&quot;&gt;
    &lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Journal/
  Daily/
    YYYY/
      MM/
        YYYY-MM-DD.md
  Weekly/
    YYYY/
      YYYY-WW.md
  Monthly/
    YYYY/
      YYYY-MM.md
  Yearly/
    YYYY.md
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;In the morning I finish yesterday’s journal entry, and begin today’s. Every Sunday I write the review of the week, the first of each month I write the review of the previous month, the first of each year I review the past year. The time allotted to each review is in inverse proportion to its frequency: so a monthly review might take an hour while a yearly review might take up a whole morning.&lt;/p&gt;

&lt;p&gt;The daily reviews are pretty freeform. Weekly and above there’s more structure. For example, for the weekly reviews I will write a list of the salient things that happened in the week. Then I list on what went well and what went poorly. And then I reflect on how I will change my behaviour to make the next week go better.&lt;/p&gt;

&lt;p&gt;Journaling is a valuable habit. I started doing it for vague reasons: I wasn’t sure what I wanted to get out of it, and it took a long time (and long stretches of not doing it) until it became a regular, daily habit. I’ve been doing it consistently now for three years, and I can identify the benefits.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;The main benefit is that to change bad patterns, you have to notice them. And it is very easy to travel in a fix orbit, day in, day out, and not notice it. Laying it out in writing helps to notice the maladaptive coping mechanisms. Reading back over the journal entries helps you notice: when an event of type X happens, I react with Y.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Today’s journal entry is a good default place for writing ad-hoc notes or thoughts. Often I wanted to write something, but didn’t know where I would file it (how do you even file these little scraps of thought?) and from not knowing where to put it, I would not do it. Nowadays I just begin writing in the journal. Later, if it is valuable to file it away, I do so.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Creating a journal entry in the morning is a good opportunity to go over the goals and priorities for the day and explicitly restate them to myself.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;The final benefit is retrospection: I can look at the past and see how my life has changed. And this is often a positive experience, because the things that worried me didn’t come to pass, the things I used to struggle with are now easy, or at least easier.&lt;/p&gt;

    &lt;p&gt;There’s a paradox with productivity: when you grind executive function enough, things that you used to struggle with become quotidian. And so what was once the ceiling becomes the new floor. You no longer feel proud that you did X, Y, Z because that’s just the new normal. It’s like the hedonic treadmill. You might feel that you never get to “productive”. Journaling helps to combat this because you can see how far you’ve come.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;time&quot;&gt;Time&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Manage time at the macro level with calendars, at the micro level with timers.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;To manage time, you need a calendar (macro) and a timer (micro).&lt;/p&gt;

&lt;h3 id=&quot;macro&quot;&gt;Macro&lt;/h3&gt;

&lt;p&gt;At the macro level, I use the calendar very lightly. Mostly for social things (to ensure I don’t forget an event, and that I don’t double-book things). I also use it to schedule the gym: if the goal is to lift, say, five times a week, I schedule five time blocks to lift. Lifting is special because it has a lot of temporal constraints:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;I lift exactly &lt;em&gt;n&lt;/em&gt; times per week.&lt;/li&gt;
  &lt;li&gt;I lift at most once a day.&lt;/li&gt;
  &lt;li&gt;I lift in the evening, which potentially clashes with social things.&lt;/li&gt;
  &lt;li&gt;There are adjacency constraints, e.g. doing shoulders the day before chest is bad.&lt;/li&gt;
  &lt;li&gt;There is at least one rest day which has to be scheduled strategically (e.g. to have maximal distance between successive deadlift sessions).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;But outside these two categories, my calendar is empty.&lt;/p&gt;

&lt;p&gt;The calendar might be useful to you as a self-binding device. If you keep dragging some project along because you “haven’t made time” for it: consider making a time block in the calendar, and sticking to it. Creating a calendar event is, literally, making time: it’s like calling &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;malloc_time()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Some people use the calendar as their entire todo list. I think this kind of works if your todo list is very coarse grained: “buy groceries” and “go to the dentist”. But I have a very fine-grained todo list, and putting my tasks in the calendar would make it overwhelming.&lt;/p&gt;

&lt;p&gt;Another problem with calendars is they are too time-bound: if I make a calendar block to do something, and I don’t do it, the calendar doesn’t know it. It just sits there, forgotten, in the past. In a todo list, everything gets dragged along until I explicitly complete it. Along the same lines, the calendar is not good for collecting vague ideas and plans for things you want to do in the future, while todo lists are ideal for this.&lt;/p&gt;

&lt;h3 id=&quot;micro&quot;&gt;Micro&lt;/h3&gt;

&lt;p&gt;The problem with todo lists is that they’re timeless: there is no sense of urgency. You look at the list and think, I could do the next task now, or in five minutes, or in an hour. There’s always &lt;em&gt;some&lt;/em&gt; time left in the day. Or tomorrow. You need a way to manufacture urgency.&lt;/p&gt;

&lt;p&gt;If you have ADHD you’ve probably heard of the Pomodoro method, tried it, and bounced off it. The way it’s framed is very neurotypical: it’s scaffolding around &lt;em&gt;doing&lt;/em&gt;, but ADHD people often have problems with the doing itself. And so the scaffolding is kind of pointless.&lt;/p&gt;

&lt;p&gt;The method works well in three kinds of contexts:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Overcoming Aversion:&lt;/strong&gt; when you have a large number of microtasks, each of which takes a few seconds to a few minutes, but the number of them, and the uncertainty factor, makes the sum seem a lot larger. A classic example for me is having to reply to like ten different people. Realistically, each person can be handled in 15s. One or two might require a couple of minutes to compose a longer reply. But often I will avoid those tasks like the plague and drag them across the entire day.&lt;/p&gt;

    &lt;p&gt;The pomodoro method works here because you’re basically trading (up to) 25m of pain for an entire day’s peace and quiet. So you get all the annoying little tasks together, start a timer, and go through them. And usually you’re done in maybe ten minutes. And you feel &lt;em&gt;really&lt;/em&gt; good after, because all those annoying little tasks are done.&lt;/p&gt;

    &lt;p&gt;It really is amazing what a little bit of fake urgency can do.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Starting:&lt;/strong&gt; sometimes the problem is just starting. It is very trite, but it’s true. You have something you &lt;em&gt;want to want&lt;/em&gt; to do, but don’t &lt;em&gt;want&lt;/em&gt; to do. I want to want to read this book, to learn this topic, to write this blog post, to work on this software project. But I don’t &lt;em&gt;want&lt;/em&gt; to do it. The pomodoro method helps you start.&lt;/p&gt;

    &lt;p&gt;You’re not committing to finishing the project. You’re not committing to months or weeks or days or even hours of work. You’re committing to a half hour. And if you work just that half hour: great, promise kept. 30m a day, over the course of a single month, is 15h of work. And often I start a 30m timer and end up working four hours, and maybe that’s a good outcome.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Stopping:&lt;/strong&gt; dually, sometimes the problem is stopping. If you’re trying to advance multiple projects at the same time, if you hyperfocus on one, it eats into the time you allocated for the others. And more broadly, spending too much time on one project can derail all your plans for the day. Maybe you meant to go to the gym at 6pm but you got so stuck in with this project that it’s 8:30pm and you’re still glued to the screen. So the gym suffers, your sleep schedule suffers, etc.&lt;/p&gt;

    &lt;p&gt;Actually stopping when the pomodoro timer goes off can prevent excessive single-mindedness.&lt;/p&gt;

    &lt;p&gt;Additionally, the five-minute break at the end of the pomodoro block is useful. It’s a time to get up from the computer, unround your shoulders, practice mindfulness, essentially, all those little things that you want to do a few times throughout the day.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;h1 id=&quot;tactics&quot;&gt;Tactics&lt;/h1&gt;

&lt;p&gt;Stratagems, tricks.&lt;/p&gt;

&lt;h2 id=&quot;task-selection&quot;&gt;Task Selection&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;To select the next task, pick either the shortest or the most-procrastinated task.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I don’t like the word “prioritize”, because it has two subtly different meanings:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;“Weak prioritization” means to sort a list of tasks by some unspecified criterion, that is, to establish an order where some things are prior to another.&lt;/li&gt;
  &lt;li&gt;“Strong prioritization” is to sort a list &lt;em&gt;specifically&lt;/em&gt; by importance.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;“Weak prioritization” is something everyone should do: it takes a moment to go over the todo list and drag the tasks into more or less the order in which you will do them. This keeps the most relevant tasks near the top, which is where your eyes naturally go to.&lt;/p&gt;

&lt;p&gt;“Strong prioritization” is a terrible job scheduling algorithm. Importance alone is not good enough.&lt;/p&gt;

&lt;p&gt;Consider the case where you have a very important task A which takes a long time to finish, and a less important task B which takes 5m to finish. For example, writing an essay versus replying to an email. Which should you do first? I would execute B first, because doing so in turn unblocks B’s successor tasks. If you reply to the email and then get to work on task A, the other person has time to read your email and reply to you. And the conversation moves forward while you are otherwise engaged.&lt;/p&gt;

&lt;p&gt;Of course, the pathological version of this is where you only action the quick wins: all the minute little chores get done instantly, but the big tasks, requiring long periods of concentration, get postponed perpetually.&lt;/p&gt;

&lt;p&gt;My task-selection algorithm is basically: do the shortest task first, with two exceptions:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Stalled tasks get a priority bump. If I created a task weeks ago, or if I’ve been postponing in for many days in a row, it has to be done now.&lt;/li&gt;
  &lt;li&gt;Content-dependence: if I’m working on a particular project, I’d rather focus on tasks from that project, rather than from the global todo list.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;visual-field-management&quot;&gt;Visual Field Management&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;To remember something, put it in your visual field. Dually: to forget, get it out of sight.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Out of sight, out of mind. The corollary: to keep something in mind, put it in your visual field; to keep it out, leave it out.&lt;/p&gt;

&lt;p&gt;My desk is very spartan: there’s a monitor, a mouse, and a keyboard, and a few trinkets. My desktop is empty. There are no files in it. The dock has only the apps I use frequently. And at a higher level, I try to keep the apartment very clean and orderly. Because everything that’s out of place is a distraction, visual noise. That’s the negative aspect: the things I remove.&lt;/p&gt;

&lt;p&gt;The positive aspect, the things I keep in my visual field: most of the time, I have two windows open on my computer the todo list occupies the left third of the screen, the right two-thirds are occupied by whatever window I have open at the time, e.g.:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/content/notes-on-managing-adhd/desktop.webp&quot; alt=&quot;A screenshot of my desktop, showing Todoist on the leftmost one-third of the screen, and Emacs on the rightmost two-thirds of the screen.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;And so at a glance, I can see:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;What I’m currently working on.&lt;/li&gt;
  &lt;li&gt;What I will work on next.&lt;/li&gt;
  &lt;li&gt;The list of active projects, so that I don’t forget they exist.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;project-check-ins&quot;&gt;Project Check-Ins&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Keep in regular contact with long-running projects.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A common failure mode I have is, I will fail to finish a project because I forget I even started it. Or, relatedly: I will let a project drag on and on until enough time has passed that my interests have shifted, the sun has set on it, and it is now a slog to finish.&lt;/p&gt;

&lt;p&gt;One reason I do this is that creative/intellectual work often requires (or feels like it requires) long stretches of uninterrupted time. So I procrastinate working on something until I can find such a chunk of time. Which never comes. Time passes and the project begins to slip the moorings of my attention, as other new and shiny things arrive.&lt;/p&gt;

&lt;p&gt;And sometimes I will pick the project back up after months or years, and I have lost so much context, it’s impossible to know what I even intended. And then you procrastinate even more, because you don’t want to feel the guilty of picking up a project and realizing it has become strange and unfamiliar to you.&lt;/p&gt;

&lt;p&gt;One way to combat this is to make regular project checkins. This could be a daily or few-times-a-week recurring task on Todoist that just says “spend 30m on this project”.&lt;/p&gt;

&lt;p&gt;You don’t even have to work on the thing: just allocate fifteen minutes to hold the project in your mind and nothing else. If it’s creative writing, you might open the Word document and just look at it. If it’s a programming project: read the Jira board and look at the code again. Don’t write anything. Just read the code. You will likely come up with a few tasks to do, so write those down. Think. Plan. Build up the structures in your mind, refresh the caches. If you can do, do, otherwise, plan, and if you can’t even do that, read.&lt;/p&gt;

&lt;p&gt;When you’re doing this regularly, when you’re in regular contact with the project, when the shape of it is clear in your mind, you will have the tasks on the top of your mind, you will no longer feel that you need a giant empty runway of time to work on it, you will be able to work on it in shorter chunks.&lt;/p&gt;

&lt;p&gt;To manage long-term creative work, keep in regular contact. That doesn’t mean work on them every day, but maybe &lt;em&gt;look&lt;/em&gt; at them every day.&lt;/p&gt;

&lt;p&gt;The &lt;a href=&quot;#timers&quot;&gt;pomodoro method&lt;/a&gt; works here. Set a timer for just 25m to keep in touch with the project.&lt;/p&gt;

&lt;h2 id=&quot;centralize-your-inboxes&quot;&gt;Centralize Your Inboxes&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Bring all tasks, broadly defined, into one todo list.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Life is full of inboxes:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Email&lt;/li&gt;
  &lt;li&gt;DMs on Twitter, iMessage, WhatsApp, Signal, Discord, etc.&lt;/li&gt;
  &lt;li&gt;Twitter bookmarks&lt;/li&gt;
  &lt;li&gt;Browser bookmarks&lt;/li&gt;
  &lt;li&gt;Your Downloads folder.&lt;/li&gt;
  &lt;li&gt;Messages in my myGov inbox.&lt;/li&gt;
  &lt;li&gt;The physical mailbox in my apartment.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These are inboxes because they fill up over time and need action to empty. You can also think of them as little domain-specific task lists. “Centralizing your inboxes” means moving all these tasks from their silos into the one, central todo list.&lt;/p&gt;

&lt;p&gt;For example, I have a daily task called “catch up” to clear the digital inboxes:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Go through all my communication apps (email, Discord, Twitter DMs etc) and triage the unread conversations: if something needs replying to, I either reply immediately or make a task to reply later so I don’t forget.&lt;/li&gt;
  &lt;li&gt;File the contents of my Downloads folder.&lt;/li&gt;
  &lt;li&gt;Go through Twitter/browser bookmarks and turn them into tasks (e.g., if I bookmark an article, the task is to read the article).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In this way I mostly manage to stay on top of comms.&lt;/p&gt;

&lt;h2 id=&quot;inbox-zero&quot;&gt;Inbox Zero&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;All inboxes should be at zero.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You have probably heard of inbox zero. It sounds like LinkedIn-tier advice. But if you struggle with comms, with replying to people in a timely manner (or at all), inbox zero is a good strategy. There are two reasons, briefly:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Inbox zero has no false negatives: if an inbox is empty, you know you’ve handled everything.&lt;/li&gt;
  &lt;li&gt;Important communications have a way of “camouflaging” themselves among irrelevance.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And, like everything: before you make it into a habit, it feels incredibly time-consuming and labour-intensive. But once you make it into a habit, it’s almost effortless.&lt;/p&gt;

&lt;p&gt;So, I will give you an example. I come in to work, and read four emails. Three could’ve been archived outright, one needed a reply from me. And I said, oh, I’ll get to it in a second. And then I got distracted with other tasks. And throughout the day I kept glancing at the email client, and thinking, yeah, I will get to it. Eventually I got used to those four emails: they are the “new normal”, and what’s normal doesn’t require action. I would think: if those emails are there, and I already looked at them, then it’s probably fine. At the end of the day I looked at the inbox again and saw, wait, no, one of those emails was actually important. That’s the failure mode of inbox greater-than-zero: the important stuff hides among the irrelevant stuff, such that a quick glance at the todo list doesn’t show anything obviously wrong. Dually, with inbox zero, if you see a single email in the inbox, you know there’s work to do.&lt;/p&gt;

&lt;p&gt;Inbox zero removes ambiguity. If there’s &lt;em&gt;anything&lt;/em&gt; in the inbox, you know, unambiguously, you have a task to complete. If there is nothing in the inbox, you know, unambiguously, there is nothing to do. Inbox zero frees you from false negatives, where you think you’ve handled your correspondence but there’s some important email, camouflaged among the trivial ones, that has not been replied to.&lt;/p&gt;

&lt;p&gt;A problem with doing inbox zero is most communication apps (like Discord, Slack, iMessage etc.) don’t have a concept of an inbox, just the read/unread flag on conversations. Since there’s no separation between the inbox and the archive, it takes more discipline to ensure every conversation is replied to.&lt;/p&gt;

&lt;h2 id=&quot;inbox-bankruptcy&quot;&gt;Inbox Bankruptcy&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;If an inbox is overwhelmed, archive it in a recoverable way.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;By the time I started to become organized I’d already accumulated thousands of bookmarks, unread emails, files in my downloads folder, papers in my physical inbox, etc. It would have been a Herculean effort to file these things away. So I didn’t. All the disorganized files, I wrapped them up in a folder and threw them in my &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Attic&lt;/code&gt; folder. Emails? Archived. Bookmarks? Exported to HTML, archived the export, and deleted them from the browser.&lt;/p&gt;

&lt;p&gt;Ideally you should do this once, at the start.&lt;/p&gt;

&lt;p&gt;And by archiving things rather than deleting them, you leave open the possibility that as some point in the future, you might be able to action some of those things. Triage the old bookmarks, sort your filesystem, etc.&lt;/p&gt;

&lt;h2 id=&quot;do-it-on-your-own-terms&quot;&gt;Do It On Your Own Terms&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Bring aversion-causing tasks into an environment that you control.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you’re averse to doing something, for emotional reasons, one way to overcome the aversion is to do it as much as possible on your own terms.&lt;/p&gt;

&lt;p&gt;An example: you have to fill out some government form. You’re averse to it because you worry about making a mistake. And just the thought of opening the form fills you with dread. So, take the boxes in the form, and make a spreadsheet for them. If fonts/colours/emojis/etc. if that makes it feel more personal, or like something you designed and created. Then fill out the form in the spreadsheet. And then copy the values to the form and submit.&lt;/p&gt;

&lt;p&gt;This helps because instead of performing the task in this external domain where you feel threatened, you’re performing the task in your own domain, in your own terms.&lt;/p&gt;

&lt;p&gt;Another example: you have an email you have to reply to, and you’re anxious about it. Just opening the email client gives you a bad feeling. Instead, try composing the email elsewhere, say, in a text editor. The change of environment changes the emotional connotation: you’re not replying to an email, you’re writing a text. You might even think of it as a work of fiction, a pseudoepigraphy.&lt;/p&gt;

&lt;h2 id=&quot;replace-interrupts-with-polling&quot;&gt;Replace Interrupts with Polling&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Turn off notifications, check comms as an explicit task.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;“Interrupts” means notifications, which arrive at unpredictable and often inconvenient times. “Polling” means manually checking the source of the notifications for things to action.&lt;/p&gt;

&lt;p&gt;The obvious benefit of replacing interrupts with polling is you don’t get interrupted by a notification. The less obvious benefit is that when notifications are smeared throughout the day, it is easy for them to fall through the cracks. Something comes in when you’re busy, and you swipe it away, and forget about it, and realize days later you forgot to respond to an important message. Polling is focused: you’ve chosen a block of time, you’re committed to going through the notifications systematically. Instead of random islands of interruptions throughout the day, you have a few short, focused blocks of going through your notifications. Often I get an email while I’m on my phone and think, well, I can’t reply, typing on mobile is horrible, I’m on a train, etc. Polling usually happens at my desk so I have no excuses: I’m in the right environment and in the right mental state.&lt;/p&gt;

&lt;p&gt;This is so trite. “Put your phone on Do Not Disturb and silence notifications”. And yet it works. For a long time I resisted this because I aspire to be the kind of person who gets a message and replies within minutes. But I didn’t notice how much notifications were impairing my focus until one day I accidentally put the phone/desktop on DND and had a wonderfully productive, distraction-free day.&lt;/p&gt;

&lt;h2 id=&quot;accountability-buddy&quot;&gt;Accountability Buddy&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Get someone to sit next to you while you work.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you’re struggling to work on something, work next to another person. Set a timer and tell them what you’re going to accomplish and when the timer ends tell them how you did. Just being around other people can make it easier to overcome aversion. This is why coworking spaces are useful.&lt;/p&gt;

&lt;p&gt;If you don’t have a person around, you might try &lt;a href=&quot;https://www.focusmate.com/&quot;&gt;Focusmate&lt;/a&gt;. It works for &lt;a href=&quot;https://parconley.com/focusmate/&quot;&gt;some people&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Sometimes I’ll start a conversation with Claude, lay out my plans for the day, and update Claude as I do things. If I’m stuck, or if I need help overcoming procrastination, I can ask Claude for help, and it’s easier to do that in an on-going thread because Claude already has the necessary context, so I don’t have to describe what I’m struggling with &lt;em&gt;ab initio&lt;/em&gt;.&lt;/p&gt;

&lt;h2 id=&quot;plan-first-do-later&quot;&gt;Plan First, Do Later&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Separate planning from action, so if you get distracted while acting, you can return to the plans.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Separating planning from doing can be useful. Firstly because planning/doing require different kinds of mental energy. When you’re too tired to do, you can often still plan. Secondly because by separating them you can look back and see how useful the plan was, how much you stuck to it, and then get better at planning.&lt;/p&gt;

&lt;p&gt;Thirdly, and most importantly, because for ADHD people doing can be a source of distractions that impair other tasks. From &lt;a href=&quot;https://www.goodreads.com/book/show/108593.Driven_to_Distraction&quot;&gt;&lt;em&gt;Driven to Distraction&lt;/em&gt;&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;The first item on the list referred to a cough drop. As I read it, I asked her about it.&lt;/p&gt;

  &lt;p&gt;“Oh,” she answered, “that is about a cough drop someone left on the dashboard of our car. The other day I saw the cough drop and thought, I’ll have to throw that away. When I arrived at my first stop, I forgot to take the cough drop to a trash can. When I got back into the car, I saw it and thought, I’ll throw it away at the gas station. The gas station came and went and I hadn’t thrown the cough drop away. Well, the whole day went like that, the cough drop still sitting on the dashboard. When I got home, I thought, I’ll take it inside with me and throw it out. In the time it took me to open the car door, I forgot about the cough drop. It was there to greet me when I got in the car the next morning. […]&lt;/p&gt;

  &lt;p&gt;It was such a classic ADD story that I’ve come to call it the “cough drop sign” when a person &lt;strong&gt;habitually has trouble following through on plans on a minute-to-minute, even second-to-second, basis&lt;/strong&gt;. This is not due to procrastination per se as much as it is due to &lt;strong&gt;the busyness of the moment interrupting or interfering with one’s memory circuits&lt;/strong&gt;. You can get up from your chair, go into the kitchen to get a glass of water, and then in the kitchen forget the reason for your being there.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Emphasis mine.&lt;/p&gt;

&lt;p&gt;When I notice a micro-task like this, my instinct is not to do it, but to put it in the todo list. &lt;em&gt;Then&lt;/em&gt; I try to do it immediately. And if I get distracted halfway through, it’s still there, in the todo list.&lt;/p&gt;

&lt;p&gt;A practical example is something I call the apartment survey. When I clean the apartment, I start by walking around, noticing everything that needs fixing, and creating a little task for it. Even something as simple as “move the book from the coffee table to the bookshelf”. But I don’t start anything until the survey is done. And when the survey is done, I execute it. And if I get distracted halfway through cleaning the apartment, I have the tasks in the list to go back to.&lt;/p&gt;

&lt;h2 id=&quot;derailment&quot;&gt;Derailment&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Introspect to find the things that ruin your productivity and avoid them.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Through &lt;a href=&quot;#introspection&quot;&gt;introspection&lt;/a&gt; you can discover the behaviours that derail your productivity.&lt;/p&gt;

&lt;p&gt;Lifting in the morning derails the day. Cardio is fine, but if I lift weights in the morning, the rest of the day I’m running on -40 IQ points. The most cognitively demanding thing I can do is wash the dishes. I’m not sure what the physiology is: maybe it’s exhaustion of the glycogen stores, or fatigue byproducts floating around in my brain, or the CNS is busy rewiring the motor cortex. The point is that I try to do the cognitively-demanding things in the morning and lift in the evening.&lt;/p&gt;

&lt;p&gt;Motion also does this. I suppose it’s the H in ADHD: hyperactivity. I used to be a big pacer: put on headphones, pace my room back and forth daydreaming for hours and hours. Some days I would pace so much my legs were sore. To think, I have to be in motion. But sometimes I’ve thought enough, and it’s time to do.&lt;/p&gt;

&lt;p&gt;Music, too, derails me. If I start listening to music very soon I start pacing the room and it’s over. Music is almost like reverse methylphenidate: it makes me restless, mentally hyperactive, and inattentive.&lt;/p&gt;

&lt;p&gt;So, to be productive I have to not move too much, and be in silence, and not have fried my brain with exercise.&lt;/p&gt;

&lt;h2 id=&quot;using-neuroticism-to-defeat-adhd&quot;&gt;Using Neuroticism to Defeat ADHD&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;If being organized makes you feel good, spend more on organizing your productivity system.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In a sense, having a really complex productivity system is like trying to use neuroticism to defeat ADHD, to use high neuroticism to defeat low conscientiousness. There’s an element of truth to that, sure (see mastery of drudgery).&lt;/p&gt;

&lt;p&gt;But here’s the thing: you have to play to your strengths. You have to. If you like order and systems and planning but you struggle with doing, then, yeah, it might work, for you, to spend more energy on the trappings of productivity (ensuring your todo list is properly formatted, organized, etc.) if that bleeds over into making it easier to do the real, meaningful things.&lt;/p&gt;

&lt;p&gt;For example: I like emojis in my todo list. The chores have a 🧼 emoji, the comms tasks have an ✉️ emoji. That kind of thing. Makes it easy to see at a glance what kind of things I have to do, to group them by category. But Todoist doesn’t support emoji icons on tasks, unlike Notion, so adding the emojis takes a bit more effort: I have to open &lt;a href=&quot;https://www.raycast.com/&quot;&gt;Raycast&lt;/a&gt; and search for the emoji I want and paste it into the task title. It adds a little friction each time I create a task, but the benefit is I enjoy using the todo list more.&lt;/p&gt;

&lt;h2 id=&quot;the-master-of-drudgery&quot;&gt;The Master of Drudgery&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Avoid spending too much productive time on worthless chores.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A productivity antipattern: indulging too much in “quick wins”.&lt;/p&gt;

&lt;p&gt;There’s this running joke, or meme, online, about the kind of person who has this huge, colossal productivity system, but they get nothing done. They have five todo list apps and everything is categorized and indexed and sorted, but their material output is zero. They complete a hundred tasks a day and when you interrogate what those tasks are they are “brush my teeth” or “reorganize my bookshelf”. There’s a lot of truth to that.&lt;/p&gt;

&lt;p&gt;Every task falls into one of two categories: the quick wins, and everything else. Life is not made of quick wins. Creative, generative, open-ended work requires long periods of focused work. A lot of unpleasant, aversion-causing things have to be done. But the quick wins are infinite: there’s always some micro-chore to do around the house, for example.&lt;/p&gt;

&lt;p&gt;I don’t have advice specifically on avoiding this. But you should notice if you’re doing it and course-correct.&lt;/p&gt;

&lt;h2 id=&quot;thrashing&quot;&gt;Thrashing&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Don’t let procrastiation on one task derail everything else.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A bad failure mode I have is: I have a task &lt;em&gt;T&lt;/em&gt; that I have to do, but I can’t, because of some kind of aversion. But when I try to work on other things, the alarms are going off in my head, telling me to work on &lt;em&gt;T&lt;/em&gt; because you’ve been putting this off for so long and life is finite and the years are short and all that. The end result is that because one thing is blocked, everything grinds to a halt. It’s a very annoying state to be in.&lt;/p&gt;

&lt;p&gt;And I don’t have a perfect solution, but I try to manage it but applying a sense of proportionality, “render unto Caesar” etc. You can’t ignore &lt;em&gt;T&lt;/em&gt; forever, dually, you probably won’t solve it in the next ten minutes. But you can timebox &lt;em&gt;T&lt;/em&gt;: allocate some block of time every day to try to advance it, or at least to work around it, e.g. to ask a friend for help, for example. And the rest of the day you can dedicate to moving other things forward.&lt;/p&gt;

&lt;h2 id=&quot;put-travel-in-the-calendar&quot;&gt;Put Travel in the Calendar&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Calculate travel time ahead of time to avoid being late.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I am chronically late. So if I have a calendar event like a party at someone’s home, I will go on Google Maps and measure the travel time (from my home or wherever I’m likely to be) to the destination, and make a time block for that. e.g., if it takes 30m to go to the dentist and back, this is what my calendar looks like:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/content/notes-on-managing-adhd/travel.webp&quot; alt=&quot;A screenshot of my calendar, showing an event to go to the dentist, bookended by two events to travel to and from the dentist.&quot; width=&quot;300&quot; style=&quot;margin-left: auto; margin-right: auto;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;This ensures I leave my home on time. If it’s something especially important I often add 15m to the travel block as a buffer.&lt;/p&gt;

&lt;h2 id=&quot;choice-of-tools&quot;&gt;Choice of Tools&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Use tools that are effective and you like.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;What productivity app should I use? Reminders? Linear? Todoist? A bullet journal?&lt;/p&gt;

&lt;p&gt;Use something that feels good and works. That’s all. Personally I use Todoist. A lot of people think todo list apps are commodities, but when you have an app open for 98% of your screentime, the little subtleties really add up. I’ve tried using Reminders, Linear, as my todo lists, and building my own. My productivity always suffers and I always go back to Todoist.&lt;/p&gt;

&lt;p&gt;One app is better than two: the more disjoint things you have to pay attention to, the worse it is.&lt;/p&gt;

&lt;p&gt;If you’re a software engineer I strongly advise against building your own, which is a terrible form of procrastination for creative types.&lt;/p&gt;

&lt;h1 id=&quot;resources&quot;&gt;Resources&lt;/h1&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://davidcain.gumroad.com/l/howtodothings&quot;&gt;&lt;em&gt;How To Do Things&lt;/em&gt;&lt;/a&gt; describes an ADHD-friendly version of the Pomodoro method. It’s a 50 page PDF with no fluff, so it’s worth buying to support writers who don’t waste the reader’s time.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Getting_Things_Done&quot;&gt;&lt;em&gt;Getting Things Done&lt;/em&gt;&lt;/a&gt; has a lot of good advice (e.g. dump your entire brain into the todo list) but it’s somewhat neurotypical in that it’s assumed you won’t have any problems actually &lt;em&gt;executing&lt;/em&gt; the tasks.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1 id=&quot;acknowledgements&quot;&gt;Acknowledgements&lt;/h1&gt;

&lt;p&gt;Thanks to Cameron Pinnegar for reviewing.&lt;/p&gt;

</content:encoded>
      </item>
    
      <item>
        <title>Inboxes are Underrated</title>
        <description>On inboxes as application-specific todo lists.</description>
        <pubDate>Sat, 24 May 2025 00:00:00 +0000</pubDate>
        <link>https://borretti.me/article/inboxes-are-underrated</link>
        <guid isPermaLink="true">
          https://borretti.me/article/inboxes-are-underrated
        </guid>
        
        <content:encoded>&lt;p&gt;I have a lot of communication apps. By volume: Twitter DMs, Signal, Whatsapp, iMessage, Discord, email. Because I have so many disjoint places where communication happens, I have a daily task on &lt;a href=&quot;https://www.todoist.com/&quot;&gt;Todoist&lt;/a&gt; to go through each of these, and ensure that every conversation is handled, where “handled” means: if I can reply immediately, I do so; otherwise, I make a task to reply. Polling is better than interrupts.&lt;/p&gt;

&lt;p&gt;But this is imperfect, because often I get distracted, and I do neither. Sometimes I read the other person’s message, and mentally begin drafting a reply, but forget to make a task. Sometimes I check DMs outside of this timeblock, when I’m less disciplined about following the checklist. Sometimes I’m interrupted before I can create the task. And so on. And all of these systems have a concept a conversation being read/unread, but it is fragile: touch it and it goes away. So if I don’t reply immediately, and I don’t make a task, I might never reply. And then new conversations pile up, burying the old ones.&lt;/p&gt;

&lt;p&gt;Email is where I get the least human communication, but it is the one system that has an inbox. And the inbox is invaluable for me, because it acts as a domain-specific todo list: it draws a hard line between the things that have been handled (archived), and the things that are not (inbox). Crossing this line requires an &lt;em&gt;explicit&lt;/em&gt; act.&lt;/p&gt;

&lt;p&gt;With email, I can execute this algorithm:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;For each conversation in the inbox:
    &lt;ul&gt;
      &lt;li&gt;If it’s spam, delete it.&lt;/li&gt;
      &lt;li&gt;If it doesn’t need a reply, archive it.&lt;/li&gt;
      &lt;li&gt;If I can reply immediately, reply and archive the conversation&lt;sup id=&quot;fnref:gtd&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:gtd&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;.&lt;/li&gt;
      &lt;li&gt;If I can’t reply immediately, make a task to reply.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because archiving requires an explicit action, there’s no possibility of forgetting to handle a conversation.&lt;/p&gt;

&lt;p&gt;This is the utility of inbox zero: it has no false negatives! If the inbox is empty, I &lt;em&gt;know&lt;/em&gt; that all of my correspondence has been handled. If the inbox is non-empty, I know there is work to do.&lt;/p&gt;

&lt;p&gt;Why do so few apps have inboxes? Probably because most people never archive their emails, they just keep everything in the inbox. And probably the concept of an inbox reminds them of email, and email feels old and corporate and spammy. Most of the email I get is transactional (e.g. login codes), notifications, and spam.&lt;/p&gt;

&lt;p&gt;For people like me who want to be conscientious about communication, and who need mechanical help to achieve that, the lack of an inbox is really, really frustrating.&lt;/p&gt;

&lt;p&gt;And while inboxes could be entirely local to the client software, the protocol doesn’t have to implement the inbox/archive distinction. But communication protocols are increasingly &lt;a href=&quot;/article/client-freedom&quot;&gt;locked down&lt;/a&gt;, so that you can’t bring your own client, with your own features.&lt;/p&gt;

&lt;p&gt;Tangentially: inbox zero is not an obvious practice at all. Rather than relying on the user to implement the inbox zero workflow, the client should make triaging a first-class workflow. Like spaced repetition: you open &lt;a href=&quot;https://apps.ankiweb.net/&quot;&gt;Anki&lt;/a&gt;, click “Study”, go through the flashcards due today, choosing either “Forgot” or “Remembered”. You open the email client, click “Triage”, and go through one conversation at a time, and choose either “Delete”, “Archive”, “Reply”, or “Skip”.&lt;/p&gt;

&lt;h1 id=&quot;footnotes&quot;&gt;Footnotes&lt;/h1&gt;

&lt;div class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;
  &lt;ol&gt;
    &lt;li id=&quot;fn:gtd&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;Usually I archive a conversation immediately after replying, but sometimes you need a reply from the other person. So I make a task on my todo list that says “Waiting for a reply from X”. The idea is from &lt;a href=&quot;https://en.wikipedia.org/wiki/Getting_Things_Done&quot;&gt;&lt;em&gt;Getting Things Done&lt;/em&gt;&lt;/a&gt;. If the person doesn’t reply, the existence of the task reminds me to ping them again. Otherwise I will certainly forget about it. &lt;a href=&quot;#fnref:gtd&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
  &lt;/ol&gt;
&lt;/div&gt;
</content:encoded>
      </item>
    
      <item>
        <title>You Can Choose Tools That Make You Happy</title>
        <description>Stop falsifying your motivations.</description>
        <pubDate>Tue, 20 May 2025 00:00:00 +0000</pubDate>
        <link>https://borretti.me/article/you-can-choose-tools-that-make-you-happy</link>
        <guid isPermaLink="true">
          https://borretti.me/article/you-can-choose-tools-that-make-you-happy
        </guid>
        
        <content:encoded>&lt;p&gt;On Hacker News and Lobsters I often see blog posts with titles like:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Why I built my startup on Common Lisp and DragonflyBSD&lt;/li&gt;
  &lt;li&gt;Rewriting PyTorch in APL (year six update)&lt;/li&gt;
  &lt;li&gt;I will never, ever, ever learn Docker&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The general form being: why Obscure Thing is better than Popular Thing. And always the justification is purportedly rational and technical. And always, always, it is complete sophistry. Why?&lt;/p&gt;

&lt;p&gt;Because people make technical decisions, in part, for affective reasons. They choose a technology because it feels good, or comfortable, or because it’s what they know. They choose obscure tech as a form of sympathetic magic, like the guy who uses NetBSD on a ThinkPad to feel like a William Gibson protagonist. They choose obsolete languages, like Lisp or Smalltalk, because they think of the heroic age of Xerox PARC, and they want to feel connected to that tradition. They find tools whose vibes align with theirs: Ada says “slow, conservative, baroque” while Rust says “fast-paced, unproven, parvenu”. They use Emacs because they read that Neal Stephenson &lt;a href=&quot;https://web.stanford.edu/class/cs81n/command.txt&quot;&gt;essay&lt;/a&gt; and they feel VS Code is for normies and Emacs is Gnostic.&lt;/p&gt;

&lt;p&gt;But many people can’t admit this to themselves! Because it is contrary to their identity: that they are unfeeling Cartesian rationalist automata. And so they invent rationalizations. Once you read enough of these posts, you see the patterns.&lt;/p&gt;

&lt;p&gt;The arguments for the Obscure Thing downplay the downsides (“yeah I had to take a six-month detour to implement an HTTP server for Fortran 2023”) and invent not-even-wrong upsides. I once read someone argue Common Lisp is great because it has garbage collection, like the writer has some obscure form of &lt;a href=&quot;https://en.wikipedia.org/wiki/Agnosia&quot;&gt;agnosia&lt;/a&gt; where their brain doesn’t register the existence of Python.&lt;/p&gt;

&lt;p&gt;The arguments against the Popular Thing are vague (“Docker is too complex”) or rely on social shaming (“the community is toxic”) or claims about identity (“Rust makes you soft and weak, C++ keeps you on your toes”). And sometimes the arguments are true, but they would not tip the scales of a more dispassionate assessment.&lt;/p&gt;

&lt;p&gt;So let’s cut the knot.&lt;/p&gt;

&lt;p&gt;Emacs is a Gnostic cult. And you know what? That’s fine. In fact, it’s great. It makes you happy, what else is needed? You are allowed to use weird, obscure, inconvenient, obsolescent, undead things if it makes you happy. We are all going to die. If you’re lucky you get three gigaseconds and you’re up. Do what you are called to do. Put ZFS in your air fryer, do your taxes in Fortran.&lt;/p&gt;

&lt;p&gt;We use tools to embody their virtues. You use Tails because it’s cyberpunk? That’s beautiful man. Go all in. Get a leather jacket. If you’re doing it for the aesthetics, go all in. Make your life a living work of art. Go backpacking in Bangkok and write a novel on a &lt;a href=&quot;https://en.wikipedia.org/wiki/Gemini_PDA&quot;&gt;Gemini&lt;/a&gt; and take pictures for your LiveJournal on a 2003 digital camera. Move the family groupchat to Signal. Dial into standup from an ISDN payphone and tell your PM the feds are after you. And write a blog post about that.&lt;/p&gt;

&lt;p&gt;Just don’t bullshit me. Don’t look me in the eye and tell me &lt;a href=&quot;https://en.wikipedia.org/wiki/SNOBOL&quot;&gt;SNOBOL&lt;/a&gt; is the language of the future. Don’t tell your boss it was a rational cost-benefit calculation that made you rewrite the frontend in Prolog.&lt;/p&gt;

&lt;p&gt;Above all, do not lie to yourself. Examine your motivations. If you pursue things out of pure obsession, and ignore reason, you might wake up and realize you’ve spent years labouring in obscurity on a dead-end.&lt;/p&gt;
</content:encoded>
      </item>
    
      <item>
        <title>Two Years of Rust</title>
        <description>Reflections on using Rust professionally for two years.</description>
        <pubDate>Tue, 15 Apr 2025 00:00:00 +0000</pubDate>
        <link>https://borretti.me/article/two-years-of-rust</link>
        <guid isPermaLink="true">
          https://borretti.me/article/two-years-of-rust
        </guid>
        
        <content:encoded>&lt;p&gt;I recently wrapped up a job where I spent the last two years writing the backend of a B2B SaaS product in &lt;a href=&quot;https://rust-lang.org/&quot;&gt;Rust&lt;/a&gt;, so now is the ideal time to reflect on the experience and write about it.&lt;/p&gt;

&lt;h1 class=&quot;no_toc&quot; id=&quot;contents&quot;&gt;Contents&lt;/h1&gt;

&lt;ol id=&quot;markdown-toc&quot;&gt;
  &lt;li&gt;&lt;a href=&quot;#learning&quot; id=&quot;markdown-toc-learning&quot;&gt;Learning&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#good&quot; id=&quot;markdown-toc-good&quot;&gt;The Good&lt;/a&gt;    &lt;ol&gt;
      &lt;li&gt;&lt;a href=&quot;#perf&quot; id=&quot;markdown-toc-perf&quot;&gt;Performance&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#tooling&quot; id=&quot;markdown-toc-tooling&quot;&gt;Tooling&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#types&quot; id=&quot;markdown-toc-types&quot;&gt;Type Safety&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#error&quot; id=&quot;markdown-toc-error&quot;&gt;Error Handling&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#borrow&quot; id=&quot;markdown-toc-borrow&quot;&gt;The Borrow Checker&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#async&quot; id=&quot;markdown-toc-async&quot;&gt;Async&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#refactoring&quot; id=&quot;markdown-toc-refactoring&quot;&gt;Refactoring&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#hiring&quot; id=&quot;markdown-toc-hiring&quot;&gt;Hiring&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#affect&quot; id=&quot;markdown-toc-affect&quot;&gt;Affect&lt;/a&gt;&lt;/li&gt;
    &lt;/ol&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#bad&quot; id=&quot;markdown-toc-bad&quot;&gt;The Bad&lt;/a&gt;    &lt;ol&gt;
      &lt;li&gt;&lt;a href=&quot;#modules&quot; id=&quot;markdown-toc-modules&quot;&gt;The Module System&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#build-perf&quot; id=&quot;markdown-toc-build-perf&quot;&gt;Build Performance&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#mock&quot; id=&quot;markdown-toc-mock&quot;&gt;Mocking&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#power&quot; id=&quot;markdown-toc-power&quot;&gt;Expressive Power&lt;/a&gt;&lt;/li&gt;
    &lt;/ol&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;h1 id=&quot;learning&quot;&gt;Learning&lt;/h1&gt;

&lt;p&gt;I didn’t learn Rust the usual way: by reading tutorials, or books; or writing tiny projects. Rather, I would say that I studied Rust, as part of the research that went into building &lt;a href=&quot;https://github.com/austral/austral&quot;&gt;Austral&lt;/a&gt;. I would read papers about Rust, and the specification, and sometimes I’d go on the &lt;a href=&quot;https://play.rust-lang.org/&quot;&gt;Rust playground&lt;/a&gt; and write a tiny program to understand how the borrow checker works on a specific edge case.&lt;/p&gt;

&lt;p&gt;So, when I started working in Rust, my knowledge was very lopsided: I had an encyclopedic knowledge of the minutiae of the borrow checker, and couldn’t have told you how to write “Hello, world!”. The largest Rust program I had written was maybe 60 lines of code and it was to empirically test how trait resolution works.&lt;/p&gt;

&lt;p&gt;This turned out fine. Within a day or two I was committing changes. The problem is when people ask me for resources to learn Rust, I draw a blank.&lt;/p&gt;

&lt;h1 id=&quot;good&quot;&gt;The Good&lt;/h1&gt;

&lt;p&gt;The way I would summarize Rust is: it’s a better Go, or a faster Python. It’s fast and statically-typed, it has SOTA tooling, and a great ecosystem. It’s not hard to learn. It’s an industrial language, not an academic language, and you can be immensely productive with it. It’s a general-purpose language, so you can build &lt;a href=&quot;https://github.com/tokio-rs/axum&quot;&gt;backends&lt;/a&gt;, &lt;a href=&quot;https://docs.rs/clap/latest/clap/&quot;&gt;CLIs&lt;/a&gt;, &lt;a href=&quot;https://github.com/ratatui/ratatui&quot;&gt;TUIs&lt;/a&gt;, &lt;a href=&quot;https://gtk-rs.org/&quot;&gt;GUIs&lt;/a&gt;, and embedded firmware. The two areas where it’s not yet a good fit are web frontends (though you can try) and native macOS apps.&lt;/p&gt;

&lt;h2 id=&quot;perf&quot;&gt;Performance&lt;/h2&gt;

&lt;p&gt;Rust is fast.&lt;/p&gt;

&lt;p&gt;You can write slow code in any language: quadratic loops and n+1 queries and bad cache usage. But these are &lt;em&gt;discrete&lt;/em&gt; bottlenecks. In Rust, when you fix the bottlenecks, the program is fast.&lt;/p&gt;

&lt;p&gt;In other languages performance problems are often &lt;em&gt;pervasive&lt;/em&gt;, so e.g. in Python it’s very common to have a situation where you’ve fixed all the bottlenecks—and everything is still unacceptably slow. Why? Because in Python the primitives are 10x to 100x slower than in Rust, and the composition of slow primitives is a slow program. No matter how much you optimize &lt;em&gt;within&lt;/em&gt; the program, the performance ceiling is set by the language itself.&lt;/p&gt;

&lt;p&gt;And when you find yourself in that situation, what is there to do? You can scale the hardware vertically, and end up like those people who spend five figures a month on AWS to get four requests per second. You can keep your dependencies up to date, and hope that the community is doing the work of improving performance. And you can use async as much as possible on the belief that your code is I/O-bound, and be disappointed when it turns out that actually you’re CPU-bound.&lt;/p&gt;

&lt;p&gt;By having a high performance ceiling, Rust lets you write programs that are default fast without thinking too much about optimization, and when you need to improve performance, you have a lot of room to optimize before you hit the performance ceiling.&lt;/p&gt;

&lt;h2 id=&quot;tooling&quot;&gt;Tooling&lt;/h2&gt;

&lt;p&gt;&lt;a href=&quot;https://doc.rust-lang.org/cargo/&quot;&gt;Cargo&lt;/a&gt; has the best DX of any build system+package manager I have used. Typically you praise the features of a program, with cargo you praise the absences: there’s no gotchas, no footguns, no lore you have to learn in anger, no weirdness, no environment variables to configure, no virtualenvs to forget to activate. When you copy a command from the documentation and run it, it works, it doesn’t spit out a useless error message that serves only as a unique identifier to find the relevant StackOverflow/Discourse thread.&lt;/p&gt;

&lt;p&gt;Much of the DX virtues are downstream of the fact that cargo is entirely declarative rather than stateful. An example: something that always trips me up with npm is when I update the dependencies in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;package.json&lt;/code&gt;, running the type-checker/build tool/whatever doesn’t pick up the change. I get an unexpected error and then I go, oh, right, I have to run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm install&lt;/code&gt; first. With cargo, if you update the dependencies in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Cargo.toml&lt;/code&gt; file, any subsequent command (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cargo check&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;build&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;run&lt;/code&gt;) will first resolve the dependencies, update &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Cargo.lock&lt;/code&gt;, download any missing dependencies, and &lt;em&gt;then&lt;/em&gt; run the command. The state of (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Cargo.toml&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Cargo.lock&lt;/code&gt;, local dependency store) is always synchronized.&lt;/p&gt;

&lt;h2 id=&quot;types&quot;&gt;Type Safety&lt;/h2&gt;

&lt;p&gt;Rust has a good type system: sum types with exhaustiveness checking, option types instead of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;null&lt;/code&gt;, no surprising type conversions. Again, as with tooling, what makes a type system good is a small number of features, and a thousand absences, mistakes that were not made.&lt;/p&gt;

&lt;p&gt;The practical consequence is you have a high degree of confidence in the robustness of your code. In e.g. Python the state of nature is you have zero confidence that the code won’t blow up in your face, so you spend your time writing tests (to compensate for the lack of a type system) and waiting for the tests to clear CI (because Python is slow as shit). In Rust you write the code and if it compiles, it almost always works. Writing tests can feel like a chore because of how rarely they surface defects.&lt;/p&gt;

&lt;p&gt;To give an example: I don’t really know how to debug Rust programs because I never had to. The only parts of the code I had to debug were the SQL queries, because SQL &lt;a href=&quot;/article/composable-sql&quot;&gt;has many deficiencies&lt;/a&gt;. But the Rust code itself was overwhelmingly solid. When there were bugs, they were usually conceptual bugs, i.e., misunderstanding the specification. The type of bugs that you can make in any language and that testing would miss.&lt;/p&gt;

&lt;h2 id=&quot;error&quot;&gt;Error Handling&lt;/h2&gt;

&lt;p&gt;There’s two ways to do errors: traditional exception handling (as in Java or Python) keeps the happy path free of error-handling code, but makes it hard to know the set of errors that can be raised at a given program point. Errors-as-values, as in Go, makes error handling more explicit at the cost of being very verbose.&lt;/p&gt;

&lt;p&gt;Rust has a really nice solution where errors are represented as ordinary values, but there’s syntactic sugar that means you don’t have to slow down to write &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;if err != nil&lt;/code&gt; a thousand times over.&lt;/p&gt;

&lt;p&gt;In Rust, an error is any type that implements the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Error&lt;/code&gt; trait. Then you have the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Result&lt;/code&gt; type:&lt;/p&gt;

&lt;div class=&quot;language-rust highlighter-rouge&quot;&gt;
  &lt;div class=&quot;highlight&quot;&gt;
    &lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;enum&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Result&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;E&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Error&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nf&quot;&gt;Ok&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;nf&quot;&gt;Err&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;E&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;Functions which are fallible simply return a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Result&lt;/code&gt;, e.g.:&lt;/p&gt;

&lt;div class=&quot;language-rust highlighter-rouge&quot;&gt;
  &lt;div class=&quot;highlight&quot;&gt;
    &lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;enum&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;DbError&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;InvalidPath&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;Timeout&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// ...&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;open_database&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Result&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Database&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;DbError&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;The question mark operator, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;?&lt;/code&gt;, makes it possible to write terse code that deals with errors. Code like this:&lt;/p&gt;

&lt;div class=&quot;language-rust highlighter-rouge&quot;&gt;
  &lt;div class=&quot;highlight&quot;&gt;
    &lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;foo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Result&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;DbError&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;db&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;open_database&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tx&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;begin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;db&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;data&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;...&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;nf&quot;&gt;rollback&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;nf&quot;&gt;Ok&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(())&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;Is transformed to the much more verbose:&lt;/p&gt;

&lt;div class=&quot;language-rust highlighter-rouge&quot;&gt;
  &lt;div class=&quot;highlight&quot;&gt;
    &lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;foo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Result&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;DbError&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;db&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;match&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;open_database&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nf&quot;&gt;Ok&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;db&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;db&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;nf&quot;&gt;Err&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;c1&quot;&gt;// Rethrow.&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;Err&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tx&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;match&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;begin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;db&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nf&quot;&gt;Ok&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;nf&quot;&gt;Err&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;Err&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;data&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;match&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;...&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nf&quot;&gt;Ok&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;nf&quot;&gt;Err&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;Err&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;match&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;rollback&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nf&quot;&gt;Ok&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt;
        &lt;span class=&quot;nf&quot;&gt;Err&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;Err&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
    &lt;span class=&quot;nf&quot;&gt;Ok&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(())&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;When you need to explicitly handle an error, you omit the question mark operator and use the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Result&lt;/code&gt; value directly.&lt;/p&gt;

&lt;h2 id=&quot;borrow&quot;&gt;The Borrow Checker&lt;/h2&gt;

&lt;p&gt;The borrow checker is Rust’s headline feature: it’s how you can have memory safety without garbage collection, it’s the thing that enables “fearless concurrency”. It’s also, for most people, the most frustrating part of learning and using Rust.&lt;/p&gt;

&lt;p&gt;Personally I didn’t have borrow checker problems, but that’s because before I started using Rust at work I’d &lt;a href=&quot;/article/how-australs-linear-type-checker-works&quot;&gt;designed and built my own borrow checker&lt;/a&gt;. I don’t know if that’s a scalable pedagogy. Many people report they have to go through a lengthy period of fighting the borrow checker, and slowly their brain discovers the implicit ruleset, and eventually they reach a point where they can write code without triggering inscrutable borrow checker errors. But that means a lot of people drop out of learning Rust because they don’t like fighting the borrow checker.&lt;/p&gt;

&lt;p&gt;So, how do you learn Rust more effectively, without building your own compiler, or banging your head against the borrow checker?&lt;/p&gt;

&lt;p&gt;Firstly, it’s useful to understand the concepts behind the borrow checker, the “aliased XOR mutable” rule, the motivation behind linear types, etc. Unfortunately I don’t have a canonical resource that explains it &lt;em&gt;ab initio&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Secondly, a change in mindset is useful: a lot of people’s mental model of the borrow checker is as something bolted “on top” of Rust, like a static analyzer you can run on a C/C++ codebase, which just happens to be built into the compiler. This mindset leads to fighting the system, because you think: my code is legitimate, it type-checks, all the types are there, it’s only this final layer, the borrow checker, that objects. It’s better to think of the borrow checker as an intrinsic part of the language semantics. Borrow checking happens, necessarily, after type-checking (because it needs to know the types of terms), but a program that fails the borrow checker is as invalid as a program that doesn’t type-check. Rather than mentally implementing something in C/C++, and then thinking, “how do I translate this to Rust in a way that satisfies the borrow-checker?”, it’s better to think, “how can I accomplish the goal within the semantics of Rust, thinking in terms of linearity and lifetimes?”. But that’s hard, because it requires a high level of fluency.&lt;/p&gt;

&lt;p&gt;When you are comfortable with the borrow checker, life is pretty good. “Fighting the borrow checker” isn’t something that happens. When the borrow checker complains it’s either because you’re doing something where multiple orthogonal features impinge on each other (e.g. async + closures + borrowing) or because you’re doing something that’s too complex, and the errors are a signal you have to simplify. Often, the borrow checker steers you towards designs that have mechanical sympathy, that are aligned with how the hardware works. When you converge on a design that leverages lifetimes to have a completely &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;clone()&lt;/code&gt;-free flow of data, it is really satisfying. When you design a linearly-typed API where the linearity makes it really hard to misuse, you’re grateful for the borrow checker.&lt;/p&gt;

&lt;h2 id=&quot;async&quot;&gt;Async&lt;/h2&gt;

&lt;p&gt;Everyone complains about async. They complain that it’s too complex or they invoke that thought-terminating cliche about “coloured functions”. It’s easy to complain about something when comparing it to some vague, abstract, ideal state of affairs; but what, exactly, is the concrete and existing alternative to async?&lt;/p&gt;

&lt;p&gt;The binding constraint is that OS threads are slow. Not accidentally but intrinsically, because of the kernel, and having to swap the CPU state and stack on each context switch. OS threads are never going to be fast. If you want to build high-performance network services, it matters a lot how many concurrent connections and how much throughput you can get per CPU.  So you need an alternative way to do concurrency that lets you maximize your hardware resources.&lt;/p&gt;

&lt;p&gt;And there are basically two alternatives.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Green threads, which give programmers the same semantics as OS threads (good!) but often leave a lot of performance on the table (bad!) because you need to allocate memory for each thread’s stack and you need a runtime scheduler to do preemptive multitasking.&lt;/li&gt;
  &lt;li&gt;Stackless coroutines, as in Rust, which add complexity to the language semantics and implementation (bad!) but have a high performance ceiling (good!).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;From the perspective of a language implementor, or someone who cares about specifying the semantics of programming languages, async is not a trivial feature. The intersection of async and lifetimes is hard to understand. From the perspective of a library implementor, someone who writes the building blocks of services and is down in the trenches with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Pin&lt;/code&gt;/&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Poll&lt;/code&gt;/&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Future&lt;/code&gt;, it’s rough.&lt;/p&gt;

&lt;p&gt;But from the perspective of a user, async Rust is pretty good. It mostly “just works”. The user perspective is you put &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;async&lt;/code&gt; in front of function definitions that perform IO and you put &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;await&lt;/code&gt; at the call sites and that’s it. The only major area where things are unergonomic is calling async functions inside iterators.&lt;/p&gt;

&lt;h2 id=&quot;refactoring&quot;&gt;Refactoring&lt;/h2&gt;

&lt;p&gt;It’s paint by numbers. The type errors make refactoring extremely straightforward and safe.&lt;/p&gt;

&lt;h2 id=&quot;hiring&quot;&gt;Hiring&lt;/h2&gt;

&lt;p&gt;Is it hard to hire Rust programmers? No.&lt;/p&gt;

&lt;p&gt;First, mainstream languages like Python and TypeScript are so easy to hire for that they wrap back around and become hard. To find a truly talented Python programmer you have to sift through a thousand resumes.&lt;/p&gt;

&lt;p&gt;Secondly, there’s a selection effect for quality. “Has used Rust”, “has written open-source code in Rust”, or “wants to use Rust professionally” are huge positive signals about a candidate because it says they are curious and they care about improving their skills.&lt;/p&gt;

&lt;p&gt;Personally I’ve never identified as a “Python programmer” or a “Rust programmer”. I’m just a programmer! When you learn enough languages you can form an orthogonal basis set of programming concept and translate them across languages. And I think the same is true for the really talented programmers: they are able to learn the language quickly.&lt;/p&gt;

&lt;h2 id=&quot;affect&quot;&gt;Affect&lt;/h2&gt;

&lt;p&gt;Enough about tech. Let’s talk about feelings.&lt;/p&gt;

&lt;p&gt;When I worked with Python+Django the characteristic feeling was &lt;em&gt;anxiety&lt;/em&gt;. Writing Python feels like building a castle out of twigs, and the higher you go, the stronger the wind gets. I expected things to go wrong, I expected the code to be slow, I expected to watch things blow up for the most absurd reasons. I had to write the code defensively, putting type assertions everywhere.&lt;/p&gt;

&lt;p&gt;Rust feels good. You can build with confidence. You can build things that not only work as desired but which are also &lt;em&gt;beautiful&lt;/em&gt;. You can be proud of the work that you do, because it’s not slop.&lt;/p&gt;

&lt;h1 id=&quot;bad&quot;&gt;The Bad&lt;/h1&gt;

&lt;p&gt;This section describes the things I don’t like.&lt;/p&gt;

&lt;h2 id=&quot;modules&quot;&gt;The Module System&lt;/h2&gt;

&lt;p&gt;In Rust, there’s two levels of code organization:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Modules&lt;/strong&gt; are namespaces with visibility rules.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Crates&lt;/strong&gt; are a collection of modules, and they can depend on other crates. Crates can be either executables or libraries.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A project, or workspace, can be made up of multiple crates. For example a web application could have library crates for each orthogonal feature and an executable crate that ties them together and starts the server.&lt;/p&gt;

&lt;p&gt;What surprised me was learning that modules are not compilation units, and I learnt this by accident when I noticed you can have a circular dependency between modules within the same crate&lt;sup id=&quot;fnref:reg&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:reg&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;. Instead, crates are the compilation unit. When you change any module in a crate, the &lt;em&gt;entire&lt;/em&gt; crate has to be recompiled. This means that compiling large crates is slow, and large projects should be broken down into many small crates, with their dependency DAG arranged to maximize parallel compilation.&lt;/p&gt;

&lt;p&gt;This is a problem because creating a module is cheap, but creating a crate is slow. Creating a new module is just creating a new file and adding an entry for it in the sibling &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mod.rs&lt;/code&gt; file. Creating a new crate requires running &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cargo new&lt;/code&gt;, and don’t forget to set &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;publish = false&lt;/code&gt; in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Cargo.toml&lt;/code&gt;, and adding the name of that crate in the workspace-wide &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Cargo.toml&lt;/code&gt; so you can import it from other crates. Importing a symbol within a crate is easy: you start typing the name, and the LSP can auto-insert the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;use&lt;/code&gt; declaration, but this doesn’t work across crates, you have to manually open the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Cargo.toml&lt;/code&gt; file for the crate you’re working on and manually add a dependency to the crate you want to import code from. This is very time-consuming.&lt;/p&gt;

&lt;p&gt;Another problem with crate-splitting is that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rustc&lt;/code&gt; has a really nice feature that warns you when code is unused. It’s very thorough and I like it because it helps to keep the codebase tidy. But it only works within a crate. In a multi-crate workspace, declarations that are exported publicly in a crate, but not imported by any other sibling crates, are not reported as unused.&lt;sup id=&quot;fnref:mach&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:mach&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;So if you want builds to be fast, you have to completely re-arrange your architecture and manually massage the dependency DAG and also do all this make-work around creating and updating crate metadata. And for that you gain… intra-crate circular imports, which are a horrible antipattern and make it much harder to understand the codebase. I would much prefer if modules were disjoint compilation units.&lt;/p&gt;

&lt;p&gt;I also think the module system is just a hair too complex, with re-exports and way too many ways to import symbols. It could be stripped down a lot.&lt;/p&gt;

&lt;h2 id=&quot;build-perf&quot;&gt;Build Performance&lt;/h2&gt;

&lt;p&gt;The worst thing about the Rust experience is the build times. This is usually blamed on &lt;a href=&quot;https://llvm.org/&quot;&gt;LLVM&lt;/a&gt;, which, fair enough, but I think part of it is just intrinsic features of the language, like the fact that modules are not independent compilation units, and of course monomorphization.&lt;/p&gt;

&lt;p&gt;There are various tricks to speed up the builds: &lt;a href=&quot;https://github.com/Swatinem/rust-cache&quot;&gt;caching&lt;/a&gt;, &lt;a href=&quot;https://github.com/LukeMathWalker/cargo-chef&quot;&gt;cargo chef&lt;/a&gt;, &lt;a href=&quot;https://matklad.github.io/2021/09/04/fast-rust-builds.html&quot;&gt;tweaking the configuration&lt;/a&gt;. But these are tricks, and tricks are fragile. When you notice a build performance regression, it could be for any number of reasons:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;The code is genuinely larger, and takes longer to build.&lt;/li&gt;
  &lt;li&gt;You’re using language features that slow down the frontend (e.g. complex type-level code).&lt;/li&gt;
  &lt;li&gt;You’re using language features that slow down the backend (e.g. excessive monomorphization).&lt;/li&gt;
  &lt;li&gt;A proc macro is taking a very long time (&lt;a href=&quot;https://docs.rs/tracing/latest/tracing/attr.instrument.html&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tracing::instrument&lt;/code&gt;&lt;/a&gt; in particular is fantastically slow).&lt;/li&gt;
  &lt;li&gt;The crate DAG has changed shape, and crates that used to be built in parallel are now being built serially.&lt;/li&gt;
  &lt;li&gt;Any of the above, but in the transitive closure of your dependencies.&lt;/li&gt;
  &lt;li&gt;You’ve added/updated an immediate dependency, which pulls in lots of transitive dependencies.&lt;/li&gt;
  &lt;li&gt;You’re caching too little, causing dependencies to be downloaded.&lt;/li&gt;
  &lt;li&gt;You’re caching &lt;em&gt;too much&lt;/em&gt;, bloating the cache, which takes longer to download.&lt;/li&gt;
  &lt;li&gt;The cache was recently invalidated (e.g. by updating &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Cargo.lock&lt;/code&gt;) and has not settled yet.&lt;/li&gt;
  &lt;li&gt;The CI runners are slow today, for reasons unknowable.&lt;/li&gt;
  &lt;li&gt;The powerset of all of the above.&lt;/li&gt;
  &lt;li&gt;(Insert Russell’s paradox joke)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It’s not worth figuring out. Just pay for the bigger CI runners. Four or eight cores should be enough. Too much parallelism is waste: run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cargo build&lt;/code&gt; with the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--timings&lt;/code&gt; flag, open the report in your browser, and look at the value of “Max concurrency”. This tells you how many crates can be built in parallel, and, therefore, how many cores you can buy before you hit diminishing returns.&lt;/p&gt;

&lt;p&gt;The main thing you can do to improve build performance is to split your workspace into multiple crates, and arranging the crate dependencies such that as much of your workspace can be built in parallel. This is easy to do at the start of a project, and very time-consuming after.&lt;/p&gt;

&lt;h2 id=&quot;mock&quot;&gt;Mocking&lt;/h2&gt;

&lt;p&gt;Maybe this is a skill issue, but I have not found a good way to write code where components have swappable dependencies and can be tested independently of their dependencies. The central issue is that lifetimes impinge on late binding.&lt;/p&gt;

&lt;p&gt;Consider a workflow for creating a new user in a web application. The three external effects are: creating a record for the user in the database, sending them a verification email, and logging the event in an audit log:&lt;/p&gt;

&lt;div class=&quot;language-rust highlighter-rouge&quot;&gt;
  &lt;div class=&quot;highlight&quot;&gt;
    &lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;create_user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;tx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Transaction&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Password&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Result&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;CustomError&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;  &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nf&quot;&gt;insert_user_record&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;nf&quot;&gt;send_verification_email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;nf&quot;&gt;log_user_created_event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;nf&quot;&gt;Ok&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(())&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;Testing this function requires spinning up a database and an email server. No good! We want to detach the workflow from its dependencies, so we can test it without transitively testing its dependencies. There’s three ways to do this:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Use traits to define the interface, and pass things at compile-time.&lt;/li&gt;
  &lt;li&gt;Use traits to define the interface, and use dynamic dispatch to pass things at run-time.&lt;/li&gt;
  &lt;li&gt;Use function types to define the interface, and pass dependencies as closures.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And all of these approaches work. But they require a lot of make-work. In TypeScript or Java or Python it would be painless, because those languages don’t have lifetimes, and so dynamic dispatch or closures “just work”.&lt;/p&gt;

&lt;p&gt;For example, say we’re using traits and doing everything at compile-time. To minimize the work let’s just focus on the dependency that writes the user’s email and password to the database. We can define a trait for it:&lt;/p&gt;

&lt;div class=&quot;language-rust highlighter-rouge&quot;&gt;
  &lt;div class=&quot;highlight&quot;&gt;
    &lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;trait&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;InsertUser&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;execute&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;mut&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;tx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Password&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Result&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;CustomError&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;(We’ve parameterized the type of database transactions because the mock won’t use a real database, therefore, we won’t have a way to construct a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Transaction&lt;/code&gt; type in the tests.)&lt;/p&gt;

&lt;p&gt;The real implementation requires defining a placeholder type, and implementing the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;InsertUser&lt;/code&gt; trait for it:&lt;/p&gt;

&lt;div class=&quot;language-rust highlighter-rouge&quot;&gt;
  &lt;div class=&quot;highlight&quot;&gt;
    &lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;InsertUserAdapter&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{}&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;impl&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;InsertUser&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Transaction&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;InsertUserAdapter&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;execute&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;mut&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;tx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Transaction&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Password&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Result&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;CustomError&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nf&quot;&gt;insert_user_record&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;nf&quot;&gt;Ok&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(())&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;The mock implementation uses the unit type &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;()&lt;/code&gt; as the type of transactions:&lt;/p&gt;

&lt;div class=&quot;language-rust highlighter-rouge&quot;&gt;
  &lt;div class=&quot;highlight&quot;&gt;
    &lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;InsertUserMock&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Password&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;impl&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;InsertUser&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;InsertUserMock&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;execute&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;mut&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;tx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Password&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Result&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;CustomError&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// Store the email and password in the mock object, so&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// we can afterwards assert the right values were passed&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// in.&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;py&quot;&gt;.email&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;.clone&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;py&quot;&gt;.password&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;.clone&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
        &lt;span class=&quot;nf&quot;&gt;Ok&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(())&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;Finally we can define the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;create_user&lt;/code&gt; workflow like this:&lt;/p&gt;

&lt;div class=&quot;language-rust highlighter-rouge&quot;&gt;
  &lt;div class=&quot;highlight&quot;&gt;
    &lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;create_user&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;I&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;InsertUser&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;tx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;insert_user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;mut&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;I&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Password&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Result&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;CustomError&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;insert_user&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;.execute&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// Todo: the rest of the dependencies.&lt;/span&gt;
    &lt;span class=&quot;nf&quot;&gt;Ok&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(())&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;The live, production implementation would look like this:&lt;/p&gt;

&lt;div class=&quot;language-rust highlighter-rouge&quot;&gt;
  &lt;div class=&quot;highlight&quot;&gt;
    &lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;create_user_for_real&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;tx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Transaction&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Password&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Result&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;CustomError&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;mut&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;insert_user&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;InsertUserAdapter&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{};&lt;/span&gt;
    &lt;span class=&quot;nf&quot;&gt;create_user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;mut&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;insert_user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;nf&quot;&gt;Ok&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(())&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;While in the unit tests we would instead create a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;InsertUserMock&lt;/code&gt; and pass it in:&lt;/p&gt;

&lt;div class=&quot;language-rust highlighter-rouge&quot;&gt;
  &lt;div class=&quot;highlight&quot;&gt;
    &lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;#[test]&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;test_create_user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Result&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;CustomError&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;mut&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;insert_user&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;InsertUserMock&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;.to_string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;.to_string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;email&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;foo@example.com&quot;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;.to_string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();;&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;password&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;hunter2&quot;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;.to_string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;

    &lt;span class=&quot;nf&quot;&gt;create_user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;mut&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;insert_user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// Assert `insert_user` was called with the right values.&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;assert_eq!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;insert_user&lt;/span&gt;&lt;span class=&quot;py&quot;&gt;.email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;foo@example.com&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;assert_eq!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;insert_user&lt;/span&gt;&lt;span class=&quot;py&quot;&gt;.password&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;hunter2&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

    &lt;span class=&quot;nf&quot;&gt;Ok&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(())&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;Obviously this is a lot of typing. Using traits and dynamic dispatch would probably make the code marginally shorter. Using closures is probably the simplest approach (a function type with type parameters is, in a sense, a trait with a single method), but then you run into the ergonomics issues of closures and lifetimes.&lt;/p&gt;

&lt;p&gt;Again, this might be a skill issue, and maybe there’s an elegant and idiomatic way to do this.&lt;/p&gt;

&lt;p&gt;Alternatively, you might deny the entire necessity of mocking, and write code without swappable implementations, but that has its own problems: tests become slower, because you have to spin up servers to mock things like API calls; tests require a lot of code to set up and tear down these dependencies; tests are necessarily end-to-end, and the more end-to-end your tests, the more test cases you need to check every path because of the combinatorial explosion of inputs.&lt;/p&gt;

&lt;h2 id=&quot;power&quot;&gt;Expressive Power&lt;/h2&gt;

&lt;p&gt;It’s easy to go insane with proc macros and trait magic and build an incomprehensible codebase where it’s impossible to follow the flow of control or debug anything. You have to rein it in.&lt;/p&gt;

&lt;h1 class=&quot;no_toc&quot; id=&quot;footnotes&quot;&gt;Footnotes&lt;/h1&gt;

&lt;div class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;
  &lt;ol&gt;
    &lt;li id=&quot;fn:reg&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;If modules were separate compilation units this wouldn’t work. If module A depends on B, to compile A you need to first compile B to know what declarations it exports and what their types are. But if B also depends on A, you have an infinite regression. &lt;a href=&quot;#fnref:reg&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:mach&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;One way to fix this is to make extremely fine-grained crates, and rely on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cargo-machete&lt;/code&gt; to identify unused code at the dependency level. But this would take up way too much time. &lt;a href=&quot;#fnref:mach&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
  &lt;/ol&gt;
&lt;/div&gt;
</content:encoded>
      </item>
    
      <item>
        <title>My Backup Infrastructure, 2025 Edition</title>
        <description>How I back up my personal data.</description>
        <pubDate>Sun, 13 Apr 2025 00:00:00 +0000</pubDate>
        <link>https://borretti.me/article/my-backup-infrastructure-2025-edition</link>
        <guid isPermaLink="true">
          https://borretti.me/article/my-backup-infrastructure-2025-edition
        </guid>
        
        <content:encoded>&lt;p&gt;tl;dr two portable SSDs, synced with &lt;a href=&quot;https://en.wikipedia.org/wiki/Rsync&quot;&gt;rsync&lt;/a&gt;; and a &lt;a href=&quot;https://www.backblaze.com/&quot;&gt;Backblaze&lt;/a&gt; bucket synced with &lt;a href=&quot;https://restic.net/&quot;&gt;restic&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/content/my-backup-infrastructure-2025-edition/infra.svg&quot; alt=&quot;A diagram of my backup infrastructure. In a box labeled &apos;local&apos;, my laptop, &apos;metauro&apos;, and the two SSDs, &apos;chiron&apos; and &apos;nessus&apos;, backed up with rsync. In a box labeled &apos;remote&apos;, the Backblaze bucket named &apos;pholus&apos;, backed up with restic.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;I’m finally satisfied with my infrastructure for backups, so I’m writing it up so others can benefit from it.&lt;/p&gt;

&lt;h1 id=&quot;criteria&quot;&gt;Criteria&lt;/h1&gt;

&lt;p&gt;My requirements for backup infrastructure are:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Open source, to minimize the risk of backdoors.&lt;/li&gt;
  &lt;li&gt;Fast, but only incrementally: an initial snapshot can be slow.&lt;/li&gt;
  &lt;li&gt;Simple configuration, with little surface area to mess things up.&lt;/li&gt;
  &lt;li&gt;Encryption with keys that I control and which never leave my device. Ideally, encryption should be mandatory, to prevent accidentally putting cleartext on backup media.&lt;/li&gt;
  &lt;li&gt;Has to satisfy the &lt;a href=&quot;https://www.hanselman.com/blog/the-computer-backup-rule-of-three&quot;&gt;3-2-1 rule&lt;/a&gt;: at least three disjoint copies, in two different media, at least one off-site.&lt;/li&gt;
  &lt;li&gt;There has to be a known (documented, memorable) path to recovery. It would be embarrassing if you went to restore your backups and suddenly realized there’s a missing link that prevents you from e.g. recovering the encryption key.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The one non-criterion is portability. Because I only use macOS, I don’t need a solution where I can restore the backups from different operating systems.&lt;/p&gt;

&lt;h1 id=&quot;local-backups&quot;&gt;Local Backups&lt;/h1&gt;

&lt;p&gt;I have two portable SSDs, Chiron and Nessus, with encrypted &lt;a href=&quot;https://en.wikipedia.org/wiki/Apple_File_System&quot;&gt;APFS&lt;/a&gt;. The filesystem itself being encrypted is extremely convenient: I just plug them in, and the &lt;a href=&quot;https://en.wikipedia.org/wiki/Keychain_(software)&quot;&gt;macOS keychain&lt;/a&gt; has the keys. There’s no possibility of accidentally leaking cleartext into the disk because the encryption is transparent.&lt;/p&gt;

&lt;p&gt;I use rsync to synchronize the laptop to the disks. The specific incantation is:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;
  &lt;div class=&quot;highlight&quot;&gt;
    &lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;rsync &lt;span class=&quot;nt&quot;&gt;--progress&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--archive&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--human-readable&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--delete&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
      ~/Root/ /Volumes/Chiron/Root
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;Which recursively copies the contents of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/Root&lt;/code&gt; into &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/Volumes/Chiron/Root&lt;/code&gt;, preserving permissions/times/the executable flag, using checksums rather than heuristics to see which files have changed, and deleting files that exist in the target but not the source.&lt;/p&gt;

&lt;p&gt;Note that in rsync, trailing slashes matter! &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rsync -a source target&lt;/code&gt; creates a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;source&lt;/code&gt; directory inside &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;target&lt;/code&gt;, while &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rsync -a source/ target&lt;/code&gt; syncs the contents of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;source&lt;/code&gt; inside &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;target&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Why two disks? No reason. &lt;a href=&quot;https://www.youtube.com/watch?v=Et4sMJP9FmM&quot;&gt;Why have one when you can have two for twice the price?&lt;/a&gt;&lt;/p&gt;

&lt;h1 id=&quot;remote-backups&quot;&gt;Remote Backups&lt;/h1&gt;

&lt;p&gt;Continuing with the centaur naming convention, I have a Backblaze bucket named Pholus, and I use restic to take snapshots of the laptop and upload them to the bucket.&lt;/p&gt;

&lt;p&gt;Why Backblaze? Because it’s cheaper than &lt;a href=&quot;https://aws.amazon.com/s3/&quot;&gt;S3&lt;/a&gt;, and less involved than S3 (no IAM/roles/policies/etc.), and it does one thing and does it well. I would use S3 if I already had other personal infrastructure on AWS, and latency was a problem (I’m in Australia, and Backblaze is not; with AWS I could have an S3 bucket with ~6ms latency to my home).&lt;/p&gt;

&lt;p&gt;Why restic? Because everything else is worse. &lt;a href=&quot;https://en.wikipedia.org/wiki/Duplicity_(software)&quot;&gt;Duplicity&lt;/a&gt; requires using &lt;a href=&quot;https://en.wikipedia.org/wiki/GNU_Privacy_Guard&quot;&gt;GnuPG&lt;/a&gt; for key management, which is like if to start a car you had to stab yourself with your keys. &lt;a href=&quot;https://www.borgbackup.org/&quot;&gt;Borg&lt;/a&gt; is written in Python, which is usually a bad sign for performance and user experience. &lt;a href=&quot;https://rclone.org/&quot;&gt;Rclone&lt;/a&gt;, by default, is just cloud rsync, it doesn’t encrypt anything, you have to use a two-level configuration, where a &lt;a href=&quot;https://rclone.org/crypt/&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;crypt&lt;/code&gt;&lt;/a&gt; backend acts as a proxy to the real storage backend. So if you misconfigure things, you could end up writing cleartext to the cloud.&lt;/p&gt;

&lt;p&gt;restic is easy to learn. The ontology is: you have a thing called a &lt;a href=&quot;https://restic.readthedocs.io/en/stable/045_working_with_repos.html&quot;&gt;repository&lt;/a&gt;, which could be a local directory or a remote object store, identified by a path and locked with a password. A repository has a list of snapshots, which are like Git commits: a snapshot of a directory at a point in time. You can &lt;a href=&quot;https://restic.readthedocs.io/en/stable/045_working_with_repos.html#listing-files-in-a-snapshot&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ls&lt;/code&gt; the contents of snapshots&lt;/a&gt; and even &lt;a href=&quot;https://restic.readthedocs.io/en/stable/050_restore.html#printing-files-to-stdout&quot;&gt;restore specific files&lt;/a&gt;, which is useful for checking that a snapshot has the data you want without restoring the whole thing.&lt;/p&gt;

&lt;p&gt;I recommend trying out the commands using &lt;a href=&quot;https://restic.readthedocs.io/en/stable/030_preparing_a_new_repo.html#local&quot;&gt;local&lt;/a&gt; repositories, where the data is stored in a directory. That lets you get a hang of the ontology and the commands. Then you can create a repository backed by cloud storage.&lt;/p&gt;

&lt;p&gt;restic supports Backblaze directly, but the documentation &lt;a href=&quot;https://restic.readthedocs.io/en/stable/030_preparing_a_new_repo.html#backblaze-b2&quot;&gt;recommends&lt;/a&gt; using Backblaze’s S3-compatible API. To do this, when creating a bucket key you have to tick “Allow List All Bucket Names”, you will also have to know how to map the Backblaze key properties to the AWS environment variables. This is the only difficulty.&lt;/p&gt;

&lt;p&gt;Taking a snapshot is just:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;
  &lt;div class=&quot;highlight&quot;&gt;
    &lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;export &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;AWS_ACCESS_KEY_ID&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=[&lt;/span&gt;Backblaze keyID]
&lt;span class=&quot;nb&quot;&gt;export &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;AWS_SECRET_ACCESS_KEY&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=[&lt;/span&gt;Backblaze applicationKey]
&lt;span class=&quot;nb&quot;&gt;export &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;RESTIC_REPOSITORY&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;s3:[bucket endpoint hostname]/[bucket name]&quot;&lt;/span&gt;
restic backup ~/Root
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;You will then be asked to enter the repository password. For added peace of mind, you can &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ls&lt;/code&gt; the snapshot and dump the contents of a few representative files.&lt;/p&gt;

&lt;h1 id=&quot;frequency&quot;&gt;Frequency&lt;/h1&gt;

&lt;p&gt;I have a recurring task on my &lt;a href=&quot;https://www.todoist.com/&quot;&gt;todo list&lt;/a&gt; whereby, once a week, I plug in the external drives, run the backup script, and also take a restic snapshot.&lt;/p&gt;

&lt;p&gt;I could leave the drives plugged in all the time, and run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rsync&lt;/code&gt; automatically every day, but my MacBook Air doesn’t have enough ports for that and, also, this risks propagating data loss to the backups, which defeats the purpose. My doing manual backups, if I lose data unintentionally, I have up to a week to notice and restore it from the SSD.&lt;/p&gt;

</content:encoded>
      </item>
    
      <item>
        <title>We Live In a Golden Age of Interoperability</title>
        <description>On the growth of open standards.</description>
        <pubDate>Tue, 08 Apr 2025 00:00:00 +0000</pubDate>
        <link>https://borretti.me/article/we-live-in-a-golden-age-of-interoperability</link>
        <guid isPermaLink="true">
          https://borretti.me/article/we-live-in-a-golden-age-of-interoperability
        </guid>
        
        <content:encoded>&lt;p&gt;Yesterday I was reading &lt;a href=&quot;https://public.resource.org/eti/&quot;&gt;&lt;em&gt;Exploring the Internet&lt;/em&gt;&lt;/a&gt;, an oral history of the
early Internet. The first part of the book describes the author’s efforts to
publish the &lt;a href=&quot;https://en.wikipedia.org/wiki/International_Telecommunication_Union&quot;&gt;ITU&lt;/a&gt;’s Blue Book: 19 kilopages of standards documents for telephony
and networks. What struck me was the description of the ITU’s documentation
stack:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;A week spent trolling the halls of the ITU had produced documentation on about
half of the proprietary, in-house text formatting system they had developed
many years ago on a Siemens mainframe. The computer division had given me nine
magnetic tapes, containing the Blue Book in all three languages. […] We had
two types of files, one of which was known to be totally useless.&lt;/p&gt;

  &lt;p&gt;The useless batch was several hundred megabytes of AUTOCAD drawings, furnished
by the draftsmen who did the CCITT illustrations. Diagrams for the Blue Book
were done in AUTOCAD, then manually assembled into the output from the
proprietary text formatting system. […]&lt;/p&gt;

  &lt;p&gt;Turned out that AUTOCAD was indeed used for the diagrams, with the exception
of any text in the illustrations. The textless diagrams were sent over to the
typing pool, where people typed on little pieces of paper ribbon and pasted
the itsy-bitsy fragments onto the illustrations. Come publication time, the
whole process would be repeated, substituting typeset ribbons for typed
ribbons. A nice production technique, but the AUTOCAD files were useless.&lt;/p&gt;

  &lt;p&gt;The rationale for this bizarre document production technique was that each
diagram needed text in each of the three official languages that the ITU
published. While AUTOCAD (and typing) was still being used, the ITU was slowly
moving over to another tool, MicroGrafix Designer. There, using the magical
concept of layers, they were proudly doing “integrated text and graphics.”&lt;/p&gt;

  &lt;p&gt;The second batch of DOS files looked more promising. Modern documents, such as
the new X.800 recommendations, were being produced in Microsoft Word for
Windows. My second batch of tapes had all the files that were available in the
Word for Windows format, the new ITU publishing standard.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Proprietary tape drives with proprietary file systems. AutoCAD for vector
graphics. Text documents in the proprietary, binary Word format.&lt;/p&gt;

&lt;p&gt;Note that the diagrams were being assembled &lt;em&gt;physically&lt;/em&gt;, by pasting pieces of
paper together. And then they were photographed. That’s why it’s called a
“camera ready” copy. And this is 1991, so it’s not a digital camera: it’s film,
silver-halogen crystals in collagen. It’s astounding to think that this medieval
process was happening as recently as the 90s.&lt;/p&gt;

&lt;p&gt;Compare this to today: you drag some images into Adobe FrameMaker and press
print.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;The ITU had documented the format we could expect the tapes to be in. Each
file had a header written in the &lt;a href=&quot;https://en.wikipedia.org/wiki/EBCDIC&quot;&gt;EBCDIC&lt;/a&gt; character set. The file itself used
a character set seemingly invented by the ITU, known by the bizarre name of
Zentec. The only problem was that the header format wasn’t EBCDIC and the
structure the ITU had told us would be on the tape wasn’t present.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Proprietary character sets!&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Next, we had to tackle TPS. This text formatting language was as complicated
as any one could imagine. Developed without the desire for clarity and
simplicity I had come to expect from the UNIX operating system and its tools,
I was lost with the Byzantine, undocumented TPS.&lt;/p&gt;

  &lt;p&gt;The solution was to take several physical volumes of the Blue Book and compare
the text to hexadecimal dumps of the files. I then went to the Trident Cafe
and spent a week drinking coffee trying to make sense of the data I had,
flipping between the four files that might be used on any given page of text
trying to map events in the one-dimensional HexWorld to two-dimensional events
in the paper output.&lt;/p&gt;

  &lt;p&gt;[…]&lt;/p&gt;

  &lt;p&gt;Finally, after pages and pages of PERL code, we had the beginnings of a
conversion program. We had tried to use the software developed at the ITU to
convert from TPS into &lt;a href=&quot;https://en.wikipedia.org/wiki/Rich_Text_Format&quot;&gt;RTF&lt;/a&gt;, but the code had been worse than useless.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A proprietary, in-house, (ironically) undocumented document-preparation system!
Today this would be a Git repo with Markdown files and &lt;a href=&quot;https://en.wikipedia.org/wiki/PGF/TikZ&quot;&gt;TikZ&lt;/a&gt;/&lt;a href=&quot;https://en.wikipedia.org/wiki/Asymptote_(vector_graphics_language)&quot;&gt;Asymptote&lt;/a&gt;
source files for the diagrams, and a Makefile to tie it all together with
&lt;a href=&quot;https://pandoc.org/&quot;&gt;Pandoc&lt;/a&gt;. Maybe a few custom scripts for the things Markdown can’t represent, like
complex tables or asides. Maybe &lt;a href=&quot;https://en.wikipedia.org/wiki/Darwin_Information_Typing_Architecture&quot;&gt;DITA&lt;/a&gt; if you really like XML.&lt;/p&gt;

&lt;p&gt;This reminded me of a similar &lt;a href=&quot;https://github.com/LispLang/ansi-spec&quot;&gt;side quest&lt;/a&gt; I attempted many years ago: I
tried to build a modern version of the &lt;a href=&quot;https://www.lispworks.com/documentation/HyperSpec/Front/index.htm&quot;&gt;Common Lisp HyperSpec&lt;/a&gt; from the
source text of the ANSI Common Lisp draft (the draft being in the public domain,
unlike the officially blessed version). The sources are in TeX, not “modern”
LaTeX but 90’s TeX. Parsing TeX is hard enough, the language is
almost-but-not-quite context free, it really is meant to be executed as it is
parsed; rather than parsed, represented, and transformed. But even if you
managed to parse the TeX sources using a very flexible and permissive TeX
parser, you have to apply a huge long tail of corrections just to fix bad parses
and obscure TeX constructs. In the end I gave up.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;We live in much better times.&lt;/p&gt;

&lt;p&gt;For every medium, we have widely-used and widely-implemented open formats:
Unicode and Markdown for text, JSON and XML for data exchange, JPEG/PNG/SVG for
images, Opus for audio, WebM for videos.&lt;/p&gt;

&lt;p&gt;Unicode is so ubiquitous it’s easy to forget what an achievement it
is. Essentially all text today is UTF-8 except the Windows APIs that were
designed in the 90s for “wide characters” i.e. UTF-16. I remember when people
used to link to the &lt;a href=&quot;https://utf8everywhere.org/&quot;&gt;&lt;em&gt;UTF-8 Everywhere&lt;/em&gt;&lt;/a&gt; manifesto. There was a time, not
long ago, when “use UTF-8” was something that had to be said.&lt;/p&gt;

&lt;p&gt;Rich text is often just Markdown. Some applications have more complex constructs
that can’t be represented in Markdown, in those cases you can usually get the
document AST as JSON. The “worst” format most people ever have to deal with is
XML, which is &lt;a href=&quot;/article/brief-defense-of-xml&quot;&gt;really not that bad&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Data exchange happens through JSON, CSV, or &lt;a href=&quot;https://en.wikipedia.org/wiki/Apache_Parquet&quot;&gt;Parquet&lt;/a&gt;. Every web API uses
JSON as the transport layer, so instead of a thousand ad-hoc binary formats, we
have one plain-text, human-readable format that can be readily mapped into
domain objects. Nobody would think to share vector graphics in &lt;a href=&quot;https://en.wikipedia.org/wiki/.dwg&quot;&gt;DWG&lt;/a&gt; format
because we have SVG, an open standard.&lt;/p&gt;

&lt;p&gt;TeX is probably the most antediluvian text “format” in widespread use, and maybe
&lt;a href=&quot;https://typst.app/&quot;&gt;Typst&lt;/a&gt; will replace it. Math is one area where we’re stuck with embedding TeX
(through &lt;a href=&quot;https://katex.org/&quot;&gt;KaTeX&lt;/a&gt; or equivalent) since MathML hasn’t taken off (understandably,
since nobody wants to write XML by hand).&lt;/p&gt;

&lt;p&gt;Filesystems are usually proprietary, but every operating system can read/write a
FAT32/NTFS flash drive. In any case networking has made filesystems less
important: if you have network access you have Google Drive or S3. And
filesystems are a lot less diverse nowadays: except for extended attributes, any
file tree can be mapped losslessly across ext4, NTFS, and APFS. This was not
true in the past!  It took decades to converge on the definition of a filesystem
as “a tree of directories with byte arrays at the leaf nodes”, e.g. &lt;a href=&quot;https://en.wikipedia.org/wiki/Hierarchical_File_System_(Apple)&quot;&gt;HFS&lt;/a&gt; had
&lt;a href=&quot;https://en.wikipedia.org/wiki/Resource_fork&quot;&gt;resource forks&lt;/a&gt;, the &lt;a href=&quot;https://en.wikipedia.org/wiki/Files-11&quot;&gt;VMS file system&lt;/a&gt; had versioning built in. File paths
were wildly different.&lt;/p&gt;

&lt;p&gt;Open standards are now the default. If someone proposes a new data exchange
format, a new programming language, or things of that nature, the expectation is
that the spec will be readable online, at the click of a button, either as HTML
or a PDF document. If implementing JSON required paying 300 CHF for a 900 page
standards document, JSON would not have taken off.&lt;/p&gt;

&lt;p&gt;Our data is more portable than ever, not just across space (e.g. if you use a
Mac and a Linux machine) but across time.&lt;/p&gt;

&lt;p&gt;In the mid-80s the BBC wanted to make a &lt;a href=&quot;https://en.wikipedia.org/wiki/BBC_Domesday_Project&quot;&gt;latter-day Domesday Book&lt;/a&gt;. It was
like a time capsule: statistical surveys, photographs, newsreels, people’s
accounts of their daily life. The data was stored on &lt;a href=&quot;https://en.wikipedia.org/wiki/LaserDisc&quot;&gt;LaserDisc&lt;/a&gt;, but the
formats were entirely &lt;em&gt;sui generis&lt;/em&gt;, and could only be read by the client
software, which was deeply integrated with a specific hardware
configuration. And within a few years the data was essentially inaccessible,
needing a team of programmer-archeologists to reverse engineer the software and
data formats.&lt;/p&gt;

&lt;p&gt;If the BBC Domesday Book was made nowadays it would last forever: the text would
be UTF-8, the images JPEGs, the videos WebM, the database records would be CSVs
or JSON files, all packaged in one big ZIP container. All widely-implemented
open standards. A century from now we will still have UTF-8 decoders and JSON
parsers and JPEG viewers, if only to preserve the vast trove of the present; or
we will have ported all the archives forward to newer formats.&lt;/p&gt;

&lt;p&gt;All this is to say: we live in a golden age of interoperability and digital
preservation.&lt;/p&gt;
</content:encoded>
      </item>
    
      <item>
        <title>Domain-Agnostic and Domain-Specific Tools</title>
        <description>Software that can do everything does any one thing poorly.</description>
        <pubDate>Thu, 03 Apr 2025 00:00:00 +0000</pubDate>
        <link>https://borretti.me/article/domain-agnostic-and-domain-specific-tools</link>
        <guid isPermaLink="true">
          https://borretti.me/article/domain-agnostic-and-domain-specific-tools
        </guid>
        
        <content:encoded>&lt;p&gt;This post is, in a sense, a continuation to &lt;a href=&quot;/article/unbundling-tools-for-thought&quot;&gt;&lt;em&gt;Unbundling Tools for Thought&lt;/em&gt;&lt;/a&gt;. It’s an argument for why you shouldn’t try to use a single tool to do everything, aimed at people who have been spent too much time shoveling prose into a “second brain” and have little to show for it.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;Software tools span a spectrum from domain-agnostic to domain-specific.&lt;/p&gt;

&lt;p&gt;Domain-agnostic tools are things like Obsidian. They have a small, spartan data model that can be made to represent most things. Obsidian’s data model is just folders, pages, and links. Pages have a title and a body, and the body is text, and text is the universal interface. They have a small number of general workflows: creating a page, editing a page, viewing backlinks, text search. You can use them as a journal, a recipe app, a todo list, etc.&lt;/p&gt;

&lt;p&gt;Domain-specific tools have a richer and more structured data model. Consider a CRM: there are first-class objects to represent people, companies, employment relations; these have rich attributes, you can represent “this person worked for this company in this position for this span of time” natively within the data model. This structure allows you to have a large number of much more specific workflows, like “see everyone who worked with this person” or “find everyone who worked for this company in 2016”. But you can’t use them outside the domain: you can’t use a CRM as a recipe app.&lt;/p&gt;

&lt;p&gt;And here’s the asymmetry: while the tools can be domain-agnostic or domain-specific, your use cases are &lt;em&gt;always&lt;/em&gt; specific. You are always doing some concrete thing. And for any one specific use case, a specific tool can deliver a better ontology and a better UX than a general tool.&lt;/p&gt;

&lt;p&gt;Because when you implement a specific use case in a domain-agnostic tool, you are always building &lt;em&gt;on top of&lt;/em&gt; the tool’s data model. If you use e.g. Obsidian (or any other note-taking app) as, e.g., a CRM, there’s an abstract concept of people, companies, employment, etc., but these concepts don’t have a first-class existence, everything is concretely implemented as pages and links. You have a page to represent a person, a page to represent a company, and you use a link from the former to the latter to represent the “employed by” relation, and the corresponding backlink represents the “employs” relation.&lt;/p&gt;

&lt;p&gt;At the ontology level, your data looks like this:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/content/domain-agnostic-and-domain-specific-tools/a.svg&quot; alt=&quot;An ER diagram showing three tables with rich attributes: people, companies, employment.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;But the concrete data model looks like this:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/content/domain-agnostic-and-domain-specific-tools/b.svg&quot; alt=&quot;An ER diagram showing two tables: pages and links.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;And all the domain-specific nuances are hidden in text, invisible to software automation.&lt;/p&gt;

&lt;p&gt;Whereas in a domain-specific tool, you are building &lt;em&gt;inside&lt;/em&gt; the data model: there’s a table that implements the concept of “a person”, it has a fixed set of attributes. At every point in time, the database has a fixed schema: you know all the attributes a company object can have, you know all your entries are consistent and coherent. Instead of a generic notion of a bidirectional link, you have first-class objects that represent relations: e.g. an employment relation that links people to companies is represented by a table that points the person and their employer and has metadata (the person’s role, the start and end date of their employment).&lt;/p&gt;

&lt;p&gt;When it comes to workflows, using a domain-agnostic tool means you either have to do most things by hand or through plugins. Doing it by hand is straightforwardly less efficient. But plugins never feel right. Often the UX feels janky because plugins are built to a lower standard. But ultimately plugins mean you go from a coherent, unified vision to a cacophony of a hundred visions which are mutually suspicious and subtly out of alignment with one another.&lt;/p&gt;

&lt;p&gt;The main benefit of using a domain-agnostic app is that everything lives in the same data silo: you can cross-link data from many different disjoint use cases, e.g. journal entries and project documents and a reference library. Unlike web links, a single unified object graph can avoid dangling links, because the app can enforce link integrity. But this linking is hardly ever useful: do you actually need to have a bidirectional link between your journal entries and your recipes? Is there a benefit to this? You know where the link leads to. And links create a maintenance burden by making the entire graph structure more rigid.&lt;/p&gt;

&lt;p&gt;So why do people use domain-agnostic apps at all? Partly, because a lot of use-cases are too rare or ad-hoc to require specific software. If you have three entries in a spreadsheet of book reviews, it’s not necessary to go looking for a piece of software to manage them. This calculation will change as AI lowers the cost of software development.&lt;/p&gt;

&lt;p&gt;But part of the reason is ideological: a lot of people bemoan data silos and have this aspirational idea that computing would be so much better if everything was more deeply interlinked. If you let go of this monistic obsession that everything under the Sun should go in the one giant object graph, and instead let each piece of data live in its own silo, you can be more effective.&lt;/p&gt;
</content:encoded>
      </item>
    
  </channel>
</rss>
