<?xml version="1.0" encoding="UTF-8"?><feed xmlns="http://www.w3.org/2005/Atom">
  <title>lukasschwab.me • blog</title>
  <id>https://lukasschwab.me/blog/</id>
  <updated>2025-05-26T00:00:00Z</updated>
  <link href="https://lukasschwab.me/blog/"></link>
  <author>
    <name>Lukas Schwab</name>
  </author>
  <entry>
    <title>Spain Bibliography</title>
    <updated>2025-05-26T00:00:00Z</updated>
    <id>https://lukasschwab.me/blog/gen/spain.html</id>
    <content type="html">&lt;html xmlns=http://www.w3.org/1999/xhtml lang xml:lang&gt;&lt;meta charset=utf-8&gt;&lt;meta name=generator content=&#34;pandoc&#34;&gt;&lt;meta name=viewport content=&#34;width=device-width,initial-scale=1,user-scalable=yes&#34;&gt;&lt;meta name=dcterms.date content=&#34;2025-05-26&#34;&gt;&lt;title&gt;Spain Bibliography&lt;/title&gt;&lt;style&gt;&#xA;      code{white-space: pre-wrap;}&#xA;      span.smallcaps{font-variant: small-caps;}&#xA;      span.underline{text-decoration: underline;}&#xA;      div.column{display: inline-block; vertical-align: top; width: 50%;}&#xA;          &lt;/style&gt;&lt;link rel=stylesheet href=../styles/common.css&gt;&lt;link rel=stylesheet href=../styles/fence-extensions/addendum.css&gt;&lt;script data-goatcounter=https://lukasschwab.goatcounter.com/count async src=//gc.zgo.at/count.js&gt;&lt;/script&gt;&lt;a href=../index.html&gt;&amp;#171; blog&lt;/a&gt; &lt;span class=date&gt;2025-05-26&lt;/span&gt;&lt;header id=title-block-header&gt;&lt;h1 class=title&gt;Spain Bibliography&lt;/h1&gt;&lt;/header&gt;&lt;p&gt;I spent two weeks in Spain and read these books along the way. The&#xA;best texts are marked with ✷.&lt;nav id=TOC role=doc-toc&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=#the-spanish-labyrinth id=toc-the-spanish-labyrinth&gt;✷&#xA;&lt;em&gt;The Spanish Labyrinth&lt;/em&gt;&lt;/a&gt; by Gerald Brenan (1950)&lt;li&gt;&lt;a href=#cities-of-spain id=toc-cities-of-spain&gt;✷ &lt;em&gt;Cities of&#xA;Spain&lt;/em&gt;&lt;/a&gt; by David Gilmour (1992)&lt;li&gt;&lt;a href=#blue-guide-spain id=toc-blue-guide-spain&gt;&lt;em&gt;Blue Guide:&#xA;Spain&lt;/em&gt;&lt;/a&gt; by Ian Robertson (1993)&lt;li&gt;&lt;a href=#cathedrals-of-spain id=toc-cathedrals-of-spain&gt;&lt;em&gt;The&#xA;Cathedrals of Spain&lt;/em&gt;&lt;/a&gt; by John Hooper Harvey (1957)&lt;li&gt;&lt;a href=#ghosts-of-spain id=toc-ghosts-of-spain&gt;&lt;em&gt;Ghosts of Spain:&#xA;Travels Through a Country’s Hidden Past&lt;/em&gt;&lt;/a&gt; by Giles Tremlett&#xA;(2007)&lt;li&gt;&lt;a href=#gothic-architecture-in-spain id=toc-gothic-architecture-in-spain&gt;&lt;em&gt;Gothic&#xA;Architecture in Spain: Invention and Imitation&lt;/em&gt;&lt;/a&gt; ed. T. Nickson&#xA;&amp; N. Jennings (2020)&lt;/ul&gt;&lt;/nav&gt;&lt;div id=the-spanish-labyrinth&gt;&lt;dl&gt;&lt;dt&gt;✷ &lt;strong&gt;The Spanish Labyrinth&lt;/strong&gt;&lt;dd&gt;Gerald Brenan. Published by Cambridge University Press, 1950. &lt;a href=https://sfpl.bibliocommons.com/v2/record/S93C1440169&gt;Library.&lt;/a&gt;&lt;/dl&gt;&lt;p&gt;This was the clear highlight of my reading in Spain. Brenan — who&#xA;lived in Spain from 1919 — offers deep background, &lt;em&gt;centuries&lt;/em&gt; of&#xA;background, for the Spanish Civil War. Though there are centuries of&#xA;ideological division, the key thing to understand is the “agrarian&#xA;question:” even in the early 20th century, Spain’s peasant farmers lived&#xA;in bitter precarity. On the eve of the revolution, a bourgeois&#xA;Republican government collapses because they can’t switfly redistribute&#xA;the land. Their coalition bungles the election, which benefits the&#xA;Falange and the Communists in turn; in the chaos of their contest,&#xA;Franco rallies forces in Morocco and invades. The rest, as they say…&lt;p&gt;This summary omits everything impressive in the book. The War is just&#xA;a motivating conclusion for Brenan’s general history of Spanish&#xA;politics, an ancient mass of overlapping tensions.&lt;p&gt;Spain has a host of local nationalisms and national identities (most&#xA;pronounced in Catalonia and the Basque Country). The agrarian question&#xA;had distinct regional characteristics, thanks to regional conventions&#xA;for landownership and leases. Spain also has unusually sharp differences&#xA;between particular cities, thanks to an old royal tendency to grant&#xA;monopolies — important industrial monopolies to Bilbao and Barcelona; a&#xA;monopoly on early trade with the Americas to Sevilla — in addition to&#xA;the natural concentration of political and military power in the&#xA;capital, Madrid. There are universally recognizable class struggles, but&#xA;even &lt;em&gt;these&lt;/em&gt; are more nuanced. The agricultural worker is&#xA;typically an anarchist, the industrial worker a syndicalist.&lt;p&gt;This web is further tangled by Madrid’s tendency to pit local&#xA;factions against each other to weaken them. Memorably, Brenan argues&#xA;Josep Dencàs — nominally a Catalan separatist and effectively a local&#xA;breed of fascist — &lt;em&gt;must&lt;/em&gt; have been an instrument of the&#xA;ascendant Right in Madrid: why else would he disband his paramilitaries&#xA;and escape to France via Barcelona sewer &lt;em&gt;within ten hours&lt;/em&gt; of&#xA;forcing a Catalan secession?&lt;p&gt;I put my money where my mouth is and bought a personal paper copy of&#xA;this book, which will stand next to my Robert Caro biographies. I look&#xA;forward to reading Brenan’s memoirs; apparently he once tried to walk to&#xA;China.&lt;/div&gt;&lt;div id=cities-of-spain&gt;&lt;dl&gt;&lt;dt&gt;✷ &lt;strong&gt;Cities of Spain&lt;/strong&gt;&lt;dd&gt;David Gilmour. Published by I.R. Dee, 1992. &lt;a href=https://sfpl.bibliocommons.com/v2/record/S93C1305976&gt;Library.&lt;/a&gt;&lt;/dl&gt;&lt;p&gt;Gilmour’s introduction is important. Spain, he argues, doesn’t have a&#xA;historically primary city the way France and England do; nor does it&#xA;have historically independent city-states, as in Italy. The cities of&#xA;Spain have distinct characters and specializations. Over the centuries,&#xA;cultural and political hegemony shifted from place to place. Toledo is&#xA;home to Church &lt;em&gt;and&lt;/em&gt; Court until Phillip II moves to Madrid.&#xA;Sevilla enjoyed a monopoly on American trade that was loosed by the&#xA;shifting sands of the Guadalquivir. Córdoba was capital to the most&#xA;developed civilization in Europe… then, suddenly, it wasn’t.&lt;p&gt;This system is &lt;em&gt;ideal&lt;/em&gt; for travel book: you too will see Spain&#xA;one city at a time. Gilmour’s depictions are a little romantic, but he&#xA;roots the present &lt;em&gt;vibe&lt;/em&gt; of a place in its historical&#xA;development. If you need to convince someone who travels to “live like a&#xA;local” that they should also care about history, &lt;em&gt;Cities of&#xA;Spain&lt;/em&gt; is a strong argument to that effect.&lt;p&gt;The book covers Toledo, Córdoba, Santiago de Compostela, Sevilla,&#xA;Salamanca, Cádiz, Barcelona, San Sebastián, and Madrid.&lt;p&gt;One complaint: Gilmour overquotes the impressions of earlier British&#xA;travelers, especially when they comment on the character of Spanish&#xA;women. I’ll read his earlier book on Lebanon, but I hope it’s in a&#xA;different style.&lt;/div&gt;&lt;div id=blue-guide-spain&gt;&lt;dl&gt;&lt;dt&gt;&lt;strong&gt;Blue Guide: Spain&lt;/strong&gt;&lt;dd&gt;Ian Robertson. Published by A &amp; C Black, 1993. &lt;a href=https://sfpl.bibliocommons.com/v2/record/S93C1422589&gt;Library.&lt;/a&gt;&lt;/dl&gt;&lt;p&gt;It’s hard to find a good travel guide. It isn’t a Schwab family&#xA;vacation unless someone complains about how hard the &lt;em&gt;Rough&#xA;Guide&lt;/em&gt; series fell off. The universal teal-and-orange color schemes,&#xA;smeared with full-color photos, scream “please steal my passport, I am&#xA;hapless.”&lt;p&gt;The &lt;em&gt;Blue Guide&lt;/em&gt; can’t fall off because it’s out of print. The&#xA;&lt;em&gt;Blue Guide,&lt;/em&gt; despite the name, is barely blue, and it has text&#xA;instead of photos. Usually the text is small, but the “asides” are truly&#xA;microscopic. The &lt;em&gt;Blue Guide&lt;/em&gt; doesn’t say “shake me, take&#xA;currency;” it says “do not talk to me; I am a vicious, unapologetic&#xA;pedant.” (This is a joke: you will not meet anyone who recognizes the&#xA;&lt;em&gt;Blue Guide.&lt;/em&gt;)&lt;p&gt;Unfortunately, this book is organized for a tour by car (which I&#xA;didn’t do). Also, because it predates your smartphone by several&#xA;decades, its walking tours are described verbally, not mapped. This is a&#xA;hopeless proposition in the most interesting neighborhoods — the oldest,&#xA;and therefore the most mazelike.&lt;p&gt;Nonetheless, I was impressed! It’s nice to have a guide that’s dense&#xA;and brainy. If I had thirty minutes on the train before the next&#xA;Andalusian city, I spent those thirty minutes cramming the &lt;em&gt;Blue&#xA;Guide.&lt;/em&gt;&lt;p&gt;Find a copy of the &lt;em&gt;Blue Guide&lt;/em&gt; if you’re interested in&#xA;Spain’s churches. Harvey’s &lt;em&gt;Cathedrals of Spain&lt;/em&gt; focuses strictly&#xA;on the cathedrals, and only on their architecture; Robertson, with&#xA;saintly patience, describes even the art and decorative details of&#xA;relatively minor buildings. These interior walking tours (chapel by&#xA;chapel) are indispensable justification for the €4 to €10 entry fee the&#xA;Church just squeezes out of you at every entrance.&lt;p&gt;Finally, Robertson’s pithy city histories serve better as reference&#xA;texts than do the chapters of &lt;em&gt;Cities of Spain,&lt;/em&gt; and of course&#xA;the &lt;em&gt;Blue Guide&lt;/em&gt; histories cover Spanish cities more&#xA;comprehensively.&lt;/div&gt;&lt;div id=cathedrals-of-spain&gt;&lt;dl&gt;&lt;dt&gt;&lt;strong&gt;The Cathedrals of Spain&lt;/strong&gt;&lt;dd&gt;John Hooper Harvey. Published by Hastings House, 1957. &lt;a href=https://sfpl.bibliocommons.com/v2/record/S93C1118018&gt;Library.&lt;/a&gt;&lt;/dl&gt;&lt;p&gt;The young John Hooper Harvey was a prominent member of the Imperial&#xA;Fascist League in the 1930s, a British political movement founded by a&#xA;camel doctor because the nascent British Union of Fascists wasn’t, uh,&#xA;&lt;em&gt;fascist enough.&lt;/em&gt; This connection is mentioned on Harvey’s&#xA;Wikipedia page and alongside his brief mention in &lt;em&gt;Gothic&#xA;Architecture in Spain&lt;/em&gt; (below), but neither describes the extent of&#xA;his commitment, the viciousness of his writings, or the sheer fucking&#xA;weirdness of his friends as clearly as Graham Macklin’s article, “The&#xA;Two Lives of John Hooper Harvey.”&lt;a href=#fn1 class=footnote-ref id=fnref1 role=doc-noteref&gt;&lt;sup&gt;1&lt;/sup&gt;&lt;/a&gt;&lt;p&gt;After the end of the Second World War, Harvey wrote prolifically on&#xA;English architecture and history, especially of the late Middle Ages.&#xA;His 1948 book &lt;em&gt;The Plantagenets&lt;/em&gt; was widely used as a textbook,&#xA;despite a passage repeating a blood libel and endorsing Edward I’s&#xA;decision to expell all Jews from England in 1290. It seems there were&#xA;minor controversies along the way, but Harvey enjoyed public&#xA;respectability as a historian until his death in 1997. His fascist&#xA;sympathies were widely publicized after his death.&lt;p&gt;The overt racism and nationalism espoused in Harvey’s early writings&#xA;make him a strange author for any foreign architectural history. This is&#xA;especially true in Spain, where so much historical architecture (and so&#xA;much of what makes up its “national style”) is originally Islamic or&#xA;Jewish. The Mosque-Cathedral of Córdoba is one clear example; any good&#xA;walking tour of Toledo, through church &lt;em&gt;and&lt;/em&gt; mosque &lt;em&gt;and&lt;/em&gt;&#xA;synagogue, is another. Spain was ruled by Franco’s fascist dictatorship&#xA;when Harvey wrote the book; I can’t help but wonder if that made it&#xA;subtly more appetizing.&lt;p&gt;Thankfully, &lt;em&gt;The Cathedrals of Spain&lt;/em&gt; stands out in Harvey’s&#xA;oeuvre for its even-handedness. Harvey is straightforwardly sympathetic&#xA;to Spain’s Jews and &lt;em&gt;mudéjar&lt;/em&gt; (those Muslims who stayed in Iberia&#xA;after the Christian reconquest), and he credits the erstwhile Islamic&#xA;government of Al-Andalus for its religious tolerance in comparison to&#xA;the Catholic monarchs who ousted them. The editors of &lt;em&gt;Gothic&#xA;Architecture in Spain&lt;/em&gt; concede Harvey’s “prejudices,” at least, “do&#xA;not surface clearly.” Macklin wonders if Harvey had “moderated his&#xA;opinions” by the time he wrote this book. At the same time, be&#xA;ideologically on guard — while his work to popularize an idea of Spanish&#xA;“national style” here seems benign, his project to do the same for&#xA;Britain (in other volumes) is more politically, and racially,&#xA;tinged.&lt;p&gt;The book’s organization is ungainly. Buildings are described by&#xA;&lt;em&gt;region.&lt;/em&gt; Regional sections are apparently meant to be read&#xA;through linearly, but then comparative references are scattered&#xA;throughout. The index is thorough, but it’s a headache. Imagine a reader&#xA;in the Catedral de Sevilla tries to read about that building; the index&#xA;directs them, no joke, to pages 57, 58, 59, 102, 103, 183, 206, 217,&#xA;223, 227, 230–6, 239, 242, 260, 8, 120, and 146–8.&lt;a href=#fn2 class=footnote-ref id=fnref2 role=doc-noteref&gt;&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt; I&#xA;think Harvey is writing for a popular rather than an academic audience&#xA;(his callout of “Dr. Pevsner” suggests Harvey addresses a reader&#xA;familiar with Pevsner’s &lt;em&gt;Architectural Guides&lt;/em&gt;&lt;a href=#fn3 class=footnote-ref id=fnref3 role=doc-noteref&gt;&lt;sup&gt;3&lt;/sup&gt;&lt;/a&gt;),&#xA;but he lands in awkward middle ground.&lt;p&gt;This book’s primary strength is its breakdown of the &lt;em&gt;major,&#xA;architectural&lt;/em&gt; elements — when a feature of a cathedral was built or&#xA;rebuilt, by whom, and so on. Its secondary strength, in my opinion, is&#xA;Harvey’s taste; I think he’s unfair to Zaragoza’s Catedral basílica (an&#xA;“extraordinary pile,” in his words), but his groans about La Almudena in&#xA;Madrid and the cathedral in Granada are apt.&lt;p&gt;The book’s primary weakness as a travel companion is its ignorance of&#xA;art and interior details; for these, refer to the &lt;em&gt;Blue&#xA;Guide.&lt;/em&gt;&lt;p&gt;Interestingly, neither the &lt;em&gt;Blue Guide&lt;/em&gt; nor &lt;em&gt;The Spanish&#xA;Labyrinth&lt;/em&gt; have anything nice to say about Antoni Gaudí’s Sagrada&#xA;Familia in Barcelona, by now one of the main tourist attractions in&#xA;Spain. (&lt;em&gt;Cities of Spain,&lt;/em&gt; for its part, quotes Brenan.) I&#xA;expected Harvey, archconservative as he is, to join their criticism;&#xA;instead, he calls Gaudí “the most profound architectural genius of&#xA;modern times,” and his masterwork “at once the culmination of the&#xA;Spanish cathedral and the link which holds together its past and&#xA;future.”&lt;/div&gt;&lt;div id=ghosts-of-spain&gt;&lt;dl&gt;&lt;dt&gt;&lt;strong&gt;Ghosts of Spain: Travels Through a Country’s Hidden&#xA;Past&lt;/strong&gt;&lt;dd&gt;Giles Tremlett. Published by Holtzbrinck, 2007. &lt;a href=https://sfpl.bibliocommons.com/v2/record/S93C2080214&gt;Library.&lt;/a&gt;&lt;/dl&gt;&lt;p&gt;Tremlett’s memoirs as a foreign correspondent are most enthusiastic&#xA;when he’s explaining some salacious aspect of “the Spanish character.”&#xA;This tendency isn’t meanspirited, but it can feel condescending. Certain&#xA;chapters are strong — the one on &lt;em&gt;flamenco&lt;/em&gt; is heartfelt — and&#xA;Tremlett is often genuinely funny, but this book, only loosely organized&#xA;by locale, wasn’t useful to this traveler.&lt;/div&gt;&lt;div id=gothic-architecture-in-spain&gt;&lt;dl&gt;&lt;dt&gt;&lt;strong&gt;Gothic Architecture in Spain: Invention and&#xA;Imitation&lt;/strong&gt;&lt;dd&gt;Ed. Tom Nickson and Nicola Jennings. Published by the Research Forum of&#xA;The Courtauld Institute of Art, 2020. &lt;a href=https://courtauld.ac.uk/wp-content/uploads/2021/04/Gothic-Architecture-in-Spain-Double-page-Final.pdf&gt;Available&#xA;online.&lt;/a&gt;&lt;/dl&gt;&lt;p&gt;This one is for the fanatics! &lt;em&gt;Gothic Architecture in Spain&lt;/em&gt;&#xA;is academic, not fun. If you’re interested, it rewards with references&#xA;to some off-the-beaten-path structures (e.g Sevilla’s&#xA;&lt;em&gt;Atarazanas&lt;/em&gt; [shipyards], currently under restoration); good&#xA;documentary photographs; and a documented picture of France’s stylistic&#xA;influence.&lt;/div&gt;&lt;div class=addendum data-date=&#34;Jun. 28, 2025&#34;&gt;&lt;p&gt;I did not stop reading about Spain.&lt;p&gt;Impressed by &lt;em&gt;Spanish Labyrinth,&lt;/em&gt; I skimmed Gerald Brenan’s&#xA;memoirs (&lt;em&gt;A Life of One’s Own&lt;/em&gt; and &lt;em&gt;A Personal Record,&#xA;1920–1972&lt;/em&gt;) and some minor texts (&lt;em&gt;Thoughts in a Dry&#xA;Season&lt;/em&gt;). Unfortunately, the most memorable passage among these was&#xA;the chapter recounting how, well into his thirties, he employed an&#xA;illiterate 15 year old so he could ‘seduce’ her.&lt;a href=#fn4 class=footnote-ref id=fnref4 role=doc-noteref&gt;&lt;sup&gt;4&lt;/sup&gt;&lt;/a&gt;&#xA;Brenan only calls Juliana — the mother of his only child! — by her first&#xA;name. I don’t detect much remorse.&lt;p&gt;I found ✷ &lt;em&gt;Babel in Spain&lt;/em&gt; by John Haycraft because Brenan&#xA;wrote its introduction. Haycraft’s reminiscences on two years spent&#xA;teaching English with his wife in Córdoba are affectionate, even poetic.&#xA;I read it cover to cover.&lt;p&gt;David Gilmour’s &lt;em&gt;Lebanon: the Fractured Country&lt;/em&gt; is only a&#xA;Spain book because it’s by one of my Spain authors. I would love to find&#xA;a short book to recommend instead of &lt;em&gt;Pity the Nation,&lt;/em&gt; Robert&#xA;Fisk’s firsthand account of the Lebanese Civil War. Gilmour’s history is&#xA;condensed, but it lacks intensity. Read Gilmour’s &lt;a href=https://www.lrb.co.uk/the-paper/v12/n07/david-gilmour/ariel-the-unlucky&gt;1990&#xA;review of &lt;em&gt;Pity the Nation&lt;/em&gt;&lt;/a&gt;, and then read that book&#xA;instead.&lt;p&gt;&lt;em&gt;The Blue Guide&lt;/em&gt; is dotted with quotes from Richard Ford’s&#xA;three-kilo &lt;em&gt;Handbook for Travelers in Spain&lt;/em&gt; (1845), so I&#xA;schlepped those home from the library. Two things impressed. First,&#xA;volume 1 describes a number of skeleton tours: given an interest, Ford&#xA;provides a shortlist of cities and describes the major sites briefly.&#xA;Second, Ford really &lt;em&gt;writes.&lt;/em&gt; For example: when major Catholic&#xA;services were still performed in Toledo, “then the vast space [of the&#xA;Cathedral] was crowded with ant-like myriads, and the city of the&#xA;sleeper awoke as by a touch of the wand, and filled its streets…”&lt;a href=#fn5 class=footnote-ref id=fnref5 role=doc-noteref&gt;&lt;sup&gt;5&lt;/sup&gt;&lt;/a&gt;&lt;p&gt;&lt;em&gt;Rebels of the Rif&lt;/em&gt; by David Woolman describes the africanist&#xA;colonial disaster at Anual in 1921, in which a small confederation of&#xA;Berber tribes wrecked the Spanish army for three straight weeks, forcing&#xA;King Alfonso XIII’s abdication in favor of a military dictator. Woolman&#xA;paints with a little too much orientalist color at points, but he takes&#xA;a real interest in the lives and motivations of Morocco’s rebel leaders.&#xA;Read it for the biographies of Riasuli and Abd el-Krim.&lt;p&gt;Hemingway is unavoidable. I read most of &lt;em&gt;Death in the&#xA;Afternoon,&lt;/em&gt; his nonfiction guide to the Spanish bullfight. The&#xA;&lt;em&gt;Blue Guide&lt;/em&gt; calls this book a “dated curiosity,” but I really&#xA;enjoyed the chapter on syphilis.&lt;p&gt;My friends will be relieved to hear I’m done reading about Spain for&#xA;now.&lt;/div&gt;&lt;aside id=footnotes class=&#34;footnotes footnotes-end-of-document&#34; role=doc-endnotes&gt;&lt;hr&gt;&lt;ol&gt;&lt;li id=fn1&gt;&lt;p&gt;Macklin, Graham. 2008. &lt;a href=&#34;https://www.tandfonline.com/doi/full/10.1080/00313220801996030?scroll=top&amp;amp;needAccess=true&#34;&gt;“The&#xA;Two Lives of John Hooper Harvey.”&lt;/a&gt; &lt;em&gt;Patterns of Prejudice&lt;/em&gt; 42&#xA;(2): 167–90. Macklin draws in particular on the MI5 record of Harvey’s&#xA;correspondences within the Imperial Fascist League and with its outside&#xA;supporters. There is, needless to say, a shocking volume of documented&#xA;antisemitism. There’s also an episode in which Harvey dresses up in a&#xA;“Robin Hood tunic with leather-belt, sandals, and brown tweed tights”&#xA;and plays flute for a pretender to the crown of Poland.&lt;a href=#fnref1 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn2&gt;&lt;p&gt;If you bring &lt;em&gt;Cathedrals of Spain,&lt;/em&gt; memorize the&#xA;index key: “Numerals in &lt;em&gt;italics&lt;/em&gt; indicate the principal&#xA;references to buildings; those in &lt;strong&gt;heavy type&lt;/strong&gt; refer to&#xA;the &lt;em&gt;figure numbers&lt;/em&gt; of the illustrations.” Unfortunately, the&#xA;italic and regular typefaces in the 1957 edition are hard to discern.&lt;a href=#fnref2 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn3&gt;&lt;p&gt;See &lt;em&gt;Cathedrals of Spain&lt;/em&gt; pages 241 (footnote)&#xA;and 271.&lt;a href=#fnref3 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn4&gt;&lt;p&gt;&lt;em&gt;A Personal Record, 1920–1972,&lt;/em&gt; from page 210.&lt;a href=#fnref4 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn5&gt;&lt;p&gt;&lt;em&gt;A Handbook for Travelers in Spain,&lt;/em&gt; Vol. III,&#xA;pages 1250–1251. A bonus for the rare footnote-reader: one of the&#xA;skeleton tours is billed a “Tour for the Idler and Man of Pleasure.”&#xA;These readers, Ford writes, should just go to Paris instead.&lt;a href=#fnref5 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;/ol&gt;&lt;/aside&gt;</content>
    <link href="https://lukasschwab.me/blog/gen/spain.html" rel="alternate"></link>
    <summary type="html">I spent two weeks in Spain and read these books along the way.</summary>
  </entry>
  <entry>
    <title>Gothic Micro-Architecture</title>
    <updated>2025-03-19T00:00:00Z</updated>
    <id>https://lukasschwab.me/blog/gen/gothic-micro-architecture.html</id>
    <content type="html">&lt;html xmlns=http://www.w3.org/1999/xhtml lang xml:lang&gt;&lt;meta charset=utf-8&gt;&lt;meta name=generator content=&#34;pandoc&#34;&gt;&lt;meta name=viewport content=&#34;width=device-width,initial-scale=1,user-scalable=yes&#34;&gt;&lt;meta name=dcterms.date content=&#34;2025-03-19&#34;&gt;&lt;title&gt;Gothic Micro-Architecture&lt;/title&gt;&lt;style&gt;&#xA;      code{white-space: pre-wrap;}&#xA;      span.smallcaps{font-variant: small-caps;}&#xA;      span.underline{text-decoration: underline;}&#xA;      div.column{display: inline-block; vertical-align: top; width: 50%;}&#xA;          &lt;/style&gt;&lt;link rel=stylesheet href=../styles/common.css&gt;&lt;link rel=stylesheet href=../styles/fence-extensions/poem.css&gt;&lt;script data-goatcounter=https://lukasschwab.goatcounter.com/count async src=//gc.zgo.at/count.js&gt;&lt;/script&gt;&lt;a href=../index.html&gt;&amp;#171; blog&lt;/a&gt; &lt;span class=date&gt;2025-03-19&lt;/span&gt;&lt;header id=title-block-header&gt;&lt;h1 class=title&gt;Gothic Micro-Architecture&lt;/h1&gt;&lt;/header&gt;&lt;p&gt;I want to highlight an architecture paper I enjoyed: François&#xA;Bucher’s &lt;a href=https://www.jstor.org/stable/766753&gt;“Micro-Architecture as the&#xA;‘Idea’ of Gothic Theory and Style,”&lt;/a&gt; published 1976.&lt;a href=#fn1 class=footnote-ref id=fnref1 role=doc-noteref&gt;&lt;sup&gt;1&lt;/sup&gt;&lt;/a&gt;&#xA;It’s short: just six pages, plus photos and notes.&lt;p&gt;Bucher’s opening paragraph warns of a “strange reversal of&#xA;reference.” For many, ‘Gothic’ is all about the big stuff, the buildings&#xA;— both our most visible relics of the Middle Ages and the main&#xA;inspirations for later Gothic revivals. For Bucher, the buildings are&#xA;formal precursors and a theoretical distractions. Gothic architecture,&#xA;he says, really gets going &lt;em&gt;in miniature.&lt;/em&gt;&lt;p&gt;Abbot Suger of St. Denis is often credited with more or less&#xA;inventing Gothic architecture wholesale in his design of a light-filled&#xA;ambulatory for the Basilica of St. Denis, just north of Paris, in&#xA;1140&lt;span class=sc&gt;ce&lt;/span&gt;. Suger didn’t cut the stones himself, and&#xA;the new pointy-arched style wasn’t without architectural precedent, but&#xA;Suger had a rare instinct for self-promotion.&lt;a href=#fn2 class=footnote-ref id=fnref2 role=doc-noteref&gt;&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt;&lt;p&gt;The Abbot was also a magpie, deeply occupied with the treasures&#xA;glittering in the light of his huge new windows. He’s oft credited with&#xA;an “anagogic” philosophy of Gothic architecture, wherein the&#xA;architecture is supposed to draw a worshiper’s attention towards the&#xA;promise of a celestial architecture (“this church has the biggest&#xA;windows I’ve ever seen; I bet the windows in heaven will blow my&#xA;mind!”). According to the Bucher paper, Suger’s early-Gothic anagogy is&#xA;more correctly a philosophy about &lt;em&gt;small objects&lt;/em&gt; — “gee, that’s&#xA;the biggest &lt;em&gt;opal&lt;/em&gt; I’ve ever seen…” — than about the pointy&#xA;arches and flying buttresses of the Basilica. Art historian Erwin&#xA;Panofsky is more insistent: the close association between Suger’s&#xA;architecture and his anagogy is a popularized “misreading.” “In&#xA;reality,” Panofsky writes, Suger’s most famous passage on anagogy&#xA;“describes &lt;em&gt;only&lt;/em&gt; the trancelike state induced by the intense&#xA;contemplation of lustrous pearls and precious stones.”&lt;a href=#fn3 class=footnote-ref id=fnref3 role=doc-noteref&gt;&lt;sup&gt;3&lt;/sup&gt;&lt;/a&gt;&lt;p&gt;In spite of this original emphasis on the miniature — those “pearls&#xA;and precious stones” on the high altar at St. Denis — the Gothic style&#xA;develops, through the early and high Gothic periods, into 12th century&#xA;mega-architecture (“megalomania” or “Gothic gigantism” in Bucher’s&#xA;words). These are the huge projects most popularly associated with the&#xA;Gothic: Notre Dame de Paris, Chartres, Amiens, and so on, grand&#xA;demonstrations of the spiritual primacy of the Church and the earthly&#xA;hegemony of their patrons. As their constructions become technically&#xA;ultra-sophisticated — ever lighter, ever thinner, with more sculptural&#xA;vaulting and more tenuous statics — the master-builders grow powerful in&#xA;their own right. Consolidating the disbursement of artistic patronage,&#xA;these architects begin “to impose their aesthetic vocabulary upon the&#xA;world of reliquaries, stalls, fonts, pulpits, tombs, etc.,” the world of&#xA;what Bucher terms “micro-architecture.”&lt;a href=#fn4 class=footnote-ref id=fnref4 role=doc-noteref&gt;&lt;sup&gt;4&lt;/sup&gt;&lt;/a&gt;&lt;figure&gt;&lt;img src=../img/gothic-micro-architecture/organ-screen-york-minster.jpg alt=&#34;Frederick H. Evans, “Organ Screen, York Minster.” Photographed 1904, retrieved from Wikimedia Commons. Built in the 15th century and exemplary of late Gothic micro-architecture — note the ultra-fine openwork above the statues.&#34;&gt;&lt;figcaption aria-hidden=true&gt;Frederick H. Evans, “Organ Screen, York&#xA;Minster.” Photographed 1904, retrieved from &lt;a href=&#34;https://commons.wikimedia.org/w/index.php?title=File:Organ_Screen,_York_Minster_MET_DP116904.jpg&amp;amp;oldid=1010990049&#34;&gt;Wikimedia&#xA;Commons&lt;/a&gt;. Built in the 15th century and exemplary of &lt;em&gt;late&lt;/em&gt;&#xA;Gothic micro-architecture — note the ultra-fine openwork above the&#xA;statues.&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;In the mid-13th century, the micro-architecture &lt;em&gt;surpasses&lt;/em&gt;&#xA;the macro. Small elements like screens and lecterns flower into&#xA;“magnificently abstract works of refined architecture.” Bucher describes&#xA;what I’d call simulacrum-architecture at small scale: sacred objects&#xA;constructed from the same basic tenets as a Gothic building, but&#xA;modeling a building that would be &lt;em&gt;materially impossible&lt;/em&gt; (either&#xA;because of its insane extrapolated scale or a fineness unachievable in&#xA;iron-reinforced stone).&lt;p&gt;At around the same time, macro-architecture encounters material&#xA;impossibility. The 47.5m-high vault of Beauvais Cathedral — still&#xA;absolutely mind-numbing today, the highest Gothic choir in the world —&#xA;collapses in 1284. The 1300s &lt;span class=sc&gt;ce&lt;/span&gt; turn out to be a&#xA;hard century for grand projects in western Europe, years dotted with&#xA;power struggle, feudal war, plague, financial collapse, and popular&#xA;rebellion. Patronage gets smaller, and architecture with it.&#xA;Already-refined micro-architectural elements replace buildings as the&#xA;main focus; projects emphasize “enrichment of details, sophisticated&#xA;treatment of materials, and daring designs.” Gothic macro-architecture&#xA;collapses in on itself, folds over into something “marvelously prickly&#xA;[…] the seeming disarray of ever more complex cantilevering, branchwork,&#xA;double curved and utterly surrealistic ribs and bent finials.”&lt;p&gt;Bucher argues it’s this micro-architecture of the late middle ages&#xA;that best exemplify the Gothic aesthetic rationale, something “detached&#xA;[from] exacting architectural discipline,” a style “gloriously&#xA;deranged.” If you believe Panofsky’s reading of Abbot Suger — that the&#xA;exegetic power he saw in St. Denis lay in its treasures, not in its&#xA;building — this derangement-in-miniature closes the loop. The Gothic&#xA;architects gave light to sacred objects, then increasingly gave them&#xA;form; finally, at the end of the middle ages, Gothic openwork&#xA;&lt;em&gt;is&lt;/em&gt; the exegetic expression of Suger’s glittering vision.&lt;p&gt;Consider the original inscription of St. Denis’s portal:&lt;a href=#fn5 class=footnote-ref id=fnref5 role=doc-noteref&gt;&lt;sup&gt;5&lt;/sup&gt;&lt;/a&gt;&lt;blockquote&gt;&lt;div class=poem&gt;&lt;pre&gt;&#xA;Whoever thou art, if thou seekest to extoll the glory of these doors,&#xA;Marvel not at the gold and the expense but at the craftsmanship of the work.&#xA;Bright is the noble work; but, being nobly bright, the work&#xA;Should brighten the minds, so that they may travel, through the true lights,&#xA;To the True Light where Christ is the true door.&#xA;In what manner it be inherent in this world the golden door defines:&#xA;The dull mind rises to truth through that which is material&#xA;And, in seeing this light, is resurrected from its former submersion.&#xA;&lt;/pre&gt;&lt;/div&gt;&lt;/blockquote&gt;&lt;figure&gt;&lt;img src=../img/gothic-micro-architecture/chapel.jpg alt=&#34;Frederick H. Evans, “Bishop Alcock’s Chapel from Reho-Choir, Ely Cathedral.” Photographed 1897, retrieved from Wikimedia Commons. This chapel screen — clustered and crocketed — dates to around 1486.&#34;&gt;&lt;figcaption aria-hidden=true&gt;Frederick H. Evans, “Bishop Alcock’s&#xA;Chapel from Reho-Choir, Ely Cathedral.” Photographed 1897, retrieved&#xA;from &lt;a href=&#34;https://commons.wikimedia.org/w/index.php?title=File:Frederick_H._Evans_(British,_1853-1943)_-_Bishop_Alcock%27s_Chapel_from_Reho-Choir,_Ely_Cathedral_-_2019.100_-_Cleveland_Museum_of_Art.jpg&amp;amp;oldid=791039537&#34;&gt;Wikimedia&#xA;Commons&lt;/a&gt;. This chapel screen — clustered and crocketed — dates to&#xA;around 1486.&lt;/figcaption&gt;&lt;/figure&gt;&lt;aside id=footnotes class=&#34;footnotes footnotes-end-of-document&#34; role=doc-endnotes&gt;&lt;hr&gt;&lt;ol&gt;&lt;li id=fn1&gt;&lt;p&gt;Bucher, François. “Micro-Architecture as the ‘Idea’ of&#xA;Gothic Theory and Style.” &lt;em&gt;Gesta&lt;/em&gt; 15, no. 1/2 (1976): 71–89. &lt;a href=https://doi.org/10.2307/766753&gt;DOI 10.2307/766753&lt;/a&gt;. &lt;a href=https://www.jstor.org/stable/766753&gt;Available online&lt;/a&gt;.&lt;p&gt;The uncited quotations in the body of this post are all from this&#xA;paper.&lt;a href=#fnref1 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn2&gt;&lt;p&gt;Suger’s writings describe how often his name was&#xA;recorded on — physically engraved into! — the Basilica of St. Denis. His&#xA;likeness appears in at least two of the stained glass windows, in the&#xA;central tympanum above the main entrance, and on the monumental bronze&#xA;doors.&lt;p&gt;Suger’s writings served as defenses of his projects at St. Denis —&#xA;think “medieval subtweets” — and drew correspondences between certain&#xA;aesthetic priorities in Gothic architecture (e.g. the emphasis on light)&#xA;and religious principles, like the anagogic relationship discussed here.&#xA;Whether or not he invented it, Suger gathered the Gothic style together&#xA;and wrote it all down. He is unavoidable.&lt;a href=#fnref2 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn3&gt;&lt;p&gt;Panofsky, Erwin and Gerda Panofsky-Soergel. &lt;em&gt;Abbot&#xA;Suger on the Abbey Church of St. Denis and Its Art Treasures: Second&#xA;Edition.&lt;/em&gt; United Kingdom: Princeton University Press, 1979. Page&#xA;191. Emphasis mine.&lt;p&gt;Beware: my description of anagogy is extremely shallow. It’s a&#xA;complex concept &lt;em&gt;both&lt;/em&gt; in the mystical Sugerian meaning and as a&#xA;mode of scriptural exegesis. Ask your theologian if anagogy is right for&#xA;you.&lt;a href=#fnref3 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn4&gt;&lt;p&gt;I’ll confess a doubt: this is an underdeveloped point in&#xA;Bucher’s argument. It’s entirely plausible that cathedral architects, as&#xA;intermediaries between patrons and the masses of artists involved in a&#xA;construction, leveraged their control of funding into direct aesthetic&#xA;control over their sculptors, woodworkers, and so on (“the traditional&#xA;wish of architects — including Wright, Le Corbusier, Mies, and so on…”).&#xA;The simple emergence of architectural motifs in Gothic sculpture &lt;em&gt;is&#xA;not&lt;/em&gt; enough to prove that dynamic. Maybe there’s stronger evidence&#xA;in annals of cathedral construction somewhere.&lt;a href=#fnref4 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn5&gt;&lt;p&gt;Panofsky, Erwin and Gerda Panofsky-Soergel. &lt;em&gt;Abbot&#xA;Suger on the Abbey Church of St. Denis and Its Art Treasures: Second&#xA;Edition.&lt;/em&gt; United Kingdom: Princeton University Press, 1979. Page 47,&#xA;translated from Suger’s “Liber De Rebus In Administratione Sua&#xA;Gestis.”&lt;a href=#fnref5 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;/ol&gt;&lt;/aside&gt;</content>
    <link href="https://lukasschwab.me/blog/gen/gothic-micro-architecture.html" rel="alternate"></link>
    <summary type="html">An informal rundown of an architecture paper I enjoyed: François Bucher&#39;s &#34;Micro-Architecture as the &#39;Idea&#39; of Gothic Theory and Style.&#34; Bucher argues Gothic architecture is best exemplified by its late-stage small works --- the mini-simulacra of cathedrals rather than the great cathedrals themselves.</summary>
  </entry>
  <entry>
    <title>Bring Your Own Linter</title>
    <updated>2025-03-09T00:00:00Z</updated>
    <id>https://lukasschwab.me/blog/gen/bring-your-own-linter.html</id>
    <content type="html">&lt;html xmlns=http://www.w3.org/1999/xhtml lang xml:lang&gt;&lt;meta charset=utf-8&gt;&lt;meta name=generator content=&#34;pandoc&#34;&gt;&lt;meta name=viewport content=&#34;width=device-width,initial-scale=1,user-scalable=yes&#34;&gt;&lt;meta name=dcterms.date content=&#34;2025-03-09&#34;&gt;&lt;title&gt;Bring Your Own Linter&lt;/title&gt;&lt;style&gt;&#xA;      code{white-space: pre-wrap;}&#xA;      span.smallcaps{font-variant: small-caps;}&#xA;      span.underline{text-decoration: underline;}&#xA;      div.column{display: inline-block; vertical-align: top; width: 50%;}&#xA;          &lt;/style&gt;&lt;style&gt;pre &gt; code.sourceCode { white-space: pre; position: relative; }&#xA;pre &gt; code.sourceCode &gt; span { display: inline-block; line-height: 1.25; }&#xA;pre &gt; code.sourceCode &gt; span:empty { height: 1.2em; }&#xA;.sourceCode { overflow: visible; }&#xA;code.sourceCode &gt; span { color: inherit; text-decoration: inherit; }&#xA;div.sourceCode { margin: 1em 0; }&#xA;pre.sourceCode { margin: 0; }&#xA;@media screen {&#xA;div.sourceCode { overflow: auto; }&#xA;}&#xA;@media print {&#xA;pre &gt; code.sourceCode { white-space: pre-wrap; }&#xA;pre &gt; code.sourceCode &gt; span { text-indent: -5em; padding-left: 5em; }&#xA;}&#xA;pre.numberSource code&#xA;  { counter-reset: source-line 0; }&#xA;pre.numberSource code &gt; span&#xA;  { position: relative; left: -4em; counter-increment: source-line; }&#xA;pre.numberSource code &gt; span &gt; a:first-child::before&#xA;  { content: counter(source-line);&#xA;    position: relative; left: -1em; text-align: right; vertical-align: baseline;&#xA;    border: none; display: inline-block;&#xA;    -webkit-touch-callout: none; -webkit-user-select: none;&#xA;    -khtml-user-select: none; -moz-user-select: none;&#xA;    -ms-user-select: none; user-select: none;&#xA;    padding: 0 4px; width: 4em;&#xA;    color: #aaaaaa;&#xA;  }&#xA;pre.numberSource { margin-left: 3em; border-left: 1px solid #aaaaaa;  padding-left: 4px; }&#xA;div.sourceCode&#xA;  {   }&#xA;@media screen {&#xA;pre &gt; code.sourceCode &gt; span &gt; a:first-child::before { text-decoration: underline; }&#xA;}&#xA;code span.al { color: #ff0000; font-weight: bold; } /* Alert */&#xA;code span.an { color: #60a0b0; font-weight: bold; font-style: italic; } /* Annotation */&#xA;code span.at { color: #7d9029; } /* Attribute */&#xA;code span.bn { color: #40a070; } /* BaseN */&#xA;code span.bu { color: #008000; } /* BuiltIn */&#xA;code span.cf { color: #007020; font-weight: bold; } /* ControlFlow */&#xA;code span.ch { color: #4070a0; } /* Char */&#xA;code span.cn { color: #880000; } /* Constant */&#xA;code span.co { color: #60a0b0; font-style: italic; } /* Comment */&#xA;code span.cv { color: #60a0b0; font-weight: bold; font-style: italic; } /* CommentVar */&#xA;code span.do { color: #ba2121; font-style: italic; } /* Documentation */&#xA;code span.dt { color: #902000; } /* DataType */&#xA;code span.dv { color: #40a070; } /* DecVal */&#xA;code span.er { color: #ff0000; font-weight: bold; } /* Error */&#xA;code span.ex { } /* Extension */&#xA;code span.fl { color: #40a070; } /* Float */&#xA;code span.fu { color: #06287e; } /* Function */&#xA;code span.im { color: #008000; font-weight: bold; } /* Import */&#xA;code span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Information */&#xA;code span.kw { color: #007020; font-weight: bold; } /* Keyword */&#xA;code span.op { color: #666666; } /* Operator */&#xA;code span.ot { color: #007020; } /* Other */&#xA;code span.pp { color: #bc7a00; } /* Preprocessor */&#xA;code span.sc { color: #4070a0; } /* SpecialChar */&#xA;code span.ss { color: #bb6688; } /* SpecialString */&#xA;code span.st { color: #4070a0; } /* String */&#xA;code span.va { color: #19177c; } /* Variable */&#xA;code span.vs { color: #4070a0; } /* VerbatimString */&#xA;code span.wa { color: #60a0b0; font-weight: bold; font-style: italic; } /* Warning */&lt;/style&gt;&lt;link rel=stylesheet href=../styles/common.css&gt;&lt;link rel=stylesheet href=../styles/fence-extensions/codefile.css&gt;&lt;link rel=stylesheet href=../styles/fence-extensions/consoleprompt.css&gt;&lt;link rel=stylesheet href=../styles/fence-extensions/fullwidth.css&gt;&lt;script data-goatcounter=https://lukasschwab.goatcounter.com/count async src=//gc.zgo.at/count.js&gt;&lt;/script&gt;&lt;a href=../index.html&gt;&amp;#171; blog&lt;/a&gt; &lt;span class=date&gt;2025-03-09&lt;/span&gt;&lt;header id=title-block-header&gt;&lt;h1 class=title&gt;Bring Your Own Linter&lt;/h1&gt;&lt;/header&gt;&lt;nav id=TOC role=doc-toc&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=#write-a-custom-linter-analysis id=toc-write-a-custom-linter-analysis&gt;Write a custom linter:&#xA;&lt;code&gt;analysis&lt;/code&gt;&lt;/a&gt;&lt;li&gt;&lt;a href=#build-a-standalone-linter-run-it id=toc-build-a-standalone-linter-run-it&gt;Build a standalone linter; run&#xA;it&lt;/a&gt;&lt;li&gt;&lt;a href=#customize-golangci-lint id=toc-customize-golangci-lint&gt;Customize&#xA;&lt;code&gt;golangci-lint&lt;/code&gt;&lt;/a&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=#adapt-your-analyzer id=toc-adapt-your-analyzer&gt;Adapt&#xA;your &lt;code&gt;Analyzer&lt;/code&gt;&lt;/a&gt;&lt;li&gt;&lt;a href=#build-the-linter-binary id=toc-build-the-linter-binary&gt;Build the linter binary&lt;/a&gt;&lt;li&gt;&lt;a href=#custom-golangci-lint-in-vs-code id=toc-custom-golangci-lint-in-vs-code&gt;Custom&#xA;&lt;code&gt;golangci-lint&lt;/code&gt; in VS Code&lt;/a&gt;&lt;li&gt;&lt;a href=#custom-golangci-lint-in-github-actions id=toc-custom-golangci-lint-in-github-actions&gt;Custom&#xA;&lt;code&gt;golangci-lint&lt;/code&gt; in GitHub Actions&lt;/a&gt;&lt;/ul&gt;&lt;li&gt;&lt;a href=#getting-back-to-basics id=toc-getting-back-to-basics&gt;Getting back to basics&lt;/a&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=#an-experimental-metalinter-glint id=toc-an-experimental-metalinter-glint&gt;An experimental metalinter:&#xA;&lt;code&gt;glint&lt;/code&gt;&lt;/a&gt;&lt;/ul&gt;&lt;li&gt;&lt;a href=#references id=toc-references&gt;References&lt;/a&gt;&lt;/ul&gt;&lt;/nav&gt;&lt;p&gt;This is a practical guide for integrating a custom linter into your&#xA;Go project.&lt;ul&gt;&lt;li&gt;&lt;p&gt;The first two sections briefly describe writing and running a&#xA;custom linter — a single, atomic rule you want to enforce.&lt;li&gt;&lt;p&gt;The third section describes using your custom linter with Go’s&#xA;most common metalinter, &lt;code&gt;golangci-lint&lt;/code&gt;: at the command line,&#xA;in your editor, and in your continuous integration environment&#xA;(CI).&lt;/ul&gt;&lt;p&gt;Finally, this is a rant about tool complexity and my defense of a&#xA;simpler Go metalinter: &lt;a href=https://github.com/lukasschwab/glint&gt;&lt;code&gt;glint&lt;/code&gt;&lt;/a&gt;.&lt;h1 id=write-a-custom-linter-analysis&gt;Write a custom linter:&#xA;&lt;code&gt;analysis&lt;/code&gt;&lt;/h1&gt;&lt;p&gt;The &lt;a href=https://pkg.go.dev/golang.org/x/tools/go/analysis&gt;&lt;code&gt;golang.org/x/tools/go/analysis&lt;/code&gt;&lt;/a&gt;&#xA;package is the rich soil to Go’s linter ecosystem. A simple linter&#xA;typically defines an &lt;code&gt;analysis.Analyzer&lt;/code&gt; — or, for a complex&#xA;linter, a &lt;em&gt;graph&lt;/em&gt; of several mutually dependent&#xA;&lt;code&gt;Analyzer&lt;/code&gt;s — that passes over files line-by-line and&#xA;publishes diagnostics about them. The package’s published overview is&#xA;short and very clear; if you want to write or debug lint rule, go read&#xA;that overview.&lt;p&gt;&lt;a href=https://github.com/lukasschwab/nilinterface/blob/main/pkg/analyzer/analyzer.go&gt;&lt;code&gt;nilinterface&lt;/code&gt;&#xA;defines a single &lt;code&gt;Analyzer&lt;/code&gt;&lt;/a&gt; without any dependencies: it&#xA;inspects each node in each file’s AST, and uses&#xA;&lt;code&gt;pass.Reportf&lt;/code&gt; to alert if any given node is the literal&#xA;&lt;code&gt;nil&lt;/code&gt; &lt;em&gt;and&lt;/em&gt; serving as an interface-typed argument to&#xA;a function.&lt;/p&gt;&lt;p&gt;Note &lt;em&gt;where&lt;/em&gt; the &lt;code&gt;Analyzer&lt;/code&gt; is defined: in its own&#xA;package, &lt;code&gt;pkg/analyzer&lt;/code&gt;. All the code therein is strictly&#xA;independent of what tools you use to run the analysis (whether a&#xA;standalone command-line binary or a metalinter like&#xA;&lt;code&gt;golangci-lint&lt;/code&gt;); the package naturally separates concerns&#xA;between the analysis itself and the means of invoking it.&lt;p&gt;The &lt;code&gt;analysistest&lt;/code&gt; subpackage lets you test your&#xA;&lt;code&gt;Analyzer&lt;/code&gt; by feeding it &lt;em&gt;real code samples&lt;/em&gt;, with&#xA;expected diagnostics as inline code comments, from a &lt;a href=https://github.com/lukasschwab/nilinterface/tree/main/pkg/analyzer/testdata/src&gt;&lt;code&gt;testdata&lt;/code&gt;&#xA;directory&lt;/a&gt;. See &lt;a href=https://github.com/lukasschwab/nilinterface/blob/main/pkg/analyzer/analyzer_test.go&gt;&lt;code&gt;analyzer_test.go&lt;/code&gt;&lt;/a&gt;&#xA;for an example.&lt;p&gt;One tip: LLM code generation is really useful for writing&#xA;&lt;code&gt;Analyzer&lt;/code&gt;s, so long as you can precisely describe the linter&#xA;behavior you want. Let the AI massage the polymorphic AST types so you&#xA;can focus on the specific diagnostic conditions. Test-driven development&#xA;works well here: write your &lt;code&gt;analysistest&lt;/code&gt; examples (start&#xA;with true-positive cases, issues you want the linter to detect) so you&#xA;can validate the AI-generated contributions.&lt;h1 id=build-a-standalone-linter-run-it&gt;Build a standalone linter; run&#xA;it&lt;/h1&gt;&lt;p&gt;The simplest means of invoking an &lt;code&gt;analysis.Analyzer&lt;/code&gt; is&#xA;to build it into a standalone binary you can run from the command-line.&#xA;The &lt;code&gt;analysis&lt;/code&gt; package kindly provides a one-liner utility&#xA;for that: just create a &lt;code&gt;package main&lt;/code&gt; and call&#xA;&lt;code&gt;singlechecker.Main&lt;/code&gt;.&lt;div class=sourceCode id=cb1&gt;&lt;pre class=&#34;sourceCode go&#34;&gt;&lt;code class=&#34;sourceCode go&#34;&gt;&lt;span id=cb1-1&gt;&lt;a href=#cb1-1 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=kw&gt;package&lt;/span&gt; main&lt;/span&gt;&#xA;&lt;span id=cb1-2&gt;&lt;a href=#cb1-2 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;/span&gt;&#xA;&lt;span id=cb1-3&gt;&lt;a href=#cb1-3 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=kw&gt;import&lt;/span&gt; &lt;span class=op&gt;(&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb1-4&gt;&lt;a href=#cb1-4 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    &lt;span class=st&gt;&amp;quot;github.com/lukasschwab/nilinterface/pkg/analyzer&amp;quot;&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb1-5&gt;&lt;a href=#cb1-5 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;/span&gt;&#xA;&lt;span id=cb1-6&gt;&lt;a href=#cb1-6 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    &lt;span class=st&gt;&amp;quot;golang.org/x/tools/go/analysis/singlechecker&amp;quot;&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb1-7&gt;&lt;a href=#cb1-7 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=op&gt;)&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb1-8&gt;&lt;a href=#cb1-8 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;/span&gt;&#xA;&lt;span id=cb1-9&gt;&lt;a href=#cb1-9 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=co&gt;// main is an entry-point for running the nilinterface analyzer as a standalone&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb1-10&gt;&lt;a href=#cb1-10 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=co&gt;// binary. The analyzer process exposed by [singlechecker.Main] is additionally&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb1-11&gt;&lt;a href=#cb1-11 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=co&gt;// compatible with `go vet -vettool`.&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb1-12&gt;&lt;a href=#cb1-12 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=kw&gt;func&lt;/span&gt; main&lt;span class=op&gt;()&lt;/span&gt; &lt;span class=op&gt;{&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb1-13&gt;&lt;a href=#cb1-13 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    singlechecker&lt;span class=op&gt;.&lt;/span&gt;Main&lt;span class=op&gt;(&lt;/span&gt;analyzer&lt;span class=op&gt;.&lt;/span&gt;Analyzer&lt;span class=op&gt;)&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb1-14&gt;&lt;a href=#cb1-14 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=op&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Congratulations! Before, you just had an analyzer; &lt;em&gt;now&lt;/em&gt; you&#xA;have a linter. You can install this &lt;code&gt;main&lt;/code&gt; package and run it&#xA;like you would any other command-line tool:&lt;div class=sourceCode id=cb2&gt;&lt;pre class=&#34;sourceCode bash consoleprompt&#34;&gt;&lt;code class=&#34;sourceCode bash&#34;&gt;&lt;span id=cb2-1&gt;&lt;a href=#cb2-1 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=ex&gt;go&lt;/span&gt; install github.com/lukasschwab/nilinterface/cmd/nilinterface@latest&lt;/span&gt;&#xA;&lt;span id=cb2-2&gt;&lt;a href=#cb2-2 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=ex&gt;nilinterface&lt;/span&gt; &lt;span class=at&gt;--help&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb2-3&gt;&lt;a href=#cb2-3 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=ex&gt;nilinterface&lt;/span&gt; ./...&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Moreover, this &lt;code&gt;singlechecker.Main&lt;/code&gt; command line interface&#xA;is compatible with &lt;code&gt;go vet&lt;/code&gt;:&lt;div class=sourceCode id=cb3&gt;&lt;pre class=&#34;sourceCode bash consoleprompt&#34;&gt;&lt;code class=&#34;sourceCode bash&#34;&gt;&lt;span id=cb3-1&gt;&lt;a href=#cb3-1 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=ex&gt;go&lt;/span&gt; vet &lt;span class=at&gt;--vettool&lt;/span&gt;&lt;span class=op&gt;=&lt;/span&gt;&lt;span class=va&gt;$(&lt;/span&gt;&lt;span class=fu&gt;which&lt;/span&gt; nilinterface&lt;span class=va&gt;)&lt;/span&gt; ./...&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This is comfortable enough for one or two custom rules, but building&#xA;and invoking so many individual binaries is tedious for a complex&#xA;project. That’s where metalinters like &lt;code&gt;golangci-lint&lt;/code&gt; come&#xA;in: they coordinate a mass of smaller linters. Perhaps you’re already&#xA;using &lt;code&gt;golangci-lint&lt;/code&gt; for the bundle of linters it provides&#xA;out of the box. How do you incorporate your custom linter into that&#xA;bundle?&lt;h1 id=customize-golangci-lint&gt;Customize&#xA;&lt;code&gt;golangci-lint&lt;/code&gt;&lt;/h1&gt;&lt;p&gt;First, a rant. The amount of space dedicated to&#xA;&lt;code&gt;golangci-lint&lt;/code&gt; in this article is not to that project’s&#xA;credit.&lt;p&gt;Before this weekend, I naturally conceived of metalinters as&#xA;&lt;em&gt;coordinators&lt;/em&gt; in an ecosystem of lightweight modular linters.&#xA;That’s not quite right. &lt;code&gt;golangci-lint&lt;/code&gt;, out of the box, is a&#xA;gatekept&lt;a href=#fn1 class=footnote-ref id=fnref1 role=doc-noteref&gt;&lt;sup&gt;1&lt;/sup&gt;&lt;/a&gt; &lt;em&gt;fixed set&lt;/em&gt; of linters, baked&#xA;into the tool at build time. Your massive &lt;code&gt;.golangci.yml&lt;/code&gt;&#xA;config? That’s not &lt;em&gt;adding&lt;/em&gt; anything new — just picking rules&#xA;from among what &lt;code&gt;golangci-lint&lt;/code&gt; sees fit to offer.&lt;p&gt;Its &lt;a href=https://golangci-lint.run/usage/linters/&gt;list of&#xA;linters&lt;/a&gt; belies an uncomfortable but inevitable tension. On one hand,&#xA;there’s pressure to keep the list &lt;em&gt;short&lt;/em&gt;: each linter makes the&#xA;&lt;code&gt;golangci-lint&lt;/code&gt; executable bigger; each one enabled by&#xA;default makes it slower by default; linting with too many rules is&#xA;unhelpful; and extending the list, especially with redundant options,&#xA;makes selecting linters tedious. On the other hand, there’s reason to&#xA;keep extending the list: there are lots of conceivably useful lint rules&#xA;to provide.&lt;p&gt;The individual linters yield to this tension by being over-capable&#xA;and configurable (i.e. non-atomic). Some (e.g. &lt;code&gt;revive&lt;/code&gt; and&#xA;&lt;code&gt;staticcheck&lt;/code&gt;) are practically metalinters in their own&#xA;right. These complex linters have complex configuration spaces — making&#xA;them work for your project’s particular needs requires wading into the&#xA;&lt;code&gt;ls .*.yml&lt;/code&gt; mire you try not to think about.&lt;a href=#fn2 class=footnote-ref id=fnref2 role=doc-noteref&gt;&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt;&lt;figure&gt;&lt;img src=https://upload.wikimedia.org/wikipedia/commons/1/1f/Gustave_Dore_Inferno32.jpg alt=&#34;“He used single quotes instead of double quotes.” (Doré, Inferno #32)&#34;&gt;&lt;figcaption aria-hidden=true&gt;“He used single quotes instead of double&#xA;quotes.” (Doré, &lt;a href=https://commons.wikimedia.org/wiki/File:Gustave_Dore_Inferno32.jpg&gt;&lt;em&gt;Inferno&lt;/em&gt;&#xA;#32&lt;/a&gt;)&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;The solution to this tension is proper modularity among&#xA;&lt;em&gt;atomic&lt;/em&gt; linters with minimal configurability. The only thing in&#xA;your way is the trickiness of &lt;code&gt;golangci-lint&lt;/code&gt;’s plugins&#xA;system. It works — more below — but with rough docs and scant examples.&#xA;It’s opaque.&lt;p&gt;The first thing to understand is that &lt;code&gt;golangci-lint&lt;/code&gt;&#xA;documents &lt;em&gt;two different&lt;/em&gt; plugin systems: there’s the new &lt;a href=https://golangci-lint.run/plugins/module-plugins/&gt;module plugin&#xA;system&lt;/a&gt;,&lt;a href=#fn3 class=footnote-ref id=fnref3 role=doc-noteref&gt;&lt;sup&gt;3&lt;/sup&gt;&lt;/a&gt; which I’ll use here, and an older&#xA;“Go plugin” system based on using &lt;code&gt;cgo&lt;/code&gt; to compile linters as&#xA;&lt;code&gt;*.so&lt;/code&gt; files. Many custom-linter discussions online don’t&#xA;explicitly specify one or the other; often, these are discussions about&#xA;the Go plugin system because it came first.&lt;p&gt;If one red herring wasn’t enough, the module plugin system suggests&#xA;there are &lt;em&gt;two&lt;/em&gt; ways to add custom linters: “The Automatic Way”&#xA;and “The Manual Way.” These are two ways to do the same thing: build a&#xA;custom &lt;code&gt;golangci-lint&lt;/code&gt; binary with some additional linters&#xA;rolled in. You don’t need to learn them both. The “automatic way”&#xA;specifies the new linters in a YAML file, then uses a build tool&#xA;&lt;em&gt;included in &lt;code&gt;golangci-lint&lt;/code&gt;&lt;/em&gt; to integrate them; the&#xA;“manual way” has you explicitly clone, modify, and build&#xA;&lt;code&gt;golangci-lint&lt;/code&gt; from source. I’ll use the “automatic&#xA;way.”&lt;/p&gt;&lt;p&gt;There are tens of thousands of public GitHub repositories using&#xA;&lt;code&gt;golangci-lint&lt;/code&gt;; at time of writing, only &lt;a href=&#34;https://github.com/search?q=path%3A%2F%28%5E%7C%5C%2F%29.custom-gcl%5C.ya%3Fml%24%2F+-owner%3Agolangci+&amp;amp;type=code&#34;&gt;&lt;em&gt;34&#xA;of them&lt;/em&gt;&lt;/a&gt; customize it with module plugins.&lt;h2 id=adapt-your-analyzer&gt;Adapt your &lt;code&gt;Analyzer&lt;/code&gt;&lt;/h2&gt;&lt;p&gt;&lt;code&gt;golangci-lint&lt;/code&gt; doesn’t integrate with&#xA;&lt;code&gt;Analyzer&lt;/code&gt;s directly, or through some&#xA;&lt;code&gt;analysis&lt;/code&gt;-provided interface. It needs to pass on the&#xA;linter’s share of YAML settings, and it needs to understand what&#xA;information about a program the linter analyzes.&lt;p&gt;It does both through a registration pattern defined in &lt;a href=https://pkg.go.dev/github.com/golangci/plugin-module-register@v0.1.1/register&gt;golangci/plugin-module-register&lt;/a&gt;.&lt;ol type=1&gt;&lt;li&gt;&lt;p&gt;Wrap your analyzer to implement the interface&#xA;&lt;code&gt;register.LinterPlugin&lt;/code&gt;.&lt;p&gt;In practice, you want to return one of the two string constants in&#xA;&lt;code&gt;register&lt;/code&gt;. There’s scant documentation on the difference&#xA;between &lt;code&gt;LoadModeSyntax&lt;/code&gt; and &lt;code&gt;LoadModeTypesInfo&lt;/code&gt; —&#xA;analyzers with &lt;code&gt;LoadModeTypesInfo&lt;/code&gt; receive &lt;em&gt;strictly more&#xA;data,&lt;/em&gt; but may run a little slower.&lt;a href=#fn4 class=footnote-ref id=fnref4 role=doc-noteref&gt;&lt;sup&gt;4&lt;/sup&gt;&lt;/a&gt; If&#xA;you provide &lt;em&gt;any other&lt;/em&gt; string, &lt;code&gt;golangci-lint&lt;/code&gt; &lt;a href=https://github.com/golangci/golangci-lint/blob/bddd1bcedbc2f3d767e2362be91ff9eb481493cd/pkg/lint/lintersdb/builder_plugin_module.go#L59-L66&gt;defaults&lt;/a&gt;&#xA;to the data-rich mode.&lt;li&gt;&lt;p&gt;Define a factory function (this &lt;em&gt;type&lt;/em&gt; is confusingly&#xA;named &lt;code&gt;NewPlugin&lt;/code&gt;) that receives untyped configuration from&#xA;&lt;code&gt;.golangci.yml&lt;/code&gt; and returns the &lt;code&gt;LinterPlugin&lt;/code&gt;&#xA;defined in step 1.&lt;li&gt;&lt;p&gt;Define an &lt;a href=https://go.dev/doc/effective_go#init&gt;&lt;code&gt;init&lt;/code&gt;&#xA;function&lt;/a&gt; that calls &lt;code&gt;register.Plugin&lt;/code&gt; to register the&#xA;factory function defined in step 2.&lt;/ol&gt;&lt;p&gt;I won’t get into the implementation details — for an atomic linter&#xA;without configuration variables, this adapter package should be short&#xA;and the logic largely reusable between linters. Here’s the &lt;a href=https://github.com/lukasschwab/nilinterface/blob/main/pkg/golangci-linter/nilinterface.go&gt;adapter&#xA;for &lt;code&gt;nilinterface&lt;/code&gt;&lt;/a&gt;; I isolated all the&#xA;&lt;code&gt;golangci&lt;/code&gt;-specific logic in its own package so it doesn’t&#xA;muddle the analysis.&lt;/p&gt;&lt;h2 id=build-the-linter-binary&gt;Build the linter binary&lt;/h2&gt;&lt;p&gt;At this point you can shift your focus from lint&lt;em&gt;er&lt;/em&gt; to&#xA;lint&lt;em&gt;ee&lt;/em&gt; — the project you’re checking with&#xA;&lt;code&gt;golangci-lint&lt;/code&gt;. If you’re content without customization,&#xA;linting this project is simple: you &lt;code&gt;go install&lt;/code&gt; a command&#xA;line tool, optionally provide it a configuration file, and you’re off to&#xA;the races.&lt;figure&gt;&lt;img src=../img/bring-your-own-linter/golangci-lint-standard.svg alt=&#34;Using golangci-lint without plugins: provide it a YAML configuration (conventionally in .golangci.yml) when you analyze your code.&#34;&gt;&lt;figcaption aria-hidden=true&gt;Using &lt;code&gt;golangci-lint&lt;/code&gt; without&#xA;plugins: provide it a YAML configuration (conventionally in&#xA;.golangci.yml) when you analyze your code.&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;The “fixed set of linters” available in this installation are&#xA;actually &lt;em&gt;baked into&lt;/em&gt; the installed executable. Even if you&#xA;haven’t &lt;em&gt;enabled&lt;/em&gt; &lt;code&gt;tagliatele&lt;/code&gt;, for example, it’s&#xA;lurking in there somewhere.&lt;p&gt;Your new custom linter, unfortunately, is not — at least not until&#xA;the maintainer accepts a PR adding it. The clever idea behind&#xA;&lt;code&gt;golangci&lt;/code&gt;’s module plugin system is that the linter&#xA;executable you installed &lt;em&gt;also&lt;/em&gt; includes a build tool,&#xA;&lt;code&gt;golangci-lint custom&lt;/code&gt;, for building another version of&#xA;itself from scratch, with additional Go modules — Go modules that&#xA;&lt;em&gt;register &lt;code&gt;LinterPlugin&lt;/code&gt;s&lt;/em&gt; — rolled in!&lt;div class=fullwidth&gt;&lt;figure&gt;&lt;img src=../img/bring-your-own-linter/golangci-lint-custom.svg style=&#34;max-height: 400px&#34; alt=&#34;Using golangci-lint with plugins (“The Automatic Way”) adds a build step: run golangci-lint custom -v to build a custom version of golangci-lint, then provide configuration to that binary to analyze your code. The custom-gcl tool is interchangeable with golangci-lint; it just has your custom linter baked in!&#34;&gt;&lt;figcaption aria-hidden=true&gt;Using &lt;code&gt;golangci-lint&lt;/code&gt; with&#xA;plugins (“The Automatic Way”) adds a build step: run&#xA;&lt;code&gt;golangci-lint custom -v&lt;/code&gt; to build a custom version of&#xA;&lt;code&gt;golangci-lint&lt;/code&gt;, then provide configuration to &lt;em&gt;that&lt;/em&gt;&#xA;binary to analyze your code. The &lt;code&gt;custom-gcl&lt;/code&gt; tool is&#xA;interchangeable with &lt;code&gt;golangci-lint&lt;/code&gt;; it just has your custom&#xA;linter baked in!&lt;/figcaption&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;You define custom modules in the &lt;code&gt;.custom-gcl.yml&lt;/code&gt; file,&#xA;distinct and &lt;em&gt;upstream&lt;/em&gt; from the familiar .golangci.yml file.&#xA;Running the build tool yields a custom linter binary, which itself reads&#xA;configuration from .golangci.yml like &lt;code&gt;golangci-lint&lt;/code&gt; did&#xA;before.&lt;p&gt;Don’t worry, this is nondestructive: the original&#xA;&lt;code&gt;golangci-lint&lt;/code&gt; executable you installed is retained&#xA;unchanged. If you want to rip out custom linting, you can delete&#xA;&lt;code&gt;custom-gcl&lt;/code&gt; and &lt;code&gt;.custom-gcl.yml&lt;/code&gt; and you’re back&#xA;to the way things were.&lt;p&gt;For example, here’s a configuration file for a version of&#xA;&lt;code&gt;golangci-lint&lt;/code&gt; with &lt;code&gt;nilinterface&lt;/code&gt;:&lt;div class=codefile data-filename=.custom-gcl.yml&gt;&lt;div class=sourceCode id=cb4&gt;&lt;pre class=&#34;sourceCode yaml&#34;&gt;&lt;code class=&#34;sourceCode yaml&#34;&gt;&lt;span id=cb4-1&gt;&lt;a href=#cb4-1 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=fu&gt;version&lt;/span&gt;&lt;span class=kw&gt;:&lt;/span&gt;&lt;span class=at&gt; v1.63.4&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-2&gt;&lt;a href=#cb4-2 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=fu&gt;plugins&lt;/span&gt;&lt;span class=kw&gt;:&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-3&gt;&lt;a href=#cb4-3 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=at&gt;  &lt;/span&gt;&lt;span class=kw&gt;-&lt;/span&gt;&lt;span class=at&gt; &lt;/span&gt;&lt;span class=fu&gt;module&lt;/span&gt;&lt;span class=kw&gt;:&lt;/span&gt;&lt;span class=at&gt; &lt;/span&gt;&lt;span class=st&gt;&amp;#39;github.com/lukasschwab/nilinterface&amp;#39;&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-4&gt;&lt;a href=#cb4-4 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=at&gt;    &lt;/span&gt;&lt;span class=fu&gt;import&lt;/span&gt;&lt;span class=kw&gt;:&lt;/span&gt;&lt;span class=at&gt; &lt;/span&gt;&lt;span class=st&gt;&amp;#39;github.com/lukasschwab/nilinterface/pkg/golangci-linter&amp;#39;&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-5&gt;&lt;a href=#cb4-5 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=at&gt;    &lt;/span&gt;&lt;span class=fu&gt;version&lt;/span&gt;&lt;span class=kw&gt;:&lt;/span&gt;&lt;span class=at&gt; v0.0.6&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;module&lt;/code&gt; field defines a &lt;a href=https://go.dev/blog/using-go-modules&gt;Go module&lt;/a&gt;, the same way&#xA;you would &lt;code&gt;import&lt;/code&gt; it in a go file. &lt;code&gt;import&lt;/code&gt;&#xA;identifies a package &lt;em&gt;inside that module&lt;/em&gt;, the adapter package&#xA;described above (see &lt;a href=#adapt-your-analyzer&gt;“Adapt your&#xA;&lt;code&gt;Analyzer&lt;/code&gt;”&lt;/a&gt;). You could include several custom linters by&#xA;defining plugins beyond this one.&lt;p&gt;Running &lt;code&gt;custom-gcl&lt;/code&gt; is exactly the same as running&#xA;bog-standard &lt;code&gt;golangci-lint&lt;/code&gt;: the command-line interface and&#xA;YAML settings are the same. Keep in mind, baking a linter into&#xA;&lt;code&gt;golangci-lint&lt;/code&gt; &lt;em&gt;doesn’t mean it’ll run by default&lt;/em&gt;;&#xA;you may need to update &lt;code&gt;.golangci.yml&lt;/code&gt; to explicitly enable&#xA;it. Using &lt;code&gt;nilinterface&lt;/code&gt; as an example:&lt;div class=codefile data-filename=.golangci.yml&gt;&lt;div class=sourceCode id=cb5&gt;&lt;pre class=&#34;sourceCode yaml&#34;&gt;&lt;code class=&#34;sourceCode yaml&#34;&gt;&lt;span id=cb5-1&gt;&lt;a href=#cb5-1 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=co&gt;# This example file is grossly simplified.&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb5-2&gt;&lt;a href=#cb5-2 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=fu&gt;linters-settings&lt;/span&gt;&lt;span class=kw&gt;:&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb5-3&gt;&lt;a href=#cb5-3 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=at&gt;  &lt;/span&gt;&lt;span class=fu&gt;custom&lt;/span&gt;&lt;span class=kw&gt;:&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb5-4&gt;&lt;a href=#cb5-4 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=at&gt;    &lt;/span&gt;&lt;span class=fu&gt;nilinterface&lt;/span&gt;&lt;span class=kw&gt;:&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb5-5&gt;&lt;a href=#cb5-5 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=at&gt;      &lt;/span&gt;&lt;span class=fu&gt;type&lt;/span&gt;&lt;span class=kw&gt;:&lt;/span&gt;&lt;span class=at&gt; &lt;/span&gt;&lt;span class=st&gt;&amp;#39;module&amp;#39;&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb5-6&gt;&lt;a href=#cb5-6 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=at&gt;      &lt;/span&gt;&lt;span class=fu&gt;description&lt;/span&gt;&lt;span class=kw&gt;:&lt;/span&gt;&lt;span class=at&gt; &lt;/span&gt;&lt;span class=st&gt;&amp;#39;forbids passing `nil` as an interface argument to function calls&amp;#39;&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb5-7&gt;&lt;a href=#cb5-7 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;/span&gt;&#xA;&lt;span id=cb5-8&gt;&lt;a href=#cb5-8 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=co&gt;# If you have `disable-all: true`, add nilinterface to your enabled linters.&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb5-9&gt;&lt;a href=#cb5-9 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=fu&gt;linters&lt;/span&gt;&lt;span class=kw&gt;:&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb5-10&gt;&lt;a href=#cb5-10 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=at&gt;  &lt;/span&gt;&lt;span class=fu&gt;disable-all&lt;/span&gt;&lt;span class=kw&gt;:&lt;/span&gt;&lt;span class=at&gt; &lt;/span&gt;&lt;span class=ch&gt;true&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb5-11&gt;&lt;a href=#cb5-11 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=at&gt;  &lt;/span&gt;&lt;span class=fu&gt;enable&lt;/span&gt;&lt;span class=kw&gt;:&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb5-12&gt;&lt;a href=#cb5-12 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=at&gt;    &lt;/span&gt;&lt;span class=kw&gt;-&lt;/span&gt;&lt;span class=at&gt; nilinterface&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;With the preceding changes to our YAML files, we can go from a clean&#xA;&lt;code&gt;golangci-lint&lt;/code&gt; to our custom analysis with this pair of&#xA;shell commands:&lt;div class=sourceCode id=cb6&gt;&lt;pre class=&#34;sourceCode bash consoleprompt&#34;&gt;&lt;code class=&#34;sourceCode bash&#34;&gt;&lt;span id=cb6-1&gt;&lt;a href=#cb6-1 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=ex&gt;golangci-lint&lt;/span&gt; custom &lt;span class=at&gt;-v&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb6-2&gt;&lt;a href=#cb6-2 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=ex&gt;./custom-gcl&lt;/span&gt; run ./...&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The first command, the build step, is way too slow to run every time&#xA;you need to lint your project. &lt;code&gt;golangci-lint custom&lt;/code&gt;&#xA;downloads and builds &lt;code&gt;golangci-lint&lt;/code&gt; from source — not a fast&#xA;process. Of course, it doesn’t make sense to commit a binary built for&#xA;&lt;em&gt;your&lt;/em&gt; system into shared source control either… so sometimes&#xA;you’re gonna have to suck it up and rebuild the thing.&lt;p&gt;Thankfully, you only need to rebuild this custom linter binary when&#xA;you change &lt;code&gt;.custom-gcl.yml&lt;/code&gt;. &lt;code&gt;Makefile&lt;/code&gt;s are&#xA;great for conditioning builds on file changes, and lazily building only&#xA;what’s definitely necessary. If you define targets in your project&#xA;&lt;code&gt;Makefile&lt;/code&gt; like so —&lt;div class=codefile data-filename=Makefile&gt;&lt;div class=sourceCode id=cb7&gt;&lt;pre class=&#34;sourceCode makefile&#34;&gt;&lt;code class=&#34;sourceCode makefile&#34;&gt;&lt;span id=cb7-1&gt;&lt;a href=#cb7-1 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=ot&gt;.PHONY:&lt;/span&gt;&lt;span class=dt&gt; lint&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb7-2&gt;&lt;a href=#cb7-2 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=dv&gt;lint:&lt;/span&gt;&lt;span class=dt&gt; custom-gcl&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb7-3&gt;&lt;a href=#cb7-3 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    ./custom-gcl run&lt;/span&gt;&#xA;&lt;span id=cb7-4&gt;&lt;a href=#cb7-4 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;/span&gt;&#xA;&lt;span id=cb7-5&gt;&lt;a href=#cb7-5 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=dv&gt;custom-gcl:&lt;/span&gt;&lt;span class=dt&gt; .custom-gcl.yml&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb7-6&gt;&lt;a href=#cb7-6 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    golangci-lint custom -v&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;— you can run &lt;code&gt;make lint&lt;/code&gt; in lieu of the pair of shell&#xA;commands above. The slow &lt;code&gt;golangci-lint custom&lt;/code&gt; process will&#xA;only run when &lt;code&gt;custom-gcl&lt;/code&gt; (the executable it builds) is out&#xA;of date.&lt;h2 id=custom-golangci-lint-in-vs-code&gt;Custom&#xA;&lt;code&gt;golangci-lint&lt;/code&gt; in VS Code&lt;/h2&gt;&lt;p&gt;There’s a silver lining to building a drop-in replacement for&#xA;&lt;code&gt;golangci-lint&lt;/code&gt;: tools that integrate tightly with&#xA;&lt;code&gt;golangci-lint&lt;/code&gt; can integrate tightly with your&#xA;&lt;code&gt;custom-gcl&lt;/code&gt; instead. If you use VS Code, specify it as an&#xA;alternate lint tool to see your custom linter applied in the editor:&lt;div class=sourceCode id=cb8&gt;&lt;pre class=&#34;sourceCode json&#34;&gt;&lt;code class=&#34;sourceCode json&#34;&gt;&lt;span id=cb8-1&gt;&lt;a href=#cb8-1 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=fu&gt;{&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb8-2&gt;&lt;a href=#cb8-2 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;  &lt;span class=dt&gt;&amp;quot;go.lintTool&amp;quot;&lt;/span&gt;&lt;span class=fu&gt;:&lt;/span&gt; &lt;span class=st&gt;&amp;quot;golangci-lint&amp;quot;&lt;/span&gt;&lt;span class=fu&gt;,&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb8-3&gt;&lt;a href=#cb8-3 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;  &lt;span class=dt&gt;&amp;quot;go.alternateTools&amp;quot;&lt;/span&gt;&lt;span class=fu&gt;:&lt;/span&gt; &lt;span class=fu&gt;{&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb8-4&gt;&lt;a href=#cb8-4 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    &lt;span class=dt&gt;&amp;quot;golangci-lint&amp;quot;&lt;/span&gt;&lt;span class=fu&gt;:&lt;/span&gt; &lt;span class=st&gt;&amp;quot;${workspaceFolder}/custom-gcl&amp;quot;&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb8-5&gt;&lt;a href=#cb8-5 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;  &lt;span class=fu&gt;}&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb8-6&gt;&lt;a href=#cb8-6 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=fu&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;${workspaceFolder}&lt;/code&gt; dynamically evaluates to &lt;a href=https://code.visualstudio.com/Docs/editor/debugging#_variable-substitution&gt;your&#xA;current VS Code workspace&lt;/a&gt;. If you’re sharing a single linter across&#xA;&lt;em&gt;several&lt;/em&gt; workspaces, specify the binary’s absolute path&#xA;instead.&lt;h2 id=custom-golangci-lint-in-github-actions&gt;Custom&#xA;&lt;code&gt;golangci-lint&lt;/code&gt; in GitHub Actions&lt;/h2&gt;&lt;p&gt;&lt;img src=../img/bring-your-own-linter/yaml.jpg style=&#34;display: inline-block; float: left; margin: 0 1em 1em 0; max-height: 200px; opacity: 70%;&#34;&gt;&#xA;The &lt;code&gt;golangci-lint&lt;/code&gt; team maintains an official GitHub Action&#xA;for running their linter, but it has &lt;em&gt;no support&lt;/em&gt; for custom&#xA;linter modules as I write this: it’ll use your&#xA;&lt;code&gt;.golangci.yml&lt;/code&gt;, but totally ignore the custom linters pinned&#xA;in &lt;code&gt;.custom-gcl.yml&lt;/code&gt;.&lt;p&gt;I built an alternative: &lt;a href=https://github.com/marketplace/actions/golangci-lint-custom&gt;golangci-lint-custom-plugins-action&lt;/a&gt;&#xA;should tide you over until the official Action &lt;a href=https://github.com/golangci/golangci-lint-action/issues/1076&gt;supports&#xA;the same&lt;/a&gt;. This alternative Action follows the steps diagrammed&#xA;above: download &lt;code&gt;golangci-lint&lt;/code&gt;, use it to build a customized&#xA;version of itself, then run that customized version. Like the official&#xA;Action, this one persists linter binaries and caches to save time.&lt;h1 id=getting-back-to-basics&gt;Getting back to basics&lt;/h1&gt;&lt;p&gt;The foregoing complexity stems from an unforced but fundamental&#xA;aspect of &lt;code&gt;golangci-lint&lt;/code&gt;’s design: the decision to ship a&#xA;binary, not a library, customizable through a baroque system of&#xA;configuration files rather than as code. The underlying code here, the&#xA;analyzers themselves, have reasonable APIs with unambiguous&#xA;documentation.&lt;p&gt;Consider the problem of determining whether a linter config is valid.&#xA;To ship a closed binary, &lt;code&gt;golangci-lint&lt;/code&gt; reads configuration&#xA;from a file — typically &lt;code&gt;.golangci.yml&lt;/code&gt; — with no&#xA;application-specific types. This is a big declarative hullaballoo for a&#xA;process that, when you configure an &lt;code&gt;Analyzer&lt;/code&gt; imperatively&#xA;in Go, is entirely guaranteed by the type system.&lt;p&gt;We can — and should — return our focus from the tool to the code. We&#xA;should consider custom extensibility a first-class priority in a&#xA;metalinter.&lt;p&gt;You should be able to set up a linter to your Go project by adding a&#xA;standalone &lt;code&gt;cmd/lint/main.go&lt;/code&gt; “metalinter” program:&lt;ol type=1&gt;&lt;li&gt;&lt;p&gt;Explicitly imports and constructs only the analyzers you want to&#xA;run, using their exported types to configure them.&lt;li&gt;&lt;p&gt;Runs those analyzers as a group, outputting their diagnostics for&#xA;consumption by other tools.&lt;/ol&gt;&lt;p&gt;Why are we all still using &lt;code&gt;golangci-lint&lt;/code&gt;?&lt;p&gt;The most important answer is &lt;em&gt;performance&lt;/em&gt;, and the most&#xA;significant factor is &lt;code&gt;golangci-lint&lt;/code&gt;’s cache: if a package&#xA;hasn’t changed, neither have the lint results. The metalinter stores&#xA;per-package results indexed by hashes of the package contents. Luckily,&#xA;there’s nothing really special going on: &lt;code&gt;golangci-lint&lt;/code&gt;&#xA;forks the &lt;a href=https://github.com/golangci/golangci-lint/blob/master/internal/go/cache/readme.md&gt;native&#xA;Go build cache&lt;/a&gt; and &lt;a href=https://github.com/golangci/golangci-lint/blob/master/internal/cache/cache.go&gt;wraps&#xA;it&lt;/a&gt; in package-hashing logic.&lt;p&gt;Less importantly, &lt;code&gt;golangci-lint&lt;/code&gt; has a first-movers&#xA;advantage: it filled the vacuum left by &lt;code&gt;go lint&lt;/code&gt;’s 2020&#xA;deprecation, and Go engineers have been building (and squabbling over)&#xA;its gatekept linter-set ever since.&lt;h2 id=an-experimental-metalinter-glint&gt;An experimental metalinter:&#xA;&lt;code&gt;glint&lt;/code&gt;&lt;/h2&gt;&lt;p&gt;In &lt;a href=https://github.com/lukasschwab/glint&gt;&lt;code&gt;glint&lt;/code&gt;&lt;/a&gt;, I’m&#xA;solving those problems for myself. That project aims to provide a&#xA;minimal harness for running sets of analyzers defined in Go, with&lt;ul&gt;&lt;li&gt;&lt;em&gt;Very simple customization&lt;/em&gt;&lt;li&gt;Reasonable performance&lt;li&gt;Reasonable compatibility with CI environments and editors&lt;li&gt;No registration requirements; just the&#xA;&lt;code&gt;analysis.Analyzer&lt;/code&gt; type&lt;/ul&gt;&lt;p&gt;Focusing on &lt;code&gt;analysis.Analyzer&lt;/code&gt; means drop-in&#xA;compatibility with the major &lt;code&gt;golangci-lint&lt;/code&gt; linters: &lt;a href=https://github.com/lukasschwab/glint/blob/main/pkg/golangci/defaults.go&gt;pkg/golangci&lt;/a&gt;&#xA;describes them, and &lt;a href=https://github.com/lukasschwab/glint/blob/main/cmd/glint/glint.go&gt;cmd/glint&lt;/a&gt;&#xA;demos wrapping them in the harness.&lt;p&gt;Adding a custom linter is a matter of defining an analyzer and adding&#xA;it to the list passed to &lt;code&gt;glint.Main&lt;/code&gt;. These programs work&#xA;with VS Code and &lt;code&gt;go vet -vettool&lt;/code&gt;, and yield annotations in&#xA;GitHub Actions.&lt;dl&gt;&lt;dt&gt;Why use &lt;code&gt;glint&lt;/code&gt; over&#xA;&lt;code&gt;analysis/multichecker&lt;/code&gt;?&lt;dd&gt;&lt;code&gt;multichecker&lt;/code&gt; is the package Go ships to help you solve this&#xA;metalinter problem: “[t]his package makes it easy for anyone to build an&#xA;analysis tool containing just the analyzers they need.”&lt;dd&gt;Unfortunately, &lt;code&gt;analysis/multichecker&lt;/code&gt; emits diagnostics to&#xA;&lt;code&gt;os.Stderr&lt;/code&gt;, which VS Code takes as an indication the lint&#xA;run is &lt;em&gt;erroring&lt;/em&gt; rather than revealing issues. Its public API&#xA;also isn’t easy to extend — it calls &lt;code&gt;os.Exit&lt;/code&gt; and depends on&#xA;a complex group of internal packages.&lt;dt&gt;What’s next for &lt;code&gt;glint&lt;/code&gt;?&lt;dd&gt;Improved performance with caching, especially for incremental runs on&#xA;large project (near-instantaneous for &lt;code&gt;golangci-lint&lt;/code&gt;).&lt;dd&gt;&lt;a href=https://github.com/golang/go/issues/53014&gt;Storing&#xA;&lt;code&gt;go vet&lt;/code&gt; results&lt;/a&gt; in the Go build cache and stressing&#xA;usage with &lt;code&gt;go vet -vettool&lt;/code&gt; is an interesting idea, but&#xA;might require new workarounds for use with VS Code.&lt;/dl&gt;&lt;h1 id=references&gt;References&lt;/h1&gt;&lt;table&gt;&lt;col style=&#34;width: 29%&#34;&gt;&lt;col style=&#34;width: 70%&#34;&gt;&lt;thead&gt;&lt;tr class=header&gt;&lt;th style=&#34;text-align: left;&#34;&gt;Reference&lt;th style=&#34;text-align: left;&#34;&gt;Description&lt;tbody&gt;&lt;tr class=odd&gt;&lt;td style=&#34;text-align: left;&#34;&gt;&lt;a href=https://pkg.go.dev/golang.org/x/tools/go/analysis&gt;&lt;code&gt;analysis&lt;/code&gt;&lt;/a&gt;&lt;td style=&#34;text-align: left;&#34;&gt;Go’s static analysis builtins:&#xA;&lt;code&gt;Analyzer&lt;/code&gt;, &lt;code&gt;singlechecker&lt;/code&gt;, etc. Strong&#xA;docs.&lt;tr class=even&gt;&lt;td style=&#34;text-align: left;&#34;&gt;&lt;a href=https://github.com/lukasschwab/nilinterface&gt;&lt;code&gt;nilinterface&lt;/code&gt;&lt;/a&gt;&lt;td style=&#34;text-align: left;&#34;&gt;A model custom linter.&lt;tr class=odd&gt;&lt;td style=&#34;text-align: left;&#34;&gt;&lt;a href=https://github.com/lukasschwab/golangci-lint-custom-plugins-action&gt;&lt;code&gt;golangci-lint-custom-plugins-action&lt;/code&gt;&lt;/a&gt;&lt;td style=&#34;text-align: left;&#34;&gt;GitHub Action for building, caching, and&#xA;running &lt;code&gt;golangci-lint&lt;/code&gt; with a custom linter.&lt;tr class=even&gt;&lt;td style=&#34;text-align: left;&#34;&gt;&lt;a href=https://github.com/lukasschwab/tiir/commit/5fe5d3069f5d173b8ed484eac559b58f546bc70a&gt;&lt;code&gt;tiir&lt;/code&gt;&lt;span class=citation data-cites=5fe5d30&gt;@5fe5d30&lt;/span&gt;&lt;/a&gt;&lt;td style=&#34;text-align: left;&#34;&gt;Diff upgrading a project from standard&#xA;&lt;code&gt;golangci-lint&lt;/code&gt; to a custom &lt;code&gt;golangci-lint&lt;/code&gt;.&lt;tr class=odd&gt;&lt;td style=&#34;text-align: left;&#34;&gt;&lt;a href=https://github.com/lukasschwab/glint&gt;&lt;code&gt;glint&lt;/code&gt;&lt;/a&gt;&lt;td style=&#34;text-align: left;&#34;&gt;An experimental Go-defined&#xA;metalinter.&lt;/table&gt;&lt;aside id=footnotes class=&#34;footnotes footnotes-end-of-document&#34; role=doc-endnotes&gt;&lt;hr&gt;&lt;ol&gt;&lt;li id=fn1&gt;&lt;p&gt;See e.g. the maintainer’s &lt;a href=https://github.com/golangci/golangci-lint/issues/4045#issuecomment-1695384402&gt;categorical&#xA;prohibition&lt;/a&gt; of warning-style linters like &lt;code&gt;nilaway&lt;/code&gt;.&lt;a href=#fnref1 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn2&gt;&lt;p&gt;And it &lt;em&gt;is&lt;/em&gt; usually YAML. The &lt;a href=https://golangci-lint.run/usage/configuration/#config-file&gt;docs&lt;/a&gt;&#xA;say you can opt for TOML or JSON; few public projects on GitHub actually&#xA;do:&lt;div style=&#34;max-width: 350px; margin: 0 2em 0 2em;&#34;&gt;&lt;table&gt;&lt;col style=&#34;width: 13%&#34;&gt;&lt;col style=&#34;width: 86%&#34;&gt;&lt;thead&gt;&lt;tr class=header&gt;&lt;th style=&#34;text-align: left;&#34;&gt;Filename&lt;th style=&#34;text-align: right;&#34;&gt;Count on GitHub, Feb. 2025&lt;tbody&gt;&lt;tr class=odd&gt;&lt;td style=&#34;text-align: left;&#34;&gt;.golangci.yml&lt;td style=&#34;text-align: right;&#34;&gt;&lt;a href=&#34;https://github.com/search?q=path%3A%2F%28%5E%7C%5C%2F%29.golangci%5C.yml%24%2F&amp;amp;type=code&#34;&gt;27,700&lt;/a&gt;&lt;tr class=even&gt;&lt;td style=&#34;text-align: left;&#34;&gt;.golangci.yaml&lt;td style=&#34;text-align: right;&#34;&gt;&lt;a href=&#34;https://github.com/search?q=path%3A%2F%28%5E%7C%5C%2F%29.golangci%5C.yaml%24%2F&amp;amp;type=code&#34;&gt;6,600&lt;/a&gt;&lt;tr class=odd&gt;&lt;td style=&#34;text-align: left;&#34;&gt;.golangci.toml&lt;td style=&#34;text-align: right;&#34;&gt;&lt;a href=&#34;https://github.com/search?q=path%3A%2F%28%5E%7C%5C%2F%29.golangci%5C.toml%24%2F&amp;amp;type=code&#34;&gt;624&lt;/a&gt;&lt;tr class=even&gt;&lt;td style=&#34;text-align: left;&#34;&gt;.golangci.json&lt;td style=&#34;text-align: right;&#34;&gt;&lt;a href=&#34;https://github.com/search?q=path%3A%2F%28%5E%7C%5C%2F%29.golangci%5C.json%24%2F&amp;amp;type=code&#34;&gt;200&lt;/a&gt;&lt;/table&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;golangci-lint&lt;/code&gt; examples are all &lt;em&gt;provided&lt;/em&gt; in&#xA;YAML, of course.&lt;a href=#fnref2 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn3&gt;&lt;p&gt;The new module plugin system, introduced March 2024, &lt;a href=https://caddyserver.com/docs/extending-caddy&gt;mimics&lt;/a&gt; a pattern&#xA;introduced by the Go webserver project &lt;code&gt;caddy&lt;/code&gt;. The older Go&#xA;plugins seem prohibitively awkward.&lt;a href=#fnref3 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn4&gt;&lt;p&gt;The &lt;code&gt;golangci-lint&lt;/code&gt; issues include&#xA;discussions of the impact of load modes: &lt;a href=https://github.com/golangci/golangci-lint/issues/1810&gt;here&lt;/a&gt;&#xA;are graphs of the different linter runtimes.&lt;p&gt;These are proxy values for &lt;a href=https://pkg.go.dev/golang.org/x/tools/go/packages#LoadMode&gt;&lt;code&gt;packages.LoadMode&lt;/code&gt;&lt;/a&gt;&#xA;settings, which determine &lt;em&gt;what&lt;/em&gt; information about a package is&#xA;loaded for analysis. Performance suffers if you load unused data.&lt;a href=#fnref4 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;/ol&gt;&lt;/aside&gt;</content>
    <link href="https://lukasschwab.me/blog/gen/bring-your-own-linter.html" rel="alternate"></link>
    <summary type="html">A practical guide for integrating a custom linter into your Go project, a screed against `golangci-lint`, and a defense of a simpler Go metalinter I&#39;m experimenting with: `glint`.</summary>
  </entry>
  <entry>
    <title>Deriving Safe ID Types in Go</title>
    <updated>2025-02-01T00:00:00Z</updated>
    <id>https://lukasschwab.me/blog/gen/deriving-safe-id-types-in-go.html</id>
    <content type="html">&lt;html xmlns=http://www.w3.org/1999/xhtml lang xml:lang&gt;&lt;meta charset=utf-8&gt;&lt;meta name=generator content=&#34;pandoc&#34;&gt;&lt;meta name=viewport content=&#34;width=device-width,initial-scale=1,user-scalable=yes&#34;&gt;&lt;meta name=dcterms.date content=&#34;2025-02-01&#34;&gt;&lt;title&gt;Deriving Safe ID Types in Go&lt;/title&gt;&lt;style&gt;&#xA;      code{white-space: pre-wrap;}&#xA;      span.smallcaps{font-variant: small-caps;}&#xA;      span.underline{text-decoration: underline;}&#xA;      div.column{display: inline-block; vertical-align: top; width: 50%;}&#xA;          &lt;/style&gt;&lt;style&gt;pre &gt; code.sourceCode { white-space: pre; position: relative; }&#xA;pre &gt; code.sourceCode &gt; span { display: inline-block; line-height: 1.25; }&#xA;pre &gt; code.sourceCode &gt; span:empty { height: 1.2em; }&#xA;.sourceCode { overflow: visible; }&#xA;code.sourceCode &gt; span { color: inherit; text-decoration: inherit; }&#xA;div.sourceCode { margin: 1em 0; }&#xA;pre.sourceCode { margin: 0; }&#xA;@media screen {&#xA;div.sourceCode { overflow: auto; }&#xA;}&#xA;@media print {&#xA;pre &gt; code.sourceCode { white-space: pre-wrap; }&#xA;pre &gt; code.sourceCode &gt; span { text-indent: -5em; padding-left: 5em; }&#xA;}&#xA;pre.numberSource code&#xA;  { counter-reset: source-line 0; }&#xA;pre.numberSource code &gt; span&#xA;  { position: relative; left: -4em; counter-increment: source-line; }&#xA;pre.numberSource code &gt; span &gt; a:first-child::before&#xA;  { content: counter(source-line);&#xA;    position: relative; left: -1em; text-align: right; vertical-align: baseline;&#xA;    border: none; display: inline-block;&#xA;    -webkit-touch-callout: none; -webkit-user-select: none;&#xA;    -khtml-user-select: none; -moz-user-select: none;&#xA;    -ms-user-select: none; user-select: none;&#xA;    padding: 0 4px; width: 4em;&#xA;    color: #aaaaaa;&#xA;  }&#xA;pre.numberSource { margin-left: 3em; border-left: 1px solid #aaaaaa;  padding-left: 4px; }&#xA;div.sourceCode&#xA;  {   }&#xA;@media screen {&#xA;pre &gt; code.sourceCode &gt; span &gt; a:first-child::before { text-decoration: underline; }&#xA;}&#xA;code span.al { color: #ff0000; font-weight: bold; } /* Alert */&#xA;code span.an { color: #60a0b0; font-weight: bold; font-style: italic; } /* Annotation */&#xA;code span.at { color: #7d9029; } /* Attribute */&#xA;code span.bn { color: #40a070; } /* BaseN */&#xA;code span.bu { color: #008000; } /* BuiltIn */&#xA;code span.cf { color: #007020; font-weight: bold; } /* ControlFlow */&#xA;code span.ch { color: #4070a0; } /* Char */&#xA;code span.cn { color: #880000; } /* Constant */&#xA;code span.co { color: #60a0b0; font-style: italic; } /* Comment */&#xA;code span.cv { color: #60a0b0; font-weight: bold; font-style: italic; } /* CommentVar */&#xA;code span.do { color: #ba2121; font-style: italic; } /* Documentation */&#xA;code span.dt { color: #902000; } /* DataType */&#xA;code span.dv { color: #40a070; } /* DecVal */&#xA;code span.er { color: #ff0000; font-weight: bold; } /* Error */&#xA;code span.ex { } /* Extension */&#xA;code span.fl { color: #40a070; } /* Float */&#xA;code span.fu { color: #06287e; } /* Function */&#xA;code span.im { color: #008000; font-weight: bold; } /* Import */&#xA;code span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Information */&#xA;code span.kw { color: #007020; font-weight: bold; } /* Keyword */&#xA;code span.op { color: #666666; } /* Operator */&#xA;code span.ot { color: #007020; } /* Other */&#xA;code span.pp { color: #bc7a00; } /* Preprocessor */&#xA;code span.sc { color: #4070a0; } /* SpecialChar */&#xA;code span.ss { color: #bb6688; } /* SpecialString */&#xA;code span.st { color: #4070a0; } /* String */&#xA;code span.va { color: #19177c; } /* Variable */&#xA;code span.vs { color: #4070a0; } /* VerbatimString */&#xA;code span.wa { color: #60a0b0; font-weight: bold; font-style: italic; } /* Warning */&lt;/style&gt;&lt;link rel=stylesheet href=../styles/common.css&gt;&lt;link rel=stylesheet href=../styles/fence-extensions/centertext.css&gt;&lt;link rel=stylesheet href=../styles/fence-extensions/firstcoltable.css&gt;&lt;link rel=stylesheet href=../styles/fence-extensions/widetable.css&gt;&lt;script data-goatcounter=https://lukasschwab.goatcounter.com/count async src=//gc.zgo.at/count.js&gt;&lt;/script&gt;&lt;a href=../index.html&gt;&amp;#171; blog&lt;/a&gt; &lt;span class=date&gt;2025-02-01&lt;/span&gt;&lt;header id=title-block-header&gt;&lt;h1 class=title&gt;Deriving Safe ID Types in Go&lt;/h1&gt;&lt;/header&gt;&lt;nav id=TOC role=doc-toc&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=#assumptions id=toc-assumptions&gt;Assumptions&lt;/a&gt;&lt;li&gt;&lt;a href=#defining-a-new-string-type id=toc-defining-a-new-string-type&gt;Defining a new &lt;code&gt;string&lt;/code&gt;&#xA;type&lt;/a&gt;&lt;li&gt;&lt;a href=#embedding-uuid.uuid id=toc-embedding-uuid.uuid&gt;Embedding &lt;code&gt;uuid.UUID&lt;/code&gt;&lt;/a&gt;&lt;li&gt;&lt;a href=#defining-a-new-uuid.uuid-type id=toc-defining-a-new-uuid.uuid-type&gt;Defining a new&#xA;&lt;code&gt;uuid.UUID&lt;/code&gt; type&lt;/a&gt;&lt;li&gt;&lt;a href=#aliasing-uuid.uuid id=toc-aliasing-uuid.uuid&gt;Aliasing&#xA;&lt;code&gt;uuid.UUID&lt;/code&gt;&lt;/a&gt;&lt;li&gt;&lt;a href=#summary id=toc-summary&gt;Summary&lt;/a&gt;&lt;/ul&gt;&lt;/nav&gt;&lt;div class=centertext&gt;&lt;p&gt;&lt;em&gt;This is entirely my advice, but I realized it while contracting&#xA;for &lt;a href=https://alpaca.markets/&gt;Alpaca&lt;/a&gt; —&lt;br&gt;special thanks&#xA;to that team for their patience and good ideas.&lt;/em&gt;&lt;/div&gt;&lt;p&gt;Suppose you’re applying &lt;a href=./safe-incompatibility.html&gt;&lt;em&gt;Safety Through&#xA;Incompatibility&lt;/em&gt;&lt;/a&gt; in Widget Corp.’s widget fulfillment system.&#xA;You want compile-time incompatible ID types for different collections,&#xA;but you don’t have the luxury of starting from scratch: Widget Corp made&#xA;the sensible&lt;a href=#fn1 class=footnote-ref id=fnref1 role=doc-noteref&gt;&lt;sup&gt;1&lt;/sup&gt;&lt;/a&gt; decision to use &lt;a href=https://en.wikipedia.org/wiki/Universally_unique_identifier&gt;random&#xA;UUIDs&lt;/a&gt; for all their canonical identifiers years ago, and has&#xA;thousands of those identifiers in the wild. What’s the lightest-touch&#xA;way to bring that living system up to speed?&lt;p&gt;Whereas my post on “Safety Through Incompatibility” was largely&#xA;language-agnostic, this one will focus on a language-specific example:&#xA;switching to UUID-based typed IDs &lt;em&gt;in Go,&lt;/em&gt; using Google’s &lt;a href=https://pkg.go.dev/github.com/google/uuid#UUID&gt;&lt;code&gt;uuid.UUID&lt;/code&gt;&lt;/a&gt;.&#xA;There are several ways to do the same thing — or so it seems — and I&#xA;found scant guidance online on how to choose between them!&lt;h1 id=assumptions&gt;Assumptions&lt;/h1&gt;&lt;p&gt;You already use UUIDs as IDs. You generate IDs for new records, but&#xA;also exchange records with the outside world: customers order widgets&#xA;through a JSON API, and you record widget IDs in your logs for debugging&#xA;and accounting. Thus, in addition to the intrinsic safety of type&#xA;incompatibility (that a &lt;code&gt;Widget&lt;/code&gt; ID be unusable where an&#xA;&lt;code&gt;Order&lt;/code&gt; ID is needed), you want three things from your ID&#xA;type:&lt;ul&gt;&lt;li&gt;&lt;p&gt;In JSON, your IDs should &lt;code&gt;json.Marshal&lt;/code&gt; to the&#xA;standard UUID format: a hexadecimal JSON string like&#xA;&lt;code&gt;&#34;f3f2e850-b5d4-11ef-ac7e-96584d5248b2&#34;&lt;/code&gt;.&lt;li&gt;&lt;p&gt;They should have the same format in your plaintext logs.&lt;a href=#fn2 class=footnote-ref id=fnref2 role=doc-noteref&gt;&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt;&lt;li&gt;&lt;p&gt;&lt;code&gt;json.Unmarshal&lt;/code&gt; should extract IDs from the same&#xA;format, e.g. when you parse an API request body.&lt;/ul&gt;&lt;p&gt;I capture these requirements in &lt;a href=https://github.com/lukasschwab/aliasing-uuids/blob/main/main_test.go&gt;this&#xA;file&lt;/a&gt; of tests.&lt;h1 id=defining-a-new-string-type&gt;Defining a new &lt;code&gt;string&lt;/code&gt;&#xA;type&lt;/h1&gt;&lt;p&gt;If you’re using raw &lt;code&gt;string&lt;/code&gt;s to represent your UUID IDs,&#xA;switching to a new type defined by &lt;code&gt;string&lt;/code&gt; is dead-simple:&#xA;serialization &lt;em&gt;works out of the box.&lt;/em&gt;&lt;div class=sourceCode id=cb1&gt;&lt;pre class=&#34;sourceCode go&#34;&gt;&lt;code class=&#34;sourceCode go&#34;&gt;&lt;span id=cb1-1&gt;&lt;a href=#cb1-1 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=kw&gt;type&lt;/span&gt; ID &lt;span class=dt&gt;string&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Update the places you &lt;em&gt;construct&lt;/em&gt; IDs — something like&#xA;&lt;code&gt;widget.ID(uuid.New().String())&lt;/code&gt; — and &lt;em&gt;boom&lt;/em&gt;, the&#xA;widget fulfillment system has a dedicated widget ID type.&lt;p&gt;The downside is a &lt;em&gt;different&lt;/em&gt; aspect of ID safety: not&#xA;incompatibility, but validity. You might be &lt;em&gt;really confident&lt;/em&gt;&#xA;all your IDs are UUIDs, but (as for the raw &lt;code&gt;string&lt;/code&gt; type you&#xA;started with) there’s no type guarantee they always will be. Maybe you&#xA;have a test that pretends &lt;code&gt;&#34;MY-PET-ID&#34;&lt;/code&gt; is a valid UUID.&#xA;Maybe you’ll get nonsense data over the wire. You can call&#xA;&lt;code&gt;uuid.Parse&lt;/code&gt; to validate your strings, but that’s a secondary&#xA;process — you have to &lt;em&gt;remember&lt;/em&gt; to do it — and one you can’t&#xA;quite push to the system boundary.&lt;p&gt;It’s easier to call a spade a spade: just use &lt;code&gt;uuid.UUID&lt;/code&gt;&#xA;for UUIDs.&lt;h1 id=embedding-uuid.uuid&gt;Embedding &lt;code&gt;uuid.UUID&lt;/code&gt;&lt;/h1&gt;&lt;div class=sourceCode id=cb2&gt;&lt;pre class=&#34;sourceCode go&#34;&gt;&lt;code class=&#34;sourceCode go&#34;&gt;&lt;span id=cb2-1&gt;&lt;a href=#cb2-1 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=kw&gt;type&lt;/span&gt; ID &lt;span class=kw&gt;struct&lt;/span&gt; &lt;span class=op&gt;{&lt;/span&gt; uuid&lt;span class=op&gt;.&lt;/span&gt;UUID &lt;span class=op&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This exemplifies a Go technique called &lt;a href=https://gobyexample.com/struct-embedding&gt;Struct Embedding&lt;/a&gt;. ID&#xA;is a &lt;code&gt;struct&lt;/code&gt; — Go’s basic composite data type — with a&#xA;single, anonymous component: a UUID.&lt;p&gt;Like aliasing &lt;code&gt;string&lt;/code&gt;, serialization &lt;em&gt;just works.&lt;/em&gt;&#xA;Unlike aliasing &lt;code&gt;string&lt;/code&gt;, UUID validity is enforced at the&#xA;system edge: &lt;code&gt;json.Unmarshal&lt;/code&gt; will error if you provide a&#xA;string that’s too long, or too short, or includes UUID-incompatible&#xA;characters.&lt;p&gt;To tell you the truth, my problems with a struct-embedding ID like&#xA;this are all nitpicky. For one, the underlying &lt;code&gt;uuid.UUID&lt;/code&gt; is&#xA;a public field; you can access it (e.g. access&#xA;&lt;code&gt;widgetID.UUID&lt;/code&gt;). Strictly speaking, that kind of conversion&#xA;back into a &lt;code&gt;uuid.UUID&lt;/code&gt; is possible for &lt;em&gt;all&lt;/em&gt; the&#xA;approaches I describe here, but it’s a conversion to generally&#xA;discourage: it’s a leaky abstraction, and it allows comparing&#xA;incompatible UUID-derived IDs &lt;em&gt;as if&lt;/em&gt; they were plain&#xA;&lt;code&gt;uuid.UUID&lt;/code&gt;s. Why dangle a public field if you don’t want&#xA;anyone to use it?&lt;p&gt;Moreover — and this is &lt;em&gt;really&lt;/em&gt; a matter of taste — the&#xA;construction &lt;code class=&#34;sourceCode go&#34;&gt;ID&lt;span class=op&gt;{&lt;/span&gt;uuid&lt;span class=op&gt;.&lt;/span&gt;New&lt;span class=op&gt;()}&lt;/span&gt;&lt;/code&gt;&#xA;just &lt;em&gt;feels&lt;/em&gt; wrong. Are there more (unset) fields in the&#xA;&lt;code&gt;ID&lt;/code&gt; struct? Single-field struct embedding uses a nonatomic&#xA;&lt;em&gt;composite&lt;/em&gt; data type to represent a conceptually atomic&#xA;identifier.&lt;h1 id=defining-a-new-uuid.uuid-type&gt;Defining a new&#xA;&lt;code&gt;uuid.UUID&lt;/code&gt; type&lt;/h1&gt;&lt;div class=sourceCode id=cb3&gt;&lt;pre class=&#34;sourceCode go&#34;&gt;&lt;code class=&#34;sourceCode go&#34;&gt;&lt;span id=cb3-1&gt;&lt;a href=#cb3-1 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=kw&gt;type&lt;/span&gt; ID uuid&lt;span class=op&gt;.&lt;/span&gt;UUID&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Like the &lt;code&gt;type ID string&lt;/code&gt; declaration considered earlier,&#xA;type definition creates a wholly new type — &lt;code&gt;ID&lt;/code&gt; — with the&#xA;same memory layout as &lt;code&gt;uuid.UUID&lt;/code&gt;. In fact,&#xA;&lt;code&gt;uuid.UUID&lt;/code&gt; &lt;em&gt;itself&lt;/em&gt; is defined through a construction&#xA;like this: &lt;a href=https://github.com/google/uuid/blob/v1.6.0/uuid.go#L20&gt;&lt;code&gt;type uuid.UUID [16]byte&lt;/code&gt;&lt;/a&gt;.&#xA;Unlike in a public struct-embedding, the underlying type isn’t&#xA;exposed.&lt;p&gt;The main draback here is the serialization. Out of the box, this&#xA;thing serializes like a &lt;code&gt;[16]byte&lt;/code&gt;: expect JSON like&#xA;&lt;code&gt;&#34;[243,242,232,80,181,212,17,239,172,126,150,88,77,82,72,178]&#34;&lt;/code&gt;&#xA;instead of anything recognizable.&lt;p&gt;You solve this by explicitly reimplementing some common single-method&#xA;interfaces already implemented by &lt;code&gt;uuid.UUID&lt;/code&gt;, essentially by&#xA;invoking those UUID implementations.&lt;dl&gt;&lt;dt&gt;&lt;code class=&#34;sourceCode go&#34;&gt;&lt;span class=kw&gt;func&lt;/span&gt; &lt;span class=op&gt;(&lt;/span&gt;id ID&lt;span class=op&gt;)&lt;/span&gt; String&lt;span class=op&gt;()&lt;/span&gt; &lt;span class=dt&gt;string&lt;/span&gt; &lt;span class=op&gt;{&lt;/span&gt; &lt;span class=op&gt;...&lt;/span&gt; &lt;span class=op&gt;}&lt;/span&gt;&lt;/code&gt;&lt;dd&gt;Implements &lt;a href=https://pkg.go.dev/fmt#Stringer&gt;&lt;code&gt;fmt.Stringer&lt;/code&gt;&lt;/a&gt;.&lt;dt&gt;&lt;code class=&#34;sourceCode go&#34;&gt;&lt;span class=kw&gt;func&lt;/span&gt; &lt;span class=op&gt;(&lt;/span&gt;id ID&lt;span class=op&gt;)&lt;/span&gt; MarshalJSON&lt;span class=op&gt;()&lt;/span&gt; &lt;span class=op&gt;([]&lt;/span&gt;&lt;span class=dt&gt;byte&lt;/span&gt;&lt;span class=op&gt;,&lt;/span&gt; &lt;span class=dt&gt;error&lt;/span&gt;&lt;span class=op&gt;)&lt;/span&gt; &lt;span class=op&gt;{&lt;/span&gt; &lt;span class=op&gt;...&lt;/span&gt; &lt;span class=op&gt;}&lt;/span&gt;&lt;/code&gt;&lt;dd&gt;Implements &lt;a href=https://pkg.go.dev/encoding/json#Marshaler&gt;&lt;code&gt;json.Marshaler&lt;/code&gt;&lt;/a&gt;.&lt;dt&gt;&lt;code class=&#34;sourceCode go&#34;&gt;&lt;span class=kw&gt;func&lt;/span&gt; &lt;span class=op&gt;(&lt;/span&gt;id &lt;span class=op&gt;*&lt;/span&gt;ID&lt;span class=op&gt;)&lt;/span&gt; UnmarshalJSON&lt;span class=op&gt;(&lt;/span&gt;data &lt;span class=op&gt;[]&lt;/span&gt;&lt;span class=dt&gt;byte&lt;/span&gt;&lt;span class=op&gt;)&lt;/span&gt; &lt;span class=dt&gt;error&lt;/span&gt; &lt;span class=op&gt;{&lt;/span&gt; &lt;span class=op&gt;...&lt;/span&gt; &lt;span class=op&gt;}&lt;/span&gt;&lt;/code&gt;&lt;dd&gt;Implements &lt;a href=https://pkg.go.dev/encoding/json#Unmarshaler&gt;&lt;code&gt;json.Unmarshaler&lt;/code&gt;&lt;/a&gt;.&lt;dd&gt;This implementation may be a little tricky for new Go programmers: you&#xA;have to cast the pointer &lt;code&gt;id&lt;/code&gt; &lt;em&gt;without dereferencing&#xA;it&lt;/em&gt;. &lt;a href=https://github.com/lukasschwab/aliasing-uuids/blob/main/pkg/uuid_type/uuid_type.go#L18-L22&gt;Here’s&#xA;my implementation&lt;/a&gt;.&lt;/dl&gt;&lt;p&gt;These implementations take a little more effort on your part to make&#xA;this approach pass the tests, and the implementations themselves are&#xA;dense little typesystem tongue-twisters. Defining a new&#xA;&lt;code&gt;uuid.UUID&lt;/code&gt; type is less sloppy than struct embedding, but&#xA;maybe we can find something less verbose?&lt;h1 id=aliasing-uuid.uuid&gt;Aliasing &lt;code&gt;uuid.UUID&lt;/code&gt;&lt;/h1&gt;&lt;div class=sourceCode id=cb4&gt;&lt;pre class=&#34;sourceCode go&#34;&gt;&lt;code class=&#34;sourceCode go&#34;&gt;&lt;span id=cb4-1&gt;&lt;a href=#cb4-1 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=kw&gt;type&lt;/span&gt; ID &lt;span class=op&gt;=&lt;/span&gt; uuid&lt;span class=op&gt;.&lt;/span&gt;UUID&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;There’s a subtle difference between declaring a new type using&#xA;another — as in the &lt;code&gt;type ID uuid.UUID&lt;/code&gt; example above — and&#xA;&lt;em&gt;aliasing&lt;/em&gt; a type in Go. Syntactically, the difference is small:&#xA;the &lt;code&gt;=&lt;/code&gt; in the alias declaration. In practice, the difference&#xA;is much bigger.&lt;p&gt;An &lt;em&gt;aliased&lt;/em&gt; type (&lt;code&gt;ID&lt;/code&gt;) preserves the receiver&#xA;methods defined on the original type (&lt;code&gt;uuid.UUID&lt;/code&gt;)! In this&#xA;case, that includes the existing standard interface implementations&#xA;&lt;code&gt;MarshalJSON&lt;/code&gt;, &lt;code&gt;UnmarshalJSON&lt;/code&gt;, and&#xA;&lt;code&gt;String&lt;/code&gt; Google ships. Not only does aliasing&#xA;&lt;code&gt;uuid.UUID&lt;/code&gt; give you built-in validation (unlike defining a&#xA;new &lt;code&gt;string&lt;/code&gt; type); it doesn’t expose a misleading inner&#xA;field (unlike struct-embedding &lt;code&gt;uuid.UUID&lt;/code&gt;); and it&#xA;serializes as expected out of the box (unlike defining a new&#xA;&lt;code&gt;uuid.UUID&lt;/code&gt; type) because it automatically implements the&#xA;necessary interfaces!&lt;p&gt;Where’s the catch? Spot it in this passage from the Go Blog’s &lt;a href=https://go.dev/blog/alias-names&gt;intro to alias&#xA;declarations&lt;/a&gt;:&lt;blockquote&gt;&lt;p&gt;In contrast to a regular type definition&lt;div class=sourceCode id=cb5&gt;&lt;pre class=&#34;sourceCode go&#34;&gt;&lt;code class=&#34;sourceCode go&#34;&gt;&lt;span id=cb5-1&gt;&lt;a href=#cb5-1 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=kw&gt;type&lt;/span&gt; T T0&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;which declares a new type that is never identical to the type on the&#xA;right-hand side of the declaration, an alias declaration&lt;div class=sourceCode id=cb6&gt;&lt;pre class=&#34;sourceCode go&#34;&gt;&lt;code class=&#34;sourceCode go&#34;&gt;&lt;span id=cb6-1&gt;&lt;a href=#cb6-1 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=kw&gt;type&lt;/span&gt; A &lt;span class=op&gt;=&lt;/span&gt; T  &lt;span class=co&gt;// the &amp;quot;=&amp;quot; indicates an alias declaration&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;declares only a new name &lt;code&gt;A&lt;/code&gt; for the type on the&#xA;right-hand side: here, &lt;code&gt;A&lt;/code&gt; and &lt;code&gt;T&lt;/code&gt; denote the same&#xA;and thus identical type &lt;code&gt;T&lt;/code&gt;.&lt;p&gt;Alias declarations make it possible to provide a new name (in a new&#xA;package!) for a given type while retaining type identity:&lt;div class=sourceCode id=cb7&gt;&lt;pre class=&#34;sourceCode go&#34;&gt;&lt;code class=&#34;sourceCode go&#34;&gt;&lt;span id=cb7-1&gt;&lt;a href=#cb7-1 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=kw&gt;package&lt;/span&gt; pkg2&lt;/span&gt;&#xA;&lt;span id=cb7-2&gt;&lt;a href=#cb7-2 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;/span&gt;&#xA;&lt;span id=cb7-3&gt;&lt;a href=#cb7-3 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=kw&gt;import&lt;/span&gt; &lt;span class=st&gt;&amp;quot;path/to/pkg1&amp;quot;&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb7-4&gt;&lt;a href=#cb7-4 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;/span&gt;&#xA;&lt;span id=cb7-5&gt;&lt;a href=#cb7-5 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=kw&gt;type&lt;/span&gt; T &lt;span class=op&gt;=&lt;/span&gt; pkg1&lt;span class=op&gt;.&lt;/span&gt;T&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The type name has changed from &lt;code&gt;pkg1.T&lt;/code&gt; to&#xA;&lt;code&gt;pkg2.T&lt;/code&gt; but values of type &lt;code&gt;pkg2.T&lt;/code&gt; have the same&#xA;type as variables of type &lt;code&gt;pkg1.T&lt;/code&gt;.&lt;/blockquote&gt;&lt;p&gt;In simpler terms, a type alias &lt;em&gt;is not a new type&lt;/em&gt; — it’s&#xA;entirely compatible with the old one, and with &lt;em&gt;other aliases of the&#xA;same original type.&lt;/em&gt; Remember the original mission: safety through&#xA;incompatibility! If you can interchange your &lt;code&gt;widget.ID&lt;/code&gt;s and&#xA;&lt;code&gt;order.ID&lt;/code&gt;s, you haven’t made anything safer.&lt;h1 id=summary&gt;Summary&lt;/h1&gt;&lt;p&gt;I introduced these approaches naïvely as four equivalent options.&#xA;Divide them instead into two groups: the first, a group of one, is built&#xA;on fundamentally the wrong type (&lt;code&gt;string&lt;/code&gt;); the other&#xA;options, based on &lt;code&gt;uuid.UUID&lt;/code&gt;, have more promise.&lt;p&gt;The type alias is fool’s gold: it’s syntactically minimal&#xA;&lt;em&gt;and&lt;/em&gt; it passes our tests, but it doesn’t prevent&#xA;interoperability.&lt;p&gt;Prefer one of the remaining two. If you’re pressed for time or&#xA;worried about introducing something that &lt;em&gt;looks&lt;/em&gt; complex, the&#xA;struct embedding is a minimal drop-in replacement for raw&#xA;&lt;code&gt;string&lt;/code&gt;s.&lt;p&gt;In a mature codebase, &lt;strong&gt;I prefer defining a new&#xA;&lt;code&gt;uuid.UUID&lt;/code&gt; type&lt;/strong&gt; (&lt;code&gt;type ID uuid.UUID&lt;/code&gt;)&#xA;and implementing the serialization interfaces yourself. These are&#xA;verbose, but you can copy-paste my implementations for starters. You&#xA;might also take advantage of the custom implementations — to prefix your&#xA;logged UUIDs with a type identifier, say, so widget&#xA;&lt;code&gt;f3f2e850-b5d4-11ef-ac7e-96584d5248b2&lt;/code&gt; appears more&#xA;unambiguously as&#xA;&lt;code&gt;Widget_f3f2e850-b5d4-11ef-ac7e-96584d5248b2&lt;/code&gt;.&lt;div class=&#34;firstcoltable widetable&#34;&gt;&lt;table&gt;&lt;col style=&#34;width: 21%&#34;&gt;&lt;col style=&#34;width: 39%&#34;&gt;&lt;col style=&#34;width: 39%&#34;&gt;&lt;thead&gt;&lt;tr class=header&gt;&lt;th style=&#34;text-align: left;&#34;&gt;Approach&lt;th style=&#34;text-align: left;&#34;&gt;Pros&lt;th style=&#34;text-align: left;&#34;&gt;Cons&lt;tbody&gt;&lt;tr class=odd&gt;&lt;td style=&#34;text-align: left;&#34;&gt;Defining a new &lt;code&gt;string&lt;/code&gt; type:&lt;br&gt;&lt;code class=&#34;sourceCode go&#34;&gt;&lt;span class=kw&gt;type&lt;/span&gt; ID &lt;span class=dt&gt;string&lt;/span&gt;&lt;/code&gt;&lt;td style=&#34;text-align: left;&#34;&gt;Passes tests out of the box&lt;td style=&#34;text-align: left;&#34;&gt;No protections against non-UUID&#xA;values&lt;tr class=even&gt;&lt;td style=&#34;text-align: left;&#34;&gt;Embedding &lt;code&gt;uuid.UUID&lt;/code&gt;:&lt;br&gt;&lt;code class=&#34;sourceCode go&#34;&gt;&lt;span class=kw&gt;type&lt;/span&gt; ID &lt;span class=kw&gt;struct&lt;/span&gt; &lt;span class=op&gt;{&lt;/span&gt; uuid&lt;span class=op&gt;.&lt;/span&gt;UUID &lt;span class=op&gt;}&lt;/span&gt;&lt;/code&gt;&lt;td style=&#34;text-align: left;&#34;&gt;Non-UUID values are impossible&lt;br&gt;Passes&#xA;tests out of the box&lt;td style=&#34;text-align: left;&#34;&gt;Exposes the embedded&#xA;&lt;code&gt;uuid.UUID&lt;/code&gt; implementation&lt;tr class=odd&gt;&lt;td style=&#34;text-align: left;&#34;&gt;Defining a new &lt;code&gt;uuid.UUID&lt;/code&gt;&#xA;type:&lt;br&gt;&lt;code class=&#34;sourceCode go&#34;&gt;&lt;span class=kw&gt;type&lt;/span&gt; ID uuid&lt;span class=op&gt;.&lt;/span&gt;UUID&lt;/code&gt;&lt;td style=&#34;text-align: left;&#34;&gt;Non-UUID values are impossible&lt;td style=&#34;text-align: left;&#34;&gt;Must reimplement&#xA;&lt;code&gt;json.Marshaler&lt;/code&gt;, &lt;code&gt;json.Unmarshaler&lt;/code&gt;, and&#xA;&lt;code&gt;fmt.Stringer&lt;/code&gt;&lt;tr class=even&gt;&lt;td style=&#34;text-align: left;&#34;&gt;Aliasing &lt;code&gt;uuid.UUID&lt;/code&gt;:&lt;br&gt;&lt;code class=&#34;sourceCode go&#34;&gt;&lt;span class=kw&gt;type&lt;/span&gt; ID &lt;span class=op&gt;=&lt;/span&gt; uuid&lt;span class=op&gt;.&lt;/span&gt;UUID&lt;/code&gt;&lt;td style=&#34;text-align: left;&#34;&gt;Non-UUID values are impossible&lt;br&gt;Passes&#xA;tests out of the box&lt;td style=&#34;text-align: left;&#34;&gt;Dangerous compatibility!&lt;/table&gt;&lt;/div&gt;&lt;p&gt;You can find implementations and tests for all the approaches&#xA;described here &lt;a href=https://github.com/lukasschwab/aliasing-uuids&gt;on GitHub&lt;/a&gt;. The&#xA;root testfile demonstrates each solution satisfies the serialization&#xA;properties; swap &lt;code&gt;widget&lt;/code&gt; imports to run the same tests&#xA;against each. Two more parts to note:&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;a href=https://github.com/lukasschwab/aliasing-uuids/blob/main/pkg/uuid_alias/uuid_alias_test.go&gt;uuid_alias_test.go&lt;/a&gt;&#xA;demonstrates the type-compatibility — and, therefore, the danger — of&#xA;true type-aliased IDs.&lt;li&gt;&lt;p&gt;&lt;a href=https://github.com/lukasschwab/aliasing-uuids/tree/main/pkg/generic_uuid_type&gt;pkg/generic_uuid_type&lt;/a&gt;&#xA;is a generic adaptation of the verbose &lt;code&gt;type ID uuid.UUID&lt;/code&gt;&#xA;approach I prefer. It implements the interfaces once (the generic&#xA;&lt;code&gt;T&lt;/code&gt; provides incompatibility) for any number of&#xA;collections.&lt;/ul&gt;&lt;aside id=footnotes class=&#34;footnotes footnotes-end-of-document&#34; role=doc-endnotes&gt;&lt;hr&gt;&lt;ol&gt;&lt;li id=fn1&gt;&lt;p&gt;See &lt;em&gt;Safety Through Incompatibility&lt;/em&gt;’s &lt;a href=./safe-incompatibility.html#fn4&gt;note&lt;/a&gt; on the intrinsic safety&#xA;of UUIDs.&lt;a href=#fnref1 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn2&gt;&lt;p&gt;This assumes your plaintext logger renders values with&#xA;the standard &lt;code&gt;fmt.Stringer&lt;/code&gt; interface. Anything based on&#xA;&lt;code&gt;fmt.Sprint&lt;/code&gt; and/or the &lt;code&gt;%v&lt;/code&gt; format directive —&#xA;like &lt;a href=https://github.com/charmbracelet/log&gt;charmbracelet/log&lt;/a&gt;, for&#xA;example — is compatible with the solutions in this post.&lt;a href=#fnref2 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;/ol&gt;&lt;/aside&gt;</content>
    <link href="https://lukasschwab.me/blog/gen/deriving-safe-id-types-in-go.html" rel="alternate"></link>
    <summary type="html">A language-specific exploration of &#34;Safety Through Incompatibility:&#34; how do you turn system of UUIDs into a system of neatly separated collection-specific ID types? There are several ways to &#34;alias&#34; types in Go; I recommend one.</summary>
  </entry>
  <entry>
    <title>In Defense of New Boggle</title>
    <updated>2025-01-18T00:00:00Z</updated>
    <id>https://lukasschwab.me/blog/gen/boggle-experiments.html</id>
    <content type="html">&lt;html xmlns=http://www.w3.org/1999/xhtml lang xml:lang&gt;&lt;meta charset=utf-8&gt;&lt;meta name=generator content=&#34;pandoc&#34;&gt;&lt;meta name=viewport content=&#34;width=device-width,initial-scale=1,user-scalable=yes&#34;&gt;&lt;meta name=dcterms.date content=&#34;2025-01-18&#34;&gt;&lt;title&gt;In Defense of New Boggle&lt;/title&gt;&lt;style&gt;&#xA;      code{white-space: pre-wrap;}&#xA;      span.smallcaps{font-variant: small-caps;}&#xA;      span.underline{text-decoration: underline;}&#xA;      div.column{display: inline-block; vertical-align: top; width: 50%;}&#xA;          &lt;/style&gt;&lt;style&gt;html { -webkit-text-size-adjust: 100%; }&#xA;pre &gt; code.sourceCode { white-space: pre; position: relative; }&#xA;pre &gt; code.sourceCode &gt; span { display: inline-block; line-height: 1.25; }&#xA;pre &gt; code.sourceCode &gt; span:empty { height: 1.2em; }&#xA;.sourceCode { overflow: visible; }&#xA;code.sourceCode &gt; span { color: inherit; text-decoration: inherit; }&#xA;div.sourceCode { margin: 1em 0; }&#xA;pre.sourceCode { margin: 0; }&#xA;@media screen {&#xA;div.sourceCode { overflow: auto; }&#xA;}&#xA;@media print {&#xA;pre &gt; code.sourceCode { white-space: pre-wrap; }&#xA;pre &gt; code.sourceCode &gt; span { text-indent: -5em; padding-left: 5em; }&#xA;}&#xA;pre.numberSource code&#xA;  { counter-reset: source-line 0; }&#xA;pre.numberSource code &gt; span&#xA;  { position: relative; left: -4em; counter-increment: source-line; }&#xA;pre.numberSource code &gt; span &gt; a:first-child::before&#xA;  { content: counter(source-line);&#xA;    position: relative; left: -1em; text-align: right; vertical-align: baseline;&#xA;    border: none; display: inline-block;&#xA;    -webkit-touch-callout: none; -webkit-user-select: none;&#xA;    -khtml-user-select: none; -moz-user-select: none;&#xA;    -ms-user-select: none; user-select: none;&#xA;    padding: 0 4px; width: 4em;&#xA;    color: #aaaaaa;&#xA;  }&#xA;pre.numberSource { margin-left: 3em; border-left: 1px solid #aaaaaa;  padding-left: 4px; }&#xA;div.sourceCode&#xA;  {   }&#xA;@media screen {&#xA;pre &gt; code.sourceCode &gt; span &gt; a:first-child::before { text-decoration: underline; }&#xA;}&#xA;code span.al { color: #ff0000; font-weight: bold; } /* Alert */&#xA;code span.an { color: #60a0b0; font-weight: bold; font-style: italic; } /* Annotation */&#xA;code span.at { color: #7d9029; } /* Attribute */&#xA;code span.bn { color: #40a070; } /* BaseN */&#xA;code span.bu { color: #008000; } /* BuiltIn */&#xA;code span.cf { color: #007020; font-weight: bold; } /* ControlFlow */&#xA;code span.ch { color: #4070a0; } /* Char */&#xA;code span.cn { color: #880000; } /* Constant */&#xA;code span.co { color: #60a0b0; font-style: italic; } /* Comment */&#xA;code span.cv { color: #60a0b0; font-weight: bold; font-style: italic; } /* CommentVar */&#xA;code span.do { color: #ba2121; font-style: italic; } /* Documentation */&#xA;code span.dt { color: #902000; } /* DataType */&#xA;code span.dv { color: #40a070; } /* DecVal */&#xA;code span.er { color: #ff0000; font-weight: bold; } /* Error */&#xA;code span.ex { } /* Extension */&#xA;code span.fl { color: #40a070; } /* Float */&#xA;code span.fu { color: #06287e; } /* Function */&#xA;code span.im { color: #008000; font-weight: bold; } /* Import */&#xA;code span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Information */&#xA;code span.kw { color: #007020; font-weight: bold; } /* Keyword */&#xA;code span.op { color: #666666; } /* Operator */&#xA;code span.ot { color: #007020; } /* Other */&#xA;code span.pp { color: #bc7a00; } /* Preprocessor */&#xA;code span.sc { color: #4070a0; } /* SpecialChar */&#xA;code span.ss { color: #bb6688; } /* SpecialString */&#xA;code span.st { color: #4070a0; } /* String */&#xA;code span.va { color: #19177c; } /* Variable */&#xA;code span.vs { color: #4070a0; } /* VerbatimString */&#xA;code span.wa { color: #60a0b0; font-weight: bold; font-style: italic; } /* Warning */&lt;/style&gt;&lt;link rel=stylesheet href=../styles/common.css&gt;&lt;link rel=stylesheet href=../styles/fence-extensions/firstcoltable.css&gt;&lt;link rel=stylesheet href=../styles/fence-extensions/frame.css&gt;&lt;link rel=stylesheet href=../styles/fence-extensions/sheetsgraph.css&gt;&lt;script data-goatcounter=https://lukasschwab.goatcounter.com/count async src=//gc.zgo.at/count.js&gt;&lt;/script&gt;&lt;a href=../index.html&gt;&amp;#171; blog&lt;/a&gt; &lt;span class=date&gt;2025-01-18&lt;/span&gt;&lt;header id=title-block-header&gt;&lt;h1 class=title&gt;In Defense of New Boggle&lt;/h1&gt;&lt;/header&gt;&lt;p&gt;They literally don’t make &lt;a href=https://en.wikipedia.org/wiki/Boggle&gt;Boggle&lt;/a&gt; like they used&#xA;to.&lt;p&gt;The words you can spell in a Boggle game depend on the letters on 16&#xA;dice rolled into a four-by-four grid. The letters on the dice are&#xA;carefully selected — like in Scrabble, common letters like&#xA;&lt;code&gt;E&lt;/code&gt; and &lt;code&gt;A&lt;/code&gt; appear more often than uncommon ones&#xA;like &lt;code&gt;V&lt;/code&gt; or the &lt;code&gt;QU&lt;/code&gt; ligature. In fact, depending&#xA;on &lt;em&gt;when&lt;/em&gt; your English-language game of Boggle was manufactured,&#xA;you have one of two sets of dice.&lt;ul&gt;&lt;li&gt;The &lt;strong&gt;“Classic Boggle”&lt;/strong&gt; set, produced from 1976 to&#xA;1986.&lt;li&gt;The &lt;strong&gt;“New Boggle”&lt;/strong&gt; set, produced from 1987 to&#xA;today.&lt;/ul&gt;&lt;p&gt;There’s scant info about the 1986 redesign online — none from the&#xA;designers themselves — but notably New Boggle makes letters&#xA;&lt;code&gt;F&lt;/code&gt; and &lt;code&gt;K&lt;/code&gt; mutually exclusive, rendering&#xA;&lt;code&gt;FUCK&lt;/code&gt; unplayable.&lt;a href=#fn1 class=footnote-ref id=fnref1 role=doc-noteref&gt;&lt;sup&gt;1&lt;/sup&gt;&lt;/a&gt; &lt;a href=https://www.bananagrammer.com/2013/10/the-boggle-cube-redesign-and-its-effect.html&gt;This&#xA;blog post by ‘Bananagrammer’&lt;/a&gt; describes the differences between the&#xA;dice sets in detail.&lt;blockquote&gt;&lt;table&gt;&lt;col style=&#34;width: 25%&#34;&gt;&lt;col style=&#34;width: 25%&#34;&gt;&lt;col style=&#34;width: 25%&#34;&gt;&lt;col style=&#34;width: 25%&#34;&gt;&lt;thead&gt;&lt;tr&gt;&lt;th style=&#34;text-align: left;&#34;&gt;Dice set&lt;th style=&#34;text-align: right;&#34;&gt;Average # of words&lt;th style=&#34;text-align: right;&#34;&gt;Average Boggle score&lt;th style=&#34;text-align: right;&#34;&gt;Average length of longest word&lt;tbody&gt;&lt;tr&gt;&lt;td style=&#34;text-align: left;&#34;&gt;New Boggle&lt;td style=&#34;text-align: right;&#34;&gt;~104&lt;td style=&#34;text-align: right;&#34;&gt;~150&lt;td style=&#34;text-align: right;&#34;&gt;6.8&lt;tr&gt;&lt;td style=&#34;text-align: left;&#34;&gt;Classic Boggle&lt;td style=&#34;text-align: right;&#34;&gt;~93&lt;td style=&#34;text-align: right;&#34;&gt;~128&lt;td style=&#34;text-align: right;&#34;&gt;6.6&lt;/table&gt;&lt;p&gt;[…] Of course, these calculations only confirmed what I already knew:&#xA;the older version of the game is harder and is the one for me. I bought&#xA;a copy of classic Boggle from eBay. The dice are made of wood rather&#xA;than plastic. The timer has sand in it and doesn’t make some noise to&#xA;tell me when time is up. Succinctly, I think it is skookum.&lt;/blockquote&gt;&lt;p&gt;I think Bananagrammer got this wrong.&lt;p&gt;The fun in Boggle isn’t &lt;em&gt;really&lt;/em&gt; in the score — it’s in the&#xA;words themselves: quality over quantity, style points, that kind of&#xA;thing. A good evening of New Boggle yields higher per-board word counts,&#xA;but how does it affect the diversity of play?&lt;h1 id=dictionaries&gt;Dictionaries&lt;/h1&gt;&lt;p&gt;A set of Boggle dice sets certain hard constraints on word formation.&#xA;Each dice set contains a single &lt;code&gt;Z&lt;/code&gt;; kiss &lt;code&gt;JAZZ&lt;/code&gt;&#xA;goodbye. Moreover, letters that &lt;em&gt;share die&lt;/em&gt; are mutually&#xA;constraining, since only one side of a given die affects any given game.&#xA;Classic Boggle’s only &lt;code&gt;QU&lt;/code&gt; and only &lt;code&gt;J&lt;/code&gt; are on the&#xA;same die, so &lt;code&gt;JERQUE&lt;/code&gt; is impossible (just like&#xA;&lt;code&gt;FUCK&lt;/code&gt; in New Boggle). You can speak of a given die set’s&#xA;“dictionary;” start with a standard dictionary (CSW19) and filter out&#xA;the categorically unplayable words.&lt;p&gt;Classic Boggle has a substantially larger dictionary: 276,339&#xA;playable words, 14,536 more than New Boggle. A full 5.3% of the Classic&#xA;dictionary is unplayable today! Only 127 words are uniquely playable in&#xA;New Boggle.&lt;p&gt;At this juncture, it seems like Bananagrammer has a point: New Boggle&#xA;gives you more words per game on average, in a smaller (dumbed-down??)&#xA;dictionary.&lt;h1 id=same-games&gt;Same-games&lt;/h1&gt;&lt;p&gt;Let’s return to the question: diversity of &lt;em&gt;play.&lt;/em&gt; A&#xA;comparison between Classic and New Boggle should consider typical&#xA;boards, not theoretical maximum dictionaries. Vanishingly few games of&#xA;Classic Boggle admit 15-letter &lt;code&gt;UNDEREMPLOYMENT&lt;/code&gt;; its 1987&#xA;exclusion barely impacts the game’s diversity in practice. A good set of&#xA;dice don’t just admit a large theoretical dictionary of words, but also&#xA;provide difference game-to-game. A good set of dice yields Boggle games&#xA;that aren’t &lt;em&gt;samey&lt;/em&gt;.&lt;p&gt;Suppose you play 10 games of Classic or New Boggle in an evening. How&#xA;many unique words can you actually find across those games?&lt;div class=sheetsgraph&gt;&lt;iframe style=&#34;display: block;&#34; width=1099 height=679 seamless frameborder=0 scrolling=no src=&#34;https://docs.google.com/spreadsheets/d/e/2PACX-1vS2cYmmgYi77Qf8xMauxdN10yqoNad4qnqvYOR08hzcmTkCrEZFAltva0AQmduu0KoOBv4UtRQ5JgwP/pubchart?oid=834428688&amp;amp;format=interactive&#34;&gt;&lt;/iframe&gt;&lt;figcaption&gt;Classic and New Boggle have similar diversity of words discoverable in&#xA;ten-game sessions. New Boggle is somewhat more diverse.&lt;/figcaption&gt;&lt;/div&gt;&lt;p&gt;By this criterion, there really isn’t a reason to prefer Classic&#xA;Boggle over New Boggle. While it has a substantially larger dictionary,&#xA;Classic Boggle offers &lt;em&gt;fewer&lt;/em&gt; words per game — Bananagrammer’s&#xA;preference, but not mine — and offers only a modest difference in the&#xA;variety of words actually encountered game-to-game.&lt;a href=#fn2 class=footnote-ref id=fnref2 role=doc-noteref&gt;&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt;&lt;h1 id=play-and-experimentation&gt;Play and Experimentation&lt;/h1&gt;&lt;p&gt;The &lt;a href=https://github.com/lukasschwab/boggle&gt;Boggle solver&lt;/a&gt;&#xA;I wrote for these experiments ships with a terminal interface for&#xA;solitaire games with my house rules.&lt;a href=#fn3 class=footnote-ref id=fnref3 role=doc-noteref&gt;&lt;sup&gt;3&lt;/sup&gt;&lt;/a&gt; You can play a hosted&#xA;game by running &lt;code&gt;ssh boggle.fly.dev&lt;/code&gt;, or install the game&#xA;from source:&lt;div class=sourceCode id=cb1&gt;&lt;pre class=&#34;sourceCode bash&#34;&gt;&lt;code class=&#34;sourceCode bash&#34;&gt;&lt;span id=cb1-1&gt;&lt;a href=#cb1-1 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=ex&gt;go&lt;/span&gt; install github.com/lukasschwab/boggle@latest&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Games look like this:&lt;div class=sourceCode id=cb2&gt;&lt;pre class=&#34;sourceCode bash&#34;&gt;&lt;code class=&#34;sourceCode bash&#34;&gt;&lt;span id=cb2-1&gt;&lt;a href=#cb2-1 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=ex&gt;$&lt;/span&gt; boggle&lt;/span&gt;&#xA;&lt;span id=cb2-2&gt;&lt;a href=#cb2-2 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=ex&gt;0/50&lt;/span&gt; • 2m41s • bGNsbmFuYnVhYXBoc3Jsbg==&lt;/span&gt;&#xA;&lt;span id=cb2-3&gt;&lt;a href=#cb2-3 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=ex&gt;┌──────────────┐&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb2-4&gt;&lt;a href=#cb2-4 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=ex&gt;│&lt;/span&gt;  l  c  l  n  │&lt;/span&gt;&#xA;&lt;span id=cb2-5&gt;&lt;a href=#cb2-5 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=ex&gt;│&lt;/span&gt;  a  n  b  u  │&lt;/span&gt;&#xA;&lt;span id=cb2-6&gt;&lt;a href=#cb2-6 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=ex&gt;│&lt;/span&gt;  a  a  p  h  │&lt;/span&gt;&#xA;&lt;span id=cb2-7&gt;&lt;a href=#cb2-7 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=ex&gt;│&lt;/span&gt;  s  r  l  n  │&lt;/span&gt;&#xA;&lt;span id=cb2-8&gt;&lt;a href=#cb2-8 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=ex&gt;└──────────────┘&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb2-9&gt;&lt;a href=#cb2-9 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=op&gt;&amp;gt;&lt;/span&gt; club&lt;span class=kw&gt;|&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb2-10&gt;&lt;a href=#cb2-10 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=ex&gt;[enter]&lt;/span&gt; submit word • &lt;span class=pp&gt;[&lt;/span&gt;&lt;span class=ss&gt;ctrl+c&lt;/span&gt;&lt;span class=pp&gt;]&lt;/span&gt; quit&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You can play with either dice set — see &lt;code&gt;boggle --help&lt;/code&gt;.&#xA;See the &lt;code&gt;experiments&lt;/code&gt; branch (especially the &lt;code&gt;cmd&lt;/code&gt;&#xA;directories) to reproduce the results in this post.&lt;p&gt;If you’re interested in Boggle solvers, implement one yourself! This&#xA;was a good data-structures exercise, and tweaking the game’s parameters&#xA;. Curious about optimization problems across Boggle boards, e.g. a&#xA;search for the Boggle board with the greatest number of findable words?&#xA;Read Dan Vanderkam’s &lt;a href=https://www.danvk.org/wp/2014-01-25/what-up-with-boggle/&gt;“What up&#xA;with Boggle” series&lt;/a&gt;.&lt;/p&gt;&lt;section id=footnotes class=&#34;footnotes footnotes-end-of-document&#34; role=doc-endnotes&gt;&lt;hr&gt;&lt;ol&gt;&lt;li id=fn1&gt;&lt;p&gt;Dan Lewis speculates &lt;code&gt;FUCK&lt;/code&gt; motivated the&#xA;1986 redesign &lt;a href=https://nowiknow.com/the-party-game-with-a-built-in-swear-filter/&gt;here&lt;/a&gt;&#xA;and, more directly, &lt;a href=https://nowiknow.com/the-lost-boggle-story/&gt;here&lt;/a&gt;. I’m&#xA;skeptical. Only 1,572 of the ~14,700 words lost contain both&#xA;&lt;code&gt;F&lt;/code&gt; and &lt;code&gt;K&lt;/code&gt;. If the only goal of the redesign were&#xA;to exclude &lt;code&gt;FUCK&lt;/code&gt; (and &lt;code&gt;FUCKUP&lt;/code&gt;, and&#xA;&lt;code&gt;FUCKWIT&lt;/code&gt;, and so on), the designers would’ve incurred less&#xA;collateral damage.&lt;a href=#fnref1 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn2&gt;&lt;p&gt;I’ve published &lt;a href=https://gist.github.com/lukasschwab/88ac15e4b610cef6f8c3904971415ad4&gt;files&lt;/a&gt;&#xA;containing the “exclusive” words, playable in one version of Boggle but&#xA;not the other.&lt;a href=#fnref2 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn3&gt;&lt;p&gt;My house rules:&lt;ul&gt;&lt;li&gt;No words under 4 letters. The set of three-letter words is small;&#xA;rote-spotting them to deny your opponents points is a waste of&#xA;time.&lt;li&gt;Count unique words, rather than letters, to determine scores. Word&#xA;length is a factor in the tie-breaker, &lt;em&gt;word quality&lt;/em&gt; (always&#xA;negotiable).&lt;li&gt;At home we play with a dynamic negotiated dictionary (Googling and&#xA;arguing), but the solver I implemented uses &lt;a href=https://en.wikipedia.org/wiki/Collins_Scrabble_Words&gt;CSW19&lt;/a&gt;.&lt;/ul&gt;&lt;p&gt;These are important if you want to reproduce the specific&#xA;distributions in this post, but the emergent similarities between these&#xA;dice sets should be robust against dictionary changes and/or inclusion&#xA;of three-letter words.&lt;a href=#fnref3 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;/ol&gt;&lt;/section&gt;</content>
    <link href="https://lukasschwab.me/blog/gen/boggle-experiments.html" rel="alternate"></link>
    <summary type="html">I use an interactive Boggle game --- and solver --- to compare the two epochs of English-language Boggle: before and after a 1987 change to the lettered dice.</summary>
  </entry>
  <entry>
    <title>&#34;Mystery Box&#34; Economics</title>
    <updated>2024-12-23T00:00:00Z</updated>
    <id>https://lukasschwab.me/blog/gen/robinhood-mystery-box.html</id>
    <content type="html">&lt;html xmlns=http://www.w3.org/1999/xhtml lang xml:lang&gt;&lt;meta charset=utf-8&gt;&lt;meta name=generator content=&#34;pandoc&#34;&gt;&lt;meta name=viewport content=&#34;width=device-width,initial-scale=1,user-scalable=yes&#34;&gt;&lt;meta name=dcterms.date content=&#34;2024-12-23&#34;&gt;&lt;title&gt;“Mystery Box” Economics&lt;/title&gt;&lt;style&gt;&#xA;      code{white-space: pre-wrap;}&#xA;      span.smallcaps{font-variant: small-caps;}&#xA;      span.underline{text-decoration: underline;}&#xA;      div.column{display: inline-block; vertical-align: top; width: 50%;}&#xA;          &lt;/style&gt;&lt;link rel=stylesheet href=../styles/common.css&gt;&lt;link rel=stylesheet href=../styles/fence-extensions/fullwidth.css&gt;&lt;link rel=stylesheet href=../styles/fence-extensions/widetable.css&gt;&lt;script data-goatcounter=https://lukasschwab.goatcounter.com/count async src=//gc.zgo.at/count.js&gt;&lt;/script&gt;&lt;a href=../index.html&gt;&amp;#171; blog&lt;/a&gt; &lt;span class=date&gt;2024-12-23&lt;/span&gt;&lt;header id=title-block-header&gt;&lt;h1 class=title&gt;“Mystery Box” Economics&lt;/h1&gt;&lt;/header&gt;&lt;p&gt;Robinhood announced a new “Mystery Box” points redemption feature to&#xA;Gold [credit] Card customers today. Launching a loot crate minigame for&#xA;consumer debt is an odd move — Robinhood, after all, spent the last few&#xA;years insisting it’s an accessible but responsible consumer financial&#xA;services company (c.f. their &lt;a href=https://robinhood.com/us/en/support/articles/robinhood-cash-card/&gt;debit&lt;/a&gt;&#xA;and &lt;a href=https://robinhood.com/us/en/about/retirement&gt;retirement&lt;/a&gt;&#xA;products) and &lt;em&gt;not&lt;/em&gt; a gambling app for candy-crushing your life’s&#xA;savings away into short-call options. Between their recent &lt;a href=https://robinhood.com/us/en/support/articles/robinhood-event-contracts/&gt;event&#xA;contracts experiment&lt;/a&gt; and Vlad’s &lt;a href=https://www.cnbc.com/2024/12/04/robinhood-considering-move-into-sports-betting-ceo-vlad-tenev-says.html&gt;chatter&#xA;about sports betting&lt;/a&gt;, they seem lately less afraid of the casino&#xA;comparison.&lt;p&gt;A “Mystery Box” costs &lt;span class=&#34;math inline&#34;&gt;1, 000&lt;/span&gt; points.&#xA;The result of about $333 in spend, &lt;span class=&#34;math inline&#34;&gt;1, 000&lt;/span&gt; points have a $10 value in cash-back&#xA;redemption.&lt;a href=#fn1 class=footnote-ref id=fnref1 role=doc-noteref&gt;&lt;sup&gt;1&lt;/sup&gt;&lt;/a&gt; Each Box yields a reward in one of&#xA;three forms:&lt;ol type=1&gt;&lt;li&gt;A fixed number of points — greater than the &lt;span class=&#34;math inline&#34;&gt;1, 000&lt;/span&gt; spent.&lt;li&gt;An elevated cash-back rate for all spend over some time period (24&#xA;hours, one week, or 30 days).&lt;li&gt;An elevated cash-back rate on your next purchase, either generally&#xA;or in a certain category (e.g. “groceries,” “restaurants,” “automotive&#xA;refuel”).&lt;/ol&gt;&lt;p&gt;Importantly, the 2nd and 3rd reward forms — boosted cashback rates —&#xA;&lt;em&gt;do not&lt;/em&gt; stack on the standard 3% cash-back rate offered by the&#xA;card. You should think of the reward as just the &lt;em&gt;marginal&lt;/em&gt;&#xA;increase over 3%: a 4% elevated rate only nets you 1% additional&#xA;cash-back.&lt;p&gt;When does it make sense to buy a “Mystery Box” instead of redeeming&#xA;to cash in a Brokerage account? The expected value depends on how you&#xA;expect to spend &lt;em&gt;after buying it.&lt;/em&gt; If you have specific plans —&#xA;e.g. a large single purchase you need to make on Amazon — the expected&#xA;value may exceed the $10 price tag.&lt;p&gt;I am not a financial advisor and this is not financial advice. These&#xA;reward definitions are from the &lt;a href=https://api.robinhood.com/creditcard/legal/reward-terms#redeeming-points-through-the-%E2%80%9Cmystery-box%E2%80%9D&gt;Robinhood&#xA;Gold Card Rewards Program Rules&lt;/a&gt; as of &lt;del&gt;December 23, 2024&lt;/del&gt;&#xA;March 31, 2026; they may change.&lt;a href=#fn2 class=footnote-ref id=fnref2 role=doc-noteref&gt;&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt;&lt;div class=&#34;widetable fullwidth&#34;&gt;&lt;table id=calculator&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Reward&lt;th&gt;Expected spend&lt;th&gt;Reward value&lt;th&gt;Probability&lt;th&gt;Expected value&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;100,000 points&lt;td style=&#34;color: lightgrey&#34;&gt;&amp;#9135;&lt;td&gt;$1,000.00&lt;td&gt;0.001%&lt;td class=ev&gt;$0.01&lt;tr&gt;&lt;td&gt;10,000 points&lt;td style=&#34;color: lightgrey&#34;&gt;&amp;#9135;&lt;td&gt;$100.00&lt;td&gt;0.20%&lt;td class=ev&gt;$0.20&lt;tr&gt;&lt;td&gt;7,500 points&lt;td style=&#34;color: lightgrey&#34;&gt;&amp;#9135;&lt;td&gt;$75.00&lt;td&gt;0.30%&lt;td class=ev&gt;$0.23&lt;tr&gt;&lt;td&gt;5,000 points&lt;td style=&#34;color: lightgrey&#34;&gt;&amp;#9135;&lt;td&gt;$50.00&lt;td&gt;0.50%&lt;td class=ev&gt;$0.25&lt;tr&gt;&lt;td&gt;2,500 points&lt;td style=&#34;color: lightgrey&#34;&gt;&amp;#9135;&lt;td&gt;$25.00&lt;td&gt;1.00%&lt;td class=ev&gt;$0.25&lt;tr&gt;&lt;td&gt;1,500 points&lt;td style=&#34;color: lightgrey&#34;&gt;&amp;#9135;&lt;td&gt;$15.00&lt;td&gt;1.50%&lt;td class=ev&gt;$0.22&lt;tr&gt;&lt;td&gt;1,250 points&lt;td style=&#34;color: lightgrey&#34;&gt;&amp;#9135;&lt;td&gt;$12.50&lt;td&gt;2.00%&lt;td class=ev&gt;$0.25&lt;tr&gt;&lt;td&gt;10% cashback on next grocery purchase&lt;td&gt;&lt;span&gt;&lt;input id=grocery10_input oninput=&#34;this.nextElementSibling.value = &#39;$&#39; + this.value&#34; type=range min=0 max=500 value=100 class=slider&gt;&#xA;&lt;output&gt;$100&lt;/output&gt;&lt;/span&gt;&lt;td&gt;&lt;output id=grocery10_output&gt;&lt;/output&gt;&lt;td id=grocery10_prob&gt;5.00%&lt;td&gt;&lt;output id=grocery10_ev class=ev&gt;&lt;/output&gt;&lt;tr&gt;&lt;td&gt;5% cashback on next grocery purchase&lt;td&gt;&lt;span&gt;&lt;input id=grocery5_input oninput=&#34;this.nextElementSibling.value = &#39;$&#39; + this.value&#34; type=range min=0 max=500 value=100 class=slider&gt;&#xA;&lt;output&gt;$100&lt;/output&gt;&lt;/span&gt;&lt;td&gt;&lt;output id=grocery5_output&gt;&lt;/output&gt;&lt;td id=grocery5_prob&gt;8.00%&lt;td&gt;&lt;output id=grocery5_ev class=ev&gt;&lt;/output&gt;&lt;tr&gt;&lt;td&gt;10% cashback on next automotive refuel&lt;td&gt;&lt;span&gt;&lt;input id=refuel_input oninput=&#34;this.nextElementSibling.value = &#39;$&#39; + this.value&#34; type=range min=0 max=300 value=50 class=slider&gt;&#xA;&lt;output&gt;$50&lt;/output&gt;&lt;/span&gt;&lt;td&gt;&lt;output id=refuel_output&gt;&lt;/output&gt;&lt;td id=refuel_prob&gt;2.00%&lt;td&gt;&lt;output id=refuel_ev class=ev&gt;&lt;/output&gt;&lt;tr&gt;&lt;td&gt;5% cashback for 30 days&lt;td&gt;&lt;span&gt;&lt;input id=30day5pct_input oninput=&#34;this.nextElementSibling.value = &#39;$&#39; + this.value&#34; type=range min=0 max=10000 value=3000 class=slider&gt;&#xA;&lt;output&gt;$3000&lt;/output&gt;&lt;/span&gt;&lt;td&gt;&lt;output id=30day5pct_output&gt;&lt;/output&gt;&lt;td id=30day5pct_prob&gt;0.25%&lt;td&gt;&lt;output id=30day5pct_ev class=ev&gt;&lt;/output&gt;&lt;tr&gt;&lt;td&gt;5% cashback for 1 week&lt;td&gt;&lt;span&gt;&lt;input id=1wk5pct_input oninput=&#34;this.nextElementSibling.value = &#39;$&#39; + this.value&#34; type=range min=0 max=2500 value=500 class=slider&gt;&#xA;&lt;output&gt;$500&lt;/output&gt;&lt;/span&gt;&lt;td&gt;&lt;output id=1wk5pct_output&gt;&lt;/output&gt;&lt;td id=1wk5pct_prob&gt;2.00%&lt;td&gt;&lt;output id=1wk5pct_ev class=ev&gt;&lt;/output&gt;&lt;tr&gt;&lt;td&gt;10% cashback for 24 hours&lt;td&gt;&lt;span&gt;&lt;input id=24hr10pct_input oninput=&#34;this.nextElementSibling.value = &#39;$&#39; + this.value&#34; type=range min=0 max=3000 value=250 class=slider&gt;&#xA;&lt;output&gt;$250&lt;/output&gt;&lt;/span&gt;&lt;td&gt;&lt;output id=24hr10pct_output&gt;&lt;/output&gt;&lt;td id=24hr10pct_prob&gt;0.50%&lt;td&gt;&lt;output id=24hr10pct_ev class=ev&gt;&lt;/output&gt;&lt;tr&gt;&lt;td&gt;5% cashback for 24 hours&lt;td&gt;&lt;span&gt;&lt;input id=24hr5pct_input oninput=&#34;this.nextElementSibling.value = &#39;$&#39; + this.value&#34; type=range min=0 max=3500 value=250 class=slider&gt;&#xA;&lt;output&gt;$250&lt;/output&gt;&lt;/span&gt;&lt;td&gt;&lt;output id=24hr5pct_output&gt;&lt;/output&gt;&lt;td id=24hr5pct_prob&gt;6.00%&lt;td&gt;&lt;output id=24hr5pct_ev class=ev&gt;&lt;/output&gt;&lt;tr&gt;&lt;td&gt;4% cashback for 24 hours&lt;td&gt;&lt;span&gt;&lt;input id=24hr4pct_input oninput=&#34;this.nextElementSibling.value = &#39;$&#39; + this.value&#34; type=range min=0 max=3000 value=250 class=slider&gt;&#xA;&lt;output&gt;$250&lt;/output&gt;&lt;/span&gt;&lt;td&gt;&lt;output id=24hr4pct_output&gt;&lt;/output&gt;&lt;td id=24hr4pct_prob&gt;17.00%&lt;td&gt;&lt;output id=24hr4pct_ev class=ev&gt;&lt;/output&gt;&lt;tr&gt;&lt;td&gt;15% cashback on your next purchase&lt;td&gt;&lt;span&gt;&lt;input id=next15pct_input oninput=&#34;this.nextElementSibling.value = &#39;$&#39; + this.value&#34; type=range min=0 max=500 value=100 class=slider&gt;&#xA;&lt;output&gt;$100&lt;/output&gt;&lt;/span&gt;&lt;td&gt;&lt;output id=next15pct_output&gt;&lt;/output&gt;&lt;td id=next15pct_prob&gt;3.00%&lt;td&gt;&lt;output id=next15pct_ev class=ev&gt;&lt;/output&gt;&lt;tr&gt;&lt;td&gt;10% cashback on next mobile wallet purchase&lt;td&gt;&lt;span&gt;&lt;input id=wallet10pct_input oninput=&#34;this.nextElementSibling.value = &#39;$&#39; + this.value&#34; type=range min=0 max=500 value=100 class=slider&gt;&#xA;&lt;output&gt;$100&lt;/output&gt;&lt;/span&gt;&lt;td&gt;&lt;output id=wallet10pct_output&gt;&lt;/output&gt;&lt;td id=wallet10pct_prob&gt;4.00%&lt;td&gt;&lt;output id=wallet10pct_ev class=ev&gt;&lt;/output&gt;&lt;tr&gt;&lt;td&gt;15% cashback on next restaurant order&lt;td&gt;&lt;span&gt;&lt;input id=restaurant15_input oninput=&#34;this.nextElementSibling.value = &#39;$&#39; + this.value&#34; type=range min=0 max=500 value=100 class=slider&gt;&#xA;&lt;output&gt;$100&lt;/output&gt;&lt;/span&gt;&lt;td&gt;&lt;output id=restaurant15_output&gt;&lt;/output&gt;&lt;td id=restaurant15_prob&gt;5.00%&lt;td&gt;&lt;output id=restaurant15_ev class=ev&gt;&lt;/output&gt;&lt;tr&gt;&lt;td&gt;10% cashback on next restaurant order&lt;td&gt;&lt;span&gt;&lt;input id=restaurant10_input oninput=&#34;this.nextElementSibling.value = &#39;$&#39; + this.value&#34; type=range min=0 max=500 value=100 class=slider&gt;&#xA;&lt;output&gt;$100&lt;/output&gt;&lt;/span&gt;&lt;td&gt;&lt;output id=restaurant10_output&gt;&lt;/output&gt;&lt;td id=restaurant10_prob&gt;25.749%&lt;td&gt;&lt;output id=restaurant10_ev class=ev&gt;&lt;/output&gt;&lt;tr&gt;&lt;td&gt;5% cashback on next restaurant order&lt;td&gt;&lt;span&gt;&lt;input id=restaurant5_input oninput=&#34;this.nextElementSibling.value = &#39;$&#39; + this.value&#34; type=range min=0 max=500 value=100 class=slider&gt;&#xA;&lt;output&gt;$100&lt;/output&gt;&lt;/span&gt;&lt;td&gt;&lt;output id=restaurant5_output&gt;&lt;/output&gt;&lt;td id=restaurant5_prob&gt;16.00%&lt;td&gt;&lt;output id=restaurant5_ev class=ev&gt;&lt;/output&gt;&lt;tfoot style=&#34;border-top: 1px solid black&#34;&gt;&lt;tr&gt;&lt;td colspan=4&gt;&lt;b&gt;&#34;Mystery Box&#34; cost&lt;/b&gt;&lt;td&gt;$10.00&lt;tr&gt;&lt;td colspan=4&gt;&lt;b&gt;Total &#34;Mystery Box&#34; expected value&lt;/b&gt;&lt;td&gt;&lt;output id=total&gt;&lt;/output&gt;&lt;/table&gt;&lt;/div&gt;&lt;script&gt;&#xA;    const sliderRates = {&#xA;      grocery10: .10,&#xA;      grocery5: .05,&#xA;      refuel: .10,&#xA;      &#34;30day5pct&#34;: .05,&#xA;      &#34;1wk5pct&#34;: .05,&#xA;      &#34;24hr10pct&#34;: .10,&#xA;      &#34;24hr5pct&#34;: .05,&#xA;      &#34;24hr4pct&#34;: .04,&#xA;      next15pct: .15,&#xA;      wallet10pct: .10,&#xA;      restaurant15: .15,&#xA;      restaurant10: .10,&#xA;      restaurant5: .05&#xA;    }&#xA;&#xA;    const calculateValue = (rate, spend) =&gt; {&#xA;        const marginalRate = rate - 0.03&#xA;        return (spend * marginalRate).toFixed(2);&#xA;    }&#xA;&#xA;    const total = document.getElementById(&#34;total&#34;);&#xA;    const recalculateTotal = () =&gt; {&#xA;        const expectedValues = Array.from(document.getElementsByClassName(&#34;ev&#34;))&#xA;            .map((elem) =&gt; elem.innerText)&#xA;            // Drop the leading &#39;$&#39; before parsing&#xA;            .map((s) =&gt; parseFloat(s.substring(1)));&#xA;        if (expectedValues.every((v) =&gt; typeof v === &#34;number&#34;)) {&#xA;            total.innerText =&#xA;                &#34;$&#34; +&#xA;                expectedValues.reduce((acc, cur) =&gt; acc + cur, 0).toFixed(2);&#xA;        }&#xA;    };&#xA;&lt;/script&gt;&lt;script&gt;&#xA;    Object.entries(sliderRates).forEach(([sliderId, rate]) =&gt; {&#xA;        const input = document.getElementById(sliderId + &#34;_input&#34;);&#xA;        const output = document.getElementById(sliderId + &#34;_output&#34;);&#xA;        const probability =&#xA;            parseFloat(&#xA;                document&#xA;                    .getElementById(sliderId + &#34;_prob&#34;)&#xA;                    .innerText.replace(/[^0-9.]/g, &#34;&#34;),&#xA;            ) / 100;&#xA;        const ev = document.getElementById(sliderId + &#34;_ev&#34;);&#xA;&#xA;        const setOutputs = (spend) =&gt; {&#xA;            output.innerText = &#34;$&#34; + calculateValue(rate, spend);&#xA;            ev.innerText = &#34;$&#34; + calculateValue(rate, spend * probability);&#xA;            recalculateTotal();&#xA;        };&#xA;&#xA;        input.addEventListener(&#34;change&#34;, (event) =&gt; {&#xA;            setOutputs(event.target.value);&#xA;        });&#xA;&#xA;        setOutputs(input.value);&#xA;    });&#xA;&lt;/script&gt;&lt;style&gt;&#xA;    #calculator {&#xA;        max-width: 830px;&#xA;    }&#xA;&#xA;    table td,&#xA;    table th {&#xA;        font-family: monospace;&#xA;        text-align: end;&#xA;    }&#xA;    tbody td:nth-child(1),&#xA;    table th:nth-child(1) {&#xA;        font-family: serif;&#xA;        text-align: left;&#xA;    }&#xA;    tbody td:nth-child(2),&#xA;    table th:nth-child(2) {&#xA;        text-align: left;&#xA;    }&#xA;    tfoot td {&#xA;        text-align: end;&#xA;        border-top: 1px solid black;&#xA;    }&#xA;&#xA;    .slider {&#xA;        margin: 0;&#xA;        vertical-align: text-bottom;&#xA;    }&#xA;    output {&#xA;        color: blue;&#xA;        min-width: 5ch;&#xA;        display: inline-block;&#xA;    }&#xA;&lt;/style&gt;&lt;p&gt;The ranges on these sliders are capped to the spend limits stated in&#xA;the &lt;a href=https://api.robinhood.com/creditcard/legal/reward-terms#redeeming-points-through-the-%E2%80%9Cmystery-box%E2%80%9D&gt;reward&#xA;terms&lt;/a&gt;, even if those limits seem unrealistic.&lt;/p&gt;&lt;section id=footnotes class=&#34;footnotes footnotes-end-of-document&#34; role=doc-endnotes&gt;&lt;hr&gt;&lt;ol&gt;&lt;li id=fn1&gt;&lt;p&gt;“Reward value” assumes the points are to be redeemed&#xA;“for Cash Back in Robinhood Brokerage Cash Account” (see &lt;a href=https://api.robinhood.com/creditcard/legal/reward-terms#redeeming-points-for-cash-back-in-robinhood-brokerage-cash-account&gt;definition&#xA;in the Terms&lt;/a&gt;). At time of writing, this redemption rate is $0.01 per&#xA;point. Points redeemed in the Travel and Shop portals, or redeemed for&#xA;gift card value, are redeemed at the same rate. Points redeemed for&#xA;&lt;em&gt;statement credit&lt;/em&gt; or &lt;em&gt;special items&lt;/em&gt; (e.g. metal card&#xA;upgrades) have a less-than-$0.01 value.&lt;a href=#fnref1 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn2&gt;&lt;p&gt;See &lt;a href=../img/robinhood-mystery-box/calculator-2024-12-23.html&gt;the&#xA;previous calculator&lt;/a&gt; for the original December 2024 rewards.&lt;a href=#fnref2 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;/ol&gt;&lt;/section&gt;</content>
    <link href="https://lukasschwab.me/blog/gen/robinhood-mystery-box.html" rel="alternate"></link>
    <summary type="html">A calculator for the expected value of a Robinhood Gold Card &#34;Mystery Box&#34; redemption, given one&#39;s anticipated subsequent spending.</summary>
  </entry>
  <entry>
    <title>Postmortems are for Reading</title>
    <updated>2024-12-05T00:00:00Z</updated>
    <id>https://lukasschwab.me/blog/gen/postmortems-are-for-reading.html</id>
    <content type="html">&lt;html xmlns=http://www.w3.org/1999/xhtml lang xml:lang&gt;&lt;meta charset=utf-8&gt;&lt;meta name=generator content=&#34;pandoc&#34;&gt;&lt;meta name=viewport content=&#34;width=device-width,initial-scale=1,user-scalable=yes&#34;&gt;&lt;meta name=dcterms.date content=&#34;2024-12-05&#34;&gt;&lt;title&gt;Postmortems are for Reading&lt;/title&gt;&lt;style&gt;&#xA;      code{white-space: pre-wrap;}&#xA;      span.smallcaps{font-variant: small-caps;}&#xA;      span.underline{text-decoration: underline;}&#xA;      div.column{display: inline-block; vertical-align: top; width: 50%;}&#xA;          &lt;/style&gt;&lt;link rel=stylesheet href=../styles/common.css&gt;&lt;script data-goatcounter=https://lukasschwab.goatcounter.com/count async src=//gc.zgo.at/count.js&gt;&lt;/script&gt;&lt;a href=../index.html&gt;&amp;#171; blog&lt;/a&gt; &lt;span class=date&gt;2024-12-05&lt;/span&gt;&lt;header id=title-block-header&gt;&lt;h1 class=title&gt;Postmortems are for Reading&lt;/h1&gt;&lt;/header&gt;&lt;p&gt;You probably have a folder of old technical incident writeups — that&#xA;time you dropped an important database table, or accidentally disabled&#xA;an error rate monitor, or sent every customer a test email. Every&#xA;company has a corporate euphemism for these writeups; I’ll call them&#xA;“postmortems.”&lt;p&gt;In Trevor Kletz’s advice to process plant managers, this history&#xA;takes physical form: the little ‘black book.’&lt;blockquote&gt;&lt;p&gt;Keep a ‘black book’ or ‘memory book’ in each control room, a folder&#xA;of reports on accidents that have occurred. Do not include falls and&#xA;bruises, but only accidents of technical interest, and include accidents&#xA;from other plants.&lt;a href=#fn1 class=footnote-ref id=fnref1 role=doc-noteref&gt;&lt;sup&gt;1&lt;/sup&gt;&lt;/a&gt;&lt;/blockquote&gt;&lt;p&gt;Now be honest: do you read your own postmortems? Has &lt;em&gt;anyone&lt;/em&gt;&#xA;revisited them since they were first written and approved? “The black&#xA;book should be compulsory reading for newcomers, at all levels,” Kletz&#xA;continues, “and old hands should dip into it from time to time to&#xA;refresh their memories.” Postmortem writeups shouldn’t just serve to&#xA;marshal an initial response; if you’re serious about preventing repeat&#xA;incidents, they should be a living part of your operational culture.&lt;p&gt;In their control room, a process plant manager like Kletz could see&#xA;evidence of that role in wear and tear.&lt;blockquote&gt;&lt;p&gt;If spotlessly clean, like poetry books in libraries, they are&#xA;probably never read and the reasons for this should be sought. Perhaps&#xA;they are incomprehensible. [… One] may be surprised how often they are&#xA;out-of-date, or cannot readily be found or are spotlessly clean.&lt;a href=#fn2 class=footnote-ref id=fnref2 role=doc-noteref&gt;&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt;&lt;/blockquote&gt;&lt;p&gt;Software engineers often ignore this secondary role of incident&#xA;postmortems — to &lt;em&gt;teach&lt;/em&gt; future engineers and incident-responders&#xA;— in favor of immediately pragmatic tasks: satisfy requirements from on&#xA;high; assuage fears; outline a narrow root cause. &lt;em&gt;Managers&lt;/em&gt; of&#xA;software engineers aren’t much better.&lt;p&gt;You can have a laudable incident management culture — a blameless and&#xA;honest one, earnest about improving your processes — that &lt;em&gt;still&lt;/em&gt;&#xA;shits out dry, perfunctory, box-ticking reports for nobody to read ever&#xA;again.&lt;/p&gt;&lt;p&gt;Instead, make postmortem readability an explicit value (not just&#xA;“this can be read,” but “I &lt;em&gt;want&lt;/em&gt; to read it”). A postmortem’s&#xA;author should step back from the obligatory template and consider what&#xA;they want various readers to remember.&lt;table&gt;&lt;col style=&#34;width: 20%&#34;&gt;&lt;col style=&#34;width: 80%&#34;&gt;&lt;thead&gt;&lt;tr class=header&gt;&lt;th style=&#34;text-align: left;&#34;&gt;Audience&lt;th style=&#34;text-align: left;&#34;&gt;Questions to consider&lt;tbody&gt;&lt;tr class=odd&gt;&lt;td style=&#34;text-align: left;&#34;&gt;Peer engineers&lt;td style=&#34;text-align: left;&#34;&gt;Is this failure part of a &lt;em&gt;pattern&lt;/em&gt;&#xA;of preventable incidents in your system?&lt;br&gt;Has some link in this&#xA;incident’s causal chain cropped up in other incidents?&lt;br&gt;Do other&#xA;parts of your product have similar failure modes?&lt;br&gt;Even if you won’t&#xA;rewrite the code in remediation, could this have been pre-empted with a&#xA;different design/implementation?&lt;tr class=even&gt;&lt;td style=&#34;text-align: left;&#34;&gt;New hires&lt;td style=&#34;text-align: left;&#34;&gt;What’s the single most important rule or&#xA;reminder to take away?&lt;br&gt;Does the indicent demonstrate anything&#xA;&lt;em&gt;unique&lt;/em&gt; about your system, a characteristic failure mode the&#xA;uninitiated wouldn’t expect?&lt;tr class=odd&gt;&lt;td style=&#34;text-align: left;&#34;&gt;Outsiders&lt;td style=&#34;text-align: left;&#34;&gt;How would you describe th eincident to a&#xA;friend on another team, or one writing software at a different&#xA;company?&lt;br&gt;How would you describe it to a chemical process&#xA;engineer?&lt;br&gt;Is that narrative — the &lt;em&gt;memorable,&#xA;widely-relatable&lt;/em&gt; version — clear in your written report?&lt;/table&gt;&lt;p&gt;These ‘black book’ summaries can go in your primary report or live in&#xA;some team-internal folder. They shouldn’t interfere with writing a&#xA;precise and descriptive report, and your responsibility to write&#xA;something &lt;em&gt;interesting&lt;/em&gt; doesn’t override responsibilities to your&#xA;colleagues (fairness) or customers (objectivity).&lt;/p&gt;&lt;p&gt;Postmortems &lt;em&gt;can&lt;/em&gt; be great reads. The NTSB and USCSB have&#xA;small cult followings, largely outside their respective industries; I&#xA;only arrived at &lt;a href=./safe-incompatibility&gt;“Safety Through&#xA;Incompatibility”&lt;/a&gt; because the USCSB’s YouTube backlog tickles my&#xA;brain. A failure that seems highly specific may exemplify a much broader&#xA;class of failures outside your domain, just as chemical process failures&#xA;are (surprise!) good analogies for crummy software engineering.&lt;p&gt;Keep in mind: it’s a privilege (one worth exercising) to treat&#xA;incident management lightheartedly. The tradition of keeping ‘black&#xA;books’ and revisiting them was a hard-learned lesson in process&#xA;engineering; I remember Kletz’s somber epigraph for &lt;em&gt;Learning from&#xA;Accidents&lt;/em&gt;:&lt;/p&gt;&lt;blockquote&gt;&lt;p style=text-align:center;&gt;This book has been written&lt;br&gt;to remember the dead and injured&lt;br&gt;and&#xA;to warn the living&lt;/blockquote&gt;&lt;aside id=footnotes class=&#34;footnotes footnotes-end-of-document&#34; role=doc-endnotes&gt;&lt;hr&gt;&lt;ol&gt;&lt;li id=fn1&gt;&lt;p&gt;Kletz, Trevor. &lt;em&gt;Learning from Accidents.&lt;/em&gt;&#xA;Routledge, 2007. Page 313. &lt;a href=https://hsseworld.com/wp-content/uploads/2020/07/Learning-from-Accidents-Book.pdf&gt;Available&#xA;online.&lt;/a&gt; He gave the same advice, in almost identical language, in an&#xA;internal memo in 1975:&lt;blockquote&gt;&lt;p&gt;One method which has been used successfully on at least one plant in&#xA;the Division is the plant black book. On this plant there is a folder&#xA;containing reports of every serious incident and near miss that has&#xA;occurred on the plant, together with reports of incidents in similar&#xA;plants in other companies. This is compulsory reading for all managers&#xA;and engineers when they first join the plant, and the old hands dip into&#xA;it from time to time to refresh their memory.&lt;p&gt;Why not start a similar folder on your plant? It may look a little&#xA;thin at first, but nevertheless it may help your successor. I can&#xA;probably help you fill it.&lt;p&gt;The [runaway reaction] incident described in the next item might have&#xA;been prevented by a plant black book.&lt;/blockquote&gt;&lt;p&gt;Imperial Chemical Industries Limited, Petrochemicals Division.&#xA;&lt;em&gt;Safety Newsletter No. 72.&lt;/em&gt; 1975. &lt;a href=https://www.icheme.org/media/10779/ici072.pdf&gt;Available&#xA;online.&lt;/a&gt; This newsletter is notable as an early specific example of&#xA;how Kletz writes about safety directly and among peers; the books cited&#xA;here are his (meta-)writings &lt;em&gt;about&lt;/em&gt; writing about safety.&lt;a href=#fnref1 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn2&gt;&lt;p&gt;Kletz, Trevor. &lt;em&gt;An Engineer’s View of Human&#xA;Error,&lt;/em&gt; Third Edition. Taylor &amp; Francis, 2001. Page 71.&lt;a href=#fnref2 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;/ol&gt;&lt;/aside&gt;</content>
    <link href="https://lukasschwab.me/blog/gen/postmortems-are-for-reading.html" rel="alternate"></link>
    <summary type="html">The typical tech industry approach to incident retrospectives don&#39;t get read enough, probably because they&#39;re written to be forgotten. Take another note from heavy industry: write for a broad audience.</summary>
  </entry>
  <entry>
    <title>Applying the &#34;Hierarchy of Controls&#34;</title>
    <updated>2024-12-03T00:00:00Z</updated>
    <id>https://lukasschwab.me/blog/gen/hierarchy-of-controls.html</id>
    <content type="html">&lt;html xmlns=http://www.w3.org/1999/xhtml lang xml:lang&gt;&lt;meta charset=utf-8&gt;&lt;meta name=generator content=&#34;pandoc&#34;&gt;&lt;meta name=viewport content=&#34;width=device-width,initial-scale=1,user-scalable=yes&#34;&gt;&lt;meta name=dcterms.date content=&#34;2024-12-03&#34;&gt;&lt;title&gt;Applying the “Hierarchy of Controls”&lt;/title&gt;&lt;style&gt;&#xA;      code{white-space: pre-wrap;}&#xA;      span.smallcaps{font-variant: small-caps;}&#xA;      span.underline{text-decoration: underline;}&#xA;      div.column{display: inline-block; vertical-align: top; width: 50%;}&#xA;          &lt;/style&gt;&lt;link rel=stylesheet href=../styles/common.css&gt;&lt;script data-goatcounter=https://lukasschwab.goatcounter.com/count async src=//gc.zgo.at/count.js&gt;&lt;/script&gt;&lt;a href=../index.html&gt;&amp;#171; blog&lt;/a&gt; &lt;span class=date&gt;2024-12-03&lt;/span&gt;&lt;header id=title-block-header&gt;&lt;h1 class=title&gt;Applying the “Hierarchy of Controls”&lt;/h1&gt;&lt;/header&gt;&lt;p&gt;One of the US Chemical Safety Board’s “Key Lessons” from the &lt;a href=./safe-incompatibility.html&gt;Atchison, Kansas accidental mixing&#xA;incident&lt;/a&gt; is that the operator in question, and others like it,&#xA;should&lt;blockquote&gt;&lt;p&gt;apply the [National Institute for Occupational Safety and Health&#xA;(NIOSH)] hierarchy of controls when evaluating controls and safeguards&#xA;for preventing inadvertent mixing. For all chemical unloading activities&#xA;that require human interaction, […] identify and address human factors&#xA;issues that may increase the potential for an incorrect connection.&lt;a href=#fn1 class=footnote-ref id=fnref1 role=doc-noteref&gt;&lt;sup&gt;1&lt;/sup&gt;&lt;/a&gt;&lt;/blockquote&gt;&lt;figure&gt;&lt;img src=../img/hierarchy-of-controls/hierarchy-of-controls.svg alt=&#34;The scourge of every Slack workspace I’ve ever joined: NIOSH’s inverted pyramid of controls. Save this bad boy to your work laptop. Apply liberally.&#34;&gt;&lt;figcaption aria-hidden=true&gt;The scourge of every Slack workspace I’ve&#xA;ever joined: NIOSH’s inverted pyramid of controls. Save this bad boy to&#xA;your work laptop. Apply liberally.&lt;a href=#fn2 class=footnote-ref id=fnref2 role=doc-noteref&gt;&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;The Hierarchy’s basic premise is that there may be several ways to&#xA;mitigate a hazard, but some of those strategies are stronger than&#xA;others:&lt;ul&gt;&lt;li&gt;&lt;p&gt;At the strong end, you can &lt;em&gt;eliminate&lt;/em&gt; the hazardous part&#xA;of your system entirely, then smack the &lt;span style=&#34;color:red; cursor:help; text-decoration:spelling-error;&#34; onclick=&#34;document.getElementById(&#39;audio&#39;).play();&#34;&gt;big red&#xA;‘Easy’ button&lt;/span&gt; on your desk.&#xA;&lt;audio id=audio src=https://www.myinstants.com/media/sounds/that_was_easy.mp3&gt;&lt;/audio&gt;&lt;li&gt;&lt;p&gt;At the weak end, you can give Mike a pair of &lt;em&gt;safety&#xA;goggles&lt;/em&gt; (PPE) so the tank of magmatic polymer ooze won’t blind him&#xA;when it burns off all his skin.&lt;/ul&gt;&lt;p&gt;I’ve had the privilege to work on several software teams who take&#xA;systems of controls (in security, compliance, and in plain program&#xA;correctness) very seriously. Some practices from process engineering,&#xA;e.g. the “blameless” retrospective reports we write after major&#xA;failures, are commonplace “best practices” in tech already: after a&#xA;major incident, the responding team will usually draw up some take-aways&#xA;and propose some changes (to code or process) to prevent the issue from&#xA;re-occurring.&lt;p&gt;&lt;em&gt;That&lt;/em&gt; juncture — comparing candidate controls — is your cue&#xA;to pull up the NIOSH diagram! The Hierarchy and its common examples are&#xA;written for industrial environments, but the applicability is&#xA;straightforward.&lt;p&gt;You might &lt;strong&gt;eliminate&lt;/strong&gt; an long-disused table of&#xA;sensitive data, &lt;strong&gt;substitute&lt;/strong&gt; a vetted vendor for a&#xA;sketchy one, introduce regression tests — an&#xA;&lt;strong&gt;engineering&lt;/strong&gt; control — or tweak your code review&#xA;practices, an &lt;strong&gt;administrative&lt;/strong&gt; one.&lt;p&gt;Some teams emphasize “defense in depth” when they pick controls.” A&#xA;recent manager of mine put special emphasis on depth in the causal&#xA;chain: can we add a control that makes recurrence less likely along with&#xA;a monitoring control for detecting it sooner?&lt;p&gt;That’s a useful principle, and totally compatible with the Hierarchy&#xA;of Controls model: a process plant doesn’t eschew PPE just because&#xA;they’ve substituted a lesser hazard for a greater one; they, too, defend&#xA;in depth. Practicing defense in depth &lt;em&gt;without&lt;/em&gt; a Hierarchy of&#xA;Controls has three main shortcomings:&lt;ol type=1&gt;&lt;li&gt;&lt;p&gt;It prompts an “incremental” defense. The call for a&#xA;“preventative” control subtly promotes solutions where the same failure&#xA;mode &lt;em&gt;can still structurally occur&lt;/em&gt; but is less likely. This&#xA;underemphasizes options for Elimination and Substitution (e.g. stop&#xA;retaining that sensitive user data rather than trying to secure it).&#xA;This is part of the additive bias I discuss in &lt;a href=./safe-incompatibility.html&gt;“Safety Through&#xA;Incompatibility.”&lt;/a&gt;&lt;li&gt;&lt;p&gt;It encourages quantity over quality. It underemphasizes the&#xA;difference between Engineering controls and Administrative controls.&#xA;Anything that must be read by engineers, enforced in code review,&#xA;covered in training, etc. is an Administrative control. If “we’ll make&#xA;sure it doesn’t happen again” is the best your team has to offer after&#xA;an incident, you have no plan (only a shallow root-cause&#xA;analysis).&lt;li&gt;&lt;p&gt;The last layer of defense, automated monitoring, is some mix of&#xA;Administrative control (“thou shalt respond to this page”) and PPE (“don&#xA;the masks and douse the flames before they spread”). That’s valuable&#xA;only if well-designed — if you’re Substituting a component for the&#xA;hazardous one, your monitoring improvements should cover the&#xA;&lt;em&gt;new&lt;/em&gt; component’s failure modes.&lt;/ol&gt;&lt;p&gt;Just use the Hierarchy. The next time you’re working on an incident&#xA;retrospective, tag the controls you propose by their type — Elimination,&#xA;Substitution, Engineering, Administrative, and PPE. Refer to them by&#xA;name when you review postmortem plans. Complex schemes of weak controls&#xA;mean waste and risk (and wasted weekends, paged to death). No more!&lt;aside id=footnotes class=&#34;footnotes footnotes-end-of-document&#34; role=doc-endnotes&gt;&lt;hr&gt;&lt;ol&gt;&lt;li id=fn1&gt;&lt;p&gt;U.S. Chemical Safety and Hazard Investigation Board.&#xA;&lt;em&gt;Key Lessons for Preventing Inadvertent Mixing During Chemical&#xA;Unloading Operations: Chemical Reaction and Release in Atchison,&#xA;Kansas,&lt;/em&gt; No. 2017-01-I-KS. 2017. &lt;a href=https://www.csb.gov/mgpi-processing-inc-toxic-chemical-release-/&gt;Available&#xA;online.&lt;/a&gt;&lt;a href=#fnref1 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn2&gt;&lt;p&gt;Pretty SVG version &lt;a href=https://commons.wikimedia.org/wiki/File:NIOSH%E2%80%99s_%E2%80%9CHierarchy_of_Controls_infographic%E2%80%9D_as_SVG.svg&gt;sourced&#xA;from Wikimedia&lt;/a&gt;.&lt;a href=#fnref2 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;/ol&gt;&lt;/aside&gt;</content>
    <link href="https://lukasschwab.me/blog/gen/hierarchy-of-controls.html" rel="alternate"></link>
    <summary type="html">Another industrial safety practice your software team should borrow. NIOSH&#39;s Hierarchy of Controls is designed to save industrial workers from horrific injuries; maybe it can save you from your next 3 a.m. on-call emergency.</summary>
  </entry>
  <entry>
    <title>Safety Through Incompatibility</title>
    <updated>2024-11-22T00:00:00Z</updated>
    <id>https://lukasschwab.me/blog/gen/safe-incompatibility.html</id>
    <content type="html">&lt;html xmlns=http://www.w3.org/1999/xhtml lang xml:lang&gt;&lt;meta charset=utf-8&gt;&lt;meta name=generator content=&#34;pandoc&#34;&gt;&lt;meta name=viewport content=&#34;width=device-width,initial-scale=1,user-scalable=yes&#34;&gt;&lt;meta name=author content=&#34;Lukas Schwab&#34;&gt;&lt;meta name=dcterms.date content=&#34;2024-11-22&#34;&gt;&lt;title&gt;Safety Through Incompatibility&lt;/title&gt;&lt;style&gt;&#xA;      code{white-space: pre-wrap;}&#xA;      span.smallcaps{font-variant: small-caps;}&#xA;      span.underline{text-decoration: underline;}&#xA;      div.column{display: inline-block; vertical-align: top; width: 50%;}&#xA;          &lt;/style&gt;&lt;style&gt;pre &gt; code.sourceCode { white-space: pre; position: relative; }&#xA;pre &gt; code.sourceCode &gt; span { display: inline-block; line-height: 1.25; }&#xA;pre &gt; code.sourceCode &gt; span:empty { height: 1.2em; }&#xA;.sourceCode { overflow: visible; }&#xA;code.sourceCode &gt; span { color: inherit; text-decoration: inherit; }&#xA;div.sourceCode { margin: 1em 0; }&#xA;pre.sourceCode { margin: 0; }&#xA;@media screen {&#xA;div.sourceCode { overflow: auto; }&#xA;}&#xA;@media print {&#xA;pre &gt; code.sourceCode { white-space: pre-wrap; }&#xA;pre &gt; code.sourceCode &gt; span { text-indent: -5em; padding-left: 5em; }&#xA;}&#xA;pre.numberSource code&#xA;  { counter-reset: source-line 0; }&#xA;pre.numberSource code &gt; span&#xA;  { position: relative; left: -4em; counter-increment: source-line; }&#xA;pre.numberSource code &gt; span &gt; a:first-child::before&#xA;  { content: counter(source-line);&#xA;    position: relative; left: -1em; text-align: right; vertical-align: baseline;&#xA;    border: none; display: inline-block;&#xA;    -webkit-touch-callout: none; -webkit-user-select: none;&#xA;    -khtml-user-select: none; -moz-user-select: none;&#xA;    -ms-user-select: none; user-select: none;&#xA;    padding: 0 4px; width: 4em;&#xA;    color: #aaaaaa;&#xA;  }&#xA;pre.numberSource { margin-left: 3em; border-left: 1px solid #aaaaaa;  padding-left: 4px; }&#xA;div.sourceCode&#xA;  {   }&#xA;@media screen {&#xA;pre &gt; code.sourceCode &gt; span &gt; a:first-child::before { text-decoration: underline; }&#xA;}&#xA;code span.al { color: #ff0000; font-weight: bold; } /* Alert */&#xA;code span.an { color: #60a0b0; font-weight: bold; font-style: italic; } /* Annotation */&#xA;code span.at { color: #7d9029; } /* Attribute */&#xA;code span.bn { color: #40a070; } /* BaseN */&#xA;code span.bu { color: #008000; } /* BuiltIn */&#xA;code span.cf { color: #007020; font-weight: bold; } /* ControlFlow */&#xA;code span.ch { color: #4070a0; } /* Char */&#xA;code span.cn { color: #880000; } /* Constant */&#xA;code span.co { color: #60a0b0; font-style: italic; } /* Comment */&#xA;code span.cv { color: #60a0b0; font-weight: bold; font-style: italic; } /* CommentVar */&#xA;code span.do { color: #ba2121; font-style: italic; } /* Documentation */&#xA;code span.dt { color: #902000; } /* DataType */&#xA;code span.dv { color: #40a070; } /* DecVal */&#xA;code span.er { color: #ff0000; font-weight: bold; } /* Error */&#xA;code span.ex { } /* Extension */&#xA;code span.fl { color: #40a070; } /* Float */&#xA;code span.fu { color: #06287e; } /* Function */&#xA;code span.im { color: #008000; font-weight: bold; } /* Import */&#xA;code span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Information */&#xA;code span.kw { color: #007020; font-weight: bold; } /* Keyword */&#xA;code span.op { color: #666666; } /* Operator */&#xA;code span.ot { color: #007020; } /* Other */&#xA;code span.pp { color: #bc7a00; } /* Preprocessor */&#xA;code span.sc { color: #4070a0; } /* SpecialChar */&#xA;code span.ss { color: #bb6688; } /* SpecialString */&#xA;code span.st { color: #4070a0; } /* String */&#xA;code span.va { color: #19177c; } /* Variable */&#xA;code span.vs { color: #4070a0; } /* VerbatimString */&#xA;code span.wa { color: #60a0b0; font-weight: bold; font-style: italic; } /* Warning */&lt;/style&gt;&lt;link rel=stylesheet href=../styles/common.css&gt;&lt;script data-goatcounter=https://lukasschwab.goatcounter.com/count async src=//gc.zgo.at/count.js&gt;&lt;/script&gt;&lt;a href=../index.html&gt;&amp;#171; blog&lt;/a&gt; &lt;span class=date&gt;2024-11-22&lt;/span&gt;&lt;header id=title-block-header&gt;&lt;h1 class=title&gt;Safety Through Incompatibility&lt;/h1&gt;&lt;/header&gt;&lt;p&gt;October 21, 2016: a Harcros Chemicals delivery truck arrives at the&#xA;MGPI Processing plant in Atchinson, Kansas for a routine early-morning&#xA;delivery of sulfuric acid. Together with a plant operator, the driver&#xA;runs a hose from their trailer tank of pressurized sulfuric acid to one&#xA;of the facility’s fill lines, checks for leaks, and starts the transfer&#xA;of acid.&lt;p&gt;Not fifteen minutes later, a yellow-green cloud erupts from one of&#xA;the facility’s holding tanks. It swallows the delivery truck. It engulfs&#xA;the nearby control building; the operators scramble from the plant on&#xA;foot. The driver takes refuge in a wastewater treatment plant nearby,&#xA;and the green cloud — highly toxic chlorine gas — drifts northeast, on&#xA;the wind, over Atchison.&lt;hr&gt;&lt;p&gt;How does a routine delivery cause an uncontrolled release of&#xA;chlorine? Through a reaction between the delivered acid and a tank of&#xA;sodium hypochlorite. At the MGPI plant, stocks of these two&#xA;“incompatible” chemicals are loaded from the same connection area, using&#xA;&lt;em&gt;an identical hose connection.&lt;/em&gt; The driver connected to the wrong&#xA;unmarked fill line.&lt;figure&gt;&lt;img src=../img/safe-incompatibility/connectors.png alt=&#34;“Neither sodium hypochlorite nor sulfuric acid fill lines had pipe markers or identification tags affixed at connection points.”&#34;&gt;&lt;figcaption aria-hidden=true&gt;“Neither sodium hypochlorite nor sulfuric&#xA;acid fill lines had pipe markers or identification tags affixed at&#xA;connection points.”&lt;a href=#fn1 class=footnote-ref id=fnref1 role=doc-noteref&gt;&lt;sup&gt;1&lt;/sup&gt;&lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;Anyone who’s done their time on a rushed software engineering team&#xA;has cleaned up an analogous accident: two things, usually separated, are&#xA;mixed by accident or mistaken design.&lt;p&gt;What did it cost you? A couple hours of work and some griping in&#xA;Slack? For a chemical process engineer (or any other species of&#xA;&lt;em&gt;real&lt;/em&gt; engineer), the stakes are astronomically higher. We have&#xA;much to learn.&lt;p&gt;Industrial chemical accidents in the U.S. are subject to&#xA;investigation by the U.S. Chemical Safety and Hazard Investigation Board&#xA;(USCSB). According to their report, the Atchison incident teaches a “Key&#xA;Lesson:” if components should never be connected, make it impossible to&#xA;connect them!&lt;blockquote&gt;&lt;p&gt;Work with chemical distributors to &lt;em&gt;select hose couplings and fill&#xA;line connections with uniquely shaped and color-coded fittings for each&#xA;chemical or class of chemicals,&lt;/em&gt; especially where severe chemicals&#xA;are unloaded in close proximity. This can include a combination of&#xA;accepted fittings with unique shapes (e.g., square for acids, hexagon&#xA;for bases) or different sized diameters (e.g., 2-inch or 3-inch round)&#xA;for each fill line.&lt;a href=#fn2 class=footnote-ref id=fnref2 role=doc-noteref&gt;&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt;&lt;figure&gt;&lt;img src=../img/safe-incompatibility/incompatible.png alt=&#34;Figure 10. Combination of fill line shapes and sizes to avoid incorrect connections during deliveries (Source: CSB).&#34;&gt;&lt;figcaption aria-hidden=true&gt;&lt;strong&gt;Figure 10.&lt;/strong&gt; Combination&#xA;of fill line shapes and sizes to avoid incorrect connections during&#xA;deliveries (Source: CSB).&lt;/figcaption&gt;&lt;/figure&gt;&lt;/blockquote&gt;&lt;hr&gt;&lt;p&gt;An early-career software engineer generally works additively. One&#xA;starts with some raw stuff (an empty project structure, then some&#xA;language primitives, some data…) and accrues features and affordances&#xA;until the program is &lt;em&gt;at least&lt;/em&gt; capable of completing some&#xA;task.&lt;p&gt;Considering &lt;em&gt;capabilities as hazards&lt;/em&gt; doesn’t come as&#xA;naturally, even though examples abound. There are examples in our&#xA;ancient jokes: &lt;code&gt;rm&lt;/code&gt; is hazardously compatible with your&#xA;precious root directory. That’s fine for a library, but death in a&#xA;system&lt;a href=#fn3 class=footnote-ref id=fnref3 role=doc-noteref&gt;&lt;sup&gt;3&lt;/sup&gt;&lt;/a&gt; — you don’t need an API endpoint&#xA;that drops your production database, even though your database libraries&#xA;could do so.&lt;p&gt;Unfortunately for our safety analysis, the raw &lt;em&gt;stuff&lt;/em&gt; of&#xA;software engineering is less definite than the raw stuff of chemical&#xA;plants (you know, the chemicals).&lt;p&gt;One valuable analogy is &lt;em&gt;types.&lt;/em&gt; A function’s parameters are&#xA;its fill lines — they constrain what sorts of data can be pumped into&#xA;it. Whereas chemical process engineers use incompatible hose couplings,&#xA;programmers can elect to use incompatible &lt;em&gt;types&lt;/em&gt; to avoid&#xA;mix-ups.&lt;p&gt;&lt;a href=https://en.wikipedia.org/wiki/Strongly_typed_identifier&gt;Strongly&#xA;typed identifiers&lt;/a&gt; are a classic example of this principle. While&#xA;there’s some underlying primitive type (e.g. an integer representing&#xA;auto-incrementing IDs, or a string representation for a UUID&lt;a href=#fn4 class=footnote-ref id=fnref4 role=doc-noteref&gt;&lt;sup&gt;4&lt;/sup&gt;&lt;/a&gt;) it’s categorically incorrect to use&#xA;an ID from one table to perform a lookup in another.&lt;p&gt;You can incorrectly mix IDs from different tables in queries, of&#xA;course. Consider this buggy SQL query:&lt;div class=sourceCode id=cb1&gt;&lt;pre class=&#34;sourceCode sql&#34;&gt;&lt;code class=&#34;sourceCode sql&#34;&gt;&lt;span id=cb1-1&gt;&lt;a href=#cb1-1 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=kw&gt;SELECT&lt;/span&gt; cart.&lt;span class=kw&gt;id&lt;/span&gt;, cart.customer_id&lt;/span&gt;&#xA;&lt;span id=cb1-2&gt;&lt;a href=#cb1-2 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=kw&gt;FROM&lt;/span&gt; customers &lt;span class=kw&gt;AS&lt;/span&gt; customer&lt;/span&gt;&#xA;&lt;span id=cb1-3&gt;&lt;a href=#cb1-3 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=kw&gt;JOIN&lt;/span&gt; carts &lt;span class=kw&gt;AS&lt;/span&gt; cart&lt;/span&gt;&#xA;&lt;span id=cb1-4&gt;&lt;a href=#cb1-4 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;  &lt;span class=co&gt;-- This *should* match cart.customer_id to customer.id, but doesn&amp;#39;t.&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb1-5&gt;&lt;a href=#cb1-5 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;  &lt;span class=kw&gt;ON&lt;/span&gt; customer.&lt;span class=kw&gt;id&lt;/span&gt; &lt;span class=op&gt;=&lt;/span&gt; cart.&lt;span class=kw&gt;id&lt;/span&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A library may allow this kind of mistake, but your program shouldn’t.&#xA;A good ORM should support strongly typed identifiers out of the box. You&#xA;may need to carefully massage raw ID representations into the correct ID&#xA;types at your system boundaries (e.g. in your API handlers), but then&#xA;you can code within that boundary without paranoia.&lt;p&gt;These kinds of mistakes &lt;em&gt;happen.&lt;/em&gt; They make nightmarish bugs:&#xA;updates meant for one entity are applied to an arbitrary other entity in&#xA;its table (Customer A adds an item to Customer B’s cart — yikes!). They&#xA;also slip through code review, especially in tandem with&#xA;over-abbreviated variable names. A team I worked with recently switched&#xA;to typed IDs, and uncovered a handful of these bugs &lt;em&gt;in&#xA;production,&lt;/em&gt; tucked away in sleepy code-paths where their filthy&#xA;impact went unnoticed.&lt;div class=sourceCode id=cb2&gt;&lt;pre class=&#34;sourceCode go&#34;&gt;&lt;code class=&#34;sourceCode go&#34;&gt;&lt;span id=cb2-1&gt;&lt;a href=#cb2-1 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=co&gt;// Less safe: inadvertent mixing.&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb2-2&gt;&lt;a href=#cb2-2 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=kw&gt;func&lt;/span&gt; unsafeDeleteCustomer&lt;span class=op&gt;(&lt;/span&gt;id &lt;span class=dt&gt;int&lt;/span&gt;&lt;span class=op&gt;)&lt;/span&gt; &lt;span class=op&gt;{&lt;/span&gt; &lt;span class=op&gt;...&lt;/span&gt; &lt;span class=op&gt;}&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb2-3&gt;&lt;a href=#cb2-3 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=kw&gt;func&lt;/span&gt; unsafeOperation&lt;span class=op&gt;(&lt;/span&gt;c Cart&lt;span class=op&gt;)&lt;/span&gt; &lt;span class=op&gt;{&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb2-4&gt;&lt;a href=#cb2-4 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;  &lt;span class=co&gt;// Compiles, but uses a cart ID to look up a customer.&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb2-5&gt;&lt;a href=#cb2-5 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;  unsafeDeleteCustomer&lt;span class=op&gt;(&lt;/span&gt;c&lt;span class=op&gt;.&lt;/span&gt;id&lt;span class=op&gt;)&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb2-6&gt;&lt;a href=#cb2-6 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=op&gt;}&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb2-7&gt;&lt;a href=#cb2-7 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;/span&gt;&#xA;&lt;span id=cb2-8&gt;&lt;a href=#cb2-8 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=co&gt;// More safe, and more expressive to boot!&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb2-9&gt;&lt;a href=#cb2-9 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=kw&gt;func&lt;/span&gt; deleteCustomer&lt;span class=op&gt;(&lt;/span&gt;id CustomerId&lt;span class=op&gt;)&lt;/span&gt; &lt;span class=op&gt;{&lt;/span&gt; &lt;span class=op&gt;...&lt;/span&gt; &lt;span class=op&gt;}&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb2-10&gt;&lt;a href=#cb2-10 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=kw&gt;func&lt;/span&gt; operation&lt;span class=op&gt;(&lt;/span&gt;c Cart&lt;span class=op&gt;)&lt;/span&gt; &lt;span class=op&gt;{&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb2-11&gt;&lt;a href=#cb2-11 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;  &lt;span class=co&gt;// Compiler error! c.id is not a CustomerId.&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb2-12&gt;&lt;a href=#cb2-12 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;  deleteCustomer&lt;span class=op&gt;(&lt;/span&gt;c&lt;span class=op&gt;.&lt;/span&gt;id&lt;span class=op&gt;)&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb2-13&gt;&lt;a href=#cb2-13 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=op&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This type-level safety through incompatibility is the generalized&#xA;response to the code smell Martin Fowler terms “primitive obsession.”&lt;a href=#fn5 class=footnote-ref id=fnref5 role=doc-noteref&gt;&lt;sup&gt;5&lt;/sup&gt;&lt;/a&gt; Using proper &lt;a href=https://en.wikipedia.org/wiki/Enumerated_type&gt;closed enums&lt;/a&gt;&#xA;over grab-bags of named values is another neat example; as for typed&#xA;IDs, a good ORM and a good database schema do much of the heavy&#xA;lifting.&lt;hr&gt;&lt;p&gt;Other incompatibilities are more specific to your system, your&#xA;business logic, or the quirks of the data you inherited; these are&#xA;harder to spot, but just as important. I recently worked on a system&#xA;where two different sources of activity data were keyed differently:&lt;ol type=1&gt;&lt;li&gt;&lt;p&gt;Granular &lt;code&gt;transactions&lt;/code&gt;, dated by when they&#xA;occurred.&lt;li&gt;&lt;p&gt;Daily &lt;code&gt;summaries&lt;/code&gt;, dated by &lt;em&gt;when they are&#xA;generated.&lt;/em&gt; The summary with date &lt;code&gt;August 10, 2024&lt;/code&gt;&#xA;actually summarizes transactions dated to&#xA;&lt;code&gt;August 9, 2024&lt;/code&gt;!&lt;/ol&gt;&lt;p&gt;The difference in those dates — a poor data model design, to be sure&#xA;— was a constant stumbling block for new engineers: it seemed like the&#xA;two dates would naturally correspond, but treating them interchangeably&#xA;(comparing them to join sets of &lt;code&gt;transactions&lt;/code&gt; with&#xA;&lt;code&gt;summaries&lt;/code&gt;) is completely misleading!&lt;a href=#fn6 class=footnote-ref id=fnref6 role=doc-noteref&gt;&lt;sup&gt;6&lt;/sup&gt;&lt;/a&gt;&lt;p&gt;We initially tried to solve this with what the USCSB would term an&#xA;“administrative control:” authors were expected to understand the&#xA;difference between the two dates, and reviewers were expected to confirm&#xA;they were used correctly. Even if you can remember to do it, this is&#xA;hard code to refactor. That &lt;code&gt;activityDate&lt;/code&gt; argument date your&#xA;function receives — what kind of date is it? Has it already been&#xA;shifted?&lt;p&gt;Selective incompatibility proved a stronger solution. These were both&#xA;dates, sure, but they &lt;em&gt;should not mix.&lt;/em&gt; Define two wrappers for&#xA;date types — &lt;code&gt;SnapshotDate&lt;/code&gt; and &lt;code&gt;TransactionDate&lt;/code&gt;&#xA;— that can’t be directly compared. Instead, define projections from one&#xA;type into the other.&lt;hr&gt;&lt;p&gt;The higher-level lesson for a software engineer reading the Atchison&#xA;incident report isn’t specific to “accidental mixing” (whether acids or&#xA;IDs). This incident analysis, with its parallels in software,&#xA;illustrates the value of drawing safety lessons from older, more&#xA;spectacularly dangerous engineering disciplines.&lt;a href=#fn7 class=footnote-ref id=fnref7 role=doc-noteref&gt;&lt;sup&gt;7&lt;/sup&gt;&lt;/a&gt;&#xA;There is nothing new under the sun.&lt;p&gt;Nothing new for process safety engineers, either.&lt;p&gt;Trevor Kletz’s 1985 &lt;em&gt;Engineer’s View of Human Error&lt;/em&gt; describes&#xA;“wrong connections.” His explanation of the concept gives an example&#xA;from the dawn of anesthesia:&lt;blockquote&gt;&lt;p&gt;Figure 2.13 shows the simple apparatus devised in 1867, in the early&#xA;days of anaesthetics, to mix chloroform vapour with air and deliver it&#xA;to the patient. If it was connected up the wrong way round liquid&#xA;chloroform was blown into the patient with results that could be fatal.&#xA;Redesigning the apparatus so that the two pipes could not be&#xA;interchanged was easy; all that was needed were different types of&#xA;connection or different sizes of pipe. Persuading doctors to use the new&#xA;design was more difficult and the old design was still killing people in&#xA;1928. Doctors believed that highly skilled professional men would not&#xA;make such a simple error but as we have seen everyone can make slips&#xA;occur however well-motivated and well-trained, in fact, only when&#xA;well-trained.&lt;a href=#fn8 class=footnote-ref id=fnref8 role=doc-noteref&gt;&lt;sup&gt;8&lt;/sup&gt;&lt;/a&gt;&lt;figure&gt;&lt;img src=../img/safe-incompatibility/chloroform.png alt=&#34;Figure 2.13 It was easy to connect it up the wrong way round and blow liquid chloroform into the patient.&#34;&gt;&lt;figcaption aria-hidden=true&gt;&lt;strong&gt;Figure 2.13&lt;/strong&gt; It was easy&#xA;to connect it up the wrong way round and blow liquid chloroform into the&#xA;patient.&lt;/figcaption&gt;&lt;/figure&gt;&lt;/blockquote&gt;&lt;p&gt;The book’s third edition (2001) includes another example:&lt;blockquote&gt;&lt;p&gt;Do not assume that chemical engineers would not make similar errors.&#xA;In 1989, in a polyethylene plant in Texas, a leak of ethylene exploded,&#xA;killing 23 people. The leak occurred because a line was opened for&#xA;repair while the air-operated valve isolating it from the rest of the&#xA;plant was open. It was open because the two compressed air lines, one to&#xA;open the valve and one to close it, had identical couplings, and they&#xA;had been interchanged. […]&lt;a href=#fn9 class=footnote-ref id=fnref9 role=doc-noteref&gt;&lt;sup&gt;9&lt;/sup&gt;&lt;/a&gt;&lt;/blockquote&gt;&lt;p&gt;The problem wasn’t solved in 1989, and it wasn’t solved in 2016; I’d&#xA;bet the U.S. still hasn’t consigned inadvertent mixing accidents to&#xA;history. The ultra-serious process engineers are still learning from&#xA;their past mistakes. So should we — from their mistakes &lt;em&gt;and&lt;/em&gt;&#xA;ours.&lt;/p&gt;&lt;aside id=footnotes class=&#34;footnotes footnotes-end-of-document&#34; role=doc-endnotes&gt;&lt;hr&gt;&lt;ol&gt;&lt;li id=fn1&gt;&lt;p&gt;U.S. Chemical Safety and Hazard Investigation Board.&#xA;&lt;em&gt;Key Lessons for Preventing Inadvertent Mixing During Chemical&#xA;Unloading Operations: Chemical Reaction and Release in Atchison,&#xA;Kansas,&lt;/em&gt; No. 2017-01-I-KS. 2017. &lt;a href=https://www.csb.gov/mgpi-processing-inc-toxic-chemical-release-/&gt;Available&#xA;online.&lt;/a&gt;&lt;a href=#fnref1 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn2&gt;&lt;p&gt;U.S. Chemical Safety and Hazard Investigation Board.&#xA;&lt;em&gt;Key Lessons for Preventing Inadvertent Mixing During Chemical&#xA;Unloading Operations: Chemical Reaction and Release in Atchison,&#xA;Kansas,&lt;/em&gt; No. 2017-01-I-KS. 2017. &lt;a href=https://www.csb.gov/mgpi-processing-inc-toxic-chemical-release-/&gt;Available&#xA;online.&lt;/a&gt;&lt;p&gt;This lesson about incompatible hose couplings is one of&#xA;&lt;em&gt;eleven&lt;/em&gt; in this report. Several others have useful analogues in&#xA;software engineering: applying the &lt;a href=https://en.wikipedia.org/wiki/Hierarchy_of_hazard_controls&gt;hierarchy&#xA;of controls&lt;/a&gt; in hazard&#xA;analysis;&#xA;introduce interlocks to make manual processing safer; physically&#xA;separate and clearly mark incompatible equipement, e.g. the fill lines&#xA;for acid and bleach here; train workers on where to find their PPE and&#xA;how to use emergency shutoff mechanisms.&lt;a href=#fnref2 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn3&gt;&lt;p&gt;This is an important point. Writing code on a team, you&#xA;&lt;em&gt;can&lt;/em&gt; assert that there are categorically incorrect ways to use&#xA;certain resources/services — at least from the current standpoint — and&#xA;you’d do well to preclude that incorrect usage initially. Type-level&#xA;restrictions are easy to unwind later, but hard to introduce after the&#xA;fact.&lt;a href=#fnref3 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn4&gt;&lt;p&gt;UUIDs are intrinsically safer than incrementing integer&#xA;IDs, since a given UUID should be globally unique (or, rather, there’s a&#xA;negligible risk of a collision). Rather than updating an arbitrary&#xA;entity, the following examples would update &lt;em&gt;nothing&lt;/em&gt; — that’s&#xA;preferable.&lt;p&gt;This is a kind of &lt;em&gt;global, data-level&lt;/em&gt;&#xA;safety-through-incompatibility without a clear analog in the Atchison&#xA;incident or USCSB report, and a major advantage for these kinds of ID&#xA;schemes.&lt;a href=#fnref4 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn5&gt;&lt;p&gt;Fowler, Martin. &lt;em&gt;Refactoring: improving the design of&#xA;existing code.&lt;/em&gt; Addison-Wesley Professional, 2018. See also &lt;a href=https://wiki.c2.com/?PrimitiveObsession&gt;this short summary&lt;/a&gt;.&lt;a href=#fnref5 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn6&gt;&lt;p&gt;The two date sets &lt;em&gt;should&lt;/em&gt; really be comparable,&#xA;but their inclusion in myriad accounting and operations queries (with&#xA;little offsets assuming their mismatch!) complicated that migration. A&#xA;database &lt;em&gt;view&lt;/em&gt; on &lt;code&gt;summaries&lt;/code&gt; with a shifted date is&#xA;probably the right starting point.&lt;a href=#fnref6 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn7&gt;&lt;p&gt;I’ve enjoyed books by Trevor Kletz and papers by Nancy&#xA;Leveson on this point. It bears mentioning that much of software safety&#xA;culture — e.g. blameless retrospectives, root-cause analysis, and&#xA;“technical risk assessments” — are derived from principles in industrial&#xA;safety (usually after some watering-down). We share an underlying&#xA;language of&#xA;“controls.”&lt;a href=#fnref7 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn8&gt;&lt;p&gt;Kletz, Trevor. &lt;em&gt;An Engineer’s View of Human&#xA;Error,&lt;/em&gt; Third Edition. Taylor &amp; Francis, 2001. Pages 31–32.&lt;a href=#fnref8 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn9&gt;&lt;p&gt;Kletz, Trevor. &lt;em&gt;An Engineer’s View of Human&#xA;Error,&lt;/em&gt; Third Edition. Taylor &amp; Francis, 2001. Pages 31–32.&lt;a href=#fnref9 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;/ol&gt;&lt;/aside&gt;</content>
    <link href="https://lukasschwab.me/blog/gen/safe-incompatibility.html" rel="alternate"></link>
    <summary type="html">A 2016 &#34;accidental mixing&#34; incident shows you should *introduce* incompatibility in a software system --- type your IDs, close your enums, and preempt dangerous comparisons. Initial notes on lessons learned from process safety engineering.</summary>
  </entry>
  <entry>
    <title>&#39;Magic&#39; in Elixir&#39;s `Logger`</title>
    <updated>2024-11-03T00:00:00Z</updated>
    <id>https://lukasschwab.me/blog/gen/elixir-logger-magic.html</id>
    <content type="html">&lt;html xmlns=http://www.w3.org/1999/xhtml lang xml:lang&gt;&lt;meta charset=utf-8&gt;&lt;meta name=generator content=&#34;pandoc&#34;&gt;&lt;meta name=viewport content=&#34;width=device-width,initial-scale=1,user-scalable=yes&#34;&gt;&lt;meta name=dcterms.date content=&#34;2024-11-03&#34;&gt;&lt;title&gt;‘Magic’ in Elixir’s Logger&lt;/title&gt;&lt;style&gt;&#xA;      code{white-space: pre-wrap;}&#xA;      span.smallcaps{font-variant: small-caps;}&#xA;      span.underline{text-decoration: underline;}&#xA;      div.column{display: inline-block; vertical-align: top; width: 50%;}&#xA;          &lt;/style&gt;&lt;style&gt;pre &gt; code.sourceCode { white-space: pre; position: relative; }&#xA;pre &gt; code.sourceCode &gt; span { display: inline-block; line-height: 1.25; }&#xA;pre &gt; code.sourceCode &gt; span:empty { height: 1.2em; }&#xA;.sourceCode { overflow: visible; }&#xA;code.sourceCode &gt; span { color: inherit; text-decoration: inherit; }&#xA;div.sourceCode { margin: 1em 0; }&#xA;pre.sourceCode { margin: 0; }&#xA;@media screen {&#xA;div.sourceCode { overflow: auto; }&#xA;}&#xA;@media print {&#xA;pre &gt; code.sourceCode { white-space: pre-wrap; }&#xA;pre &gt; code.sourceCode &gt; span { text-indent: -5em; padding-left: 5em; }&#xA;}&#xA;pre.numberSource code&#xA;  { counter-reset: source-line 0; }&#xA;pre.numberSource code &gt; span&#xA;  { position: relative; left: -4em; counter-increment: source-line; }&#xA;pre.numberSource code &gt; span &gt; a:first-child::before&#xA;  { content: counter(source-line);&#xA;    position: relative; left: -1em; text-align: right; vertical-align: baseline;&#xA;    border: none; display: inline-block;&#xA;    -webkit-touch-callout: none; -webkit-user-select: none;&#xA;    -khtml-user-select: none; -moz-user-select: none;&#xA;    -ms-user-select: none; user-select: none;&#xA;    padding: 0 4px; width: 4em;&#xA;    color: #aaaaaa;&#xA;  }&#xA;pre.numberSource { margin-left: 3em; border-left: 1px solid #aaaaaa;  padding-left: 4px; }&#xA;div.sourceCode&#xA;  {   }&#xA;@media screen {&#xA;pre &gt; code.sourceCode &gt; span &gt; a:first-child::before { text-decoration: underline; }&#xA;}&#xA;code span.al { color: #ff0000; font-weight: bold; } /* Alert */&#xA;code span.an { color: #60a0b0; font-weight: bold; font-style: italic; } /* Annotation */&#xA;code span.at { color: #7d9029; } /* Attribute */&#xA;code span.bn { color: #40a070; } /* BaseN */&#xA;code span.bu { color: #008000; } /* BuiltIn */&#xA;code span.cf { color: #007020; font-weight: bold; } /* ControlFlow */&#xA;code span.ch { color: #4070a0; } /* Char */&#xA;code span.cn { color: #880000; } /* Constant */&#xA;code span.co { color: #60a0b0; font-style: italic; } /* Comment */&#xA;code span.cv { color: #60a0b0; font-weight: bold; font-style: italic; } /* CommentVar */&#xA;code span.do { color: #ba2121; font-style: italic; } /* Documentation */&#xA;code span.dt { color: #902000; } /* DataType */&#xA;code span.dv { color: #40a070; } /* DecVal */&#xA;code span.er { color: #ff0000; font-weight: bold; } /* Error */&#xA;code span.ex { } /* Extension */&#xA;code span.fl { color: #40a070; } /* Float */&#xA;code span.fu { color: #06287e; } /* Function */&#xA;code span.im { color: #008000; font-weight: bold; } /* Import */&#xA;code span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Information */&#xA;code span.kw { color: #007020; font-weight: bold; } /* Keyword */&#xA;code span.op { color: #666666; } /* Operator */&#xA;code span.ot { color: #007020; } /* Other */&#xA;code span.pp { color: #bc7a00; } /* Preprocessor */&#xA;code span.sc { color: #4070a0; } /* SpecialChar */&#xA;code span.ss { color: #bb6688; } /* SpecialString */&#xA;code span.st { color: #4070a0; } /* String */&#xA;code span.va { color: #19177c; } /* Variable */&#xA;code span.vs { color: #4070a0; } /* VerbatimString */&#xA;code span.wa { color: #60a0b0; font-weight: bold; font-style: italic; } /* Warning */&lt;/style&gt;&lt;link rel=stylesheet href=../styles/common.css&gt;&lt;link rel=stylesheet href=../styles/fence-extensions/addendum.css&gt;&lt;script data-goatcounter=https://lukasschwab.goatcounter.com/count async src=//gc.zgo.at/count.js&gt;&lt;/script&gt;&lt;a href=../index.html&gt;&amp;#171; blog&lt;/a&gt; &lt;span class=date&gt;2024-11-03&lt;/span&gt;&lt;header id=title-block-header&gt;&lt;h1 class=title&gt;‘Magic’ in Elixir’s &lt;code&gt;Logger&lt;/code&gt;&lt;/h1&gt;&lt;/header&gt;&lt;div class=addendum data-date=&#34;Nov. 3, 2024&#34;&gt;&lt;p&gt;I first drafted this post in December 2022. I stopped following&#xA;progress in Elixir shortly after; maybe its logging has completely&#xA;changed! That’s besides the point: this is a post about &lt;em&gt;control&lt;/em&gt;&#xA;and &lt;em&gt;cognitive load,&lt;/em&gt; specifically with process-scoped global&#xA;variables. Even if the examples go stale, the principle holds.&lt;/div&gt;&lt;p&gt;Consider this Elixir function:&lt;div class=sourceCode id=cb1&gt;&lt;pre class=&#34;sourceCode elixir&#34;&gt;&lt;code class=&#34;sourceCode elixir&#34;&gt;&lt;span id=cb1-1&gt;&lt;a href=#cb1-1 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=kw&gt;def&lt;/span&gt; log_and_operate&lt;span class=fu&gt;(&lt;/span&gt;trace_id&lt;span class=fu&gt;)&lt;/span&gt; &lt;span class=kw&gt;do&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb1-2&gt;&lt;a href=#cb1-2 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;  &lt;span class=cn&gt;Logger&lt;/span&gt;&lt;span class=op&gt;.&lt;/span&gt;metadata&lt;span class=fu&gt;(&lt;/span&gt;&lt;span class=va&gt;trace_id:&lt;/span&gt; trace_id&lt;span class=fu&gt;)&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb1-3&gt;&lt;a href=#cb1-3 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;  &lt;span class=cn&gt;Logger&lt;/span&gt;&lt;span class=op&gt;.&lt;/span&gt;info&lt;span class=fu&gt;(&lt;/span&gt;&lt;span class=st&gt;&amp;quot;Reached pre-operation&amp;quot;&lt;/span&gt;&lt;span class=fu&gt;)&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb1-4&gt;&lt;a href=#cb1-4 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;  operate&lt;span class=fu&gt;()&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb1-5&gt;&lt;a href=#cb1-5 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;  &lt;span class=cn&gt;Logger&lt;/span&gt;&lt;span class=op&gt;.&lt;/span&gt;info&lt;span class=fu&gt;(&lt;/span&gt;&lt;span class=st&gt;&amp;quot;Reached post-operation&amp;quot;&lt;/span&gt;&lt;span class=fu&gt;)&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb1-6&gt;&lt;a href=#cb1-6 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=kw&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Setting a &lt;code&gt;trace_id&lt;/code&gt; in your structured logger should&#xA;contextualize one log entry by locating it in a stream of other log&#xA;entries with the same key. You include it because you want to fetch your&#xA;logs later, search for something like&#xA;&lt;code&gt;trace_id:&#34;correct-horse&#34;&lt;/code&gt;, and see logs like this:&lt;pre&gt;&lt;code&gt;{trace_id: &amp;quot;correct-horse&amp;quot;, message: &amp;quot;Reached pre-operation&amp;quot;}&#xA;# ...logs from operate()&#xA;{trace_id: &amp;quot;correct-horse&amp;quot;, message: &amp;quot;Reached post-operation&amp;quot;}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;But, in Elixir&lt;a href=#fn1 class=footnote-ref id=fnref1 role=doc-noteref&gt;&lt;sup&gt;1&lt;/sup&gt;&lt;/a&gt;, &lt;code&gt;operate()&lt;/code&gt; has free&#xA;access to all the logger metadata, &lt;em&gt;including&lt;/em&gt; the&#xA;&lt;code&gt;trace_id&lt;/code&gt; field doing the correlating! If it so chooses,&#xA;it’ll break your &lt;code&gt;trace_id&lt;/code&gt; connection between the pre- and&#xA;post-operation log lines.&lt;div class=sourceCode id=cb3&gt;&lt;pre class=&#34;sourceCode elixir&#34;&gt;&lt;code class=&#34;sourceCode elixir&#34;&gt;&lt;span id=cb3-1&gt;&lt;a href=#cb3-1 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=kw&gt;def&lt;/span&gt; operate&lt;span class=fu&gt;()&lt;/span&gt; &lt;span class=kw&gt;do&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb3-2&gt;&lt;a href=#cb3-2 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;  &lt;span class=cn&gt;Logger&lt;/span&gt;&lt;span class=op&gt;.&lt;/span&gt;metadata&lt;span class=fu&gt;(&lt;/span&gt;&lt;span class=va&gt;trace_id:&lt;/span&gt; &lt;span class=st&gt;&amp;quot;battery-staple&amp;quot;&lt;/span&gt;&lt;span class=fu&gt;)&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb3-3&gt;&lt;a href=#cb3-3 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;  &lt;span class=cn&gt;Logger&lt;/span&gt;&lt;span class=op&gt;.&lt;/span&gt;info&lt;span class=fu&gt;(&lt;/span&gt;&lt;span class=st&gt;&amp;quot;Changed the trace ID&amp;quot;&lt;/span&gt;&lt;span class=fu&gt;)&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb3-4&gt;&lt;a href=#cb3-4 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=kw&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;pre class=jsonl&gt;&lt;code&gt;{trace_id: &amp;quot;correct-horse&amp;quot;, message: &amp;quot;Reached pre-operation&amp;quot;}&#xA;{trace_id: &amp;quot;battery-staple&amp;quot;, message: &amp;quot;Changed the trace ID&amp;quot;}&#xA;{trace_id: &amp;quot;battery-staple&amp;quot;, message: &amp;quot;Reached post-operation&amp;quot;}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Of course, that’s a reductive example. In practice, your&#xA;&lt;code&gt;trace_id&lt;/code&gt; has to survive a huge tree of&#xA;conditionally-connected codepaths, &lt;em&gt;any of which&lt;/em&gt; could trample&#xA;your precious log metadata. You have to read &lt;em&gt;a lot&lt;/em&gt; of code to&#xA;feel confident that won’t happen.&lt;p&gt;&lt;code&gt;Logger&lt;/code&gt; metadata is a process-scoped variable, so it’s&#xA;intrinsically &lt;em&gt;tied to what happens in that process, and in what&#xA;order&lt;/em&gt; (“control”&lt;a href=#fn2 class=footnote-ref id=fnref2 role=doc-noteref&gt;&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt;). Even if a function with&#xA;&lt;code&gt;Logger&lt;/code&gt; calls doesn’t receive any logger-specific arguments&#xA;— that is, it doesn’t explicitly depend on logging elsewhere — its&#xA;behavior can implicitly depend on all &lt;em&gt;sequentially prior&lt;/em&gt;&#xA;operations in the same process. If you imagine the process’s execution&#xA;as the depth-first traversal of a tree of function calls, this means&#xA;knowing all the previously-visited vertices, not just the lexical&#xA;parents of the current vertex.&lt;p&gt;Elixir’s standard &lt;code&gt;Logger&lt;/code&gt; does what Peter Bourgon calls&#xA;&lt;a href=https://peter.bourgon.org/blog/2017/06/09/theory-of-modern-go.html&gt;magic&lt;/a&gt;:&#xA;it obscures a complex process-global data-interdependency.&#xA;Unfortunately, ruling out influence is an indispensable part of reading&#xA;code. When influence extends to a whole process, and when your program&#xA;is anything but strictly non-recursive, you can’t just treat code&#xA;lexically: you have to mentally model the process execution as you read&#xA;it.&lt;h1 id=functional-work-arounds&gt;Functional work-arounds&lt;/h1&gt;&lt;p&gt;One work-around is to, for some particularly sensitive metadata keys,&#xA;defer to those already on the logger:&lt;div class=sourceCode id=cb5&gt;&lt;pre class=&#34;sourceCode elixir&#34;&gt;&lt;code class=&#34;sourceCode elixir&#34;&gt;&lt;span id=cb5-1&gt;&lt;a href=#cb5-1 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=co&gt;# safe_set_trace_id is a dedicated setter for a single Logger metadata field.&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb5-2&gt;&lt;a href=#cb5-2 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=kw&gt;def&lt;/span&gt; safe_set_trace_id&lt;span class=fu&gt;(&lt;/span&gt;trace_id&lt;span class=fu&gt;)&lt;/span&gt; &lt;span class=kw&gt;do&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb5-3&gt;&lt;a href=#cb5-3 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;  &lt;span class=cf&gt;if&lt;/span&gt;&lt;span class=op&gt; !&lt;/span&gt;&lt;span class=cn&gt;Logger&lt;/span&gt;&lt;span class=op&gt;.&lt;/span&gt;metadata&lt;span class=fu&gt;(&lt;/span&gt;&lt;span class=va&gt;:trace_id&lt;/span&gt;&lt;span class=fu&gt;)&lt;/span&gt; &lt;span class=kw&gt;do&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb5-4&gt;&lt;a href=#cb5-4 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    &lt;span class=cn&gt;Logger&lt;/span&gt;&lt;span class=op&gt;.&lt;/span&gt;metadata&lt;span class=fu&gt;(&lt;/span&gt;&lt;span class=va&gt;trace_id:&lt;/span&gt; trace_id&lt;span class=fu&gt;)&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb5-5&gt;&lt;a href=#cb5-5 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;  &lt;span class=kw&gt;end&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb5-6&gt;&lt;a href=#cb5-6 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=kw&gt;end&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb5-7&gt;&lt;a href=#cb5-7 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;/span&gt;&#xA;&lt;span id=cb5-8&gt;&lt;a href=#cb5-8 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=co&gt;# protected_metadata and safe_set_metadata protect a static set of metadata&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb5-9&gt;&lt;a href=#cb5-9 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=co&gt;# keys.&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb5-10&gt;&lt;a href=#cb5-10 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=ot&gt;@protected_metadata&lt;/span&gt; &lt;span class=ot&gt;[&lt;/span&gt;&lt;span class=va&gt;:trace_id&lt;/span&gt;, &lt;span class=va&gt;:others&lt;/span&gt;&lt;span class=ot&gt;]&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb5-11&gt;&lt;a href=#cb5-11 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=kw&gt;def&lt;/span&gt; safe_set_metadata&lt;span class=fu&gt;(&lt;/span&gt;opts&lt;span class=fu&gt;)&lt;/span&gt; &lt;span class=kw&gt;do&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb5-12&gt;&lt;a href=#cb5-12 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;  opts&lt;/span&gt;&#xA;&lt;span id=cb5-13&gt;&lt;a href=#cb5-13 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;  &lt;span class=op&gt;|&amp;gt;&lt;/span&gt; &lt;span class=cn&gt;Enum&lt;/span&gt;&lt;span class=op&gt;.&lt;/span&gt;each&lt;span class=fu&gt;(&lt;/span&gt;&lt;span class=kw&gt;fn&lt;/span&gt; &lt;span class=fu&gt;{&lt;/span&gt;key, value&lt;span class=fu&gt;}&lt;/span&gt; &lt;span class=op&gt;-&amp;gt;&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb5-14&gt;&lt;a href=#cb5-14 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    &lt;span class=cf&gt;if&lt;/span&gt;&lt;span class=op&gt; !&lt;/span&gt;&lt;span class=cn&gt;Enum&lt;/span&gt;&lt;span class=op&gt;.&lt;/span&gt;member&lt;span class=fu&gt;(&lt;/span&gt;&lt;span class=ot&gt;@protected_metadata&lt;/span&gt;, key&lt;span class=fu&gt;)&lt;/span&gt; &lt;span class=op&gt;|| !&lt;/span&gt;&lt;span class=cn&gt;Logger&lt;/span&gt;&lt;span class=op&gt;.&lt;/span&gt;metadata&lt;span class=fu&gt;(&lt;/span&gt;key&lt;span class=fu&gt;)&lt;/span&gt; &lt;span class=kw&gt;do&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb5-15&gt;&lt;a href=#cb5-15 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;      &lt;span class=cn&gt;Logger&lt;/span&gt;&lt;span class=op&gt;.&lt;/span&gt;metadata&lt;span class=fu&gt;(&lt;/span&gt;key, value&lt;span class=fu&gt;)&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb5-16&gt;&lt;a href=#cb5-16 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;  &lt;span class=kw&gt;end&lt;/span&gt;&lt;span class=fu&gt;)&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb5-17&gt;&lt;a href=#cb5-17 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=kw&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This requires either sharing a canonical set of precious metadata&#xA;keys, then enforcing that every &lt;code&gt;Logger&lt;/code&gt; call checks the&#xA;list, or writing a module wrapping &lt;code&gt;Logger&lt;/code&gt; and forbidding&#xA;direct &lt;code&gt;Logger&lt;/code&gt; calls.&lt;p&gt;Alternatively, you can enforce that keys for correlation be unchanged&#xA;by redundantly(?) re-setting them:&lt;div class=sourceCode id=cb6&gt;&lt;pre class=&#34;sourceCode elixir&#34;&gt;&lt;code class=&#34;sourceCode elixir&#34;&gt;&lt;span id=cb6-1&gt;&lt;a href=#cb6-1 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=kw&gt;def&lt;/span&gt; log_and_operate&lt;span class=fu&gt;(&lt;/span&gt;trace_id&lt;span class=fu&gt;)&lt;/span&gt; &lt;span class=kw&gt;do&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb6-2&gt;&lt;a href=#cb6-2 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;  &lt;span class=cn&gt;Logger&lt;/span&gt;&lt;span class=op&gt;.&lt;/span&gt;metadata&lt;span class=fu&gt;(&lt;/span&gt;&lt;span class=va&gt;trace_id:&lt;/span&gt; trace_id&lt;span class=fu&gt;)&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb6-3&gt;&lt;a href=#cb6-3 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;  &lt;span class=cn&gt;Logger&lt;/span&gt;&lt;span class=op&gt;.&lt;/span&gt;info&lt;span class=fu&gt;(&lt;/span&gt;&lt;span class=st&gt;&amp;quot;Reached pre-operation&amp;quot;&lt;/span&gt;&lt;span class=fu&gt;)&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb6-4&gt;&lt;a href=#cb6-4 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;  operate&lt;span class=fu&gt;()&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb6-5&gt;&lt;a href=#cb6-5 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;  &lt;span class=cn&gt;Logger&lt;/span&gt;&lt;span class=op&gt;.&lt;/span&gt;info&lt;span class=fu&gt;(&lt;/span&gt;&lt;span class=st&gt;&amp;quot;Reached post-operation&amp;quot;&lt;/span&gt;, &lt;span class=va&gt;trace_id:&lt;/span&gt; trace_id&lt;span class=fu&gt;)&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb6-6&gt;&lt;a href=#cb6-6 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=kw&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h1 id=better-explicit-sharing&gt;Better: explicit sharing&lt;/h1&gt;&lt;p&gt;The Go approach is to relay the updates through return values: an&#xA;updated logger or &lt;code&gt;context.Context&lt;/code&gt;, which callers can take&#xA;or leave selectively.&lt;p&gt;In Go convention, instead of having process-scoped logger metadata,&#xA;loggers (with metadata) are &lt;em&gt;objects.&lt;/em&gt; Of course, you commonly&#xA;want to inherit some logger metadata; that’s why you see code building&#xA;on a service-wide base logger:&lt;div class=sourceCode id=cb7&gt;&lt;pre class=&#34;sourceCode go&#34;&gt;&lt;code class=&#34;sourceCode go&#34;&gt;&lt;span id=cb7-1&gt;&lt;a href=#cb7-1 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=kw&gt;func&lt;/span&gt; &lt;span class=op&gt;(&lt;/span&gt;s &lt;span class=op&gt;*&lt;/span&gt;Server&lt;span class=op&gt;)&lt;/span&gt; Operate&lt;span class=op&gt;()&lt;/span&gt; &lt;span class=op&gt;{&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb7-2&gt;&lt;a href=#cb7-2 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;  s&lt;span class=op&gt;.&lt;/span&gt;Logger&lt;span class=op&gt;.&lt;/span&gt;With&lt;span class=op&gt;(&lt;/span&gt;&lt;span class=st&gt;&amp;quot;key&amp;quot;&lt;/span&gt;&lt;span class=op&gt;,&lt;/span&gt; &lt;span class=st&gt;&amp;quot;value&amp;quot;&lt;/span&gt;&lt;span class=op&gt;)&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb7-3&gt;&lt;a href=#cb7-3 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=op&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Extracting a logger from a received context:&lt;div class=sourceCode id=cb8&gt;&lt;pre class=&#34;sourceCode go&#34;&gt;&lt;code class=&#34;sourceCode go&#34;&gt;&lt;span id=cb8-1&gt;&lt;a href=#cb8-1 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=kw&gt;func&lt;/span&gt; Operate&lt;span class=op&gt;(&lt;/span&gt;ctx context&lt;span class=op&gt;.&lt;/span&gt;Context&lt;span class=op&gt;)&lt;/span&gt; &lt;span class=op&gt;{&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb8-2&gt;&lt;a href=#cb8-2 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;  log&lt;span class=op&gt;.&lt;/span&gt;From&lt;span class=op&gt;(&lt;/span&gt;ctx&lt;span class=op&gt;).&lt;/span&gt;With&lt;span class=op&gt;(&lt;/span&gt;&lt;span class=st&gt;&amp;quot;key&amp;quot;&lt;/span&gt;&lt;span class=op&gt;,&lt;/span&gt; &lt;span class=st&gt;&amp;quot;value&amp;quot;&lt;/span&gt;&lt;span class=op&gt;)&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb8-3&gt;&lt;a href=#cb8-3 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=op&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Or receiving a logger from the caller, then forking it — extending&#xA;the received logger’s metadata in a local copy:&lt;div class=sourceCode id=cb9&gt;&lt;pre class=&#34;sourceCode go&#34;&gt;&lt;code class=&#34;sourceCode go&#34;&gt;&lt;span id=cb9-1&gt;&lt;a href=#cb9-1 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=kw&gt;func&lt;/span&gt; Operate&lt;span class=op&gt;(&lt;/span&gt;logger &lt;span class=op&gt;*&lt;/span&gt;zap&lt;span class=op&gt;.&lt;/span&gt;SugaredLogger&lt;span class=op&gt;)&lt;/span&gt; &lt;span class=op&gt;{&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb9-2&gt;&lt;a href=#cb9-2 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;  &lt;span class=kw&gt;var&lt;/span&gt; value &lt;span class=dt&gt;string&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb9-3&gt;&lt;a href=#cb9-3 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;  logger &lt;span class=op&gt;=&lt;/span&gt; logger&lt;span class=op&gt;.&lt;/span&gt;With&lt;span class=op&gt;(&lt;/span&gt;&lt;span class=st&gt;&amp;quot;key&amp;quot;&lt;/span&gt;&lt;span class=op&gt;,&lt;/span&gt; &lt;span class=st&gt;&amp;quot;value&amp;quot;&lt;/span&gt;&lt;span class=op&gt;)&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb9-4&gt;&lt;a href=#cb9-4 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=op&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Even in Go, sometimes you &lt;em&gt;do&lt;/em&gt; want to modify a logger for the&#xA;caller scope. How do you do this without process-scoped magic? Return&#xA;values that let the caller decide what to do with their own logger.&#xA;Admittedly, this can feel a little clumsy — threading contextual data up&#xA;and down the callstack resists later refactoring, and including an&#xA;updated logger among return values feels even weirder. Regardless, at&#xA;least the Go approach is explicit.&lt;p&gt;That’s one of my main complaints about Elixir. Every function call&#xA;implicitly delegates — even just briefly — control of &lt;em&gt;the&#xA;caller’s&lt;/em&gt; logger. If you want to make any strong guarantees about&#xA;the logging behavior of any given function, you have to know&lt;ol type=1&gt;&lt;li&gt;Everything that ran in the process before the function was called,&#xA;if you want to make assumptions about the original state.&lt;li&gt;Everything that runs &lt;em&gt;between&lt;/em&gt; logging calls in that&#xA;function, all the way down.&lt;/ol&gt;&lt;p&gt;And I don’t want to know that much! &lt;em&gt;Excluding&lt;/em&gt; swaths of code&#xA;from consideration is essential to being productive in a big, shared&#xA;codebase (and especially for doing the kind of parachute-in spot&#xA;debugging routinely required in a live product).&lt;/p&gt;&lt;p&gt;My point isn’t that you should pick Go&lt;a href=#fn3 class=footnote-ref id=fnref3 role=doc-noteref&gt;&lt;sup&gt;3&lt;/sup&gt;&lt;/a&gt;,&#xA;but that you should spurn ‘magic,’ particularly the&#xA;“comprehension-subverting magic of globals” (Bourgon), a point on which&#xA;the Go community is pretty disciplined. Think less, get more done;&#xA;better to be ugly and explicit than opaque.&lt;aside id=footnotes class=&#34;footnotes footnotes-end-of-document&#34; role=doc-endnotes&gt;&lt;hr&gt;&lt;ol&gt;&lt;li id=fn1&gt;&lt;p&gt;At its heart, Elixir’s &lt;code&gt;Logger&lt;/code&gt; really uses&#xA;the metadata management API exposed by &lt;a href=https://www.erlang.org/doc/man/logger.html&gt;Erlang’s&#xA;&lt;code&gt;logger&lt;/code&gt; module&lt;/a&gt;.&lt;a href=#fnref1 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn2&gt;&lt;p&gt;For some more formal discussion, see Moseley, Ben, and&#xA;Peter Marks. “Out of the tar pit.” &lt;em&gt;Software Practice Advancement&#xA;(SPA)&lt;/em&gt; 2006 (2006):&lt;blockquote&gt;&lt;p&gt;Control is basically about the &lt;em&gt;order&lt;/em&gt; in which things&#xA;happen.&lt;p&gt;The problem with control is that very often we do not want to have to&#xA;be concerned with this. […]&lt;p&gt;This is because the person reading the code above must effectively&#xA;duplicate the work of the hypothetical compoiler — they must (by virtue&#xA;of the definition of the language semantics) start with the assumption&#xA;that the ordering specified &lt;em&gt;is&lt;/em&gt; seignificant, and then by&#xA;further inspection determine that it is not (in cases less trivial than&#xA;the one above determining this can become very difficult). The problem&#xA;here is that mistakes in this determination can lead to the introduction&#xA;of very subtle and hard-to-find bugs. (p. 8-9)&lt;/blockquote&gt;&lt;p&gt;&lt;a href=https://blog.royalsloth.eu/archive/outOfTheTarPit.pdf&gt;Available&#xA;online&lt;/a&gt;.&lt;a href=#fnref2 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn3&gt;&lt;p&gt;This isn’t to say the language you write doesn’t matter.&#xA;Choose wisely! Some things to consider:&lt;ol type=1&gt;&lt;li&gt;Strong structured logging.&lt;li&gt;Static types.&lt;li&gt;Linters. Do they exist? Are they extensible?&lt;li&gt;Talent. Is the language niche? How much time will engineers spend&#xA;learning it? Can you find community-maintained clients for common APIs,&#xA;or will you have to roll your own?&lt;/ol&gt;&lt;p&gt;Anecdotally, the functional PLs I’ve written professionally (Scala&#xA;and Elixir) have disappointed in several of these respects.&#xA;&lt;a href=#fnref3 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;/ol&gt;&lt;/aside&gt;</content>
    <link href="https://lukasschwab.me/blog/gen/elixir-logger-magic.html" rel="alternate"></link>
    <summary type="html">Not the good kind. These are old notes on the problem of process-scoped variables, an unforced error in Elixir&#39;s structured logging and a common problem in functional programs.</summary>
  </entry>
  <entry>
    <title>Applying &#34;Laws&#34; of Software Evolution</title>
    <updated>2024-10-26T00:00:00Z</updated>
    <id>https://lukasschwab.me/blog/gen/lehman.html</id>
    <content type="html">&lt;html xmlns=http://www.w3.org/1999/xhtml lang xml:lang&gt;&lt;meta charset=utf-8&gt;&lt;meta name=generator content=&#34;pandoc&#34;&gt;&lt;meta name=viewport content=&#34;width=device-width,initial-scale=1,user-scalable=yes&#34;&gt;&lt;meta name=dcterms.date content=&#34;2024-10-26&#34;&gt;&lt;title&gt;Applying “Laws” of Software Evolution&lt;/title&gt;&lt;style&gt;&#xA;      code{white-space: pre-wrap;}&#xA;      span.smallcaps{font-variant: small-caps;}&#xA;      span.underline{text-decoration: underline;}&#xA;      div.column{display: inline-block; vertical-align: top; width: 50%;}&#xA;          &lt;/style&gt;&lt;link rel=stylesheet href=../styles/common.css&gt;&lt;script data-goatcounter=https://lukasschwab.goatcounter.com/count async src=//gc.zgo.at/count.js&gt;&lt;/script&gt;&lt;a href=../index.html&gt;&amp;#171; blog&lt;/a&gt; &lt;span class=date&gt;2024-10-26&lt;/span&gt;&lt;header id=title-block-header&gt;&lt;h1 class=title&gt;Applying “Laws” of Software Evolution&lt;/h1&gt;&lt;/header&gt;&lt;p&gt;Common models can be didactically useful even if they are, basically,&#xA;not correct. That’s how I feel about Lehman’s &lt;a href=https://users.ece.utexas.edu/~perry/education/SE-Intro/lehman.pdf&gt;“Programs,&#xA;Life Cycles, and Laws of Software Evolution,”&lt;/a&gt; published in IEEE Vol.&#xA;68, 1980. The “laws” themselves — reinterpreted to death&lt;a href=#fn1 class=footnote-ref id=fnref1 role=doc-noteref&gt;&lt;sup&gt;1&lt;/sup&gt;&lt;/a&gt; —&#xA;are neither surprising nor axiomatic today.&lt;p&gt;One of the models Lehman uses to construct his laws, on the other&#xA;hand, is a didactic gem: his hierarchy of program complexities (&lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;S&lt;/em&gt;&lt;/span&gt;-, &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;P&lt;/em&gt;&lt;/span&gt;-, and &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;E&lt;/em&gt;&lt;/span&gt;-programs).&lt;a href=#fn2 class=footnote-ref id=fnref2 role=doc-noteref&gt;&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt;&lt;p&gt;Here’s a &lt;em&gt;very condensed&lt;/em&gt; version of those concepts:&lt;table&gt;&lt;col style=&#34;width: 5%&#34;&gt;&lt;col style=&#34;width: 12%&#34;&gt;&lt;col style=&#34;width: 34%&#34;&gt;&lt;col style=&#34;width: 47%&#34;&gt;&lt;thead&gt;&lt;tr class=header&gt;&lt;th style=&#34;text-align: left;&#34;&gt;Class&lt;th style=&#34;text-align: left;&#34;&gt;Characteristic&lt;th style=&#34;text-align: left;&#34;&gt;Example&lt;th style=&#34;text-align: left;&#34;&gt;Characteristics&lt;tbody&gt;&lt;tr class=odd&gt;&lt;td style=&#34;text-align: left;&#34;&gt;&lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;S&lt;/em&gt;&lt;/span&gt;&lt;td style=&#34;text-align: left;&#34;&gt;Specified&lt;td style=&#34;text-align: left;&#34;&gt;Solution to the classical &lt;a href=https://en.wikipedia.org/wiki/Travelling_salesman_problem&gt;travelling&#xA;salesman problem&lt;/a&gt;&lt;td style=&#34;text-align: left;&#34;&gt;“Programs whose function is formally&#xA;defined by and derivable from a specification.”&lt;tr class=even&gt;&lt;td style=&#34;text-align: left;&#34;&gt;&lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;P&lt;/em&gt;&lt;/span&gt;&lt;td style=&#34;text-align: left;&#34;&gt;Abstract&lt;td style=&#34;text-align: left;&#34;&gt;Simulation game where the player directs&#xA;virtual salespeople&lt;td style=&#34;text-align: left;&#34;&gt;“It is a model of an abstraction of a&#xA;real-world situation, containing uncertainties, unknowns, arbitrary&#xA;criteria, continuous variables.”&lt;tr class=odd&gt;&lt;td style=&#34;text-align: left;&#34;&gt;&lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;E&lt;/em&gt;&lt;/span&gt;&lt;td style=&#34;text-align: left;&#34;&gt;Reflexive&lt;td style=&#34;text-align: left;&#34;&gt;Program for managing real salespeople&lt;td style=&#34;text-align: left;&#34;&gt;“The program has become a part of the&#xA;world it models, it is embedded in it.”&lt;/table&gt;&lt;p&gt;Section II of the Lehman paper — “Programs as Models” — includes&#xA;extended examples of each category. Of course, the categories aren’t&#xA;unrelated! Basically “all large programs (software systems) will be&#xA;constructed as structures of &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;S&lt;/em&gt;&lt;/span&gt;-programs,” Lehman explains.&lt;a href=#fn3 class=footnote-ref id=fnref3 role=doc-noteref&gt;&lt;sup&gt;3&lt;/sup&gt;&lt;/a&gt; There’s an analogous compositional&#xA;relationship between &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;P&lt;/em&gt;&lt;/span&gt; and&#xA;&lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;E&lt;/em&gt;&lt;/span&gt;:&lt;figure&gt;&lt;img src=../img/lehman/composition.svg alt=&#34;S programs are functionally composed into P programs, which are temporally and organizationally composed into E programs. I mean “organization” as in “the product or company that pays your salary.”&#34;&gt;&lt;figcaption aria-hidden=true&gt;&lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;S&lt;/em&gt;&lt;/span&gt; programs are functionally composed&#xA;into &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;P&lt;/em&gt;&lt;/span&gt; programs, which are&#xA;&lt;em&gt;temporally&lt;/em&gt; and &lt;em&gt;organizationally&lt;/em&gt; composed into &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;E&lt;/em&gt;&lt;/span&gt; programs. I mean “organization” as&#xA;in “the product or company that pays your salary.”&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;At the complex end, we aren’t typically cobbling several&#xA;&lt;em&gt;independent&lt;/em&gt; &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;P&lt;/em&gt;&lt;/span&gt;-programs together to form &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;E&lt;/em&gt;&lt;/span&gt;-programs. Instead, think of &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;E&lt;/em&gt;&lt;/span&gt;-programs as &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;P&lt;/em&gt;&lt;/span&gt;-programs in the real world. For&#xA;example,&lt;ul&gt;&lt;li&gt;&lt;p&gt;As a product with users: through use, it generates new problems,&#xA;desires, and requirements in the market it addresses. In other words,&#xA;&lt;em&gt;organizational composition.&lt;/em&gt;&lt;li&gt;&lt;p&gt;As an evolving system: it reflexively influences its own progress&#xA;from &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;P&lt;/em&gt;&lt;sub&gt;0&lt;/sub&gt; → &lt;em&gt;P&lt;/em&gt;&lt;sub&gt;1&lt;/sub&gt; → ⋯ → &lt;em&gt;P&lt;/em&gt;&lt;sub&gt;&lt;em&gt;n&lt;/em&gt;&lt;/sub&gt;&lt;/span&gt;&#xA;because each version encourages or discourages subsequent versions. In&#xA;other words, &lt;em&gt;temporal composition.&lt;/em&gt;&lt;/ul&gt;&lt;p&gt;In the second sense, &lt;a href=./technical-debt.html&gt;technical&#xA;debt&lt;/a&gt; is a self-contained &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;E&lt;/em&gt;&lt;/span&gt;-program problem: the code you&#xA;write today impacts your program’s &lt;a href=https://en.wikipedia.org/wiki/Cognitive_dimensions_of_notations#List_of_the_cognitive_dimensions&gt;viscosity&lt;/a&gt;&#xA;— its resistance to change — but also determines the nature and urgency&#xA;of future changes. Managing tech debt &lt;em&gt;well&lt;/em&gt; requires&#xA;foresight.&lt;/p&gt;&lt;p&gt;Almost by definition, &lt;em&gt;useful&lt;/em&gt; software engineering&#xA;contributes at least indirectly to some &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;E&lt;/em&gt;&lt;/span&gt;-program, “mechanize[s] a human or&#xA;societal activity.”&lt;a href=#fn4 class=footnote-ref id=fnref4 role=doc-noteref&gt;&lt;sup&gt;4&lt;/sup&gt;&lt;/a&gt; Even really simple programs — basic&#xA;Unix commands, for example — are &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;E&lt;/em&gt;&lt;/span&gt;-programs by nature of their&#xA;widespread use.&lt;p&gt;In fact, &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;E&lt;/em&gt;&lt;/span&gt;-program-style&#xA;problems are so ubiquitous we should question Lehman’s trichotomy. Are&#xA;they really a &lt;em&gt;distinct&lt;/em&gt; kind of program? Or should we focus on&#xA;&lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;E&lt;/em&gt;&lt;/span&gt;-&lt;em&gt;programming&lt;/em&gt; as a&#xA;verb instead, a way of forecasting-despite-reflexivity in &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;S&lt;/em&gt;&lt;/span&gt;- and &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;P&lt;/em&gt;&lt;/span&gt;-programs?&lt;p&gt;&lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;E&lt;/em&gt;&lt;/span&gt;-program skills are not&#xA;underrated, but they &lt;em&gt;are&lt;/em&gt; under-discussed.&lt;p&gt;Randall Koutnik &lt;a href=https://rkoutnik.com/2016/04/21/implementers-solvers-and-finders.html&gt;suggests&lt;/a&gt;&#xA;we replace traditional SWE levels with terms for the kind of work we do.&#xA;In this model, we develop &lt;em&gt;autonomy&lt;/em&gt; rather than seniority. You&#xA;started as an “implementer” of concrete specs, became a “solver” of more&#xA;loosely-defined problems, and finally grew to be a “finder” of problems&#xA;to solve. Sounds familiar? That three-stage development corresponds to&#xA;the kinds of thinking required by each of Lehman’s program classes.&lt;table&gt;&lt;col style=&#34;width: 32%&#34;&gt;&lt;col style=&#34;width: 27%&#34;&gt;&lt;col style=&#34;width: 40%&#34;&gt;&lt;thead&gt;&lt;tr class=header&gt;&lt;th style=&#34;text-align: left;&#34;&gt;Koutnik level&lt;a href=#fn5 class=footnote-ref id=fnref5 role=doc-noteref&gt;&lt;sup&gt;5&lt;/sup&gt;&lt;/a&gt;&lt;th style=&#34;text-align: left;&#34;&gt;Koutnik role&lt;th style=&#34;text-align: left;&#34;&gt;Lehman program class&lt;tbody&gt;&lt;tr class=odd&gt;&lt;td style=&#34;text-align: left;&#34;&gt;Junior&lt;td style=&#34;text-align: left;&#34;&gt;Implementer&lt;td style=&#34;text-align: left;&#34;&gt;&lt;em&gt;S&lt;/em&gt;-programs&lt;tr class=even&gt;&lt;td style=&#34;text-align: left;&#34;&gt;Regular&lt;td style=&#34;text-align: left;&#34;&gt;Solver&lt;td style=&#34;text-align: left;&#34;&gt;&lt;em&gt;P&lt;/em&gt;-programs&lt;tr class=odd&gt;&lt;td style=&#34;text-align: left;&#34;&gt;Senior&lt;td style=&#34;text-align: left;&#34;&gt;Finder&lt;td style=&#34;text-align: left;&#34;&gt;&lt;em&gt;E&lt;/em&gt;-programs&lt;/table&gt;&lt;p&gt;You can think of promotion between the Koutnik roles as the&#xA;combination of two things:&lt;ol type=1&gt;&lt;li&gt;Effectiveness in your present role.&lt;li&gt;Comfort decomposing the next Lehman program class into the present&#xA;one.&lt;/ol&gt;&lt;p&gt;For example, promotion from “Implementer” to “Solver” requires that&#xA;you can decompose &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;P&lt;/em&gt;&lt;/span&gt;-programs&#xA;into &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;S&lt;/em&gt;&lt;/span&gt;-programs — complex&#xA;problems into simple behaviors — and that you can effectively implement&#xA;those decomposed elements. Per the diagram above, “decompose” typically&#xA;means functional decomposition.&lt;p&gt;Promotion from “Solver” to “Finder” requires breaking &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;E&lt;/em&gt;&lt;/span&gt;-programs down into &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;P&lt;/em&gt;&lt;/span&gt;-programs, preferably for&#xA;delegation. The two kinds of &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;E&lt;/em&gt; → &lt;em&gt;P&lt;/em&gt;&lt;/span&gt; composition I&#xA;described have inverses:&lt;p&gt;&lt;strong&gt;Temporal decomposition&lt;/strong&gt;&lt;ul&gt;&lt;li&gt;Consider &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;P&lt;/em&gt;&lt;sub&gt;&lt;em&gt;n&lt;/em&gt;&lt;/sub&gt;&lt;/span&gt; when you’re&#xA;implementing &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;P&lt;/em&gt;&lt;sub&gt;0&lt;/sub&gt;&lt;/span&gt;.&#xA;Can you solve this problem once instead of solving &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;n&lt;/em&gt;&lt;/span&gt; variants before consolidating?&lt;ul&gt;&lt;li&gt;This doesn’t mean you always &lt;em&gt;should.&lt;/em&gt; How confident are you&#xA;about the future variants?&lt;li&gt;Similarly: does the work you’re doing today unnecessarily resist&#xA;future changes, e.g. by making unsafe assumptions about &lt;a href=./product-ontology.html&gt;product model constraints&lt;/a&gt;?&lt;/ul&gt;&lt;li&gt;How is your team’s tempo and trajectory? This is where planning,&#xA;headcount, and talent development come into play.&lt;ul&gt;&lt;li&gt;Can you make your teammates more effective? What skills do you think&#xA;each of them should develop most urgently?&lt;li&gt;Given the opportunity, what skill does your team need most&#xA;desperately? Does the sourcing you’re doing find candidates with that&#xA;skill? Does your interview loop detect it unambiguously?&lt;li&gt;&lt;em&gt;After&lt;/em&gt; that hire, what’s the next limiting factor?&lt;/ul&gt;&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;Organizational decomposition&lt;/strong&gt;&lt;ul&gt;&lt;li&gt;Where are there new product opportunities?&lt;ul&gt;&lt;li&gt;Consider induced demand. Does the product you ship today generate&#xA;new needs/requirements?&lt;li&gt;Consider your domain. Answer Hamming’s question: “what are the&#xA;important problems in [your] field?”&lt;a href=#fn6 class=footnote-ref id=fnref6 role=doc-noteref&gt;&lt;sup&gt;6&lt;/sup&gt;&lt;/a&gt;&lt;/ul&gt;&lt;li&gt;What interface does your program provide the rest of the&#xA;organization?&lt;ul&gt;&lt;li&gt;Does it encourage reuse, expansion, investment?&lt;li&gt;How &lt;em&gt;apparent&lt;/em&gt; is its value? Would a different interface&#xA;demonstrate value more clearly, or offer you more leverage in&#xA;planning?&lt;/ul&gt;&lt;/ul&gt;&lt;p&gt;Setting aside my specific examples, decomposition from &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;E&lt;/em&gt;&lt;/span&gt;- to &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;P&lt;/em&gt;&lt;/span&gt;-programs means determining what&#xA;problems to solve in a &lt;em&gt;reflexive &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;E&lt;/em&gt;&lt;/span&gt;-program environment,&lt;/em&gt; where&#xA;the problem you solve today determines what new problem you’ll want to&#xA;solve tomorrow.&lt;p&gt;“Temporal” and “organizational” aren’t an exhaustive list of &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;E&lt;/em&gt;&lt;/span&gt;-program (de)compositions — shoot&#xA;me an email if you think of another. No matter what source of&#xA;program-reflexivity you focus on, you’re likely to wind up with a focus&#xA;area more actionable than the typical Regular-to-Senior values&#xA;(e.g. unspecific “leadership”).&lt;p&gt;To engineers: your workplace might encourage you to cede exercising&#xA;foresight to your Product or Engineering Manager. Don’t! These are&#xA;important aspects of your craft; engage them, even if you don’t have the&#xA;autonomy to act on your own predictions. Engage them out of self-respect&#xA;if nothing else.&lt;p&gt;In the early stages, earning engineering autonomy means writing the&#xA;&lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;P&lt;/em&gt;&lt;/span&gt;-program you need&#xA;&lt;em&gt;today.&lt;/em&gt; Later, it’s about navigating the &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;E&lt;/em&gt;&lt;/span&gt;-program you’re building&#xA;&lt;em&gt;indefinitely.&lt;/em&gt; Don’t miss the forest for the trees.&lt;aside id=footnotes class=&#34;footnotes footnotes-end-of-document&#34; role=doc-endnotes&gt;&lt;hr&gt;&lt;ol&gt;&lt;li id=fn1&gt;&lt;p&gt;See myriad other attempts to get you to care,&#xA;e.g. Adrian Colyer’s &lt;a href=https://blog.acolyer.org/2020/02/14/programs-life-cycles-laws/&gt;“Programs,&#xA;life cycles, and laws of software evolution”&lt;/a&gt; (2020) or Matt&#xA;Rickard’s &lt;a href=https://matt-rickard.com/laws-of-program-evolution&gt;“Laws of&#xA;Software Evolution”&lt;/a&gt; (2021).&lt;a href=#fnref1 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn2&gt;&lt;p&gt;Lehman, Meir M. “Programs, life cycles, and laws of&#xA;software evolution.” &lt;em&gt;Proceedings of the IEEE&lt;/em&gt; 68, no. 9 (1980):&#xA;1060-1076. &lt;a href=https://users.ece.utexas.edu/~perry/education/SE-Intro/lehman.pdf&gt;Available&#xA;online.&lt;/a&gt;&lt;a href=#fnref2 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn3&gt;&lt;p&gt;Lehman, p. 1061.&lt;a href=#fnref3 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn4&gt;&lt;p&gt;Lehman, p. 1062.&lt;a href=#fnref4 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn5&gt;&lt;p&gt;We might call these levels “Software Engineer,” “Senior&#xA;Software Engineer,” and “Staff Software Engineer” today.&lt;a href=#fnref5 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn6&gt;&lt;p&gt;Hamming dedicated one half-day per work week to stepping&#xA;back and considering his field in its entirety; from &lt;a href=https://www.cs.virginia.edu/~robins/YouAndYourResearch.html&gt;“You&#xA;and Your Research”&lt;/a&gt;:&lt;blockquote&gt;&lt;p&gt;Along those lines at some urging from John Tukey and others, I&#xA;finally adopted what I called “Great Thoughts Time.” When I went to&#xA;lunch Friday noon, I would only discuss great thoughts after that. By&#xA;great thoughts I mean ones like: “What will be the role of computers in&#xA;all of AT&amp;amp;T?”, “How will computers change science?” […] Friday&#xA;afternoons for years — great thoughts only — means that I committed 10%&#xA;of my time trying to understand the bigger problems in the field,&#xA;i.e. what was and what was not important. I found in the early days I&#xA;had believed ‘this’ and yet had spent all week marching in ‘that’&#xA;direction. It was kind of foolish. If I really believe the action is&#xA;over there, why do I march in this direction? I either had to change my&#xA;goal or change what I did. So I changed something I did and I marched in&#xA;the direction I thought was important. It’s that easy.&lt;/blockquote&gt;&lt;a href=#fnref6 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;/ol&gt;&lt;/aside&gt;</content>
    <link href="https://lukasschwab.me/blog/gen/lehman.html" rel="alternate"></link>
    <summary type="html">Meir Lehman&#39;s &#34;Laws of Software Evolution&#34; are over-discussed, but his 1980 paper sketches one really good idea: a hierarchy of program types according to their complexity. The implication of that model --- *how* complex programs decompose into simple components --- can structure advancement in software engineering careers.</summary>
  </entry>
  <entry>
    <title>Paris Redux</title>
    <updated>2024-02-19T00:00:00Z</updated>
    <id>https://lukasschwab.me/blog/gen/paris-redux.html</id>
    <content type="html">&lt;html xmlns=http://www.w3.org/1999/xhtml lang xml:lang&gt;&lt;meta charset=utf-8&gt;&lt;meta name=generator content=&#34;pandoc&#34;&gt;&lt;meta name=viewport content=&#34;width=device-width,initial-scale=1,user-scalable=yes&#34;&gt;&lt;meta name=author content=&#34;Lukas Schwab&#34;&gt;&lt;meta name=dcterms.date content=&#34;2024-02-19&#34;&gt;&lt;title&gt;Paris Redux&lt;/title&gt;&lt;style&gt;&#xA;      code{white-space: pre-wrap;}&#xA;      span.smallcaps{font-variant: small-caps;}&#xA;      span.underline{text-decoration: underline;}&#xA;      div.column{display: inline-block; vertical-align: top; width: 50%;}&#xA;          &lt;/style&gt;&lt;link rel=stylesheet href=../styles/common.css&gt;&lt;script data-goatcounter=https://lukasschwab.goatcounter.com/count async src=//gc.zgo.at/count.js&gt;&lt;/script&gt;&lt;a href=../index.html&gt;&amp;#171; blog&lt;/a&gt; &lt;span class=date&gt;2024-02-19&lt;/span&gt;&lt;header id=title-block-header&gt;&lt;h1 class=title&gt;Paris Redux&lt;/h1&gt;&lt;/header&gt;&lt;p&gt;I’m looking forward to visiting Paris again this spring. That’s as&#xA;good an excuse as any to share two Paris snippets from my recent&#xA;reading.&lt;p&gt;Friedensreich Hundertwasser wrote about looping strolls through Paris&#xA;—&lt;blockquote&gt;&lt;p&gt;I have a bicycle. Paris is big. I want to say that the lines I draw&#xA;with my bicycle through this great city are extraordinary. The lines are&#xA;just as wonderful as all the other lines I cross traced by all the other&#xA;people. I ride around people and obstacles. I am happy at last to be in&#xA;harmony and in contact with the others. These lines, for which I need&#xA;many hours and which form an enormous circle by the time I come back and&#xA;which make me tired, are more beautiful, more genuine and more justified&#xA;than those I could draw on paper. And I dare say that the lines I trace&#xA;with my feet on the pavement walking to the museum are more important&#xA;than the lines I will find there hanging on the walls inside. And it&#xA;pleases me enormously to see that the line I trace is never straight,&#xA;never confused, but has a reason to be like this in every tiny part.&#xA;Beware of the straight line and the drunken line. But above all beware&#xA;of the T-squared straight line. The straight line leads to the downfall&#xA;of humanity. &lt;em&gt;La ligne droite conduit à la perte de&#xA;l’humanité.&lt;/em&gt;&lt;a href=#fn1 class=footnote-ref id=fnref1 role=doc-noteref&gt;&lt;sup&gt;1&lt;/sup&gt;&lt;/a&gt;&lt;/blockquote&gt;&lt;p&gt;— and Rilke imagined warm welcomes:&lt;blockquote&gt;&lt;p&gt;I walked right along the Quai d’Anjou, and into one side-turning&#xA;after another, as though I were burdened with a lot of memories which in&#xA;fact I don’t possess at all — it was such a strange afternoon.&#xA;Somewhere, at a particularly carefully muffled high window, a corner of&#xA;the curtain was lifted as I went past, and I thought it must be a sign&#xA;meant for me; there was a feeling here, and then again, as if I had only&#xA;to walk in, as if everything would be explained even to the very smell&#xA;that meets you, as if one had been long expected, as if a kind of relief&#xA;must overwhelm all these silent houses if one decided to go in… a&#xA;staircase, a hall, not a moment’s hesitation, that is the door, ‘Ah,&#xA;c’est vous enfin’, does someone say? no matter, it is in the air, in the&#xA;dusk, the fire on the hearth knows it, all things are quite sure…&lt;a href=#fn2 class=footnote-ref id=fnref2 role=doc-noteref&gt;&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt;&lt;/blockquote&gt;&lt;p&gt;&lt;em&gt;Ah, c’est vous enfin…&lt;/em&gt; I can’t wait.&lt;aside id=footnotes class=&#34;footnotes footnotes-end-of-document&#34; role=doc-endnotes&gt;&lt;hr&gt;&lt;ol&gt;&lt;li id=fn1&gt;&lt;p&gt;“Die gerade Linie führt zum Untergang,” text&#xA;accompanying Hundertwasser’s first exhibition in Paris (1953). Available&#xA;online in &lt;a href=https://hundertwasser.com/en/texts/die_gerade_linie_fuehrt_zum_untergang&gt;English&lt;/a&gt;&#xA;and &lt;a href=https://hundertwasser.com/texte/the_straight_line_leads_to_the_downfall_of_our_civilisation&gt;German&lt;/a&gt;.&lt;a href=#fnref1 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn2&gt;&lt;p&gt;A 1914 letter from Rilke, quoted by Marie von Thurn und&#xA;Taxis, reproduced in &lt;em&gt;Rainer Maria Rilke: A Study of His Later&#xA;Poetry&lt;/em&gt; (Hans Egon Holthusen, trans. Stern; 1952).&lt;a href=#fnref2 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;/ol&gt;&lt;/aside&gt;</content>
    <link href="https://lukasschwab.me/blog/gen/paris-redux.html" rel="alternate"></link>
    <summary type="html">Snippets on Paris from two recent interests: Friedensreich Hundertwasser and Rainer Maria Rilke.</summary>
  </entry>
  <entry>
    <title>Three-Step Lifted Interfaces</title>
    <updated>2023-12-30T00:00:00Z</updated>
    <id>https://lukasschwab.me/blog/gen/three-step-lifted-interfaces.html</id>
    <content type="html">&lt;html xmlns=http://www.w3.org/1999/xhtml lang xml:lang&gt;&lt;meta charset=utf-8&gt;&lt;meta name=generator content=&#34;pandoc&#34;&gt;&lt;meta name=viewport content=&#34;width=device-width,initial-scale=1,user-scalable=yes&#34;&gt;&lt;meta name=author content=&#34;Lukas Schwab&#34;&gt;&lt;meta name=dcterms.date content=&#34;2023-12-30&#34;&gt;&lt;title&gt;Three-Step Lifted Interfaces&lt;/title&gt;&lt;style&gt;&#xA;      code{white-space: pre-wrap;}&#xA;      span.smallcaps{font-variant: small-caps;}&#xA;      span.underline{text-decoration: underline;}&#xA;      div.column{display: inline-block; vertical-align: top; width: 50%;}&#xA;          &lt;/style&gt;&lt;style&gt;pre &gt; code.sourceCode { white-space: pre; position: relative; }&#xA;pre &gt; code.sourceCode &gt; span { display: inline-block; line-height: 1.25; }&#xA;pre &gt; code.sourceCode &gt; span:empty { height: 1.2em; }&#xA;.sourceCode { overflow: visible; }&#xA;code.sourceCode &gt; span { color: inherit; text-decoration: inherit; }&#xA;div.sourceCode { margin: 1em 0; }&#xA;pre.sourceCode { margin: 0; }&#xA;@media screen {&#xA;div.sourceCode { overflow: auto; }&#xA;}&#xA;@media print {&#xA;pre &gt; code.sourceCode { white-space: pre-wrap; }&#xA;pre &gt; code.sourceCode &gt; span { text-indent: -5em; padding-left: 5em; }&#xA;}&#xA;pre.numberSource code&#xA;  { counter-reset: source-line 0; }&#xA;pre.numberSource code &gt; span&#xA;  { position: relative; left: -4em; counter-increment: source-line; }&#xA;pre.numberSource code &gt; span &gt; a:first-child::before&#xA;  { content: counter(source-line);&#xA;    position: relative; left: -1em; text-align: right; vertical-align: baseline;&#xA;    border: none; display: inline-block;&#xA;    -webkit-touch-callout: none; -webkit-user-select: none;&#xA;    -khtml-user-select: none; -moz-user-select: none;&#xA;    -ms-user-select: none; user-select: none;&#xA;    padding: 0 4px; width: 4em;&#xA;    color: #aaaaaa;&#xA;  }&#xA;pre.numberSource { margin-left: 3em; border-left: 1px solid #aaaaaa;  padding-left: 4px; }&#xA;div.sourceCode&#xA;  {   }&#xA;@media screen {&#xA;pre &gt; code.sourceCode &gt; span &gt; a:first-child::before { text-decoration: underline; }&#xA;}&#xA;code span.al { color: #ff0000; font-weight: bold; } /* Alert */&#xA;code span.an { color: #60a0b0; font-weight: bold; font-style: italic; } /* Annotation */&#xA;code span.at { color: #7d9029; } /* Attribute */&#xA;code span.bn { color: #40a070; } /* BaseN */&#xA;code span.bu { color: #008000; } /* BuiltIn */&#xA;code span.cf { color: #007020; font-weight: bold; } /* ControlFlow */&#xA;code span.ch { color: #4070a0; } /* Char */&#xA;code span.cn { color: #880000; } /* Constant */&#xA;code span.co { color: #60a0b0; font-style: italic; } /* Comment */&#xA;code span.cv { color: #60a0b0; font-weight: bold; font-style: italic; } /* CommentVar */&#xA;code span.do { color: #ba2121; font-style: italic; } /* Documentation */&#xA;code span.dt { color: #902000; } /* DataType */&#xA;code span.dv { color: #40a070; } /* DecVal */&#xA;code span.er { color: #ff0000; font-weight: bold; } /* Error */&#xA;code span.ex { } /* Extension */&#xA;code span.fl { color: #40a070; } /* Float */&#xA;code span.fu { color: #06287e; } /* Function */&#xA;code span.im { color: #008000; font-weight: bold; } /* Import */&#xA;code span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Information */&#xA;code span.kw { color: #007020; font-weight: bold; } /* Keyword */&#xA;code span.op { color: #666666; } /* Operator */&#xA;code span.ot { color: #007020; } /* Other */&#xA;code span.pp { color: #bc7a00; } /* Preprocessor */&#xA;code span.sc { color: #4070a0; } /* SpecialChar */&#xA;code span.ss { color: #bb6688; } /* SpecialString */&#xA;code span.st { color: #4070a0; } /* String */&#xA;code span.va { color: #19177c; } /* Variable */&#xA;code span.vs { color: #4070a0; } /* VerbatimString */&#xA;code span.wa { color: #60a0b0; font-weight: bold; font-style: italic; } /* Warning */&lt;/style&gt;&lt;link rel=stylesheet href=../styles/common.css&gt;&lt;link rel=stylesheet href=../styles/fence-extensions/codefile.css&gt;&lt;script data-goatcounter=https://lukasschwab.goatcounter.com/count async src=//gc.zgo.at/count.js&gt;&lt;/script&gt;&lt;a href=../index.html&gt;&amp;#171; blog&lt;/a&gt; &lt;span class=date&gt;2023-12-30&lt;/span&gt;&lt;header id=title-block-header&gt;&lt;h1 class=title&gt;Three-Step Lifted Interfaces&lt;/h1&gt;&lt;/header&gt;&lt;p&gt;Look — I might just be a types person. I admit it! Mea maxima culpa!&#xA;Strong static types are a major factor in how quickly I learn a&#xA;programming language and how confidently I come to use it. There are&#xA;myriad other factors, but many of those factors themselves rely on&#xA;static types (or a very convincing simulation thereof).&lt;p&gt;Thankfully, I’m in okay company. Today I revisited a Jonathan Blow&#xA;stream in which Jon pauses to gloat about how much C++’s static types&#xA;simplify his work.&lt;blockquote&gt;&lt;p&gt;You just &lt;em&gt;break&lt;/em&gt; your program and then you fix all the compile&#xA;errors. When the compile errors are fixed, you know your program works&#xA;again. This is something that these dynamic languages don’t let you do,&#xA;and it’s actually one of the most powerful programming techniques. No&#xA;troll.&lt;p&gt;We’re just doing a bunch of not-very-interesting but also&#xA;not-very-scary drudge work, but we’re using this to migrate us from one&#xA;place in the space of all possible programs to another one, by a safe&#xA;route where we don’t smash everything up.&lt;a href=#fn1 class=footnote-ref id=fnref1 role=doc-noteref&gt;&lt;sup&gt;1&lt;/sup&gt;&lt;/a&gt;&lt;/blockquote&gt;&lt;p&gt;Jon demonstrates one such route: rooting out all inner usage of an&#xA;outer-scope variable by defining a conflicting variable with an&#xA;incompatible type (&lt;a href=&#34;https://youtu.be/2J-HIh3kXCQ?si=oatke7WRs5r-ftrM&amp;amp;t=166&#34;&gt;2:46&lt;/a&gt;).&lt;p&gt;I &lt;em&gt;also&lt;/em&gt; lean on statically typed languages to guide my&#xA;refactoring. That reliance is especially pronounced when I’m “lifting&#xA;interfaces” out of single-class implementations, a precursor step to&#xA;introducing alternative implementations or test doubles. My one weird&#xA;trick for three-step lifted interfaces is… broken partial renames.&lt;h1 id=example-refactoring-a-logger&gt;Example: refactoring a logger&lt;/h1&gt;&lt;p&gt;You’ve implemented a cute little &lt;code&gt;Logger&lt;/code&gt; that writes to a&#xA;file. Your application — &lt;code&gt;main&lt;/code&gt; — passes it around as a value&#xA;(to functions &lt;code&gt;foo&lt;/code&gt; and &lt;code&gt;bar&lt;/code&gt;).&lt;div class=codefile data-filename=demo.go&gt;&lt;div class=sourceCode id=cb1&gt;&lt;pre class=&#34;sourceCode numberSource go numberLines&#34;&gt;&lt;code class=&#34;sourceCode go&#34;&gt;&lt;span id=cb1-1&gt;&lt;a href=#cb1-1&gt;&lt;/a&gt;&lt;span class=kw&gt;package&lt;/span&gt; main&lt;/span&gt;&#xA;&lt;span id=cb1-2&gt;&lt;a href=#cb1-2&gt;&lt;/a&gt;&lt;/span&gt;&#xA;&lt;span id=cb1-3&gt;&lt;a href=#cb1-3&gt;&lt;/a&gt;&lt;span class=kw&gt;import&lt;/span&gt; &lt;span class=st&gt;&amp;quot;os&amp;quot;&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb1-4&gt;&lt;a href=#cb1-4&gt;&lt;/a&gt;&lt;/span&gt;&#xA;&lt;span id=cb1-5&gt;&lt;a href=#cb1-5&gt;&lt;/a&gt;&lt;span class=kw&gt;type&lt;/span&gt; Logger &lt;span class=kw&gt;struct&lt;/span&gt; &lt;span class=op&gt;{&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb1-6&gt;&lt;a href=#cb1-6&gt;&lt;/a&gt;    file &lt;span class=op&gt;*&lt;/span&gt;os&lt;span class=op&gt;.&lt;/span&gt;File&lt;/span&gt;&#xA;&lt;span id=cb1-7&gt;&lt;a href=#cb1-7&gt;&lt;/a&gt;&lt;span class=op&gt;}&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb1-8&gt;&lt;a href=#cb1-8&gt;&lt;/a&gt;&lt;/span&gt;&#xA;&lt;span id=cb1-9&gt;&lt;a href=#cb1-9&gt;&lt;/a&gt;&lt;span class=kw&gt;func&lt;/span&gt; &lt;span class=op&gt;(&lt;/span&gt;l Logger&lt;span class=op&gt;)&lt;/span&gt; Info&lt;span class=op&gt;(&lt;/span&gt;message &lt;span class=dt&gt;string&lt;/span&gt;&lt;span class=op&gt;)&lt;/span&gt; &lt;span class=op&gt;{&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb1-10&gt;&lt;a href=#cb1-10&gt;&lt;/a&gt;    l&lt;span class=op&gt;.&lt;/span&gt;file&lt;span class=op&gt;.&lt;/span&gt;WriteString&lt;span class=op&gt;(&lt;/span&gt;&lt;span class=st&gt;&amp;quot;[INFO] &amp;quot;&lt;/span&gt; &lt;span class=op&gt;+&lt;/span&gt; message &lt;span class=op&gt;+&lt;/span&gt; &lt;span class=st&gt;&amp;quot;&lt;/span&gt;&lt;span class=ch&gt;\n&lt;/span&gt;&lt;span class=st&gt;&amp;quot;&lt;/span&gt;&lt;span class=op&gt;)&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb1-11&gt;&lt;a href=#cb1-11&gt;&lt;/a&gt;&lt;span class=op&gt;}&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb1-12&gt;&lt;a href=#cb1-12&gt;&lt;/a&gt;&lt;/span&gt;&#xA;&lt;span id=cb1-13&gt;&lt;a href=#cb1-13&gt;&lt;/a&gt;&lt;span class=kw&gt;func&lt;/span&gt; &lt;span class=op&gt;(&lt;/span&gt;l Logger&lt;span class=op&gt;)&lt;/span&gt; Warning&lt;span class=op&gt;(&lt;/span&gt;message &lt;span class=dt&gt;string&lt;/span&gt;&lt;span class=op&gt;)&lt;/span&gt; &lt;span class=op&gt;{&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb1-14&gt;&lt;a href=#cb1-14&gt;&lt;/a&gt;    l&lt;span class=op&gt;.&lt;/span&gt;file&lt;span class=op&gt;.&lt;/span&gt;WriteString&lt;span class=op&gt;(&lt;/span&gt;&lt;span class=st&gt;&amp;quot;[WARN] &amp;quot;&lt;/span&gt; &lt;span class=op&gt;+&lt;/span&gt; message &lt;span class=op&gt;+&lt;/span&gt; &lt;span class=st&gt;&amp;quot;&lt;/span&gt;&lt;span class=ch&gt;\n&lt;/span&gt;&lt;span class=st&gt;&amp;quot;&lt;/span&gt;&lt;span class=op&gt;)&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb1-15&gt;&lt;a href=#cb1-15&gt;&lt;/a&gt;&lt;span class=op&gt;}&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb1-16&gt;&lt;a href=#cb1-16&gt;&lt;/a&gt;&lt;/span&gt;&#xA;&lt;span id=cb1-17&gt;&lt;a href=#cb1-17&gt;&lt;/a&gt;&lt;span class=kw&gt;func&lt;/span&gt; main&lt;span class=op&gt;()&lt;/span&gt; &lt;span class=op&gt;{&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb1-18&gt;&lt;a href=#cb1-18&gt;&lt;/a&gt;    &lt;span class=kw&gt;var&lt;/span&gt; f &lt;span class=op&gt;*&lt;/span&gt;os&lt;span class=op&gt;.&lt;/span&gt;File&lt;/span&gt;&#xA;&lt;span id=cb1-19&gt;&lt;a href=#cb1-19&gt;&lt;/a&gt;    logger &lt;span class=op&gt;:=&lt;/span&gt; Logger&lt;span class=op&gt;{&lt;/span&gt;file&lt;span class=op&gt;:&lt;/span&gt; f&lt;span class=op&gt;}&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb1-20&gt;&lt;a href=#cb1-20&gt;&lt;/a&gt;&lt;/span&gt;&#xA;&lt;span id=cb1-21&gt;&lt;a href=#cb1-21&gt;&lt;/a&gt;    foo&lt;span class=op&gt;(&lt;/span&gt;logger&lt;span class=op&gt;)&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb1-22&gt;&lt;a href=#cb1-22&gt;&lt;/a&gt;    bar&lt;span class=op&gt;(&lt;/span&gt;logger&lt;span class=op&gt;)&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb1-23&gt;&lt;a href=#cb1-23&gt;&lt;/a&gt;&lt;span class=op&gt;}&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb1-24&gt;&lt;a href=#cb1-24&gt;&lt;/a&gt;&lt;/span&gt;&#xA;&lt;span id=cb1-25&gt;&lt;a href=#cb1-25&gt;&lt;/a&gt;&lt;span class=kw&gt;func&lt;/span&gt; foo&lt;span class=op&gt;(&lt;/span&gt;l Logger&lt;span class=op&gt;)&lt;/span&gt; &lt;span class=op&gt;{&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb1-26&gt;&lt;a href=#cb1-26&gt;&lt;/a&gt;    l&lt;span class=op&gt;.&lt;/span&gt;Info&lt;span class=op&gt;(&lt;/span&gt;&lt;span class=st&gt;&amp;quot;Called foo&amp;quot;&lt;/span&gt;&lt;span class=op&gt;)&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb1-27&gt;&lt;a href=#cb1-27&gt;&lt;/a&gt;    bar&lt;span class=op&gt;(&lt;/span&gt;l&lt;span class=op&gt;)&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb1-28&gt;&lt;a href=#cb1-28&gt;&lt;/a&gt;&lt;span class=op&gt;}&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb1-29&gt;&lt;a href=#cb1-29&gt;&lt;/a&gt;&lt;/span&gt;&#xA;&lt;span id=cb1-30&gt;&lt;a href=#cb1-30&gt;&lt;/a&gt;&lt;span class=kw&gt;func&lt;/span&gt; bar&lt;span class=op&gt;(&lt;/span&gt;l Logger&lt;span class=op&gt;)&lt;/span&gt; &lt;span class=op&gt;{&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb1-31&gt;&lt;a href=#cb1-31&gt;&lt;/a&gt;    l&lt;span class=op&gt;.&lt;/span&gt;Warning&lt;span class=op&gt;(&lt;/span&gt;&lt;span class=st&gt;&amp;quot;Called bar&amp;quot;&lt;/span&gt;&lt;span class=op&gt;)&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb1-32&gt;&lt;a href=#cb1-32&gt;&lt;/a&gt;&lt;span class=op&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;Ah, single-implementation serenity. &lt;code&gt;go build demo.go&lt;/code&gt;&#xA;works, &lt;code&gt;foo&lt;/code&gt; and &lt;code&gt;bar&lt;/code&gt; chug cheerfully along&#xA;logging things. Imagine a happy little green diagram — the&#xA;&lt;code&gt;Logger&lt;/code&gt; type and the three ways this code uses it:&lt;figure&gt;&lt;img src=../img/refactors/fig1.svg alt=&#34;A happy little green diagram. Logger is used in three ways: in a constructor in main, as a function argument type in foo and bar, and via method calls in those functions.&#34;&gt;&lt;figcaption aria-hidden=true&gt;A happy little green diagram.&#xA;&lt;code&gt;Logger&lt;/code&gt; is used in three ways: in a constructor in&#xA;&lt;code&gt;main&lt;/code&gt;, as a function argument type in &lt;code&gt;foo&lt;/code&gt; and&#xA;&lt;code&gt;bar&lt;/code&gt;, and via method calls in those functions.&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;It’s almost &lt;em&gt;too&lt;/em&gt; serene. For one reason or another, you want&#xA;more from this program.&lt;p&gt;Maybe you want to selectively use a &lt;em&gt;different&lt;/em&gt; logger with&#xA;the same &lt;code&gt;main&lt;/code&gt; routine, one that routes your messages to a&#xA;log server or &lt;code&gt;stdout&lt;/code&gt;.&lt;p&gt;Maybe you need a &lt;em&gt;mock&lt;/em&gt; logger for your unit tests, to confirm&#xA;&lt;code&gt;foo&lt;/code&gt; and &lt;code&gt;bar&lt;/code&gt; emit key log messages without&#xA;simultaneous tests racing for the file.&lt;a href=#fn2 class=footnote-ref id=fnref2 role=doc-noteref&gt;&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt;&lt;p&gt;If you want &lt;code&gt;main&lt;/code&gt; to work with a variety of loggers&#xA;interchangeably, you have to drive an abstracting wedge between the&#xA;implementation and its users: an interface. A refactor like this has&#xA;three goals:&lt;ol type=1&gt;&lt;li&gt;&lt;p&gt;Lift an interface capturing the existing logger’s public surface&#xA;(&lt;code&gt;Info&lt;/code&gt; and &lt;code&gt;Error&lt;/code&gt; functions).&lt;li&gt;&lt;p&gt;Update the file-writing logger to implement that interface.&lt;a href=#fn3 class=footnote-ref id=fnref3 role=doc-noteref&gt;&lt;sup&gt;3&lt;/sup&gt;&lt;/a&gt;&lt;li&gt;&lt;p&gt;Update functions to ambivalently receive any implementation of&#xA;the logger interface — a logger for tests, a logger for your log server,&#xA;whatever!&lt;/ol&gt;&lt;p&gt;Just like Jon Blow’s C++ compiler, the Go compiler won’t let you&#xA;build confused code where you’re accidentally passing one type in lieu&#xA;of another. In theory you could refactor manually and unsystematically,&#xA;and when eventually something builds you’ve likely achieved your goals.&#xA;Like Jon, you’d be using type safety “to migrate […] from one place in&#xA;the space of all possible programs to another one, by &lt;em&gt;a&lt;/em&gt; safe&#xA;route.”&lt;p&gt;Of course, some routes are shorter than others. I take this one&#xA;because it’s short.&lt;h2 id=step-1-break-everything&gt;Step 1: break everything&lt;/h2&gt;&lt;p&gt;Seriously, break everything. Maybe you proceed cautiously when you&#xA;adding new functionality to a program — incrementally adding bits and&#xA;pieces, careful to resolve issues whenever you introduce them.&#xA;Refactoring working code is a fundamentally different flow: break the&#xA;build confidently and selectively, then let the type system&#xA;&lt;em&gt;guide&lt;/em&gt; you back to a correct program.&lt;p&gt;Step 1 is to rename &lt;code&gt;Logger&lt;/code&gt; to something (almost&#xA;anything!) else. &lt;code&gt;FileLogger&lt;/code&gt; is a fine name here.&lt;p&gt;The key is to rename it locally, not to reach for an IDE-automated&#xA;“Refactor → Rename” that updates every reference to &lt;code&gt;Logger&lt;/code&gt;.&#xA;Let those references break; you &lt;em&gt;want&lt;/em&gt; them to be broken.&#xA;&lt;em&gt;Break everything.&lt;/em&gt;&lt;div class=codefile data-filename=demo.go&gt;&lt;div class=sourceCode id=cb2&gt;&lt;pre class=&#34;sourceCode numberSource go numberLines&#34;&gt;&lt;code class=&#34;sourceCode go&#34;&gt;&lt;span id=cb2-1&gt;&lt;a href=#cb2-1&gt;&lt;/a&gt;&lt;span class=co&gt;// FileLogger is Logger after a rename; otherwise unchanged.&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb2-2&gt;&lt;a href=#cb2-2&gt;&lt;/a&gt;&lt;span class=kw&gt;type&lt;/span&gt; FileLogger &lt;span class=kw&gt;struct&lt;/span&gt; &lt;span class=op&gt;{&lt;/span&gt; … &lt;span class=op&gt;}&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb2-3&gt;&lt;a href=#cb2-3&gt;&lt;/a&gt;&lt;/span&gt;&#xA;&lt;span id=cb2-4&gt;&lt;a href=#cb2-4&gt;&lt;/a&gt;&lt;span class=kw&gt;func&lt;/span&gt; &lt;span class=op&gt;(&lt;/span&gt;l FileLogger&lt;span class=op&gt;)&lt;/span&gt; Info&lt;span class=op&gt;(&lt;/span&gt;message &lt;span class=dt&gt;string&lt;/span&gt;&lt;span class=op&gt;)&lt;/span&gt; &lt;span class=op&gt;{&lt;/span&gt; … &lt;span class=op&gt;}&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb2-5&gt;&lt;a href=#cb2-5&gt;&lt;/a&gt;&lt;/span&gt;&#xA;&lt;span id=cb2-6&gt;&lt;a href=#cb2-6&gt;&lt;/a&gt;&lt;span class=kw&gt;func&lt;/span&gt; &lt;span class=op&gt;(&lt;/span&gt;l FileLogger&lt;span class=op&gt;)&lt;/span&gt; Warning&lt;span class=op&gt;(&lt;/span&gt;message &lt;span class=dt&gt;string&lt;/span&gt;&lt;span class=op&gt;)&lt;/span&gt; &lt;span class=op&gt;{&lt;/span&gt; … &lt;span class=op&gt;}&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb2-7&gt;&lt;a href=#cb2-7&gt;&lt;/a&gt;&lt;/span&gt;&#xA;&lt;span id=cb2-8&gt;&lt;a href=#cb2-8&gt;&lt;/a&gt;&lt;span class=kw&gt;func&lt;/span&gt; main&lt;span class=op&gt;()&lt;/span&gt; &lt;span class=op&gt;{&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb2-9&gt;&lt;a href=#cb2-9&gt;&lt;/a&gt;    &lt;span class=kw&gt;var&lt;/span&gt; f &lt;span class=op&gt;*&lt;/span&gt;os&lt;span class=op&gt;.&lt;/span&gt;File&lt;/span&gt;&#xA;&lt;span id=cb2-10&gt;&lt;a href=#cb2-10&gt;&lt;/a&gt;    logger &lt;span class=op&gt;:=&lt;/span&gt; Logger&lt;span class=op&gt;{&lt;/span&gt;file&lt;span class=op&gt;:&lt;/span&gt; f&lt;span class=op&gt;}&lt;/span&gt;   &lt;span class=co&gt;// 🚨 undefined: Logger&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb2-11&gt;&lt;a href=#cb2-11&gt;&lt;/a&gt;&lt;/span&gt;&#xA;&lt;span id=cb2-12&gt;&lt;a href=#cb2-12&gt;&lt;/a&gt;    foo&lt;span class=op&gt;(&lt;/span&gt;logger&lt;span class=op&gt;)&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb2-13&gt;&lt;a href=#cb2-13&gt;&lt;/a&gt;    bar&lt;span class=op&gt;(&lt;/span&gt;logger&lt;span class=op&gt;)&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb2-14&gt;&lt;a href=#cb2-14&gt;&lt;/a&gt;&lt;span class=op&gt;}&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb2-15&gt;&lt;a href=#cb2-15&gt;&lt;/a&gt;&lt;/span&gt;&#xA;&lt;span id=cb2-16&gt;&lt;a href=#cb2-16&gt;&lt;/a&gt;&lt;span class=kw&gt;func&lt;/span&gt; foo&lt;span class=op&gt;(&lt;/span&gt;l Logger&lt;span class=op&gt;)&lt;/span&gt; &lt;span class=op&gt;{&lt;/span&gt; … &lt;span class=op&gt;}&lt;/span&gt;        &lt;span class=co&gt;// 🚨 undefined: Logger&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb2-17&gt;&lt;a href=#cb2-17&gt;&lt;/a&gt;&lt;/span&gt;&#xA;&lt;span id=cb2-18&gt;&lt;a href=#cb2-18&gt;&lt;/a&gt;&lt;span class=kw&gt;func&lt;/span&gt; bar&lt;span class=op&gt;(&lt;/span&gt;l Logger&lt;span class=op&gt;)&lt;/span&gt; &lt;span class=op&gt;{&lt;/span&gt; … &lt;span class=op&gt;}&lt;/span&gt;        &lt;span class=co&gt;// 🚨 undefined: Logger&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;Where it once succeeded, &lt;code&gt;go build main.go&lt;/code&gt; lists compiler&#xA;errors (&lt;code&gt;undefined: Logger&lt;/code&gt;) at each broken reference to the&#xA;old name. &lt;code&gt;FileLogger&lt;/code&gt; is fine: unused, but internally&#xA;consistent. Everything else needs you to define a type called&#xA;&lt;code&gt;Logger&lt;/code&gt; — the very definition you just removed!&lt;figure&gt;&lt;img src=../img/refactors/fig2.svg alt=&#34;After Step 1 (“break everything”), everything is broken.&#34;&gt;&lt;figcaption aria-hidden=true&gt;After Step 1 (“break everything”),&#xA;everything is broken.&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;In his example, Jon Blow uses the compiler’s list of broken&#xA;references as a checklist for manual corrections: he investigates the&#xA;compiler errors one by one, resolves them sensibly, and then he’s done&#xA;(“safe route”).&lt;p&gt;Your safe route is slightly different. The rename doesn’t just break&#xA;everything to yield a checklist; it also opens space for a new type, one&#xA;that will clear most of that checklist at once.&lt;h2 id=step-2-lift-the-interface&gt;Step 2: lift the interface&lt;/h2&gt;&lt;p&gt;Instead of getting bogged down in the compiler error nitty-gritty,&#xA;focus on the goal of the refactor: you’re driving an interface, like a&#xA;wedge, between &lt;code&gt;FileLogger&lt;/code&gt; and its callers.&lt;p&gt;Step 2 is to introduce that interface — in this example, it’s just&#xA;the &lt;code&gt;FileLogger&lt;/code&gt; methods &lt;code&gt;Info&lt;/code&gt; and&#xA;&lt;code&gt;Warning&lt;/code&gt;.&lt;p&gt;Here’s the trick: call the new interface what &lt;code&gt;FileLogger&lt;/code&gt;&#xA;&lt;em&gt;used&lt;/em&gt; to be called.&lt;div class=codefile data-filename=demo.go&gt;&lt;div class=sourceCode id=cb3&gt;&lt;pre class=&#34;sourceCode numberSource go numberLines&#34;&gt;&lt;code class=&#34;sourceCode go&#34;&gt;&lt;span id=cb3-1&gt;&lt;a href=#cb3-1&gt;&lt;/a&gt;&lt;span class=co&gt;// Logger is the lifted interface.&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb3-2&gt;&lt;a href=#cb3-2&gt;&lt;/a&gt;&lt;span class=kw&gt;type&lt;/span&gt; Logger &lt;span class=kw&gt;interface&lt;/span&gt; &lt;span class=op&gt;{&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb3-3&gt;&lt;a href=#cb3-3&gt;&lt;/a&gt;    Info&lt;span class=op&gt;(&lt;/span&gt;message &lt;span class=dt&gt;string&lt;/span&gt;&lt;span class=op&gt;)&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb3-4&gt;&lt;a href=#cb3-4&gt;&lt;/a&gt;    Warning&lt;span class=op&gt;(&lt;/span&gt;message &lt;span class=dt&gt;string&lt;/span&gt;&lt;span class=op&gt;)&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb3-5&gt;&lt;a href=#cb3-5&gt;&lt;/a&gt;&lt;span class=op&gt;}&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb3-6&gt;&lt;a href=#cb3-6&gt;&lt;/a&gt;&lt;/span&gt;&#xA;&lt;span id=cb3-7&gt;&lt;a href=#cb3-7&gt;&lt;/a&gt;&lt;span class=co&gt;// FileLogger implicitly implements Logger: it defines Info and Warning.&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb3-8&gt;&lt;a href=#cb3-8&gt;&lt;/a&gt;&lt;span class=kw&gt;type&lt;/span&gt; FileLogger &lt;span class=kw&gt;struct&lt;/span&gt; &lt;span class=op&gt;{&lt;/span&gt; … &lt;span class=op&gt;}&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb3-9&gt;&lt;a href=#cb3-9&gt;&lt;/a&gt;&lt;/span&gt;&#xA;&lt;span id=cb3-10&gt;&lt;a href=#cb3-10&gt;&lt;/a&gt;&lt;span class=kw&gt;func&lt;/span&gt; &lt;span class=op&gt;(&lt;/span&gt;l FileLogger&lt;span class=op&gt;)&lt;/span&gt; Info&lt;span class=op&gt;(&lt;/span&gt;message &lt;span class=dt&gt;string&lt;/span&gt;&lt;span class=op&gt;)&lt;/span&gt; &lt;span class=op&gt;{&lt;/span&gt; … &lt;span class=op&gt;}&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb3-11&gt;&lt;a href=#cb3-11&gt;&lt;/a&gt;&lt;/span&gt;&#xA;&lt;span id=cb3-12&gt;&lt;a href=#cb3-12&gt;&lt;/a&gt;&lt;span class=kw&gt;func&lt;/span&gt; &lt;span class=op&gt;(&lt;/span&gt;l FileLogger&lt;span class=op&gt;)&lt;/span&gt; Warning&lt;span class=op&gt;(&lt;/span&gt;message &lt;span class=dt&gt;string&lt;/span&gt;&lt;span class=op&gt;)&lt;/span&gt; &lt;span class=op&gt;{&lt;/span&gt; … &lt;span class=op&gt;}&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb3-13&gt;&lt;a href=#cb3-13&gt;&lt;/a&gt;&lt;/span&gt;&#xA;&lt;span id=cb3-14&gt;&lt;a href=#cb3-14&gt;&lt;/a&gt;&lt;span class=kw&gt;func&lt;/span&gt; main&lt;span class=op&gt;()&lt;/span&gt; &lt;span class=op&gt;{&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb3-15&gt;&lt;a href=#cb3-15&gt;&lt;/a&gt;    &lt;span class=kw&gt;var&lt;/span&gt; f &lt;span class=op&gt;*&lt;/span&gt;os&lt;span class=op&gt;.&lt;/span&gt;File&lt;/span&gt;&#xA;&lt;span id=cb3-16&gt;&lt;a href=#cb3-16&gt;&lt;/a&gt;    logger &lt;span class=op&gt;:=&lt;/span&gt; Logger&lt;span class=op&gt;{&lt;/span&gt;file&lt;span class=op&gt;:&lt;/span&gt; f&lt;span class=op&gt;}&lt;/span&gt;   &lt;span class=co&gt;// 🚨 invalid composite literal type Logger&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb3-17&gt;&lt;a href=#cb3-17&gt;&lt;/a&gt;&lt;/span&gt;&#xA;&lt;span id=cb3-18&gt;&lt;a href=#cb3-18&gt;&lt;/a&gt;    foo&lt;span class=op&gt;(&lt;/span&gt;logger&lt;span class=op&gt;)&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb3-19&gt;&lt;a href=#cb3-19&gt;&lt;/a&gt;    bar&lt;span class=op&gt;(&lt;/span&gt;logger&lt;span class=op&gt;)&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb3-20&gt;&lt;a href=#cb3-20&gt;&lt;/a&gt;&lt;span class=op&gt;}&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb3-21&gt;&lt;a href=#cb3-21&gt;&lt;/a&gt;&lt;/span&gt;&#xA;&lt;span id=cb3-22&gt;&lt;a href=#cb3-22&gt;&lt;/a&gt;&lt;span class=kw&gt;func&lt;/span&gt; foo&lt;span class=op&gt;(&lt;/span&gt;l Logger&lt;span class=op&gt;)&lt;/span&gt; &lt;span class=op&gt;{&lt;/span&gt; … &lt;span class=op&gt;}&lt;/span&gt;        &lt;span class=co&gt;// ✅ Fixed!&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb3-23&gt;&lt;a href=#cb3-23&gt;&lt;/a&gt;&lt;/span&gt;&#xA;&lt;span id=cb3-24&gt;&lt;a href=#cb3-24&gt;&lt;/a&gt;&lt;span class=kw&gt;func&lt;/span&gt; bar&lt;span class=op&gt;(&lt;/span&gt;l Logger&lt;span class=op&gt;)&lt;/span&gt; &lt;span class=op&gt;{&lt;/span&gt; … &lt;span class=op&gt;}&lt;/span&gt;        &lt;span class=co&gt;// ✅ Fixed!&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;By reoccupying the vacated typename &lt;code&gt;Logger&lt;/code&gt;, lifting the&#xA;interface next to the implementation &lt;em&gt;instantly&lt;/em&gt; resolves the&#xA;&lt;code&gt;undefined: Logger&lt;/code&gt; build errors.&lt;p&gt;You didn’t have to track down &lt;code&gt;foo&lt;/code&gt; and &lt;code&gt;bar&lt;/code&gt;,&#xA;much less edit them — they just receive interfaces now (tada!) because&#xA;the name of the type they already received, &lt;code&gt;Logger&lt;/code&gt;, now&#xA;references a compatible interface. This is the chief advantage of the&#xA;three-step approach: in the real world you might have hundreds of&#xA;individual functions analogous to &lt;code&gt;foo&lt;/code&gt; and&#xA;&lt;code&gt;bar&lt;/code&gt;.&lt;p&gt;&lt;code&gt;main&lt;/code&gt;, of course, is still broken. The build error,&#xA;&lt;code&gt;invalid composite literal type Logger&lt;/code&gt;, means that&#xA;&lt;code&gt;Logger&lt;/code&gt; is an interface, but you’re trying to instantiate it&#xA;&lt;em&gt;as if&lt;/em&gt; it were a concrete class.&lt;figure&gt;&lt;img src=../img/refactors/fig3.svg alt=&#34;Reintroducing a type called Logger satisfies the function signature usage (and the method calls downstream of those). There’s a new type error for the constructor.&#34;&gt;&lt;figcaption aria-hidden=true&gt;Reintroducing a type called&#xA;&lt;code&gt;Logger&lt;/code&gt; satisfies the function signature usage (and the&#xA;method calls downstream of those). There’s a new type error for the&#xA;constructor.&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;At this point, you’ve achieved all three of the goals laid out at the&#xA;beginning of the refactor: you’ve lifted an interface, the old&#xA;file-writing logger implements that interface, and the functions&#xA;&lt;code&gt;main&lt;/code&gt; calls are ambivalent about what kind of&#xA;&lt;code&gt;Logger&lt;/code&gt; you give them!&lt;p&gt;Now all you need is a program that builds.&lt;h2 id=step-3-fix-the-build&gt;Step 3: fix the build&lt;/h2&gt;&lt;p&gt;This is it: the “not-very-interesting but also not-very-scary drudge&#xA;work.”&lt;p&gt;Because introducing the interface resolved all the references in&#xA;&lt;code&gt;foo&lt;/code&gt; and &lt;code&gt;bar&lt;/code&gt;, there’s not much left for you to&#xA;fix: just the constructor call in &lt;code&gt;main&lt;/code&gt; is broken. You can’t&#xA;directly construct the &lt;code&gt;Logger&lt;/code&gt; interface, so you have to&#xA;construct something that implements it.&lt;p&gt;Great news: there’s no wrong choice! The only available&#xA;&lt;code&gt;Logger&lt;/code&gt; implementation is &lt;code&gt;FileLogger&lt;/code&gt; with the&#xA;original file-writing behavior.&lt;p&gt;Step 3 is to replace the broken constructor reference with the new&#xA;implementation name &lt;code&gt;FileLogger&lt;/code&gt;.&lt;div class=codefile data-filename=demo.go&gt;&lt;div class=sourceCode id=cb4&gt;&lt;pre class=&#34;sourceCode numberSource go numberLines&#34;&gt;&lt;code class=&#34;sourceCode go&#34;&gt;&lt;span id=cb4-1&gt;&lt;a href=#cb4-1&gt;&lt;/a&gt;&lt;span class=kw&gt;type&lt;/span&gt; Logger &lt;span class=kw&gt;interface&lt;/span&gt; &lt;span class=op&gt;{&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-2&gt;&lt;a href=#cb4-2&gt;&lt;/a&gt;    Info&lt;span class=op&gt;(&lt;/span&gt;message &lt;span class=dt&gt;string&lt;/span&gt;&lt;span class=op&gt;)&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-3&gt;&lt;a href=#cb4-3&gt;&lt;/a&gt;    Warning&lt;span class=op&gt;(&lt;/span&gt;message &lt;span class=dt&gt;string&lt;/span&gt;&lt;span class=op&gt;)&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-4&gt;&lt;a href=#cb4-4&gt;&lt;/a&gt;&lt;span class=op&gt;}&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-5&gt;&lt;a href=#cb4-5&gt;&lt;/a&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-6&gt;&lt;a href=#cb4-6&gt;&lt;/a&gt;&lt;span class=kw&gt;type&lt;/span&gt; FileLogger &lt;span class=kw&gt;struct&lt;/span&gt; &lt;span class=op&gt;{&lt;/span&gt; … &lt;span class=op&gt;}&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-7&gt;&lt;a href=#cb4-7&gt;&lt;/a&gt;&lt;span class=kw&gt;func&lt;/span&gt; &lt;span class=op&gt;(&lt;/span&gt;l FileLogger&lt;span class=op&gt;)&lt;/span&gt; Info&lt;span class=op&gt;(&lt;/span&gt;message &lt;span class=dt&gt;string&lt;/span&gt;&lt;span class=op&gt;)&lt;/span&gt; &lt;span class=op&gt;{&lt;/span&gt; … &lt;span class=op&gt;}&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-8&gt;&lt;a href=#cb4-8&gt;&lt;/a&gt;&lt;span class=kw&gt;func&lt;/span&gt; &lt;span class=op&gt;(&lt;/span&gt;l FileLogger&lt;span class=op&gt;)&lt;/span&gt; Warning&lt;span class=op&gt;(&lt;/span&gt;message &lt;span class=dt&gt;string&lt;/span&gt;&lt;span class=op&gt;)&lt;/span&gt; &lt;span class=op&gt;{&lt;/span&gt; … &lt;span class=op&gt;}&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-9&gt;&lt;a href=#cb4-9&gt;&lt;/a&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-10&gt;&lt;a href=#cb4-10&gt;&lt;/a&gt;&lt;span class=kw&gt;func&lt;/span&gt; main&lt;span class=op&gt;()&lt;/span&gt; &lt;span class=op&gt;{&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-11&gt;&lt;a href=#cb4-11&gt;&lt;/a&gt;    &lt;span class=kw&gt;var&lt;/span&gt; f &lt;span class=op&gt;*&lt;/span&gt;os&lt;span class=op&gt;.&lt;/span&gt;File&lt;/span&gt;&#xA;&lt;span id=cb4-12&gt;&lt;a href=#cb4-12&gt;&lt;/a&gt;    logger &lt;span class=op&gt;:=&lt;/span&gt; FileLogger&lt;span class=op&gt;{&lt;/span&gt;file&lt;span class=op&gt;:&lt;/span&gt; f&lt;span class=op&gt;}&lt;/span&gt; &lt;span class=co&gt;// ✅ Fixed!&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-13&gt;&lt;a href=#cb4-13&gt;&lt;/a&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-14&gt;&lt;a href=#cb4-14&gt;&lt;/a&gt;    foo&lt;span class=op&gt;(&lt;/span&gt;logger&lt;span class=op&gt;)&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-15&gt;&lt;a href=#cb4-15&gt;&lt;/a&gt;    bar&lt;span class=op&gt;(&lt;/span&gt;logger&lt;span class=op&gt;)&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-16&gt;&lt;a href=#cb4-16&gt;&lt;/a&gt;&lt;span class=op&gt;}&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-17&gt;&lt;a href=#cb4-17&gt;&lt;/a&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-18&gt;&lt;a href=#cb4-18&gt;&lt;/a&gt;&lt;span class=kw&gt;func&lt;/span&gt; foo&lt;span class=op&gt;(&lt;/span&gt;l Logger&lt;span class=op&gt;)&lt;/span&gt; &lt;span class=op&gt;{&lt;/span&gt; … &lt;span class=op&gt;}&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-19&gt;&lt;a href=#cb4-19&gt;&lt;/a&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-20&gt;&lt;a href=#cb4-20&gt;&lt;/a&gt;&lt;span class=kw&gt;func&lt;/span&gt; bar&lt;span class=op&gt;(&lt;/span&gt;l Logger&lt;span class=op&gt;)&lt;/span&gt; &lt;span class=op&gt;{&lt;/span&gt; … &lt;span class=op&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;With that manual update, everything clicks satisfyingly into place.&#xA;&lt;code&gt;go build demo.go&lt;/code&gt; succeeds. All three of your goals for the&#xA;refactor are satisfied. The diagram is green. To quote the&#xA;fortune-cookie fortune taped to my monitor at home, “☺ The job is well&#xA;done. ☺”&lt;figure&gt;&lt;img src=../img/refactors/fig4.svg alt=&#34;Now that main constructs an implementation, all the type references in our program refer to appropriate type definitions.&#34;&gt;&lt;figcaption aria-hidden=true&gt;Now that &lt;code&gt;main&lt;/code&gt; constructs an&#xA;implementation, all the type references in our program refer to&#xA;appropriate type definitions.&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;At this point you’re free to proceed with what you set out to&#xA;achieve: additional &lt;code&gt;Logger&lt;/code&gt; implementations.&lt;p&gt;You’re also free to swap in different names (e.g. reverting&#xA;&lt;code&gt;FileLogger&lt;/code&gt; to &lt;code&gt;Logger&lt;/code&gt; and naming the interface&#xA;&lt;code&gt;ILogger&lt;/code&gt; if that’s more your style). Now that you have the&#xA;type relationships you wanted, your IDE’s “Refactor → Rename”&#xA;functionality can take care of the semantic fixes.&lt;h1 id=shortest-paths&gt;Shortest paths&lt;/h1&gt;&lt;p&gt;This was a simple example, but this three-step method works for&#xA;complex interfaces, with more diverse users, in &lt;em&gt;far, far&#xA;gnarlier&lt;/em&gt; codebases.&lt;p&gt;But beware! Selectively vacating names before redefining them with&#xA;compatible types is a neat trick, but it isn’t the right path for every&#xA;refactor. Static type-checking provides “&lt;em&gt;safe&lt;/em&gt; route[s] where we&#xA;don’t smash everything up,” but it’s up to you to pick a &lt;em&gt;short&lt;/em&gt;&#xA;one.&lt;p&gt;In this logger interface example, you break references to the&#xA;implementation because, after the migration, you expect to have many&#xA;more references to the interface &lt;code&gt;Logger&lt;/code&gt; (hundreds of&#xA;function signatures) than to the implementation &lt;code&gt;FileLogger&lt;/code&gt;&#xA;(a handful of constructors: easy to update manually).&lt;p&gt;Were that relationship were reversed — if you were interfacing a type&#xA;that’s frequently constructed and infrequently received — manually&#xA;reconnecting the constructors would be tedious. You would actually save&#xA;time by updating the few function signatures &lt;em&gt;without&lt;/em&gt; renaming.&#xA;Then again, if something’s frequently constructed and infrequently&#xA;received, you’re less likely to be replacing it with an interface!&lt;p&gt;To be sure, you can follow this three-step refactor path in a&#xA;language without static types, but you have to very cautiously assess&#xA;whether you’re actually done. Jon Blow again:&lt;blockquote&gt;&lt;p&gt;Yeah, the type safety part of Python — or the lack thereof — is huge.&#xA;A lot of the stuff that we did today, these grunt work refactors I’m&#xA;doing, would be a lot scarier in Python. You’d be like, “I don’t know if&#xA;I just broke something.” We’ve done ten things today, at least, where&#xA;you’d be like “I don’t know if that broke something,” where in C++&#xA;you’re like “yeah, I &lt;em&gt;know&lt;/em&gt; I didn’t break something.” It’s very&#xA;different.&lt;a href=#fn4 class=footnote-ref id=fnref4 role=doc-noteref&gt;&lt;sup&gt;4&lt;/sup&gt;&lt;/a&gt;&lt;/blockquote&gt;&lt;p&gt;Every broken build is a blessing in disguise: it could’ve been a&#xA;runtime exception. So long as your language has your back, break things&#xA;with confidence. You might just be a types person.&lt;aside id=footnotes class=&#34;footnotes footnotes-end-of-document&#34; role=doc-endnotes&gt;&lt;hr&gt;&lt;ol&gt;&lt;li id=fn1&gt;&lt;p&gt;YouTube: &lt;a href=https://youtu.be/2J-HIh3kXCQ&gt;Jonathan&#xA;Blow on Refactoring&lt;/a&gt;.&lt;a href=#fnref1 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn2&gt;&lt;p&gt;I only sat down to write this post because Vladimir&#xA;Khorikov uses a similar logger-interface example in &lt;em&gt;Unit Testing&#xA;Principles, Practices, and Patterns&lt;/em&gt; (2020). Paraphrasing: log lines&#xA;are outcoming effects of your program, so mocking them is reasonable,&#xA;but consider implementing a domain-specific logger for the messages that&#xA;are observable aspects of the system under test.&lt;a href=#fnref2 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn3&gt;&lt;p&gt;In this Go example, that’s a trivial side-effect of the&#xA;first requirement (Go interfaces are implemented &lt;em&gt;implicitly&lt;/em&gt;).&#xA;&lt;a href=https://go.dev/tour/methods/10&gt;A Tour of Go: Interfaces are&#xA;implemented implicitly&lt;/a&gt; pairs a beginner-friendly explanation with a&#xA;short code sample.&lt;a href=#fnref3 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn4&gt;&lt;p&gt;YouTube: &lt;a href=https://youtu.be/2J-HIh3kXCQ&gt;Jonathan&#xA;Blow on Refactoring&lt;/a&gt;.&lt;a href=#fnref4 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;/ol&gt;&lt;/aside&gt;</content>
    <link href="https://lukasschwab.me/blog/gen/three-step-lifted-interfaces.html" rel="alternate"></link>
    <summary type="html">In which I confess my dependence on strongly-statically-typed programming languages, bore you to death about &#34;abstraction&#34; and &#34;refactoring,&#34; and tell you to break programs in order to fix them.</summary>
  </entry>
  <entry>
    <title>Four Translations of *Todesfuge*</title>
    <updated>2023-06-10T00:00:00Z</updated>
    <id>https://lukasschwab.me/blog/gen/celan-translations.html</id>
    <content type="html">&lt;html xmlns=http://www.w3.org/1999/xhtml lang xml:lang&gt;&lt;meta charset=utf-8&gt;&lt;meta name=generator content=&#34;pandoc&#34;&gt;&lt;meta name=viewport content=&#34;width=device-width,initial-scale=1,user-scalable=yes&#34;&gt;&lt;meta name=dcterms.date content=&#34;2023-06-10&#34;&gt;&lt;title&gt;Four Translations of Todesfuge&lt;/title&gt;&lt;style&gt;&#xA;      code{white-space: pre-wrap;}&#xA;      span.smallcaps{font-variant: small-caps;}&#xA;      span.underline{text-decoration: underline;}&#xA;      div.column{display: inline-block; vertical-align: top; width: 50%;}&#xA;          &lt;/style&gt;&lt;style&gt;pre &gt; code.sourceCode { white-space: pre; position: relative; }&#xA;pre &gt; code.sourceCode &gt; span { display: inline-block; line-height: 1.25; }&#xA;pre &gt; code.sourceCode &gt; span:empty { height: 1.2em; }&#xA;.sourceCode { overflow: visible; }&#xA;code.sourceCode &gt; span { color: inherit; text-decoration: inherit; }&#xA;div.sourceCode { margin: 1em 0; }&#xA;pre.sourceCode { margin: 0; }&#xA;@media screen {&#xA;div.sourceCode { overflow: auto; }&#xA;}&#xA;@media print {&#xA;pre &gt; code.sourceCode { white-space: pre-wrap; }&#xA;pre &gt; code.sourceCode &gt; span { text-indent: -5em; padding-left: 5em; }&#xA;}&#xA;pre.numberSource code&#xA;  { counter-reset: source-line 0; }&#xA;pre.numberSource code &gt; span&#xA;  { position: relative; left: -4em; counter-increment: source-line; }&#xA;pre.numberSource code &gt; span &gt; a:first-child::before&#xA;  { content: counter(source-line);&#xA;    position: relative; left: -1em; text-align: right; vertical-align: baseline;&#xA;    border: none; display: inline-block;&#xA;    -webkit-touch-callout: none; -webkit-user-select: none;&#xA;    -khtml-user-select: none; -moz-user-select: none;&#xA;    -ms-user-select: none; user-select: none;&#xA;    padding: 0 4px; width: 4em;&#xA;    color: #aaaaaa;&#xA;  }&#xA;pre.numberSource { margin-left: 3em; border-left: 1px solid #aaaaaa;  padding-left: 4px; }&#xA;div.sourceCode&#xA;  {   }&#xA;@media screen {&#xA;pre &gt; code.sourceCode &gt; span &gt; a:first-child::before { text-decoration: underline; }&#xA;}&#xA;code span.al { color: #ff0000; font-weight: bold; } /* Alert */&#xA;code span.an { color: #60a0b0; font-weight: bold; font-style: italic; } /* Annotation */&#xA;code span.at { color: #7d9029; } /* Attribute */&#xA;code span.bn { color: #40a070; } /* BaseN */&#xA;code span.bu { color: #008000; } /* BuiltIn */&#xA;code span.cf { color: #007020; font-weight: bold; } /* ControlFlow */&#xA;code span.ch { color: #4070a0; } /* Char */&#xA;code span.cn { color: #880000; } /* Constant */&#xA;code span.co { color: #60a0b0; font-style: italic; } /* Comment */&#xA;code span.cv { color: #60a0b0; font-weight: bold; font-style: italic; } /* CommentVar */&#xA;code span.do { color: #ba2121; font-style: italic; } /* Documentation */&#xA;code span.dt { color: #902000; } /* DataType */&#xA;code span.dv { color: #40a070; } /* DecVal */&#xA;code span.er { color: #ff0000; font-weight: bold; } /* Error */&#xA;code span.ex { } /* Extension */&#xA;code span.fl { color: #40a070; } /* Float */&#xA;code span.fu { color: #06287e; } /* Function */&#xA;code span.im { color: #008000; font-weight: bold; } /* Import */&#xA;code span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Information */&#xA;code span.kw { color: #007020; font-weight: bold; } /* Keyword */&#xA;code span.op { color: #666666; } /* Operator */&#xA;code span.ot { color: #007020; } /* Other */&#xA;code span.pp { color: #bc7a00; } /* Preprocessor */&#xA;code span.sc { color: #4070a0; } /* SpecialChar */&#xA;code span.ss { color: #bb6688; } /* SpecialString */&#xA;code span.st { color: #4070a0; } /* String */&#xA;code span.va { color: #19177c; } /* Variable */&#xA;code span.vs { color: #4070a0; } /* VerbatimString */&#xA;code span.wa { color: #60a0b0; font-weight: bold; font-style: italic; } /* Warning */&lt;/style&gt;&lt;link rel=stylesheet href=../styles/common.css&gt;&lt;link rel=stylesheet href=../styles/fence-extensions/addendum.css&gt;&lt;link rel=stylesheet href=../styles/fence-extensions/firstcoltable.css&gt;&lt;link rel=stylesheet href=../styles/fence-extensions/fullwidth.css&gt;&lt;link rel=stylesheet href=../styles/fence-extensions/poem.css&gt;&lt;script data-goatcounter=https://lukasschwab.goatcounter.com/count async src=//gc.zgo.at/count.js&gt;&lt;/script&gt;&lt;a href=../index.html&gt;&amp;#171; blog&lt;/a&gt; &lt;span class=date&gt;2023-06-10&lt;/span&gt;&lt;header id=title-block-header&gt;&lt;h1 class=title&gt;Four Translations of &lt;em&gt;Todesfuge&lt;/em&gt;&lt;/h1&gt;&lt;/header&gt;&lt;p&gt;&lt;em&gt;German Art After 1960&lt;/em&gt; at SFMOMA includes two huge Anselm&#xA;Kiefer canvases, heavy oil paintings matted with straw.&#xA;&lt;em&gt;Sulamith&lt;/em&gt; (1980) depicts a low groin-vaulted interior like a&#xA;crypt, built of brown and blackened stones. Small flames burn at the end&#xA;of a row of alcoves. The painting’s name — Hebrew in origin, a princess&#xA;in the Song of Songs — is scratched into the upper left.&lt;div class=fullwidth&gt;&lt;figure&gt;&lt;img src=../img/celan-translations/kiefer-sulamith-margarethe.png alt=&#34;Left: Sulamith, 1983. Right: Margarethe, 1981. Both photographs by SFMOMA.&#34;&gt;&lt;figcaption aria-hidden=true&gt;&lt;strong&gt;Left:&lt;/strong&gt; &lt;a href=https://www.sfmoma.org/artwork/FC.598/&gt;&lt;em&gt;Sulamith,&lt;/em&gt;&#xA;1983.&lt;/a&gt;&lt;br&gt;&lt;strong&gt;Right:&lt;/strong&gt; &lt;a href=https://www.sfmoma.org/artwork/FC.595/&gt;&lt;em&gt;Margarethe,&lt;/em&gt;&#xA;1981.&lt;/a&gt;&lt;br&gt;Both photographs by SFMOMA.&lt;/figcaption&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;&lt;em&gt;Margarethe&lt;/em&gt; (1981) looks like a series of wicks, each a long&#xA;bent tangle of straw topped with a little painted flame. Ash dapples the&#xA;ground, and the background sky is dotted with what could be stars behind&#xA;a cool milky-grey wash like wet smoke. The name “Margarethe” is scrawled&#xA;prominently across its center in dark-grey looping cursive. In &lt;a href=https://www.metmuseum.org/art/collection/search/490046&gt;an&#xA;earlier, cheerier watercolor version&lt;/a&gt; of this composition, Kiefer&#xA;paints golden wheat bowing under a blue sky.&lt;p&gt;The museum insciption explains these titles refer to the Paul Celan&#xA;poem &lt;em&gt;Todesfuge&lt;/em&gt; (Deathfugue), in which the narrator — a Jewish&#xA;prisoner forced by his blue-eyed guard to dig graves in a German&#xA;concentration camp — repeats “your golden hair Margarete” (&lt;em&gt;dein&#xA;goldenes Haar Margarete&lt;/em&gt;) and “your ashen hair Sulamith” (&lt;em&gt;dein&#xA;aschenes Haar Sulamith&lt;/em&gt;).&lt;p&gt;&lt;em&gt;Goldenes&lt;/em&gt; and &lt;em&gt;aschenes&lt;/em&gt; are descriptors rich with&#xA;nonvisiual connotations. Obviously blondness is of vain importance to&#xA;aryanism, but &lt;em&gt;gold&lt;/em&gt; specifically has been a German national&#xA;color since the Napoleonic Wars: in the tricolor, gold is classically&#xA;associated with the promise of a liberated future, the &lt;em&gt;goldene Licht&#xA;der Freiheit&lt;/em&gt; (golden light of freedom). Sulamith’s ashen hair, on&#xA;the other hand, confirms the Jewishness of her name. “Ashen hair” aligns&#xA;Hebrew-Biblical mourning (II Sam. xiii. 9; Josh. vii. 6; Job ii. 12&lt;a href=#fn1 class=footnote-ref id=fnref1 role=doc-noteref&gt;&lt;sup&gt;1&lt;/sup&gt;&lt;/a&gt;) with the Nazi extermination camps’&#xA;crematoria.&lt;p&gt;“Dein aschenes Haar Sulamith wir schaufeln ein Grab in den Lüften da&#xA;liegt man nicht eng;” does Celan address Sulamith directly? Are the&#xA;prisonsers digging a grave for her? Is &lt;em&gt;ein Grab in den Lüften&lt;/em&gt;&#xA;(approx. “a grave in the air”) a metaphor for cremation, or an image for&#xA;collective memory? When you read &lt;em&gt;Todesfuge&lt;/em&gt; in English, your&#xA;interpretation of these ambiguities is at your translator’s mercy. Each&#xA;takes a different approach. Together they demonstrate the difficulties&#xA;of translation, broadly, but also — by necessarily foregrounding&#xA;different aspects, and falling short in different ways — they reveal&#xA;what resonates in the original.&lt;/p&gt;&lt;h1 id=todesfuge-1948&gt;&lt;em&gt;Todesfuge&lt;/em&gt; (1948)&lt;/h1&gt;&lt;p&gt;Paul Celan, born in 1920 to a German-speaking Jewish family in&#xA;Cernăuți (Chernivtsi), survived a Romanian forced labor camp, lost his&#xA;parents to the S.S., and began publishing original poetry pseudonymously&#xA;after the war. Here he’s recorded reading &lt;em&gt;Todesfuge:&lt;/em&gt;&lt;/p&gt;&lt;audio controls&gt;&lt;source src=../img/celan-translations/celan-todesfuge.mp3 type=audio/mpeg&gt;&lt;/audio&gt;&lt;div class=poem&gt;&lt;pre&gt;&#xA;&lt;b&gt;Todesfuge&lt;/b&gt;&#xA;&#xA;Schwarze Milch der Frühe wir trinken sie abends&#xA;wir trinken sie mittags und morgens wir trinken sie nachts&#xA;wir trinken und trinken&#xA;wir schaufeln ein Grab in den Lüften da liegt man nicht eng&#xA;Ein Mann wohnt im Haus der spielt mit den Schlangen der schreibt&#xA;der schreibt wenn es dunkelt nach Deutschland dein goldenes Haar Margarete&#xA;er schreibt es und tritt vor das Haus und es blitzen die Sterne er pfeift seine Rüden herbei&#xA;er pfeift seine Juden hervor lässt schaufeln ein Grab in der Erde&#xA;er befiehlt uns spielt auf nun zum Tanz&#xA;&#xA;Schwarze Milch der Frühe wir trinken dich nachts&#xA;wir trinken dich morgens und mittags wir trinken dich abends&#xA;wir trinken und trinken&#xA;Ein Mann wohnt im Haus der spielt mit den Schlangen der schreibt&#xA;der schreibt wenn es dunkelt nach Deutschland dein goldenes Haar Margarete&#xA;Dein aschenes Haar Sulamith wir schaufeln ein Grab in den Lüften da liegt man nicht eng&#xA;&#xA;Er ruft stecht tiefer ins Erdreich ihr einen ihr andern singet und spielt&#xA;er greift nach dem Eisen im Gurt er schwingts seine Augen sind blau&#xA;stecht tiefer die Spaten ihr einen ihr andern spielt weiter zum Tanz auf&#xA;&#xA;Schwarze Milch der Frühe wir trinken dich nachts&#xA;wir trinken dich mittags und morgens wir trinken dich abends&#xA;wir trinken und trinken&#xA;ein Mann wohnt im Haus dein goldenes Haar Margarete&#xA;dein aschenes Haar Sulamith er spielt mit den Schlangen&#xA;&#xA;Er ruft spielt süsser den Tod der Tod ist ein Meister aus Deutschland&#xA;er ruft streicht dunkler die Geigen dann steigt ihr als Rauch in die Luft&#xA;dan habt ihr ein Grab in den Wolken da liegt man nicht eng&#xA;&#xA;Schwarze Milch der Frühe wir trinken dich nachts&#xA;wir trinken dich mittags der Tod ist ein Meister aus Deutschland&#xA;wir trinken dich abends und morgens wir trinken und trinken&#xA;der Tod ist ein Meister aus Deutschland sein Auge ist blau&#xA;er trifft dich mit bleierner Kugel er trifft dich genau&#xA;ein Mann wohnt im Haus dein goldenes Haar Margarete&#xA;er hetzt seine Ruden auf uns er schenkt uns ein Grab in der Luft&#xA;er spielt mit den Schlangen und träumet der Tod ist ein Meister aus Deutschland&#xA;&#xA;dein goldenes Haar Margarete&#xA;dein aschenes Haar Sulamith&#xA;&lt;/pre&gt;&lt;/div&gt;&lt;h1 id=translations&gt;Translations&lt;/h1&gt;&lt;p&gt;After seeing the Anselm Kiefer paintings, I found and typed&#xA;translations of &lt;em&gt;Todesfuge&lt;/em&gt; by John Felstiner, Michael Hamburger,&#xA;Jerome Rothenberg, and Karl S. Weimar respectively (all reproduced&#xA;below), and generated these files highlighting their pairwise&#xA;differences:&lt;a href=#fn2 class=footnote-ref id=fnref2 role=doc-noteref&gt;&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt;&lt;div class=firstcoltable&gt;&lt;table&gt;&lt;col style=&#34;width: 5%&#34;&gt;&lt;col style=&#34;width: 31%&#34;&gt;&lt;col style=&#34;width: 32%&#34;&gt;&lt;col style=&#34;width: 30%&#34;&gt;&lt;thead&gt;&lt;tr class=header&gt;&lt;th style=&#34;text-align: left;&#34;&gt;&lt;th style=&#34;text-align: left;&#34;&gt;Hamburger&lt;th style=&#34;text-align: left;&#34;&gt;Rothenberg&lt;th style=&#34;text-align: left;&#34;&gt;Weimar&lt;tbody&gt;&lt;tr class=odd&gt;&lt;td style=&#34;text-align: left;&#34;&gt;Felstiner&lt;td style=&#34;text-align: left;&#34;&gt;&lt;a href=../img/celan-translations/diffs/felstiner-hamburger.diff.html&gt;felstiner-hamburger.diff&lt;/a&gt;&lt;td style=&#34;text-align: left;&#34;&gt;&lt;a href=../img/celan-translations/diffs/felstiner-rothenberg.diff.html&gt;felstiner-rothenberg.diff&lt;/a&gt;&lt;td style=&#34;text-align: left;&#34;&gt;&lt;a href=../img/celan-translations/diffs/felstiner-weimar.diff.html&gt;felstiner-weimar.diff&lt;/a&gt;&lt;tr class=even&gt;&lt;td style=&#34;text-align: left;&#34;&gt;Hamburger&lt;td style=&#34;text-align: left;&#34;&gt;&lt;td style=&#34;text-align: left;&#34;&gt;&lt;a href=../img/celan-translations/diffs/hamburger-rothenberg.diff.html&gt;hamburger-rothenberg.diff&lt;/a&gt;&lt;td style=&#34;text-align: left;&#34;&gt;&lt;a href=../img/celan-translations/diffs/hamburger-weimar.diff.html&gt;hamburger-weimar.diff&lt;/a&gt;&lt;tr class=odd&gt;&lt;td style=&#34;text-align: left;&#34;&gt;Rothenberg&lt;td style=&#34;text-align: left;&#34;&gt;&lt;td style=&#34;text-align: left;&#34;&gt;&lt;td style=&#34;text-align: left;&#34;&gt;&lt;a href=../img/celan-translations/diffs/rothenberg-weimar.diff.html&gt;rothenberg-weimar.diff&lt;/a&gt;&lt;/table&gt;&lt;/div&gt;&lt;p&gt;Each translator has his editorial priorities. Weimar carefully&#xA;preserves the original’s meter, Hamburger its verbal simplicity.&#xA;Felstiner’s untranslated German phrases fragment the closing stanzas.&#xA;I’ve included short notes on what sets each apart.&lt;p&gt;One could spend ages doing these comparisons. Weimar critiques&#xA;several prior translations I haven’t read: Gertrude C. Schwebell’s&#xA;(1962), Donald White’s (1966), and Joachim Neugroschel’s (1971).&#xA;Felstiner notes at least “fifteen published English translations of&#xA;Todesfuge (and several unpublished),” and that was almost forty years&#xA;ago! These are just a few examples.&lt;h2 id=john-felstiner-1986&gt;John Felstiner (1986)&lt;/h2&gt;&lt;div class=poem&gt;&lt;pre&gt;&#xA;&lt;b&gt;Deathfugue&lt;/b&gt;&#xA;&#xA;Black milk of daybreak we drink it at evening&#xA;we drink it at midday and morning we drink it at night&#xA;we drink and we drink&#xA;we shovel a grave in the air where you won&#39;t lie too cramped&#xA;A man lives in the house he plays with his vipers he writes&#xA;he writes when it grows dark to Deutschland your golden hair Margareta&#xA;he writes it and steps out of doors and the stars are all sparkling he whistles his hounds to stay close&#xA;he whistles his Jews into rows has them shovel a grave in the ground&#xA;he commands us play up for the dance&#xA;&#xA;Black milk of daybreak we drink you at night&#xA;we drink you at morning and midday we drink you at evening&#xA;we drink and we drink&#xA;A man lives in the house he plays with his vipers he writes&#xA;he writes when it grows dark to Deutschland your golden hair Margareta&#xA;Your ashen hair Shulamith we shovel a grave in the air where you won&#39;t lie too cramped&#xA;&#xA;He shouts dig this earth deeper you lot there you others sing up and play&#xA;he grabs for the rod in his belt he swings it his eyes are so blue&#xA;stick your spades deeper you lot there you others play on for the dancing&#xA;&#xA;Black milk of daybreak we drink you at night&#xA;we drink you at midday and morning we drink you at evening&#xA;we drink and we drink&#xA;a man lives in the house your goldenes Haar Margareta&#xA;your aschenes Haar Shulamith he plays with his vipers&#xA;&#xA;He shouts play death more sweetly this Death is a master from Deutschland&#xA;he shouts scrape your strings darker you&#39;ll rise up as smoke to the sky&#xA;you&#39;ll then have a grave in the clouds where you won&#39;t lie too cramped&#xA;&#xA;Black milk of daybreak we drink you at night&#xA;we drink you at midday Death is a master aus Deutschland&#xA;we drink you at evening and morning we drink and we drink&#xA;this Death is ein Meister aus Deutschland his eye it is blue&#xA;he shoots you with shot made of lead shoots you level and true&#xA;a man lives in the house your goldenes Haar Margarete&#xA;he looses his hounds on us grants us a grave in the air&#xA;he plays with his vipers and daydreams der Tod ist ein Meister aus Deutschland&#xA;&#xA;dein goldenes Haar Margarete&#xA;dein aschenes Haar Sulamith&#xA;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Published in “Selected Poems and Prose of Paul Celan,” 2001.&lt;a href=#fn3 class=footnote-ref id=fnref3 role=doc-noteref&gt;&lt;sup&gt;3&lt;/sup&gt;&lt;/a&gt;&lt;p&gt;Felstiner published his notes, which describe Celan’s personal&#xA;history and the poem’s cultural significance in postwar Europe, under&#xA;the title “Paul Celan’s Todesfuge” (1986).&lt;a href=#fn4 class=footnote-ref id=fnref4 role=doc-noteref&gt;&lt;sup&gt;4&lt;/sup&gt;&lt;/a&gt;&#xA;After &lt;em&gt;Todesfuge&lt;/em&gt; achieved wide recognition, Felstiner recalls,&#xA;Celan’s verse grew more abstruse; he resisted &lt;em&gt;Todesfuge&lt;/em&gt;’s&#xA;inclusion in anthologies; he resented the poem’s public palatability&#xA;(what Felstiner politely terms “too congenial a reading,” the one that&#xA;wound up in German grade school textbooks).&lt;p&gt;Like any good translator, Felstiner hems and haws over words: “what&#xA;do you do, for instance, with Meister — a god, a champion, a guildsman,&#xA;a master of arts or theology, a labour-camp overseer, a musical maestro,&#xA;a rabbi, the ‘master’ race, not to speak of Richard Wagner’s&#xA;&lt;em&gt;Meistersinger von Nurnberg&lt;/em&gt;…” He gives special care to the&#xA;flawed assonance of &lt;em&gt;Rüden&lt;/em&gt; (hounds) and &lt;em&gt;Juden&lt;/em&gt; (Jews) in&#xA;the first stanza, explicitly rejecting Rothenberg’s “draw near”/“appear”&#xA;rhyme in favor of “stay close” and “into rows.” Actually,&#xA;&lt;em&gt;Weimar’s&lt;/em&gt; solution — “come up” and “come out” — best captures&#xA;the original effect.&lt;p&gt;Words aside, Felstiner’s translation is my favorite among these four&#xA;because it captures the tumbling, subtly accelerating &lt;em&gt;cadence&lt;/em&gt;&#xA;of Celan’s reading. “In translating,” Felstiner writes, “I [went] back&#xA;again and again, longingly, to Celan’s own recorded voice — he read with&#xA;‘a cold heat,’ one friend said — to absorb his rhythms, pauses,&#xA;emphases, retards, quickenings, caustic articulation.”&lt;p&gt;The translation’s progressive dissolution back into German is key: at&#xA;first, “Death is a master from Deutschland,” then “Death is a master&#xA;&lt;em&gt;aus&lt;/em&gt; Deutschland,” “ein Meister aus Deutschland,” and finally&#xA;“der Tod ist ein Meister aus Deutschland.” In the final couplet, even&#xA;the names are left unadapted. The effect is undeniably fuguelike&#xA;(feverish swirling, multiplicity of voices).&lt;p&gt;Of course, Celan’s original doens’t have this progression or anything&#xA;like it. Weimar’s structural analysis considers the &lt;em&gt;Tod ist ein&#xA;Meister aus Deutschland&lt;/em&gt; refrain a distinct “voice,” distinguished&#xA;by the repetition. Break each repetition into translated and&#xA;untranslated portions and the repetition is gone.&lt;p&gt;What place does a change this &lt;em&gt;blatantly editorial&lt;/em&gt; have in&#xA;translation? My anthology of Felstiner’s translations includes original&#xA;German texts, in which a detail-oriented non-germanophone can spot the&#xA;original repetition. Readers with a dedicated interest in Celan&#xA;translations might have more “neutral” (less editorial!) points of&#xA;reference, like the Hamburger translation below. Less dedicated readers&#xA;might not recognize the translator’s hand.&lt;p&gt;Felstiner’s preface to “Selected Poems” argues this license is&#xA;authentic: Celan himself relied on it for his translations into&#xA;German.&lt;blockquote&gt;&lt;p&gt;For Celan as translator, faithful often did mean fresh. Vis-à-vis&#xA;French or Russian or English verse, he was given to fracturing,&#xA;contracting, omitting, specifying, intensifying, inventing, repeating&#xA;where the original had no repetition, changing nouns into verbs,&#xA;indicatives into imperatives or gerunds, and so on.&lt;p&gt;[…]&lt;p&gt;Too easily, I believe, lyric poetry gets labeled untranslatable,&#xA;especially in the case of someone [Celan] whose personal losses rendered&#xA;his German language at once precarious and privileged, inalienable yet&#xA;irreplicable; someone calling himself “whitegravel stutterer;” someone&#xA;speaking from his “true- / stammered mouth” about “eternity / bloodblack&#xA;embabeled,” blutschwarz umbabelt. But then why not think of translation&#xA;as the specific art of loss, and begin from there?&lt;a href=#fn5 class=footnote-ref id=fnref5 role=doc-noteref&gt;&lt;sup&gt;5&lt;/sup&gt;&lt;/a&gt;&lt;/blockquote&gt;&lt;h2 id=michael-hamburger-1972&gt;Michael Hamburger (1972)&lt;/h2&gt;&lt;div class=poem&gt;&lt;pre&gt;&#xA;&lt;b&gt;Death Fugue&lt;/b&gt;&#xA;&#xA;Black milk of daybreak we drink it at sundown&#xA;we drink it at noon in the morning we drink it at night&#xA;we drink and we drink it&#xA;we dig a grave in the breezes there one lies unconfined&#xA;A man lives in the house he plays with the serpents he writes&#xA;he writes when dusk falls to Germany your golden hair Margarete&#xA;he writes it and steps out of doors and the stars are flashing he whistles his pack out&#xA;he whistles his Jews out in earth has them dig for a grave&#xA;he commands us strike up for the dance&#xA;&#xA;Black milk of daybreak we drink you at night&#xA;we drink in the morning at noon we drink you at sundown&#xA;we drink and we drink you&#xA;A man lives in the house he plays with the serpents he writes&#xA;he writes when dusk falls to Germany your golden hair Margarete&#xA;your ashen hair Shulamith we dig a grave in the breezes there one lies unconfined&#xA;&#xA;He calls out jab deeper into the earth you lot you others sing now and play&#xA;he grabs at the iron in his belt he waves it his eyes are blue&#xA;jab deeper you lot with your spades you others play on for the dance&#xA;&#xA;Black milk of daybreak we drink you at night&#xA;we drink you at noon in the morning we drink you at sundown&#xA;we drink and we drink you&#xA;a man lives in the house your golden hair Margarete&#xA;your ashen hair Shulamith he plays with the serpents&#xA;&#xA;He calls out more sweetly play death death is a master from Germany&#xA;he calls out more darkly now stroke your strings then as smoke you will rise into air&#xA;then a grave you will have in the clouds there one lies unconfined&#xA;&#xA;Black milk of daybreak we drink you at night&#xA;we drink you at noon death is a master from Germany&#xA;we drink you at sundown and in the morning we drink and we drink you&#xA;death is a master from Germany his eyes are blue&#xA;he strikes you with leaden bullets his aim is true&#xA;a man lives in the house your golden hair Margarete&#xA;he sets his pack on to us he grants us a grave in the air&#xA;he plays with the serpents and daydreams death is a master from Germany&#xA;&#xA;your golden hair Margarete&#xA;your ashen hair Shulamith&#xA;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Published in “Poems of Paul Celan,” 1972.&lt;a href=#fn6 class=footnote-ref id=fnref6 role=doc-noteref&gt;&lt;sup&gt;6&lt;/sup&gt;&lt;/a&gt;&lt;p&gt;Hamburger seems to assign the adverbs &lt;em&gt;süßer&lt;/em&gt; and&#xA;&lt;em&gt;dunkler&lt;/em&gt; strangely: they describe the calls rather than the&#xA;playing. This construction is true to the original word order, and with&#xA;the right punctuation the call could read as a forced second-position&#xA;verb:&lt;blockquote&gt;&lt;p&gt;He calls out, “more sweetly play death…”&lt;/blockquote&gt;&lt;p&gt;Unfortunately, there isn’t any punctuation to prompt this&#xA;interpretation, so we read “He calls out more sweetly, ‘play death…’”&#xA;Even if they were punctuated, these lines would be awkward. Distracting.&#xA;I’m doubly puzzled because Hamburger isn’t always precious about word&#xA;order: &lt;em&gt;er pfeift seine Juden hervor lässt schaufeln ein Grab &lt;u&gt;in&#xA;der Erde&lt;/u&gt;&lt;/em&gt; is rendered as “he whistles his Jews out &lt;u&gt;in&#xA;earth&lt;/u&gt; has them dig for a grave.” Maybe Hamburger just prefers&#xA;awkward phrasings.&lt;p&gt;Otherwise, this translation seems the most neutral of the bunch — the&#xA;closest to word-by-word translation, without left fragments of German or&#xA;unseemly new images…&lt;h2 id=jerome-rothenberg-2005&gt;Jerome Rothenberg (2005)&lt;/h2&gt;&lt;div class=poem&gt;&lt;pre&gt;&#xA;&lt;b&gt;Death Fugue&lt;/b&gt;&#xA;&#xA;Black milk of morning we drink you at dusktime&#xA;we drink you at noontime and dawntime we drink you at night&#xA;we drink and drink&#xA;we scoop out a grave in the sky where it&#39;s roomy to lie&#xA;There&#39;s a man in this house who cultivates snakes and who writes&#xA;who writes when it&#39;s nightfall nach Deutschland your golden hair Margareta&#xA;he writes it and walks from the house and the stars all start flashing he whistles his dogs to draw near&#xA;whistles his Jews to appear starts us scooping a grave out of sand&#xA;he commands us play up for the dance&#xA;&#xA;Black milk of morning we drink you at night&#xA;we drink you at dawntime and noontime we drink you at dusktime&#xA;we drink and drink&#xA;There&#39;s a man in this house who cultivates snakes and who writes&#xA;who writes when it&#39;s nightfall nach Deutschland your golden hair Margareta&#xA;your ashen hair Shulamite we we scoop out a grave in the sky where it&#39;s roomy to lie&#xA;&#xA;He calls jab it deep in the soil you men you other men sing and play&#xA;he tugs at the sword in his belt he swings it his eyes are blue&#xA;jab your spades deeper you men you other men play up again for the dance&#xA;&#xA;Black milk of morning we drink you at night&#xA;we drink you at noontime and dawntime we drink you at dusktime&#xA;we drink and drink&#xA;there&#39;s a man in this house your golden hair Margareta&#xA;your ashen hair Shulamite he cultivates snakes&#xA;&#xA;He calls play that death thing more sweetly Death is a gang-boss aus Deutschland&#xA;he calls scrape that fiddle more darkly then hover like smoke in the air&#xA;then scoop out a grave in the clouds where it&#39;s roomy to lie&#xA;&#xA;Black milk of morning we drink you at night&#xA;we drink you at noontime Death is a gang-boss aus Deutschland&#xA;we drink you at dusktime and dawntime we drink and drink&#xA;Death is a gang-boss aus Deutschland his eye is blue&#xA;he hits you with leaden bullets his aim is true&#xA;there&#39;s a man in this house your golden hair Margareta&#xA;he sets his dogs on our trail he gives us a grave in the sky&#xA;he cultivates snakes and he dreams Death is a gang-boss aus Deutschland&#xA;&#xA;your golden hair Margareta&#xA;your ashen hair Shulamite&#xA;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Published in “Paul Celan: Selections,” 2005,&lt;a href=#fn7 class=footnote-ref id=fnref7 role=doc-noteref&gt;&lt;sup&gt;7&lt;/sup&gt;&lt;/a&gt; but&#xA;this translation is much older — Rothenberg translated&#xA;&lt;em&gt;Todesfuge&lt;/em&gt; for a City Lights booklet published in 1959.&lt;a href=#fn8 class=footnote-ref id=fnref8 role=doc-noteref&gt;&lt;sup&gt;8&lt;/sup&gt;&lt;/a&gt;&lt;p&gt;Frankly, this one isn’t for me. “Roomy” and “scooping” have childish&#xA;sounds and sandcastle connotations. “Sword” is an improbable translation&#xA;of &lt;em&gt;Eisen&lt;/em&gt; (lit. “iron”). Why is &lt;em&gt;den Tod&lt;/em&gt; rendered “that&#xA;death thing?”&lt;p&gt;Why is &lt;em&gt;Meister&lt;/em&gt; — which has a musical second meaning —&#xA;rendered “gang-boss,” apparently referring to the blue-eyed commandant?&#xA;In the other translations, this stanza suggests personification by&#xA;melding fragmented references to Death, the musical master from Germany,&#xA;and the gang-boss. Eliminating the musical connotations of &lt;em&gt;Meister&#xA;aus Deutschland&lt;/em&gt; eliminates a character from that trio and bluntly&#xA;joins the other two.&lt;p&gt;I get the impression Rothenberg injects what Weimar calls an&#xA;“artificially poetic element.” &lt;em&gt;Steigen&lt;/em&gt; is a simple verb&#xA;suggesting motion (lit. “to climb”), but Rothenberg picks “hover,”&#xA;suggesting stillness. &lt;em&gt;Abends&lt;/em&gt; becomes “dusktime;”&#xA;&lt;em&gt;mittags,&lt;/em&gt; “noontime;” &lt;em&gt;morgens,&lt;/em&gt; “dawntime.”&lt;p&gt;The uncapitalized &lt;em&gt;sie&lt;/em&gt; in the original first stanza is a&#xA;third-person pronoun (the other translations here use “it,” then switch&#xA;to “you” in later stanzas when Celan uses &lt;em&gt;du&lt;/em&gt;). Did Celan really&#xA;mean the formal, second-person, capitalized &lt;em&gt;Sie&lt;/em&gt; or, more&#xA;likely, does this translation efface the third- to second-person&#xA;shift?&lt;p&gt;Another pronominal controversy: Weimar complains Rothenberg defuses&#xA;the dramatic tension of Celan’s original by using “who” instead of “he”&#xA;in the first and second stanzas.&lt;blockquote&gt;&lt;p&gt;The actions are simple and swift and Celan’s intention, clearly&#xA;reflected in the almost breathless monotonous way in which he himself&#xA;reads the work and in the complete absence of the printed text, is to&#xA;infuse the poem with a relentless, irresistible momentum […] Both&#xA;Schwebell’s and Rothenberg’s translations destroy this unity of short,&#xA;tense, disconnected sentences, especially when they render the&#xA;domonstrative pronoun &lt;em&gt;der&lt;/em&gt; (l. 5 [&lt;em&gt;Ein Mann wohnt im Haus der&#xA;spielt…&lt;/em&gt;]) as a relative pronoun.&lt;a href=#fn9 class=footnote-ref id=fnref9 role=doc-noteref&gt;&lt;sup&gt;9&lt;/sup&gt;&lt;/a&gt;&lt;/blockquote&gt;&lt;p&gt;Elsewhere he calls Rothenberg’s word choice “boldly scabrous.”&lt;p&gt;Like Felstiner, Rothenberg leaves &lt;em&gt;Deutschland&lt;/em&gt; (and its&#xA;prepositions) untranslated, but the untranslated phrases don’t progress&#xA;stanza to stanza. This is a reasonable minimum: can you really imagine&#xA;Hamburger and Weimar’s blue-eyed men writing to “Germany?”&lt;h2 id=karl-s.-weimar-1974&gt;Karl S. Weimar (1974)&lt;/h2&gt;&lt;div class=poem&gt;&lt;pre&gt;&#xA;&lt;b&gt;Fugue of Death&lt;/b&gt;&#xA;&#xA;Coal-black milk of morning we drink it at evening&#xA;we drink it at noon and at daybreak we drink it at night&#xA;we drink and we drink&#xA;we shovel a grave in the skies there is room enough there&#xA;A man lives in the house he plays with his vipers he writes&#xA;he writes when it darkens to Germany your golden hair Marguerite&#xA;he writes it and steps out of doors and the stars are shining he whistles his dogs to come up&#xA;he whistles his Jews to come out to shovel a grave in the ground&#xA;he commands us strike up a tune for the dance&#xA;&#xA;Coal-black milk of morning we drink you at night&#xA;we drink you at daybreak and noon and we drink you at evening&#xA;we drink and we drink&#xA;A man lives in the house he plays with his vipers he writes&#xA;he writes when it darkens to Germany your golden hair Marguerite&#xA;Your ashen hair Shulamite we shovel a grave in the skies there is room enough there&#xA;&#xA;He shouts dig deeper into the earth you here and you there start singing and playing&#xA;he clutches the gun in his belt he waves it his eyes are blue&#xA;dig deeper your spades you here and you there keep playing that dance tune&#xA;&#xA;Coal-black milk of morning we drink you at night&#xA;we drink you at noon and at daybreak we drink you at evening&#xA;we drink and we drink&#xA;a man lives in the house your golden hair Marguerite&#xA;your ashen hair Shulamite he plays with his vipers&#xA;&#xA;He shouts play the death tune sweeter death is a master from Germany&#xA;he shouts strike up the fiddles more darkly you&#39;ll rise like the smoke to the sky&#xA;you&#39;ll have your own grave in the clouds there is room enough there&#xA;&#xA;Coal-black milk of morning we drink you at night&#xA;we drink you at noon death is a master from Germany&#xA;we drink you at evening and at daybreak we drink and we drink&#xA;death is a master from Germany his eye is blue&#xA;he hits you with bullets of lead his target is you&#xA;a man lives in the house your golden hair Marguerite&#xA;he sets loose his dogs after us he gives us a grave in the sky&#xA;he plays with his vipers and dreams death is a master from Germany&#xA;&#xA;your golden hair Marguerite&#xA;your ashen hair Shulamite&#xA;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Published with translator notes in “Paul Celan’s ‘Todesfuge’:&#xA;Translation and Interpretation,” 1974.&lt;a href=#fn10 class=footnote-ref id=fnref10 role=doc-noteref&gt;&lt;sup&gt;10&lt;/sup&gt;&lt;/a&gt;&lt;p&gt;Weimar takes the poem’s title (&lt;em&gt;fuge,&lt;/em&gt; meaning “fugue”) as a&#xA;suggestion to focus on its meter in translation. “Coal-black,” like&#xA;&lt;em&gt;schwarze,&lt;/em&gt; is a spondee: two syllables, with stress on the&#xA;first.&lt;a href=#fn11 class=footnote-ref id=fnref11 role=doc-noteref&gt;&lt;sup&gt;11&lt;/sup&gt;&lt;/a&gt;&lt;p&gt;I have mixed feelings about “coal” here. On one hand, the focus on&#xA;meter sets this translation apart, and Celan’s reading is distinctively&#xA;rhythmic. On the other, “coal” is a baggaged noun — especially in a&#xA;German modern-industrial context, and in the Holocaust more&#xA;specifically, where Weimar’s “coal-black” prefigures “the smoke to the&#xA;sky” more than the original German &lt;em&gt;schwartze&lt;/em&gt;. Moreover, Weimar&#xA;weakens Celan’s reference to a &lt;a href=https://jewishcurrents.org/to-life&gt;Rose Ausländer poem&lt;/a&gt;&#xA;mentioning black milk.&lt;p&gt;Elsewhere (“golden hair” and “ashen hair”) Weimar focuses on&#xA;retaining the metric &lt;em&gt;parallel&lt;/em&gt; rather than the specific meter:&#xA;both adjectives lose a syllable. “Aureate” might’ve worked, but he&#xA;rejects it: it’s too posh and there isn’t a good grey equivalent. Like&#xA;Hamburger’s selective preservation of word-order, this puzzles me; if&#xA;you’ll settle for parallels, ditch “coal-black!”&lt;p&gt;Weimar’s structural and metric analysis does a lot to convince me of&#xA;a translation I still don’t particularly like &lt;em&gt;reading.&lt;/em&gt; Because&#xA;they focus on the translator’s task and directly critique other&#xA;translations (especially Rothenberg’s), the notes are a useful&#xA;companion.&lt;aside id=footnotes class=&#34;footnotes footnotes-end-of-document&#34; role=doc-endnotes&gt;&lt;hr&gt;&lt;ol&gt;&lt;li id=fn1&gt;&lt;p&gt;Jewish Encyclopedia. &lt;em&gt;Ashes.&lt;/em&gt; &lt;a href=https://www.jewishencyclopedia.com/articles/1944-ashes&gt;Available&#xA;online.&lt;/a&gt;&lt;a href=#fnref1 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn2&gt;&lt;p&gt;&lt;code&gt;diff -u michael-hamburger.txt john-felstiner.txt&lt;/code&gt;&#xA;yields a simple diff; piping that to &lt;a href=https://github.com/so-fancy/diff-so-fancy&gt;&lt;code&gt;diff-so-fancy&lt;/code&gt;&lt;/a&gt;&#xA;yields a GitHub-style token-by-token diff; &lt;a href=https://github.com/theZiz/aha&gt;&lt;code&gt;aha&lt;/code&gt;&lt;/a&gt; converts the&#xA;output to self-contained HTML. All together:&lt;div class=sourceCode id=cb1&gt;&lt;pre class=&#34;sourceCode bash&#34;&gt;&lt;code class=&#34;sourceCode bash&#34;&gt;&lt;span id=cb1-1&gt;&lt;a href=#cb1-1 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=fu&gt;diff&lt;/span&gt; &lt;span class=at&gt;-u&lt;/span&gt; michael-hamburger.txt john-felstiner.txt &lt;span class=kw&gt;|&lt;/span&gt; &lt;span class=ex&gt;diff-so-fancy&lt;/span&gt; &lt;span class=kw&gt;|&lt;/span&gt; &lt;span class=ex&gt;aha&lt;/span&gt; &lt;span class=op&gt;&amp;gt;&lt;/span&gt; hf.diff.htm&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class=addendum data-date=&#34;May 1, 2025&#34;&gt;&lt;p&gt;Not anymore! I regenerated these diffs with a tool I wrote: &lt;a href=https://github.com/lukasschwab/poem-diff&gt;poem-diff&lt;/a&gt;. The&#xA;results are semantically clearer than &lt;code&gt;aha&lt;/code&gt; output and don’t&#xA;suggest a pre- and post-edit state like a typical code-diff tool.&lt;/div&gt;&lt;a href=#fnref2 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn3&gt;&lt;p&gt;Paul Celan, trans. John Felstiner. &lt;em&gt;Selected Poems&#xA;and Prose of Paul Celan&lt;/em&gt; (New York: W. W. Norton, 2001), 30–33.&lt;a href=#fnref3 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn4&gt;&lt;p&gt;Felstiner, John. “Paul Celan’s Todesfuge.” &lt;em&gt;Holocaust&#xA;and genocide studies&lt;/em&gt; 1, no. 2 (1986): 249-264.&lt;a href=#fnref4 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn5&gt;&lt;p&gt;Paul Celan, trans. John Felstiner. &lt;em&gt;Selected Poems&#xA;and Prose of Paul Celan&lt;/em&gt; (New York: W. W. Norton, 2001), xxxiii.&lt;a href=#fnref5 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn6&gt;&lt;p&gt;Paul Celan, trans. Michael Hamburger. &lt;em&gt;Poems of Paul&#xA;Celan.&lt;/em&gt; (New York: Persea Books, 1972), 62–65.&lt;a href=#fnref6 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn7&gt;&lt;p&gt;Paul Celan, ed. Pierre Joris and Jerome Rothenberg.&#xA;&lt;em&gt;Paul Celan: Selections.&lt;/em&gt; (Berkeley: University of California&#xA;Press, 2005), 46–47.&lt;a href=#fnref7 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn8&gt;&lt;p&gt;Jerome Rothenberg. “‘Reading Celan’ (1959, 1995, 2020)&#xA;for the hundredth anniversary of Paul Celan’s birth.” &lt;a href=https://jacket2.org/commentary/jerome-rothenberg-2&gt;Available&#xA;online.&lt;/a&gt;&lt;a href=#fnref8 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn9&gt;&lt;p&gt;Karl S. Weimar. “Paul Celan’s “Todesfuge”: Translation&#xA;and Interpretation.” &lt;em&gt;PMLA 89,&lt;/em&gt; no. 1 (1974): 85–96.&lt;a href=#fnref9 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn10&gt;&lt;p&gt;Karl S. Weimar. “Paul Celan’s “Todesfuge”: Translation&#xA;and Interpretation.” &lt;em&gt;PMLA 89,&lt;/em&gt; no. 1 (1974): 85–96.&lt;a href=#fnref10 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn11&gt;&lt;p&gt;Poetry Foundation has a &lt;a href=https://www.poetryfoundation.org/learn/glossary-terms/foot&gt;good&#xA;little gloassary&lt;/a&gt; of poetic meters. I’m sure there are good reasons&#xA;for the historic names, but they’re easily rendered as binary strings: a&#xA;length (number of syllables), where each syllable is stressed&#xA;(&lt;code&gt;1&lt;/code&gt;) or unstressed (&lt;code&gt;0&lt;/code&gt;).&lt;table&gt;&lt;col style=&#34;width: 30%&#34;&gt;&lt;col style=&#34;width: 25%&#34;&gt;&lt;col style=&#34;width: 44%&#34;&gt;&lt;thead&gt;&lt;tr class=header&gt;&lt;th&gt;Metric foot&lt;th&gt;Example&lt;th style=&#34;text-align: center;&#34;&gt;Syllabic emphasis&lt;tbody&gt;&lt;tr class=odd&gt;&lt;td&gt;&lt;a href=https://www.poetryfoundation.org/learn/glossary-terms/pyrrhic-meter&gt;pyrrhic&lt;/a&gt;&lt;td&gt;to a&lt;td style=&#34;text-align: center;&#34;&gt;&lt;code&gt;00&lt;/code&gt;&lt;tr class=even&gt;&lt;td&gt;&lt;a href=https://www.poetryfoundation.org/learn/glossary-terms/iamb&gt;iamb&lt;/a&gt;&lt;td&gt;unite&lt;td style=&#34;text-align: center;&#34;&gt;&lt;code&gt;01&lt;/code&gt;&lt;tr class=odd&gt;&lt;td&gt;&lt;a href=https://www.poetryfoundation.org/learn/glossary-terms/trochee&gt;trochee&lt;/a&gt;&lt;td&gt;highway&lt;td style=&#34;text-align: center;&#34;&gt;&lt;code&gt;10&lt;/code&gt;&lt;tr class=even&gt;&lt;td&gt;&lt;a href=https://www.poetryfoundation.org/learn/glossary-terms/spondee&gt;spondee&lt;/a&gt;&lt;td&gt;hog-wild&lt;td style=&#34;text-align: center;&#34;&gt;&lt;code&gt;11&lt;/code&gt;&lt;tr class=odd&gt;&lt;td&gt;&lt;a href=https://www.poetryfoundation.org/learn/glossary-terms/dactyl&gt;dactyl&lt;/a&gt;&lt;td&gt;poetry&lt;td style=&#34;text-align: center;&#34;&gt;&lt;code&gt;100&lt;/code&gt;&lt;tr class=even&gt;&lt;td&gt;&lt;a href=https://www.poetryfoundation.org/learn/glossary-terms/anapest&gt;anapest&lt;/a&gt;&lt;td&gt;underfoot&lt;td style=&#34;text-align: center;&#34;&gt;&lt;code&gt;001&lt;/code&gt;&lt;/table&gt;&lt;p&gt;You’d think they’d &lt;em&gt;at least&lt;/em&gt; get demonstrative names, but&#xA;“dactyl” is one unemphasized syllable short of actually being a&#xA;dactyl.&lt;a href=#fnref11 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;/ol&gt;&lt;/aside&gt;</content>
    <link href="https://lukasschwab.me/blog/gen/celan-translations.html" rel="alternate"></link>
    <summary type="html">I dug up four English translations of Paul Celan&#39;s Holocaust poem *Todesfuge* (Deathfugue) in order to understand a pair of Anselm Kiefer paintings on view at SFMOMA. Why bother with four? They demonstrate why translation&#39;s so difficult by highlighting different aspects of the original German.</summary>
  </entry>
  <entry>
    <title>Ceci n&#39;est pas...</title>
    <updated>2023-03-19T00:00:00Z</updated>
    <id>https://lukasschwab.me/blog/gen/broodthaers.html</id>
    <content type="html">&lt;html xmlns=http://www.w3.org/1999/xhtml lang xml:lang&gt;&lt;meta charset=utf-8&gt;&lt;meta name=generator content=&#34;pandoc&#34;&gt;&lt;meta name=viewport content=&#34;width=device-width,initial-scale=1,user-scalable=yes&#34;&gt;&lt;meta name=dcterms.date content=&#34;2023-03-19&#34;&gt;&lt;title&gt;Ceci n’est pas…&lt;/title&gt;&lt;style&gt;&#xA;      code{white-space: pre-wrap;}&#xA;      span.smallcaps{font-variant: small-caps;}&#xA;      span.underline{text-decoration: underline;}&#xA;      div.column{display: inline-block; vertical-align: top; width: 50%;}&#xA;          &lt;/style&gt;&lt;link rel=stylesheet href=../styles/common.css&gt;&lt;link rel=stylesheet href=../styles/fence-extensions/centertext.css&gt;&lt;link rel=stylesheet href=../styles/fence-extensions/frame.css&gt;&lt;link rel=stylesheet href=../styles/fence-extensions/fullwidth.css&gt;&lt;link rel=stylesheet href=../styles/fence-extensions/poem.css&gt;&lt;script data-goatcounter=https://lukasschwab.goatcounter.com/count async src=//gc.zgo.at/count.js&gt;&lt;/script&gt;&lt;a href=../index.html&gt;&amp;#171; blog&lt;/a&gt; &lt;span class=date&gt;2023-03-19&lt;/span&gt;&lt;header id=title-block-header&gt;&lt;h1 class=title&gt;Ceci n’est pas…&lt;/h1&gt;&lt;/header&gt;&lt;p&gt;Marcel Broodthaers’s career as a visual artist begins where his&#xA;career as a poet — in the traditional poems-on-paper sense — comes to an&#xA;abrupt end: in 1964, in an oblong blob of plaster. The work, &lt;em&gt;La&#xA;Pense-Bête&lt;/em&gt;, stands as a cenotaph to earnest meaning. “I, too,&#xA;wondered whether I could not sell something and succeed in life,”&#xA;Broodthaers writes in the exhibition prospectus. “For some time I had&#xA;been no good at anything. I am forty years old… Finally the idea of&#xA;inventing something insincere crossed my mind and I set to work&#xA;straightaway.”&lt;a href=#fn1 class=footnote-ref id=fnref1 role=doc-noteref&gt;&lt;sup&gt;1&lt;/sup&gt;&lt;/a&gt;&lt;div class=frame&gt;&lt;figure&gt;&lt;img src=../img/broodthaers/pense-bete.jpg alt=&#34;Marcel Broodthaers, Le Pense Bête, 1964. Photograph by S.M.A.K., Ghent.&#34;&gt;&lt;figcaption aria-hidden=true&gt;Marcel Broodthaers, &lt;em&gt;Le Pense&#xA;Bête&lt;/em&gt;, 1964. Photograph by S.M.A.K., Ghent.&lt;/figcaption&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;Forty-four copies of Broodthaers’s last book of poems stand upright,&#xA;closed and partially wrapped in newspaper, in a haphazard glob of&#xA;plaster. A faded-orange orb like a shattered buoy (the Tate Gallery’s&#xA;1980 retrospective calls it “nacreous”) nestles in the high mound of&#xA;plaster at the books’ spines. A plaster egg is crushed into the far end&#xA;of the agglomeration, where it peters into the inked fiberboard&#xA;base.&lt;/p&gt;&lt;p&gt;&lt;em&gt;La Pense-Bête&lt;/em&gt;’s plaster mass sits in S.M.A.K (acronym for&#xA;“Stedelijk Museum voor Actuele Kunst”). I visited S.M.A.K. in October&#xA;2021, part of a weeklong march through Belgium’s art museums, the day&#xA;after my &lt;em&gt;first introduction&lt;/em&gt; to Broodthaers: an exhibit of his&#xA;Industrial Poems at WIELS (not an acronym, capitalized anyway).&lt;p&gt;The Industrial Poems are a series of 120 plaques manufactured in&#xA;groups of seven between 1968 and 1972. The signs are glossy plastic,&#xA;vacuum-formed on plywood matrices and then painted. In a self-published&#xA;self-interview, Broodthaers facetiously points out “[t]hese plaques are&#xA;fabricated like waffles.”&lt;a href=#fn2 class=footnote-ref id=fnref2 role=doc-noteref&gt;&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt;&lt;p&gt;Each sign sports color-blocking unique in its group. Though they have&#xA;a common formed surface, the paint selects which raised features to&#xA;foreground and which to leave back. Seven plaques from the same mold&#xA;suggest different meanings through selective emphasis: one highlights&#xA;just diacritic marks; another highlights certain words; another certain&#xA;&lt;em&gt;other&lt;/em&gt; words; and so on. Though any plaque, taken individually,&#xA;is cryptic, their arrangement together invites a game of&#xA;subset-selection: “perhaps some different subset of these words/shapes&#xA;&lt;em&gt;holds the meaning.&lt;/em&gt;” Broodthaers refers to the plaque-puzzles as&#xA;“rebuses,” intentionally opaque demonstrations of the “difficulty of&#xA;reading that results when you use this substance.”&lt;a href=#fn3 class=footnote-ref id=fnref3 role=doc-noteref&gt;&lt;sup&gt;3&lt;/sup&gt;&lt;/a&gt;&lt;div class=fullwidth&gt;&lt;figure&gt;&lt;img src=../img/broodthaers/museum.jpg alt=&#34;Left: Museum. Enfants non admis, 1968–69.  Right: Museum. Enfants non admis, 1968–69. Both photographs by The Museum of Modern Art.&#34;&gt;&lt;figcaption aria-hidden=true&gt;&lt;strong&gt;Left:&lt;/strong&gt; &lt;a href=https://www.moma.org/collection/works/146980&gt;&lt;em&gt;Museum. Enfants&#xA;non admis&lt;/em&gt;, 1968–69.&lt;/a&gt;&lt;br&gt;&lt;strong&gt;Right:&lt;/strong&gt; &lt;a href=https://www.moma.org/collection/works/146979&gt;&lt;em&gt;Museum. Enfants&#xA;non admis&lt;/em&gt;, 1968–69.&lt;/a&gt;&lt;br&gt;Both photographs by The Museum of Modern&#xA;Art.&lt;/figcaption&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;That “difficulty of reading” is the central concept of Broodthaers’s&#xA;oeuvre, a thread he started pulling in that 1964 inaugural exhibit.&#xA;&lt;em&gt;La Pense-Bête,&lt;/em&gt; he writes, turned a book into “the object of a&#xA;prohibition:”&lt;blockquote&gt;&lt;p&gt;I took a bundle of fifty copies of a book called &lt;em&gt;Pense-Bête&lt;/em&gt;&#xA;and half-embedded them in plaster. The wrapping paper is torn off at the&#xA;top of the “sculpture,” so you can see the stack of books (the bottom&#xA;part is hidden by the plaster). Here you cannot read the book without&#xA;destroying its sculptural aspect. It is a concrete gesture that passes&#xA;the prohibition on to the viewer — at least that’s what I thought would&#xA;happen.&lt;a href=#fn4 class=footnote-ref id=fnref4 role=doc-noteref&gt;&lt;sup&gt;4&lt;/sup&gt;&lt;/a&gt;&lt;/blockquote&gt;&lt;p&gt;The thread continues through his other work. A 2019 Ghent exhibition&#xA;of unreadable books called Broodthaers a “leader of illegibility.”&lt;a href=#fn5 class=footnote-ref id=fnref5 role=doc-noteref&gt;&lt;sup&gt;5&lt;/sup&gt;&lt;/a&gt; Through blacked-out concrete poetry,&#xA;text on crumpled packing paper, miniscule atlases, and &lt;a href=&#34;https://www.youtube.com/watch?v=sFuHPOMKmt4&#34;&gt;cat interviews&lt;/a&gt;,&#xA;Broodthaers explores tantalizing illegibility. Sometimes, as with &lt;em&gt;La&#xA;Pense-Bête&lt;/em&gt;, there’s a sculptural “prohibition.” By contrast, the&#xA;Industrial Poems are sculpturally &lt;em&gt;legible&lt;/em&gt; but dubiously&#xA;&lt;em&gt;meaningful&lt;/em&gt;, at least in a direct way — are they cryptic, or are&#xA;they entirely nonsensical?&lt;div class=frame&gt;&lt;figure&gt;&lt;img src=../img/broodthaers/broodthaers.jpg alt=&#34;Broodthaers at “Der Adler vom Oligozän bis heute,” Kunsthalle Düsseldorf, 1972.&#34;&gt;&lt;figcaption aria-hidden=true&gt;Broodthaers at “Der Adler vom Oligozän&#xA;bis heute,” Kunsthalle Düsseldorf, 1972.&lt;/figcaption&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;Unsurprisingly, commentaries on Broodthaers often connect his&#xA;fixation with meaning to contemporary semiology. These are the 1960s in&#xA;francophone Europe: Jacques Derrida is dissolving structuralist&#xA;linguistics into a phenomenological fever-dream, and Roland Barthes is&#xA;announcing &lt;em&gt;La mort de l’auteur&lt;/em&gt; (1967). Broodthaers shares their&#xA;solidarities — when Paris’s universities spark the May 68 general&#xA;strike, he occupies the Brussels Palais des Beaux-Arts — and makes&#xA;insincere references to their work: “[h]is comment on Magritte takes the&#xA;form of directing the reader to Michel Foucault’s essay ‘Ceci n’est pas&#xA;une pipe,’ which at that time existed only as a published lecture that&#xA;[he] had almost certainly not read.”&lt;a href=#fn6 class=footnote-ref id=fnref6 role=doc-noteref&gt;&lt;sup&gt;6&lt;/sup&gt;&lt;/a&gt;&lt;p&gt;Art historian Benjamin H.D. Buchloh relates Broodthaers’s&#xA;illegibility (“the linguistic sign becoming the object of semiological&#xA;and poetical decomposition”) to Ferdinand de Saussure’s bipartite theory&#xA;of signs: the separation of a &lt;em&gt;signifier&lt;/em&gt; — an image or an&#xA;utterance or what have you — from a &lt;em&gt;signified,&lt;/em&gt; that concept it&#xA;represents.&lt;a href=#fn7 class=footnote-ref id=fnref7 role=doc-noteref&gt;&lt;sup&gt;7&lt;/sup&gt;&lt;/a&gt; In practice, the jump from signifier&#xA;to signified can appear so automatic Saussure’s distinction seems&#xA;pedantic. An iconographic signifier (e.g. the illuminated&#xA;seatbelts-fastened sign in an airplane) visually resembles its signified&#xA;(you buckling your damn seatbelt). But signs aren’t always so direct. An&#xA;illustrated eagle &lt;em&gt;could&lt;/em&gt; iconographically signify a certain&#xA;bird, but it could also represent a heraldic national identity; same&#xA;sign, different signifieds!&lt;p&gt;For Buchloh, Broodthaers’s industrial poems assert the Saussurean&#xA;dichotomy by refusing to usefully fulfill it. Rather than presenting a&#xA;straightforward signified, their writing “refuse[s] the visual or&#xA;sensual data which the viewer demands.” Maybe they’re cryptic —&#xA;signifying something inaccessible — but we can’t rule out that they’re&#xA;nonsensical, they signify nothing at all! Buchloh calls these ‘anomic&#xA;objects,’ defined by their “withdrawal from systems of communication,&#xA;[their] self-imposed condition of muteness and silence.”&lt;a href=#fn8 class=footnote-ref id=fnref8 role=doc-noteref&gt;&lt;sup&gt;8&lt;/sup&gt;&lt;/a&gt;&#xA;Whereas &lt;em&gt;La Pense-Bête&lt;/em&gt; prohibited reading by embedding books in&#xA;plaster, the Industrial Poems allow reading but inhibit its automatic&#xA;effect (the reader’s translation from sign to signifier).&lt;p&gt;Ethnographer Michael Oppitz, on the other hand, identifies&#xA;Broodthaers with Roland Barthes’s reaction to Saussure, wherein&#xA;signifier and signified are more weakly coupled by the interpreting&#xA;reader.&lt;a href=#fn9 class=footnote-ref id=fnref9 role=doc-noteref&gt;&lt;sup&gt;9&lt;/sup&gt;&lt;/a&gt;&lt;div class=fullwidth&gt;&lt;figure&gt;&lt;img src=../img/broodthaers/broodthaers-porte.jpg alt=&#34;Left: Porte A, 1969. Right: Porte A, 1969. Both photographs by The Museum of Modern Art.&#34;&gt;&lt;figcaption aria-hidden=true&gt;&lt;strong&gt;Left:&lt;/strong&gt; &lt;a href=https://www.moma.org/collection/works/146981&gt;&lt;em&gt;Porte A&lt;/em&gt;,&#xA;1969.&lt;/a&gt; &lt;strong&gt;Right:&lt;/strong&gt; &lt;a href=https://www.moma.org/collection/works/146982&gt;&lt;em&gt;Porte A&lt;/em&gt;,&#xA;1969.&lt;/a&gt;&lt;br&gt;Both photographs by The Museum of Modern Art.&lt;/figcaption&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;My lasting impression from the WIELS exhibit is that both Buchloh and&#xA;Oppitz miss an opportunity to treat the Industrial Poems &lt;em&gt;as&#xA;objects&lt;/em&gt; first and foremost. By considering the plaques differently,&#xA;we can reconsider how a museum should stage encounters with&#xA;meta-referential art.&lt;p&gt;In essence, this requires a generalizing step-back from&#xA;&lt;em&gt;signs&lt;/em&gt; to &lt;em&gt;tools.&lt;/em&gt; In general, Saussure treats each&#xA;instance of a sign (e.g. the inked imprint of a word) as a tool for&#xA;meaning-making. For the most part — as with our automatic interpretation&#xA;of the seatbelts-fastened indicator — they signify smoothly, so long as&#xA;they belong to a familiar lexicon or fit into a familiar structure. “We&#xA;read in two ways,” he writes: “a new or unknown word is spelled out&#xA;letter by letter; but a common, ordinary word is embraced by a single&#xA;glance, independently of its letters, so that the image of the whole&#xA;word acquires an ideographic value.”&lt;a href=#fn10 class=footnote-ref id=fnref10 role=doc-noteref&gt;&lt;sup&gt;10&lt;/sup&gt;&lt;/a&gt;&lt;p&gt;The difference between familiar (automatic) and unfamiliar&#xA;(letter-by-letter) signs mirrors Heidegger’s notion of the tool&#xA;ready-to-hand (&lt;em&gt;zuhandenheit&lt;/em&gt;), a sort of teleological status&#xA;where an object disappears into a task without you having to think about&#xA;it. When you’re hammering nails, the hammer-as-object disappears into&#xA;the hammer-as-purpose. You don’t consider &lt;em&gt;this&lt;/em&gt; hammer. You just&#xA;&lt;em&gt;hammer.&lt;/em&gt;&lt;a href=#fn11 class=footnote-ref id=fnref11 role=doc-noteref&gt;&lt;sup&gt;11&lt;/sup&gt;&lt;/a&gt;&lt;p&gt;You’re rudely introduced to the tool-analogue of Saussure’s&#xA;unfamiliar sign when you take a swing and the head of the hammer flies&#xA;off. A broken hammer refuses to be reduced, invisibly, to its purpose.&#xA;You can only get back to the purpose (the bliss of hammering) by&#xA;considering &lt;em&gt;this&lt;/em&gt; hammer as an object (or, suddenly, two&#xA;objects). How did it break? What are the handle and head materials? How&#xA;are they textured? The broken tool interrupts its own invisibility, the&#xA;same way an unfamiliar word interrupts ideographic reading and forces&#xA;you to consider the individual letters.&lt;p&gt;If the Industrial Poems were just &lt;em&gt;words,&lt;/em&gt; Heidegger’s&#xA;tool-phenomenology wouldn’t tell us anything Saussure can’t. They’re&#xA;more than words: they’re tools with words on them! Whereas Saussure’s&#xA;familiarity model implies a one-way flow of signifiers (from unfamiliar&#xA;to familiar), Heidegger’s tool analysis allows familiar objects to&#xA;become unfamiliar. That’s how the plaques function: they appear legible&#xA;at first.&lt;p&gt;Broodthaers’s promotional materials cautioned his audience against&#xA;reducing the Industrial Poems to semiological demonstrations. “What&#xA;kinds of simpletons do you catch with your plaques?” he asks himself;&#xA;“those who take these plaques for pictures and hang them on their walls.&#xA;Although there’s no proof that the real simpleton isn’t the author&#xA;himself, who thought he was a linguist able to leap over the bar in the&#xA;signifier/signified formula, but who might in fact have been merely&#xA;playing the professor.”&lt;a href=#fn12 class=footnote-ref id=fnref12 role=doc-noteref&gt;&lt;sup&gt;12&lt;/sup&gt;&lt;/a&gt; The Tate Gallery delivers a direct&#xA;caution. Because “[t]he plastic panels contain a series of puzzles with&#xA;no solution […] the sophisticated, unable to read them, but recognizing&#xA;them as signs, would perhaps find in them an art of semiology — but they&#xA;would be wrong.” Like the other object-assemblages Broodthaers exhibited&#xA;(the books in plaster, the miniscule atlas, etc.), the Industrial Poems’&#xA;“meaning was primary and had no status as metalanguage. They are made up&#xA;of elements drawn from the culture but do not directly comment on it or&#xA;describe it.”&lt;a href=#fn13 class=footnote-ref id=fnref13 role=doc-noteref&gt;&lt;sup&gt;13&lt;/sup&gt;&lt;/a&gt;&lt;p&gt;This deepens Broodthaers’s prohibition on intelligibility: you can’t&#xA;discern a &lt;em&gt;direct&lt;/em&gt; meaning from the Industrial Poems, and you’re&#xA;instructed to avoid secondarily interpreting them as the didactic&#xA;expression of a semiotic concept.&lt;p&gt;Moreover, the Industrial Poems take the form of &lt;em&gt;plaques.&lt;/em&gt; A&#xA;museum is full of plaques-as-tools, sometimes descriptive — little white&#xA;museum labels — and sometimes prohibitive. Broodthaers’s Industrial&#xA;Poems camouflage themselves as tools in the tool-world of signage. Some&#xA;even purport to be museum signs, bearing museum-specific descriptions&#xA;(e.g. &lt;em&gt;Porte A&lt;/em&gt;) or prohibitions (e.g. &lt;em&gt;Museum. Enfants non&#xA;admis&lt;/em&gt;). That camouflage invites an unwitting viewer to see them as&#xA;tools first, and to run headlong into interruption when their signifiers&#xA;make no sense.&lt;figure&gt;&lt;img src=../img/broodthaers/camouflage.jpg alt=&#34;Musée d’Art Moderne, Département des Aigles, Section Publicité, 1971. Photograph by MASI Lugano.&#34;&gt;&lt;figcaption aria-hidden=true&gt;&lt;em&gt;Musée d’Art Moderne, Département des&#xA;Aigles, Section Publicité&lt;/em&gt;, 1971. Photograph by MASI&#xA;Lugano.&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;That risk-of-interruption is a function of the Industrial Poems’&#xA;status as &lt;em&gt;tool objects.&lt;/em&gt; If you reduce them to their rebuses —&#xA;present them as prints, say, with pictureframes, behind glass — the trap&#xA;of their brittle readiness-to-hand gives way to an actual, unbroken&#xA;readiness-to-hand as &lt;em&gt;art objects.&lt;/em&gt;&lt;p&gt;Oppitz remarks that René Magritte was “unable to protect his painting&#xA;[The Treachery of Images] against the art-historical significance it&#xA;would acquire […] as art object,” and hopes Broodthaers’s work fares&#xA;better.&lt;a href=#fn14 class=footnote-ref id=fnref14 role=doc-noteref&gt;&lt;sup&gt;14&lt;/sup&gt;&lt;/a&gt; Broodthaers designed the Industrial&#xA;Poems with similar concerns in mind, fears developed in what he saw as&#xA;the &lt;em&gt;failure&lt;/em&gt; of &lt;em&gt;La Pense-Bête&lt;/em&gt;. He &lt;em&gt;thought&lt;/em&gt; his&#xA;concrete gesture would render &lt;em&gt;La Pense-Bête&lt;/em&gt; illegible, but he’d&#xA;inadvertently formed an art object for which that prohibition held no&#xA;weight: their status as art stripped the books of their status as&#xA;books.&lt;blockquote&gt;&lt;p&gt;viewers reacted quite differently from what I had imagined. Everyone&#xA;so far, no matter who, has perceived the object either as an artistic&#xA;expression or as a curiosity. “Look! Books in plaster!” No one had any&#xA;curiosity about the text; nobody had any idea whether this was the final&#xA;burial of prose or poetry, of sadness or pleasure. No one was affected&#xA;by the prohibition.&lt;a href=#fn15 class=footnote-ref id=fnref15 role=doc-noteref&gt;&lt;sup&gt;15&lt;/sup&gt;&lt;/a&gt;&lt;/blockquote&gt;&lt;p&gt;The Industrial Poems’ advantage is their subtlety, the possibility&#xA;that they can slip — at least for a moment, for an unfamiliar viewer —&#xA;out of the world of art, into the world of tools. Their plasticity plays&#xA;a special role. “These plaques,” Broodthaers explains in his&#xA;introduction to their exhibition, “occupy the border between object and&#xA;image. According to their mechanical production they seem to &lt;em&gt;deny&#xA;their status as art objects.&lt;/em&gt;”&lt;a href=#fn16 class=footnote-ref id=fnref16 role=doc-noteref&gt;&lt;sup&gt;16&lt;/sup&gt;&lt;/a&gt; At WIELS, the&#xA;painstakingly comprehensive display of Industrial Poems undermined this&#xA;denial and ensuing surprise: mounting the Poems in sets from the same&#xA;mold invites the viewer to try comparative interpretation or, worse, to&#xA;lose the individual plaques in the candy-colored mass of the&#xA;edition.&lt;p&gt;Memorably, I saw one more Industrial Poem in a dim corner of Mu.ZEE&#xA;in Ostend. It was a surprise, not least because of its slippery&#xA;crimson-and-gold shine (on a floor of blue-green pontilists, in the&#xA;greyest city in the world). The plaque seems utilitarian there, even&#xA;commercial; even if you discern the irony, you could mistake it for the&#xA;same tongue-in-cheek trendiness forcing the capitalized museum names.&#xA;It’s too bad my recognition prevented me puzzling over the French.&lt;p&gt;Broodthaers, of course, exhibited the Industrial Poems together.&#xA;Unlike WIELS, his 1972 exhibition bore a disclaimer on every object, “a&#xA;label saying either in French, German, or English ‘This is not a work of&#xA;art!’”&lt;a href=#fn17 class=footnote-ref id=fnref17 role=doc-noteref&gt;&lt;sup&gt;17&lt;/sup&gt;&lt;/a&gt;&lt;p&gt;If they’re not works of art, nor intelligible texts, nor didactic&#xA;semiology, how should you consider them? Broodthaers’s prospectus for&#xA;his inaugural solo exhibition, the one that entombs his poetry and&#xA;declares his insincerity, at least &lt;em&gt;appears&lt;/em&gt; to support my&#xA;hunch.&lt;div class=centertext&gt;&lt;p&gt;CE QUE C’EST?&lt;br&gt;EN FAIT, DES OBJETS.&lt;p&gt;WHAT IS IT?&lt;br&gt;IN FACT, IT IS OBJECTS.&lt;/div&gt;&lt;aside id=footnotes class=&#34;footnotes footnotes-end-of-document&#34; role=doc-endnotes&gt;&lt;hr&gt;&lt;ol&gt;&lt;li id=fn1&gt;&lt;p&gt;&lt;em&gt;Marcel Broodthaers.&lt;/em&gt; London: Tate Gallery, 1980.&#xA;Pages 12-13.&lt;a href=#fnref1 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn2&gt;&lt;p&gt;Broodthaers, Marcel, Irmeline Lebeer, and Paul Schmidt.&#xA;“Ten thousand francs reward.” &lt;em&gt;October&lt;/em&gt; 42 (1987): 41. Trés&#xA;belge!&lt;a href=#fnref2 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn3&gt;&lt;p&gt;Broodthaers, Marcel, Irmeline Lebeer, and Paul Schmidt.&#xA;“Ten thousand francs reward.” &lt;em&gt;October&lt;/em&gt; 42 (1987): 41.&lt;a href=#fnref3 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn4&gt;&lt;p&gt;Broodthaers, Marcel, Irmeline Lebeer, and Paul Schmidt.&#xA;“Ten thousand francs reward.” &lt;em&gt;October&lt;/em&gt; 42 (1987): 44.&lt;a href=#fnref4 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn5&gt;&lt;p&gt;BOEKS, &lt;a href=https://boeks.gent/en/stoppen-met-lezen/&gt;&lt;em&gt;Stoppen met&#xA;lezen&lt;/em&gt;&lt;/a&gt;. (Ghent: KASK, 2019).&lt;a href=#fnref5 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn6&gt;&lt;p&gt;Compton, Michael. “In Praise of the Subject.” &lt;em&gt;Marcel&#xA;Broodthaers&lt;/em&gt; (1989), Walker Art Center: 51.&lt;a href=#fnref6 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn7&gt;&lt;p&gt;Buchloh, Benjamin H.D. “Marcel Broodthaers: Allegories&#xA;of the Avant-Garde” in &lt;em&gt;Artforum&lt;/em&gt; (May 1980). &lt;a href=https://www.artforum.com/print/198005/marcel-broodthaers-allegories-of-the-avant-garde-35808&gt;Online.&lt;/a&gt;&lt;a href=#fnref7 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn8&gt;&lt;p&gt;Buchloh, Benjamin HD. “Open letters, industrial poems.”&#xA;&lt;em&gt;October&lt;/em&gt; (1987): 73.&lt;a href=#fnref8 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn9&gt;&lt;p&gt;Oppitz, Michael, and Chris Cullens. “Eagle/Pipe/Urinal.”&#xA;&lt;em&gt;October&lt;/em&gt; 42 (1987): 155–56.&lt;a href=#fnref9 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn10&gt;&lt;p&gt;Bally, Charles, Albert Sechehaye, and A. Riedlinger.&#xA;“Ferdinand de Saussure: Courses in General Linguistics.” &lt;em&gt;London:&#xA;Peter Owen&lt;/em&gt; (1960). Quoted in Buchloh (1987, p.77).&lt;a href=#fnref10 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn11&gt;&lt;p&gt;Everyone’s trying to make sense of Heidegger. Mark&#xA;Foster Gage offers a thorougher example:&lt;blockquote&gt;&lt;p&gt;Heidegger’s tool analysis posits that &lt;em&gt;while a tool is&#xA;functioning&lt;/em&gt; [ready-to-hand], or its function is visible, the mind&#xA;registers it as equipment and, as such, &lt;em&gt;it is rendered invisible to&#xA;our attention&lt;/em&gt;. For example, the keyboard on which I type is&#xA;equipment insofar as it looks like what it does and is being used for a&#xA;particular function. As Heidegger might say, the keyboard recedes into&#xA;the background and is inconspicuous while in use or while ready to be&#xA;used. As I use the keyboard I do not notice its aesthetic properties or&#xA;other qualities. However, if the keyboard ceased to function, it would&#xA;become visible to my attention as an object, no longer mentally&#xA;processed as background equipment.&lt;/blockquote&gt;&lt;p&gt;Of course, we could both be completely and embarrassingly wrong. See&#xA;Gage, Mark Foster. “Killing Simplicity: Object-Oriented Philosophy in&#xA;Architecture.” &lt;em&gt;Log&lt;/em&gt; 33 (2015): 97.&lt;a href=#fnref11 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn12&gt;&lt;p&gt;Broodthaers, Marcel, Irmeline Lebeer, and Paul Schmidt.&#xA;“Ten thousand francs reward.” &lt;em&gt;October&lt;/em&gt; 42 (1987): 42.&lt;a href=#fnref12 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn13&gt;&lt;p&gt;&lt;em&gt;Marcel Broodthaers.&lt;/em&gt; London: Tate Gallery,&#xA;1980. Page 18.&lt;a href=#fnref13 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn14&gt;&lt;p&gt;Oppitz, Michael, and Chris Cullens.&#xA;“Eagle/Pipe/Urinal.” &lt;em&gt;October&lt;/em&gt; 42 (1987): 155–56.&lt;a href=#fnref14 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn15&gt;&lt;p&gt;Broodthaers, Marcel, Irmeline Lebeer, and Paul Schmidt.&#xA;“Ten thousand francs reward.” &lt;em&gt;October&lt;/em&gt; 42 (1987): 44.&lt;a href=#fnref15 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn16&gt;&lt;p&gt;Broodthaers, Marcel. &lt;em&gt;BROODTHAERS,&lt;/em&gt; exhibition&#xA;announcement for Galerie Gerda Bassenge, Berlin (1969). Available &lt;a href=https://www.moma.org/collection/works/109528&gt;online&lt;/a&gt; in the&#xA;original German; translated in Buchloh, p. 96. Emphasis added.&lt;a href=#fnref16 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn17&gt;&lt;p&gt;Compton, Michael. “In Praise of the Subject.”&#xA;&lt;em&gt;Marcel Broodthaers&lt;/em&gt; (1989), Walker Art Center: 51.&lt;a href=#fnref17 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;/ol&gt;&lt;/aside&gt;</content>
    <link href="https://lukasschwab.me/blog/gen/broodthaers.html" rel="alternate"></link>
    <summary type="html">My extended reflections on an exhibit of Marcel Broodthaers&#39;s Industrial Poems at WIELS in Brussels, in 2021. I claim Ferdinand de Saussure&#39;s notion of &#39;ideographic value&#39; is a linguistic-specific case of Martin Heidegger&#39;s &#39;ready-to-hand;&#39; treating the Industrial Poems as tools rather than language reveals more about their function.</summary>
  </entry>
  <entry>
    <title>Easier OCR on macOS</title>
    <updated>2023-02-18T00:00:00Z</updated>
    <id>https://lukasschwab.me/blog/gen/ocr.html</id>
    <content type="html">&lt;html xmlns=http://www.w3.org/1999/xhtml lang xml:lang&gt;&lt;meta charset=utf-8&gt;&lt;meta name=generator content=&#34;pandoc&#34;&gt;&lt;meta name=viewport content=&#34;width=device-width,initial-scale=1,user-scalable=yes&#34;&gt;&lt;meta name=author content=&#34;Lukas Schwab&#34;&gt;&lt;meta name=dcterms.date content=&#34;2023-02-18&#34;&gt;&lt;title&gt;Easier OCR on macOS&lt;/title&gt;&lt;style&gt;&#xA;      code{white-space: pre-wrap;}&#xA;      span.smallcaps{font-variant: small-caps;}&#xA;      span.underline{text-decoration: underline;}&#xA;      div.column{display: inline-block; vertical-align: top; width: 50%;}&#xA;          &lt;/style&gt;&lt;link rel=stylesheet href=../styles/common.css&gt;&lt;script data-goatcounter=https://lukasschwab.goatcounter.com/count async src=//gc.zgo.at/count.js&gt;&lt;/script&gt;&lt;a href=../index.html&gt;&amp;#171; blog&lt;/a&gt; &lt;span class=date&gt;2023-02-18&lt;/span&gt;&lt;header id=title-block-header&gt;&lt;h1 class=title&gt;Easier OCR on macOS&lt;/h1&gt;&lt;/header&gt;&lt;p&gt;Scanning a document digitizes an &lt;em&gt;image&lt;/em&gt; of a printed page,&#xA;but doesn’t digitize the &lt;em&gt;text&lt;/em&gt; on that page: you can’t search&#xA;for a keyword or copy a relevant passage into another document. Optical&#xA;Character Recognition programs interpret the scanned images, “see” the&#xA;individual letterforms, and overlay that textual information over the&#xA;image (a PDF “text layer”) so you can search and copy-paste the scanned&#xA;document to your heart’s content. For example, search for “asymmetry” in&#xA;this scanned page &lt;a href=../img/ocr/james.pdf&gt;before&lt;/a&gt; and &lt;a href=../img/ocr/james-ocr.pdf&gt;after OCR&lt;/a&gt;.&lt;p&gt;Adobe would love to charge you for OCR (built into Acrobat), but the&#xA;best-in-class OCR engine — Google’s Tesseract, initially developed by HP&#xA;in the 1980s — is free and open-source.&lt;p&gt;The issue is the interface: rather than being an end-user program&#xA;like Acrobat, Tesseract is a tool for building an end-user program. My&#xA;favorite program, &lt;a href=https://ocrmypdf.readthedocs.io/en/latest/&gt;OCRmyPDF&lt;/a&gt;, is a&#xA;little better: it’s designed for end-users, but only end-users&#xA;comfortable working at the command line.&lt;p&gt;Rather than navigating folders and running OCRmyPDF from Terminal,&#xA;you can use Apple’s built-in &lt;a href=https://support.apple.com/guide/shortcuts-mac/welcome/mac&gt;Shortcuts&lt;/a&gt;&#xA;(available with macOS 12 Monterey and later) to run OCR by&#xA;right-clicking on a PDF in Finder.&lt;h1 id=install-ocrmypdf&gt;Install OCRmyPDF&lt;/h1&gt;&lt;p&gt;Unfortunately, this setup is a two-step process. Unlike an app you&#xA;can download form the App Store or an installable &lt;code&gt;*.pkg&lt;/code&gt;&#xA;file you download and install from the internet, OCRmyPDF is distributed&#xA;via a common macOS package manager, &lt;a href=https://en.wikipedia.org/wiki/Homebrew_(package_manager)&gt;Homebrew&lt;/a&gt;.&#xA;First you install Homebrew, then you use Homebrew to install&#xA;OCRmyPDF.&lt;p&gt;You can install both from the command line, the most ancient way to&#xA;interact with the machine you know and love. You might be used to&#xA;mouse-navigated apps with buttons and menus; at the command line there’s&#xA;nothing but text.&lt;p&gt;You type instructions (commands) and press &lt;kbd&gt;enter&lt;/kbd&gt; to run&#xA;them; as long as they’re cogent, the computer will obey! Below I’ve&#xA;included the specific commands to run this way.&lt;ol type=1&gt;&lt;li&gt;&lt;p&gt;Open the application &lt;strong&gt;Terminal&lt;/strong&gt;. You can search&#xA;for it in Finder or find it at Applications → Utilities →&#xA;Terminal.&lt;li&gt;&lt;p&gt;Install &lt;strong&gt;Homebrew&lt;/strong&gt; if you don’t have it.&lt;p&gt;To check if you already have Homebrew installed, type&#xA;&lt;code&gt;which brew&lt;/code&gt; into Terminal and press &lt;kbd&gt;enter&lt;/kbd&gt;. If you&#xA;see an output like &lt;code&gt;/opt/homebrew/bin/brew&lt;/code&gt;, you have&#xA;Homebrew installed and you can proceed to step 3.&lt;p&gt;If you see an output like &lt;code&gt;brew not found&lt;/code&gt;, install&#xA;Homebrew by copying the “Install Homebrew” command listed on &lt;a href=https://brew.sh/&gt;brew.sh&lt;/a&gt; into your Terminal and pressing&#xA;&lt;kbd&gt;enter&lt;/kbd&gt;. As that installation process runs, it may prompt you&#xA;for additional input or approval. You may also need to input your&#xA;computer password.&lt;p&gt;For a more detailed tutorial on installing Homebrew, see&#xA;DigitalOcean’s &lt;a href=https://www.digitalocean.com/community/tutorials/how-to-install-and-use-homebrew-on-macos&gt;‘How&#xA;To Install and Use Homebrew on macOS.’&lt;/a&gt;&lt;p&gt;Once Homebrew is finished installing, confirm the installation: type&#xA;&lt;code&gt;brew --version&lt;/code&gt; into Terminal and press&#xA;&lt;kbd&gt;enter&lt;/kbd&gt;.&lt;li&gt;&lt;p&gt;Install &lt;strong&gt;OCRmyPDF&lt;/strong&gt;: type&#xA;&lt;code&gt;brew install ocrmypdf&lt;/code&gt; into Terminal and press&#xA;&lt;kbd&gt;enter&lt;/kbd&gt;.&lt;/ol&gt;&lt;p&gt;It’s worth briefly touching on how to use OCRmyPDF from the Terminal.&#xA;At any given time, the Terminal is operating in a certain folder on your&#xA;computer — you can use the command &lt;code&gt;pwd&lt;/code&gt; to see what folder&#xA;you’re in, the command &lt;code&gt;ls&lt;/code&gt; to list files and child folders&#xA;from your current position, and the command &lt;code&gt;cd&lt;/code&gt; to move into&#xA;another folder.&lt;p&gt;When you’re in a folder with a PDF — let’s call it ‘my-scan.pdf’ —&#xA;running &lt;code&gt;ocrmypdf my-scan.pdf my-scan_ocr.pdf&lt;/code&gt; creates a new&#xA;PDF (my-scan_ocr.pdf) with the same pages as my-scan.pdf, but with the&#xA;searchable and copyable PDF text layer added.&lt;h1 id=add-a-shortcut&gt;Add a Shortcut&lt;/h1&gt;&lt;ol type=1&gt;&lt;li&gt;&lt;p&gt;Install the “OCR PDF” Shortcut using &lt;a href=https://www.icloud.com/shortcuts/5c00359f4ef64bedb5a554581412cc1d&gt;this&#xA;iCloud sharing link&lt;/a&gt;.&lt;a href=#fn1 class=footnote-ref id=fnref1 role=doc-noteref&gt;&lt;sup&gt;1&lt;/sup&gt;&lt;/a&gt;&lt;li&gt;&lt;p&gt;The last action, “Run Shell Script,” may come with a warning&#xA;message: “This action cannot be run because Scripting actions are&#xA;disabled.” Hit the ‘Open Preferences’ button, then ‘Allow Running&#xA;Scripts.’&lt;figure&gt;&lt;img src=../img/ocr/shortcut-initial.png alt=&#34;The “Run Shell Script” action is disabled by default. Click the “Open Preferences” button.&#34;&gt;&lt;figcaption aria-hidden=true&gt;The “Run Shell Script” action is disabled&#xA;by default. Click the “Open Preferences” button.&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure&gt;&lt;img src=../img/ocr/shortcut-prefs.png alt=&#34;In the Preferences pane, check “Allow Running Scripts” to allow the shortcut to run OCRmyPDF.&#34;&gt;&lt;figcaption aria-hidden=true&gt;In the Preferences pane, check “Allow&#xA;Running Scripts” to allow the shortcut to run OCRmyPDF.&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;Diving into advanced settings to toggle off a security-minded default&#xA;might reasonably make you nervous: shell scripts are powerful tools, so&#xA;in theory ‘Allow Running Scripts’ could let a malicious shortcut&#xA;manipulate your computer. The good news is this script is&#xA;&lt;em&gt;short&lt;/em&gt;: it &lt;em&gt;only&lt;/em&gt; runs OCRmyPDf, which is&#xA;trustworthy.&lt;li&gt;&lt;p&gt;Use the button in the upper-right-hand corner of the Shortcuts&#xA;window to navigate to the ‘Shortcut Details.’ Check ‘Use as Quick&#xA;Action’ and ‘Finder’ to add the “OCR PDF” action to your right-click&#xA;menu.&lt;figure&gt;&lt;img src=../img/ocr/shortcut-final.png alt=&#34;‘Use as Quick Action’ and its sub-option ‘Finder’ are checked in the right-hand panel.&#34;&gt;&lt;figcaption aria-hidden=true&gt;‘Use as Quick Action’ and its sub-option&#xA;‘Finder’ are checked in the right-hand panel.&lt;/figcaption&gt;&lt;/figure&gt;&lt;/ol&gt;&lt;p&gt;Now you can run OCRmyPDF from Shortcuts! Use the ‘Play’ button in the&#xA;upper-right of this window (it’ll prompt you to select a PDF)&#xA;&lt;em&gt;or&lt;/em&gt; right-click on a PDF and use Quick Actions → OCR PDF.&lt;figure&gt;&lt;img src=../img/ocr/shortcut-option.png alt=&#34;The final product: make a PDF searchable by right clicking on it.&#34;&gt;&lt;figcaption aria-hidden=true&gt;The final product: make a PDF searchable&#xA;by right clicking on it.&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;While your PDF is being processed, you’ll see a Shortcuts status&#xA;indicator in the menu bar at the top of your screen. When it’s finished,&#xA;look for a freshly OCR’d PDF in the same folder as the one you selected&#xA;(if you right-clicked on ‘my-scan.pdf’, expect ‘my-scan_ocr.pdf’). This&#xA;can take a few minutes for large files.&lt;p&gt;In retrospect, I wish I’d used OCR much more in college, when my&#xA;course readings were often chapters scanned from university library&#xA;books. I imagine it’s also a useful tool for working with big and&#xA;relatively low-tech corpuses, like public records. Sure, a university&#xA;student or a public records professional might have an institutional&#xA;Adobe license, but we should expect more!&lt;p&gt;This technology has been in development since the mid-80s, and free&#xA;for almost 20 years… &lt;em&gt;if&lt;/em&gt; you’re the right kind of computer-user,&#xA;comfortable writing your own scripts and troubleshooting at the command&#xA;line. A consumer low-code app like Shortcuts, by providing a generic&#xA;graphical interface for shell scripts, extends the same options to a&#xA;wider range of users.&lt;aside id=footnotes class=&#34;footnotes footnotes-end-of-document&#34; role=doc-endnotes&gt;&lt;hr&gt;&lt;ol&gt;&lt;li id=fn1&gt;&lt;p&gt;This is a shared shortcut I found &lt;a href=https://www.reddit.com/r/shortcuts/comments/t15tym/ocr_text_detection_for_any_pdf_on_macos_needs/&gt;on&#xA;Reddit&lt;/a&gt;; props to u/epic_lurk_time for building it.&lt;a href=#fnref1 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;/ol&gt;&lt;/aside&gt;</content>
    <link href="https://lukasschwab.me/blog/gen/ocr.html" rel="alternate"></link>
    <summary type="html">A PDF you can highlight and search is better than a PDF you can&#39;t. Open source command-line interfaces like OCRmyPDF let you automatically introduce PDF text layers, but they&#39;re uncomfortable interfaces for many of the users who could use OCR day to day. Here are instructions for setting up a Shortcut (on macOS 12 or later) to OCR PDFs by right-clicking them in Finder.</summary>
  </entry>
  <entry>
    <title>Comparing Vertex Cover Algorithms</title>
    <updated>2022-10-30T00:00:00Z</updated>
    <id>https://lukasschwab.me/blog/gen/comparing-vertex-cover-algorithms.html</id>
    <content type="html">&lt;html xmlns=http://www.w3.org/1999/xhtml lang xml:lang&gt;&lt;meta charset=utf-8&gt;&lt;meta name=generator content=&#34;pandoc&#34;&gt;&lt;meta name=viewport content=&#34;width=device-width,initial-scale=1,user-scalable=yes&#34;&gt;&lt;meta name=dcterms.date content=&#34;2022-10-30&#34;&gt;&lt;title&gt;Comparing Vertex Cover Algorithms&lt;/title&gt;&lt;style&gt;&#xA;      code{white-space: pre-wrap;}&#xA;      span.smallcaps{font-variant: small-caps;}&#xA;      span.underline{text-decoration: underline;}&#xA;      div.column{display: inline-block; vertical-align: top; width: 50%;}&#xA;          &lt;/style&gt;&lt;link rel=stylesheet href=../styles/common.css&gt;&lt;link rel=stylesheet href=../styles/fence-extensions/addendum.css&gt;&lt;link rel=stylesheet href=../styles/fence-extensions/dirtree.css&gt;&lt;link rel=stylesheet href=../styles/fence-extensions/firstcoltable.css&gt;&lt;link rel=stylesheet href=../styles/fence-extensions/frame.css&gt;&lt;link rel=stylesheet href=../styles/fence-extensions/fullwidth.css&gt;&lt;link rel=stylesheet href=../styles/fence-extensions/widetable.css&gt;&lt;script data-goatcounter=https://lukasschwab.goatcounter.com/count async src=//gc.zgo.at/count.js&gt;&lt;/script&gt;&lt;a href=../index.html&gt;&amp;#171; blog&lt;/a&gt; &lt;span class=date&gt;2022-10-30&lt;/span&gt;&lt;header id=title-block-header&gt;&lt;h1 class=title&gt;Comparing Vertex Cover Algorithms&lt;/h1&gt;&lt;/header&gt;&lt;nav id=TOC role=doc-toc&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=#experiment-space id=toc-experiment-space&gt;Experiment&#xA;space&lt;/a&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=#vertex-cover-algorithms id=toc-vertex-cover-algorithms&gt;Vertex cover algorithms&lt;/a&gt;&lt;li&gt;&lt;a href=#topologies id=toc-topologies&gt;Topologies&lt;/a&gt;&lt;li&gt;&lt;a href=#weight-schemes id=toc-weight-schemes&gt;Weight&#xA;schemes&lt;/a&gt;&lt;/ul&gt;&lt;li&gt;&lt;a href=#project-structure id=toc-project-structure&gt;Project&#xA;structure&lt;/a&gt;&lt;li&gt;&lt;a href=#simulations id=toc-simulations&gt;Simulations&lt;/a&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=#lavrov-overview id=toc-lavrov-overview&gt;&lt;code&gt;lavrov&lt;/code&gt; overview&lt;/a&gt;&lt;li&gt;&lt;a href=#clever-overview id=toc-clever-overview&gt;&lt;code&gt;clever&lt;/code&gt; overview&lt;/a&gt;&lt;/ul&gt;&lt;/ul&gt;&lt;/nav&gt;&lt;p&gt;In &lt;a href=./graphs-at-work.html&gt;“Graphs at Work”&lt;/a&gt; I claim&lt;blockquote&gt;&lt;p&gt;To improve the customer experience, what you need to understand is&#xA;the distribution of strategy performances for a given algorithm on&#xA;&lt;em&gt;real-world graphs,&lt;/em&gt; not the theoretical worst case.&lt;/blockquote&gt;&lt;p&gt;Of course, that’s a simplification. Theoretical worst-case&#xA;performance &lt;em&gt;does&lt;/em&gt; impact the customer experience, either on&#xA;accident or because some adversary wants to throttle your program. Worry&#xA;less if your “customer concerns are well-isolated,” but they rarely&#xA;are.&lt;/p&gt;&lt;p&gt;By “real-world graphs” I suggested a family of associations graphs,&#xA;distinct from the family of all possible associations graphs, might&#xA;describe the bulk of customer desires. For example, if the typical&#xA;customer associates Contacts, Deals, and other objects via a mutual&#xA;association with a Company, those customers’ “real-world graphs” will&#xA;approximate &lt;a href=https://en.wikipedia.org/wiki/Star_(graph_theory)&gt;stars&lt;/a&gt; with&#xA;the Company at the center.&lt;p&gt;Parameterizing those graph families, you can simulate the candidate&#xA;algorithms’ performance against a wide range of possible customer&#xA;configurations (even configurations impossible in practice, e.g. because&#xA;of a limit on the number of HubSpot types).&lt;h1 id=experiment-space&gt;Experiment space&lt;/h1&gt;&lt;p&gt;All the uncertainty about what’s “real-world” translates into a huge&#xA;experimental space. Any given experiment embeds a set of assumptions&#xA;about what the user probably wants and what they have in HubSpot. All&#xA;the experiments taken together are only useful if they describe the&#xA;problem well enough, in all its dimensions, to react to user information&#xA;as it comes in.&lt;p&gt;Most of the variables here are discrete categories — different vertex&#xA;cover algorithms, different topological families of graphs, and&#xA;different weighting schemes for vertices. For these purposes, there&#xA;isn’t a continuous space between Vazirani’s algorithm and Lavrov’s.&lt;p&gt;Some of those categories introduce scalar variables, like the size of&#xA;a random graph or the probability two vertices are adjacent.&lt;h2 id=vertex-cover-algorithms&gt;Vertex cover algorithms&lt;/h2&gt;&lt;p&gt;An unweighted vertex cover algorithm will &lt;em&gt;never&lt;/em&gt; &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;k&lt;/em&gt;&lt;/span&gt;-approximate a minimal weighted&#xA;vertex cover. Unweighted algorithms behave as if all vertices have the&#xA;same weight, then minimize the number of vertices in the cover &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;V&lt;/em&gt;&lt;sub&gt;req&lt;/sub&gt;&lt;/span&gt;. When you add&#xA;weights to &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;G&lt;/em&gt; = {&lt;em&gt;V&lt;/em&gt;, &lt;em&gt;E&lt;/em&gt;}&lt;/span&gt;, &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;V&lt;/em&gt;&lt;sub&gt;req&lt;/sub&gt;&lt;/span&gt; might be arbitrarily&#xA;bad: each vertex in &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;V&lt;/em&gt;&lt;sub&gt;req&lt;/sub&gt;&lt;/span&gt; can be arbitrarily&#xA;expensive, and vertices in &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;V&lt;/em&gt; − &lt;em&gt;V&lt;/em&gt;&lt;sub&gt;req&lt;/sub&gt;&lt;/span&gt; can be&#xA;arbitrarily cheap.&lt;p&gt;Obviously the &lt;em&gt;best&lt;/em&gt; an unweighted algorithm can perform on a&#xA;weighted graph is to accidentally suggest the weighted optimal&#xA;strategy.&lt;p&gt;How should we expect it to perform given a random distribution of&#xA;weights? I think of a single graph with its topology-determined minimum&#xA;unweighted vertex cover &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;V&lt;/em&gt;&lt;sub&gt;req&lt;/sub&gt;&lt;/span&gt;. The expected weight&#xA;of that vertex cover is &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;μ&lt;/em&gt;∥&lt;em&gt;V&lt;/em&gt;&lt;sub&gt;req&lt;/sub&gt;∥&lt;/span&gt; where&#xA;&lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;μ&lt;/em&gt;&lt;/span&gt; is the mean of the weight&#xA;distribution.&lt;p&gt;What if weights aren’t distributed randomly, and instead correlate&#xA;with graph topology? I’m not sure what to expect. That question is&#xA;easier to simulate than to theorize.&lt;p&gt;I implement three of the algorithms discussed in my last post.&lt;dl&gt;&lt;dt&gt;&lt;strong&gt;&lt;code&gt;clever&lt;/code&gt;&lt;/strong&gt;&lt;dd&gt;&lt;p&gt;The greedy algorithm I &lt;a href=http://lukasschwab.me/blog/gen/graphs-at-work.html#fn3&gt;previously&#xA;implemented in Elixir&lt;/a&gt;. I came up with it independently, but I take&#xA;the name from Michel Zito.&lt;a href=#fn1 class=footnote-ref id=fnref1 role=doc-noteref&gt;&lt;sup&gt;1&lt;/sup&gt;&lt;/a&gt;&lt;p&gt;The punchline: this algorithm &lt;em&gt;isn’t&lt;/em&gt; clever. It performs so&#xA;badly on &lt;code&gt;tricky&lt;/code&gt; graphs it doesn’t &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;k&lt;/em&gt;&lt;/span&gt;-approximate minimal vertex covers.&#xA;To this algorithm’s credit, it has an intuitive heuristic: add the&#xA;highest-degree vertex in &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;G&lt;/em&gt;&lt;/span&gt; to&#xA;&lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;V&lt;/em&gt;&lt;sub&gt;req&lt;/sub&gt;&lt;/span&gt;, remove it&#xA;from &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;G&lt;/em&gt;&lt;/span&gt;, and recurse for a&#xA;minimal vertex cover on what remains.&lt;/dl&gt;&lt;dl&gt;&lt;dt&gt;&lt;strong&gt;&lt;code&gt;lavrov&lt;/code&gt;&lt;/strong&gt;&lt;dd&gt;&lt;p&gt;the “2-approximating [unweighted] vertex cover” variation suggested&#xA;by Mikhail Lavrov:&lt;blockquote&gt;&lt;p&gt;Start with &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;V&lt;/em&gt;&lt;sub&gt;req&lt;/sub&gt; = ∅&lt;/span&gt;. Then, as long&#xA;as &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;V&lt;/em&gt;&lt;sub&gt;req&lt;/sub&gt;&lt;/span&gt; doesn’t&#xA;cover every edge, we pick an uncovered edge and add &lt;em&gt;both&lt;/em&gt;&#xA;endpoints to &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;V&lt;/em&gt;&lt;sub&gt;req&lt;/sub&gt;&lt;/span&gt;.&lt;a href=#fn2 class=footnote-ref id=fnref2 role=doc-noteref&gt;&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt;&lt;/blockquote&gt;&lt;p&gt;My implementation is an even greedier variant, at the cost of some&#xA;runtime complexity: rather than arbitrarily picking an uncovered edge,&#xA;it picks the uncovered edge with the highest combined degree between its&#xA;incident vertices. Lavrov proves the 2-approximation.&lt;/dl&gt;&lt;dl&gt;&lt;dt&gt;&lt;strong&gt;&lt;code&gt;vazirani&lt;/code&gt;&lt;/strong&gt;&lt;dd&gt;The only &lt;em&gt;weighted&lt;/em&gt; vertex cover algorithm I implement. Rather&#xA;than removing vertices outright, it relaxes virtual weights &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;t&lt;/em&gt;(&lt;em&gt;v&lt;/em&gt;)&lt;/span&gt; for the vertices&#xA;incident on a “taken” (uncovered) edge, then adds one or both to &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;V&lt;/em&gt;&lt;sub&gt;req&lt;/sub&gt;&lt;/span&gt; depending on the&#xA;result. My implementation leaves edge-selection arbitrary, per&#xA;Vazirani’s definition.&lt;a href=#fn3 class=footnote-ref id=fnref3 role=doc-noteref&gt;&lt;sup&gt;3&lt;/sup&gt;&lt;/a&gt;&lt;/dl&gt;&lt;p&gt;As the only &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;k&lt;/em&gt;&lt;/span&gt;-approximation&#xA;of a minimum &lt;em&gt;weighted&lt;/em&gt; vertex cover, it’s safe to assume&#xA;&lt;code&gt;vazirani&lt;/code&gt; should perform the most &lt;em&gt;stably.&lt;/em&gt; That&#xA;doens’t moot the experiment. As I noted, &lt;a href=http://lukasschwab.me/blog/gen/graphs-at-work.html#fn8&gt;there are&#xA;graphs&lt;/a&gt; where it yields a worse result than &lt;code&gt;clever&lt;/code&gt;!&lt;h2 id=topologies&gt;Topologies&lt;/h2&gt;&lt;p&gt;Well, what’s a “real-world graph?” In the absence of customer data —&#xA;I don’t have any — we can try simulating &lt;em&gt;several&lt;/em&gt; parameterized&#xA;families of graphs instead of just guessing. I implement two&#xA;families.&lt;dl&gt;&lt;dt&gt;&lt;strong&gt;&lt;code&gt;random&lt;/code&gt;&lt;/strong&gt;&lt;dd&gt;A graph with &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;n&lt;/em&gt;&lt;/span&gt; vertices. Any&#xA;pair &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;v&lt;/em&gt;&lt;sub&gt;1&lt;/sub&gt;, &lt;em&gt;v&lt;/em&gt;&lt;sub&gt;2&lt;/sub&gt; ∈ &lt;em&gt;V&lt;/em&gt;&lt;/span&gt;&#xA;are connected with probability &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;p&lt;/em&gt;&lt;/span&gt;. Notably, these graphs have a&#xA;nondeterministic total connectivity — there’s always a &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;p&lt;/em&gt;&lt;sup&gt;2&lt;sup&gt;&lt;em&gt;n&lt;/em&gt;&lt;/sup&gt;&lt;/sup&gt;&lt;/span&gt;&#xA;chance of getting a complete graph! — but that’s just a reason to&#xA;simulate repeatedly.&lt;dt&gt;&lt;strong&gt;&lt;code&gt;tricky&lt;/code&gt;&lt;/strong&gt;&lt;dd&gt;&lt;p&gt;“Tricky” graphs follow a construction from Lavrov designed to force&#xA;the &lt;code&gt;clever&lt;/code&gt; algorithm into arbitrarily bad vertex covers.&#xA;Instead of taking a number of vertices, the construction starts with&#xA;integers &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;a&lt;/em&gt;&lt;/span&gt; and &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;k&lt;/em&gt; ≤ &lt;em&gt;a&lt;/em&gt;&lt;/span&gt;; then&lt;blockquote&gt;&lt;p&gt;On one side of the graph, put &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;n&lt;/em&gt;&lt;/span&gt; vertices, for some (large) &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;n&lt;/em&gt;&lt;/span&gt;. Call the set of these vertices&#xA;&lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;A&lt;/em&gt;&lt;/span&gt;.&lt;p&gt;On the other side of the graph, put &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;k&lt;/em&gt; − 1&lt;/span&gt; “blocks” of vertices, numbered&#xA;&lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;B&lt;/em&gt;&lt;sub&gt;2&lt;/sub&gt;, &lt;em&gt;B&lt;/em&gt;&lt;sub&gt;3&lt;/sub&gt;, ..., &lt;em&gt;B&lt;/em&gt;&lt;sub&gt;&lt;em&gt;k&lt;/em&gt;&lt;/sub&gt;&lt;/span&gt;.&#xA;Block &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;B&lt;/em&gt;&lt;sub&gt;&lt;em&gt;i&lt;/em&gt;&lt;/sub&gt;&lt;/span&gt;&#xA;will have &lt;span class=&#34;math inline&#34;&gt;⌊&lt;em&gt;n&lt;/em&gt;/&lt;em&gt;i&lt;/em&gt;⌋&lt;/span&gt;&#xA;vertices.&lt;p&gt;Each vertex in &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;A&lt;/em&gt;&lt;/span&gt; gets one&#xA;edge to a vertex in &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;B&lt;/em&gt;&lt;sub&gt;&lt;em&gt;i&lt;/em&gt;&lt;/sub&gt;&lt;/span&gt;, and they are&#xA;distributed as evenly as possible: each vertex in &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;B&lt;/em&gt;&lt;sub&gt;&lt;em&gt;i&lt;/em&gt;&lt;/sub&gt;&lt;/span&gt; ends up with&#xA;either &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;i&lt;/em&gt;&lt;/span&gt; or &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;i&lt;/em&gt; + 1&lt;/span&gt; neighbors in &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;A&lt;/em&gt;&lt;/span&gt;.&lt;a href=#fn4 class=footnote-ref id=fnref4 role=doc-noteref&gt;&lt;sup&gt;4&lt;/sup&gt;&lt;/a&gt;&lt;/blockquote&gt;&lt;figure&gt;&lt;img src=../img/comparing-vertex-cover-algorithms/lavrov-tricky.jpg alt=&#34;From Lavrov (2020): a “tricky” graph with n = 20 and k = 5. The edges adjacent on B_3 illustrate each B-group is adjacent to all of A.&#34;&gt;&lt;figcaption aria-hidden=true&gt;From Lavrov (2020): a “tricky” graph with&#xA;&lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;n&lt;/em&gt; = 20&lt;/span&gt; and &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;k&lt;/em&gt; = 5&lt;/span&gt;. The edges adjacent on &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;B&lt;/em&gt;&lt;sub&gt;3&lt;/sub&gt;&lt;/span&gt; illustrate each &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;B&lt;/em&gt;&lt;/span&gt;-group is adjacent to all of &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;A&lt;/em&gt;&lt;/span&gt;.&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;What makes it tricky? Initially the highest-degree vertices are in&#xA;&lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;B&lt;/em&gt;&lt;sub&gt;&lt;em&gt;k&lt;/em&gt;&lt;/sub&gt;&lt;/span&gt;. When&#xA;&lt;code&gt;clever&lt;/code&gt; has included those in the cover &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;V&lt;/em&gt;&lt;sub&gt;req&lt;/sub&gt;&lt;/span&gt;, the highest-degree&#xA;vertices are in &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;B&lt;/em&gt;&lt;sub&gt;&lt;em&gt;k&lt;/em&gt; − 1&lt;/sub&gt;&lt;/span&gt;. This&#xA;continues until &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;V&lt;/em&gt;&lt;sub&gt;req&lt;/sub&gt; = &lt;em&gt;B&lt;/em&gt;&lt;sub&gt;2&lt;/sub&gt; ∪ &lt;em&gt;B&lt;/em&gt;&lt;sub&gt;3&lt;/sub&gt; ∪ ... ∪ &lt;em&gt;B&lt;/em&gt;&lt;sub&gt;&lt;em&gt;k&lt;/em&gt;&lt;/sub&gt;&lt;/span&gt;,&#xA;even though there’s a smaller vertex cover staring right at us: &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;A&lt;/em&gt;&lt;/span&gt;!&lt;p&gt;These graphs have &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;a&lt;/em&gt; ⋅ &lt;em&gt;H&lt;/em&gt;&lt;sub&gt;&lt;em&gt;k&lt;/em&gt;&lt;/sub&gt;&lt;/span&gt;&#xA;vertices, where &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;H&lt;/em&gt;&lt;sub&gt;&lt;em&gt;k&lt;/em&gt;&lt;/sub&gt;&lt;/span&gt; is the &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;k&lt;/em&gt;&lt;/span&gt;th &lt;a href=https://en.wikipedia.org/wiki/Harmonic_number&gt;harmonic&#xA;number&lt;/a&gt;: &lt;span class=&#34;math inline&#34;&gt;∥&lt;em&gt;A&lt;/em&gt;∥ = &lt;em&gt;a&lt;/em&gt;&lt;/span&gt;&#xA;and &lt;span class=&#34;math inline&#34;&gt;∥&lt;em&gt;B&lt;/em&gt;&lt;sub&gt;&lt;em&gt;i&lt;/em&gt;&lt;/sub&gt;∥ = (&lt;em&gt;i&lt;/em&gt;&lt;sup&gt;−1&lt;/sup&gt;)&lt;em&gt;a&lt;/em&gt;&lt;/span&gt;.&lt;/dl&gt;&lt;p&gt;There are potentially many more families to explore. Like I mention&#xA;above, stars might capture HubSpot’s ontology for built-in types well:&#xA;it’s a CRM, so pretty much &lt;em&gt;everything&lt;/em&gt; associates, directly or&#xA;indirectly, with a Company. Graphs composed of stars, joined at the&#xA;leaves, might reflect HubSpot customers using custom objects to hang&#xA;subontologies off of the built-in types: a Company’s association with a&#xA;Deal indirectly associates it with a collection of Deal-specific custom&#xA;object types.&lt;/p&gt;&lt;p&gt;I considered testing a &lt;a href=https://en.wikipedia.org/wiki/Small-world_network&gt;“small-world”&#xA;family&lt;/a&gt; of graphs, with vertices highly-adjacent to “neighbors” and&#xA;adjacent to non-“neighbors” with some lower probability, but I’m not&#xA;sure it’s relevant to the HubSpot problem definition. There’s no natural&#xA;concept of “neghbors” among HubSpot object types. Remember: customer&#xA;“configurations” are subgraphs of a &lt;em&gt;complete&lt;/em&gt; graph of types.&#xA;Emergent graphs — especially physical networks — may well have&#xA;small-world properties, but it’s less clear why a &lt;em&gt;subtractive&lt;/em&gt;&#xA;configuration-selection process would yield them.&lt;h2 id=weight-schemes&gt;Weight schemes&lt;/h2&gt;&lt;p&gt;The cost of requesting the objects of a HubSpot type is the number of&#xA;members divided by the APIs request page size. How many objects of each&#xA;type exist? Who knows! Like with graph families, it’s easier to simulate&#xA;&lt;em&gt;several&lt;/em&gt; of them than to evidence-free agonize over which is&#xA;“realest.”&lt;dl&gt;&lt;dt&gt;&lt;strong&gt;&lt;code&gt;uniform&lt;/code&gt;&lt;/strong&gt;&lt;dd&gt;Assigns every vertex the same weight, unweighting the vertex cover&#xA;problem.&lt;dt&gt;&lt;strong&gt;&lt;code&gt;random&lt;/code&gt;&lt;/strong&gt;&lt;dd&gt;Assigns each vertex a random weight on the half-open interval &lt;span class=&#34;math inline&#34;&gt;[0, 1)&lt;/span&gt;.&lt;dt&gt;&lt;strong&gt;&lt;code&gt;degree-negative&lt;/code&gt;&lt;/strong&gt;&lt;dd&gt;A weight scheme where a vertex’s weight is negatively correlated with&#xA;its degree. To keep weights positive, I use &lt;span class=&#34;math inline&#34;&gt;(&lt;em&gt;d&lt;/em&gt;(&lt;em&gt;v&lt;/em&gt;)+&lt;em&gt;ϵ&lt;/em&gt;)&lt;sup&gt;−1&lt;/sup&gt;&lt;/span&gt;,&#xA;where &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;ϵ&lt;/em&gt;&lt;/span&gt; is a small positive&#xA;constant to prevent divison-by-zero panics on disconnected vertices.&lt;dt&gt;&lt;strong&gt;&lt;code&gt;degree-positive&lt;/code&gt;&lt;/strong&gt;&lt;dd&gt;Each vertex’s weight is its degree.&lt;dt&gt;&lt;strong&gt;&lt;code&gt;degree-positive-superlinear&lt;/code&gt;&lt;/strong&gt;&lt;dd&gt;Each vertex’s weight is the square of its degree.&lt;/dl&gt;&lt;p&gt;I expect &lt;code&gt;degree-negative&lt;/code&gt; and&#xA;&lt;code&gt;degree-positive&lt;/code&gt; to reward and punish the&#xA;&lt;code&gt;clever&lt;/code&gt; algorithm, respectively. &lt;code&gt;clever&lt;/code&gt; gobbles&#xA;high-degree vertices into &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;V&lt;/em&gt;&lt;sub&gt;req&lt;/sub&gt;&lt;/span&gt; oblivious to their&#xA;weight. In one case that greedy strategy correlates with low weights, in&#xA;the other case with high.&lt;h1 id=project-structure&gt;Project structure&lt;/h1&gt;&lt;p&gt;Why reach for Go? My first instict was to use &lt;a href=https://pkg.go.dev/testing#hdr-Benchmarks&gt;built-in&#xA;benchmarking&lt;/a&gt; to compare algorithms. That &lt;em&gt;is&lt;/em&gt; a cool tool&#xA;(one of my favorite take-home interview hacks) but I don’t wind up using&#xA;it. Runtime performance is just too dependent on implementation details;&#xA;I’m better off spending my time building a &lt;code&gt;graph&lt;/code&gt; package&#xA;with a decent interface than, say, using separate graph implementations&#xA;optimized for each algorithm.&lt;p&gt;There are other, better reasons to write Go. Static types prevent a&#xA;suite of bugs I’d encounter in Python — Go’s a “when it compiles, it&#xA;does what you mean” experience — and module management is simpler than&#xA;in Typescript. The standard &lt;code&gt;testing&lt;/code&gt; library is pleasant.&#xA;&lt;code&gt;godoc&lt;/code&gt; is built in, and linters abound.&lt;p&gt;The stand-out advantage, I found, is a convention: the &lt;a href=https://github.com/golang-standards/project-layout&gt;standard Go&#xA;project layout&lt;/a&gt;, which separates reusable library code in&#xA;&lt;code&gt;pkg&lt;/code&gt; subdirectories from applications in &lt;code&gt;cmd&lt;/code&gt;,&#xA;is well-suited to experimentation. I wound up with a project structure&#xA;like this:&lt;div class=dirtree data-root=vertex-cover&gt;&lt;ul&gt;&lt;li&gt;&lt;span class=file&gt;README.md&lt;/span&gt;&lt;li&gt;&lt;span class=directory&gt;pkg&lt;/span&gt;&lt;ul&gt;&lt;li&gt;&lt;span class=directory&gt;graph&lt;/span&gt;&lt;li&gt;&lt;span class=directory&gt;cover&lt;/span&gt;&lt;/ul&gt;&lt;li&gt;&lt;span class=directory&gt;cmd&lt;/span&gt;&lt;ul&gt;&lt;li&gt;&lt;span class=directory&gt;01-lavrov-clever&lt;/span&gt;&lt;ul&gt;&lt;li&gt;&lt;span class=file&gt;README.md&lt;/span&gt;&lt;li&gt;&lt;span class=file&gt;main.go&lt;/span&gt;&lt;/ul&gt;&lt;li&gt;&lt;span class=directory&gt;02-clever-vazirani&lt;/span&gt;&lt;li&gt;&lt;span class=directory&gt;03-another-experiment&lt;/span&gt;&lt;/ul&gt;&lt;/ul&gt;&lt;/div&gt;&lt;p&gt;The simulation models depend on one another, but they’re independent&#xA;of the experiments that use them. &lt;code&gt;pkg/graph&lt;/code&gt; exports&#xA;weighted and unweighted graphs, along with some known graphs used in&#xA;tests. &lt;code&gt;pkg/cover&lt;/code&gt; implements vertex cover algorithms on&#xA;those graph types.&lt;p&gt;Both packages include tests. Testing is especially useful in&#xA;&lt;code&gt;pkg/cover&lt;/code&gt;, where it tests two kinds of correctness: that my&#xA;code behaves how I expect it to, &lt;em&gt;and&lt;/em&gt; that my expectations match&#xA;the algorithm definitions in literature! By testing on known graphs&#xA;(defined in &lt;code&gt;pkg/graph/fixtures.go&lt;/code&gt;), I check my&#xA;implementations match examples from papers.&#xA;&lt;code&gt;TestClever_Tricky&lt;/code&gt; confirms &lt;code&gt;clever&lt;/code&gt; selects&#xA;&lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;V&lt;/em&gt;&lt;sub&gt;req&lt;/sub&gt; = &lt;em&gt;B&lt;/em&gt;&lt;/span&gt;&#xA;like Lavrov claims; if it doesn’t, there’s some mistake in the&#xA;implementation &lt;em&gt;even if&lt;/em&gt; it yields valid vertex covers.&lt;p&gt;Experiments go in subdirectories of &lt;code&gt;cmd&lt;/code&gt;. Each directory&#xA;is space for notes, outputs, experiment-specific helper code, and so on.&#xA;Because there’s no risk of one experiment impacting the others — there&#xA;aren’t interdependencies in &lt;code&gt;cmd&lt;/code&gt; — these are&#xA;scratchpads.&lt;p&gt;In summary:&lt;ol type=1&gt;&lt;li&gt;Put reusable simulation components in &lt;code&gt;pkg&lt;/code&gt;.&lt;li&gt;Use unit tests to confirm your implementations against prior art&#xA;examples.&lt;li&gt;Explore in &lt;code&gt;cmd&lt;/code&gt;.&lt;/ol&gt;&lt;p&gt;Next time I might reach for a scientific computing package like &lt;a href=https://www.gonum.org/&gt;Gonum&lt;/a&gt; to aggregate data in&#xA;experiments. It even ships with a &lt;a href=https://pkg.go.dev/gonum.org/v1/gonum/graph&gt;&lt;code&gt;graph&lt;/code&gt;&#xA;package&lt;/a&gt;, albeit one suited for edge-weighted problems over&#xA;vertex-weighted ones.&lt;a href=#fn5 class=footnote-ref id=fnref5 role=doc-noteref&gt;&lt;sup&gt;5&lt;/sup&gt;&lt;/a&gt;&lt;h1 id=simulations&gt;Simulations&lt;/h1&gt;&lt;p&gt;Any pair of experiments should be comparable at a glance. Absolute&#xA;vertex cover weights are sensitive to graph topology and individual&#xA;vertex weights, which makes it hard to compare an algorithm’s&#xA;performance on a ten-vertex graph against another algorithm’s&#xA;performance on a hundred-vertex graph.&lt;p&gt;Instead, since my goal is to compare algorithms, I measure&#xA;&lt;em&gt;relative&lt;/em&gt; cover weight against a baseline:&#xA;&lt;code&gt;vazirani&lt;/code&gt;. Because it’s a 2-approximation, it guarantees a&#xA;bound on any algorithm’s relative performance: if it’s implemented&#xA;correctly, nothing can do more than 50% better than&#xA;&lt;code&gt;vazirani&lt;/code&gt;.&lt;p&gt;In the heatmaps below,&lt;ul&gt;&lt;li&gt;-50 (teal) denotes a vertex weight 50% better than&#xA;&lt;code&gt;vazirani&lt;/code&gt; on the same graph.&lt;li&gt;0 (yellow) denotes a vertex weight at parity with&#xA;&lt;code&gt;vazirani&lt;/code&gt;.&lt;li&gt;50 (red) denotes a vertex weight 50% worse than&#xA;&lt;code&gt;vazirani&lt;/code&gt;. Worse performances are the same deep red —&#xA;without a &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;k&lt;/em&gt;&lt;/span&gt;-approximation, an&#xA;algorithm can be worse up to the total weight of &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;V&lt;/em&gt;&lt;/span&gt;!&lt;/ul&gt;&lt;p&gt;Each cell averages ten relative performances: ten times, the&#xA;simulation generates a graph &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;G&lt;/em&gt;&lt;/span&gt;&#xA;with the given parameters, then finds vertex covers on that &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;G&lt;/em&gt;&lt;/span&gt; with the &lt;code&gt;vazirani&lt;/code&gt; and&#xA;tested algorithms.&lt;p&gt;For starters, we can visually confirm a behavior predicted in&#xA;literature: Lavrov’s &lt;code&gt;tricky&lt;/code&gt; graphs are designed to&#xA;arbitrarily damage the &lt;code&gt;clever&lt;/code&gt; algorithm’s performance.&lt;div class=fullwidth&gt;&lt;iframe src=../img/comparing-vertex-cover-algorithms/clever-uniform-tricky.html style=&#34;height: 525px;&#34;&gt;&lt;/iframe&gt;&lt;/div&gt;&lt;p&gt;As Lavrov suggests, the “clever” algorithm is tricked for large &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;a&lt;/em&gt;, &lt;em&gt;k&lt;/em&gt;&lt;/span&gt;. (Note: while &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;k&lt;/em&gt;&lt;/span&gt; is an integer in Lavrov’s&#xA;&lt;code&gt;tricky&lt;/code&gt; graph construction, here it’s expressed as a&#xA;multiple of the number of vertices in &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;A&lt;/em&gt;&lt;/span&gt;.) Does the &lt;code&gt;lavrov&lt;/code&gt;&#xA;algorithm do better?&lt;div class=fullwidth&gt;&lt;iframe src=../img/comparing-vertex-cover-algorithms/lavrov-uniform-tricky.html style=&#34;height: 525px;&#34;&gt;&lt;/iframe&gt;&lt;/div&gt;&lt;p&gt;Indeed it does! Strikingly, &lt;code&gt;lavrov&lt;/code&gt; performs &lt;em&gt;just as&#xA;well&lt;/em&gt; as &lt;code&gt;vazirani&lt;/code&gt; on a uniformly-weigted graph. Upon&#xA;reflection, I realize &lt;code&gt;vazirani&lt;/code&gt; and &lt;code&gt;lavrov&lt;/code&gt; are&#xA;equivalent in a uniformly-weighted context: &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;t&lt;/em&gt;(&lt;em&gt;v&lt;/em&gt;)&lt;/span&gt; always goes to zero&#xA;for &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;u&lt;/em&gt;&lt;/span&gt; &lt;em&gt;and&lt;/em&gt; &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;v&lt;/em&gt;&lt;/span&gt; at every step, because both have&#xA;the same starting weight!&lt;p&gt;To my surprise, &lt;code&gt;clever&lt;/code&gt; outperformed&#xA;&lt;code&gt;vazirani&lt;/code&gt; on tricky graphs when vertex weights corresponded&#xA;1:1 to degree.&lt;div class=fullwidth&gt;&lt;iframe src=../img/comparing-vertex-cover-algorithms/clever-degreePositive-tricky.html style=&#34;height: 525px;&#34;&gt;&lt;/iframe&gt;&lt;/div&gt;&lt;p&gt;On reflection, this makes sense too. Any vertex cover of a&#xA;&lt;code&gt;degree-positive&lt;/code&gt;-weighted graph covers every edge, which&#xA;means it has a minimum weight &lt;span class=&#34;math inline&#34;&gt;∥&lt;em&gt;E&lt;/em&gt;∥&lt;/span&gt;. By minimizing redundant&#xA;coverage, &lt;code&gt;clever&lt;/code&gt; approximates that optimal cover. Since&#xA;&lt;code&gt;vazirani&lt;/code&gt; is a 2-approximation, the optimal cover is at most&#xA;50% better.&lt;p&gt;What if vertex weights are &lt;em&gt;superlinearly&lt;/em&gt; positively&#xA;correlated with vertex degree?&lt;div class=fullwidth&gt;&lt;iframe src=../img/comparing-vertex-cover-algorithms/clever-degreePositiveSuperlinear-tricky.html style=&#34;height: 525px;&#34;&gt;&lt;/iframe&gt;&lt;/div&gt;&lt;p&gt;Huh! To my surprise, except for a low-&lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;n&lt;/em&gt;&lt;/span&gt; low-&lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;k&lt;/em&gt;&lt;/span&gt; blip, &lt;code&gt;clever&lt;/code&gt;&#xA;&lt;em&gt;still&lt;/em&gt; generally outperforms &lt;code&gt;vazirani&lt;/code&gt;. Even&#xA;weirder:&lt;div class=fullwidth&gt;&lt;iframe src=../img/comparing-vertex-cover-algorithms/clever-degreeNegative-tricky.html style=&#34;height: 525px;&#34;&gt;&lt;/iframe&gt;&lt;/div&gt;&lt;p&gt;I expected degree-negative weighting would reward the&#xA;&lt;code&gt;clever&lt;/code&gt; algorithm for picking high-degree vertices, but it&#xA;doesn’t!&lt;h2 id=lavrov-overview&gt;&lt;code&gt;lavrov&lt;/code&gt; overview&lt;/h2&gt;&lt;p&gt;A weight-naïve version of &lt;code&gt;vazirani&lt;/code&gt;, &lt;code&gt;lavrov&lt;/code&gt;&#xA;achieves parity when it isn’t punished for weight-naïveté (when weights&#xA;are &lt;code&gt;uniform&lt;/code&gt;). Otherwise, &lt;code&gt;vazirani&lt;/code&gt;’s&#xA;performance lower-bounds it; you’d only prefer &lt;code&gt;lavrov&lt;/code&gt; if&#xA;you need its programmatic simplicity.&lt;div class=&#34;firstcoltable widetable&#34;&gt;&lt;table&gt;&lt;col style=&#34;width: 12%&#34;&gt;&lt;col style=&#34;width: 43%&#34;&gt;&lt;col style=&#34;width: 43%&#34;&gt;&lt;thead&gt;&lt;tr class=header&gt;&lt;th&gt;&lt;strong&gt;Weight/Topology&lt;/strong&gt;&lt;th style=&#34;text-align: center;&#34;&gt;&lt;code&gt;random&lt;/code&gt;&lt;th style=&#34;text-align: center;&#34;&gt;&lt;code&gt;tricky&lt;/code&gt;&lt;tbody&gt;&lt;tr class=odd&gt;&lt;td&gt;&lt;code&gt;uniform&lt;/code&gt;&lt;td style=&#34;text-align: center;&#34;&gt;&lt;img src=../img/comparing-vertex-cover-algorithms/thumbs/lavrov-uniform-random.png&gt;&lt;td style=&#34;text-align: center;&#34;&gt;&lt;img src=../img/comparing-vertex-cover-algorithms/thumbs/lavrov-uniform-tricky.png&gt;&lt;tr class=even&gt;&lt;td&gt;&lt;code&gt;random&lt;/code&gt;&lt;td style=&#34;text-align: center;&#34;&gt;&lt;img src=../img/comparing-vertex-cover-algorithms/thumbs/lavrov-random-random.png&gt;&lt;td style=&#34;text-align: center;&#34;&gt;&lt;img src=../img/comparing-vertex-cover-algorithms/thumbs/lavrov-random-tricky.png&gt;&lt;tr class=odd&gt;&lt;td&gt;&lt;code&gt;degree-negative&lt;/code&gt;&lt;td style=&#34;text-align: center;&#34;&gt;&lt;img src=../img/comparing-vertex-cover-algorithms/thumbs/lavrov-degreeNegative-random.png&gt;&lt;td style=&#34;text-align: center;&#34;&gt;&lt;img src=../img/comparing-vertex-cover-algorithms/thumbs/lavrov-degreeNegative-tricky.png&gt;&lt;tr class=even&gt;&lt;td&gt;&lt;code&gt;degree-positive&lt;/code&gt;&lt;td style=&#34;text-align: center;&#34;&gt;&lt;img src=../img/comparing-vertex-cover-algorithms/thumbs/lavrov-degreePositive-random.png&gt;&lt;td style=&#34;text-align: center;&#34;&gt;&lt;img src=../img/comparing-vertex-cover-algorithms/thumbs/lavrov-degreePositive-tricky.png&gt;&lt;tr class=odd&gt;&lt;td&gt;&lt;code&gt;degree-positive-superlinear&lt;/code&gt;&lt;td style=&#34;text-align: center;&#34;&gt;&lt;img src=../img/comparing-vertex-cover-algorithms/thumbs/lavrov-degreePositiveSuperlinear-random.png&gt;&lt;td style=&#34;text-align: center;&#34;&gt;&lt;img src=../img/comparing-vertex-cover-algorithms/thumbs/lavrov-degreePositiveSuperlinear-tricky.png&gt;&lt;/table&gt;&lt;/div&gt;&lt;h2 id=clever-overview&gt;&lt;code&gt;clever&lt;/code&gt; overview&lt;/h2&gt;&lt;p&gt;As expected, &lt;code&gt;clever&lt;/code&gt; does poorly on &lt;code&gt;tricky&lt;/code&gt;&#xA;graphs with &lt;code&gt;uniform&lt;/code&gt; and &lt;code&gt;random&lt;/code&gt; vertex&#xA;weights.&lt;p&gt;Unexpectedly, it performs quite well on &lt;code&gt;tricky&lt;/code&gt; graphs&#xA;where vertex weight correlates positively with vertex degree! In&#xA;domain-specific terms, that suggests &lt;code&gt;clever&lt;/code&gt; is good choice&#xA;if your HubSpot account’s most-associated types are also your most&#xA;numerous collections.&lt;p&gt;I still gut-distrust this conclusion. It’s just so easy to think up a&#xA;small but plausible associations graph where &lt;code&gt;clever&lt;/code&gt;’s&#xA;dismal because, say, you have hundreds of thousands of Contacts.&lt;div class=firstcoltable&gt;&lt;table&gt;&lt;col style=&#34;width: 12%&#34;&gt;&lt;col style=&#34;width: 43%&#34;&gt;&lt;col style=&#34;width: 43%&#34;&gt;&lt;thead&gt;&lt;tr class=header&gt;&lt;th&gt;&lt;strong&gt;Weight/Topology&lt;/strong&gt;&lt;th style=&#34;text-align: center;&#34;&gt;&lt;code&gt;random&lt;/code&gt;&lt;th style=&#34;text-align: center;&#34;&gt;&lt;code&gt;tricky&lt;/code&gt;&lt;tbody&gt;&lt;tr class=odd&gt;&lt;td&gt;&lt;code&gt;uniform&lt;/code&gt;&lt;td style=&#34;text-align: center;&#34;&gt;&lt;img src=../img/comparing-vertex-cover-algorithms/thumbs/clever-uniform-random.png&gt;&lt;td style=&#34;text-align: center;&#34;&gt;&lt;img src=../img/comparing-vertex-cover-algorithms/thumbs/clever-uniform-tricky.png&gt;&lt;tr class=even&gt;&lt;td&gt;&lt;code&gt;random&lt;/code&gt;&lt;td style=&#34;text-align: center;&#34;&gt;&lt;img src=../img/comparing-vertex-cover-algorithms/thumbs/clever-random-random.png&gt;&lt;td style=&#34;text-align: center;&#34;&gt;&lt;img src=../img/comparing-vertex-cover-algorithms/thumbs/clever-random-tricky.png&gt;&lt;tr class=odd&gt;&lt;td&gt;&lt;code&gt;degree-negative&lt;/code&gt;&lt;td style=&#34;text-align: center;&#34;&gt;&lt;img src=../img/comparing-vertex-cover-algorithms/thumbs/clever-degreeNegative-random.png&gt;&lt;td style=&#34;text-align: center;&#34;&gt;&lt;img src=../img/comparing-vertex-cover-algorithms/thumbs/clever-degreeNegative-tricky.png&gt;&lt;tr class=even&gt;&lt;td&gt;&lt;code&gt;degree-positive&lt;/code&gt;&lt;td style=&#34;text-align: center;&#34;&gt;&lt;img src=../img/comparing-vertex-cover-algorithms/thumbs/clever-degreePositive-random.png&gt;&lt;td style=&#34;text-align: center;&#34;&gt;&lt;img src=../img/comparing-vertex-cover-algorithms/thumbs/clever-degreePositive-tricky.png&gt;&lt;tr class=odd&gt;&lt;td&gt;&lt;code&gt;degree-positive-superlinear&lt;/code&gt;&lt;td style=&#34;text-align: center;&#34;&gt;&lt;img src=../img/comparing-vertex-cover-algorithms/thumbs/clever-degreePositiveSuperlinear-random.png&gt;&lt;td style=&#34;text-align: center;&#34;&gt;&lt;img src=../img/comparing-vertex-cover-algorithms/thumbs/clever-degreePositiveSuperlinear-tricky.png&gt;&lt;/table&gt;&lt;/div&gt;&lt;p&gt;If you want to poke around the experimental results, check out the&#xA;full set of &lt;a href=../img/comparing-vertex-cover-algorithms/unified.html&gt;interactive&#xA;graphs.&lt;/a&gt; If you want to reproduce them or inspect my implementations,&#xA;see the &lt;a href=https://github.com/lukasschwab/vertex-cover&gt;source&#xA;code on GitHub&lt;/a&gt;.&lt;aside id=footnotes class=&#34;footnotes footnotes-end-of-document&#34; role=doc-endnotes&gt;&lt;hr&gt;&lt;ol&gt;&lt;li id=fn1&gt;&lt;p&gt;Zito, Michele. &lt;a href=https://cgi.csc.liv.ac.uk/~michele/TEACHING/COMP309/2005/Lec10.4.4.pdf&gt;“Vertex&#xA;Cover.”&lt;/a&gt; (2005). Also see Lavrov (2020).&lt;a href=#fnref1 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn2&gt;&lt;p&gt;Lavrov, Mikhail. &lt;a href=https://faculty.math.illinois.edu/~mlavrov/docs/482-spring-2020/lecture36.pdf&gt;“Lecture&#xA;36: Approximation Algorithms”&lt;/a&gt; (2020). 2-3. Note: I’ve modified the&#xA;definition to share variables with the other algorithms.&lt;a href=#fnref2 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn3&gt;&lt;p&gt;Vazirani, Vijay V. &lt;a href=https://doc.lagout.org/science/0_Computer%20Science/2_Algorithms/Approximation%20Algorithms%20%5BVazirani%202010-12-01%5D.pdf&gt;&lt;em&gt;Approximation&#xA;Algorithms.&lt;/em&gt;&lt;/a&gt; Vol. 1. Berlin: Springer, 2001.&lt;a href=#fnref3 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn4&gt;&lt;p&gt;Lavrov, Mikhail. &lt;a href=https://faculty.math.illinois.edu/~mlavrov/docs/482-spring-2020/lecture36.pdf&gt;“Lecture&#xA;36: Approximation Algorithms”&lt;/a&gt; (2020). 2-3. Note: I’ve modified the&#xA;definition to share variables with the other algorithms.&lt;a href=#fnref4 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn5&gt;&lt;p&gt;Since there’s a minimal Steiner tree weighted-edge&#xA;equivalent to this vertex cover problem — see my addendum in &lt;a href=https://lukasschwab.me/blog/gen/graphs-at-work&gt;Graphs at Work&lt;/a&gt;&#xA;— I &lt;em&gt;could&lt;/em&gt; compare Steiner tree algorithms using&#xA;&lt;code&gt;gonum.org/v1/gonum/graph&lt;/code&gt;.&lt;p&gt;There are advantages to reimplementing &lt;code&gt;graph&lt;/code&gt;, though.&#xA;For example, &lt;code&gt;clever&lt;/code&gt; and &lt;code&gt;lavrov&lt;/code&gt; &lt;em&gt;both&lt;/em&gt;&#xA;recurse into searches on subgraphs, so my &lt;code&gt;graph&lt;/code&gt; package&#xA;defines an easy-to-call &lt;code&gt;(*Weighted).Without(removed Vertex)&lt;/code&gt;&#xA;function.&lt;a href=#fnref5 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;/ol&gt;&lt;/aside&gt;</content>
    <link href="https://lukasschwab.me/blog/gen/comparing-vertex-cover-algorithms.html" rel="alternate"></link>
    <summary type="html">Go simulations, on several families of graphs, comparing the algorithms discussed in &#34;Graphs at Work.&#34; Can you reasonably give up a $k$-approximation (Vazirani) for a simpler weight-naïve strategy?</summary>
  </entry>
  <entry>
    <title>Graphs at Work</title>
    <updated>2022-09-03T00:00:00Z</updated>
    <id>https://lukasschwab.me/blog/gen/graphs-at-work.html</id>
    <content type="html">&lt;html xmlns=http://www.w3.org/1999/xhtml lang xml:lang&gt;&lt;meta charset=utf-8&gt;&lt;meta name=generator content=&#34;pandoc&#34;&gt;&lt;meta name=viewport content=&#34;width=device-width,initial-scale=1,user-scalable=yes&#34;&gt;&lt;meta name=author content=&#34;Lukas Schwab&#34;&gt;&lt;meta name=dcterms.date content=&#34;2022-09-03&#34;&gt;&lt;title&gt;Graphs at Work&lt;/title&gt;&lt;style&gt;&#xA;      code{white-space: pre-wrap;}&#xA;      span.smallcaps{font-variant: small-caps;}&#xA;      span.underline{text-decoration: underline;}&#xA;      div.column{display: inline-block; vertical-align: top; width: 50%;}&#xA;          &lt;/style&gt;&lt;style&gt;pre &gt; code.sourceCode { white-space: pre; position: relative; }&#xA;pre &gt; code.sourceCode &gt; span { display: inline-block; line-height: 1.25; }&#xA;pre &gt; code.sourceCode &gt; span:empty { height: 1.2em; }&#xA;.sourceCode { overflow: visible; }&#xA;code.sourceCode &gt; span { color: inherit; text-decoration: inherit; }&#xA;div.sourceCode { margin: 1em 0; }&#xA;pre.sourceCode { margin: 0; }&#xA;@media screen {&#xA;div.sourceCode { overflow: auto; }&#xA;}&#xA;@media print {&#xA;pre &gt; code.sourceCode { white-space: pre-wrap; }&#xA;pre &gt; code.sourceCode &gt; span { text-indent: -5em; padding-left: 5em; }&#xA;}&#xA;pre.numberSource code&#xA;  { counter-reset: source-line 0; }&#xA;pre.numberSource code &gt; span&#xA;  { position: relative; left: -4em; counter-increment: source-line; }&#xA;pre.numberSource code &gt; span &gt; a:first-child::before&#xA;  { content: counter(source-line);&#xA;    position: relative; left: -1em; text-align: right; vertical-align: baseline;&#xA;    border: none; display: inline-block;&#xA;    -webkit-touch-callout: none; -webkit-user-select: none;&#xA;    -khtml-user-select: none; -moz-user-select: none;&#xA;    -ms-user-select: none; user-select: none;&#xA;    padding: 0 4px; width: 4em;&#xA;    color: #aaaaaa;&#xA;  }&#xA;pre.numberSource { margin-left: 3em; border-left: 1px solid #aaaaaa;  padding-left: 4px; }&#xA;div.sourceCode&#xA;  {   }&#xA;@media screen {&#xA;pre &gt; code.sourceCode &gt; span &gt; a:first-child::before { text-decoration: underline; }&#xA;}&#xA;code span.al { color: #ff0000; font-weight: bold; } /* Alert */&#xA;code span.an { color: #60a0b0; font-weight: bold; font-style: italic; } /* Annotation */&#xA;code span.at { color: #7d9029; } /* Attribute */&#xA;code span.bn { color: #40a070; } /* BaseN */&#xA;code span.bu { color: #008000; } /* BuiltIn */&#xA;code span.cf { color: #007020; font-weight: bold; } /* ControlFlow */&#xA;code span.ch { color: #4070a0; } /* Char */&#xA;code span.cn { color: #880000; } /* Constant */&#xA;code span.co { color: #60a0b0; font-style: italic; } /* Comment */&#xA;code span.cv { color: #60a0b0; font-weight: bold; font-style: italic; } /* CommentVar */&#xA;code span.do { color: #ba2121; font-style: italic; } /* Documentation */&#xA;code span.dt { color: #902000; } /* DataType */&#xA;code span.dv { color: #40a070; } /* DecVal */&#xA;code span.er { color: #ff0000; font-weight: bold; } /* Error */&#xA;code span.ex { } /* Extension */&#xA;code span.fl { color: #40a070; } /* Float */&#xA;code span.fu { color: #06287e; } /* Function */&#xA;code span.im { color: #008000; font-weight: bold; } /* Import */&#xA;code span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Information */&#xA;code span.kw { color: #007020; font-weight: bold; } /* Keyword */&#xA;code span.op { color: #666666; } /* Operator */&#xA;code span.ot { color: #007020; } /* Other */&#xA;code span.pp { color: #bc7a00; } /* Preprocessor */&#xA;code span.sc { color: #4070a0; } /* SpecialChar */&#xA;code span.ss { color: #bb6688; } /* SpecialString */&#xA;code span.st { color: #4070a0; } /* String */&#xA;code span.va { color: #19177c; } /* Variable */&#xA;code span.vs { color: #4070a0; } /* VerbatimString */&#xA;code span.wa { color: #60a0b0; font-weight: bold; font-style: italic; } /* Warning */&lt;/style&gt;&lt;link rel=stylesheet href=../styles/common.css&gt;&lt;link rel=stylesheet href=../styles/fence-extensions/addendum.css&gt;&lt;link rel=stylesheet href=../styles/fence-extensions/firstcoltable.css&gt;&lt;link rel=stylesheet href=../styles/fence-extensions/frame.css&gt;&lt;link rel=stylesheet href=../styles/fence-extensions/smallfig.css&gt;&lt;link rel=stylesheet href=../styles/fence-extensions/widetable.css&gt;&lt;script data-goatcounter=https://lukasschwab.goatcounter.com/count async src=//gc.zgo.at/count.js&gt;&lt;/script&gt;&lt;a href=../index.html&gt;&amp;#171; blog&lt;/a&gt; &lt;span class=date&gt;2022-09-03&lt;/span&gt;&lt;header id=title-block-header&gt;&lt;h1 class=title&gt;Graphs at Work&lt;/h1&gt;&lt;/header&gt;&lt;p&gt;You might never invert a binary tree, but graph theory — and graph&#xA;algorithms — worm their way into software engineering, whether you&#xA;recognize them or not. This problem wormed its way into my brain last&#xA;week, where it reminded me API engineering isn’t all just flat-mapping&#xA;responses. Conceiving this as a graph problem doesn’t just guide&#xA;implementation: it determines the product’s attributes, which will&#xA;impact its direction.&lt;p&gt;One of Sequin’s products is a &lt;a href=https://docs.sequin.io/hubspot/reference&gt;two-way integration&lt;/a&gt;&#xA;betwen our customers’ HubSpot accounts and their Postgres databases,&#xA;using the HubSpot API to live-update the database and forward writes to&#xA;Postgres back to HubSpot. There’s a variety of entities in HubSpot:&#xA;Companies, Contacts, Deals, and all the custom object types our&#xA;customers’ hearts desire. (Want to index Cars in HubSpot? Make a custom&#xA;Car type.) Our customers might have thousands of Contacts, hundreds of&#xA;Companies, and so on.&lt;p&gt;Of course, these entities aren’t islands; a Contact works for a&#xA;Company, a Deal binds a company and is negotiated by a Contact, and so&#xA;on. Rather than capturing every possible relationship on each type,&#xA;HubSpot has a separate &lt;em&gt;associations&lt;/em&gt; model for connecting two&#xA;objects. For example, an association might declare “Contact Lukas Schwab&#xA;is associated with Company Sequin.” Those associations are always&#xA;bidirectional; if I’m associated with Sequin, Sequin’s also associated&#xA;with me.&lt;p&gt;Because associations are a core part of HubSpot’s product model,&#xA;Sequin’s sync needs to scrape them into Postgres. HubSpot’s API makes&#xA;that a little tricky; I’ll try to explain this in brief, because it’s&#xA;important to undertanding the task at hand.&lt;p&gt;If you want to fetch all associations between Contacts and Companies&#xA;from the HubSpot v3 API, you can use either of two endpoints:&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;code&gt;/crm/v3/objects/{fromObjectType}/{fromObjectId}/associations/{toObjectType}&lt;/code&gt;&lt;p&gt;This tells you all of the associations between a single object —&#xA;specified by &lt;code&gt;fromObjectType&lt;/code&gt; and &lt;code&gt;fromObjectId&lt;/code&gt; —&#xA;and objects of &lt;code&gt;toObjectType&lt;/code&gt;. For example, you could&#xA;&lt;code&gt;GET /crm/v3/objects/contact/1860/associations/companies&lt;/code&gt; to&#xA;list all Companies associated with Contact 1860.&lt;p&gt;Of course, if you have 10,000 Contacts, you’ll have to call this&#xA;endpoint 10,000 times to get a complete picture of Contact ↔︎ Company&#xA;associations. If you also want associations between Contacts and Deals,&#xA;you’ll have to make &lt;em&gt;another&lt;/em&gt; 10,000 requests with&#xA;&lt;code&gt;toObjectType&lt;/code&gt; set to &lt;code&gt;deal&lt;/code&gt;.&lt;li&gt;&lt;p&gt;&lt;code&gt;/crm/v3/objects/{fromObjectType}&lt;/code&gt;&lt;p&gt;This endpoint for listing objects of &lt;code&gt;fromObjectType&lt;/code&gt;, 100&#xA;at a time, has a handy &lt;code&gt;associations&lt;/code&gt; parameter. When&#xA;specified, the response includes &lt;em&gt;all&lt;/em&gt; associations with objects&#xA;of the specified types!&lt;p&gt;If you have 10,000 Contacts and you want to get Contact ↔︎ Company and&#xA;Contact ↔︎ Deal associations, 100 requests will do the job.&lt;a href=#fn1 class=footnote-ref id=fnref1 role=doc-noteref&gt;&lt;sup&gt;1&lt;/sup&gt;&lt;/a&gt;&lt;/ul&gt;&lt;p&gt;Because HubSpot limits each customer’s daily API usage, the second&#xA;endpoint is the much more desirable access pattern. Still, we have to&#xA;pick a request strategy. Given the association types a customer wants&#xA;synced to Postgres — type pairs like Contact ↔︎ Company — what&#xA;&lt;code&gt;fromObjectType&lt;/code&gt;s should Sequin list? The project&#xA;requirements are to design a request strategy which&lt;ol type=1&gt;&lt;li&gt;Systematically pulls all associations the customer desires,&#xA;specified as pairs of types.&lt;li&gt;Minimizes the number of requests to HubSpot’s API.&lt;/ol&gt;&lt;p&gt;For instance, a customer’s desired association types might look like&#xA;this:&lt;table&gt;&lt;thead&gt;&lt;tr class=header&gt;&lt;th style=&#34;text-align: left;&#34;&gt;From&lt;th style=&#34;text-align: left;&#34;&gt;To&lt;tbody&gt;&lt;tr class=odd&gt;&lt;td style=&#34;text-align: left;&#34;&gt;Contact&lt;td style=&#34;text-align: left;&#34;&gt;Company&lt;tr class=even&gt;&lt;td style=&#34;text-align: left;&#34;&gt;Deal&lt;td style=&#34;text-align: left;&#34;&gt;Contact&lt;tr class=odd&gt;&lt;td style=&#34;text-align: left;&#34;&gt;Company&lt;td style=&#34;text-align: left;&#34;&gt;Deal&lt;tr class=even&gt;&lt;td style=&#34;text-align: left;&#34;&gt;Company&lt;td style=&#34;text-align: left;&#34;&gt;Car&lt;tr class=odd&gt;&lt;td style=&#34;text-align: left;&#34;&gt;Company&lt;td style=&#34;text-align: left;&#34;&gt;Policy&lt;tr class=even&gt;&lt;td style=&#34;text-align: left;&#34;&gt;Car&lt;td style=&#34;text-align: left;&#34;&gt;Policy&lt;tr class=odd&gt;&lt;td style=&#34;text-align: left;&#34;&gt;Company&lt;td style=&#34;text-align: left;&#34;&gt;Owner&lt;/table&gt;&lt;p&gt;But it’s easier to start thinking about a simpler configuration:&lt;table&gt;&lt;thead&gt;&lt;tr class=header&gt;&lt;th style=&#34;text-align: left;&#34;&gt;From&lt;th style=&#34;text-align: left;&#34;&gt;To&lt;tbody&gt;&lt;tr class=odd&gt;&lt;td style=&#34;text-align: left;&#34;&gt;Contact&lt;td style=&#34;text-align: left;&#34;&gt;Company&lt;tr class=even&gt;&lt;td style=&#34;text-align: left;&#34;&gt;Deal&lt;td style=&#34;text-align: left;&#34;&gt;Contact&lt;/table&gt;&lt;p&gt;This is when I recognized graphs would be good visualizations for&#xA;request strategies. Make each unique type (in this example, Contact,&#xA;Company, and Deal) a vertex; then the associations we want to fetch are&#xA;edges on the graph. The customer configuration is essentially an&#xA;adjacency list!&lt;div class=smallfig&gt;&lt;figure&gt;&lt;img src=../img/graphs-at-work/simple.svg alt=&#34;The associations we want, expressed as a graph: Contact ↔ Company and Deal ↔ Contact. The Contact type is involved in both associations, but it’s reduced to a single vertex.&#34;&gt;&lt;figcaption aria-hidden=true&gt;The associations we want, expressed as a&#xA;graph: Contact ↔︎ Company and Deal ↔︎ Contact. The Contact type is&#xA;involved in both associations, but it’s reduced to a single&#xA;vertex.&lt;/figcaption&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;Coloring the types you’d list with &lt;code&gt;GET&lt;/code&gt; requests gives&#xA;you a visual representation of each request strategy.&lt;table&gt;&lt;col style=&#34;width: 54%&#34;&gt;&lt;col style=&#34;width: 45%&#34;&gt;&lt;thead&gt;&lt;tr class=header&gt;&lt;th style=&#34;text-align: left;&#34;&gt;Request strategy&lt;th style=&#34;text-align: center;&#34;&gt;Visualization&lt;tbody&gt;&lt;tr class=odd&gt;&lt;td style=&#34;text-align: left;&#34;&gt;&lt;code&gt;GET&lt;/code&gt;&#xA;contacts&lt;br&gt;&lt;code&gt;associations=[company]&lt;/code&gt;&lt;br&gt;&lt;br&gt;&lt;code&gt;GET&lt;/code&gt;&#xA;deals&lt;br&gt;&lt;code&gt;associations=[contact]&lt;/code&gt;&lt;td style=&#34;text-align: center;&#34;&gt;&lt;img src=../img/graphs-at-work/simple-contacts-deals.svg&gt;&lt;tr class=even&gt;&lt;td style=&#34;text-align: left;&#34;&gt;&lt;code&gt;GET&lt;/code&gt;&#xA;contacts&lt;br&gt;&lt;code&gt;associations=[company, deal]&lt;/code&gt;&lt;td style=&#34;text-align: center;&#34;&gt;&lt;img src=../img/graphs-at-work/simple-contacts.svg&gt;&lt;tr class=odd&gt;&lt;td style=&#34;text-align: left;&#34;&gt;&lt;code&gt;GET&lt;/code&gt;&#xA;deals&lt;br&gt;&lt;code&gt;associations=[contact]&lt;/code&gt;&lt;br&gt;&lt;br&gt;&lt;code&gt;GET&lt;/code&gt;&#xA;companies&lt;br&gt;&lt;code&gt;associations=[contact]&lt;/code&gt;&lt;td style=&#34;text-align: center;&#34;&gt;&lt;img src=../img/graphs-at-work/simple-deals-companies.svg&gt;&lt;/table&gt;&lt;p&gt;Now that you’re working with graphs, you have access to a pithier&#xA;expression of the problem. The set of desired associations defines a&#xA;graph &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;G&lt;/em&gt; = (&lt;em&gt;V&lt;/em&gt;,&lt;em&gt;E&lt;/em&gt;)&lt;/span&gt;, where&#xA;the edges &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;E&lt;/em&gt;&lt;/span&gt; are desired&#xA;associations and the vertices &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;V&lt;/em&gt;&lt;/span&gt; are the HubSpot types involved.&#xA;Our search for a “request strategy” is a search for an optimal subset&#xA;&lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;V&lt;/em&gt;&lt;sub&gt;req&lt;/sub&gt; ⊆ &lt;em&gt;V&lt;/em&gt;&lt;/span&gt;&#xA;such that every &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;e&lt;/em&gt; ∈ &lt;em&gt;E&lt;/em&gt;&lt;/span&gt;&#xA;is incident upon — touching — at least one object type &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;v&lt;/em&gt; ∈ &lt;em&gt;V&lt;/em&gt;&lt;sub&gt;req&lt;/sub&gt;&lt;/span&gt;.&lt;p&gt;All of the strategies in the table above satisfy that condition, but&#xA;one of them — and only one valid strategy for that graph! — only lists&#xA;objects of a single type, Contacts.&lt;p&gt;There isn’t always a single-type silver bullet. For some graphs, no&#xA;single vertex is incident on every edge.&lt;figure&gt;&lt;img src=../img/graphs-at-work/nonsimple.svg alt=&#34;In this associations graph, the simplest valid strategy lists objects of two types: Deal and Company. No single vertex is incident on every edge.&#34;&gt;&lt;figcaption aria-hidden=true&gt;In this associations graph, the simplest&#xA;valid strategy lists objects of &lt;em&gt;two&lt;/em&gt; types: Deal and Company. No&#xA;single vertex is incident on every edge.&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;You’re faced with the algorithmic question I faced on Friday: given&#xA;any graph like this, how can you systematically produce a valid minimal&#xA;strategy?&lt;/p&gt;&lt;h1 id=cracking-the-graph&gt;Cracking the graph&lt;/h1&gt;&lt;p&gt;As a rule of thumb, greedy graph algorithms — approaches which&#xA;repeatedly apply a single heuristic to make local decisions — are easier&#xA;to stomach. Because the heuristic is the crux of the implementation, you&#xA;can focus on documenting that. With any luck, it won’t give your code&#xA;reviewer a headache.&lt;p&gt;Luckily, there’s an intuitive greedy heuristic for selecting &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;V&lt;/em&gt;&lt;sub&gt;req&lt;/sub&gt;&lt;/span&gt; from &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;V&lt;/em&gt;&lt;/span&gt;! Consider your start state and end&#xA;state:&lt;ul&gt;&lt;li&gt;&lt;p&gt;When your algorithm starts, you have an empty &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;V&lt;/em&gt;&lt;sub&gt;req&lt;/sub&gt;&lt;/span&gt;; you don’t know what&#xA;types to request. By definition, the set of edges &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;E&lt;/em&gt;&lt;sub&gt;req&lt;/sub&gt;&lt;/span&gt; incident on &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;V&lt;/em&gt;&lt;sub&gt;req&lt;/sub&gt;&lt;/span&gt; is empty: no&#xA;vertices, no edges.&lt;li&gt;&lt;p&gt;When your algorithm ends, &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;E&lt;/em&gt;&lt;sub&gt;req&lt;/sub&gt; = &lt;em&gt;E&lt;/em&gt;&lt;/span&gt;: every&#xA;edge in the graph is incident on &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;V&lt;/em&gt;&lt;sub&gt;req&lt;/sub&gt;&lt;/span&gt;.&lt;/ul&gt;&lt;p&gt;At any intermediate step, what change to the &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;V&lt;/em&gt;&lt;sub&gt;req&lt;/sub&gt;&lt;/span&gt; you’re building&#xA;makes the most progress towards that end state? The one that adds the&#xA;most new edges in &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;E&lt;/em&gt;&lt;/span&gt; to &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;E&lt;/em&gt;&lt;sub&gt;req&lt;/sub&gt;&lt;/span&gt;!&lt;p&gt;More formally, given an associations graph &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;G&lt;/em&gt; = (&lt;em&gt;V&lt;/em&gt;,&lt;em&gt;E&lt;/em&gt;)&lt;/span&gt;,&lt;ol type=1&gt;&lt;li&gt;Let &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;V&lt;/em&gt;&lt;sub&gt;req&lt;/sub&gt; = {}&lt;/span&gt;,&#xA;&lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;E&lt;/em&gt;&lt;sub&gt;req&lt;/sub&gt; = {}&lt;/span&gt;.&lt;li&gt;While &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;E&lt;/em&gt; ≠ {}&lt;/span&gt;:&lt;ul&gt;&lt;li&gt;Find &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;v&lt;/em&gt; ∈ &lt;em&gt;V&lt;/em&gt;&lt;/span&gt; with&#xA;the greatest number of incident edges.&lt;li&gt;Add &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;v&lt;/em&gt;&lt;/span&gt; to &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;V&lt;/em&gt;&lt;sub&gt;req&lt;/sub&gt;&lt;/span&gt;. Add the incident&#xA;edges to &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;E&lt;/em&gt;&lt;sub&gt;req&lt;/sub&gt;&lt;/span&gt;.&lt;li&gt;Remove &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;v&lt;/em&gt;&lt;/span&gt; from &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;G&lt;/em&gt;&lt;/span&gt;.&lt;/ul&gt;&lt;li&gt;Return &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;V&lt;/em&gt;&lt;sub&gt;req&lt;/sub&gt;&lt;/span&gt;.&lt;/ol&gt;&lt;p&gt;&lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;G&lt;/em&gt;&lt;/span&gt; shrinks with each step,&#xA;so this is guaranteed to terminate for any finite associations graph&#xA;(and, since there’s a finite limit to the number of object types in a&#xA;HubSpot account, &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;G&lt;/em&gt;&lt;/span&gt; is&#xA;guaranteed to be finite — phew). I’ll call this the&#xA;&lt;strong&gt;clever-greedy algorithm&lt;/strong&gt;, after Zito.&lt;a href=#fn2 class=footnote-ref id=fnref2 role=doc-noteref&gt;&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt;&lt;p&gt;This has a straightforward — if inefficient — recursive&#xA;implementation,&lt;a href=#fn3 class=footnote-ref id=fnref3 role=doc-noteref&gt;&lt;sup&gt;3&lt;/sup&gt;&lt;/a&gt; but don’t sweat it if you don’t want&#xA;to read code. You can use the visualizations to see successive&#xA;applications of the algorithm chip away at &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;G&lt;/em&gt;&lt;/span&gt; as it builds the set of types&#xA;&lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;V&lt;/em&gt;&lt;sub&gt;req&lt;/sub&gt;&lt;/span&gt;.&lt;div class=firstcoltable&gt;&lt;table&gt;&lt;col style=&#34;width: 3%&#34;&gt;&lt;col style=&#34;width: 10%&#34;&gt;&lt;col style=&#34;width: 21%&#34;&gt;&lt;col style=&#34;width: 22%&#34;&gt;&lt;col style=&#34;width: 41%&#34;&gt;&lt;thead&gt;&lt;tr class=header&gt;&lt;th style=&#34;text-align: right;&#34;&gt;Step&lt;th style=&#34;text-align: left;&#34;&gt;&lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;V&lt;/em&gt;&lt;sub&gt;req&lt;/sub&gt;&lt;/span&gt;&lt;th style=&#34;text-align: left;&#34;&gt;&lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;E&lt;/em&gt;&lt;sub&gt;req&lt;/sub&gt;&lt;/span&gt;&lt;th&gt;Subgraph&lt;th style=&#34;text-align: left;&#34;&gt;Evaluation&lt;tbody&gt;&lt;tr class=odd&gt;&lt;td style=&#34;text-align: right;&#34;&gt;0&lt;td style=&#34;text-align: left;&#34;&gt;&lt;span class=&#34;math inline&#34;&gt;∅&lt;/span&gt;&lt;td style=&#34;text-align: left;&#34;&gt;&lt;span class=&#34;math inline&#34;&gt;∅&lt;/span&gt;&lt;td&gt;&lt;img src=../img/graphs-at-work/nonsimple.svg&gt;&lt;td style=&#34;text-align: left;&#34;&gt;Company and Deal both have three incident&#xA;edges. Take Deal.&lt;tr class=even&gt;&lt;td style=&#34;text-align: right;&#34;&gt;1&lt;td style=&#34;text-align: left;&#34;&gt;{Deal}&lt;td style=&#34;text-align: left;&#34;&gt;Deal↔︎Contact&lt;br&gt;Deal↔︎Company&lt;br&gt;Deal↔︎Policy&lt;td&gt;&lt;img src=../img/graphs-at-work/nonsimple-1.svg&gt;&lt;td style=&#34;text-align: left;&#34;&gt;Company has two incident edges; Contact&#xA;and Policy each have one. Take Company.&lt;tr class=odd&gt;&lt;td style=&#34;text-align: right;&#34;&gt;2&lt;td style=&#34;text-align: left;&#34;&gt;{Deal, Company}&lt;td style=&#34;text-align: left;&#34;&gt;Deal↔︎Contact&lt;br&gt;Deal↔︎Company&lt;br&gt;Deal↔︎Policy&lt;br&gt;Company↔︎Contact&lt;br&gt;Company↔︎Policy&lt;td&gt;&lt;img src=../img/graphs-at-work/nonsimple-2.svg&gt;&lt;td style=&#34;text-align: left;&#34;&gt;There aren’t any more edges in G, so we&#xA;have a valid &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;V&lt;/em&gt;&lt;sub&gt;req&lt;/sub&gt;&lt;/span&gt;!&lt;/table&gt;&lt;/div&gt;&lt;p&gt;Problem solved, right? You reframed the problem in terms of graphs,&#xA;which led you to a greedy algorithm — just a couple-dozen lines of code&#xA;— that’ll produce a valid, seemingly minimal request strategy every&#xA;time.&lt;p&gt;There’s one issue. In practice, &lt;em&gt;what’s optimal?&lt;/em&gt; What do the&#xA;project requirements say about optimality?&lt;blockquote&gt;&lt;ol type=1&gt;&lt;li&gt;Systematically pulls all associations the customer desires,&#xA;specified as pairs of types.&lt;li&gt;&lt;em&gt;Minimizes the number of requests to HubSpot’s API.&lt;/em&gt;&lt;/ol&gt;&lt;/blockquote&gt;&lt;p&gt;Are you minimizing API usage with the greedy strategy? Not&#xA;necessarily! You’re minimizing the number of types you have to list,&#xA;true, but the API cost of listing a type is &lt;em&gt;nonuniform&lt;/em&gt;. Because&#xA;each &lt;code&gt;GET&lt;/code&gt; request only returns a page of ≤100 objects, the&#xA;number of API requests we have to make for a strategy scales with the&#xA;total number of objects of the types in our strategy.&lt;p&gt;Do fewer types mean fewer objects? That’s not an unreasonable guess&#xA;if you know nothing about the system, but you don’t have to look far for&#xA;a counterexample. Consider a tweaked version of our initial simple&#xA;associations graph, where you have additional info about the number of&#xA;objects in HubSpot: there are 300 Contacts, 100 Companies, and 100&#xA;Deals.&lt;div id=weighted class=widetable&gt;&lt;table&gt;&lt;col style=&#34;width: 45%&#34;&gt;&lt;col style=&#34;width: 27%&#34;&gt;&lt;col style=&#34;width: 27%&#34;&gt;&lt;thead&gt;&lt;tr class=header&gt;&lt;th&gt;Strategy&lt;th style=&#34;text-align: left;&#34;&gt;&lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;V&lt;/em&gt;&lt;sub&gt;req&lt;/sub&gt;&lt;/span&gt;&lt;th style=&#34;text-align: left;&#34;&gt;API cost&lt;tbody&gt;&lt;tr class=odd&gt;&lt;td&gt;&lt;img src=../img/graphs-at-work/simple-weighted.svg&gt;&lt;td style=&#34;text-align: left;&#34;&gt;{Contact}&lt;td style=&#34;text-align: left;&#34;&gt;300 contacts.&lt;br&gt;&lt;span class=&#34;math inline&#34;&gt;∴&lt;/span&gt; Three requests.&lt;tr class=even&gt;&lt;td&gt;&lt;img src=../img/graphs-at-work/simple-weighted-optimal.svg&gt;&lt;td style=&#34;text-align: left;&#34;&gt;{Deal, Company}&lt;td style=&#34;text-align: left;&#34;&gt;100 Deals.&lt;br&gt;100 Companies.&lt;br&gt;&lt;span class=&#34;math inline&#34;&gt;∴&lt;/span&gt; Two requests.&lt;/table&gt;&lt;/div&gt;&lt;p&gt;The first strategy is simpler — it only involves listing objects of&#xA;one type — but it’s &lt;em&gt;costlier.&lt;/em&gt; The project requires optimizing&#xA;API cost, not strategy simplicity!&lt;p&gt;Can you get there by modifying the greedy strategy? That’s not clear.&#xA;The simple heuristic (“pick the vertex that adds the most new edges in&#xA;&lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;E&lt;/em&gt;&lt;/span&gt; to &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;E&lt;/em&gt;&lt;sub&gt;req&lt;/sub&gt;&lt;/span&gt;”) &lt;em&gt;also&lt;/em&gt;&#xA;needs to factor in each vertex’s cost. Sometimes a voluminous type is&#xA;worth requesting because it’s incident on many edges; sometimes it&#xA;isn’t. In complex graphs, it’s hard to figure out locally whether a&#xA;given vertex will wind up being a good pick or a bad one. Hmm.&lt;/p&gt;&lt;h1 id=the-graph-cracks-back&gt;The graph cracks back&lt;/h1&gt;&lt;p&gt;Looks can be deceiving! This puzzle is a &lt;a href=https://en.wikipedia.org/wiki/Vertex_cover&gt;&lt;strong&gt;minimal vertex&#xA;cover&lt;/strong&gt;&lt;/a&gt; problem disguised as a HubSpot API question.&#xA;Introducing cost minimization makes it a &lt;em&gt;weighted&lt;/em&gt; minimal&#xA;vertex cover problem. With or without weights, the optimization problem&#xA;is NP-hard: as far as the career math-freaks know, there isn’t a&#xA;solution that works in polynomial time. Any general algorithm for&#xA;&lt;em&gt;truly&lt;/em&gt; optimizing your HubSpot associations-fetching is going to&#xA;be really, really slow. Bummer!&lt;p&gt;If you don’t care about performance, there’s a brute-force solution&#xA;in &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;O&lt;/em&gt;(2&lt;sup&gt;&lt;em&gt;n&lt;/em&gt;&lt;/sup&gt;)&lt;/span&gt;:&lt;ol type=1&gt;&lt;li&gt;Filter the &lt;span class=&#34;math inline&#34;&gt;2&lt;sup&gt;&lt;em&gt;n&lt;/em&gt;&lt;/sup&gt;&lt;/span&gt;&#xA;subsets of &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;V&lt;/em&gt;&lt;/span&gt; for vertex covers&#xA;of &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;G&lt;/em&gt;&lt;/span&gt;, the valid API&#xA;strategies.&lt;li&gt;For each, calculate the API cost.&lt;li&gt;Take the lowest-cost strategy.&lt;/ol&gt;&lt;p&gt;What’s wrong with the clever-greedy algorithm? Provided a graph&#xA;designed to trip it up, it can’t even guarantee results within &lt;em&gt;any&#xA;constant-factor multiple&lt;/em&gt; of the optimal weight!&lt;a href=#fn4 class=footnote-ref id=fnref4 role=doc-noteref&gt;&lt;sup&gt;4&lt;/sup&gt;&lt;/a&gt;&#xA;There are other greedy algorithms for vertex cover guaranteeing results&#xA;no larger than twice the minimum (2-approximations).&lt;a href=#fn5 class=footnote-ref id=fnref5 role=doc-noteref&gt;&lt;sup&gt;5&lt;/sup&gt;&lt;/a&gt; My&#xA;favorite is Savage’s, which simply prunes the leaves from a spanning&#xA;tree.&lt;a href=#fn6 class=footnote-ref id=fnref6 role=doc-noteref&gt;&lt;sup&gt;6&lt;/sup&gt;&lt;/a&gt;&lt;p&gt;It stands to reason that introducing weights makes this harder;&#xA;unfortunately, since we’re working with weighted &lt;em&gt;vertices,&lt;/em&gt; you&#xA;can’t just apply Savage’s reasoning to a minimum weighted spanning tree&#xA;(which you could build with Prim’s algorithm). There &lt;em&gt;are&lt;/em&gt;&#xA;2-approximation algorithms for weighted vertex cover, like this one&#xA;adapted from Vazirani:&lt;a href=#fn7 class=footnote-ref id=fnref7 role=doc-noteref&gt;&lt;sup&gt;7&lt;/sup&gt;&lt;/a&gt;&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;2.11&lt;/strong&gt; Consider the following algorithm for the&#xA;weighted vertex cover problem. For each vertex &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;v&lt;/em&gt;&lt;/span&gt;, &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;t&lt;/em&gt;(&lt;em&gt;v&lt;/em&gt;)&lt;/span&gt; is initialized to its&#xA;weight), and when &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;t&lt;/em&gt;(&lt;em&gt;v&lt;/em&gt;)&lt;/span&gt; drops to 0, &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;v&lt;/em&gt;&lt;/span&gt; is picked in the cover.&lt;div style=&#34;border: 1px solid black; padding: 0 0.5em;&#34;&gt;&lt;p&gt;&lt;strong&gt;Algorithm 2.17&lt;/strong&gt;&lt;ol type=1&gt;&lt;li&gt;Initialization:&lt;ul&gt;&lt;li&gt;&lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;V&lt;/em&gt;&lt;sub&gt;req&lt;/sub&gt; ← ∅&lt;/span&gt;&lt;li&gt;&lt;span class=&#34;math inline&#34;&gt;∀&lt;em&gt;v&lt;/em&gt; ∈ &lt;em&gt;V&lt;/em&gt;, &lt;em&gt;t&lt;/em&gt;(&lt;em&gt;v&lt;/em&gt;) ← weight(&lt;em&gt;v&lt;/em&gt;)&lt;/span&gt;&lt;/ul&gt;&lt;li&gt;While &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;V&lt;/em&gt;&lt;sub&gt;req&lt;/sub&gt;&lt;/span&gt; is&#xA;not a vertex cover do:&lt;ul&gt;&lt;li&gt;Pick an uncovered edge, say &lt;span class=&#34;math inline&#34;&gt;(&lt;em&gt;u&lt;/em&gt;,&lt;em&gt;v&lt;/em&gt;)&lt;/span&gt;. Let &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;m&lt;/em&gt; = min (&lt;em&gt;t&lt;/em&gt;(&lt;em&gt;u&lt;/em&gt;),&lt;em&gt;t&lt;/em&gt;(&lt;em&gt;v&lt;/em&gt;))&lt;/span&gt;.&lt;li&gt;&lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;t&lt;/em&gt;(&lt;em&gt;u&lt;/em&gt;) ← &lt;em&gt;t&lt;/em&gt;(&lt;em&gt;u&lt;/em&gt;) − &lt;em&gt;m&lt;/em&gt;&lt;/span&gt;.&lt;li&gt;&lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;t&lt;/em&gt;(&lt;em&gt;v&lt;/em&gt;) ← &lt;em&gt;t&lt;/em&gt;(&lt;em&gt;v&lt;/em&gt;) − &lt;em&gt;m&lt;/em&gt;&lt;/span&gt;.&lt;li&gt;Include in &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;V&lt;/em&gt;&lt;sub&gt;req&lt;/sub&gt;&lt;/span&gt;&#xA;all vertices having &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;t&lt;/em&gt;(&lt;em&gt;v&lt;/em&gt;) = 0&lt;/span&gt;.&lt;/ul&gt;&lt;li&gt;Output &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;V&lt;/em&gt;&lt;sub&gt;req&lt;/sub&gt;&lt;/span&gt;.&lt;/ol&gt;&lt;/div&gt;&lt;/blockquote&gt;&lt;p&gt;This has essentially the same halting condition as our greedy&#xA;algorithm, but it decreases a vertex’s weight &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;t&lt;/em&gt;(&lt;em&gt;v&lt;/em&gt;)&lt;/span&gt; as its neighbors are&#xA;added to the eventual set cover &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;V&lt;/em&gt;&lt;sub&gt;req&lt;/sub&gt;&lt;/span&gt;. Intuitively, a&#xA;vertex with a high initial weight is unlikely to ever hit &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;t&lt;/em&gt;(&lt;em&gt;v&lt;/em&gt;) = 0&lt;/span&gt;, so it’s unlikely&#xA;to wind up in &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;V&lt;/em&gt;&lt;sub&gt;req&lt;/sub&gt;&lt;/span&gt;.&#xA;An algorithm like Vazirani’s might make a fine solution for getting an&#xA;&lt;em&gt;okay&lt;/em&gt; minimization.&lt;p&gt;Is it the solution for you? Like the earlier question about whether a&#xA;“small” strategy was optimal, this comes down to the requirements —&#xA;implicit and explicit — of the project. The pragmatizing effect of&#xA;project requirements is really, in some sense, what sets software&#xA;engineering apart from computer science.&lt;p&gt;This puzzle explicitly requires you minimize API usage.&#xA;Unfortunately, neither the clever-greedy algorithm nor Vazirani’s&#xA;algorithm guarantee optimal outcomes. Vazirani guarantees at worst a&#xA;2-approximation of the optimal strategy, whereas the clever-greedy&#xA;algorithm can’t even promise a constant factor for an unweighted set&#xA;cover. Practically, though, worst-case performance might not matter!&#xA;User-provided lists of assosiations to fetch usually won’t be the graphs&#xA;that trick clever-greedy into poor outcomes. If customer concerns are&#xA;well-isolated, an adversarial customer can force your algorithm into an&#xA;inefficient solution… but they can’t impact anyone else’s experience. To&#xA;improve the customer experience, what you need to understand is the&#xA;&lt;em&gt;distribution&lt;/em&gt; of strategy performances for a given algorithm&#xA;&lt;em&gt;on real-world graphs&lt;/em&gt;, not the theoretical worst case. Here&#xA;clever-greedy &lt;em&gt;might&lt;/em&gt; be better, even if it assumes constant&#xA;vertex weights.&lt;a href=#fn8 class=footnote-ref id=fnref8 role=doc-noteref&gt;&lt;sup&gt;8&lt;/sup&gt;&lt;/a&gt;&lt;p&gt;Instead, implicit project requirements — values inherent to the&#xA;engineering task — could reasonably determine what you implement. New&#xA;engineers will have to read and modify the algorithm you choose; because&#xA;that time trades off with other work, your customers may be&#xA;better-served &lt;em&gt;holistically&lt;/em&gt; if you pick a simple, maintainable&#xA;algorithm here over a strong &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;n&lt;/em&gt;&lt;/span&gt;-approximation. That’s the&#xA;principle behind &lt;a href=./technical-debt&gt;minimizing tech&#xA;debt&lt;/a&gt;.&lt;p&gt;Lehman describes the evolution of programs through a system of three&#xA;nesting levels:&lt;ol type=1&gt;&lt;li&gt;&lt;em&gt;S-programs&lt;/em&gt; implement specifications: “return the lowest&#xA;common multiple of two integers.”&lt;li&gt;&lt;em&gt;P-programs&lt;/em&gt;, made up of S-programs, strive to perform&#xA;optimal behaviors: “play chess.”&lt;li&gt;&lt;em&gt;E-programs&lt;/em&gt;, made up of P-programs, “mechanize a human or&#xA;societal activity.” Software prducts, by nature of having collaborators&#xA;and users, are E-programs.&lt;a href=#fn9 class=footnote-ref id=fnref9 role=doc-noteref&gt;&lt;sup&gt;9&lt;/sup&gt;&lt;/a&gt;&lt;/ol&gt;&lt;p&gt;The HubSpot project requirements ask you to design a P-program. At&#xA;several junctures, feeling comfortable working with graphs helped break&#xA;that task into S-programs: graphs gave us a natural visualization, which&#xA;helped design a greedy algorithm. Recognizing a well-studied problem in&#xA;graph theory, the vertex cover, exposed flaws in that greedy algorithm —&#xA;the adversarial cases — and led to the conclusion all known optimal&#xA;solutions are inefficient.&lt;p&gt;Critically, though, graph theory is handy for understanding the&#xA;relationship between P-program and E-program. Discovering an existing&#xA;2-approximation like Vazirani’s might be the difference between a&#xA;worthwhile feature and a waste of time. Having the vocabulary to study&#xA;the associations graphs you see in practice lets you study whether&#xA;worst-case performance matters, or if maybe something simpler&#xA;(unweighted) will usually do better.&lt;p&gt;In summary, this isn’t just engineering navel gazing; recognizing a&#xA;minimum weigted vertex cover problem when you see one impacts your&#xA;bottom line. Losing engineering hours to battling &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;P&lt;/em&gt; ≠ &lt;em&gt;N&lt;/em&gt;&lt;em&gt;P&lt;/em&gt;&lt;/span&gt; or reading&#xA;abstruse optimizations won’t keep the lights on.&lt;p&gt;Brush up on your graph theory, and wield it, but remember you’re&#xA;building a product — don’t miss the forest for the trees.&lt;div class=addendum data-date=&#34;September 3, 2022: Extra Math&#34;&gt;&lt;p&gt;I didn’t recognize the weighted vertex cover problem at first.&#xA;Intead, I worked through expressing this problem as two other NP-hard&#xA;optimization problems, distrusting my own conclusions. Here’s a bit more&#xA;math for anyone curious!&lt;p&gt;My first theory was that I could calculate the &lt;a href=https://en.wikipedia.org/wiki/Edge_cover&gt;minimum edge cover&lt;/a&gt;&#xA;of a modified &lt;a href=https://en.wikipedia.org/wiki/Line_graph&gt;line&#xA;graph&lt;/a&gt; &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;L&lt;/em&gt;(&lt;em&gt;G&lt;/em&gt;)&lt;/span&gt; — a&#xA;task much easier than NP-hard — but that was wrong. For our problem, the&#xA;edges in &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;L&lt;/em&gt;(&lt;em&gt;G&lt;/em&gt;)&lt;/span&gt; are&#xA;non-independent; you have to take or leave all the Contact edges as a&#xA;batch. If I successfully reduced minimum vertex cover to minimum edge&#xA;cover, I would suddenly be very, very popular.&lt;p&gt;Fiddling with line graphs led me to this more fruitful representation&#xA;of the &lt;a href=#weighted&gt;three-vertex weighted example discussed&#xA;above&lt;/a&gt;:&lt;figure&gt;&lt;img src=../img/graphs-at-work/steiner.svg alt=&#34;The root is an artificial vertex. It’s connected to each vertex in V by an edge weighted by the cost of listing all objects of that type. The final row of vertices represents the association types we want to fetch. An association vertex’s parents are the types at either end of the association. Once you’ve paid the cost of listing Contacts, you have all the associations involving contexts; that holds for any type, so all of these edges are weighted 0.&#34;&gt;&lt;figcaption aria-hidden=true&gt;The root is an artificial vertex. It’s&#xA;connected to each vertex in &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;V&lt;/em&gt;&lt;/span&gt;&#xA;by an edge weighted by the cost of listing all objects of that type. The&#xA;final row of vertices represents the association types we want to fetch.&#xA;An association vertex’s parents are the types at either end of the&#xA;association. Once you’ve paid the cost of listing Contacts, you have all&#xA;the associations involving contexts; that holds for any type, so all of&#xA;these edges are weighted 0.&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;The optimal request strategy is a subgraph spanning the blue terminal&#xA;vertices, the subgraph with the &lt;em&gt;smallest total edge weight&lt;/em&gt; that&#xA;does so. Think of it this way: the bottom row of vertices represents the&#xA;data we need to get to. We can either go through (list) Deals to get the&#xA;Contact↔︎Deal associations, or we have to go through (list) Contacts.&lt;p&gt;We’re looking for a &lt;a href=https://en.wikipedia.org/wiki/Steiner_tree_problem&gt;&lt;strong&gt;minimal&#xA;Steiner tree&lt;/strong&gt;&lt;/a&gt; in a directed graph — like a minimal spanning&#xA;tree, but free to ignore non-terminal vertices. The terminal vertices&#xA;are in blue above: the root and the desired association types.&lt;figure&gt;&lt;img src=../img/graphs-at-work/steiner-optimal.svg alt=&#34;The minimal Steiner tree for this simple example has weight 200.&#34;&gt;&lt;figcaption aria-hidden=true&gt;The minimal Steiner tree for this simple&#xA;example has weight 200.&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;Certain properties of these graphs make them a simpler than the&#xA;general class of Steiner tree problems (namely, their fixed height&#xA;guarantees that they’re pseudo-bipartite), but that doesn’t save it from&#xA;NP-hard status.&lt;blockquote&gt;&lt;p&gt;Computing minimum-cost &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;r&lt;/em&gt;&lt;/span&gt;-Steiner trees is NP-hard for &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;r&lt;/em&gt; ≥ 4&lt;/span&gt;, even if the underlying graph&#xA;is quasibipartite. The complexity status for &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;r&lt;/em&gt; = 3&lt;/span&gt; is unresolved, and the case&#xA;&lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;r&lt;/em&gt; = 2&lt;/span&gt; reduces to the&#xA;minimum-cost spanning tree problem.&lt;a href=#fn10 class=footnote-ref id=fnref10 role=doc-noteref&gt;&lt;sup&gt;10&lt;/sup&gt;&lt;/a&gt;&lt;/blockquote&gt;&lt;p&gt;I’ll spare you the details, but &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;r&lt;/em&gt;&lt;/span&gt; here is one plus the highest&#xA;degree of a vertex in &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;G&lt;/em&gt;&lt;/span&gt; —&#xA;three for this simple example case, and often greater than or equal to&#xA;four in practice.&lt;p&gt;Still, I wondered if there was some other relevant feature of these&#xA;graphs that simplifies the problem. I looked for a more familiar&#xA;representation, and found one: a &lt;a href=https://en.wikipedia.org/wiki/Set_cover_problem&gt;&lt;strong&gt;weighted&#xA;set cover&lt;/strong&gt;&lt;/a&gt;.&lt;p&gt;For brevity, I’ll use shorthand for the HubSpot types; instead of&#xA;Contact, Company, Deal, etc., I’ll write &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;A&lt;/em&gt;&lt;/span&gt;, &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;B&lt;/em&gt;&lt;/span&gt;, &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;C&lt;/em&gt;&lt;/span&gt;, and so on. You’re looking for a&#xA;number of associations specified by unique unordered pairs of those&#xA;types; for example, &lt;span class=&#34;math inline&#34;&gt;{(&lt;em&gt;A&lt;/em&gt;,&lt;em&gt;B&lt;/em&gt;), (&lt;em&gt;B&lt;/em&gt;,&lt;em&gt;C&lt;/em&gt;), (&lt;em&gt;B&lt;/em&gt;,&lt;em&gt;D&lt;/em&gt;), (&lt;em&gt;C&lt;/em&gt;,&lt;em&gt;D&lt;/em&gt;)}&lt;/span&gt;.&lt;/p&gt;&lt;p&gt;This set of pairs is the universe &lt;span class=&#34;math inline&#34;&gt;𝒰&lt;/span&gt;&#xA;to be covered. The sets doing the covering represent the endpoints we&#xA;can call: for each type, the associations in &lt;span class=&#34;math inline&#34;&gt;𝒰&lt;/span&gt; incident on that type:&lt;ul&gt;&lt;li&gt;&lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;A&lt;/em&gt;&lt;sub&gt;set&lt;/sub&gt; = {(&lt;em&gt;A&lt;/em&gt;,&lt;em&gt;B&lt;/em&gt;)}&lt;/span&gt;&lt;li&gt;&lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;B&lt;/em&gt;&lt;sub&gt;set&lt;/sub&gt; = {(&lt;em&gt;A&lt;/em&gt;,&lt;em&gt;B&lt;/em&gt;), (&lt;em&gt;B&lt;/em&gt;,&lt;em&gt;C&lt;/em&gt;), (&lt;em&gt;B&lt;/em&gt;,&lt;em&gt;D&lt;/em&gt;)}&lt;/span&gt;&lt;li&gt;&lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;C&lt;/em&gt;&lt;sub&gt;set&lt;/sub&gt; = {(&lt;em&gt;B&lt;/em&gt;,&lt;em&gt;C&lt;/em&gt;), (&lt;em&gt;C&lt;/em&gt;,&lt;em&gt;D&lt;/em&gt;)}&lt;/span&gt;&lt;li&gt;&lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;D&lt;/em&gt;&lt;sub&gt;set&lt;/sub&gt; = {(&lt;em&gt;C&lt;/em&gt;,&lt;em&gt;D&lt;/em&gt;)}&lt;/span&gt;&lt;/ul&gt;&lt;p&gt;These constitute a family &lt;span class=&#34;math inline&#34;&gt;𝒮&lt;/span&gt; of&#xA;subsets of &lt;span class=&#34;math inline&#34;&gt;𝒰&lt;/span&gt;. The set cover problem is&#xA;to find a minimal number of members in that family such that their union&#xA;spans &lt;span class=&#34;math inline&#34;&gt;𝒰&lt;/span&gt;, analogous to the optimal&#xA;HubSpot API strategy. In this case you can get all desired associations&#xA;by listing objects of two types: &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;B&lt;/em&gt;&lt;sub&gt;set&lt;/sub&gt;⋃&lt;em&gt;C&lt;/em&gt;&lt;sub&gt;set&lt;/sub&gt;&lt;/span&gt;&#xA;&lt;span class=&#34;math inline&#34;&gt; = {(&lt;em&gt;A&lt;/em&gt;,&lt;em&gt;B&lt;/em&gt;), (&lt;em&gt;B&lt;/em&gt;,&lt;em&gt;C&lt;/em&gt;), (&lt;em&gt;B&lt;/em&gt;,&lt;em&gt;D&lt;/em&gt;)}⋃{(&lt;em&gt;B&lt;/em&gt;,&lt;em&gt;C&lt;/em&gt;), (&lt;em&gt;C&lt;/em&gt;,&lt;em&gt;D&lt;/em&gt;)}&lt;/span&gt;&#xA;&lt;span class=&#34;math inline&#34;&gt; = {(&lt;em&gt;A&lt;/em&gt;,&lt;em&gt;B&lt;/em&gt;), (&lt;em&gt;B&lt;/em&gt;,&lt;em&gt;C&lt;/em&gt;), (&lt;em&gt;B&lt;/em&gt;,&lt;em&gt;D&lt;/em&gt;), (&lt;em&gt;C&lt;/em&gt;,&lt;em&gt;D&lt;/em&gt;)}&lt;/span&gt;&#xA;&lt;span class=&#34;math inline&#34;&gt; = 𝒰&lt;/span&gt;.&lt;p&gt;Bad news: finding a minimal set cover is NP-hard.&lt;p&gt;As you might imagine, the weighted set cover assigns a weight to each&#xA;member of &lt;span class=&#34;math inline&#34;&gt;𝒮&lt;/span&gt; and demands you minimize&#xA;the weights. For this problem, you’d assign a weight &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;A&lt;/em&gt;&lt;sub&gt;set&lt;/sub&gt;&lt;/span&gt; with the API cost of&#xA;listing all objects of type A… but don’t bother: solving&#xA;&lt;em&gt;weighted&lt;/em&gt; set cover isn’t any easier.&lt;p&gt;Interestingly, there are exceptions! Finding a minimum set cover on&#xA;2-sets is equivalent to finding a minimum edge cover, which &lt;em&gt;is&lt;/em&gt;&#xA;easier. Tugging on this thread&lt;a href=#fn11 class=footnote-ref id=fnref11 role=doc-noteref&gt;&lt;sup&gt;11&lt;/sup&gt;&lt;/a&gt; led me to discovering&#xA;the weighted vertex cover problem, which exactly matched my original&#xA;project definition.&lt;p&gt;That match confirmed it: any weighted vertex cover problem can be&#xA;expressed as a question about HubSpot associations, so optimizing this&#xA;HubSpot API strategy &lt;em&gt;is&lt;/em&gt;, in fact, NP-hard.&lt;/div&gt;&lt;aside id=footnotes class=&#34;footnotes footnotes-end-of-document&#34; role=doc-endnotes&gt;&lt;hr&gt;&lt;ol&gt;&lt;li id=fn1&gt;&lt;p&gt;A request for a page of Contacts, with Company and Deal&#xA;associations included, takes this form. Note the weird URL-encoding.&lt;div class=sourceCode id=cb1&gt;&lt;pre class=&#34;sourceCode bash&#34;&gt;&lt;code class=&#34;sourceCode bash&#34;&gt;&lt;span id=cb1-1&gt;&lt;a href=#cb1-1 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=ex&gt;curl&lt;/span&gt; &lt;span class=at&gt;--request&lt;/span&gt; GET &lt;span class=dt&gt;\&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb1-2&gt;&lt;a href=#cb1-2 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;  &lt;span class=at&gt;--url&lt;/span&gt; &lt;span class=st&gt;&amp;#39;https://api.hubapi.com/crm/v3/objects/contacts?limit=10&amp;amp;associations=contact&amp;amp;associations=deal&amp;amp;associations=company&amp;amp;archived=false&amp;#39;&lt;/span&gt; &lt;span class=dt&gt;\&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb1-3&gt;&lt;a href=#cb1-3 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;  &lt;span class=at&gt;--header&lt;/span&gt; &lt;span class=st&gt;&amp;#39;authorization: Bearer YOUR_ACCESS_TOKEN&amp;#39;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;a href=#fnref1 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn2&gt;&lt;p&gt;Zito, Michele. &lt;a href=https://cgi.csc.liv.ac.uk/~michele/TEACHING/COMP309/2005/Lec10.4.4.pdf&gt;“Vertex&#xA;Cover.”&lt;/a&gt; (2005). Also see Lavrov (2020).&lt;a href=#fnref2 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn3&gt;&lt;p&gt;Here’s an Elixir implementation of the greedy strategy&#xA;for finding &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;V&lt;/em&gt;&lt;sub&gt;req&lt;/sub&gt;&lt;/span&gt;:&lt;div class=sourceCode id=cb2&gt;&lt;pre class=&#34;sourceCode elixir&#34;&gt;&lt;code class=&#34;sourceCode elixir&#34;&gt;&lt;span id=cb2-1&gt;&lt;a href=#cb2-1 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=ot&gt;@spec&lt;/span&gt; greedy_V_req&lt;span class=fu&gt;(&lt;/span&gt;list&lt;span class=fu&gt;({&lt;/span&gt;&lt;span class=cn&gt;String&lt;/span&gt;&lt;span class=op&gt;.&lt;/span&gt;t&lt;span class=fu&gt;()&lt;/span&gt;, &lt;span class=cn&gt;String&lt;/span&gt;&lt;span class=op&gt;.&lt;/span&gt;t&lt;span class=fu&gt;()}))&lt;/span&gt; :: list&lt;span class=fu&gt;(&lt;/span&gt;&lt;span class=cn&gt;String&lt;/span&gt;&lt;span class=op&gt;.&lt;/span&gt;t&lt;span class=fu&gt;())&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb2-2&gt;&lt;a href=#cb2-2 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=kw&gt;def&lt;/span&gt; greedy_V_req&lt;span class=fu&gt;(&lt;/span&gt;&lt;span class=ot&gt;[]&lt;/span&gt;&lt;span class=fu&gt;)&lt;/span&gt;, &lt;span class=va&gt;do:&lt;/span&gt; &lt;span class=ot&gt;[]&lt;/span&gt; &lt;span class=co&gt;# Base case: no edges in G.&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb2-3&gt;&lt;a href=#cb2-3 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;/span&gt;&#xA;&lt;span id=cb2-4&gt;&lt;a href=#cb2-4 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=kw&gt;def&lt;/span&gt; greedy_V_req&lt;span class=fu&gt;(&lt;/span&gt;graph&lt;span class=fu&gt;)&lt;/span&gt; &lt;span class=kw&gt;do&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb2-5&gt;&lt;a href=#cb2-5 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;  &lt;span class=co&gt;# Greedily take the highest-degree vertex.&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb2-6&gt;&lt;a href=#cb2-6 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;  taken &lt;span class=op&gt;=&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb2-7&gt;&lt;a href=#cb2-7 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    graph&lt;/span&gt;&#xA;&lt;span id=cb2-8&gt;&lt;a href=#cb2-8 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    &lt;span class=op&gt;|&amp;gt;&lt;/span&gt; &lt;span class=cn&gt;Enum&lt;/span&gt;&lt;span class=op&gt;.&lt;/span&gt;flat_map&lt;span class=fu&gt;(&lt;/span&gt;&lt;span class=op&gt;&amp;amp;&lt;/span&gt;&lt;span class=cn&gt;Tuple&lt;/span&gt;&lt;span class=op&gt;.&lt;/span&gt;to_list&lt;span class=fu&gt;(&lt;/span&gt;&lt;span class=op&gt;&amp;amp;&lt;/span&gt;&lt;span class=dv&gt;1&lt;/span&gt;&lt;span class=fu&gt;))&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb2-9&gt;&lt;a href=#cb2-9 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    &lt;span class=op&gt;|&amp;gt;&lt;/span&gt; &lt;span class=cn&gt;Enum&lt;/span&gt;&lt;span class=op&gt;.&lt;/span&gt;frequencies&lt;span class=fu&gt;()&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb2-10&gt;&lt;a href=#cb2-10 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    &lt;span class=op&gt;|&amp;gt;&lt;/span&gt; &lt;span class=cn&gt;Enum&lt;/span&gt;&lt;span class=op&gt;.&lt;/span&gt;max_by&lt;span class=fu&gt;(&lt;/span&gt;&lt;span class=kw&gt;fn&lt;/span&gt; &lt;span class=fu&gt;{&lt;/span&gt;_k, freq&lt;span class=fu&gt;}&lt;/span&gt; &lt;span class=op&gt;-&amp;gt;&lt;/span&gt; freq &lt;span class=kw&gt;end&lt;/span&gt;&lt;span class=fu&gt;)&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb2-11&gt;&lt;a href=#cb2-11 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    &lt;span class=op&gt;|&amp;gt;&lt;/span&gt; elem&lt;span class=fu&gt;(&lt;/span&gt;&lt;span class=dv&gt;0&lt;/span&gt;&lt;span class=fu&gt;)&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb2-12&gt;&lt;a href=#cb2-12 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;/span&gt;&#xA;&lt;span id=cb2-13&gt;&lt;a href=#cb2-13 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;  &lt;span class=co&gt;# Remove taken from graph; recurse.&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb2-14&gt;&lt;a href=#cb2-14 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;  subgraph &lt;span class=op&gt;=&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb2-15&gt;&lt;a href=#cb2-15 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    graph&lt;/span&gt;&#xA;&lt;span id=cb2-16&gt;&lt;a href=#cb2-16 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    &lt;span class=op&gt;|&amp;gt;&lt;/span&gt; &lt;span class=cn&gt;Enum&lt;/span&gt;&lt;span class=op&gt;.&lt;/span&gt;filter&lt;span class=fu&gt;(&lt;/span&gt;&lt;span class=kw&gt;fn&lt;/span&gt; pair &lt;span class=op&gt;-&amp;gt;&lt;/span&gt; taken &lt;span class=kw&gt;not&lt;/span&gt; &lt;span class=kw&gt;in&lt;/span&gt; &lt;span class=cn&gt;Tuple&lt;/span&gt;&lt;span class=op&gt;.&lt;/span&gt;to_list&lt;span class=fu&gt;(&lt;/span&gt;pair&lt;span class=fu&gt;)&lt;/span&gt; &lt;span class=kw&gt;end&lt;/span&gt;&lt;span class=fu&gt;)&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb2-17&gt;&lt;a href=#cb2-17 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;/span&gt;&#xA;&lt;span id=cb2-18&gt;&lt;a href=#cb2-18 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;  &lt;span class=ot&gt;[&lt;/span&gt;taken &lt;span class=op&gt;|&lt;/span&gt; greedy_V_req&lt;span class=fu&gt;(&lt;/span&gt;subgraph&lt;span class=fu&gt;)&lt;/span&gt;&lt;span class=ot&gt;]&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb2-19&gt;&lt;a href=#cb2-19 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=kw&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;&amp;amp;greedy_V_req/1&lt;/code&gt; receives &lt;code&gt;graph&lt;/code&gt; as an&#xA;adjacency list of order-agnostic tuples. To specify the example&#xA;graph:&lt;div class=sourceCode id=cb3&gt;&lt;pre class=&#34;sourceCode elixir&#34;&gt;&lt;code class=&#34;sourceCode elixir&#34;&gt;&lt;span id=cb3-1&gt;&lt;a href=#cb3-1 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;greedy_V_req&lt;span class=fu&gt;(&lt;/span&gt;&lt;span class=ot&gt;[&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb3-2&gt;&lt;a href=#cb3-2 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;  &lt;span class=fu&gt;{&lt;/span&gt;&lt;span class=st&gt;&amp;quot;Contact&amp;quot;&lt;/span&gt;, &lt;span class=st&gt;&amp;quot;Company&amp;quot;&lt;/span&gt;&lt;span class=fu&gt;}&lt;/span&gt;,&lt;/span&gt;&#xA;&lt;span id=cb3-3&gt;&lt;a href=#cb3-3 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;  &lt;span class=fu&gt;{&lt;/span&gt;&lt;span class=st&gt;&amp;quot;Contact&amp;quot;&lt;/span&gt;, &lt;span class=st&gt;&amp;quot;Deal&amp;quot;&lt;/span&gt;&lt;span class=fu&gt;}&lt;/span&gt;,&lt;/span&gt;&#xA;&lt;span id=cb3-4&gt;&lt;a href=#cb3-4 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;  &lt;span class=fu&gt;{&lt;/span&gt;&lt;span class=st&gt;&amp;quot;Deal&amp;quot;&lt;/span&gt;, &lt;span class=st&gt;&amp;quot;Company&amp;quot;&lt;/span&gt;&lt;span class=fu&gt;}&lt;/span&gt;,&lt;/span&gt;&#xA;&lt;span id=cb3-5&gt;&lt;a href=#cb3-5 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;  &lt;span class=fu&gt;{&lt;/span&gt;&lt;span class=st&gt;&amp;quot;Policy&amp;quot;&lt;/span&gt;, &lt;span class=st&gt;&amp;quot;Deal&amp;quot;&lt;/span&gt;&lt;span class=fu&gt;}&lt;/span&gt;,&lt;/span&gt;&#xA;&lt;span id=cb3-6&gt;&lt;a href=#cb3-6 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;  &lt;span class=fu&gt;{&lt;/span&gt;&lt;span class=st&gt;&amp;quot;Policy&amp;quot;&lt;/span&gt;, &lt;span class=st&gt;&amp;quot;Company&amp;quot;&lt;/span&gt;&lt;span class=fu&gt;}&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb3-7&gt;&lt;a href=#cb3-7 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=ot&gt;]&lt;/span&gt;&lt;span class=fu&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This implementation is inefficient — &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;O&lt;/em&gt;(|&lt;em&gt;V&lt;/em&gt;|&lt;sup&gt;2&lt;/sup&gt;)&lt;/span&gt; — but it&#xA;isn’t worth optimizing.&lt;a href=#fnref3 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn4&gt;&lt;p&gt;Lavrov, Mikhail. &lt;a href=https://faculty.math.illinois.edu/~mlavrov/docs/482-spring-2020/lecture36.pdf&gt;“Lecture&#xA;36: Approximation Algorithms”&lt;/a&gt; (2020). 1-2.&lt;a href=#fnref4 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn5&gt;&lt;p&gt;Zito, Michele. &lt;a href=https://cgi.csc.liv.ac.uk/~michele/TEACHING/COMP309/2005/Lec10.4.4.pdf&gt;“Vertex&#xA;Cover.”&lt;/a&gt; (2005). Also see Lavrov (2020).&lt;a href=#fnref5 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn6&gt;&lt;p&gt;Savage, Carla. &lt;a href=https://www.sciencedirect.com/science/article/abs/pii/0020019082900229&gt;“Depth-first&#xA;search and the vertex cover problem.”&lt;/a&gt; &lt;em&gt;Information processing&#xA;letters&lt;/em&gt; 14, no. 5 (1982): 233-235. A very tidy three pages.&lt;a href=#fnref6 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn7&gt;&lt;p&gt;Vazirani, Vijay V. &lt;a href=https://doc.lagout.org/science/0_Computer%20Science/2_Algorithms/Approximation%20Algorithms%20%5BVazirani%202010-12-01%5D.pdf&gt;&lt;em&gt;Approximation&#xA;Algorithms.&lt;/em&gt;&lt;/a&gt; Vol. 1. Berlin: Springer, 2001. My adaptation&#xA;renames &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;C&lt;/em&gt;&lt;/span&gt; to &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;V&lt;/em&gt;&lt;sub&gt;req&lt;/sub&gt;&lt;/span&gt; for consistency with&#xA;earlier examples, and removes steps updating &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;c&lt;/em&gt;(&lt;em&gt;e&lt;/em&gt;)&lt;/span&gt; — our problem doesn’t&#xA;require we track edge “costs,” part of Vazirani’s exercise.&lt;a href=#fnref7 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn8&gt;&lt;p&gt;I want to test this on random graphs. For now, consider&#xA;the strategy Vazirani’s algorithm picks for this simple weighted&#xA;graph:&lt;div class=smallfig&gt;&lt;figure&gt;&lt;img src=../img/graphs-at-work/simple-weighted-varizani.svg alt=&#34;Vazirani’s algorithm lists all three types, even though, once you’ve listed any pair of types, listing the third is strictly redundant.&#34;&gt;&lt;figcaption aria-hidden=true&gt;Vazirani’s algorithm lists &lt;em&gt;all&#xA;three&lt;/em&gt; types, even though, once you’ve listed any pair of types,&#xA;listing the third is strictly redundant.&lt;/figcaption&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;Notably, this four-request solution is still a 2-approximation of the&#xA;optimal 2-request solution from running clever-greedy on the unweighted&#xA;graph. You could tweak Vazirani’s last step (“Include in &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;C&lt;/em&gt;&lt;/span&gt; all vertices having &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;t&lt;/em&gt;(&lt;em&gt;v&lt;/em&gt;) = 0&lt;/span&gt;”) to guard against&#xA;trivially-unnecessary additions like this.&lt;p&gt;Counterintuitively, one 2-approximation algorithm for the unweighted&#xA;set cover &lt;em&gt;intentionally&lt;/em&gt; includes vertices which redundantly&#xA;cover a given edge; doing so thwarts the clever-greedy adversarial&#xA;case.&lt;a href=#fnref8 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn9&gt;&lt;p&gt;Lehman, Meir M. “Programs, life cycles, and laws of&#xA;software evolution.” &lt;em&gt;Proceedings of the IEEE&lt;/em&gt; 68, no. 9 (1980):&#xA;1060-1076.&lt;a href=#fnref9 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn10&gt;&lt;p&gt;Könemann, Jochen, David Pritchard, and Kunlun Tan. &lt;a href=https://arxiv.org/pdf/0712.3568.pdf&gt;“A partition-based relaxation&#xA;for Steiner trees.”&lt;/a&gt; &lt;em&gt;Mathematical Programming&lt;/em&gt; 127, no. 2&#xA;(2011): 345-370. The introduction to this paper is the clearest overview&#xA;of Steiner trees I found.&lt;a href=#fnref10 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn11&gt;&lt;p&gt;Thanks to &lt;a href=https://cs.stackexchange.com/users/9550/david-richerby&gt;David&#xA;Richerby&lt;/a&gt; for answering &lt;a href=https://cs.stackexchange.com/q/77579&gt;“Set cover problem with sets&#xA;of size 2”&lt;/a&gt; (July 5, 2017).&lt;a href=#fnref11 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;/ol&gt;&lt;/aside&gt;</content>
    <link href="https://lukasschwab.me/blog/gen/graphs-at-work.html" rel="alternate"></link>
    <summary type="html">Last week at work I found a wicked graph problem at the heart of an innocuous API task. This post explains the problem, its relationship to several well-studied discrete math puzzles, and comments on the difference between computer science and software engineering.</summary>
  </entry>
  <entry>
    <title>Technical Writing for Sequin</title>
    <updated>2022-08-27T00:00:00Z</updated>
    <id>https://lukasschwab.me/blog/gen/sequin-technical-writing.html</id>
    <content type="html">&lt;html xmlns=http://www.w3.org/1999/xhtml lang xml:lang&gt;&lt;meta charset=utf-8&gt;&lt;meta name=generator content=&#34;pandoc&#34;&gt;&lt;meta name=viewport content=&#34;width=device-width,initial-scale=1,user-scalable=yes&#34;&gt;&lt;meta name=dcterms.date content=&#34;2022-08-27&#34;&gt;&lt;title&gt;Technical Writing for Sequin&lt;/title&gt;&lt;style&gt;&#xA;      code{white-space: pre-wrap;}&#xA;      span.smallcaps{font-variant: small-caps;}&#xA;      span.underline{text-decoration: underline;}&#xA;      div.column{display: inline-block; vertical-align: top; width: 50%;}&#xA;          &lt;/style&gt;&lt;link rel=stylesheet href=../styles/common.css&gt;&lt;script data-goatcounter=https://lukasschwab.goatcounter.com/count async src=//gc.zgo.at/count.js&gt;&lt;/script&gt;&lt;a href=../index.html&gt;&amp;#171; blog&lt;/a&gt; &lt;span class=date&gt;2022-08-27&lt;/span&gt;&lt;header id=title-block-header&gt;&lt;h1 class=title&gt;Technical Writing for Sequin&lt;/h1&gt;&lt;/header&gt;&lt;p&gt;I joined &lt;a href=https://www.sequin.io/&gt;Sequin&lt;/a&gt; earlier this&#xA;year. I joined in part because they promised I could wear many hats;&#xA;they’re true to their word, and I’m moonlighting in content&#xA;marketing!&lt;p&gt;I’m focused on improving two aspects of my technical writing:&lt;ul&gt;&lt;li&gt;Writing conversationally. My usual written style’s a little stilted,&#xA;but someone’s more likely to read your marketing blog if you seem, uh,&#xA;&lt;em&gt;cool.&lt;/em&gt;&lt;li&gt;Explaining visually. &lt;a href=https://whimsical.com/&gt;Whimsical&lt;/a&gt;&#xA;is a solid diagram editor; I’m using it outside of work.&lt;/ul&gt;&lt;p&gt;See &lt;a href=./blogging-with-tikz&gt;Blogging with TikZ&lt;/a&gt;, &lt;a href=borges-automata.html&gt;State Machines via Jorge Luis Borges&lt;/a&gt;,&#xA;and &lt;a href=./cloud-function-pdf-processing&gt;Processing PDFs with Cloud&#xA;Functions&lt;/a&gt; for examples of (loosely) technical writing outside my&#xA;work.&lt;p&gt;I’ll keep this list up-to-date instead of cross-posting articles.&lt;hr&gt;&lt;dl&gt;&lt;dt&gt;&lt;a href=https://blog.sequin.io/what-to-do-without-hubspot-webhooks/&gt;What&#xA;to do without HubSpot Webhooks&lt;/a&gt;&lt;dd&gt;Published July 11, 2022.&lt;dd&gt;A review of shortcomings in HubSpot’s CRM Webhooks API. Webhooks&#xA;&lt;em&gt;should&lt;/em&gt; be a neat system for triggering event-driven tasks&#xA;outside HubSpot, but these have a number of shortcomings. So does&#xA;HubSpot’s low-code automation system, a commonly-recommended&#xA;alternative.&lt;dt&gt;&lt;a href=https://blog.sequin.io/stripe-metered-billing-simplified/&gt;Stripe&#xA;metered billing, simplified&lt;/a&gt;&lt;dd&gt;Published August 26, 2022.&lt;dd&gt;Stripe’s metered billing strategy lets you bill customers according to&#xA;how much they use your product. That should let you tether your billing&#xA;to the value your product provides, at the cost of requiring you&#xA;constantly report cusotmer usage to Stripe. Reading Stripe data through&#xA;Sequin saves an API call in each reporting cycle.&lt;/dl&gt;</content>
    <link href="https://lukasschwab.me/blog/gen/sequin-technical-writing.html" rel="alternate"></link>
    <summary type="html">An index of writing I publish as part of my work on Sequin --- a data integration tool for software engineers --- usually on Sequin&#39;s blog. I&#39;ll keep this list up-to-date instead of cross-posting articles.</summary>
  </entry>
  <entry>
    <title>Managing Emails from Recruiters</title>
    <updated>2022-07-06T00:00:00Z</updated>
    <id>https://lukasschwab.me/blog/gen/recruiting-emails.html</id>
    <content type="html">&lt;html xmlns=http://www.w3.org/1999/xhtml lang xml:lang&gt;&lt;meta charset=utf-8&gt;&lt;meta name=generator content=&#34;pandoc&#34;&gt;&lt;meta name=viewport content=&#34;width=device-width,initial-scale=1,user-scalable=yes&#34;&gt;&lt;meta name=author content=&#34;Lukas Schwab&#34;&gt;&lt;meta name=dcterms.date content=&#34;2022-07-06&#34;&gt;&lt;title&gt;Managing Emails from Recruiters&lt;/title&gt;&lt;style&gt;&#xA;      code{white-space: pre-wrap;}&#xA;      span.smallcaps{font-variant: small-caps;}&#xA;      span.underline{text-decoration: underline;}&#xA;      div.column{display: inline-block; vertical-align: top; width: 50%;}&#xA;          &lt;/style&gt;&lt;link rel=stylesheet href=../styles/common.css&gt;&lt;link rel=stylesheet href=../styles/fence-extensions/frame.css&gt;&lt;script data-goatcounter=https://lukasschwab.goatcounter.com/count async src=//gc.zgo.at/count.js&gt;&lt;/script&gt;&lt;a href=../index.html&gt;&amp;#171; blog&lt;/a&gt; &lt;span class=date&gt;2022-07-06&lt;/span&gt;&lt;header id=title-block-header&gt;&lt;h1 class=title&gt;Managing Emails from Recruiters&lt;/h1&gt;&lt;/header&gt;&lt;p&gt;I have a GitHub profile, so I’m doomed to receive emails from tech&#xA;recruiters until the end of time.&lt;p&gt;Until recently, I just archived emails that didn’t interest me. An&#xA;unfortunate proportion of the recruiters take this as an invitation to&#xA;email me again (and again) to ask if I’ve seen the first. This is a bad&#xA;strategy on their part.&lt;p&gt;I’ve taken to sending a short, generic &lt;a href=&#34;https://support.google.com/a/users/answer/9308990?hl=en&#34;&gt;template&#xA;email&lt;/a&gt; explicitly declining their invitations:&lt;blockquote&gt;&lt;p&gt;Hello––thanks for reaching out!&lt;p&gt;I’m not looking for new job opportunities at this time, but I’ve read&#xA;and labeled your email so I can find it if my priorities change.&lt;p&gt;Best of luck in your search,&lt;br&gt;Lukas&lt;br&gt;&lt;span style=color:white;-webkit-text-stroke-width:0.3px;-webkit-text-stroke-color:grey;&gt;labelRecruiting&lt;/span&gt;&lt;/blockquote&gt;&lt;p&gt;“Read &lt;em&gt;and labeled?&lt;/em&gt;” Sometimes it really is useful to have&#xA;these emails categorized:&lt;ul&gt;&lt;li&gt;When you’re selling a SaaS product.&lt;li&gt;When you’re looking for examples of strong/weak recruiting&#xA;emails.&lt;li&gt;When you’re hiring for an effective recruiter.&lt;li&gt;When (if!) you really do become interested in the advertised&#xA;role.&lt;/ul&gt;&lt;p&gt;As you might’ve noticed, I include a line in white text at the end of&#xA;my template: &lt;code&gt;labelRecruiting&lt;/code&gt;.&lt;p&gt;This is the crux of my trick: &lt;em&gt;Gmail filters are evaluated for&#xA;outgoing mail.&lt;/em&gt; I set up a filter on&#xA;&lt;code&gt;label:sent labelRecruiting&lt;/code&gt; that applies the label&#xA;&lt;kbd&gt;Recruiting&lt;/kbd&gt;.&lt;div class=frame&gt;&lt;figure&gt;&lt;img src=../img/recruiting-emails/filter.jpg alt=&#34;Image of the Gmail query panel: label:sent captures all sent mail, and labelRecruiting searches email text for that token.&#34;&gt;&lt;figcaption aria-hidden=true&gt;Image of the Gmail query panel:&#xA;&lt;code&gt;label:sent&lt;/code&gt; captures all sent mail, and&#xA;&lt;code&gt;labelRecruiting&lt;/code&gt; searches email text for that&#xA;token.&lt;/figcaption&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;It takes two clicks to fill the template response. When an inbound&#xA;email is especially good or especially off-base, I add a short note to&#xA;that effect before sending. The filter notices the lazily-hidden keyword&#xA;and applies my label.&lt;p&gt;As much as it chafes to dignify plainly spammy recruiters with a&#xA;response, replying does usually prevent pushy follow-ups. The underlying&#xA;mechanism — using a hidden string to trigger filter behavior — should be&#xA;useful whenever you need to bulk reply-and-categorize. Let me know if&#xA;you think of something clever!</content>
    <link href="https://lukasschwab.me/blog/gen/recruiting-emails.html" rel="alternate"></link>
    <summary type="html">You can set up a Gmail filter that evaluates for outgoing mail. Hiding a keyword in a template response is a neat time-saver for cold outreach from recruiters.</summary>
  </entry>
  <entry>
    <title>Tech Debt and Taxes</title>
    <updated>2022-03-14T00:00:00Z</updated>
    <id>https://lukasschwab.me/blog/gen/technical-debt.html</id>
    <content type="html">&lt;html xmlns=http://www.w3.org/1999/xhtml lang xml:lang&gt;&lt;meta charset=utf-8&gt;&lt;meta name=generator content=&#34;pandoc&#34;&gt;&lt;meta name=viewport content=&#34;width=device-width,initial-scale=1,user-scalable=yes&#34;&gt;&lt;meta name=dcterms.date content=&#34;2022-03-14&#34;&gt;&lt;title&gt;Tech Debt and Taxes&lt;/title&gt;&lt;style&gt;&#xA;      code{white-space: pre-wrap;}&#xA;      span.smallcaps{font-variant: small-caps;}&#xA;      span.underline{text-decoration: underline;}&#xA;      div.column{display: inline-block; vertical-align: top; width: 50%;}&#xA;          &lt;/style&gt;&lt;link rel=stylesheet href=../styles/common.css&gt;&lt;link rel=stylesheet href=../styles/fence-extensions/addendum.css&gt;&lt;script data-goatcounter=https://lukasschwab.goatcounter.com/count async src=//gc.zgo.at/count.js&gt;&lt;/script&gt;&lt;a href=../index.html&gt;&amp;#171; blog&lt;/a&gt; &lt;span class=date&gt;2022-03-14&lt;/span&gt;&lt;header id=title-block-header&gt;&lt;h1 class=title&gt;Tech Debt and Taxes&lt;/h1&gt;&lt;/header&gt;&lt;p&gt;In &lt;a href=https://increment.com/planning/reframing-tech-debt/&gt;Reframing&#xA;Tech Debt&lt;/a&gt;, Leemay Nassery suggests framing technical housekeeping&#xA;positively — as building “tech wealth” — to convince business and&#xA;product stakeholders it’s worthwhile.&lt;p&gt;If I were one of those stakeholders, a self-professed&#xA;customer-obsessive, I’d balk (rightfully!) at the idea of a secondary&#xA;system of value independent from the user experience. “What does ‘tech&#xA;wealth’ mean for a customer?” Fair question!&lt;p&gt;I think a negative framing is more useful here, but Nassery’s&#xA;suggestion that, say, refactoring a program yields a &lt;em&gt;resource to&#xA;reinvest&lt;/em&gt; gets at the issue with calling it “debt.” Technical “debt”&#xA;sounds like a fixed cost that can be deferred; more accurately, we&#xA;should say debt exacts an ongoing &lt;em&gt;tax&lt;/em&gt; from go-to-market teams.&#xA;The costs of quick development may be hidden, but they aren’t&#xA;externalized.&lt;p&gt;If you make implementation shortcuts in the name of product velocity,&#xA;but you don’t have the discipline to clean them up, those shortcuts will&#xA;eventually be counterproductive to the speed-of-delivery you hoped to&#xA;prioritize. Changes to a codebase are more often integrative than&#xA;strictly additive: they have to contend with the code that’s &lt;em&gt;already&#xA;there.&lt;/em&gt; Writing sloppy code today means writing code slowly&#xA;tomorrow.&lt;ul&gt;&lt;li&gt;&lt;p&gt;Imagine a minor update to to a webhook implementation. If the&#xA;webhook’s baseline behavior is undocumented (tech debt!), or its input&#xA;validation is rickety (tech debt!), verifying the change is a herculean&#xA;task of either retroactively establishing that baseline with unit tests&#xA;or, worse, manually testing sample inputs. Even though the update’s&#xA;minor and implemented well, the QA process spends time on the multitude&#xA;of poorly-implemented cases just because it &lt;em&gt;modifies&lt;/em&gt;&#xA;debt-addled code.&lt;li&gt;&lt;p&gt;Imagine you want to release a new billing page to admin users. If&#xA;the permissions module (which canonically establishes whether a given&#xA;user is an admin) has an unclear interface, your engineer’s more likely&#xA;to initially misuse it. Either they’ll catch the mistake, then burn time&#xA;debugging it, or you’ll have a product permissions bug in production.&#xA;Either way, a dark cloud over what should be a joyous feature release,&#xA;all because it &lt;em&gt;uses&lt;/em&gt; debt-addled code.&lt;li&gt;&lt;p&gt;Imagine you’re testing a new feature, and you want to release it&#xA;to a beta testing group behind a feature flag. Your frontend engineer&#xA;has added &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;n&lt;/em&gt;&lt;/span&gt; feature flags&#xA;already; why’s this one taking them so long? Maybe they’re stuck testing&#xA;&lt;span class=&#34;math inline&#34;&gt;2&lt;sup&gt;&lt;em&gt;n&lt;/em&gt; + 1&lt;/sup&gt;&lt;/span&gt; possible&#xA;configurations because your flagged features are mutually-dependent.&lt;a href=#fn1 class=footnote-ref id=fnref1 role=doc-noteref&gt;&lt;sup&gt;1&lt;/sup&gt;&lt;/a&gt; Yikes!&lt;/ul&gt;&lt;ul&gt;&lt;li&gt;Even unused and unmodified code taxes productivity. If you have a&#xA;deprecated service, changes to its dependencies &lt;em&gt;still&lt;/em&gt; require&#xA;changes in the disused service! At its most insidious, this dynamic&#xA;prevents you from improving joint dependencies with the highest-value&#xA;parts of your product, where inefficiencies are &lt;em&gt;really&lt;/em&gt;&#xA;costly.&lt;/ul&gt;&lt;p&gt;In each of these cases, tech debt’s the common enemy of the product&#xA;stakeholder and even the most navel-gazing, customer-indifferent&#xA;engineer. While the underlying debt isn’t addressed, the stakeholder&#xA;pays a tax — as a delay, or as systematic under-verification that’ll&#xA;eventually yield bugs.&lt;p&gt;This dynamic is particular to API and product debt — unnecessary&#xA;complexity in how parts of the system interact with each other&#xA;(interface and data model design, documentation, and testing practices)&#xA;or the space of customer states (feature flags and billing). These forms&#xA;of debt tax new projects in proportion to those projects’ complexity;&#xA;big projects and radical changes in product behavior are proportionally&#xA;punished. Of course, not all “tech debt” should be prioritized this way;&#xA;poor database performance might usually be considered debt, but it’s&#xA;less likely than an unexpressive API to derail your next feature.&lt;p&gt;The usual refrain — including in Nassery’s article — is to&#xA;preallocate blocks of time for building tech wealth. The “tax”&#xA;translation from tech into customer impact should let a team prioritize&#xA;&lt;em&gt;specific&lt;/em&gt; tech debt initiatives, according to the same criteria&#xA;as features.&lt;a href=#fn2 class=footnote-ref id=fnref2 role=doc-noteref&gt;&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt;&lt;p&gt;Unsurprisingly, it falls to engineers and their managers to use this&#xA;framing device effectively.&lt;ul&gt;&lt;li&gt;&lt;p&gt;If an element of tech debt really can’t be translated to customer&#xA;impact, even when taking tech tax into consideration, deprioritize&#xA;it.&lt;li&gt;&lt;p&gt;Keep a shared list of known issues. This can be simple: a short&#xA;name, a quick description, and a way to voice agreement.&lt;li&gt;&lt;p&gt;Measure tech tax while you work. How much time did you burn&#xA;grappling with tech debt? Dealing with unexpected integration or testing&#xA;issues? Associate the tax with an underlying issue (“known-bad&#xA;permission API: one day”) and aggregate costs across projects.&lt;li&gt;&lt;p&gt;Keep engineering leaders abreast of implementation details and&#xA;the stumbling blocks therein. If individually-contributing engineers&#xA;aren’t involved in early planning, it’s a manager’s or tech lead’s&#xA;responsibility to explain the relationship between tech debt and their&#xA;team’s productivity.&lt;li&gt;&lt;p&gt;If the best you can do is preallocate blocks, try to allocate&#xA;time &lt;em&gt;early&lt;/em&gt; in the planning period rather than at the very end!&#xA;An allocation is easier to justify if it yields improvements in the same&#xA;iteration. Leave enough time before the quarterly retrospective that you&#xA;have something to show for your efforts.&lt;/ul&gt;&lt;p&gt;Business and product stakeholders can take some proactive action.&lt;ul&gt;&lt;li&gt;&lt;p&gt;Clean up after experiments. Suppose you test a new billing model&#xA;with a handful of customers, but ultimately decide against it. If you&#xA;roll those customers into one of the pre-existing tiers, engineers can&#xA;remove the experimental code to leave billing as simple and inspectable&#xA;as when the experiment started, and customers love free&#xA;upgrades!&lt;li&gt;&lt;p&gt;Negotiate firm sunset timelines for deprecated features. Ideally,&#xA;keep them short. Ensure users who postdate a feature’s deprecation won’t&#xA;start to depend on it; usage should dwindle before feature support&#xA;ends.&lt;li&gt;&lt;p&gt;Practice empathy: engage your engineers over &lt;em&gt;why&lt;/em&gt; — not&#xA;just whether — a project runs late or turns out buggy. If you spot&#xA;common issues, speak up! Bell Labs researcher Richard Hamming &lt;a href=https://www.cs.virginia.edu/~robins/YouAndYourResearch.html&gt;credits&#xA;scientists&lt;/a&gt; in &lt;em&gt;other&lt;/em&gt; departments with solving his resourcing&#xA;struggles.&lt;blockquote&gt;&lt;p&gt;Every time I had to tell some scientist in some other area, “No I&#xA;can’t; I haven’t the machine capacity,” he complained. I said “Go tell&#xA;&lt;em&gt;your&lt;/em&gt; Vice President that Hamming needs more computing&#xA;capacity.” After a while I could see what was happening up there at the&#xA;top; many people said to my Vice President, “Your man needs more&#xA;computing capacity.” I got it!&lt;/blockquote&gt;&lt;p&gt;This should help you hit your targets, and it’ll &lt;em&gt;definitely&lt;/em&gt;&#xA;make you friends.&lt;/ul&gt;&lt;p&gt;The issue with “tech debt” and “tech wealth” is that they&#xA;underemphasize software’s impact on a company’s agility, its ongoing&#xA;ability to adapt to serve its users’ needs. Without recourse to customer&#xA;value, advocating against technical debt means advocating for a separate&#xA;and competing system of value, something business and product&#xA;stakeholders are right to treat with skepticism.&lt;p&gt;Emphasis on the productivity impact makes technical debt expressible&#xA;in the &lt;em&gt;primary&lt;/em&gt; system of value, where it’s comparable against —&#xA;and can be prioritized over — any other company initiative.&lt;div class=addendum data-date=&#34;March 30, 2022&#34;&gt;&lt;p&gt;&lt;em&gt;Process Plants: A Handbook for Inherently Safer Design&lt;/em&gt;&#xA;(Kletz 1998) reminded me of this essay. A process plant designer can&#xA;intervene to prevent hazards (e.g. a “snakepit” plant layout), but&#xA;investors don’t value hazard mitigation for its own sake. Quoting&#xA;Kirkland,&lt;blockquote&gt;&lt;p&gt;This cannot all be the fault of the “money men” and their failure to&#xA;understand us. It is often much more our inability to express ourselves&#xA;in a language they can understand.&lt;a href=#fn3 class=footnote-ref id=fnref3 role=doc-noteref&gt;&lt;sup&gt;3&lt;/sup&gt;&lt;/a&gt;&lt;/blockquote&gt;&lt;p&gt;I’m more optimistic than Kirkland. I think the “money” stakeholders&#xA;&lt;em&gt;do&lt;/em&gt; understand hazards, but that they’re better-equipped to&#xA;predict and prioritize money. To that end, Kletz offers napkin math:&lt;blockquote&gt;&lt;p&gt;Remember that if it costs $1 to fix a problem at the conceptual&#xA;stage, it will cost&lt;ul&gt;&lt;li&gt;about $10 at the flowsheet stage,&lt;li&gt;about $100 at the line diagram stage,&lt;li&gt;about $1000 after the plant is built,&lt;li&gt;about $10,000 to clean up after an accident.&lt;a href=#fn4 class=footnote-ref id=fnref4 role=doc-noteref&gt;&lt;sup&gt;4&lt;/sup&gt;&lt;/a&gt;&lt;/ul&gt;&lt;/blockquote&gt;&lt;p&gt;Software could use some equivalent with costs in various stages of&#xA;planning, development, and deployment. “Hazard” may be a better analogy&#xA;than “tax” for describing the running cost of technical debt.&lt;/div&gt;&lt;div class=addendum data-date=&#34;April 11, 2023&#34;&gt;&lt;p&gt;Jamie Brandon discusses technical debt &lt;a href=https://www.scattered-thoughts.net/writing/complexity-budgets/&gt;in&#xA;terms of a team’s “complexity budget.”&lt;/a&gt; That post argues complexity&#xA;compounds rather than exacting a tax:&lt;blockquote&gt;&lt;p&gt;Complexity limits how much of the system can fit into the heads of&#xA;the developers, and in doing so breeds more complexity. Every time you&#xA;are forced to do something ugly in one place because of existing&#xA;ugliness in another place you are feeling this cost.&lt;/blockquote&gt;&lt;p&gt;Moreover, complexity isn’t &lt;em&gt;linearly&lt;/em&gt; costly: at certain&#xA;thresholds, a small complication may dramatically impact your&#xA;effectiveness. Conversely, when the team is small — maybe it’s just you!&#xA;— and the program is simple, complexity &lt;em&gt;seems&lt;/em&gt; free. That’s what&#xA;makes technical debt so pernicious!&lt;blockquote&gt;&lt;p&gt;Worse, there are cliffs in the cost. As soon as a particular&#xA;subsystem cannot fit into the head of a single developer there are huge&#xA;additional overheads for communication. Opportunities for improvements&#xA;or simplification are missed because no one person can see all the parts&#xA;of the problem.&lt;/blockquote&gt;&lt;/div&gt;&lt;div class=addendum data-date=&#34;June 17, 2023&#34;&gt;&lt;p&gt;In hindsight, “interest” is a better extension of the debt analogy&#xA;than “tax.” Avery Pennarun &lt;a href=https://apenwarr.ca/log/20230605&gt;follows that terminology to&#xA;extend the metaphor even further&lt;/a&gt;: differentiating between high- and&#xA;low-interest technical debt, describing debt-income ratios (a better&#xA;formulation than my sketchy footnote calculus), and so on.&lt;/div&gt;&lt;aside id=footnotes class=&#34;footnotes footnotes-end-of-document&#34; role=doc-endnotes&gt;&lt;hr&gt;&lt;ol&gt;&lt;li id=fn1&gt;&lt;p&gt;Take the time to keep your feature flags mutually&#xA;independent, even if that means running migrations to split or merge&#xA;flags; &lt;a href=https://kevinmahoney.co.uk/articles/applying-misu/&gt;make&#xA;invalid states unrepresentable&lt;/a&gt;. If you don’t, each new flag&#xA;effectively &lt;em&gt;doubles&lt;/em&gt; the user interface surface you design and&#xA;test.&lt;a href=#fnref1 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn2&gt;&lt;p&gt;Development acceleration should have a double-integral&#xA;relationship to the value delivered by new development, but expressing&#xA;this accurately would require some complex discounting.&lt;p&gt;Imagine you can divide your fixed engineering time in a quarter&#xA;between &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;t&lt;/em&gt;&lt;sub&gt;debt&lt;/sub&gt;&lt;/span&gt; and&#xA;&lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;t&lt;/em&gt;&lt;sub&gt;feat&lt;/sub&gt;&lt;/span&gt;. Customer&#xA;value delivered looks something like &lt;span class=&#34;math inline&#34;&gt;(&lt;em&gt;v&lt;/em&gt;+&lt;em&gt;t&lt;/em&gt;&lt;sub&gt;debt&lt;/sub&gt;) ⋅ &lt;em&gt;t&lt;/em&gt;&lt;sub&gt;feat&lt;/sub&gt;&lt;/span&gt;,&#xA;where &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;v&lt;/em&gt;&lt;/span&gt; is your initial&#xA;product velocity. As &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;t&lt;/em&gt;&lt;sub&gt;feat&lt;/sub&gt;&lt;/span&gt; approaches zero,&#xA;the team spends all of its time preparing to deliver value… but none of&#xA;its time actually doing so.&lt;p&gt;This model is short several coefficients, but hopefully you get the&#xA;idea.&lt;a href=#fnref2 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn3&gt;&lt;p&gt;Kirkland, C. J. &lt;em&gt;The Channel Tunnel: Engineering&#xA;under Private Finance — Innovation or Frustration&lt;/em&gt; (London:&#xA;Fellowship of Engineering, 1989). Quoted in Kletz, page 157.&lt;a href=#fnref3 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn4&gt;&lt;p&gt;Kletz, Trevor. &lt;em&gt;Process plants: A handbook for&#xA;inherently safer design.&lt;/em&gt; CRC Press, 2010. Page 165. For specific&#xA;examples of how “chang[ing] early” saves money, see page 202.&lt;a href=#fnref4 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;/ol&gt;&lt;/aside&gt;</content>
    <link href="https://lukasschwab.me/blog/gen/technical-debt.html" rel="alternate"></link>
    <summary type="html">An argument for highlighting the customer cost, via product unreliability and feature delays, of technical debt. Getting the time to clean up old bodges is a matter of revealing the constant but oft-hidden productivity tax of leaving them in place.</summary>
  </entry>
  <entry>
    <title>Collected Memories of Evans Hall</title>
    <updated>2022-02-13T00:00:00Z</updated>
    <id>https://lukasschwab.me/blog/gen/memories-of-evans-hall.html</id>
    <content type="html">&lt;html xmlns=http://www.w3.org/1999/xhtml lang xml:lang&gt;&lt;meta charset=utf-8&gt;&lt;meta name=generator content=&#34;pandoc&#34;&gt;&lt;meta name=viewport content=&#34;width=device-width,initial-scale=1,user-scalable=yes&#34;&gt;&lt;meta name=author content=&#34;Lukas Schwab&#34;&gt;&lt;meta name=dcterms.date content=&#34;2022-02-13&#34;&gt;&lt;title&gt;Collected Memories of Evans Hall&lt;/title&gt;&lt;style&gt;&#xA;      code{white-space: pre-wrap;}&#xA;      span.smallcaps{font-variant: small-caps;}&#xA;      span.underline{text-decoration: underline;}&#xA;      div.column{display: inline-block; vertical-align: top; width: 50%;}&#xA;          &lt;/style&gt;&lt;link rel=stylesheet href=../styles/common.css&gt;&lt;script data-goatcounter=https://lukasschwab.goatcounter.com/count async src=//gc.zgo.at/count.js&gt;&lt;/script&gt;&lt;a href=../index.html&gt;&amp;#171; blog&lt;/a&gt; &lt;span class=date&gt;2022-02-13&lt;/span&gt;&lt;header id=title-block-header&gt;&lt;h1 class=title&gt;Collected Memories of Evans Hall&lt;/h1&gt;&lt;/header&gt;&lt;p&gt;Berkeley’s Evans Hall is unmitigatably ugly, inside and out, even at&#xA;a great distance; to the discerning student, Evans is the ugliest&#xA;structure on campus. Last week &lt;a href=https://www.dailycal.org/2022/02/06/tends-to-stick-out-evans-hall-to-be-demolished-replaced/&gt;The&#xA;Daily Californian reported&lt;/a&gt; it’s to be demolished as part of the&#xA;University’s 2021 Long Range Development Plan.&lt;p&gt;This news is a long time coming. As the Daily Californian and &lt;a href=https://www.sfgate.com/bayarea/article/cal-evans-hall-to-be-demolished-16914778.php&gt;SFGATE&lt;/a&gt;&#xA;articles discuss, Evans has been widely disliked since it was built to&#xA;Gardner Dailey’s design in 1971. The University floated a proposal to&#xA;demolish Evans, and build a complex of low buildings on the site, in&#xA;2000; they punted that decision, citing budgetary constraints and a long&#xA;line of competing seismic priorities.&lt;a href=#fn1 class=footnote-ref id=fnref1 role=doc-noteref&gt;&lt;sup&gt;1&lt;/sup&gt;&lt;/a&gt; Now Evans is on the&#xA;docket again: it’s just unacceptably bad to look at.&lt;p&gt;This perspective is true, but it elides a deeper reading of the value&#xA;in buildings. There’s a sort of memory in Evans Hall itself: of&#xA;institutional dehumanization; of the studentry’s miniature ingenuity; of&#xA;milestones, and major pests, in computer science; and of love (yes, even&#xA;in Evans).&lt;p&gt;Always the best primer for a UC Berkeley building, &lt;em&gt;The Loafer’s&#xA;Guide&lt;/em&gt; notes Evans Hall’s&lt;blockquote&gt;&lt;p&gt;main stairwell balcony that faces a concrete wall, eye level with a&#xA;bare fluorescent light fixture. This building, Davis, and Wurster are&#xA;Neobrutalist, derived from the French word for “exposed concrete.”&#xA;Before its completion, the Daily Cal printed an editorial entitled “Love&#xA;can’t live in an ugly temple.” In addition to design and mechanical&#xA;problems beginning Day 1 at “Fort Evans,” students began to paint and&#xA;otherwise “adapt” the building. The “Death of Archimedes” and other&#xA;unsanctioned artworks are worth checking out.&lt;a href=#fn2 class=footnote-ref id=fnref2 role=doc-noteref&gt;&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt;&lt;/blockquote&gt;&lt;p&gt;John Rhodes, Professor Emeritus of Mathematics, remembers those&#xA;“unsanctioned artworks” as a kind of antifascist gesture. The department&#xA;was moved into Evans Hall in 1971, the year it was completed. Striving&#xA;to do creative work, they felt consigned to a building so austere Rhodes&#xA;found it dehumanizing.&lt;blockquote&gt;&lt;p&gt;Sometime after this seminar [on creativity in mathemtatics], I&#xA;organized a one-hour talk in 60 Evans by an assistant professor in the&#xA;architecture department entitled &lt;em&gt;Fascism and Architecture,&lt;/em&gt;&#xA;being as Evans Hall was despised by many as a dehumanizing workplace.&#xA;The subject of the talk was the effect of architecture on politics and&#xA;the spirit. I remember the speaker showed slides of Hitler waving to&#xA;crowds from the balcony of buildings in Berlin.&lt;p&gt;After the applause abated at the end of this well-received talk, I&#xA;passed out paintbrushes and paint cans. Even Sarah Hallam (senior&#xA;administrative assistant of the Department for many, many years) grabbed&#xA;a paintbrush. We rushed upstairs and painted some of the walls on the&#xA;7th to 10th floors.&lt;p&gt;Voilà! That’s how the “murals” got there. Some were painted by famous&#xA;mathematicians — [William] Thurston and [Dennis] Sullivan. And I know&#xA;&lt;em&gt;La Mort de Galois&lt;/em&gt; on the 7th floor was painted by my son’s&#xA;uncle, Jack Knutson. I thought perhaps some of you — young and old —&#xA;might like to know the history of these paintings. I hope as many of&#xA;them can be preserved as possible.&lt;a href=#fn3 class=footnote-ref id=fnref3 role=doc-noteref&gt;&lt;sup&gt;3&lt;/sup&gt;&lt;/a&gt;&lt;/blockquote&gt;&lt;p&gt;The painting of Galois &lt;a href=https://math.berkeley.edu/~ribet/114/&gt;included here&lt;/a&gt; is dated&#xA;December 1971; it may be &lt;em&gt;La Mort de Galois,&lt;/em&gt; but I’m not sure.&#xA;Sullivan also reports it was 1971.&lt;a href=#fn4 class=footnote-ref id=fnref4 role=doc-noteref&gt;&lt;sup&gt;4&lt;/sup&gt;&lt;/a&gt; Remember, Evans was&#xA;&lt;em&gt;completed&lt;/em&gt; in ’71; the math department got mad fast. Sarah&#xA;Hallam’s retirement in 1978&lt;a href=#fn5 class=footnote-ref id=fnref5 role=doc-noteref&gt;&lt;sup&gt;5&lt;/sup&gt;&lt;/a&gt; is a latest date. In any&#xA;case, I’ll try to photograph these murals the next time I’m on&#xA;campus.&lt;p&gt;Evans was only painted its current institutional grey-green in the&#xA;late 1990s, part of a renovation to prevent “large pieces of concrete&#xA;[…] falling off the face of Evans Hall without warning” (yikes).&lt;a href=#fn6 class=footnote-ref id=fnref6 role=doc-noteref&gt;&lt;sup&gt;6&lt;/sup&gt;&lt;/a&gt; Apocryphally, the color was selected&#xA;to blend the towering blight into the hillside; in practice, this marks&#xA;the building as a place of great aesthetic shame. &lt;em&gt;Béton brut&lt;/em&gt;&#xA;was better.&lt;p&gt;In 2002, shortly after the repainting, first-year architecture&#xA;student Alfred Twu ran the &lt;a href=https://www.ocf.berkeley.edu/~atwu/firstcultural/evanshall.html&gt;Evans&#xA;Hall Coloring Contest&lt;/a&gt;: entrants printed, then recolored, folding&#xA;scale models of Evans. First and second place went to &lt;em&gt;trompe&#xA;l’oeil&lt;/em&gt; panoramas, braver (and more surreal) imaginings of the&#xA;hidden-in-plain-sight grey-green. The competition’s a great example of&#xA;lighthearted Berkeley creative initiative.&lt;a href=#fn7 class=footnote-ref id=fnref7 role=doc-noteref&gt;&lt;sup&gt;7&lt;/sup&gt;&lt;/a&gt; All&#xA;contest submissions and the vote tally are &lt;a href=https://www.ocf.berkeley.edu/~atwu/firstcultural/evanshallwinners.html&gt;here&lt;/a&gt;.&lt;figure&gt;&lt;img src=../img/memories-of-evans-hall/leong.jpg alt=&#34;L. K. Leong’s winning submission to the Evans Hall Coloring Contest (2002). A foreground trees in silhouette shade the walkway around Evans; behind them, low peaks (the Berkeley Hills?) blush orange.&#34;&gt;&lt;figcaption aria-hidden=true&gt;L. K. Leong’s winning submission to the&#xA;Evans Hall Coloring Contest (2002). A foreground trees in silhouette&#xA;shade the walkway around Evans; behind them, low peaks (the Berkeley&#xA;Hills?) blush orange.&lt;a href=#fn8 class=footnote-ref id=fnref8 role=doc-noteref&gt;&lt;sup&gt;8&lt;/sup&gt;&lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure&gt;&lt;img src=../img/memories-of-evans-hall/barber.jpg alt=&#34;Tom Barber’s second-place submission to the Evans Hall Coloring Contest (2002). It appears to be a photo of Memorial Glade (the eastward view from Evans) stretched to gargantuan proportions.&#34;&gt;&lt;figcaption aria-hidden=true&gt;Tom Barber’s second-place submission to&#xA;the Evans Hall Coloring Contest (2002). It appears to be a photo of&#xA;Memorial Glade (the eastward view from Evans) stretched to gargantuan&#xA;proportions.&lt;a href=#fn9 class=footnote-ref id=fnref9 role=doc-noteref&gt;&lt;sup&gt;9&lt;/sup&gt;&lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;Of course, there’s more to Evans than its aesthetics. The departments&#xA;it houses have astounding academic records; at work, I regularly use&#xA;software spawned in its miserable greenish bowels. All that the comes&#xA;from hardworking people, decades of students and academics, and the&#xA;building is a testament to their experience inasmuch as they tell&#xA;stories about its architecture.&lt;p&gt;The most elaborate such stories are in the &lt;a href=https://www.erzo.org/shannon/writing/csua/encyclopedia.html&gt;CSUA&#xA;Encyclopedia&lt;/a&gt;.&lt;a href=#fn10 class=footnote-ref id=fnref10 role=doc-noteref&gt;&lt;sup&gt;10&lt;/sup&gt;&lt;/a&gt; Though it was written after the&#xA;Computer Science Undergraduate Association’s relocation to Soda Hall,&#xA;the Encyclopedia can’t disentangle the CSUA’s history and in-jokes from&#xA;Evans itself.&lt;p&gt;Sometimes Evans was a nuisance:&lt;blockquote&gt;&lt;dl&gt;&lt;dt&gt;&lt;code&gt;elevatorP&lt;/code&gt;&lt;dd&gt;Incantation to summon the elevator to the basement of Evans. Also, a&#xA;means to subvert the security measures of Evans. Largely obsolete.&#xA;Usually initiated from the tvi920c in the WEB print room.&lt;dd&gt;In ye olden days, the public computers at Berkeley were grouped in two&#xA;locations — in the WEB, located in the basement of Evans, and in E260,&#xA;located on the second floor of Evans. The WEB was easilly accessible,&#xA;but E260 was in an area of Evans which was locked in the evenings.&#xA;Fortunately, the elevators on the south side of the building could be&#xA;used to subvert the lackluster security measures; unfortunately, the&#xA;elevators could not be called from the basement. &lt;code&gt;elevatorP&lt;/code&gt;&#xA;was initiated by students in the WEB to call out to their brethren in&#xA;E260, and so summon elevators down to the WEB. Could also be used to&#xA;gain access to the former CSUA office in E238.&lt;dt&gt;Lock Collection&lt;dd&gt;Holy CSUA artifacts. A set of locks rumored to have once adorned the&#xA;secret north door of Evans Hall.&lt;dd&gt;Little is recorded regarding how the Lock Collection might have come&#xA;into existence, but according to quiet whispers, the north door of Evans&#xA;might once have been a secret entrance to the hallowed lands of E260 and&#xA;the CSUA office in E238. It has been suggested that, perhaps, if the&#xA;lock on the north door went missing, it would have been easier for&#xA;computer students to attain the computers of the second floor Evans, but&#xA;archaelogical evidence is too scant to verify this. In all likelihood,&#xA;the CSUA Lock Collection is actually composed of discarded locks and has&#xA;no relation to this theory.&lt;dt&gt;Three Evans Route&lt;dd&gt;Secret path through the Evans Security Perimeter.&lt;dd&gt;The administration tried very hard to maintain the Evans Security&#xA;Barrier, manning the area with campus police and fixing the occasional&#xA;mysteriously missing lock; one must given them credit for that. However,&#xA;no one ever said they were very bright in their efforts. Take a&#xA;case-in-point, the Three Evans Route.&lt;dd&gt;Locked glass doors can be used to prevent student scum from entering the&#xA;upper reaches of Evans without barring them from the twin nirvana of WEB&#xA;and vending machines; no question about it. But, what if there were a&#xA;room, say 3 Evans, which had two entrances, one on either side of these&#xA;magic glass doors? If there were such a theoretical bone-headed design,&#xA;it would be very simple to slip past the Perimeter, without having to&#xA;wait for a doosher to notice the call of &lt;code&gt;elevatorP&lt;/code&gt;.&lt;dd&gt;Purely theoretical, and in any case the administration remembered to&#xA;lock the outer Three Evans doors on occasion.&lt;/dl&gt;&lt;/blockquote&gt;&lt;p&gt;And sometimes Evans was a place to practice being a nuisance:&lt;blockquote&gt;&lt;dl&gt;&lt;dt&gt;CSUA Bat (or CSUA Baseball Bat)&lt;dd&gt;Holy CSUA artifact and the symbol of office for the Vice President. A&#xA;black Louisville slugger, well-worn and loved. […] Particularly useful&#xA;for obsoleting CRTs, gaining votes at politburo meetings, emphasizing&#xA;points at general meetings, and &lt;em&gt;hitting Evans to relieve stress&lt;/em&gt;&#xA;(the last being its original purpose, according to rumors).&lt;dt&gt;Fencing Club&lt;dd&gt;Saber-rattling bunch of CS students nearly foiled by the UC police.&#xA;Their motto: “don’t take a fence.”&lt;dd&gt;Picking up fences and hauling them across campus is very funny.&#xA;Explaining to UC police that you weren’t doing so is even funnier. Still&#xA;funnier is getting a chain-link fence, dragging it through the Evans&#xA;Security Perimeter, and then using it to block off the office of a&#xA;beloved lecturer. Even the beloved lecturer will agree it’s as funny as&#xA;anything, while plaintively asking that said chain-link fence please be&#xA;removed.&lt;/dl&gt;&lt;/blockquote&gt;&lt;p&gt;And sometimes Evans was more than the sum of its nuisances. Deep in&#xA;the CSUA Encyclopedia, in the “Poetry” entry, in a full-alphabet&#xA;acrostick, there’s an extraordinary name.&lt;blockquote&gt;&lt;p&gt;i is for ikiru, Euphrasia Lavette Alzena Guri Scientia Ventura Ikiru;&#xA;and then&lt;br&gt;j is for more ikiru, Alvera Ganbatte Gelasia&#xA;Curvilinearjky… van Bezooijen&lt;/blockquote&gt;&lt;p&gt;ikiru pased away in 2011. She took the additional “van Bezooijen”&#xA;when she married Eric van Bezooijen (CSUA Encyclopedia contributor) in&#xA;1997. &lt;a href=http://euphrasiaweb.herokuapp.com/index.html&gt;Eric’s&#xA;eulogy for Euphrasia&lt;/a&gt; paints a brilliant and complicated picture,&#xA;though reading it feels uncomfortably like an intrusion on something&#xA;personal: “I could spend all day telling you the things she gave&#xA;me.”&lt;p&gt;“How did I meet Euphrasia? We met in the early 1990s in Evans Hall&#xA;room 260, a UC Berkeley computer lab, not the most romantic of all&#xA;places.”&lt;p&gt;Actually, I think this captures the romance of architectural memory.&#xA;Our common architectural referent — Evans — makes Eric and Euphrasia&#xA;real, makes their experience seem immediate. Locating Eric’s story sets&#xA;up an exchange of meanings: it marks Evans 260 as a place, in Marc&#xA;Augé’s sense.&lt;a href=#fn11 class=footnote-ref id=fnref11 role=doc-noteref&gt;&lt;sup&gt;11&lt;/sup&gt;&lt;/a&gt; That means, in turn, Evans 260’s&#xA;existence testifies to the reality of their experience. The place is an&#xA;encounter between us, now, and them, then.&lt;/p&gt;&lt;p&gt;Moreover, Evans’s architectural memory is &lt;em&gt;alive&lt;/em&gt;; students&#xA;are still groaning about the green-grey, about the slow elevators, about&#xA;the hulking blight. Evans isn’t a museum of the work that &lt;em&gt;was&lt;/em&gt;&#xA;done there; it’s a place to take part in the continuation of that work,&#xA;to participate (even in some miniscule way) in the history that is the&#xA;very basis for this memory.&lt;p&gt;Of course, participation means imagining something better. Working in&#xA;Evans Hall every day made me unahppy, and would make me unhappy again.&#xA;The University’s plan to raze Evans Hall is even older than the Coloring&#xA;Contest plans to dress it up as nature. They’re right to prioritize&#xA;current and future academics over campus history: UC Berkeley isn’t a&#xA;museum, it’s a workplace.&lt;p&gt;One shouldn’t desperately conserve miserable places, but one should&#xA;read buildings — beyond their aesthetics and functionality — for what&#xA;they &lt;em&gt;remember:&lt;/em&gt; our collective memory, in architecture.&lt;aside id=footnotes class=&#34;footnotes footnotes-end-of-document&#34; role=doc-endnotes&gt;&lt;hr&gt;&lt;ol&gt;&lt;li id=fn1&gt;&lt;p&gt;Keller, Josh. &lt;a href=https://web.archive.org/web/20110713120707/http://www.joshmkeller.com/stories/evans.html&gt;“No&#xA;Stirrings of Pride”&lt;/a&gt; in &lt;em&gt;The Chronicle of Higher Education.&lt;/em&gt;&#xA;(2007).&lt;a href=#fnref1 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn2&gt;&lt;p&gt;Dougherty, Carolyn. &lt;a href=https://loafersguide.github.io/guide/existing-campus#evans&gt;“The&#xA;Existing Campus”&lt;/a&gt; in &lt;em&gt;The Loafer’s Guide to UC Berkeley.&lt;/em&gt;&lt;a href=#fnref2 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn3&gt;&lt;p&gt;Rhodes, John. &lt;a href=../img/memories-of-evans-hall/rhodes.pdf&gt;“A History of the Murals&#xA;in Evans Hall”&lt;/a&gt; in &lt;em&gt;Berkeley Mathematics Newsletter,&lt;/em&gt; Fall&#xA;2002 Vol. IX, No. 1. (2002)&lt;a href=#fnref3 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn4&gt;&lt;p&gt;Mosher, Lee and Bill Casselman. &lt;a href=https://www.ams.org/notices/200303/what-is-web.pdf&gt;“What is… A&#xA;Train Track?”&lt;/a&gt; in &lt;em&gt;Notices of the AMS,&lt;/em&gt; Vol. 50, No. 3.&#xA;(2003). According to Thurston, the mural-painting continued — over&#xA;administrative protestation — after Rhodes’s start:&lt;blockquote&gt;&lt;p&gt;Last fall Sullivan wrote to Mosher: “In 1971 I was a guest of the&#xA;University of California giving lectures in the Math Dept. At the same&#xA;time there was a confrontation between the trustees and the graduate&#xA;students et al. The latter planned to continue decorating the walls of&#xA;the department by painting attractive murals and the trustees forbade&#xA;it. At tea some students came up and invited me to join their painting&#xA;the next day. I became enthusiastic when one bearded fellow [Thurston]&#xA;showed me an incredible drawing of an embedded curve in the triply&#xA;punctured disk and asked if I thought this would be interesting to&#xA;paint. I said, ‘You bet,’ and the next day we spent all afternoon doing&#xA;it.&lt;p&gt;[…]&lt;p&gt;Thurston wrote in a note to Mosher that the project “was in response&#xA;to a little flurry with administration sanctions of some sort when John&#xA;Rhodes painted the wall outside his office, I think with a political&#xA;slogan related to one of the issues of the times (Vietnam war, invasion&#xA;of Cambodia, People’s Park?).”&lt;/blockquote&gt;&lt;a href=#fnref4 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn5&gt;&lt;p&gt;Felde, Marie. &lt;a href=https://www.berkeley.edu/news/berkeleyan/1996/0313/hallam.html&gt;“Hallam&#xA;Bequest Benefits Math”&lt;/a&gt; in &lt;em&gt;The Berkeleyan,&lt;/em&gt; March 13,&#xA;1996.&lt;p&gt;There’s some disagreement: a departmental chronicle reports Hallam&#xA;retired six years earlier, in 1972. Interestingly, it also reports she&#xA;was hired half-time by Evans — the building’s namesake — in 1936, for a&#xA;$400 salary. See &lt;em&gt;Mathematics at Berkeley: A History&lt;/em&gt; by Calvin&#xA;C. Moore (2007).&lt;a href=#fnref5 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn6&gt;&lt;p&gt;Keller, Josh. &lt;a href=https://web.archive.org/web/20110713120707/http://www.joshmkeller.com/stories/evans.html&gt;“No&#xA;Stirrings of Pride”&lt;/a&gt; in &lt;em&gt;The Chronicle of Higher Education.&lt;/em&gt;&#xA;(2007).&lt;a href=#fnref6 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn7&gt;&lt;p&gt;The Evans Hall Coloring Contest is theoretically&#xA;interesting. Using scale models as a means of participatory architecture&#xA;would be right at home in &lt;a href=https://www.anycorp.com/store/log50&gt;&lt;em&gt;Log 50: Model&#xA;Behavior&lt;/em&gt;&lt;/a&gt; — and it would stand out. The authors are largely&#xA;focused on one-off canonical models (the stuff of architecture&#xA;exhibitions) or models as devices for iterative studio design, and they&#xA;all reference the same Borges story and enumerate the issue-theme&#xA;multi-entendre.&lt;p&gt;The Evans Hall Coloring Contest’s paper models are cheaply&#xA;reproduced, structurally simple, and accessible to nonprofessional&#xA;participants.&lt;a href=#fnref7 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn8&gt;&lt;p&gt;Twu, Alfred and contestants. &lt;a href=https://www.ocf.berkeley.edu/~atwu/firstcultural/evanshallwinners.html&gt;&lt;em&gt;Evans&#xA;Hall Coloring Contest Winners.&lt;/em&gt;&lt;/a&gt; (2002).&lt;a href=#fnref8 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn9&gt;&lt;p&gt;Twu, Alfred and contestants. &lt;a href=https://www.ocf.berkeley.edu/~atwu/firstcultural/evanshallwinners.html&gt;&lt;em&gt;Evans&#xA;Hall Coloring Contest Winners.&lt;/em&gt;&lt;/a&gt; (2002).&lt;a href=#fnref9 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn10&gt;&lt;p&gt;Appelcline, Shannon with pictures by Eric van&#xA;Bezooijen. &lt;a href=https://www.erzo.org/shannon/writing/csua/encyclopedia.html&gt;&lt;em&gt;Welcome&#xA;to soda.csua.berkeley.edu, Now Go Home! or When Geeks Collide&lt;/em&gt;&lt;/a&gt;.&#xA;(1994).&lt;p&gt;This is really a remarkable document; well worth the read if you have&#xA;any affiliation with UC Berkeley Computer Science.&lt;a href=#fnref10 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn11&gt;&lt;p&gt;See vaguely Augé, Marc. &lt;em&gt;Non-places: Introduction to&#xA;an anthropology of supermodernity.&lt;/em&gt; (1992).&lt;a href=#fnref11 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;/ol&gt;&lt;/aside&gt;</content>
    <link href="https://lukasschwab.me/blog/gen/memories-of-evans-hall.html" rel="alternate"></link>
    <summary type="html">Evans Hall, once home to UC Berkeley&#39;s Computer Science department and current home to several others, is slated for demolition. While that&#39;s good news, Evans Hall is a point of contact with history, a sort of concrete collective memory.</summary>
  </entry>
  <entry>
    <title>Hearst Hall: Continued Reading</title>
    <updated>2021-07-04T00:00:00Z</updated>
    <id>https://lukasschwab.me/blog/gen/hearst-hall-continued-reading.html</id>
    <content type="html">&lt;html xmlns=http://www.w3.org/1999/xhtml lang xml:lang&gt;&lt;meta charset=utf-8&gt;&lt;meta name=generator content=&#34;pandoc&#34;&gt;&lt;meta name=viewport content=&#34;width=device-width,initial-scale=1,user-scalable=yes&#34;&gt;&lt;meta name=author content=&#34;Lukas Schwab&#34;&gt;&lt;meta name=dcterms.date content=&#34;2021-07-04&#34;&gt;&lt;title&gt;Hearst Hall: Continued Reading&lt;/title&gt;&lt;style&gt;&#xA;      code{white-space: pre-wrap;}&#xA;      span.smallcaps{font-variant: small-caps;}&#xA;      span.underline{text-decoration: underline;}&#xA;      div.column{display: inline-block; vertical-align: top; width: 50%;}&#xA;          &lt;/style&gt;&lt;link rel=stylesheet href=../styles/common.css&gt;&lt;link rel=stylesheet href=../styles/fence-extensions/addendum.css&gt;&lt;link rel=stylesheet href=../styles/fence-extensions/frame.css&gt;&lt;link rel=stylesheet href=../styles/fence-extensions/gallery.css&gt;&lt;script data-goatcounter=https://lukasschwab.goatcounter.com/count async src=//gc.zgo.at/count.js&gt;&lt;/script&gt;&lt;a href=../index.html&gt;&amp;#171; blog&lt;/a&gt; &lt;span class=date&gt;2021-07-04&lt;/span&gt;&lt;header id=title-block-header&gt;&lt;h1 class=title&gt;Hearst Hall: Continued Reading&lt;/h1&gt;&lt;/header&gt;&lt;nav id=TOC role=doc-toc&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=#style-and-structure id=toc-style-and-structure&gt;Style&#xA;and Structure&lt;/a&gt;&lt;li&gt;&lt;a href=#adaptation id=toc-adaptation&gt;Adaptation&lt;/a&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=#loss-as-final-adaptation id=toc-loss-as-final-adaptation&gt;Loss as final adaptation&lt;/a&gt;&lt;li&gt;&lt;a href=#losing-hearst-hall id=toc-losing-hearst-hall&gt;Losing&#xA;Hearst Hall&lt;/a&gt;&lt;/ul&gt;&lt;li&gt;&lt;a href=#modernism id=toc-modernism&gt;Modernism&lt;/a&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=#wurster-hall id=toc-wurster-hall&gt;Wurster Hall&lt;/a&gt;&lt;/ul&gt;&lt;li&gt;&lt;a href=#on-sources id=toc-on-sources&gt;On sources&lt;/a&gt;&lt;/ul&gt;&lt;/nav&gt;&lt;p&gt;Writing typically hurries one of my interests out of its&#xA;“all-consuming” phase and into “embarrassed retirement,” but &lt;a href=../gen/hearst-hall.html&gt;Hearst Hall&lt;/a&gt; keeps coming up. I&#xA;borrowed Kenneth Cardwell’s &lt;em&gt;Bernard Maybeck: Artisan, Architect,&#xA;Artist&lt;/em&gt; from the SF Public Library, and suddenly I’m all-consumed&#xA;again.&lt;p&gt;That last post was heavy on my own, uh, sketchily-supported&#xA;commentary. Cardwell, to my relief, supports a number of those claims.&#xA;He dedicates the better part of a chapter to Hearst Hall—in Cardwell’s&#xA;telling of Maybeck’s career, it’s prototypical of his first professional&#xA;phase: redwood buildings, mixing gothic-evocative forms and prominent&#xA;structural rhythms (especially of trusses and arches).&lt;p&gt;The book has its shortcomings. Cardwell praises unrelentingly. When a&#xA;Maybeck building disappoints, it’s always because the owner had poor&#xA;taste, or meddled in rennovations, or that Maybeck must’ve been too&#xA;hands-off. Cardwell refuses to discuss Maybeck in relation to his&#xA;contemporary architects besides, very briefly, a teacher (Louis-Jules&#xA;André) and a student (Julia Morgan). Greene &amp; Greene, who built the&#xA;William R. Thorsen House in Berkeley, are conspicuously absent. John&#xA;Galen Howard is treated like a bureaucratic gadfly. Cardwell, after all,&#xA;dedicated much of his career to Maybeck alone: building an archive of&#xA;Maybeck’s documents for Cal’s College of Environmental Design. Some&#xA;auteurism is to be expected.&lt;p&gt;I won’t introduce any &lt;em&gt;truly new&lt;/em&gt; ideas. Instead, I’ll string&#xA;together excerpts (mostly, but not exclusively, from Cardwell, and with&#xA;emphasis added) pertinent to the arguments in the last essay: that&#xA;Hearst Hall’s bare structure is notable; that Maybeck’s philosophy&#xA;stressed the adaptability of spaces, even to the point of accepting&#xA;their destruction; and that Maybeck exemplifies certain foundational&#xA;principles of modernism.&lt;h1 id=style-and-structure&gt;Style and Structure&lt;/h1&gt;&lt;p&gt;I did my best to argue the prominence of Hearst Hall’s&#xA;structure—those laminated archies—in its deisgn, but I only had a&#xA;handful of interior and exterior photos. Cardwell describes that&#xA;structure, and the means of its production, in finer detail.&lt;figure&gt;&lt;img src=../img/hearst-hall/construction.jpg alt=&#34;“Hearst Hall under construction.”&#34;&gt;&lt;figcaption aria-hidden=true&gt;“Hearst Hall under construction.”&lt;a href=#fn1 class=footnote-ref id=fnref1 role=doc-noteref&gt;&lt;sup&gt;1&lt;/sup&gt;&lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;&lt;blockquote&gt;&lt;p&gt;The reception pavilion, Hearst Hall, was built on the south side of&#xA;Channing Way, adjoining Mrs. Hearst’s residence on Piedmont Avenue. It&#xA;was a building sixty feet in width and one hundred forty feet in length,&#xA;with its principal axis oriented in a north-south direction on a site&#xA;which sloped gently to the south and west. &lt;em&gt;The structure—the&#xA;engineering of which was worked out in collaboration with Maybeck’s&#xA;university colleague Herman Kower—was an exciting and bold concept that&#xA;employed laminated arches and diaphragms to create sections of the&#xA;buildings as independent, movable, and re-assembled units.&lt;/em&gt; Twelve&#xA;wooden arches rising fifty-four feet above the ground floor level were&#xA;paired to form six structural bays. Single arches were used at the walls&#xA;of the end bays. &lt;em&gt;These arches were constructed of laminated&#xA;two-by-eight timbers,&lt;/em&gt; formed horizontally on the site by nailing&#xA;the timbers to a radius of approximately sixty feet. Holes were then&#xA;drilled and bolts placed which were tightened after erection loads had&#xA;been imposed. The arches were given lateral stability by the floor of&#xA;the main hall which was twelve feet above the ground floor. it consisted&#xA;of a diagonal grid of beams and joists, which was left exposed in the&#xA;ceiling of the lower room. The arches were further tied, at&#xA;approximately half height, by a horizontal plane surrounding the&#xA;exterior of the arches, creating a floor for an outdoor gallery. &lt;em&gt;The&#xA;exterior walls of the building were curtain walls of stud construction&#xA;with columns at each arch. These columns were tied at the middle to the&#xA;arch by horizontal members which extended through the exterior to&#xA;support planting boxes.&lt;/em&gt;&lt;a href=#fn2 class=footnote-ref id=fnref2 role=doc-noteref&gt;&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt;&lt;/blockquote&gt;&lt;p&gt;Laminated timber seems a pretty modern technical decision, but I’ll&#xA;come to a stronger point about concrete later. The real surprise in this&#xA;passage is about the ties between the arches and the curtain walls&#xA;extending to form Hearst Hall’s exterior planters. The ogive arches are&#xA;outwardly apparent in Hearst Hall’s front elevation—that might suffice&#xA;to argue its structural frankness—but these planters give outward&#xA;expression to these ties, these &lt;em&gt;much more obscure&lt;/em&gt; structural&#xA;elements. There’s a similar structural extension &lt;em&gt;through&lt;/em&gt; the&#xA;building in Maybeck’s Outdoor Art Club building in Mill Valley.&lt;blockquote&gt;&lt;p&gt;The design for the Outdoor Art Club (1904) in Mill Valley added a new&#xA;roof variation. It has a simple rectangular plan spanned by rafters&#xA;trussed with a king post and collar ties. &lt;em&gt;The ties extend through&#xA;the roof covering and join a vertical extension of the wall&#xA;columns,&lt;/em&gt; thereby strengthening the weakest part of the rafter chord&#xA;between the tie and the support. Maybeck probably adopted this form for&#xA;two reasons: first, the spaciousness of an uninterrupted vertical volume&#xA;under a roof plane pleased him; second, &lt;em&gt;the adaptation of a king&#xA;post truss thrusting through the roof fulfilled his desire to indicate&#xA;the nature of the building’s framing from the exterior.&lt;/em&gt;&lt;a href=#fn3 class=footnote-ref id=fnref3 role=doc-noteref&gt;&lt;sup&gt;3&lt;/sup&gt;&lt;/a&gt;&lt;/blockquote&gt;&lt;figure&gt;&lt;img src=../img/hearst-hall/outdoor-art-club.png alt=&#34;The Outdoor Art Club in Mill Valley. Structural members extend through the roof to the building’s exterior, similar to the extended ties that support Hearst Hall planters. A gabled interior volume nests in—and exceeds—a rectangular structural frame.&#34;&gt;&lt;figcaption aria-hidden=true&gt;The Outdoor Art Club in Mill Valley.&#xA;Structural members extend &lt;em&gt;through&lt;/em&gt; the roof to the building’s&#xA;exterior, similar to the extended ties that support Hearst Hall&#xA;planters. A gabled interior volume nests in—and exceeds—a rectangular&#xA;structural frame.&lt;a href=#fn4 class=footnote-ref id=fnref4 role=doc-noteref&gt;&lt;sup&gt;4&lt;/sup&gt;&lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;Speaking of exterior extensions, the “weird turret-obelisk” dangling&#xA;from the Hall’s west face serves a purpose:&lt;blockquote&gt;&lt;p&gt;The tower at the right contained the flue from the heating plant. It&#xA;also embellished the terrace roof garden gallery.&lt;a href=#fn5 class=footnote-ref id=fnref5 role=doc-noteref&gt;&lt;sup&gt;5&lt;/sup&gt;&lt;/a&gt;&lt;/blockquote&gt;&lt;p&gt;It seems Maybeck didn’t bother moving it—the chimney is bare in all&#xA;photos of Hearst Hall on campus—and I can’t say I blame him. Hearst&#xA;Hall’s exterior sillhouette isn’t all that charming (an effect worsened&#xA;in black and white), but the integration of the original chimney seems&#xA;slapdash, after-the-fact, like a towering wart. I haven’t been able to&#xA;find any photos of the “terrace roof garden;” it sounds fantastic.&lt;p&gt;Cardwell sheds some light on the matryoshka spaces-within-spaces&#xA;effect—not the round arches of electric lights, but the alcoves and the&#xA;fabric frozen in mid-air:&lt;blockquote&gt;&lt;p&gt;The alcoves formed by the spaces between the ribs of the arches were&#xA;hung with Gobelin tapestries and various paintings from the Hearst&#xA;collection. When the Hall was used for less formal occasions, such as&#xA;afternoon teas given by Mrs. Hearst, a large canopy of Genoese rose&#xA;velvet was suspended from the arches in the middle of the hall.&lt;a href=#fn6 class=footnote-ref id=fnref6 role=doc-noteref&gt;&lt;sup&gt;6&lt;/sup&gt;&lt;/a&gt;&lt;/blockquote&gt;&lt;p&gt;Posh! These tapestries (“of the fifteenth and sixteenth centuries […]&#xA;all historic treasures”) were relocated to the Maybeck-designed Hearst&#xA;manse at Wyntoon, where presumably many were destroyed (with the castle)&#xA;in a 1929 fire.&lt;figure&gt;&lt;img src=../img/hearst-hall/tapestries.png alt=&#34;Tapestries in Hearst Hall (left) and at Wyntoon (right). It’s hard to tell in these scans, but the tapestry in the middle Hearst Hall alcove is—post-relocation—also the far tapestry in the Wyntoon photo.&#34;&gt;&lt;figcaption aria-hidden=true&gt;Tapestries in Hearst Hall (left)&lt;a href=#fn7 class=footnote-ref id=fnref7 role=doc-noteref&gt;&lt;sup&gt;7&lt;/sup&gt;&lt;/a&gt; and at Wyntoon (right).&lt;a href=#fn8 class=footnote-ref id=fnref8 role=doc-noteref&gt;&lt;sup&gt;8&lt;/sup&gt;&lt;/a&gt; It’s hard to tell in these scans,&#xA;but the tapestry in the middle Hearst Hall alcove&#xA;is—post-relocation—also the far tapestry in the Wyntoon&#xA;photo.&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;Finally, and importantly, Cardwell identifies some material decisions&#xA;that don’t quite read in the interior photos.&lt;blockquote&gt;&lt;p&gt;&lt;em&gt;The interior of the main hall was finished with redwood barn&#xA;shakes similar to those on the exterior&lt;/em&gt; but treated with a sulphate&#xA;of iron solution which rendered them a silvery-grey. Light came from&#xA;clerestory windows covered by grilles of turned wood balusters of&#xA;flemish derivation, typical of Maybeck’s ornamentation. […] The&#xA;&lt;em&gt;plain pine floor&lt;/em&gt; was covered by two large Chinese rugs of&#xA;special design executed in grey-blue with a gold border. At night the&#xA;room was lighted with more than a thousand incandescent bulbs. Two rows&#xA;of colored lights were mounted directly on each arch of the room, and&#xA;suspended from each arch were &lt;em&gt;two rows of white lights with milk&#xA;glass reflectors hung in an arch complementary to the structural&#xA;members.&lt;/em&gt;&lt;a href=#fn9 class=footnote-ref id=fnref9 role=doc-noteref&gt;&lt;sup&gt;9&lt;/sup&gt;&lt;/a&gt;&lt;/blockquote&gt;&lt;p&gt;These aren’t frank structural elements, but they’re interesting&#xA;inasmuch as the interior presented Maybeck a &lt;a href=../gen/subtraction-tests.html&gt;margin of freedom&lt;/a&gt;: he could&#xA;readily have built an interior that elided the Hall’s structure.&#xA;Instead, the interior surfaces repeat the exterior (shingles! inside!)&#xA;and lights trace the ogive arches.&lt;h1 id=adaptation&gt;Adaptation&lt;/h1&gt;&lt;p&gt;My last essay argued Maybeck’s architecture involved a certain&#xA;openness towards its occupants. I was working from two pieces of&#xA;evidence: first, that Hearst Hall was primarily a multipurpose social&#xA;space, and then that Maybeck reflected on the importance of&#xA;architecture’s “inmates” in his written reflections: “the real interest&#xA;must come from those who are to live in it.”&lt;p&gt;Luckily for me, Hearst Hall isn’t a fluke; this openness was a part&#xA;of Maybeck’s practice &lt;em&gt;in general&lt;/em&gt;, a part especially pronounced&#xA;in his residential architecture.&lt;blockquote&gt;&lt;p&gt;In addition to technical and business details, he expressed his ideas&#xA;on the relationship of a house, its owners, and its furnishing [to a&#xA;Mr. and Mrs. Havens, clients]. “We believe that when you have seen the&#xA;house and become accustomed to it,” he wrote,&lt;blockquote&gt;&lt;p&gt;there will be a thousand and one ideas come rushing to your mind to&#xA;complete it… We therefore are getting an idea of the cost of things and&#xA;when you have decided &lt;em&gt;what you will do to put life into the&#xA;rooms,&lt;/em&gt; you will make such a success of it that the thing as a whole&#xA;will have that interest and personality which even as a plain business&#xA;proposition will be a good investment… We also were under the impression&#xA;that &lt;em&gt;you could get your own personality into your house better if we&#xA;stopped at the building proper,&lt;/em&gt; so as to leave you unhampered in&#xA;the final work of furnishings.&lt;a href=#fn10 class=footnote-ref id=fnref10 role=doc-noteref&gt;&lt;sup&gt;10&lt;/sup&gt;&lt;/a&gt;&lt;/blockquote&gt;&lt;/blockquote&gt;&lt;p&gt;Lewis Mumford remembers not every starchitect was this&#xA;open-minded:&lt;blockquote&gt;&lt;p&gt;Frank Lloyd Wright, it is said, once turned upon a client—let’s call&#xA;him John Smith—who had added a few pleasant rugs and comfortable Aalto&#xA;chairs to Mr. Wright’s furnishings, and exclaimed, “You have ruined this&#xA;place completely, and you have disgraced &lt;em&gt;me&lt;/em&gt;. This is no longer&#xA;a Frank Lloyd Wright house. It is a John Smith house now.”&lt;a href=#fn11 class=footnote-ref id=fnref11 role=doc-noteref&gt;&lt;sup&gt;11&lt;/sup&gt;&lt;/a&gt;&lt;/blockquote&gt;&lt;p&gt;The physical openness of Hearst Hall’s floorplan isn’t unusual given&#xA;the project parameters: a subdivided main space wouldn’t have suited&#xA;music or theatrical performances, for example. Later in his career&#xA;Maybeck advocated for open floorplans in a setting where they&#xA;&lt;em&gt;were&lt;/em&gt; unusual at the time: in residential architecture.&lt;blockquote&gt;&lt;p&gt;In 1923 &lt;em&gt;Sunset Magazine&lt;/em&gt; published an article entitled “The&#xA;Maybeck One-Room House,” in which he proposed the incorporation of house&#xA;and garden into one entity, concentrating time and effort as well as&#xA;cost on one handsomely proportioned and beautifully furnished space.&#xA;Service rooms were to be reduced to such a minimal size that they would&#xA;become mere alcoves.&lt;/blockquote&gt;&lt;p&gt;Maybeck’s argument in &lt;em&gt;Sunset Magazine&lt;/em&gt; is essentially that&#xA;the “One-Room House” is economical, but it’s predicated on the idea that&#xA;one “beautifully furnished space” can serve a variety of functions&#xA;admirably.&lt;p&gt;Maybeck’s preference—that users should tailor and modify his&#xA;work—extends to his urban- and campus-planning. A campus, he contends,&#xA;is eventually “lived in” in the same cozy sense as a house.&lt;blockquote&gt;&lt;p&gt;Fifty years from now the plan of the University will have become&#xA;modified and softened; it will be transformed many times, because so&#xA;easily done. The buildings will have undergone changes, so planned from&#xA;the beginning that they could grow. This patchwork gives the same&#xA;feeling to the whole composition as the new stones in a cathedral––the&#xA;newness becomes tarnished, the monotony of exactness relieved. By that&#xA;time the gardens will be older, and &lt;em&gt;in places a vine may soften the&#xA;harshness of perfection.&lt;/em&gt;&lt;a href=#fn12 class=footnote-ref id=fnref12 role=doc-noteref&gt;&lt;sup&gt;12&lt;/sup&gt;&lt;/a&gt;&lt;/blockquote&gt;&lt;h2 id=loss-as-final-adaptation&gt;Loss as final adaptation&lt;/h2&gt;&lt;p&gt;Maybeck saw an astounding portion of his work destroyed by fire. His&#xA;office, with his early records and drawings, burned in the fires that&#xA;followed the 1906 San Francisco earthquake. Innumerable early “Gothic&#xA;houses” were leveled in East Bay blazes. Wyntoon burned. Hearst Hall, of&#xA;course, burned. Twice Maybeck insists on destruction as a sort of final&#xA;adaptation—a recapture into a different aesthetic scheme, a radical&#xA;extension of the vine that softens the harshness of perfection.&lt;p&gt;The Palace of Fine Arts used the impression of ruination to its&#xA;advantage from the getgo. Maybeck aimed to set a somber mood, to give&#xA;the impression of a palace that was always &lt;em&gt;already lost&lt;/em&gt;.&lt;blockquote&gt;&lt;p&gt;[…] Frank Morton Todd gives complete technical and physical&#xA;descriptions of the structure, but when attempting to describe the&#xA;character of the building in his volumes, he hesitantly states&lt;blockquote&gt;&lt;p&gt;[…] The theme itself we might attempt to state as the mortality of&#xA;grandeur and to describe as having some affinity with our eternal&#xA;sorrows over the vanity of human wishes…&lt;p&gt;Some such feeling as this, though vague, must have come to every&#xA;responsive intelligence that looked across the Fine Arts Lagoon and the&#xA;Palace itself. It represented the beauty and grandeur of the past. &lt;em&gt;A&#xA;cloister enclosing nothing, a colonnade without a roof, stairs that&#xA;ended nowhere,&lt;/em&gt; a fane with a lonely votary kneeling at a dying&#xA;flame, fluted shafts that rose, &lt;em&gt;half hid in vines,&lt;/em&gt; from the&#xA;lush growth of an old swamp… all these things were in the picture.&lt;/blockquote&gt;&lt;p&gt;It was evident that Maybeck had succeeded in setting a mood. It was&#xA;also evident that he had achieved his goal of creating a beautiful&#xA;building. But all the words and the praise heaped on the building fail&#xA;to explain what it was about the architectural forms that contributed to&#xA;its universal appeal. Even when Maybeck wrote about the Palace, not one&#xA;word refers to the building itself, or even to any of its parts. All of&#xA;his explanation is devoted to the mood appropriate for an art&#xA;gallery––which was “a sad and serious matter”––and this is done through&#xA;&lt;em&gt;repeated allusions to the haunting character of remanants of past&#xA;civilizations and natural landscape forms.&lt;/em&gt; In his small booklet,&#xA;&lt;em&gt;The Palace of Fine Arts and Lagoon&lt;/em&gt;, he writes: “I find that the&#xA;keynote of a Fine Arts Palace should be that of Sadness, modified by the&#xA;feeling that beauty has a soothing influence.”&lt;a href=#fn13 class=footnote-ref id=fnref13 role=doc-noteref&gt;&lt;sup&gt;13&lt;/sup&gt;&lt;/a&gt;&lt;/blockquote&gt;&lt;p&gt;Eventually, in the decades after the Exposition, Maybeck’s&#xA;facsimile-ruination gave way to its less picturesque referent: by the&#xA;early sixties the structure was physically crumbling… which didn’t&#xA;concern Maybeck much at all.&lt;blockquote&gt;&lt;p&gt;During the period when funds were being gathered for restoration,&#xA;Maybeck had his own ideas concerning the Palace of Fine Arts. They&#xA;ranged from demolishing it in order to create an active community&#xA;center, to heavily planting its site with redwoods in order that&#xA;children of the future might find bits of ornament and sculpture of a&#xA;wondrous ruin of a previous generation among the trees.&lt;a href=#fn14 class=footnote-ref id=fnref14 role=doc-noteref&gt;&lt;sup&gt;14&lt;/sup&gt;&lt;/a&gt;&lt;/blockquote&gt;&lt;p&gt;Earlier, in 1947, Maybeck suggested encapsulating a ruined cathedral&#xA;in a transparent geodesic dome to serve as a memorial rather than&#xA;rebuilding it:&lt;blockquote&gt;&lt;p&gt;Several week after the conclusion of Fuller’s stay at the University&#xA;[Buckminster Fuller in 1950], Annie Maybeck telephoned to report that&#xA;Ben was most anxious to see me. She said “It has something to do with&#xA;that man and his domes.” When I arrived at Maybeck’s studio, he had&#xA;spread before him a copy of &lt;em&gt;Life&lt;/em&gt; magazine showing Sir Basil&#xA;Spence’s proposal for the reconstruction of Coventry Cathedral as a war&#xA;memorial. Maybeck was visibly agitated. “Look, look,” he said, “a&#xA;reconstructed Gothic cathedral is not what is needed; what should be&#xA;done is to preserve the bombed ruins as a memorial.” He then suggested&#xA;that a large glazed geodesic dome covering the ruins would be the&#xA;perfect solution and urged me to contact Fuller to initiate such a&#xA;project.&lt;a href=#fn15 class=footnote-ref id=fnref15 role=doc-noteref&gt;&lt;sup&gt;15&lt;/sup&gt;&lt;/a&gt;&lt;/blockquote&gt;&lt;p&gt;Like with the Palace of Fine Arts, the ruins in Coventry had an&#xA;affect all their own: the walls suggest vaults, the tracery suggests&#xA;glass, and so on. Rebuilding would mean losing a means of&#xA;&lt;em&gt;authentically&lt;/em&gt; signifying loss. If a memorial gestures at a&#xA;historic void, its form should gesture at precisely the structure that&#xA;was lost.&lt;h2 id=losing-hearst-hall&gt;Losing Hearst Hall&lt;/h2&gt;&lt;p&gt;It’s surprisingly hard to find the precise date for the fire that&#xA;destroyed Hearst Hall—even Cardwell just gives the year—perhaps because&#xA;it’s overshadowed by the resulting rush to commission Hearst Memorial&#xA;Gym. Cal’s 1923-1924 yearbook mentions the fire in a page anticipating&#xA;the rebuild:&lt;blockquote&gt;&lt;p&gt;[Hearst Memorial Gym] has for several years been merely a dream, but&#xA;at last our dreams have come true because Mr. William Randolph Hearst&#xA;has promised the women of the University a new Building. The other&#xA;Hearst Hall was used as a place for gathering as well as for a&#xA;gymnasium, but the new building will be used mainly for a gymnasium. The&#xA;former hall was a gift from the late Mrs. Phoebe Hearst to the&#xA;University, but it was destroyed during a fire on &lt;em&gt;June 20,&#xA;1922.&lt;/em&gt;&lt;a href=#fn16 class=footnote-ref id=fnref16 role=doc-noteref&gt;&lt;sup&gt;16&lt;/sup&gt;&lt;/a&gt;&lt;/blockquote&gt;&lt;p&gt;The June 24, 1922 &lt;em&gt;Daily Californian&lt;/em&gt; also dates the fire to&#xA;June 20th. William Randolph Hearst promised a replacement, by telegram,&#xA;the day after the blaze. “In fireproof materials.”&lt;a href=#fn17 class=footnote-ref id=fnref17 role=doc-noteref&gt;&lt;sup&gt;17&lt;/sup&gt;&lt;/a&gt;&#xA;Hearst specifically requested Maybeck get the assignment.&lt;a href=#fn18 class=footnote-ref id=fnref18 role=doc-noteref&gt;&lt;sup&gt;18&lt;/sup&gt;&lt;/a&gt;&lt;p&gt;Contemporary Blue &amp; Gold yearbooks are the best testament to&#xA;Hearst Hall’s adaptability. Plays, concerts, pep rallies, banquets,&#xA;dances, insensitive “Italian carnivals”… the Hall was home to student&#xA;events of all sorts.&lt;a href=#fn19 class=footnote-ref id=fnref19 role=doc-noteref&gt;&lt;sup&gt;19&lt;/sup&gt;&lt;/a&gt; Cardwell calls it “the center of&#xA;the campus social and cultural life;” that seems apt.&lt;div class=addendum data-date=&#34;June 22, 2022&#34;&gt;&lt;p&gt;Steven Finacom penned &lt;a href=https://www.eastbaytimes.com/2022/06/21/berkeley-a-look-back-hearst-hall-destroyed-in-tragic-1922-fire/&gt;an&#xA;article&lt;/a&gt; about the Hearst Hall fire on its hundredth anniversary,&#xA;including a contemporary account of the firefighting response and a&#xA;poetic description of the building.&lt;/div&gt;&lt;h1 id=modernism&gt;Modernism&lt;/h1&gt;&lt;p&gt;Cardwell works hard to differentiate Maybeck both from the Arts &amp;&#xA;Crafts tradition and from International Style modernism. Peeling Maybeck&#xA;away from Arts &amp; Crafts is definitely the harder task, but Cardwell&#xA;approaches both the same way: he diagnoses phobias in both traditions&#xA;and finds Maybeck projects that broke from them.&lt;blockquote&gt;&lt;p&gt;Even though much of Maybeck’s early work received strong acclaim from&#xA;people who were supporters of the ideas of John Ruskin and William&#xA;Morris, &lt;em&gt;he was not a proponent of a crafts revival.&lt;/em&gt; Maybeck&#xA;started his designs on premises more sophisticated than those attributed&#xA;to the Arts and Crafts movement. &lt;em&gt;Exploitation of structural order,&#xA;application of visual phenomena, and respect for new materials and&#xA;industrial processes,&lt;/em&gt; as well as a firm belief in the expressive&#xA;qualities of form were the bases for his work. A building was designed&#xA;in the manner that Maybeck felt was appropriate for the conditions of&#xA;the problem given. […] The unprecedented laminated wood arch framing of&#xA;Hearst Hall was designed primarily to solve the problems of dismantling&#xA;and reassembly and secondarily to provide a Gothic allusion of shape.&#xA;The fact that Maybeck was ready either to adapt historic forms or to&#xA;&lt;em&gt;explore the boundaries of current construction technology&lt;/em&gt; went&#xA;unnoticed by others advocating nature-inspired ornament and the infusion&#xA;of handcrafts into the building process.&lt;a href=#fn20 class=footnote-ref id=fnref20 role=doc-noteref&gt;&lt;sup&gt;20&lt;/sup&gt;&lt;/a&gt;&lt;/blockquote&gt;&lt;p&gt;Frankly, it’s a stretch to argue Maybeck’s buildings are just&#xA;&lt;em&gt;incidentally&lt;/em&gt; classics of California Arts &amp; Crafts.&#xA;&lt;em&gt;Maybe&lt;/em&gt; one could argue that A&amp;amp;C was already an established&#xA;Bay Area vernacular style, and so similarities in Maybeck’s projects&#xA;(like his monomaniacal respect for wood) are conventional. If he&#xA;articulated it, this argument would seem a litte too convenient:&#xA;Cardwell’s happy to attribute just about everything else to Maybeck’s&#xA;genius.&lt;p&gt;Maybeck &lt;em&gt;did&lt;/em&gt; make certain material decisions that’d make an&#xA;A&amp;amp;C purist wince (in one episode he orders industrial sash windows&#xA;for a church, to the manufacturer’s alarm). Nonetheless, Maybeck never&#xA;strayed far from his crafts aesthetic: heavy redwood beams, corbels, and&#xA;so on. All his technical innovation is subordinate, in his finished&#xA;projects, to that aesthetic effect. That’s precisely why Cardwell can’t&#xA;identify him with modernism: a true modernist would avoid the old, the&#xA;romantic.&lt;blockquote&gt;&lt;p&gt;The immediate reaction was to look at Maybeck as a forerunner of the&#xA;modern movement. It is true that some of his architectural innovations&#xA;broke with the immediate past and used products of a machine technology&#xA;in fresh and imaginative solutions to building problems. However, in the&#xA;sense of “out with the old order and in with the new,” Maybeck was not a&#xA;revolutionary. The architect’s primary function, he reasoned, was to&#xA;fulfill one of man’s greatest yearnings––to be surrounded by beauty. As&#xA;an architect he strove to produce, with modern materials and techniques,&#xA;an architecture as beautiful and meaningful at that which had been&#xA;created in past times and for past cultures.&lt;a href=#fn21 class=footnote-ref id=fnref21 role=doc-noteref&gt;&lt;sup&gt;21&lt;/sup&gt;&lt;/a&gt;&lt;/blockquote&gt;&lt;p&gt;Their respective phobias aside, I’m not the first to claim there’s an&#xA;intersection between Arts &amp; Crafts and modernism. Cardwell invokes&#xA;Nikolaus Pevsner (&lt;em&gt;Pioneers of Modern Design&lt;/em&gt;, 1949) to claim “in&#xA;architecture, particularly in the domestic field, [Arts &amp; Crafts&#xA;movement] architects produced designs with a basic simplicity and&#xA;functionalism that have been identified as the beginnings of modern&#xA;architecture.” Some of their efforts are “euphemistically called&#xA;‘rustic,’” but Maybeck’s are simple and functional.&lt;a href=#fn22 class=footnote-ref id=fnref22 role=doc-noteref&gt;&lt;sup&gt;22&lt;/sup&gt;&lt;/a&gt;&lt;p&gt;The best argument I found (by far) is Lewis Mumford’s, cited in&#xA;passing. Pevsner worships Gropius; Mumford loathes Gropius, and&#xA;absolutely roasts Gropius’s imitators from his column in The New Yorker&#xA;(1947). For Pevsner, Arts &amp; Crafts posed modernism’s original&#xA;problem (the unity of structure and decoration); for Mumford, on the&#xA;other hand, Maybeck’s architecture softens modernism’s hard edge, makes&#xA;it less afraid of &lt;em&gt;comfort.&lt;/em&gt;&lt;blockquote&gt;&lt;p&gt;Certainly Le Corbusier’s dictum of the twenties––that the modern&#xA;house is a machine for living in––has become old hat; the modern accent&#xA;is on living, not on the machine. (This change must hit hardest those&#xA;academic American modernists who imitated Le Corbusier and Mies van der&#xA;Rohe and Gropius, as their fathers imitated the reigning lights of the&#xA;Ecole des Beaux-Arts.) Mr. Siegfried Giedion, once a leader of the&#xA;mechanical rigorists, has come out for the monumental and the symbolic,&#xA;and among the younger people an inclination to play with the “feeling”&#xA;elements in design––with color, texture, even painting and&#xA;sculpture––has become insuppressible. “Functionalism,” writes a rather&#xA;pained critic in a recent issue of the &lt;em&gt;Architectural Review&lt;/em&gt; of&#xA;London, “the only real aesthetic faith to which the modern architect&#xA;could lay claim in the inter-war years, is now, if not repudiated,&#xA;certainly called into question … by those who were formerly its most&#xA;illustrious supporters.”&lt;p&gt;We are bound to hear more of this development during the next decade,&#xA;but I am not alarmed by the prospect. What was called functionalism was&#xA;a one-sided interpretation of function, and it was an interpretation&#xA;that Louis Sullivan, who popularized the slogan “Form follows function,”&#xA;never subscribed to. The rigorists placed the mechanical functionings of&#xA;a building above its human functions; they neglected the feelings, the&#xA;sentiments, and the interests of the person who was to occupy it.&#xA;Instead of regarding engineering as a foundation for form, they treated&#xA;it as an end.&lt;p&gt;[…]&lt;p&gt;Well, it was time that some of our architects remembered the&#xA;non-mechanical and non-formal elements in architecture, that they&#xA;remembered what a building says as well as what it does. A house, as the&#xA;Uruguayan architect Julio Vilamajó has put it, should be as personal as&#xA;one’s clothes and should fit the family life just as well. This is not a&#xA;new doctrine in the United States. People like Bernhard Maybeck and&#xA;William Wilson Wurster in California, always practiced it, and they took&#xA;good care that their houses did not resemble factories or museums. So I&#xA;don’t propose to join the solemn gentelmen who, aware of this natural&#xA;reaction against a sterile and abstract modernism, are predicting a&#xA;return to the graceful sterotypes of the eighteenth century. Rather,&#xA;&lt;em&gt;I look for the continued spread, to every part of our country, of&#xA;that native and humane form of modernism&lt;/em&gt; which one might call the&#xA;Bay Region style, a free yet unobtrusive expression of the terrain, the&#xA;climate, and the way of life on the Coast. &lt;em&gt;That style took root&#xA;about fifty years ago in Berkeley, California, in the early work of John&#xA;Galen Howard and Maybeck,&lt;/em&gt; and by now, on the Coast, it is simply&#xA;taken for granted; no one out there is foolish enough to imagine that&#xA;there is any other proper way of building in our time. The style is&#xA;actually a product of the meeting of Oriental and Occidental&#xA;architectural traditions, and it is far more truly a universal style&#xA;than the so-called international style of the nineteen-thirties, since&#xA;it permits regional adaptations and modifications. Some of the best&#xA;examples of this at once native and universal tradition are being built&#xA;in New England. The change that is now going on in both Europe and&#xA;America means only that &lt;em&gt;modern architecture is past its adolescent&#xA;period, with its quixotic purities, its awkward self-consciousness, its&#xA;assertive dogmatism.&lt;/em&gt; The good young architects today are familiar&#xA;enough with the machine and its products and processes to take them for&#xA;granted, and so they are ready to relax and enjoy themselves a little.&#xA;That will be better for all of us.&lt;a href=#fn23 class=footnote-ref id=fnref23 role=doc-noteref&gt;&lt;sup&gt;23&lt;/sup&gt;&lt;/a&gt;&lt;/blockquote&gt;&lt;p&gt;I’m surprised that Mumford never mentions Arts &amp; Crafts, either&#xA;as a name for what he calls “the Bay Region style” or as a movement.&#xA;Does he identify Maybeck with it? If not, does he share Cardwell’s&#xA;reasons—that Maybeck wasn’t puritanical enough about manufactured&#xA;elements?&lt;/p&gt;&lt;h2 id=wurster-hall&gt;Wurster Hall&lt;/h2&gt;&lt;p&gt;I haven’t found anything suggesting a direct architectural influence&#xA;between Hearst and Wurster Halls. This is a little disappointing, but to&#xA;be expected—Hearst Hall was gone thirty-five years before Wurster Hall&#xA;was commissioned. Nonetheless, I’m confident in two connections. The&#xA;first is an indirect connection to that most-publicly-loathed feature of&#xA;Wurster, its use of concrete. The second connection is direct: the&#xA;architectural philosophy of William Wurster.&lt;p&gt;Maybeck frequently used the same materials for his projects’&#xA;structures and their interior and exterior textures. For the most part,&#xA;these were wood—redwood when it was widely available. In at least one&#xA;tight-budgeted project, the San Francisco Settlement Association’s Boys’&#xA;Club (1910), the outer walls and roof were left uncovered in the&#xA;interior; two sides of the same boards and shingles constituted the&#xA;outer and inner walls.&lt;a href=#fn24 class=footnote-ref id=fnref24 role=doc-noteref&gt;&lt;sup&gt;24&lt;/sup&gt;&lt;/a&gt; Wurster’s concrete interior is&#xA;analogous to Hearst Hall’s interior shakes: both buildings frankly reuse&#xA;their structural materials.&lt;p&gt;Maybeck even worked with raw concrete interiors. The First Church of&#xA;Christ, Scientist in Berkeley is &lt;em&gt;highly&lt;/em&gt; ornamented, but its&#xA;enormous carved beams rest on lightly-adorned concrete pillars. Much of&#xA;the exterior is concrete, hidden as it may be behind quintessentially&#xA;Maybeck trellises. Hearst Memorial Gym? Concrete. He surfaced his own&#xA;studio (1 Maybeck Twin Dr, Berkeley) in burlap sacks dipped in the&#xA;stuff, an experiment in quick construction, and built his son’s house in&#xA;the Berkeley hills out of pre-cast concrete slabs. “building was&#xA;designed in the manner that Maybeck felt was appropriate for the&#xA;conditions of the problem given.”&lt;figure&gt;&lt;img src=../img/hearst-hall/concrete.jpg alt=&#34;Maybeck’s bare-burlap-and-concrete studio in the Berkeley hills, now a City of Berkeley Landmark.&#34;&gt;&lt;figcaption aria-hidden=true&gt;Maybeck’s bare-burlap-and-concrete studio&#xA;in the Berkeley hills, now a &lt;a href=http://berkeleyplaques.org/plaque/the-maybeck-cottage/&gt;City of&#xA;Berkeley Landmark&lt;/a&gt;.&lt;a href=#fn25 class=footnote-ref id=fnref25 role=doc-noteref&gt;&lt;sup&gt;25&lt;/sup&gt;&lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;There are strong echoes of Maybeck’s philosophies in William&#xA;Wurster’s initial dreams for Wurster Hall—most prominently, his dream of&#xA;a building that will earn its character through use and adaptation.&lt;blockquote&gt;&lt;p&gt;“I wanted it to look like a ruin that no regent would like… It’s&#xA;absolutely unfinished, uncouth, and brilliantly strong… The Ark [the&#xA;preceding college of architecture], for instance, is a ripe building; it&#xA;has been lived in; it’s been used, it’s been beaten up and everything&#xA;else. It’s arrived. Our building will take twenty years to arrive.”&lt;a href=#fn26 class=footnote-ref id=fnref26 role=doc-noteref&gt;&lt;sup&gt;26&lt;/sup&gt;&lt;/a&gt;&lt;/blockquote&gt;&lt;p&gt;Like the inside-and-out shingles of Maybeck’s Boys’ Club, Wurster&#xA;Hall’s concrete serves a practical purpose: economy.&lt;blockquote&gt;&lt;p&gt;The decision to construct it of concrete inside and out reflected&#xA;both economic considerations and the aesthetic of the times. As often&#xA;happens within the subculture of architecture, the architects were&#xA;reacting to another building, the Yale School of Architecture, which had&#xA;been designed by Paul Rudolph. […] For Wurster such design was anathema;&#xA;he made his point loud and clear. “I want you to design a ruin,” he&#xA;would say, pounding the table for emphasis. Wurster’s idea of a “ruin”&#xA;was a building that achieved timelessness through freedom from stylistic&#xA;quirks. While Wurster Hall has been categorized generally as a Brutalist&#xA;design, the architects protest that the Brutalist aesthetic did not&#xA;cause their preoccupation with consistency in the use of materials and&#xA;forms.&lt;a href=#fn27 class=footnote-ref id=fnref27 role=doc-noteref&gt;&lt;sup&gt;27&lt;/sup&gt;&lt;/a&gt;&lt;/blockquote&gt;&lt;p&gt;Exposing ductwork and piping was similarly economical.&lt;blockquote&gt;&lt;p&gt;One aspect of the design that has been generally misinterpreted is&#xA;the exposure of the ductwork and of the other mechanical equipment. Far&#xA;from being an expression of style, the exposure was a means of avoiding&#xA;the tunnel-like corridors that a dropped ceiling could produce and to&#xA;obtain the effect of a high ceiling in the rooms. […] Having the&#xA;mechanical equipment exposed also made maintenance easier, and was&#xA;useful, to some extent, in teaching.&lt;/blockquote&gt;&lt;p&gt;Concessions as these might’ve been, Wurster’s architects hardly&#xA;adhered to Le Corbusier’s “machine for living” stricture. The hall’s&#xA;courtyard is a sentimental feature, a callback (albeit an economized on)&#xA;to the brick courtyard of the old John Galen Howard-designed&#xA;architecture college (“the Ark,” now North Gate Hall) Wurster Hall would&#xA;replace.&lt;blockquote&gt;&lt;p&gt;[Wurster] and others also considered a courtyard imperative. One of&#xA;the most cherished parts of the old Architecture building, called the&#xA;Ark, was the brick-paved court where social and ceremonial occasions&#xA;took place. The new court was intended to express continuity with the&#xA;old setting, to be the symbolic heart of the new college building.&lt;a href=#fn28 class=footnote-ref id=fnref28 role=doc-noteref&gt;&lt;sup&gt;28&lt;/sup&gt;&lt;/a&gt;&lt;/blockquote&gt;&lt;p&gt;This all seems like a Maybeckish mix of technical pragmatism and&#xA;personality, albeit in the colder (and more fire-resistant!) concrete&#xA;vernacular of the fifties. It’s not whimsical, but it’s honest; it was&#xA;built to be lived in. Wurster’s comment about the Regents is a little&#xA;insouciant, and I’m not sure Wurster Hall achieves “timelessness through&#xA;freedom from stylistic quirks,” but he’s right about how to judge the&#xA;building: what matters is &lt;em&gt;not&lt;/em&gt; the Regents’ kneejerk griping,&#xA;but how the building “arrives” via the life given it by educators and&#xA;students decades down the line. As Maybeck puts it, “you could get your&#xA;own personality into your house better if we stopped at the building&#xA;proper.”&lt;h1 id=on-sources&gt;On sources&lt;/h1&gt;&lt;p&gt;My access to resources on Maybeck are more or less limited to what’s&#xA;available online—Google Books and the Internet Archive have been&#xA;indispensible—and in the collections of the SF Public Library (which&#xA;includes Cardwell’s book and reference copies of vintage Blue and Gold&#xA;yearbooks). The Berkeley Library Digital Collections have scans of Daily&#xA;Cal papers dating back to 1898.&lt;p&gt;UC Berkeley holds several collections that might be of interest to&#xA;someone who &lt;em&gt;really&lt;/em&gt; wants to dive deep.&lt;ul&gt;&lt;li&gt;&lt;p&gt;The Berkeley College of Environmental Design’s &lt;a href=&#34;http://www.oac.cdlib.org/view?docId=tf0h4n986k;developer=local;style=oac4;doc.view=items&#34;&gt;Bernard&#xA;Maybeck collection&lt;/a&gt;.&lt;li&gt;&lt;p&gt;Bancroft Library’s &lt;a href=https://oac.cdlib.org/findaid/ark:/13030/kt6n39q43j/admin/&gt;William&#xA;Randolph Hearst papers&lt;/a&gt;.&lt;li&gt;&lt;p&gt;Berkeley’s Newspapers &amp; Microforms Library, for contemporary&#xA;reception of Maybeck’s architecture.&lt;/ul&gt;&lt;p&gt;Berkeley’s libraries have been closed because of COVID-19, and won’t&#xA;reopen until the fall.&lt;/p&gt;&lt;p&gt;One more text stands out, which deserves attention but doesn’t fit&#xA;neatly into this essay: Ursula K. Le Guin wrote an essay, &lt;a href=&#34;https://www.google.com/books/edition/Words_Are_My_Matter/CZKzDwAAQBAJ?hl=en&amp;amp;gbpv=1&amp;amp;dq=%22The%20house%20after%20all%20is%20only%20the%20shell%20and%20the%20real%20interest%20must%20come%20from%20those%20who%20are%20to%20live%20in%20it.%22&amp;amp;pg=PA51&amp;amp;printsec=frontcover&#34;&gt;“Living&#xA;in a Work of Art”&lt;/a&gt;, about growing up in Maybeck’s Schneider house.&#xA;She covers a lot of the same ground—even borrows some of the same quotes&#xA;from Cardwell, whose book she recommends—but she tells of growing up in&#xA;the house, &lt;em&gt;really&lt;/em&gt; living in it, before she understood its&#xA;architectural significance. “I have trouble distinguishing the ethical&#xA;from the aesthetic,” she writes,&lt;blockquote&gt;&lt;p&gt;Is it not fair to say that every building has a morality, in this&#xA;sense, and not merely a metaphorical one, in the honesty and integrity&#xA;of its design and materials, or the dishonesty expressed in&#xA;incompetence, incoherence, shoddiness, fakery, snobbery?&lt;p&gt;I think I absorbed this morality of the building as I did the smell&#xA;of redwood or the sense of complex space.&lt;p&gt;I think the moral conception of the building was as admirable as its&#xA;aesthetic conception, from which it is, to me, inseparable.&lt;a href=#fn29 class=footnote-ref id=fnref29 role=doc-noteref&gt;&lt;sup&gt;29&lt;/sup&gt;&lt;/a&gt;&lt;/blockquote&gt;&lt;aside id=footnotes class=&#34;footnotes footnotes-end-of-document&#34; role=doc-endnotes&gt;&lt;hr&gt;&lt;ol&gt;&lt;li id=fn1&gt;&lt;p&gt;Cardwell, Kenneth H. &lt;em&gt;Bernard Maybeck: artisan,&#xA;architect, artist.&lt;/em&gt; Santa Barbara and Salt Lake City: Peregrine&#xA;Smith, 1977. (48)&lt;a href=#fnref1 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn2&gt;&lt;p&gt;Cardwell, &lt;em&gt;Bernard Maybeck&lt;/em&gt;, 46–47.&lt;a href=#fnref2 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn3&gt;&lt;p&gt;Cardwell, &lt;em&gt;Bernard Maybeck&lt;/em&gt;, 86.&lt;a href=#fnref3 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn4&gt;&lt;p&gt;Cardwell, &lt;em&gt;Bernard Maybeck&lt;/em&gt;, 86.&lt;a href=#fnref4 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn5&gt;&lt;p&gt;Cardwell, &lt;em&gt;Bernard Maybeck&lt;/em&gt;, 47.&lt;a href=#fnref5 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn6&gt;&lt;p&gt;Cardwell, &lt;em&gt;Bernard Maybeck&lt;/em&gt;, 48.&lt;a href=#fnref6 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn7&gt;&lt;p&gt;Cardwell, &lt;em&gt;Bernard Maybeck&lt;/em&gt;, 49.&lt;a href=#fnref7 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn8&gt;&lt;p&gt;Wikimedia Commons contributors, &lt;a href=&#34;https://commons.wikimedia.org/w/index.php?title=File:American_homes_and_gardens_(1905)_(17963343698).jpg&amp;amp;oldid=478401747&#34;&gt;“File:American&#xA;homes and gardens (1905) (17963343698).jpg,”&lt;/a&gt; &lt;em&gt;Wikimedia Commons,&#xA;the free media repository&lt;/em&gt; (accessed July 6, 2021).&lt;a href=#fnref8 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn9&gt;&lt;p&gt;Cardwell, &lt;em&gt;Bernard Maybeck&lt;/em&gt;, 48.&lt;a href=#fnref9 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn10&gt;&lt;p&gt;Cardwell, &lt;em&gt;Bernard Maybeck&lt;/em&gt;, 106.&lt;a href=#fnref10 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn11&gt;&lt;p&gt;Mumford, Lewis. &lt;a href=https://archives.newyorker.com/newyorker/1947-10-11/flipbook/109/&gt;“The&#xA;Sky Line: Status Quo.”&lt;/a&gt; &lt;em&gt;The New Yorker,&lt;/em&gt; October 11, 1947.&#xA;(104-110)&lt;a href=#fnref11 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn12&gt;&lt;p&gt;Cardwell, &lt;em&gt;Bernard Maybeck&lt;/em&gt;, 189.&lt;a href=#fnref12 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn13&gt;&lt;p&gt;Cardwell, &lt;em&gt;Bernard Maybeck&lt;/em&gt;, 144.&lt;p&gt;Todd’s picture of “a cloister enclosing nothing, a colonnade without&#xA;a roof, stairs that ended nowhere” reminds me of a &lt;em&gt;very&#xA;abstract&lt;/em&gt; explanation of a cube with a missing octant from Peter&#xA;Eisenman:&lt;blockquote&gt;&lt;p&gt;Traditionally architecture, let’s say the last 400 years of&#xA;architecture, has been defined by what I call a humanist&#xA;architecture—that is, that man took the forms of his building and&#xA;related them back, and gave them some meaning, by virtue of their&#xA;relationship to some ideal, or more pure state. Something that was&#xA;symmetrical, something that was contained, something that was whole.&#xA;Something that was understandable as a more perfect condition than the&#xA;reality of which man lived in.&lt;p&gt;Now, my argument is that that ideal state that we can refer to and&#xA;get meaning from no longer exists. Man’s position relative to God and&#xA;nature has changed. Man has unwittingly unleashed natural forces of&#xA;terrible proportion which threatened to extinguish the civilization. We&#xA;have no model of architecture which defines that condition that is the&#xA;opposite of ideal. It is the negative of an ideal state. And I want to&#xA;try and say, “alright, what would a design process be that starts with&#xA;the negative of an ideal?”&lt;p&gt;Take these two objects. These are two partial cubes. They each have&#xA;an octant cut out of them. They could be seen to be in the process of&#xA;growing toward something ideal, or disappearing to a point; that is,&#xA;they are in a state of instability. And I want to try and suspend the&#xA;architecture between the former classical ideal notion of what&#xA;architecture was, and this negative which may represent man’s destiny&#xA;today—that is, the future which is, uh, nothing.&lt;/blockquote&gt;&lt;p&gt;I’m not convined Eisenman’s example. He calls his cubes, well,&#xA;“partial cubes”—they, like referential humanism before them, certainly&#xA;refer to some ideal (a platonic solid, no less!). I suppose Eisenman&#xA;isn’t arguing against &lt;em&gt;all reference&lt;/em&gt;, but rather the inheritance&#xA;of humanistic principles… but then the cube’s missing octant seems&#xA;superfluous: does the missing octant make it &lt;em&gt;less&lt;/em&gt; humanistic or&#xA;more? This seems to be of grave importance to Peter.&lt;p&gt;In any case, Eisenman and Maybeck are using geometric form similarly&#xA;to signify loss. We imagine the cube’s missing octant, we imagine a roof&#xA;on the PFA’s colonnade; in both cases, we realize an absence. The rise&#xA;of a staircase-to-nothing and the slant of a cube’s edges towards the&#xA;vertex opposite the void (Eisenman &lt;a href=../img/hearst-hall/eisenman.jpg&gt;touches it&lt;/a&gt; to punctuate&#xA;“disappearing to a point”) function similarly: they imply a motion&#xA;towards an end.&lt;p&gt;For Eisenman’s view of architecture and cubes, see Michael&#xA;Blackwood’s &lt;em&gt;Beyond Utopia: Changing Attitudes in American&#xA;Architecture&lt;/em&gt; (1983), 47:36–49:27. I found it on Kanopy.&lt;a href=#fnref13 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn14&gt;&lt;p&gt;Cardwell, &lt;em&gt;Bernard Maybeck&lt;/em&gt;, 152.&lt;div class=addendum data-date=&#34;July 10, 2021&#34;&gt;&lt;p&gt;Cardwell mentions the October 16, 1915 “Preservation Day” and&#xA;subsequent formation of a PFA preservation league, but plainly says they&#xA;failed and that the Palace fell to “forty-five years of disintegration&#xA;and decay;” (151) he skips, or misses, updates made in the thirties.&lt;blockquote&gt;&lt;p&gt;As a part of the process of permanent rehabilitation of the Palace of&#xA;Fine Arts, [painter Frank Joseph] Van Sloun was commissioned in the&#xA;spring of 1936 to execute eight large murals in the ceiling of the&#xA;octagonal rotunda […] His works are destined to be viewed by countless&#xA;tourists, for the lagoon and lovely building are always sought by the&#xA;sight-seer.&lt;/blockquote&gt;&lt;p&gt;Van Sloun’s murals were destroyed in the Palace’s 1962 demolition and&#xA;reconstruction. The above quote is from &lt;a href=&#34;https://digitalassets.lib.berkeley.edu/cara/ucb/text/Cara_Volume_08.pdf#page=130&#34;&gt;&lt;em&gt;California&#xA;Art Research,&lt;/em&gt; Volume 8&lt;/a&gt; (1937, pages 117-118), which describes&#xA;them.&lt;figure&gt;&lt;img src=../img/hearst-hall/van-sloun.jpg alt=&#34;Van Sloun’s “Jupiter” panel. Baird, Joseph A., Jr., photographer. Ceiling of Rotunda Dome, 1956. Photograph. From Wikimedia Commons, (accessed July 10, 2021).&#34;&gt;&lt;figcaption aria-hidden=true&gt;Van Sloun’s “Jupiter” panel. Baird,&#xA;Joseph A., Jr., photographer. &lt;em&gt;Ceiling of Rotunda Dome, 1956.&lt;/em&gt;&#xA;Photograph. From Wikimedia Commons, (&lt;a href=&#34;https://commons.wikimedia.org/w/index.php?title=File:Historic_American_Buildings_Survey_Joseph_A._Baird,_Jr.,_Photographer_Photo-_1956_CEILING_OF_ROTUNDA_DOME_-_Palace_of_Fine_Arts,_Baker_Street,_San_Francisco,_San_Francisco_HABS_CAL,38-SANFRA,77-3.tif&amp;amp;oldid=543584641&#34;&gt;accessed&#xA;July 10, 2021&lt;/a&gt;).&lt;/figcaption&gt;&lt;/figure&gt;&lt;/div&gt;&lt;a href=#fnref14 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn15&gt;&lt;p&gt;Cardwell, &lt;em&gt;Bernard Maybeck&lt;/em&gt;, 10. Spence’s design&#xA;&lt;em&gt;did&lt;/em&gt; preserve the footprint of the old cathedral, which now&#xA;encloses a courtyard; the new cathedral sits adjacent.&lt;a href=#fnref15 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn16&gt;&lt;p&gt;&lt;a href=&#34;https://www.google.com/books/edition/1925_Blue_Gold/TDNQAQAAMAAJ?hl=en&amp;amp;gbpv=0&#34;&gt;1925&#xA;Blue &amp; Gold: A Record of the College Year 1923-1924&lt;/a&gt;. United&#xA;States: University of California, 1924. (&lt;a href=&#34;https://books.google.com/books/content?id=TDNQAQAAMAAJ&amp;amp;pg=PP335&amp;amp;img=1&amp;amp;zoom=3&amp;amp;hl=en&amp;amp;bul=1&amp;amp;sig=ACfU3U2aHA-Yg-6PE7d6nKI2d7wNKEzGwg&amp;amp;ci=43%2C45%2C880%2C1287&amp;amp;edge=0&#34;&gt;336&lt;/a&gt;)&lt;a href=#fnref16 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn17&gt;&lt;p&gt;&lt;a href=https://digicoll.lib.berkeley.edu/record/63344&gt;“W. R. Hearst to&#xA;Rebuild Woman’s Gymnasium Given by His Mother.”&lt;/a&gt; &lt;em&gt;The Daily&#xA;Californian.&lt;/em&gt; June 24, 1922. (1, 6)&lt;a href=#fnref17 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn18&gt;&lt;p&gt;&lt;a href=https://digicoll.lib.berkeley.edu/record/63343&gt;“Will Choose Site&#xA;for a Quadrangle of Women’s Halls.”&lt;/a&gt; &lt;em&gt;The Daily Californian.&lt;/em&gt;&#xA;June 27, 1922.&lt;p&gt;This article mentions plans to develop a whole women’s quad, and&#xA;Robert Gordon Sproul (then Comptroller, later President of the&#xA;University of California) reports that the Regents are “extremely&#xA;anxious, according to Mr. Sproul [Robert Gordon, then Comptroller and&#xA;later President of the University of California], to embark as soon as&#xA;practicable upon a policy of supplying dormitories than women.”&lt;p&gt;Cardwell mentions that Maybeck had grand plans too (&lt;a href=&#34;http://www.oac.cdlib.org/ark:/13030/tf096n98dc/?brand=oac4&#34;&gt;his&#xA;early sketches&lt;/a&gt; included a complex of buildings—not just a gym, but&#xA;also a domed auditorium, museums, etc.). The University reined in&#xA;Maybeck and only built the gym. Maybe the Regents were more anxious&#xA;about the project’s soonness than its scope…&lt;a href=#fnref18 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn19&gt;&lt;p&gt;&lt;a href=https://archive.org/details/bub_gb_zPM2AQAAMAAJ/page/n339/mode/2up&gt;1918&#xA;Blue &amp; Gold: A Record of the College Year 1916-1917&lt;/a&gt;.  United&#xA;States: University of California, 1917. (&lt;a href=https://archive.org/details/bub_gb_zPM2AQAAMAAJ/page/n68/mode/1up&gt;56&lt;/a&gt;)&#xA;“Insensitive ‘Italian carnivals?’”&lt;blockquote&gt;&lt;p&gt;The main floor was arranged with numerous booths, decorated with&#xA;chili, garlic, and onions, in which artists and fortune-tellers were&#xA;busily engaged. Italian peasant girls in bandanas, bright-colored aprons&#xA;and skirts, sold balloons, flowers, and candy. Organ grinders, singers,&#xA;tarantula and dagger dancers afforded entertainment, and in the gaily&#xA;decorated annex there was dancing. Down-stairs, peasant girls sold&#xA;fruit, spaghetti, raviolas, and tamales.&lt;/blockquote&gt;&lt;p&gt;Yes, tamales.&lt;a href=#fnref19 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn20&gt;&lt;p&gt;Cardwell, &lt;em&gt;Bernard Maybeck&lt;/em&gt;, 83.&lt;a href=#fnref20 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn21&gt;&lt;p&gt;Cardwell, &lt;em&gt;Bernard Maybeck&lt;/em&gt;, 236.&lt;a href=#fnref21 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn22&gt;&lt;p&gt;Cardwell, &lt;em&gt;Bernard Maybeck&lt;/em&gt;, 84.&lt;p&gt;I’m a little skeptical of Cardwell’s characterization. Pevsner seems&#xA;to be praising C. R. Ashbee, and only because he renounced William&#xA;Morris’s “intellectual Ludditism;” “the true pioneers of the Modern&#xA;Movement are those who from the outset stood for machine art.” His&#xA;description of Morris is blistering, though Morris gets &lt;em&gt;some&lt;/em&gt;&#xA;credit for emphasizing stylistic unity. See &lt;a href=https://archive.org/details/pioneersofmodern00niko/page/24/mode/2up&gt;&lt;em&gt;Pioneers&#xA;of Modern Design: from William Morris to Walter Gropius&lt;/em&gt;&lt;/a&gt;, pages&#xA;24–26.&lt;a href=#fnref22 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn23&gt;&lt;p&gt;Mumford, Lewis. &lt;a href=https://archives.newyorker.com/newyorker/1947-10-11/flipbook/109/&gt;“The&#xA;Sky Line: Status Quo.”&lt;/a&gt; &lt;em&gt;The New Yorker,&lt;/em&gt; October 11, 1947.&#xA;(104-110)&lt;a href=#fnref23 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn24&gt;&lt;p&gt;Cardwell, &lt;em&gt;Bernard Maybeck&lt;/em&gt;, 160.&lt;a href=#fnref24 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn25&gt;&lt;p&gt;Cardwell, &lt;em&gt;Bernard Maybeck&lt;/em&gt;, 215.&lt;a href=#fnref25 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn26&gt;&lt;p&gt;Woodbridge, Sally. &lt;a href=../img/hearst-hall/woodbridge-1984.pdf&gt;“Reflections on the&#xA;Founding: Wurster Hall and The College of Environmental Design [Two&#xA;Place Tales].”&lt;/a&gt; &lt;em&gt;Places&lt;/em&gt; 1, no. 4 (1984).&lt;a href=#fnref26 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn27&gt;&lt;p&gt;Woodbridge, Sally. &lt;a href=../img/hearst-hall/woodbridge-1984.pdf&gt;“Reflections on the&#xA;Founding: Wurster Hall and The College of Environmental Design [Two&#xA;Place Tales].”&lt;/a&gt; &lt;em&gt;Places&lt;/em&gt; 1, no. 4 (1984).&lt;a href=#fnref27 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn28&gt;&lt;p&gt;Woodbridge, Sally. &lt;a href=../img/hearst-hall/woodbridge-1984.pdf&gt;“Reflections on the&#xA;Founding: Wurster Hall and The College of Environmental Design [Two&#xA;Place Tales].”&lt;/a&gt; &lt;em&gt;Places&lt;/em&gt; 1, no. 4 (1984).&lt;a href=#fnref28 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn29&gt;&lt;p&gt;Le Guin, Ursula K. “Living in a Work of Art.” &lt;em&gt;Words&#xA;Are My Matter: Writings About Life and Books, 2000-2016, with a Journal&#xA;of a Writer’s Week.&lt;/em&gt; Small Beer Press, 2016. (61–62)&lt;a href=#fnref29 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;/ol&gt;&lt;/aside&gt;</content>
    <link href="https://lukasschwab.me/blog/gen/hearst-hall-continued-reading.html" rel="alternate"></link>
    <summary type="html">Extended notes on *Hearst Hall.* This essay reviews *Bernard Maybeck: Artisan, Architect, Artist* by rehashing old arguments with new evidence: extended excerpts from that text, from Sally Woodbridge&#39;s history of Wurster Hall, and zingers (&#34;quixotic purities...&#34;) from Lewis Mumford.</summary>
  </entry>
  <entry>
    <title>Genres and Subtraction Tests</title>
    <updated>2021-06-13T00:00:00Z</updated>
    <id>https://lukasschwab.me/blog/gen/subtraction-tests.html</id>
    <content type="html">&lt;html xmlns=http://www.w3.org/1999/xhtml lang xml:lang&gt;&lt;meta charset=utf-8&gt;&lt;meta name=generator content=&#34;pandoc&#34;&gt;&lt;meta name=viewport content=&#34;width=device-width,initial-scale=1,user-scalable=yes&#34;&gt;&lt;meta name=author content=&#34;Lukas Schwab&#34;&gt;&lt;meta name=dcterms.date content=&#34;2021-06-13&#34;&gt;&lt;title&gt;Genres and Subtraction Tests&lt;/title&gt;&lt;style&gt;&#xA;      code{white-space: pre-wrap;}&#xA;      span.smallcaps{font-variant: small-caps;}&#xA;      span.underline{text-decoration: underline;}&#xA;      div.column{display: inline-block; vertical-align: top; width: 50%;}&#xA;          &lt;/style&gt;&lt;link rel=stylesheet href=../styles/common.css&gt;&lt;link rel=stylesheet href=../styles/fence-extensions/addendum.css&gt;&lt;script data-goatcounter=https://lukasschwab.goatcounter.com/count async src=//gc.zgo.at/count.js&gt;&lt;/script&gt;&lt;a href=../index.html&gt;&amp;#171; blog&lt;/a&gt; &lt;span class=date&gt;2021-06-13&lt;/span&gt;&lt;header id=title-block-header&gt;&lt;h1 class=title&gt;Genres and Subtraction Tests&lt;/h1&gt;&lt;/header&gt;&lt;p&gt;The Subtraction Test isn’t a mental model in itself, but it relates&#xA;to one: genre. Given some corpus of texts, a genre is a pragmatic class&#xA;of some members in that corpus—that is, texts don’t &lt;em&gt;innately&lt;/em&gt;&#xA;belong to a genre, but we organize them (&lt;em&gt;a posteriori&lt;/em&gt; and&#xA;inductively&lt;a href=#fn1 class=footnote-ref id=fnref1 role=doc-noteref&gt;&lt;sup&gt;1&lt;/sup&gt;&lt;/a&gt;) into learned genres because it’s&#xA;analytically useful to do so.&lt;p&gt;Genres are hazily, even personally, defined; Genre Theory aims to&#xA;explain why some things belong and other things don’t. Rick Altman&#xA;imagines an illustrative argument about musicals:&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;A:&lt;/strong&gt; I mean, what do you do with Elvis Presley films?&#xA;You can hardly call them musicals.&lt;p&gt;&lt;strong&gt;B:&lt;/strong&gt; Why not? They’re loaded with songs and they’ve&#xA;got a narrative that ties the numbers together, don’t they?&lt;p&gt;&lt;strong&gt;A:&lt;/strong&gt; Yeah, I suppose. I guess you’d have to call&#xA;&lt;em&gt;Fun in Acapulco&lt;/em&gt; a musical, but it’s sure no &lt;em&gt;Singin’ in the&#xA;Rain.&lt;/em&gt; Now there’s a real musical.&lt;a href=#fn2 class=footnote-ref id=fnref2 role=doc-noteref&gt;&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt;&lt;/blockquote&gt;&lt;p&gt;Altman goes on to decompose film genre features into syntactic&#xA;features (structural elements, like that a Western involve a&#xA;governmental frontier, whether that be the borders of a town or some&#xA;intergalactic outer limit) and semantic features (characteristic&#xA;symbols: dust and dusters for the Western, song and dance for the&#xA;musical, and so on). I like this breakdown for film—syntaxes link&#xA;apparently distant texts—but it doesn’t generalize to the genre&#xA;mental-model.&lt;p&gt;We can make Altman more generic by setting aside the feature&#xA;categories (syntactic and semantic).&lt;p&gt;Suppose we have a genre &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;G&lt;/em&gt;&lt;/span&gt;.&#xA;The simplest definition for the genre would be to decompose it into a&#xA;complete set of necessary features &lt;span class=&#34;math inline&#34;&gt;{&lt;em&gt;f&lt;/em&gt;&lt;sub&gt;1&lt;/sub&gt;, &lt;em&gt;f&lt;/em&gt;&lt;sub&gt;2&lt;/sub&gt;, …&lt;em&gt;f&lt;/em&gt;&lt;sub&gt;&lt;em&gt;n&lt;/em&gt;&lt;/sub&gt;}&lt;/span&gt;,&#xA;where every feature &lt;em&gt;must&lt;/em&gt; exist in a text &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;T&lt;/em&gt;&lt;/span&gt; if it’s to be included in the&#xA;genre. That is, &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;T&lt;/em&gt; ∈ &lt;em&gt;G&lt;/em&gt; ⇔ {&lt;em&gt;f&lt;/em&gt;&lt;sub&gt;1&lt;/sub&gt;, &lt;em&gt;f&lt;/em&gt;&lt;sub&gt;2&lt;/sub&gt;, …&lt;em&gt;f&lt;/em&gt;&lt;sub&gt;&lt;em&gt;n&lt;/em&gt;&lt;/sub&gt;} ⊆ the&#xA;features of &lt;em&gt;T&lt;/em&gt;&lt;/span&gt;.&lt;a href=#fn3 class=footnote-ref id=fnref3 role=doc-noteref&gt;&lt;sup&gt;3&lt;/sup&gt;&lt;/a&gt; Using this to filter&#xA;texts gives us the “inclusive list,”&lt;blockquote&gt;&lt;p&gt;an unwieldy list of texts corresponding to a simple, tautological&#xA;definition of the genre (e.g., western=film that takes place in the&#xA;American West, or musical=film with diegetic music).&lt;a href=#fn4 class=footnote-ref id=fnref4 role=doc-noteref&gt;&lt;sup&gt;4&lt;/sup&gt;&lt;/a&gt;&lt;/blockquote&gt;&lt;p&gt;The inclusive list is unwieldy because it doesn’t leave much room for&#xA;nuance, e.g. to say that casting Elvis makes something less&#xA;characteristic of the musical genre. We can complicate the definitions&#xA;by adding unnecessary features and weighting them,&lt;a href=#fn5 class=footnote-ref id=fnref5 role=doc-noteref&gt;&lt;sup&gt;5&lt;/sup&gt;&lt;/a&gt; but&#xA;that gives us unwieldy definitions and a false sense of precision.&lt;/p&gt;&lt;p&gt;When I say we learn genres inductively, I mean we learn the genre&#xA;holistically: rather than accumulating a set of atomic features &lt;span class=&#34;math inline&#34;&gt;{&lt;em&gt;f&lt;/em&gt;&lt;sub&gt;1&lt;/sub&gt;, &lt;em&gt;f&lt;/em&gt;&lt;sub&gt;2&lt;/sub&gt;, …&lt;em&gt;f&lt;/em&gt;&lt;sub&gt;&lt;em&gt;n&lt;/em&gt;&lt;/sub&gt;}&lt;/span&gt;&#xA;that characterize a cluster of texts, we learn a genre &lt;a href=https://en.wikipedia.org/wiki/Duck_test&gt;duck test&lt;/a&gt;: “if it&#xA;looks like a [musical] and quacks like a [musical], it’s a [musical].”&#xA;Formally, a test function &lt;span class=&#34;math inline&#34;&gt;duck&lt;sub&gt;&lt;em&gt;G&lt;/em&gt;&lt;/sub&gt;(&lt;em&gt;T&lt;/em&gt;) → {true, false}&lt;/span&gt;&#xA;returns &lt;span class=&#34;math inline&#34;&gt;true&lt;/span&gt; if and only if the text&#xA;&lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;T&lt;/em&gt;&lt;/span&gt; belongs to the genre &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;G&lt;/em&gt;&lt;/span&gt;.&lt;p&gt;A &lt;em&gt;Subtraction Test&lt;/em&gt; (my invented term) is a way of revealing&#xA;relationships between particular features of a text and the genres to&#xA;which the text belongs.&lt;p&gt;Imagine decomposing a text &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;T&lt;/em&gt;&lt;/span&gt; into its features: &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;T&lt;/em&gt; = {&lt;em&gt;t&lt;/em&gt;&lt;sub&gt;1&lt;/sub&gt;, &lt;em&gt;t&lt;/em&gt;&lt;sub&gt;2&lt;/sub&gt;, …&lt;em&gt;t&lt;/em&gt;&lt;sub&gt;&lt;em&gt;n&lt;/em&gt;&lt;/sub&gt;}&lt;/span&gt;.&#xA;Subtraction-testing means comparing &lt;span class=&#34;math inline&#34;&gt;duck&lt;sub&gt;&lt;em&gt;G&lt;/em&gt;&lt;/sub&gt;(&lt;em&gt;T&lt;/em&gt;)&lt;/span&gt; and&#xA;&lt;span class=&#34;math inline&#34;&gt;duck&lt;sub&gt;&lt;em&gt;G&lt;/em&gt;&lt;/sub&gt;(&lt;em&gt;T&lt;/em&gt;\{&lt;em&gt;t&lt;/em&gt;&lt;sub&gt;&lt;em&gt;i&lt;/em&gt;&lt;/sub&gt;})&lt;/span&gt;:&#xA;does removing the feature &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;t&lt;/em&gt;&lt;sub&gt;&lt;em&gt;i&lt;/em&gt;&lt;/sub&gt;&lt;/span&gt; change&#xA;whether the text is a member of &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;G&lt;/em&gt;&lt;/span&gt;?&lt;table&gt;&lt;col style=&#34;width: 12%&#34;&gt;&lt;col style=&#34;width: 12%&#34;&gt;&lt;col style=&#34;width: 25%&#34;&gt;&lt;col style=&#34;width: 49%&#34;&gt;&lt;thead&gt;&lt;tr class=header&gt;&lt;th style=&#34;text-align: left;&#34;&gt;&lt;span class=&#34;math inline&#34;&gt;duck&lt;sub&gt;&lt;em&gt;G&lt;/em&gt;&lt;/sub&gt;(&lt;em&gt;T&lt;/em&gt;)&lt;/span&gt;&lt;th style=&#34;text-align: left;&#34;&gt;&lt;span class=&#34;math inline&#34;&gt;duck&lt;sub&gt;&lt;em&gt;G&lt;/em&gt;&lt;/sub&gt;(&lt;em&gt;T&lt;/em&gt;\{&lt;em&gt;t&lt;/em&gt;&lt;sub&gt;&lt;em&gt;i&lt;/em&gt;&lt;/sub&gt;})&lt;/span&gt;&lt;th style=&#34;text-align: left;&#34;&gt;Implication for &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;G&lt;/em&gt;&lt;/span&gt;&lt;th style=&#34;text-align: left;&#34;&gt;Example&lt;tbody&gt;&lt;tr class=odd&gt;&lt;td style=&#34;text-align: left;&#34;&gt;&lt;span class=&#34;math inline&#34;&gt;true&lt;/span&gt;&lt;td style=&#34;text-align: left;&#34;&gt;&lt;span class=&#34;math inline&#34;&gt;true&lt;/span&gt;&lt;td style=&#34;text-align: left;&#34;&gt;&lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;t&lt;/em&gt;&lt;sub&gt;&lt;em&gt;i&lt;/em&gt;&lt;/sub&gt;&lt;/span&gt; is not&#xA;necessary.&lt;td style=&#34;text-align: left;&#34;&gt;&lt;em&gt;High Noon&lt;/em&gt; without revolvers is&#xA;still a Western.&lt;br&gt;&lt;span class=&#34;math inline&#34;&gt;∴&lt;/span&gt; a Western&#xA;needn’t include revolvers.&lt;tr class=even&gt;&lt;td style=&#34;text-align: left;&#34;&gt;&lt;span class=&#34;math inline&#34;&gt;true&lt;/span&gt;&lt;td style=&#34;text-align: left;&#34;&gt;&lt;span class=&#34;math inline&#34;&gt;false&lt;/span&gt;&lt;td style=&#34;text-align: left;&#34;&gt;&lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;t&lt;/em&gt;&lt;sub&gt;&lt;em&gt;i&lt;/em&gt;&lt;/sub&gt;&lt;/span&gt; may be&#xA;necessary.&lt;td style=&#34;text-align: left;&#34;&gt;&lt;em&gt;Annie&lt;/em&gt; without musical numbers is&#xA;no longer a musical.&lt;br&gt;&lt;span class=&#34;math inline&#34;&gt;∴&lt;/span&gt; a Musical&#xA;may need musical numbers.&lt;tr class=odd&gt;&lt;td style=&#34;text-align: left;&#34;&gt;&lt;span class=&#34;math inline&#34;&gt;false&lt;/span&gt;&lt;td style=&#34;text-align: left;&#34;&gt;&lt;span class=&#34;math inline&#34;&gt;true&lt;/span&gt;&lt;td style=&#34;text-align: left;&#34;&gt;&lt;span class=&#34;math inline&#34;&gt;¬&lt;em&gt;t&lt;/em&gt;&lt;sub&gt;&lt;em&gt;i&lt;/em&gt;&lt;/sub&gt;&lt;/span&gt; may be&#xA;sufficient.&lt;td style=&#34;text-align: left;&#34;&gt;I don’t know, can &lt;em&gt;you&lt;/em&gt; think of&#xA;one?&lt;tr class=even&gt;&lt;td style=&#34;text-align: left;&#34;&gt;&lt;span class=&#34;math inline&#34;&gt;false&lt;/span&gt;&lt;td style=&#34;text-align: left;&#34;&gt;&lt;span class=&#34;math inline&#34;&gt;false&lt;/span&gt;&lt;td style=&#34;text-align: left;&#34;&gt;&lt;span class=&#34;math inline&#34;&gt;¬&lt;em&gt;t&lt;/em&gt;&lt;sub&gt;&lt;em&gt;i&lt;/em&gt;&lt;/sub&gt;&lt;/span&gt; is not&#xA;sufficient.&lt;td style=&#34;text-align: left;&#34;&gt;&lt;em&gt;No Country for Old Men&lt;/em&gt; without&#xA;milk is &lt;em&gt;still&lt;/em&gt; not a musical.&lt;br&gt;&lt;span class=&#34;math inline&#34;&gt;∴&lt;/span&gt; milklessness does not a Musical make.&lt;/table&gt;&lt;p&gt;Note how the implications are qualified: subtraction tests are&#xA;confident when subtracting a feature &lt;em&gt;does not&lt;/em&gt; impact a text’s&#xA;classification. When the classification changes—either from &lt;span class=&#34;math inline&#34;&gt;true → false&lt;/span&gt; (indicating necessity) or from&#xA;&lt;span class=&#34;math inline&#34;&gt;false → true&lt;/span&gt; (sufficiency)—we learn&#xA;something about the directional influence of the feature but can’t make&#xA;too strong a claim. Maybe you don’t think &lt;em&gt;Star Wars&lt;/em&gt; is a&#xA;Western, but you imagine it’d pass if it didn’t have the space travel;&#xA;that means you think space travel makes a film less Western-y, but it&#xA;obviously &lt;em&gt;doesn’t&lt;/em&gt; mean every space-travel-free film is a&#xA;Western: &lt;em&gt;Annie&lt;/em&gt; ain’t.&lt;p&gt;Notably, this works for multi-feature sets as well as single&#xA;features: we can check &lt;span class=&#34;math inline&#34;&gt;duck&lt;sub&gt;&lt;em&gt;G&lt;/em&gt;&lt;/sub&gt;(&lt;em&gt;T&lt;/em&gt;\{&lt;em&gt;t&lt;/em&gt;&lt;sub&gt;&lt;em&gt;i&lt;/em&gt;&lt;/sub&gt;,&lt;em&gt;t&lt;/em&gt;&lt;sub&gt;&lt;em&gt;j&lt;/em&gt;&lt;/sub&gt;})&lt;/span&gt;.&#xA;Nothing guarantees that our features contribute &lt;em&gt;independently&lt;/em&gt;&#xA;to genre membership—rolling papers signify differently in front of a Bob&#xA;Marley poster and in a gumshoe’s breast pocket—but I rarely think about&#xA;independence. If you’re designing Taguchi experiments, either you’re&#xA;deep in the realm of false precision or your inductive categories are&#xA;already sophisticated.&lt;p&gt;I’ve been leaning on films because they’re popularly associated with&#xA;genre, but the model and test are portable to other informal clustering&#xA;activities. Do you cluster inbound support tickets to prioritize product&#xA;improvements? Pinning down litmus tests for whether a new ticket belongs&#xA;in a certain cluster means you can share the task with a person who&#xA;would naturally cluster differently.&lt;p&gt;We learn duck tests for informal categories all the time; Subtraction&#xA;Tests inch usefully towards formalism.&lt;p&gt;Caution: not everything that looks and quacks like a Subtraction Test&#xA;&lt;em&gt;is&lt;/em&gt; one. I’m only writing any of this down because Gilman’s&#xA;&lt;em&gt;The Theory of Gothic Architecture and the Effect of Shellfire at&#xA;Rheims and Soissons&lt;/em&gt;&lt;a href=#fn6 class=footnote-ref id=fnref6 role=doc-noteref&gt;&lt;sup&gt;6&lt;/sup&gt;&lt;/a&gt; falsely reminded me of Altman’s&#xA;Elvis example. Gilman, writing in 1920, discusses gothic cathedrals&#xA;damaged in World War I. The War did the subtraction for him, so he&#xA;needn’t &lt;em&gt;imagine&lt;/em&gt; structures without features: where artillery&#xA;removevd a particular feature, Gilman considers whether the damage&#xA;cascaded (e.g. in a collapse), which would indicate that the feature was&#xA;structurally necessary.&lt;p&gt;Structurally &lt;em&gt;unnecessary&lt;/em&gt; features, he reasons, are&#xA;expressions of a gothic architectural ideal unmuddled by the cathedral’s&#xA;physical pragmatics. The style is purest-expressed in the ornamentatal,&#xA;and Gilman’s contemporaries debated whether gothic cathedrals revealed&#xA;their true structures, which might’ve otherwise been hidden, or whether&#xA;the cathedrals were ornamented with faux-structure—piers that support&#xA;nothing, decorative buttresses, and so on. Gilman concludes the&#xA;cathedrals are sometimes honest—&lt;blockquote&gt;&lt;p&gt;Closely connected with the principle of logic is that of revelation&#xA;of structure for esthetic effect. Here again the ruins generally&#xA;illustrate the theory and confirm it in its broad application. In the&#xA;vaulting, the manner in which the breakage has occured, as well as its&#xA;extent, does confirm strikingly the theory that the ribs carry the vault&#xA;cells and are not only real functioning members, but are the most&#xA;important part of the vault.&lt;a href=#fn7 class=footnote-ref id=fnref7 role=doc-noteref&gt;&lt;sup&gt;7&lt;/sup&gt;&lt;/a&gt;&lt;/blockquote&gt;&lt;p&gt;—and sometimes not:&lt;blockquote&gt;&lt;p&gt;Now actually we find that some of the cases often considered as&#xA;revealed structure are not the real structure. At Soissons the single&#xA;shaft on the lower story of the nave piers does not really support the&#xA;vaulting shafts and, in turn, the vaults above […] It by no means&#xA;follows that the shaft is not good design, only that it does not express&#xA;the facts of this structure; it is in fact an eye-satisfying fiction,&#xA;although perhaps, a proper one.&lt;a href=#fn8 class=footnote-ref id=fnref8 role=doc-noteref&gt;&lt;sup&gt;8&lt;/sup&gt;&lt;/a&gt;&lt;/blockquote&gt;&lt;figure&gt;&lt;img src=../img/subtraction-tests/vaults.png alt=&#34;Left: “The Church at Nettancourt.” Right: “Shell Holes in Vaults: Rheims.” Gilman’s evidence for the structural necessity of vault ribs. He found vaults with just the ribs intact, but none where the cells outlasted the ribs.&#34;&gt;&lt;figcaption aria-hidden=true&gt;Left: “The Church at Nettancourt.” Right:&#xA;“Shell Holes in Vaults: Rheims.” Gilman’s evidence for the structural&#xA;necessity of vault ribs. He found vaults with just the ribs intact, but&#xA;none where the cells outlasted the ribs.&lt;a href=#fn9 class=footnote-ref id=fnref9 role=doc-noteref&gt;&lt;sup&gt;9&lt;/sup&gt;&lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;The first sign that Gilman’s treatment of subtraction and genre&#xA;differs from our Subtraction Tests is that subtractability leads Gilman&#xA;to the opposite conclusion. In the damaged cathedral, that “the single&#xA;shaft on the lower story of the nave piers does not really support the&#xA;vaulting shafts”—that those single shafts are subtractible in Gilman’s&#xA;sense—means we should consider them a purer reflection of a gothic&#xA;architect’s ideal. That’s &lt;em&gt;very&lt;/em&gt; different from concluding “&lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;t&lt;/em&gt;&lt;sub&gt;&lt;em&gt;i&lt;/em&gt;&lt;/sub&gt;&lt;/span&gt; [the lower&#xA;story of the nave piers] is not necessary” for a building to be gothic;&#xA;that may be true (a feature can be characteristic without being&#xA;necessary), but seeing a cathedral sans piers doesn’t give us evidence&#xA;to that effect. It doesn’t even mean the feature is insignificant to the&#xA;genre: that an architect chose a nonstructural pier rather than a&#xA;nonstructural doric column makes the building &lt;em&gt;more&lt;/em&gt; gothic, not&#xA;less.&lt;p&gt;The key difference is that Gilman &lt;em&gt;isn’t applying a duck&#xA;test.&lt;/em&gt; When he sees a partially destroyed vault, he asks not “is&#xA;this still gothic?,” but &lt;em&gt;“is this still standing?”&lt;/em&gt; He isn’t&#xA;comparing whether the building’s genre status changes, and he gives no&#xA;reason to believe it does (a building may go from “gothic cathedral” to&#xA;“gothic semi-ruins,” but nobody thinks it stops being gothic).&lt;p&gt;Gilman’s subtractions tell us about &lt;em&gt;architectural&#xA;intent&lt;/em&gt;—choices to build structurally unnecessary elements, despite&#xA;their difficulty and expense—not about necessity and sufficiency to his&#xA;architectural category. Be careful that your duck test is, in fact, a&#xA;duck test.&lt;div class=addendum data-date=&#34;Jun. 17, 2020&#34;&gt;&lt;p&gt;Is “gothic” a genre? The category certainly developed &lt;em&gt;a&#xA;posteriori&lt;/em&gt; and inductively. Its builders were aware of their style,&#xA;but I haven’t read any indication that they were self-awarely&#xA;participating in a break from the sacred architecture that came before&#xA;them. To the contrary,&lt;blockquote&gt;&lt;p&gt;The architect who built the choir of St Denis must have spoken to&#xA;Suger about the &lt;em&gt;arcus&lt;/em&gt; in the vaults; William of Sens in&#xA;speaking to the Abbot of Canterbury no doubt used the term &lt;em&gt;fornices&#xA;arcutae&lt;/em&gt;, and Villard de Honnecourt probably spoke to his&#xA;apprentices of Ogives. However, no name for the style itself is known to&#xA;have existed at this time; indeed, it is unlikely that any name did&#xA;exist, for, in the regions where the Gothic style was born and&#xA;developed, ‘building in the Gothic style’ was simply called building.&lt;a href=#fn10 class=footnote-ref id=fnref10 role=doc-noteref&gt;&lt;sup&gt;10&lt;/sup&gt;&lt;/a&gt;&lt;/blockquote&gt;&lt;p&gt;“Gothic” was only identified as a &lt;em&gt;genre&lt;/em&gt;, with common&#xA;architectural elements and decorative tendencies, in retrospect&#xA;(typical: we have to find noncontemporary names for yesterday’s&#xA;“modern”):&lt;blockquote&gt;&lt;p&gt;When in the course of the nineteenth century historical knowledge of&#xA;the birth of the Gothic style, its development, and its spread&#xA;increased, the Late Gothic style was still regarded as part of the&#xA;Gothic style. Moreover, the growing study of the essential Gothic&#xA;elements, beginning with Wetter’s book, and the attempts to interpret&#xA;the essence of the Gothic, which began with the work of Viollet-le-Duc,&#xA;and have continued to our own day, have neutralized the effects of&#xA;personal taste and have led to a more intensive analysis of the concept&#xA;of Gothic style.&lt;p&gt;The scholarly consideration of the Gothic style began with&#xA;descriptions of individual buildings and so came to the study of&#xA;individual members, such as pointed arches, piers, rib-vaults, windows,&#xA;doorways, roofs, and towers. At the same time a desire grew to&#xA;understand the essence of the Gothic style which could permeate such&#xA;divergent features as piers and windows. The Gothic style was said to&#xA;possess a picturesque quality, a quality of infinity, a vegetal quality,&#xA;a romantic quality. All these different concepts were first formulated&#xA;in the eighteenth century and were then considered more closely in the&#xA;nineteenth and systematically bound into one unified concept.&lt;a href=#fn11 class=footnote-ref id=fnref11 role=doc-noteref&gt;&lt;sup&gt;11&lt;/sup&gt;&lt;/a&gt;&lt;/blockquote&gt;&lt;p&gt;Interestingly, the genre-awareness of 19th-century restorationism&#xA;means “restoration” may be a misnomer for what Viollet-le-Duc went about&#xA;doing. Viollet-le-Duc was widely criticized for intervening&#xA;ahistorically in the buildings he restored, emboldened by his&#xA;conclusions about the driving principles of Gothic style, including&#xA;scrubbing them of &lt;em&gt;historically authentic&lt;/em&gt;, if post-gothic,&#xA;features. Perhaps the very notion of a unified principle is fraught:&#xA;even the individual cathedrals in Voillet-le-Duc’s canon were&#xA;constructed over the courses of centuries. Should we believe these&#xA;principles stayed fixed over several generations of builders?&lt;p&gt;Incidentally, Frankl also offers a better term than Gilman for style&#xA;expressed in the structurally unnecessary, a “margin of freedom:”&lt;blockquote&gt;&lt;p&gt;In other words, function always leaves a ‘margin of freedom’ which&#xA;cannot be resolved by utilitarian considerations and which is not&#xA;objectively pre-determined. […] Gothic vaults must be examined in the&#xA;light of their function; […] even when these functions have been&#xA;understood, the question as to why these mebers have so many different&#xA;profiles in different churches still remains unanswered, since each&#xA;profile fulfils the necessary static function, in so far as it exists&#xA;equally well.&lt;a href=#fn12 class=footnote-ref id=fnref12 role=doc-noteref&gt;&lt;sup&gt;12&lt;/sup&gt;&lt;/a&gt;&lt;/blockquote&gt;&lt;/div&gt;&lt;aside id=footnotes class=&#34;footnotes footnotes-end-of-document&#34; role=doc-endnotes&gt;&lt;hr&gt;&lt;ol&gt;&lt;li id=fn1&gt;&lt;p&gt;Olsen, Stein Haugom. &lt;a href=http://yoksis.bilkent.edu.tr/pdf/files/13620.pdf&gt;“The concept of&#xA;literary genre.”&lt;/a&gt; &lt;em&gt;Metodo. International Studies in Phenomenology&#xA;and Philosophy&lt;/em&gt; 6.1 (2018).&lt;a href=#fnref1 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn2&gt;&lt;p&gt;Altman, Rick. “A semantic/syntactic approach to film&#xA;genre.” &lt;em&gt;Cinema Journal&lt;/em&gt; (1984): 6-18.&lt;a href=#fnref2 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn3&gt;&lt;p&gt;This formulation is inspired by Olsen. See especially&#xA;page 56.&lt;p&gt;I’ve adjusted the notation, and I think Olsen’s direction is a little&#xA;misleading: theorizing about weighted features assumes we sort texts&#xA;into genres in a really precise way. In practice, when you ask yourself&#xA;“is &lt;em&gt;Fun in Acapulco&lt;/em&gt; a musical?,” you’re just applying a litmus&#xA;test rather than weighting and summing.&lt;p&gt;Extracting weights would be an interesting approach if we wanted to&#xA;take a corpus and learn features that predicted membership, but that&#xA;means starting with a corpus and working towards a genre definition,&#xA;which isn’t what Olsen’s discussing.&lt;a href=#fnref3 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn4&gt;&lt;p&gt;Altman, Rick. “A semantic/syntactic approach to film&#xA;genre.” &lt;em&gt;Cinema Journal&lt;/em&gt; (1984): 6-18.&lt;a href=#fnref4 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn5&gt;&lt;p&gt;Olsen, Stein Haugom. &lt;a href=http://yoksis.bilkent.edu.tr/pdf/files/13620.pdf&gt;“The concept of&#xA;literary genre.”&lt;/a&gt; &lt;em&gt;Metodo. International Studies in Phenomenology&#xA;and Philosophy&lt;/em&gt; 6.1 (2018).&lt;a href=#fnref5 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn6&gt;&lt;p&gt;Gilman, Roger. “The theory of gothic architecture and&#xA;the effect of shellfire at Rheims and Soissons.” &lt;em&gt;American Journal of&#xA;Archaeology&lt;/em&gt; 24, no. 1 (1920): 37-72.&lt;a href=#fnref6 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn7&gt;&lt;p&gt;Gilman, Roger. “The theory of gothic architecture and&#xA;the effect of shellfire at Rheims and Soissons.” &lt;em&gt;American Journal of&#xA;Archaeology&lt;/em&gt; 24, no. 1 (1920): 37-72.&lt;a href=#fnref7 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn8&gt;&lt;p&gt;Gilman, Roger. “The theory of gothic architecture and&#xA;the effect of shellfire at Rheims and Soissons.” &lt;em&gt;American Journal of&#xA;Archaeology&lt;/em&gt; 24, no. 1 (1920): 37-72.&lt;a href=#fnref8 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn9&gt;&lt;p&gt;Gilman, Roger. “The theory of gothic architecture and&#xA;the effect of shellfire at Rheims and Soissons.” &lt;em&gt;American Journal of&#xA;Archaeology&lt;/em&gt; 24, no. 1 (1920): 37-72.&lt;a href=#fnref9 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn10&gt;&lt;p&gt;Frankl, Paul, and Paul Crossley. Gothic architecture.&#xA;Vol. 58. Yale University Press, 2000. 217-219.&lt;a href=#fnref10 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn11&gt;&lt;p&gt;Frankl, Paul, and Paul Crossley. Gothic architecture.&#xA;Vol. 58. Yale University Press, 2000. 217-219.&lt;a href=#fnref11 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn12&gt;&lt;p&gt;Frankl, Paul, and Paul Crossley. Gothic architecture.&#xA;Vol. 58. Yale University Press, 2000. 228.&lt;a href=#fnref12 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;/ol&gt;&lt;/aside&gt;</content>
    <link href="https://lukasschwab.me/blog/gen/subtraction-tests.html" rel="alternate"></link>
    <summary type="html">Discusses &#39;genre&#39; as a generic model for informally learned categories, and a test for making them more formal by using a duck test to learn if a feature is necessary or sufficient for membership in a category.</summary>
  </entry>
  <entry>
    <title>Blogging with TikZ</title>
    <updated>2021-05-21T00:00:00Z</updated>
    <id>https://lukasschwab.me/blog/gen/blogging-with-tikz.html</id>
    <content type="html">&lt;html xmlns=http://www.w3.org/1999/xhtml lang xml:lang&gt;&lt;meta charset=utf-8&gt;&lt;meta name=generator content=&#34;pandoc&#34;&gt;&lt;meta name=viewport content=&#34;width=device-width,initial-scale=1,user-scalable=yes&#34;&gt;&lt;meta name=author content=&#34;Lukas Schwab&#34;&gt;&lt;meta name=dcterms.date content=&#34;2021-05-21&#34;&gt;&lt;title&gt;Blogging with TikZ&lt;/title&gt;&lt;style&gt;&#xA;      code{white-space: pre-wrap;}&#xA;      span.smallcaps{font-variant: small-caps;}&#xA;      span.underline{text-decoration: underline;}&#xA;      div.column{display: inline-block; vertical-align: top; width: 50%;}&#xA;          &lt;/style&gt;&lt;style&gt;pre &gt; code.sourceCode { white-space: pre; position: relative; }&#xA;pre &gt; code.sourceCode &gt; span { display: inline-block; line-height: 1.25; }&#xA;pre &gt; code.sourceCode &gt; span:empty { height: 1.2em; }&#xA;.sourceCode { overflow: visible; }&#xA;code.sourceCode &gt; span { color: inherit; text-decoration: inherit; }&#xA;div.sourceCode { margin: 1em 0; }&#xA;pre.sourceCode { margin: 0; }&#xA;@media screen {&#xA;div.sourceCode { overflow: auto; }&#xA;}&#xA;@media print {&#xA;pre &gt; code.sourceCode { white-space: pre-wrap; }&#xA;pre &gt; code.sourceCode &gt; span { text-indent: -5em; padding-left: 5em; }&#xA;}&#xA;pre.numberSource code&#xA;  { counter-reset: source-line 0; }&#xA;pre.numberSource code &gt; span&#xA;  { position: relative; left: -4em; counter-increment: source-line; }&#xA;pre.numberSource code &gt; span &gt; a:first-child::before&#xA;  { content: counter(source-line);&#xA;    position: relative; left: -1em; text-align: right; vertical-align: baseline;&#xA;    border: none; display: inline-block;&#xA;    -webkit-touch-callout: none; -webkit-user-select: none;&#xA;    -khtml-user-select: none; -moz-user-select: none;&#xA;    -ms-user-select: none; user-select: none;&#xA;    padding: 0 4px; width: 4em;&#xA;    color: #aaaaaa;&#xA;  }&#xA;pre.numberSource { margin-left: 3em; border-left: 1px solid #aaaaaa;  padding-left: 4px; }&#xA;div.sourceCode&#xA;  {   }&#xA;@media screen {&#xA;pre &gt; code.sourceCode &gt; span &gt; a:first-child::before { text-decoration: underline; }&#xA;}&#xA;code span.al { color: #ff0000; font-weight: bold; } /* Alert */&#xA;code span.an { color: #60a0b0; font-weight: bold; font-style: italic; } /* Annotation */&#xA;code span.at { color: #7d9029; } /* Attribute */&#xA;code span.bn { color: #40a070; } /* BaseN */&#xA;code span.bu { color: #008000; } /* BuiltIn */&#xA;code span.cf { color: #007020; font-weight: bold; } /* ControlFlow */&#xA;code span.ch { color: #4070a0; } /* Char */&#xA;code span.cn { color: #880000; } /* Constant */&#xA;code span.co { color: #60a0b0; font-style: italic; } /* Comment */&#xA;code span.cv { color: #60a0b0; font-weight: bold; font-style: italic; } /* CommentVar */&#xA;code span.do { color: #ba2121; font-style: italic; } /* Documentation */&#xA;code span.dt { color: #902000; } /* DataType */&#xA;code span.dv { color: #40a070; } /* DecVal */&#xA;code span.er { color: #ff0000; font-weight: bold; } /* Error */&#xA;code span.ex { } /* Extension */&#xA;code span.fl { color: #40a070; } /* Float */&#xA;code span.fu { color: #06287e; } /* Function */&#xA;code span.im { color: #008000; font-weight: bold; } /* Import */&#xA;code span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Information */&#xA;code span.kw { color: #007020; font-weight: bold; } /* Keyword */&#xA;code span.op { color: #666666; } /* Operator */&#xA;code span.ot { color: #007020; } /* Other */&#xA;code span.pp { color: #bc7a00; } /* Preprocessor */&#xA;code span.sc { color: #4070a0; } /* SpecialChar */&#xA;code span.ss { color: #bb6688; } /* SpecialString */&#xA;code span.st { color: #4070a0; } /* String */&#xA;code span.va { color: #19177c; } /* Variable */&#xA;code span.vs { color: #4070a0; } /* VerbatimString */&#xA;code span.wa { color: #60a0b0; font-weight: bold; font-style: italic; } /* Warning */&lt;/style&gt;&lt;link rel=stylesheet href=../styles/common.css&gt;&lt;link rel=stylesheet href=../styles/fence-extensions/addendum.css&gt;&lt;link rel=stylesheet href=../styles/fence-extensions/dirtree.css&gt;&lt;link rel=stylesheet href=../styles/fence-extensions/frame.css&gt;&lt;script data-goatcounter=https://lukasschwab.goatcounter.com/count async src=//gc.zgo.at/count.js&gt;&lt;/script&gt;&lt;a href=../index.html&gt;&amp;#171; blog&lt;/a&gt; &lt;span class=date&gt;2021-05-21&lt;/span&gt;&lt;header id=title-block-header&gt;&lt;h1 class=title&gt;Blogging with TikZ&lt;/h1&gt;&lt;/header&gt;&lt;p&gt;Programming Langauges and Compilers (CS 164) with Paul Hilfinger was&#xA;the hardest class I took in college, bar none. I recommend it to anyone&#xA;studying CS at Cal: the correspondence between formal grammars and state&#xA;machines-as-parsers has been pretty personally impactful. State machine&#xA;diagrams were an indispensible part of &lt;a href=https://inst.eecs.berkeley.edu/~cs164/fa20/notes/notes.pdf&gt;Hilfinger’s&#xA;notes on those subjects&lt;/a&gt;, and drawing out a state machine diagram to&#xA;tap out the state transitions is how I recall those notes now.&lt;p&gt;State machine diagrams are indespensible to &lt;a href=./borges-automata.html&gt;&lt;em&gt;State Machines via Jorge Luis&#xA;Borges&lt;/em&gt;&lt;/a&gt; because, without all my mnemonic drawing and tapping, I&#xA;wouldn’t have recognized the connection between text and theory. The&#xA;diagrams are also an indespensable way to &lt;em&gt;concretize&lt;/em&gt; the&#xA;examples in that post. I’m not prepared to explain (non)determinism&#xA;without converting a specific graph, and I don’t have the patience to&#xA;squeeze all ambiguity out of mock-Borges prose.&lt;p&gt;Diagramming also presented an opportunity to go &lt;em&gt;further&lt;/em&gt; than&#xA;the CS 164 notes: rather than just presenting diagrams and counting on&#xA;readers to understand the rules governing state transitions, I wanted&#xA;the diagrams to demonstrate those rules by &lt;em&gt;illustrating dynamic user&#xA;input.&lt;/em&gt;&lt;p&gt;I did all of this with TikZ, a LaTeX extension for specifying&#xA;diagrams (probably the same extension Hilfinger used). This might seem&#xA;like an odd choice for interactive demos, but there’s a logic behind the&#xA;bodge!&lt;h1 id=arriving-at-tikz&gt;Arriving at TikZ&lt;/h1&gt;&lt;p&gt;&lt;a href=https://github.com/lukasschwab/pandoc-blog&gt;&lt;code&gt;pandoc-blog&lt;/code&gt;&lt;/a&gt;,&#xA;the static site generator I use, is full of opinions. It uses Pandoc to&#xA;convert Markdown and LaTeX source to HTML documents in order to&lt;ul&gt;&lt;li&gt;&lt;p&gt;Avoid big dependencies.&lt;a href=#fn1 class=footnote-ref id=fnref1 role=doc-noteref&gt;&lt;sup&gt;1&lt;/sup&gt;&lt;/a&gt;&lt;li&gt;&lt;p&gt;Prefer print-friendliness.&lt;br&gt;The primary motivation in using&#xA;Pandoc was to be able to swap out converters: rendering a blog post as a&#xA;PDF should be as simple as &lt;code&gt;pandoc -s source.md -o out.pdf&lt;/code&gt;.&#xA;Avoid drawing content with Javascript: it shouldn’t take PhantomJS to&#xA;produce a PDF.&lt;li&gt;&lt;p&gt;Prefer semantic markup.&lt;br&gt;In practice, this mostly means not&#xA;abusing HTML elements to do things they aren’t intended to do, but it’s&#xA;also reason to prefer a diagram specified in the DOM over a raster image&#xA;(when possible).&lt;/ul&gt;&lt;p&gt;&lt;em&gt;State Machines&lt;/em&gt; imposed particular requirements. All of my&#xA;diagrams would have a great deal in common; they should represent states&#xA;and transitions, with labels, in a consistent style. Given that&#xA;similarity, I wanted a tool that’d let me define the state machines as&#xA;minimally as possible. Each diagram definition should be easy to read&#xA;and version.&lt;p&gt;Finally, I needed something I could make &lt;em&gt;interactive:&lt;/em&gt; node&#xA;appearances need to change according to user input.&lt;p&gt;Avoiding big dependencies and preferring print-friendliness meant&#xA;eschewing Javascript libraries for drawing graphs, even though a&#xA;JS-controlled &lt;code&gt;canvas&lt;/code&gt; element would probably serve well for&#xA;interactivity.&lt;a href=#fn2 class=footnote-ref id=fnref2 role=doc-noteref&gt;&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt;&lt;p&gt;That interactivity requirement, along with the preferences for&#xA;semantic markup and for concise and versionable definitions, rules out&#xA;tools for drawing graphs that only output raster images.&lt;p&gt;HTML &lt;code&gt;canvas&lt;/code&gt; and &lt;code&gt;svg&lt;/code&gt; elements are natural&#xA;fits for the technical requirements. They’re lightweight, reasonably&#xA;print-friendly, and semantic (which, in turn, means they’re scriptable&#xA;for interactivity). The trouble is SVG is just &lt;em&gt;too&lt;/em&gt; flexible—I&#xA;want to be quickly defining labeled states and edges rather than&#xA;individually arranging SVG elements.&lt;p&gt;That’s where TikZ comes in: it’s concise.&lt;a href=#fn3 class=footnote-ref id=fnref3 role=doc-noteref&gt;&lt;sup&gt;3&lt;/sup&gt;&lt;/a&gt; The&#xA;problem with TikZ is that it’s a LaTeX extension meant for generating&#xA;PostScript; I need it to generate SVG.&lt;h1 id=implementation&gt;Implementation&lt;/h1&gt;&lt;h2 id=converting-tikz-to-svg&gt;Converting TikZ to SVG&lt;/h2&gt;&lt;p&gt;Defining a state machine diagram in TikZ is fairly&#xA;straightforward:&lt;ol type=1&gt;&lt;li&gt;Define a set of states, with their positions, labels, and IDs.&lt;li&gt;Define a set of edges, with labels, between states.&lt;/ol&gt;&lt;p&gt;These steps are just a matter of using styles and macros provided by&#xA;the &lt;code&gt;tikz-automata&lt;/code&gt; TikZ library; for an in-depth&#xA;explanation, check out &lt;a href=https://www3.nd.edu/~kogge/courses/cse30151-fa17/Public/other/tikz_tutorial.pdf&gt;&lt;em&gt;Drawing&#xA;Finite State Machines in LaTeX using &lt;code&gt;tikz&lt;/code&gt;&lt;/em&gt;&lt;/a&gt;. In&#xA;practice, a TikZ state machine diagram is defined like this:&lt;div class=sourceCode id=cb1&gt;&lt;pre class=&#34;sourceCode tex&#34;&gt;&lt;code class=&#34;sourceCode latex&#34;&gt;&lt;span id=cb1-1&gt;&lt;a href=#cb1-1 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=kw&gt;\begin&lt;/span&gt;{&lt;span class=ex&gt;tikzpicture&lt;/span&gt;}&lt;/span&gt;&#xA;&lt;span id=cb1-2&gt;&lt;a href=#cb1-2 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;  &lt;span class=co&gt;% 1. Define the states, with their positions, labels, and IDs.&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb1-3&gt;&lt;a href=#cb1-3 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;  &lt;span class=fu&gt;\node&lt;/span&gt;[state, accepting, initial] (Rc) {&lt;span class=ss&gt;$C$&lt;/span&gt;};&lt;/span&gt;&#xA;&lt;span id=cb1-4&gt;&lt;a href=#cb1-4 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;  &lt;span class=fu&gt;\node&lt;/span&gt;[state, below=of Rc] (Rb) {&lt;span class=ss&gt;$B$&lt;/span&gt;};&lt;/span&gt;&#xA;&lt;span id=cb1-5&gt;&lt;a href=#cb1-5 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;  &lt;span class=fu&gt;\node&lt;/span&gt;[state, right=of Rc, yshift=-2cm] (Ra) {&lt;span class=ss&gt;$A$&lt;/span&gt;};&lt;/span&gt;&#xA;&lt;span id=cb1-6&gt;&lt;a href=#cb1-6 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;  &lt;span class=co&gt;% 2. Define the edges, with labels, between states.&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb1-7&gt;&lt;a href=#cb1-7 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;  &lt;span class=fu&gt;\draw&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb1-8&gt;&lt;a href=#cb1-8 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    (Rc) edge[bend right, left] node{1} (Rb)&lt;/span&gt;&#xA;&lt;span id=cb1-9&gt;&lt;a href=#cb1-9 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    (Rb) edge[bend right, right] node{0} (Rc)&lt;/span&gt;&#xA;&lt;span id=cb1-10&gt;&lt;a href=#cb1-10 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    (Rb) edge[bend right, below] node{0} (Ra)&lt;/span&gt;&#xA;&lt;span id=cb1-11&gt;&lt;a href=#cb1-11 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    (Ra) edge[loop right, right] node{1} (Ra)&lt;/span&gt;&#xA;&lt;span id=cb1-12&gt;&lt;a href=#cb1-12 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    (Ra) edge[bend right, above] node{1} (Rc);&lt;/span&gt;&#xA;&lt;span id=cb1-13&gt;&lt;a href=#cb1-13 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=kw&gt;\end&lt;/span&gt;{&lt;span class=ex&gt;tikzpicture&lt;/span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This syntax isn’t perfect—in particular, I found positioning nodes&#xA;pretty fiddly—but it’s concise (each node and edge on its own line) and&#xA;readable (as readable as the labels and IDs one chooses).&lt;p&gt;Normally this &lt;code&gt;tikzpicture&lt;/code&gt; would be embedded into a LaTeX&#xA;document; how can it be turned into a standalone SVG? There’s an &lt;a href=https://tex.stackexchange.com/a/51766/183126&gt;answer on the TeX&#xA;Stack Exchange&lt;/a&gt;:&lt;ol type=1&gt;&lt;li&gt;&lt;p&gt;Use the &lt;a href=https://ctan.org/pkg/standalone&gt;&lt;code&gt;standalone&lt;/code&gt;&lt;/a&gt; TeX&#xA;package to generate a PDF of just the TikZ figure, without any other&#xA;document cruft.&lt;li&gt;&lt;p&gt;Use &lt;a href=https://github.com/dawbarton/pdf2svg&gt;&lt;code&gt;pdf2svg&lt;/code&gt;&lt;/a&gt; to&#xA;convert that PDF to an SVG file. Miraculously—I have no idea how this&#xA;works—that SVG file is reasonably human-readable, if verbose: each&#xA;element in the PDF is converted to an SVG &lt;code&gt;path&lt;/code&gt;&#xA;element.&lt;/ol&gt;&lt;p&gt;Using &lt;code&gt;standalone&lt;/code&gt; is as simple as wrapping our&#xA;&lt;code&gt;tikzpicture&lt;/code&gt; in a document with the &lt;code&gt;standalone&lt;/code&gt;&#xA;class. Doing so (and adding some TikZ style directives to the preamble)&#xA;gives us our final definition of our figure:&lt;div class=sourceCode id=cb2&gt;&lt;pre class=&#34;sourceCode tex&#34;&gt;&lt;code class=&#34;sourceCode latex&#34;&gt;&lt;span id=cb2-1&gt;&lt;a href=#cb2-1 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=co&gt;% This preamble is reused for every diagram.&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb2-2&gt;&lt;a href=#cb2-2 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=bu&gt;\documentclass&lt;/span&gt;{&lt;span class=ex&gt;standalone&lt;/span&gt;}&lt;/span&gt;&#xA;&lt;span id=cb2-3&gt;&lt;a href=#cb2-3 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=bu&gt;\usepackage&lt;/span&gt;{&lt;span class=ex&gt;xcolor&lt;/span&gt;}&lt;/span&gt;&#xA;&lt;span id=cb2-4&gt;&lt;a href=#cb2-4 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=bu&gt;\usepackage&lt;/span&gt;{&lt;span class=ex&gt;tikz&lt;/span&gt;}&lt;/span&gt;&#xA;&lt;span id=cb2-5&gt;&lt;a href=#cb2-5 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=fu&gt;\usetikzlibrary&lt;/span&gt;{automata, positioning, arrows}&lt;/span&gt;&#xA;&lt;span id=cb2-6&gt;&lt;a href=#cb2-6 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=fu&gt;\tikzstyle&lt;/span&gt;{defaults}=[&lt;/span&gt;&#xA;&lt;span id=cb2-7&gt;&lt;a href=#cb2-7 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;  -&amp;gt;,&lt;/span&gt;&#xA;&lt;span id=cb2-8&gt;&lt;a href=#cb2-8 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;  &amp;gt;=stealth&amp;#39;,&lt;/span&gt;&#xA;&lt;span id=cb2-9&gt;&lt;a href=#cb2-9 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;  every state/.style={thick, fill=gray!10},&lt;/span&gt;&#xA;&lt;span id=cb2-10&gt;&lt;a href=#cb2-10 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;]&lt;/span&gt;&#xA;&lt;span id=cb2-11&gt;&lt;a href=#cb2-11 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;/span&gt;&#xA;&lt;span id=cb2-12&gt;&lt;a href=#cb2-12 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=kw&gt;\begin&lt;/span&gt;{&lt;span class=ex&gt;document&lt;/span&gt;}&lt;/span&gt;&#xA;&lt;span id=cb2-13&gt;&lt;a href=#cb2-13 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;  &lt;span class=fu&gt;\nopagecolor&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb2-14&gt;&lt;a href=#cb2-14 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;  &lt;span class=co&gt;% The same tikzpicture as before.&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb2-15&gt;&lt;a href=#cb2-15 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;  &lt;span class=kw&gt;\begin&lt;/span&gt;{&lt;span class=ex&gt;tikzpicture&lt;/span&gt;}&lt;/span&gt;&#xA;&lt;span id=cb2-16&gt;&lt;a href=#cb2-16 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    [defaults, node distance = 3cm]&lt;/span&gt;&#xA;&lt;span id=cb2-17&gt;&lt;a href=#cb2-17 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    &lt;span class=fu&gt;\node&lt;/span&gt;[state, accepting, initial] (Rc) {&lt;span class=ss&gt;$C$&lt;/span&gt;};&lt;/span&gt;&#xA;&lt;span id=cb2-18&gt;&lt;a href=#cb2-18 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    &lt;span class=fu&gt;\node&lt;/span&gt;[state, below=of Rc] (Rb) {&lt;span class=ss&gt;$B$&lt;/span&gt;};&lt;/span&gt;&#xA;&lt;span id=cb2-19&gt;&lt;a href=#cb2-19 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    &lt;span class=fu&gt;\node&lt;/span&gt;[state, right=of Rc, yshift=-2cm] (Ra) {&lt;span class=ss&gt;$A$&lt;/span&gt;};&lt;/span&gt;&#xA;&lt;span id=cb2-20&gt;&lt;a href=#cb2-20 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    &lt;span class=fu&gt;\draw&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb2-21&gt;&lt;a href=#cb2-21 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;      (Rc) edge[bend right, left] node{1} (Rb)&lt;/span&gt;&#xA;&lt;span id=cb2-22&gt;&lt;a href=#cb2-22 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;      (Rb) edge[bend right, right] node{0} (Rc)&lt;/span&gt;&#xA;&lt;span id=cb2-23&gt;&lt;a href=#cb2-23 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;      (Rb) edge[bend right, below] node{0} (Ra)&lt;/span&gt;&#xA;&lt;span id=cb2-24&gt;&lt;a href=#cb2-24 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;      (Ra) edge[loop right, right] node{1} (Ra)&lt;/span&gt;&#xA;&lt;span id=cb2-25&gt;&lt;a href=#cb2-25 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;      (Ra) edge[bend right, above] node{1} (Rc);&lt;/span&gt;&#xA;&lt;span id=cb2-26&gt;&lt;a href=#cb2-26 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;  &lt;span class=kw&gt;\end&lt;/span&gt;{&lt;span class=ex&gt;tikzpicture&lt;/span&gt;}&lt;/span&gt;&#xA;&lt;span id=cb2-27&gt;&lt;a href=#cb2-27 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=kw&gt;\end&lt;/span&gt;{&lt;span class=ex&gt;document&lt;/span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I wrote a short script, &lt;a href=https://github.com/lukasschwab/bin/blob/master/tikz2svg&gt;&lt;code&gt;tikz2svg&lt;/code&gt;&lt;/a&gt;,&#xA;which handles the TikZ → PDF → SVG conversion. Running&#xA;&lt;code&gt;tikz2svg nfa-conversion-pre.tex&lt;/code&gt; yields the SVG we’re&#xA;after:&lt;figure&gt;&lt;img src=../img/borges-automata/nfa-conversion-pre.svg alt=&#34;nfa-conversion-pre.svg: the state machine diagram produced by tikz2svg nfa-conversion-pre.tex.&#34;&gt;&lt;figcaption aria-hidden=true&gt;&lt;code&gt;nfa-conversion-pre.svg&lt;/code&gt;: the&#xA;state machine diagram produced by&#xA;&lt;code&gt;tikz2svg nfa-conversion-pre.tex&lt;/code&gt;.&lt;/figcaption&gt;&lt;/figure&gt;&lt;h2 id=building-images-in-pandoc-blog&gt;Building images in&#xA;&lt;code&gt;pandoc-blog&lt;/code&gt;&lt;/h2&gt;&lt;p&gt;&lt;code&gt;pandoc-blog&lt;/code&gt;’s build processes are &lt;a href=https://github.com/lukasschwab/pandoc-blog/blob/master/Makefile&gt;managed&#xA;with &lt;code&gt;make&lt;/code&gt;&lt;/a&gt;: if one modifies a post’s Markdown source,&#xA;the &lt;code&gt;gen/%.html&lt;/code&gt; target rebuilds that post’s generated HTML&#xA;document.&lt;p&gt;Converting &lt;code&gt;.tex&lt;/code&gt; precursors to &lt;code&gt;.svg&lt;/code&gt; images&#xA;&lt;em&gt;manually&lt;/em&gt; is a hassle. This reconversion should happen&#xA;automatically, the same way Markdown is reconverted to HTML: whenever a&#xA;&lt;code&gt;tex&lt;/code&gt; precursor is modified, rebuild the corresponding&#xA;SVG.&lt;p&gt;Doing this for &lt;em&gt;State Machines&lt;/em&gt; involved adding one Makefile&#xA;and modifying another. The relevant directories are structured like&#xA;this:&lt;div class=dirtree data-root=blog&gt;&lt;ul&gt;&lt;li&gt;&lt;span class=file&gt;Makefile&lt;/span&gt;&lt;li&gt;&lt;span class=directory&gt;posts&lt;/span&gt;&lt;ul&gt;&lt;li&gt;&lt;span class=file&gt;borges-automata.md&lt;/span&gt;&lt;/ul&gt;&lt;li&gt;&lt;span class=directory&gt;img&lt;/span&gt;&lt;ul&gt;&lt;li&gt;&lt;span class=directory&gt;borges-automata&lt;/span&gt;&lt;ul&gt;&lt;li&gt;&lt;span class=file&gt;Makefile&lt;/span&gt;&lt;li&gt;&lt;span class=file&gt;nfa-conversion-pre.tex&lt;/span&gt;&lt;li&gt;&lt;span class=file&gt;nfa-conversion-pre.svg&lt;/span&gt;&lt;/ul&gt;&lt;/ul&gt;&lt;li&gt;&lt;span class=directory&gt;gen&lt;/span&gt;&lt;ul&gt;&lt;li&gt;&lt;span class=file&gt;borges-automata.html&lt;/span&gt;&lt;/ul&gt;&lt;/ul&gt;&lt;/div&gt;&lt;p&gt;Most of this is typical of &lt;code&gt;pandoc-blog&lt;/code&gt;:&lt;ul&gt;&lt;li&gt;A root &lt;code&gt;blog/Makefile&lt;/code&gt; (re)builds the full site.&lt;li&gt;The &lt;code&gt;blog/posts&lt;/code&gt; directory contains Markdown source files&#xA;for each post; &lt;code&gt;make&lt;/code&gt; generates corresponding HTML files in&#xA;&lt;code&gt;blog/gen&lt;/code&gt;.&lt;li&gt;The &lt;code&gt;blog/img&lt;/code&gt; directory holds static assets that can be&#xA;referenced in post markup. Those assets are organized into&#xA;subdirectories for tidiness: one for each post.&lt;/ul&gt;&lt;p&gt;What’s &lt;em&gt;atypical&lt;/em&gt; is the inclusion of our precursor&#xA;(&lt;code&gt;nfa-conversion-pre.tex&lt;/code&gt;) and its build product&#xA;(&lt;code&gt;nfa-conversion-pre.svg&lt;/code&gt;) in&#xA;&lt;code&gt;blog/img/borges-automata&lt;/code&gt;: generated content outside of&#xA;&lt;code&gt;blog/gen&lt;/code&gt;!&lt;p&gt;&lt;code&gt;blog/img/borges-automata/Makefile&lt;/code&gt; builds all the&#xA;diagrams in &lt;code&gt;blog/img/borges-automata&lt;/code&gt;:&lt;div class=sourceCode id=cb3&gt;&lt;pre class=&#34;sourceCode makefile&#34;&gt;&lt;code class=&#34;sourceCode makefile&#34;&gt;&lt;span id=cb3-1&gt;&lt;a href=#cb3-1 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=dt&gt;SOURCES &lt;/span&gt;&lt;span class=ch&gt;=&lt;/span&gt;&lt;span class=st&gt; &lt;/span&gt;&lt;span class=ch&gt;$(&lt;/span&gt;&lt;span class=kw&gt;wildcard&lt;/span&gt;&lt;span class=st&gt; *.tex&lt;/span&gt;&lt;span class=ch&gt;)&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb3-2&gt;&lt;a href=#cb3-2 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=dt&gt;TARGETS &lt;/span&gt;&lt;span class=ch&gt;=&lt;/span&gt;&lt;span class=st&gt; &lt;/span&gt;&lt;span class=ch&gt;$(&lt;/span&gt;&lt;span class=kw&gt;patsubst&lt;/span&gt;&lt;span class=st&gt; %.tex&lt;/span&gt;&lt;span class=kw&gt;,&lt;/span&gt;&lt;span class=st&gt;%.svg&lt;/span&gt;&lt;span class=kw&gt;,&lt;/span&gt;&lt;span class=ch&gt;$(&lt;/span&gt;&lt;span class=dt&gt;SOURCES&lt;/span&gt;&lt;span class=ch&gt;))&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb3-3&gt;&lt;a href=#cb3-3 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;/span&gt;&#xA;&lt;span id=cb3-4&gt;&lt;a href=#cb3-4 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=dv&gt;all:&lt;/span&gt;&lt;span class=dt&gt; &lt;/span&gt;&lt;span class=ch&gt;$(&lt;/span&gt;&lt;span class=dt&gt;TARGETS&lt;/span&gt;&lt;span class=ch&gt;)&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb3-5&gt;&lt;a href=#cb3-5 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;/span&gt;&#xA;&lt;span id=cb3-6&gt;&lt;a href=#cb3-6 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=dv&gt;%.svg:&lt;/span&gt;&lt;span class=dt&gt; %.tex&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb3-7&gt;&lt;a href=#cb3-7 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    tikz2svg &lt;span class=ch&gt;$&amp;lt;&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb3-8&gt;&lt;a href=#cb3-8 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;/span&gt;&#xA;&lt;span id=cb3-9&gt;&lt;a href=#cb3-9 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=ot&gt;.PHONY:&lt;/span&gt;&lt;span class=dt&gt; clean&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb3-10&gt;&lt;a href=#cb3-10 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=dv&gt;clean:&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb3-11&gt;&lt;a href=#cb3-11 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    rm *.svg&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You can imagine any &lt;code&gt;blog/img/*/Makefile&lt;/code&gt; exposing the&#xA;same interface. The main &lt;code&gt;blog/Makefile&lt;/code&gt; can delegate&#xA;figure-generation to the &lt;code&gt;img&lt;/code&gt; subdirectory for each blog&#xA;post; this one happens to be TikZ-specific, but for future blog posts I&#xA;can write image-generating &lt;code&gt;Makefiles&lt;/code&gt; that encapsulate&#xA;whatever other behavior I need.&lt;p&gt;We can update &lt;code&gt;blog/Makefile&lt;/code&gt; to run &lt;code&gt;make&lt;/code&gt; in&#xA;each blog post’s subdirectory:&lt;div class=sourceCode id=cb4&gt;&lt;pre class=&#34;sourceCode makefile&#34;&gt;&lt;code class=&#34;sourceCode makefile&#34;&gt;&lt;span id=cb4-1&gt;&lt;a href=#cb4-1 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=dt&gt;POSTS&lt;/span&gt;&lt;span class=ch&gt;=$(&lt;/span&gt;&lt;span class=kw&gt;shell&lt;/span&gt;&lt;span class=st&gt; find posts/*&lt;/span&gt;&lt;span class=ch&gt;)&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-2&gt;&lt;a href=#cb4-2 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=co&gt;# OUT contains all names of static HTML targets corresponding to markdown files&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-3&gt;&lt;a href=#cb4-3 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=co&gt;# in the posts directory.&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-4&gt;&lt;a href=#cb4-4 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=dt&gt;OUT&lt;/span&gt;&lt;span class=ch&gt;=$(&lt;/span&gt;&lt;span class=kw&gt;patsubst&lt;/span&gt;&lt;span class=st&gt; posts/%.md&lt;/span&gt;&lt;span class=kw&gt;,&lt;/span&gt;&lt;span class=st&gt; gen/%.html&lt;/span&gt;&lt;span class=kw&gt;,&lt;/span&gt;&lt;span class=st&gt; &lt;/span&gt;&lt;span class=ch&gt;$(&lt;/span&gt;&lt;span class=dt&gt;POSTS&lt;/span&gt;&lt;span class=ch&gt;))&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-5&gt;&lt;a href=#cb4-5 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=co&gt;# IMAGEDIRS is a list of img/ subdirectories containing makefiles.&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-6&gt;&lt;a href=#cb4-6 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=dt&gt;IMAGEDIRS&lt;/span&gt;&lt;span class=ch&gt;=$(&lt;/span&gt;&lt;span class=kw&gt;shell&lt;/span&gt;&lt;span class=st&gt; find img/*/Makefile | xargs -0 -n1 dirname&lt;/span&gt;&lt;span class=ch&gt;)&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-7&gt;&lt;a href=#cb4-7 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-8&gt;&lt;a href=#cb4-8 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=dv&gt;all:&lt;/span&gt;&lt;span class=dt&gt; &lt;/span&gt;&lt;span class=ch&gt;$(&lt;/span&gt;&lt;span class=dt&gt;OUT&lt;/span&gt;&lt;span class=ch&gt;)&lt;/span&gt;&lt;span class=dt&gt; &lt;/span&gt;&lt;span class=ch&gt;$(&lt;/span&gt;&lt;span class=dt&gt;IMAGEDIRS&lt;/span&gt;&lt;span class=ch&gt;)&lt;/span&gt;&lt;span class=dt&gt; index.html&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-9&gt;&lt;a href=#cb4-9 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-10&gt;&lt;a href=#cb4-10 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=co&gt;# ... most targets unchanged from pandoc-blog/Makefile.&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-11&gt;&lt;a href=#cb4-11 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-12&gt;&lt;a href=#cb4-12 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=co&gt;# Runs `make` in the target directory.&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-13&gt;&lt;a href=#cb4-13 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=dv&gt;img/%:&lt;/span&gt;&lt;span class=dt&gt; img/%/*.tex img/%/Makefile&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-14&gt;&lt;a href=#cb4-14 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    &lt;span class=ch&gt;$(&lt;/span&gt;&lt;span class=dt&gt;MAKE&lt;/span&gt;&lt;span class=ch&gt;)&lt;/span&gt; -C &lt;span class=st&gt;&amp;quot;&lt;/span&gt;&lt;span class=ch&gt;$@&lt;/span&gt;&lt;span class=st&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-15&gt;&lt;a href=#cb4-15 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    &lt;span class=ch&gt;@&lt;/span&gt;&lt;span class=fu&gt;touch &lt;/span&gt;&lt;span class=st&gt;&amp;quot;&lt;/span&gt;&lt;span class=ch&gt;$@&lt;/span&gt;&lt;span class=st&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now the &lt;code&gt;make watch&lt;/code&gt; loop provided by&#xA;&lt;code&gt;blog/Makefile&lt;/code&gt; automatically rebuilds the diagrams whenever&#xA;we save a change to the TikZ source!&lt;p&gt;This isn’t a super novel way of composing Makefiles, but it’s a fun&#xA;complication of &lt;code&gt;pandoc-blog&lt;/code&gt;. I didn’t originally expect to&#xA;be generating images, and I’m pleased the root &lt;code&gt;Makefile&lt;/code&gt;&#xA;isn’t sullied with blog-post-specific targets.&lt;h2 id=adding-interactivity&gt;Adding interactivity&lt;/h2&gt;&lt;p&gt;Diagrams are well and good, but you can’t build an interactive state&#xA;machine demo without, well, implementing a state machine. Generically,&#xA;this means turning a topology (all the various states and transitions&#xA;between them) and initial state into some object that exposes an&#xA;interface for feeding it input.&lt;p&gt;I do this with an &lt;a href=https://github.com/lukasschwab/automata-demos/blob/53099893010121265cd00f854cee51105b50d9ff/automaton.js#L14-L76&gt;&lt;code&gt;Automaton&lt;/code&gt;&lt;/a&gt;&#xA;class, which I won’t discuss in depth. The key to an&#xA;&lt;code&gt;Automaton&lt;/code&gt;’s connection to an SVG diagram is buried in the&#xA;&lt;code&gt;nodes&lt;/code&gt; constructor argument: each state is defined with two&#xA;callback functions, &lt;code&gt;enter&lt;/code&gt; and &lt;code&gt;exit&lt;/code&gt;, which are&#xA;called when input causes the &lt;code&gt;Automaton&lt;/code&gt; to enter and leave&#xA;that state, respectively. When it enters state &lt;code&gt;A&lt;/code&gt;, it calls&#xA;&lt;code&gt;A.enter()&lt;/code&gt;; when it receives any subsequent input, it calls&#xA;&lt;code&gt;A.exit()&lt;/code&gt; before proceeding.&lt;p&gt;To represent the &lt;code&gt;Automaton&lt;/code&gt;’s state in the diagram,&#xA;&lt;code&gt;A.enter()&lt;/code&gt; should turn the SVG element corresponding to&#xA;&lt;code&gt;A&lt;/code&gt; blue; &lt;code&gt;A.exit()&lt;/code&gt; should reverse the change.&#xA;All we need is a reference to that SVG element.&lt;p&gt;The problem: those SVG elements were defined by &lt;code&gt;pdf2svg&lt;/code&gt;.&#xA;Some of them have automatically-assigned IDs, but (for some reason) the&#xA;circular paths demarcating states do not. I found the right paths with&#xA;Chrome’s dev tools and added &lt;code&gt;id&lt;/code&gt;s manually to match the TikZ&#xA;IDs:&lt;div class=sourceCode id=cb5&gt;&lt;pre class=&#34;sourceCode svg&#34;&gt;&lt;code class=&#34;sourceCode xml&#34;&gt;&lt;span id=cb5-1&gt;&lt;a href=#cb5-1 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&amp;lt;&lt;span class=kw&gt;path&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb5-2&gt;&lt;a href=#cb5-2 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=ot&gt;  id=&lt;/span&gt;&lt;span class=st&gt;&amp;quot;R0&amp;quot;&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb5-3&gt;&lt;a href=#cb5-3 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=ot&gt;  style=&lt;/span&gt;&lt;span class=st&gt;&amp;quot;fill-rule:nonzero;fill:rgb(94.999695%,94.999695%,94.999695%);fill-opacity:1;stroke-width:0.79701;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;&amp;quot;&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb5-4&gt;&lt;a href=#cb5-4 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=ot&gt;  d=&lt;/span&gt;&lt;span class=st&gt;&amp;quot;M 123.198937 42.518875 C 123.198937 49.397781 117.620812 54.972 110.745812 54.972 C 103.866906 54.972 98.292687 49.397781 98.292687 42.518875 C 98.292687 35.643875 103.866906 30.06575 110.745812 30.06575 C 117.620812 30.06575 123.198937 35.643875 123.198937 42.518875 Z M 123.198937 42.518875 &amp;quot;&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb5-5&gt;&lt;a href=#cb5-5 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=ot&gt;  transform=&lt;/span&gt;&lt;span class=st&gt;&amp;quot;matrix(1,0,0,-1,53.137,83.722)&amp;quot;&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb5-6&gt;&lt;a href=#cb5-6 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;/&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Which leaves us to define &lt;code&gt;enter&lt;/code&gt; and&#xA;&lt;code&gt;exit&lt;/code&gt;:&lt;div class=sourceCode id=cb6&gt;&lt;pre class=&#34;sourceCode javascript&#34;&gt;&lt;code class=&#34;sourceCode javascript&#34;&gt;&lt;span id=cb6-1&gt;&lt;a href=#cb6-1 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=kw&gt;const&lt;/span&gt; enter &lt;span class=op&gt;=&lt;/span&gt; () &lt;span class=kw&gt;=&amp;gt;&lt;/span&gt; {&lt;/span&gt;&#xA;&lt;span id=cb6-2&gt;&lt;a href=#cb6-2 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;  &lt;span class=bu&gt;document&lt;/span&gt;&lt;span class=op&gt;.&lt;/span&gt;&lt;span class=fu&gt;getElementById&lt;/span&gt;(&lt;span class=st&gt;&amp;quot;R0&amp;quot;&lt;/span&gt;)&lt;span class=op&gt;.&lt;/span&gt;&lt;span class=at&gt;style&lt;/span&gt;&lt;span class=op&gt;.&lt;/span&gt;&lt;span class=at&gt;fill&lt;/span&gt; &lt;span class=op&gt;=&lt;/span&gt; &lt;span class=st&gt;&amp;quot;cyan&amp;quot;&lt;/span&gt;&lt;span class=op&gt;;&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb6-3&gt;&lt;a href=#cb6-3 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;}&lt;/span&gt;&#xA;&lt;span id=cb6-4&gt;&lt;a href=#cb6-4 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=kw&gt;const&lt;/span&gt; exit &lt;span class=op&gt;=&lt;/span&gt; () &lt;span class=kw&gt;=&amp;gt;&lt;/span&gt; {&lt;/span&gt;&#xA;&lt;span id=cb6-5&gt;&lt;a href=#cb6-5 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;  &lt;span class=co&gt;// The original fill is a light grey.&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb6-6&gt;&lt;a href=#cb6-6 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;  &lt;span class=bu&gt;document&lt;/span&gt;&lt;span class=op&gt;.&lt;/span&gt;&lt;span class=fu&gt;getElementById&lt;/span&gt;(&lt;span class=st&gt;&amp;quot;R0&amp;quot;&lt;/span&gt;)&lt;span class=op&gt;.&lt;/span&gt;&lt;span class=at&gt;style&lt;/span&gt;&lt;span class=op&gt;.&lt;/span&gt;&lt;span class=at&gt;fill&lt;/span&gt; &lt;span class=op&gt;=&lt;/span&gt; &lt;span class=st&gt;&amp;quot;rgb(94.999695%,94.999695%,94.999695%)&amp;quot;&lt;/span&gt;&lt;span class=op&gt;;&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb6-7&gt;&lt;a href=#cb6-7 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;…and so on for each node in the graph; see the &lt;a href=../img/borges-automata/demo/deterministic-binary-fsa.html&gt;deterministic-binary-fsa.html&lt;/a&gt;&#xA;source for a full example of a modified SVG and &lt;code&gt;Automaton&lt;/code&gt;&#xA;configuration bundled into an HTML document.&lt;p&gt;The final demo—of a nondeterministic finite state machine handling&#xA;input &lt;em&gt;alongside&lt;/em&gt; its deterministic equivalent—introduces a last&#xA;complication. There’s no structural issue with plopping two generated&#xA;SVGs into an HTML document side-by-side, but there’s a semantic one: ID&#xA;collisions.&lt;p&gt;SVG’s &lt;code&gt;&amp;lt;use&gt;&lt;/code&gt; element lets you duplicate another&#xA;element by its ID. This is a neat little optimization: if you have a&#xA;complex path you need to use twice, better to define it once and reuse&#xA;it by reference! &lt;code&gt;pdf2svg&lt;/code&gt; seizes on this optimization. The&#xA;form of the digit &lt;code&gt;1&lt;/code&gt; is only defined once; all the other&#xA;&lt;code&gt;1&lt;/code&gt;s are defined, in source, by &lt;code&gt;&amp;lt;use&gt;&lt;/code&gt;&#xA;elements referencing the original. The browser looks up the original&#xA;element by its ID and re-renders it.&lt;p&gt;Remember how some of the &lt;code&gt;pdf2svg&lt;/code&gt;-generated elements have&#xA;IDs? Those are of the form &lt;code&gt;glyph0-0&lt;/code&gt;, &lt;code&gt;glyph0-1&lt;/code&gt;,&#xA;and so on, which means that &lt;em&gt;every&lt;/em&gt;&#xA;&lt;code&gt;pdf2svg&lt;/code&gt;-generated SVG has (and perhaps &lt;code&gt;use&lt;/code&gt;s) a&#xA;&lt;code&gt;glyph0-0&lt;/code&gt;.&lt;p&gt;This wouldn’t be an issue but for the way we’re combining them:&#xA;plopping the SVG source unceremoniously into an HTML document. When the&#xA;browser looks up the DFA’s &lt;code&gt;glyph0-0&lt;/code&gt; by reference… it finds&#xA;the &lt;em&gt;NFA’s&lt;/em&gt; &lt;code&gt;glyph0-0&lt;/code&gt; instead. The first diagram is&#xA;fine; the second one is a garbled mess of glyphs from the first.&lt;p&gt;The solution: prefix each SVG’s IDs. Find-and-replace is robust&#xA;enough:&lt;ol type=1&gt;&lt;li&gt;Replace all instances of &lt;code&gt;id=&#34;&lt;/code&gt; with&#xA;&lt;code&gt;id=&#34;{prefix}&lt;/code&gt;.&lt;li&gt;Replace all instances of &lt;code&gt;&#34;#&lt;/code&gt; with&#xA;&lt;code&gt;&#34;#{prefix}&lt;/code&gt;.&lt;/ol&gt;&lt;div class=addendum data-date=&#34;May 22, 2021&#34;&gt;&lt;p&gt;I’ve since &lt;a href=https://github.com/lukasschwab/bin/commit/9401b3a0c88eabbc0b79808766c7fb4e53db7a3d&gt;updated&lt;/a&gt;&#xA;&lt;code&gt;tikz2svg&lt;/code&gt; to do this ID replacement automatically; now it&#xA;prefixes generated SVG element IDs with the source filename. If you want&#xA;to reuse filenames, insert a random component instead.&lt;/div&gt;&lt;p&gt;The Javascript animating this compound diagram is somewhat more&#xA;complicated: it instantiates two &lt;code&gt;Automaton&lt;/code&gt;s, and feeds&#xA;inputs to them simultaneously. See the &lt;a href=../img/borges-automata/demo/nfa-conversion.html&gt;nfa-conversion.html&lt;/a&gt;&#xA;source for details.&lt;h1 id=future-work&gt;Future work&lt;/h1&gt;&lt;p&gt;Ideally, I’d like to have a script to convert a TikZ state machine&#xA;definition directly to an encapsulated demo.&lt;ol type=1&gt;&lt;li&gt;&lt;p&gt;TikZ itself is an academic diagramming syntax; it offers a great&#xA;deal of flexibility that must be excluded if the script is to extract a&#xA;working state machine definition (that is, a topology).&lt;li&gt;&lt;p&gt;The multi-step process for producing an SVG—renderinga PDF, then&#xA;running that through &lt;code&gt;pdf2svg&lt;/code&gt;—means TikZ-specified data like&#xA;state IDs are missing from the generated SVG. Remember: we re-added&#xA;these IDs manually.&lt;/ol&gt;&lt;p&gt;These issues are solved by avoiding TikZ, and instead building&#xA;diagrams from some other domain-specific format. Javascript&#xA;representations of the automaton topologies, like those configuring the&#xA;interactive demos, would work. On the other hand, that means resigning&#xA;TikZ’s advantages. Honestly? I don’t think I’ll bother!&lt;p&gt;I’d like to make some other incidental improvements (avoiding&#xA;embedded &lt;code&gt;iframe&lt;/code&gt;s; referencing a single LaTeX header file&#xA;between diagrams; things like that).&lt;p&gt;For now, I have what I need. Blogging with TikZ allowed me to avoid&#xA;big dependencies, to keep my blog print-friendly, to keep its markup&#xA;semantic, and to share interactive demos with the next generation&#xA;terrorized by CS 164.&lt;div class=addendum data-date=&#34;June 3, 2021&#34;&gt;&lt;p&gt;If my commitment to the ideological purity of my Pandoc-generated&#xA;blog seems extreme, check out &lt;a href=https://github.com/phiresky&gt;phiresky&lt;/a&gt;’s &lt;a href=https://phiresky.github.io/blog/2021/hosting-sqlite-databases-on-github-pages/&gt;“Hosting&#xA;SQLite databases on GitHub Pages”&lt;/a&gt;.&lt;p&gt;That blog’s &lt;a href=https://github.com/phiresky/blog/blob/master/posts/2021/hosting-sqlite-databases-on-github-pages.md&gt;written&#xA;as Pandoc Markdown&lt;/a&gt;; the Pandoc artifact is postprocessed with React&#xA;to generate charts and runnable code blocks (including queries of a&#xA;statically-hosted SQL database). &lt;em&gt;That’s&lt;/em&gt; commitment.&lt;/div&gt;&lt;aside id=footnotes class=&#34;footnotes footnotes-end-of-document&#34; role=doc-endnotes&gt;&lt;hr&gt;&lt;ol&gt;&lt;li id=fn1&gt;&lt;p&gt;Relevant: Maciej Cegłowski’s evergreen &lt;a href=https://idlewords.com/talks/website_obesity.htm#minimalism&gt;slides&#xA;on Chickenshit Minimalism&lt;/a&gt;. I’m proud to keep this site hard-to-read&#xA;without any help from npm.&lt;a href=#fnref1 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn2&gt;&lt;p&gt;Graph-drawing libraries are smaller than I expected, but&#xA;not so small. The entirety of &lt;em&gt;State Machines&lt;/em&gt; (complete with its&#xA;diagrams and demos) is about the same size as minified d3.js: 94.5 and&#xA;83.6 kB, respectively.&lt;a href=#fnref2 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn3&gt;&lt;p&gt;There was another draw to TikZ which I didn’t ever&#xA;ultimately achieve: I figured that, for the static diagrams, I could&#xA;define TikZ diagrams inline (in a LaTeX environment) in my post’s&#xA;Markdown source. I still think this is &lt;em&gt;possible&lt;/em&gt;, but it’d take&#xA;writing a custom Pandoc extension to extract the TikZ picture, convert&#xA;it to an SVG, and replace it with an HTML &lt;code&gt;img&lt;/code&gt; referencing&#xA;that SVG.&lt;p&gt;I decided it’d take too much fiddling to make that Pandoc extension&#xA;reusable (especially outside of &lt;code&gt;pandoc-blog&lt;/code&gt;), and that I&#xA;might not actually have reason to reuse it.&lt;a href=#fnref3 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;/ol&gt;&lt;/aside&gt;</content>
    <link href="https://lukasschwab.me/blog/gen/blogging-with-tikz.html" rel="alternate"></link>
    <summary type="html">Notes on using TikZ, a LaTeX extension for defining vector graphics, to create static diagrams and interactive demos for *State Machines via Jorge Luis Borges.* These notes cover my motivations, my tools for generating SVGs as I wrote, and how I modified the script-generated diagrams for interactivity.</summary>
  </entry>
  <entry>
    <title>State Machines via Jorge Luis Borges</title>
    <updated>2021-02-19T00:00:00Z</updated>
    <id>https://lukasschwab.me/blog/gen/borges-automata.html</id>
    <content type="html">&lt;html xmlns=http://www.w3.org/1999/xhtml lang xml:lang&gt;&lt;meta charset=utf-8&gt;&lt;meta name=generator content=&#34;pandoc&#34;&gt;&lt;meta name=viewport content=&#34;width=device-width,initial-scale=1,user-scalable=yes&#34;&gt;&lt;meta name=author content=&#34;Lukas Schwab&#34;&gt;&lt;meta name=dcterms.date content=&#34;2021-02-19&#34;&gt;&lt;title&gt;State Machines via Jorge Luis Borges&lt;/title&gt;&lt;style&gt;&#xA;      code{white-space: pre-wrap;}&#xA;      span.smallcaps{font-variant: small-caps;}&#xA;      span.underline{text-decoration: underline;}&#xA;      div.column{display: inline-block; vertical-align: top; width: 50%;}&#xA;          &lt;/style&gt;&lt;link rel=stylesheet href=../styles/common.css&gt;&lt;link rel=stylesheet href=../styles/fence-extensions/addendum.css&gt;&lt;link rel=stylesheet href=../styles/fence-extensions/firstcoltable.css&gt;&lt;link rel=stylesheet href=../styles/fence-extensions/frame.css&gt;&lt;link rel=stylesheet href=../styles/fence-extensions/gallery.css&gt;&lt;link rel=stylesheet href=../styles/fence-extensions/widetable.css&gt;&lt;script data-goatcounter=https://lukasschwab.goatcounter.com/count async src=//gc.zgo.at/count.js&gt;&lt;/script&gt;&lt;a href=../index.html&gt;&amp;#171; blog&lt;/a&gt; &lt;span class=date&gt;2021-02-19&lt;/span&gt;&lt;header id=title-block-header&gt;&lt;h1 class=title&gt;State Machines via Jorge Luis Borges&lt;/h1&gt;&lt;/header&gt;&lt;p&gt;This post is, first and foremost, an introduction to state machines&#xA;(also called “automata”) via an extended Borgean allegory about an&#xA;existence in a labyrinth, a sort of infinite gallery of mazes. It&#xA;introduces several significant concepts—equivalence, finitude,&#xA;acceptance, determinism, and the powerset construction—as a succession&#xA;of realizations about that existence.&lt;p&gt;Why Borges? I picked up &lt;a href=https://www.amazon.com/Labyrinths-Directions-Paperbook-Jorge-Borges/dp/0811216993&gt;&lt;em&gt;Labyrinths&lt;/em&gt;&lt;/a&gt;&#xA;in early February, an anthology and a maze of furtive interreferences in&#xA;its own right. Borges toys with combinatorics. He writes that “Lully’s&#xA;machine, Mill’s fear and Lasswitz’s chaotic library can be the subject&#xA;of jokes, but they exaggerate a propension which is common: making&#xA;metaphysics and the arts into a kind of play with combinations.” He&#xA;joins them, even reconstructing Lasswitz’s library in &lt;em&gt;The Library of&#xA;Babel.&lt;/em&gt; The stories and essays are replete with villas subdivided&#xA;into infinitely many rooms, repetitions collapsed into single events,&#xA;points that contain all points, and so on.&lt;p&gt;I realize adding the allegory risks muddling the subject matter. My&#xA;hope is that the metaphysical confusion of the labyrinth is relatable in&#xA;a way that makes the mathematics feel more familiar. If I get away with&#xA;that binding, it’ll work in reverse: if the math resembles familiar&#xA;philosophy, we can use the mathematical concepts as a grammar for their&#xA;philosophical doubles.&lt;div class=addendum data-date=&#34;May 22, 2021&#34;&gt;&lt;p&gt;I’ve written &lt;a href=./blogging-with-tikz.html&gt;another post&lt;/a&gt;&#xA;about generating these diagrams and interactives with TikZ.&lt;/div&gt;&lt;hr&gt;&lt;blockquote&gt;&lt;p&gt;All the parts of the house are repeated many times, any place is&#xA;another place. There is no one pool, courtyard, drinking trough, manger;&#xA;the mangers, drinking troughs, courtyards, pools are fourteen (infinite)&#xA;in number. The house is the same size as the world; or rather, it is the&#xA;world.&lt;p&gt;&lt;em&gt;The House of Asterion&lt;/em&gt;&lt;a href=#fn1 class=footnote-ref id=fnref1 role=doc-noteref&gt;&lt;sup&gt;1&lt;/sup&gt;&lt;/a&gt;&lt;/blockquote&gt;&lt;p&gt;Imagine, if you will—though you can relate to this freely if it&#xA;sounds familiar—that you live your life in a vast labyrinth of circular&#xA;rooms, with marked doors that open into passageways which bring you from&#xA;one room to the next. Each room is indistinguishable, except maybe by&#xA;the number and labels of its exits. You’re so disoriented that you can’t&#xA;know how you entered the room you’re in. In fact, there may be no way to&#xA;return to the room from whence you came—the passages seem to close up&#xA;behind you as you step out of them. There’s no backtracking; you can&#xA;only progress into the labyrinth, choosing among the doors each room&#xA;affords you.&lt;p&gt;In your heart you know this is not a labyrinth one &lt;em&gt;leaves.&lt;/em&gt;&#xA;The labyrinth is the same size as the world; or rather, it is the&#xA;world.&lt;p&gt;In one of the maze’s innumerable connected rooms you’re visited by&#xA;the whisper of a blind sorcerer. The sorcerer explains he built the&#xA;labyrinth—designed its general schematic, its endless succession of&#xA;identical rooms, the workings of its corridors, the labels on its&#xA;innumerable doors, etc.—but, blind, he can’t read the signs, can’t&#xA;travel the monotonous expanse of corridors and galleries he devised.&lt;p&gt;This is the sorcerer’s proposition: starting in some fixed room in&#xA;the maze, you are to travel through a series of doors, following exactly&#xA;his instructions for which door to select. If, at any point, he gives&#xA;you an instruction you can’t follow—gives you some label that does not&#xA;match any door out of the room you’re in—you can cry out, and (zap!) he&#xA;will teleport you back to the starting room to receive a new set of&#xA;instructions. When a set of instructions can be followed to the end, you&#xA;are (zap!) also teleported back.&lt;p&gt;In this way, with you as a proxy, the sorcerer explores the great&#xA;maze. In return, he promises, the many forms of the labyrinth and the&#xA;meaning of your hermitude will be revealed.&lt;p&gt;Naturally curious, you accept.&lt;p&gt;This assemblage—you in your maze, following instructions—is&#xA;essentially a state machine:&lt;ol type=1&gt;&lt;li&gt;&lt;p&gt;Each room is a state; you can be in one state at a time.&lt;li&gt;&lt;p&gt;The fixed topology of the labyrinth determines which state&#xA;transitions are allowed. A given room does not necessarily allow passage&#xA;into every other room in the labyrinth; you can only move to a&#xA;successive state available from your present state.&lt;li&gt;&lt;p&gt;Your task is to discover whether the sorcerer’s inputs can be&#xA;followed, whether they’re &lt;em&gt;acceptable&lt;/em&gt; to the state machine.&lt;a href=#fn2 class=footnote-ref id=fnref2 role=doc-noteref&gt;&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt;&lt;/ol&gt;&lt;h1 id=equivalence&gt;Equivalence&lt;/h1&gt;&lt;blockquote&gt;&lt;p&gt;The universe (which others call the Library) is composed of an&#xA;indefinite and perhaps infinite number of hexagonal galleries, with vast&#xA;air shafts between, surrounded by very low railings. From any of the&#xA;hexagons one can see, interminably, the upper and lower floors. The&#xA;distribution of the galleries is invariable.&lt;p&gt;&lt;em&gt;The Library of Babel&lt;/em&gt;&lt;/blockquote&gt;&lt;p&gt;The first room has two doors: one marked &lt;code&gt;left&lt;/code&gt;, the other&#xA;marked &lt;code&gt;right&lt;/code&gt;. The sorcerer’s first instruction,&#xA;&lt;code&gt;left&lt;/code&gt;, takes you through that left door and its narrow&#xA;passageway, into another room with two doors: one marked&#xA;&lt;code&gt;left&lt;/code&gt;, the other marked &lt;code&gt;right&lt;/code&gt;.&lt;p&gt;In fact, every door—&lt;code&gt;left&lt;/code&gt; or &lt;code&gt;right&lt;/code&gt;—opens on&#xA;another indistinguishable room with identical doors; there is always a&#xA;&lt;code&gt;left&lt;/code&gt; door and a &lt;code&gt;right&lt;/code&gt; door to pass through.&#xA;Each foray ends either with some unfollowable instruction (e.g. to take&#xA;a door marked &lt;code&gt;south&lt;/code&gt;, or &lt;code&gt;salad bar&lt;/code&gt;, or&#xA;&lt;code&gt;de Broglie&lt;/code&gt;: none of these doors is ever available) or with&#xA;the exhaustion of the sorcerer’s sequence, at which point (zap!) you&#xA;begin, with a new sequence, from the start. A lifetime of lefts and&#xA;rights passes. The sorcerer’s instructions are inexhaustible; some&#xA;sequences take eons to follow.&lt;p&gt;In fact, if it’s constructed how it appears—each room leading to two&#xA;more rooms—this labyrinth must fork infinitely towards the horizon,&#xA;right? And, since only one set of left-right instruction sequences leads&#xA;you to any one of the infinite rooms, the sorcerer has a literally&#xA;inexhaustible list of routes to explore: one route for each room.&lt;a href=#fn3 class=footnote-ref id=fnref3 role=doc-noteref&gt;&lt;sup&gt;3&lt;/sup&gt;&lt;/a&gt; We can diagram the start of this&#xA;maze as follows:&lt;figure&gt;&lt;img src=../img/borges-automata/infinite-maze.svg alt=&#34;A Garden of Forking Paths. Each room has two doors—one marked left, one marked right—each of which leads to a room with two doors, and so on ad infinitum. The ellipses at the right of this diagram indicate continuation. In fact, at each ellipsis begins a series of forks exactly like the one pictured.&#34;&gt;&lt;figcaption aria-hidden=true&gt;A Garden of Forking Paths. Each room has&#xA;two doors—one marked &lt;code&gt;left&lt;/code&gt;, one marked&#xA;&lt;code&gt;right&lt;/code&gt;—each of which leads to a room with two doors, and so&#xA;on ad infinitum. The ellipses at the right of this diagram indicate&#xA;continuation. In fact, at each ellipsis begins a series of forks exactly&#xA;like the one pictured.&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;While it explains what you observe of the maze, this seems a bleak&#xA;existential outlook… but you’re rightfully suspicious of existential&#xA;certitudes! In fact, out of the mire of indistinguishable rooms and&#xA;doors and passageways surfaces a suspicion about the maze itself.&lt;p&gt;The sorcerer’s instructions seem farsical. Predictably, choosing the&#xA;&lt;code&gt;left&lt;/code&gt; door or the &lt;code&gt;right&lt;/code&gt; door makes no&#xA;difference: they lead to indistinguishable rooms. What if the&#xA;&lt;code&gt;left&lt;/code&gt; and &lt;code&gt;right&lt;/code&gt; doors mark, in truth, two&#xA;passageways into the &lt;em&gt;one&lt;/em&gt; room?&lt;figure&gt;&lt;img src=../img/borges-automata/infinite-maze-braid.svg alt=&#34;A Braid. Maybe this is our maze: each pair of doors leads to the same room, which has its own pair of doors, and so on and so forth.&#34;&gt;&lt;figcaption aria-hidden=true&gt;A Braid. Maybe this is our maze: each&#xA;pair of doors leads to the &lt;em&gt;same&lt;/em&gt; room, which has its own pair of&#xA;doors, and so on and so forth.&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;Here, it seems, there are fewer rooms to see. Whereas before there&#xA;was one room for every acceptable sequence of instructions, now there is&#xA;only one room for every &lt;em&gt;length&lt;/em&gt; of acceptable instructions. Any&#xA;sequence of three instructions—be it &lt;code&gt;left-left-right&lt;/code&gt;,&#xA;&lt;code&gt;right-right-right&lt;/code&gt;, or some other—leads you to the third&#xA;room. There are precisely &lt;span class=&#34;math inline&#34;&gt;2&lt;sup&gt;&lt;em&gt;n&lt;/em&gt;&lt;/sup&gt;&lt;/span&gt; paths to the &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;n&lt;/em&gt;&lt;sup&gt;th&lt;/sup&gt;&lt;/span&gt; room.&lt;p&gt;This does little to relax the absurdity of your agreement with the&#xA;sorcerer. First of all, no sequence of sorcerers’ instructions will ever&#xA;disambiguate an infinite Braid from an infinite Garden of Forking Paths,&#xA;even though their diagrams look different. An instruction valid in one&#xA;is always valid in the other; an instruction invalid in one is&#xA;correspondingly invalid.&lt;p&gt;Second, and more importantly, an infinite Braid is still infinite.&#xA;Though there may be more than one path to a given room in the sequence,&#xA;there is still an infinite number of rooms; the sorcerer will never&#xA;visit them all. If you were to draw out a map of this labyrinth which&#xA;admits an infinite number of paths, the map would extend infinitely to&#xA;one side (this map would, you realize, expand to cover the labyrinth&#xA;itself, no matter how small its lines and letters).&lt;p&gt;In one of your travels, after an eternity, something occurs to you&#xA;about the indistinguishability of the rooms: just as two doors may be&#xA;identical aside from their labels, as in the Braid diagrammed above,&#xA;perhaps two indistinguishable rooms…&lt;p&gt;You stop the blind sorcerer between instructions. You explain to him&#xA;that he has, in visiting this room, visited the labyrinth. In fact, for&#xA;all the vertigo of his infinite acceptable sequences, this room&#xA;&lt;em&gt;is&lt;/em&gt; equivalently the labyrinth.&lt;p&gt;The sorcerer, with an air of didactic satisfaction, asks for an&#xA;explanation.&lt;p&gt;It’s a little roundabout to start an explanation of &lt;em&gt;finite&lt;/em&gt;&#xA;state machines with an allegory involving infinite rooms—infinitely many&#xA;states, a sort of “infinite state machine.” These don’t have any&#xA;practical application except to illustrate the relationship between the&#xA;topology of the maze and the set of instructions it accepts. First we&#xA;have a maze that accepts an infinite number of instruction sequences,&#xA;and where no distinct sequences lead to the same room; then we have&#xA;another maze that accepts an infinite number of instruction sequences&#xA;(the very same!), but where distinct sequences can lead us to the same&#xA;place.&lt;p&gt;These two labyrinths, the Garden and the Braid, also introduce the&#xA;idea of &lt;em&gt;equivalence.&lt;/em&gt; If two state machines accept the same&#xA;inputs—if there’s no series of instructions from the sorcerer that&#xA;reveals, via its acceptability in one and unacceptability in the other,&#xA;in which maze we’re trapped—then the two state machines are&#xA;equivalent.&lt;p&gt;From here on we’ll deal with &lt;em&gt;finite&lt;/em&gt; state machines&#xA;(labyrinths with a finite number of rooms and doors), beginning with&#xA;your answer to the sorcerer. Finite state machines are more useful: we&#xA;can actually write programs that implement them, we can draw them out in&#xA;finite diagrams, and there’s nothing we can express in an infinite maze&#xA;that we can’t equivalently express in a finite one.&lt;h1 id=finitude&gt;Finitude&lt;/h1&gt;&lt;blockquote&gt;&lt;p&gt;Today, one of the churches of Tlön Platonically maintains that a&#xA;certain [room with two doors is] the only reality. All men, in the&#xA;vertiginous moment of coitus, are the same man. All men who repeat a&#xA;line from Shakespeare are William Shakespeare.&lt;p&gt;&lt;em&gt;Tlön, Uqbar, Orbis Tertius&lt;/em&gt;&lt;/blockquote&gt;&lt;p&gt;This is your explanation: these rooms are indistinguishable to your&#xA;explorations; every state permits the same two state transitions as the&#xA;start state, so every state transition is essentially a return to the&#xA;start state. Framing the state machine this way simplifies it: we can&#xA;think of a single room instead of an infinite number of rooms!&lt;figure&gt;&lt;img src=../img/borges-automata/finite-maze.svg alt=&#34;Our first finite state machine: all the seemingly infinite rooms are a single room.&#34;&gt;&lt;figcaption aria-hidden=true&gt;Our first &lt;em&gt;finite&lt;/em&gt; state machine:&#xA;all the seemingly infinite rooms are a single room.&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;Importantly, this simplification doesn’t change what instructions the&#xA;maze allows: any among the infinite number of instruction sequences&#xA;accepted by the infinite state machines diagrammed above is also&#xA;accepted by the one-room simplified state machine diagrammed here. It’s&#xA;a finite maze with an infinite number of paths.&lt;p&gt;In fact, any finite maze with a &lt;em&gt;cycle&lt;/em&gt;—any way to leave a&#xA;room and eventually return to it—accepts an infinite number of paths,&#xA;because you can validly move through the cycle once, twice, or any&#xA;number of times. This one is a one-room cycle: you can transition from&#xA;the one state to itself.&lt;p&gt;This dynamic is what makes finite state machines so useful: they’re&#xA;concise.&lt;p&gt;Sometimes they offer minimal notations for sequences that are&#xA;otherwise hard to describe.&lt;a href=#fn4 class=footnote-ref id=fnref4 role=doc-noteref&gt;&lt;sup&gt;4&lt;/sup&gt;&lt;/a&gt; It’s easy to describe&#xA;“any sequence of &lt;code&gt;left&lt;/code&gt;s and &lt;code&gt;right&lt;/code&gt;s,” but&#xA;sometimes we want to describe more complicated families of instructions.&#xA;Compare the following diagrams to the verbal descriptions of the&#xA;instructions they allow:&lt;div class=widetable&gt;&lt;table&gt;&lt;col style=&#34;width: 49%&#34;&gt;&lt;col style=&#34;width: 50%&#34;&gt;&lt;thead&gt;&lt;tr class=header&gt;&lt;th style=&#34;text-align: left;&#34;&gt;Description&lt;th&gt;Diagram&lt;tbody&gt;&lt;tr class=odd&gt;&lt;td style=&#34;text-align: left;&#34;&gt;Any sequence of &lt;code&gt;left&lt;/code&gt;s and&#xA;&lt;code&gt;right&lt;/code&gt;s where odd-indexed instructions are&#xA;&lt;code&gt;right&lt;/code&gt;.&lt;td&gt;&lt;img src=../img/borges-automata/finitude-1.svg&gt;&lt;tr class=even&gt;&lt;td style=&#34;text-align: left;&#34;&gt;Any sequence of &lt;code&gt;left&lt;/code&gt;s and&#xA;&lt;code&gt;right&lt;/code&gt;s, but where every third instruction can also be&#xA;&lt;code&gt;up&lt;/code&gt;.&lt;td&gt;&lt;img src=../img/borges-automata/finitude-2.svg&gt;&lt;tr class=odd&gt;&lt;td style=&#34;text-align: left;&#34;&gt;Any number of &lt;code&gt;up&lt;/code&gt;&#xA;instructions, followed by any number of repetitions of the order&#xA;&lt;code&gt;right-up-left&lt;/code&gt;.&lt;td&gt;&lt;img src=../img/borges-automata/finitude-3.svg&gt;&lt;/table&gt;&lt;/div&gt;&lt;p&gt;Each of these mazes describes a distinct infinite set of instruction&#xA;sequences. The diagrams are clearer than the verbal descriptions.&lt;p&gt;The set of instructions the maze allows is locked into the maze’s&#xA;topology; the maze is a &lt;em&gt;grammar&lt;/em&gt; for its own instructions. We&#xA;can use the maze to generate instructions, not just test them: if the&#xA;sorcerer were to ask you to produce a valid instruction sequence for a&#xA;maze (some “syntagm” in the maze’s grammar), you can take an arbitrary&#xA;series of doors to build one.&lt;p&gt;Note, these state machines’ properties are stable independent of&#xA;their labels. We’ve been working with directional labels&#xA;(&lt;code&gt;left&lt;/code&gt; and &lt;code&gt;right&lt;/code&gt;) because they suit the&#xA;labyrinthine theme, but nothing changes if we replace &lt;code&gt;left&lt;/code&gt;&#xA;with &lt;code&gt;0&lt;/code&gt; and &lt;code&gt;right&lt;/code&gt; with &lt;code&gt;1&lt;/code&gt;.&lt;figure&gt;&lt;img src=../img/borges-automata/binary-fsa.svg alt=&#34;If we replace the lefts and rights, with 1s and 0s respectively, we have a state machine that accepts any finite binary string.&#34;&gt;&lt;figcaption aria-hidden=true&gt;If we replace the &lt;code&gt;left&lt;/code&gt;s and&#xA;&lt;code&gt;right&lt;/code&gt;s, with &lt;code&gt;1&lt;/code&gt;s and &lt;code&gt;0&lt;/code&gt;s&#xA;respectively, we have a state machine that accepts any finite binary&#xA;string.&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;In fact, there are &lt;em&gt;more&lt;/em&gt; equivalent finite&#xA;labyrinths—labyrinths with a finite number of rooms, that admit any&#xA;sequence of binary instructions—than those covered here. Their&#xA;construction is left as an exercise for the reader.&lt;a href=#fn5 class=footnote-ref id=fnref5 role=doc-noteref&gt;&lt;sup&gt;5&lt;/sup&gt;&lt;/a&gt;&lt;p&gt;The blind sorcerer listens quietly to your explanation of the&#xA;infinite maze with one room, which perfectly describes every instruction&#xA;you could ever accept from him—every path in the Garden of Forking&#xA;Paths. He smiles, apparently satisfied with the explanation; “if all its&#xA;infinite rooms are one room, this is a crummy corner of my labyrinth.&#xA;Let’s find someplace with variety.”&lt;p&gt;As he says these words (zap!) you are transported to a wholly&#xA;different maze.&lt;/p&gt;&lt;h1 id=acceptance&gt;Acceptance&lt;/h1&gt;&lt;blockquote&gt;&lt;p&gt;When it was proclaimed that the Library contained all books, the&#xA;first impression was one of extravagant happiness. All men felt&#xA;themselves to be the masters of an intact and secret treasure. There was&#xA;no personal or world problem whose eloquent solution did not exist in&#xA;some [room].&lt;p&gt;&lt;em&gt;The Library of Babel&lt;/em&gt;&lt;/blockquote&gt;&lt;p&gt;In the interest of difference, the sorcerer introduces a new rule. At&#xA;first you were to accept any set of instructions you could follow; now&#xA;you are to reject unfollowable instructions, but &lt;em&gt;also&lt;/em&gt; to object&#xA;if the last instruction in a followable sequence &lt;em&gt;doesn’t&lt;/em&gt; lead&#xA;you to a room containing a MacGuffin.&lt;p&gt;Two rooms with the same doors—up to this point,&#xA;indistinguishable—might now be different: perhaps one contains a&#xA;MacGuffin and the other one does not. Moreover, two rooms which&#xA;&lt;em&gt;are&lt;/em&gt; indistinguishable to their occupant at a point in time (two&#xA;rooms with the same doors, neither of which contains a MacGuffin) may be&#xA;provably distinct because they have different positions relative to&#xA;MacGuffin rooms in the maze’s topology.&lt;p&gt;As you explore these mazes, you’re struck by their insistence on&#xA;their own size. If an infinite braid of rooms has a single MacGuffin in&#xA;the hundredth rooms, the rooms before the MacGuffin &lt;em&gt;can’t&lt;/em&gt; be&#xA;summarized as a single room: one is just before the MacGuffin room; the&#xA;room before that is two rooms before the MacGuffin; and so on. Even&#xA;though they have the same doors, these rooms are clearly&#xA;distinguishable.&lt;p&gt;Past the MacGuffin, though, you have no such information. You know&#xA;there is a room after the MacGuffin, but since you will never encounter&#xA;another MacGuffin you can never know whether there are rooms after that&#xA;one (e.g. in an infinite braid) or whether it’s a single trick room with&#xA;exits that lead you back into it.&lt;p&gt;Before this introduction of MacGuffins, in the language of state&#xA;machines, every node was an &lt;em&gt;acceptor&lt;/em&gt;—it was as if every room&#xA;contained a MacGuffin. Now only certain nodes are acceptor states. If,&#xA;after exhausting input, a state machine is an acceptor state, it&#xA;“accepts” the input sequence.&lt;p&gt;State machine diagrams denote acceptors with double-walled circles&#xA;(which is why the diagrams before this section have all had&#xA;double-walled rooms: every room was an acceptor). Single-walled circles&#xA;are &lt;em&gt;not&lt;/em&gt; acceptors—these are the rooms we can pass through but&#xA;don’t want to end in.&lt;p&gt;If this dynamic isn’t clear, take a moment to pretend to be the&#xA;sorcerer. The room you’re in is colored cyan; you can use the&#xA;&lt;code&gt;0&lt;/code&gt; and &lt;code&gt;1&lt;/code&gt; buttons to provide input (to select&#xA;doors to take. The checkbox at the bottom indicates whether the state&#xA;machine accepts the input: it’s checked when you’re in the acceptor&#xA;state &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;R&lt;/em&gt;&lt;sub&gt;1&lt;/sub&gt;&lt;/span&gt;, but not&#xA;when you’re in any other state.&lt;/p&gt;&lt;iframe src=../img/borges-automata/demo/deterministic-binary-fsa.html style=&#34;height: 280px;&#34;&gt;&lt;/iframe&gt;&lt;p&gt;Significantly, these mazes can more finely restrict what instruction&#xA;sequences they accept. Without these special MacGuffin rooms, any prefix&#xA;of an acceptable instruction was itself an acceptable instruction; if&#xA;less than every room contains a MacGuffin, that’s not necessarily the&#xA;case! Before, every room was an acceptor, every state an acceptable&#xA;terminal state. Now there are nonterminal states: valid instructions can&#xA;take us through them, but can’t end there. Now we can design mazes where&#xA;a prefix of a valid instruction sequence &lt;em&gt;isn’t&lt;/em&gt; guaranteed to be&#xA;acceptable:&lt;table&gt;&lt;col style=&#34;width: 48%&#34;&gt;&lt;col style=&#34;width: 51%&#34;&gt;&lt;thead&gt;&lt;tr class=header&gt;&lt;th style=&#34;text-align: left;&#34;&gt;Description&lt;th&gt;Diagram&lt;tbody&gt;&lt;tr class=odd&gt;&lt;td style=&#34;text-align: left;&#34;&gt;Any nonempty binary string that begins and&#xA;ends with the same digit. This accepts &lt;code&gt;1-0-0-0-1&lt;/code&gt;, but not&#xA;&lt;code&gt;1-0-0-0&lt;/code&gt;.&lt;td&gt;&lt;img src=../img/borges-automata/acceptance-1.svg&gt;&lt;tr class=even&gt;&lt;td style=&#34;text-align: left;&#34;&gt;Any binary string ending in&#xA;&lt;code&gt;0-0&lt;/code&gt;. This accepts &lt;code&gt;1-1-1-0-0&lt;/code&gt;, but not&#xA;&lt;code&gt;1-1-1&lt;/code&gt; or &lt;code&gt;1-1-1-0&lt;/code&gt;.&lt;td&gt;&lt;img src=../img/borges-automata/acceptance-2.svg&gt;&lt;/table&gt;&lt;p&gt;As your understanding of the labyrinths deepens, your exploration&#xA;with the sorcerer seems to accelerate, as if freed from ponderous&#xA;existential questions. Your juvenile dread that the great labyrinth&#xA;might be meaningless is supplanted by a vertigo of interpretations: a&#xA;din of instructions, a nauseating race through passageway after&#xA;passageway, MacGuffin and non-MacGuffin rooms, accepted and rejected&#xA;sequences, and the blind sorcerer’s increasingly frenetic jumps (zap!&#xA;zap! zap!) from one maze to the next.&lt;h1 id=determinism&gt;Determinism&lt;/h1&gt;&lt;blockquote&gt;&lt;p&gt;In all fictional works, each time a man is confronted with several&#xA;alternatives, he chooses one and eliminates the others; in the fiction&#xA;of the almost inextricable Ts’ui Pên, he chooses—simultaneously—all of&#xA;them. &lt;em&gt;He creates,&lt;/em&gt; in this way, diverse futures, diverse times&#xA;which themselves also proliferate and fork.&lt;p&gt;&lt;em&gt;The Garden of Forking Paths&lt;/em&gt;&lt;/blockquote&gt;&lt;p&gt;That nausea is interrupted, suddenly, by a dilemma. In a new maze,&#xA;you’re confronted with choice: one door is marked &lt;code&gt;0&lt;/code&gt;, and&#xA;&lt;em&gt;two&lt;/em&gt; doors are marked &lt;code&gt;1&lt;/code&gt;. When the sorcerer&#xA;instructs you to pass through the door marked &lt;code&gt;1&lt;/code&gt;, through&#xA;which door should you pass?&lt;figure&gt;&lt;img src=../img/borges-automata/nondeterministic-binary-fsa.svg alt=&#34;In this two-room maze we must end up in the acceptor room R_1; any instructions that leave us in R_0 are invalid. Depending on how we interpret the last 1, the instruction set 0-0-0-0-1 could either be valid (if we take the door leading from R_0 to the acceptor room R_1) or invalid (if we take the door leading from R_0 back into R_0).&#34;&gt;&lt;figcaption aria-hidden=true&gt;In this two-room maze we &lt;em&gt;must&lt;/em&gt;&#xA;end up in the acceptor room &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;R&lt;/em&gt;&lt;sub&gt;1&lt;/sub&gt;&lt;/span&gt;; any instructions that&#xA;leave us in &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;R&lt;/em&gt;&lt;sub&gt;0&lt;/sub&gt;&lt;/span&gt; are&#xA;invalid. Depending on how we interpret the last &lt;code&gt;1&lt;/code&gt;, the&#xA;instruction set &lt;code&gt;0-0-0-0-1&lt;/code&gt; could &lt;em&gt;either&lt;/em&gt; be valid&#xA;(if we take the door leading from &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;R&lt;/em&gt;&lt;sub&gt;0&lt;/sub&gt;&lt;/span&gt; to the acceptor room&#xA;&lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;R&lt;/em&gt;&lt;sub&gt;1&lt;/sub&gt;&lt;/span&gt;) or invalid (if&#xA;we take the door leading from &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;R&lt;/em&gt;&lt;sub&gt;0&lt;/sub&gt;&lt;/span&gt; back into &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;R&lt;/em&gt;&lt;sub&gt;0&lt;/sub&gt;&lt;/span&gt;).&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;“You will take both doors,” explains the blind sorcerer. Your task is&#xA;to find out whether the instructions lead you to a MacGuffin through&#xA;&lt;em&gt;either&lt;/em&gt; door. This puzzles you, but the sorcerer explains that&#xA;there are a couple ways to take it literally:&lt;ol type=1&gt;&lt;li&gt;&lt;p&gt;Whenever you encounter several identical door, the sorcerer can&#xA;conjure an avatar of you to pass through each door in&#xA;&lt;strong&gt;parallel.&lt;/strong&gt; If two avatars would meet in some room down&#xA;the line, they are consolidated into one.&lt;p&gt;This prospect deeply unsettles your sense of self (are &lt;em&gt;you&lt;/em&gt;&#xA;the avatar? or the original?), and the sorcerer admits it gives him&#xA;migraines: conjuring avatars takes time and effort, and delivering&#xA;simultaneous instructions to innumerable (and maybe ever-multiplying!)&#xA;avatars is, he says, “a pain in the ass.”&lt;li&gt;&lt;p&gt;You can pass through both doors by &lt;strong&gt;backtracking.&lt;/strong&gt;&#xA;When you choose one door over another identical one, note the decision;&#xA;then, if the first door doesn’t lead you to accept the instructions, you&#xA;can return to take the second door.&lt;p&gt;This constant retracing, of course, takes time: you have to return to&#xA;a room once for each identical door, and subsequent rooms can themselves&#xA;have identical doors. Before, you would pass through at most &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;n&lt;/em&gt;&lt;/span&gt; rooms to (in)validate a sequence&#xA;of &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;n&lt;/em&gt;&lt;/span&gt; instructions. Now you may&#xA;need to pass through many more&lt;/ol&gt;&lt;p&gt;You commit to backtracking (since, if you have to spend your&#xA;existence trying to make sense of an infinite labyrinth for a mad&#xA;sorcerer, you might as well feel existentially unique while you do it).&#xA;The vertigo returns, but you have the kernel of an idea to eliminate the&#xA;problem of choice.&lt;p&gt;These mazes—with several indistinguishable exits from a given&#xA;room—are nondeterministic finite state machines (NFAs). They’re&#xA;“nondeterministic” because your state at a given time isn’t totally&#xA;determined by a set of instructions; you &lt;em&gt;could&lt;/em&gt; be in several&#xA;states at a given time, depending on which doors you chose. The two&#xA;strategies proposed by the sorcerer correspond to two strategies&#xA;available to a program implementing an NFA: they can evaluate possible&#xA;states sequentially, or they can track multiple states in parallel. Both&#xA;of these strategies are undesirable: you can have avatars in up to&#xA;&lt;em&gt;every&lt;/em&gt; room of a maze at once, which makes applying inputs&#xA;burdensome. Regular expression implementations use NFAs; these dynamics&#xA;are what make certain regular expressions inefficient.&lt;p&gt;You think, in all the time you now spend backtracking, about the&#xA;avatars. Some number of avatars, in certain rooms in a finite maze,&#xA;receive a certain instruction and at once spring into certain other&#xA;rooms. In this process, some avatars will split, some will consolidate,&#xA;and those for whom the instructions are unfollowable will blink out of&#xA;existence.&lt;p&gt;It’s difficult to imagine the possible configurations of avatars in&#xA;rooms because there are so many&lt;a href=#fn6 class=footnote-ref id=fnref6 role=doc-noteref&gt;&lt;sup&gt;6&lt;/sup&gt;&lt;/a&gt;, but a marvelous fact&#xA;occurs to you: from the sorcerer’s perspective, a configuration of&#xA;avatars behaves deterministically. If avatars are in some set of rooms,&#xA;then after receiving a certain instruction they will be in a certain&#xA;determinable second set of rooms regardless of how they traveled to that&#xA;prior arrangement.&lt;p&gt;Is this passage from configuration to configuration really so&#xA;different from you passing, deterministically, from one room to the&#xA;next?&lt;p&gt;You interrupt the sorcerer a second time, and you make him an offer:&#xA;for any finite maze confused with choice, you say, any maze burdened&#xA;with identical doors, you can design for him a finite equivalent maze&#xA;devoid of choice.&lt;p&gt;This is what the avatars reveal: every nondeterministic finite state&#xA;machine is equivalent to some deterministic one!&lt;p&gt;Your proposal:&lt;ol type=1&gt;&lt;li&gt;&lt;p&gt;For every configuration of avatars in rooms in the&#xA;nondeterministic maze, build a room in our new deterministic maze. If a&#xA;configuration has an avatar in a room with a MacGuffin, then put a&#xA;MacGuffin in the new room corresponding to that configuration.&lt;li&gt;&lt;p&gt;For each configuration of avatars, consider each instruction that&#xA;at least one avatar could validly follow (in other words, consider the&#xA;set of door-labels the avatars see at once). For each instruction&#xA;&lt;code&gt;I&lt;/code&gt;:&lt;ol type=i&gt;&lt;li&gt;&lt;p&gt;Figure out what every avatar would do if given this instruction:&#xA;some move simply into another room, some split and move into several.&#xA;This gives you a successor configuration.&lt;li&gt;&lt;p&gt;In the new deterministic maze, build a door out of the room&#xA;corresponding to the initial configuration that leads to the successor&#xA;configuration. Label it with the instruction &lt;code&gt;I&lt;/code&gt;.&lt;/ol&gt;&lt;li&gt;&lt;p&gt;Some rooms in our new maze may not be reachable from the maze’s&#xA;start. Demolish these; they’re superfluous.&lt;/ol&gt;&lt;p&gt;Since we never repeatedly assess the same instruction applied to the&#xA;same configuration of avatars, we will never build two doors in the same&#xA;room of our new maze that have the same label. Thus, the new maze can be&#xA;explored deterministically.&lt;p&gt;Since any configuration in the nondeterministic maze accepts the same&#xA;instructions as its corresponding room in the deterministic one, we&#xA;haven’t changed the followability of any instructions. Because every&#xA;room with a MacGuffin in the new maze corresponds to a configuration in&#xA;the nondeterministic maze where some avatar is in a room with a&#xA;MacGuffin, the mazes make the same terminal judgements of followable&#xA;sequences. They’re equivalent!&lt;p&gt;Of course, since there are so many possible configurations of avatars&#xA;in mazes, the new deterministic maze may be considerably larger than the&#xA;nondeterministic maze it models.&lt;p&gt;This algorithm—the &lt;a href=https://en.wikipedia.org/wiki/Powerset_construction&gt;powerset&#xA;construction&lt;/a&gt;, adapted for our allegory—is best explained with an&#xA;example. Consider the following nondeterministic finite maze, with rooms&#xA;labeled &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;A&lt;/em&gt;&lt;/span&gt;, &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;B&lt;/em&gt;&lt;/span&gt;, and &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;C&lt;/em&gt;&lt;/span&gt;:&lt;figure&gt;&lt;img src=../img/borges-automata/nfa-conversion-pre.svg alt=&#34;A nondeterministic maze. There are two doors out of room B marked 0 and two doors out of room A marked 1. Any instruction sequence that leaves an avatar into room C is accepted.&#34;&gt;&lt;figcaption aria-hidden=true&gt;A nondeterministic maze. There are two&#xA;doors out of room &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;B&lt;/em&gt;&lt;/span&gt; marked&#xA;&lt;code&gt;0&lt;/code&gt; and two doors out of room &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;A&lt;/em&gt;&lt;/span&gt; marked &lt;code&gt;1&lt;/code&gt;. Any&#xA;instruction sequence that leaves an avatar into room &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;C&lt;/em&gt;&lt;/span&gt; is accepted.&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;Our first step is to plan out the rooms in the new maze, a finite&#xA;state machine with one state for each possible configuration of avatars&#xA;in the nondeterministic maze. We can use set notation to describe the&#xA;arrangements of avatars:&lt;ul&gt;&lt;li&gt;&lt;span class=&#34;math inline&#34;&gt;{&lt;em&gt;C&lt;/em&gt;}&lt;/span&gt; is the state&#xA;corresponding to the configuration with a single avatar in room &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;C&lt;/em&gt;&lt;/span&gt;,&lt;li&gt;&lt;span class=&#34;math inline&#34;&gt;{&lt;em&gt;A&lt;/em&gt;, &lt;em&gt;B&lt;/em&gt;}&lt;/span&gt; is the&#xA;state corresponding to the configuration with an avatar in room &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;A&lt;/em&gt;&lt;/span&gt; and another in room &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;B&lt;/em&gt;&lt;/span&gt;,&lt;/ul&gt;&lt;p&gt;and so on. Any state with an avatar in room &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;C&lt;/em&gt;&lt;/span&gt; is an acceptor state, and at the&#xA;start there is only one avatar (in room &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;C&lt;/em&gt;&lt;/span&gt;). We can list out the seven states&#xA;corresponding to the seven possible arrangements of avatars in the&#xA;three-room maze:&lt;div class=widetable&gt;&lt;table&gt;&lt;col style=&#34;width: 10%&#34;&gt;&lt;col style=&#34;width: 42%&#34;&gt;&lt;col style=&#34;width: 15%&#34;&gt;&lt;col style=&#34;width: 15%&#34;&gt;&lt;col style=&#34;width: 15%&#34;&gt;&lt;thead&gt;&lt;tr class=header&gt;&lt;th style=&#34;text-align: left;&#34;&gt;State in new maze&lt;th style=&#34;text-align: left;&#34;&gt;Acceptor?&lt;th style=&#34;text-align: center;&#34;&gt;A&lt;th style=&#34;text-align: center;&#34;&gt;B&lt;th style=&#34;text-align: center;&#34;&gt;C&lt;tbody&gt;&lt;tr class=odd&gt;&lt;td style=&#34;text-align: left;&#34;&gt;&lt;span class=&#34;math inline&#34;&gt;{&lt;em&gt;A&lt;/em&gt;}&lt;/span&gt;&lt;td style=&#34;text-align: left;&#34;&gt;No&lt;td style=&#34;text-align: center;&#34;&gt;✔️&lt;td style=&#34;text-align: center;&#34;&gt;&lt;td style=&#34;text-align: center;&#34;&gt;&lt;tr class=even&gt;&lt;td style=&#34;text-align: left;&#34;&gt;&lt;span class=&#34;math inline&#34;&gt;{&lt;em&gt;B&lt;/em&gt;}&lt;/span&gt;&lt;td style=&#34;text-align: left;&#34;&gt;No&lt;td style=&#34;text-align: center;&#34;&gt;&lt;td style=&#34;text-align: center;&#34;&gt;✔️&lt;td style=&#34;text-align: center;&#34;&gt;&lt;tr class=odd&gt;&lt;td style=&#34;text-align: left;&#34;&gt;&lt;span class=&#34;math inline&#34;&gt;{&lt;em&gt;C&lt;/em&gt;}&lt;/span&gt; (start state)&lt;td style=&#34;text-align: left;&#34;&gt;Yes&lt;td style=&#34;text-align: center;&#34;&gt;&lt;td style=&#34;text-align: center;&#34;&gt;&lt;td style=&#34;text-align: center;&#34;&gt;✔️&lt;tr class=even&gt;&lt;td style=&#34;text-align: left;&#34;&gt;&lt;span class=&#34;math inline&#34;&gt;{&lt;em&gt;A&lt;/em&gt;, &lt;em&gt;B&lt;/em&gt;}&lt;/span&gt;&lt;td style=&#34;text-align: left;&#34;&gt;No&lt;td style=&#34;text-align: center;&#34;&gt;✔️&lt;td style=&#34;text-align: center;&#34;&gt;✔️&lt;td style=&#34;text-align: center;&#34;&gt;&lt;tr class=odd&gt;&lt;td style=&#34;text-align: left;&#34;&gt;&lt;span class=&#34;math inline&#34;&gt;{&lt;em&gt;A&lt;/em&gt;, &lt;em&gt;C&lt;/em&gt;}&lt;/span&gt;&lt;td style=&#34;text-align: left;&#34;&gt;Yes&lt;td style=&#34;text-align: center;&#34;&gt;✔️&lt;td style=&#34;text-align: center;&#34;&gt;&lt;td style=&#34;text-align: center;&#34;&gt;✔️&lt;tr class=even&gt;&lt;td style=&#34;text-align: left;&#34;&gt;&lt;span class=&#34;math inline&#34;&gt;{&lt;em&gt;B&lt;/em&gt;, &lt;em&gt;C&lt;/em&gt;}&lt;/span&gt;&lt;td style=&#34;text-align: left;&#34;&gt;Yes&lt;td style=&#34;text-align: center;&#34;&gt;&lt;td style=&#34;text-align: center;&#34;&gt;✔️&lt;td style=&#34;text-align: center;&#34;&gt;✔️&lt;tr class=odd&gt;&lt;td style=&#34;text-align: left;&#34;&gt;&lt;span class=&#34;math inline&#34;&gt;{&lt;em&gt;A&lt;/em&gt;, &lt;em&gt;B&lt;/em&gt;, &lt;em&gt;C&lt;/em&gt;}&lt;/span&gt;&lt;td style=&#34;text-align: left;&#34;&gt;Yes&lt;td style=&#34;text-align: center;&#34;&gt;✔️&lt;td style=&#34;text-align: center;&#34;&gt;✔️&lt;td style=&#34;text-align: center;&#34;&gt;✔️&lt;/table&gt;&lt;/div&gt;&lt;p&gt;Next, we need to consider the possible transitions out of each of&#xA;these states. Thankfully this is a pretty simple maze: we only need to&#xA;consider what happens to each of these states if the sorcerer delivers&#xA;the instruction &lt;code&gt;0&lt;/code&gt; or the instruction &lt;code&gt;1&lt;/code&gt;:&lt;div class=firstcoltable&gt;&lt;table&gt;&lt;thead&gt;&lt;tr class=header&gt;&lt;th style=&#34;text-align: left;&#34;&gt;State&lt;th style=&#34;text-align: left;&#34;&gt;+&lt;code&gt;0&lt;/code&gt;&lt;th style=&#34;text-align: left;&#34;&gt;+&lt;code&gt;1&lt;/code&gt;&lt;tbody&gt;&lt;tr class=odd&gt;&lt;td style=&#34;text-align: left;&#34;&gt;&lt;span class=&#34;math inline&#34;&gt;{&lt;em&gt;A&lt;/em&gt;}&lt;/span&gt;&lt;td style=&#34;text-align: left;&#34;&gt;&lt;span class=&#34;math inline&#34;&gt;{&lt;em&gt;A&lt;/em&gt;, &lt;em&gt;C&lt;/em&gt;}&lt;/span&gt;&lt;td style=&#34;text-align: left;&#34;&gt;Invalid&lt;tr class=even&gt;&lt;td style=&#34;text-align: left;&#34;&gt;&lt;span class=&#34;math inline&#34;&gt;{&lt;em&gt;B&lt;/em&gt;}&lt;/span&gt;&lt;td style=&#34;text-align: left;&#34;&gt;Invalid&lt;td style=&#34;text-align: left;&#34;&gt;&lt;span class=&#34;math inline&#34;&gt;{&lt;em&gt;A&lt;/em&gt;, &lt;em&gt;C&lt;/em&gt;}&lt;/span&gt;&lt;tr class=odd&gt;&lt;td style=&#34;text-align: left;&#34;&gt;&lt;span class=&#34;math inline&#34;&gt;{&lt;em&gt;C&lt;/em&gt;}&lt;/span&gt;&lt;td style=&#34;text-align: left;&#34;&gt;&lt;span class=&#34;math inline&#34;&gt;{&lt;em&gt;B&lt;/em&gt;}&lt;/span&gt;&lt;td style=&#34;text-align: left;&#34;&gt;Invalid&lt;tr class=even&gt;&lt;td style=&#34;text-align: left;&#34;&gt;&lt;span class=&#34;math inline&#34;&gt;{&lt;em&gt;A&lt;/em&gt;, &lt;em&gt;B&lt;/em&gt;}&lt;/span&gt;&lt;td style=&#34;text-align: left;&#34;&gt;&lt;span class=&#34;math inline&#34;&gt;{&lt;em&gt;A&lt;/em&gt;, &lt;em&gt;C&lt;/em&gt;}&lt;/span&gt;&lt;td style=&#34;text-align: left;&#34;&gt;&lt;span class=&#34;math inline&#34;&gt;{&lt;em&gt;A&lt;/em&gt;, &lt;em&gt;C&lt;/em&gt;}&lt;/span&gt;&lt;tr class=odd&gt;&lt;td style=&#34;text-align: left;&#34;&gt;&lt;span class=&#34;math inline&#34;&gt;{&lt;em&gt;A&lt;/em&gt;, &lt;em&gt;C&lt;/em&gt;}&lt;/span&gt;&lt;td style=&#34;text-align: left;&#34;&gt;&lt;span class=&#34;math inline&#34;&gt;{&lt;em&gt;A&lt;/em&gt;, &lt;em&gt;B&lt;/em&gt;, &lt;em&gt;C&lt;/em&gt;}&lt;/span&gt;&lt;td style=&#34;text-align: left;&#34;&gt;Invalid&lt;tr class=even&gt;&lt;td style=&#34;text-align: left;&#34;&gt;&lt;span class=&#34;math inline&#34;&gt;{&lt;em&gt;B&lt;/em&gt;, &lt;em&gt;C&lt;/em&gt;}&lt;/span&gt;&lt;td style=&#34;text-align: left;&#34;&gt;&lt;span class=&#34;math inline&#34;&gt;{&lt;em&gt;B&lt;/em&gt;}&lt;/span&gt;&lt;td style=&#34;text-align: left;&#34;&gt;&lt;span class=&#34;math inline&#34;&gt;{&lt;em&gt;A&lt;/em&gt;, &lt;em&gt;C&lt;/em&gt;}&lt;/span&gt;&lt;tr class=odd&gt;&lt;td style=&#34;text-align: left;&#34;&gt;&lt;span class=&#34;math inline&#34;&gt;{&lt;em&gt;A&lt;/em&gt;, &lt;em&gt;B&lt;/em&gt;, &lt;em&gt;C&lt;/em&gt;}&lt;/span&gt;&lt;td style=&#34;text-align: left;&#34;&gt;&lt;span class=&#34;math inline&#34;&gt;{&lt;em&gt;A&lt;/em&gt;, &lt;em&gt;B&lt;/em&gt;, &lt;em&gt;C&lt;/em&gt;}&lt;/span&gt;&lt;td style=&#34;text-align: left;&#34;&gt;&lt;span class=&#34;math inline&#34;&gt;{&lt;em&gt;A&lt;/em&gt;, &lt;em&gt;C&lt;/em&gt;}&lt;/span&gt;&lt;/table&gt;&lt;/div&gt;&lt;p&gt;Finally, we prune the unreachable states and diagram our&#xA;deterministic finite state machine. It’s worth taking a moment to&#xA;compare it to the tables above: confirm that each of the acceptor states&#xA;includes an avatar in room &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;C&lt;/em&gt;&lt;/span&gt;;&#xA;check that the state transitions correspond to those described in the&#xA;instruction table.&lt;figure&gt;&lt;img src=../img/borges-automata/nfa-conversion-post.svg alt=&#34;Our powerset-constructed deterministic equivalent!&#34;&gt;&lt;figcaption aria-hidden=true&gt;Our powerset-constructed deterministic&#xA;equivalent!&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;This process lets us do away with all the complexity of avatars and&#xA;backtracking, without changing which instructions are accepted and&#xA;rejected, for any finite state machine, no matter how complex. You and&#xA;the sorcerer can explore these deterministic mazes without any&#xA;insufferable nondeterminisms; though this deterministic state machine&#xA;looks nothing like the equivalent it models, exploring it is essentially&#xA;exploring the collective experience of the avatars in their&#xA;nondeterministic maze. For any arrangement the avatars would realize&#xA;collectively, you reach a corresponding state.&lt;p&gt;If you need to convince yourself of this correspondence, you can use&#xA;the interactive demo below to provide simultaneous inputs to the&#xA;nondeterministic and deterministic equivalent state machines. Cyan nodes&#xA;indicate the presence of an avatar in a room; note that the state of the&#xA;deterministic machine &lt;em&gt;always&lt;/em&gt; identifies the occupied rooms in&#xA;the nondeterministic one.&lt;/p&gt;&lt;iframe src=../img/borges-automata/demo/nfa-conversion.html style=&#34;height: 376px;&#34;&gt;&lt;/iframe&gt;&lt;h1 id=allegory&gt;Allegory&lt;/h1&gt;&lt;blockquote&gt;&lt;p&gt;Augustine had written that Jesus is the straight path that saves us&#xA;from the circular labyrinth followed by the impious; these Aurelian,&#xA;laboriously trivial, compared with Ixion, with the liver of Prometheus,&#xA;with Sisyphus, with the king of thebes who saw two suns, with&#xA;stuttering, with parrots, with mirrors, with echoes, with the mules of a&#xA;noria and with two-horned syllogisms. […] Like all those possessing a&#xA;library, Aurelian was aware that he was guilty of not knowing his in its&#xA;entirety; this controversy enabled him to fulfill his obligations to&#xA;many books which seemed to reproach him for his neglect.&lt;p&gt;&lt;em&gt;The Theologians&lt;/em&gt;&lt;/blockquote&gt;&lt;p&gt;The sorcerer (Borges) and his mazes are an indulgent conceit for an&#xA;explanation of state machines. For a reader &lt;em&gt;really&lt;/em&gt; interested&#xA;in the mathematical concepts, the comparison might confuse more than it&#xA;makes clear: it may be harder to imagine yourself splitting into&#xA;multiple indistinguishable avatars than it is to imagine a multi-node&#xA;state, and so my explanations in this essay become less allegorical as&#xA;the concepts become more disorienting.&lt;p&gt;I’m satisfied with my exploration of the correspondence between state&#xA;machines and sequences, but the allegory says nothing about the “meaning&#xA;of your hermitude” in the maze. To do so requires stepping back out of&#xA;the diegetic “you” in the allegory, the little traveler in the&#xA;labyrinth, and considering the you &lt;em&gt;reading&lt;/em&gt; the allegory.&lt;p&gt;First, I can offer you a shallow meaning: state machines are useful&#xA;tools for turning seemingly complex grammars or agents into simple,&#xA;applicable programs. That’s how they were covered in my undergraduate&#xA;computer science curriculum. It’s not a &lt;em&gt;wrong&lt;/em&gt;&#xA;justification—these applications are worthwhile—but it’s partial.&lt;p&gt;The other justification is to recognize that this essay is a reversal&#xA;of work Borges has already done. His stories don’t discuss mathematics&#xA;explicitly (they’d be worse—more gimmicky—if they did) but they rework&#xA;topology and combinatorics into phenomenological and existential&#xA;puzzles. I can explain topology via a phenomenological puzzle about a&#xA;maze because Borges establishes, in various narrative guises, the&#xA;structure of the allegory. Even if Borges hadn’t, at certain touchpoints&#xA;the allegory is obvious. Topology connects intuitively to the everyday&#xA;way one moves between physical spaces; surprising topological twists,&#xA;like the equivalence of an infinite maze and a minimal one, correspond&#xA;to uncanny views of the everyday.&lt;p&gt;Borges skillfully extends these allegories beyond their natural&#xA;correspondences. The magic of &lt;em&gt;A New Refutation of Time&lt;/em&gt; is that&#xA;it imagines a kind of spatialized time, wherein two indistinguishable&#xA;moments are ontologically one:&lt;blockquote&gt;&lt;p&gt;That pure representation of homogeneous objects—the night in&#xA;serenity, a limpid little wall, the provincial scent of the honeysuckle,&#xA;the elemental earth—is not merely identical to the one present on that&#xA;corner so many years ago; it is, without resemblances or repetitions,&#xA;the very same. Time, if we can intuitively grasp such an identity, is a&#xA;delusion: the difference and inseparability of one moment belonging to&#xA;its apparent past from another belonging to its apparent present is&#xA;sufficient to disintegrate it.&lt;/blockquote&gt;&lt;p&gt;This is precisely the same radical idealism that lets us say an&#xA;infinite maze is a single room. If you prefer a philosophical&#xA;connection, Borges treats the phenomenology of time the way Augé treats&#xA;place.&lt;p&gt;Where does Borges’s labyrinth lead? To a dizzying metaphysics, as&#xA;compelling as it is disconcerting. The Borgean maze is a trap, an&#xA;entanglement of symbols and experiences, reflections and repetitions. I&#xA;hope that following his allegories in the opposite direction—beginning&#xA;with the existential allegory, but taking it as a point of departure&#xA;into the mathematics Borges adapts but elides—has the opposite effect:&#xA;mazes can be games, too. The trick is to enjoy them.&lt;/p&gt;&lt;aside id=footnotes class=&#34;footnotes footnotes-end-of-document&#34; role=doc-endnotes&gt;&lt;hr&gt;&lt;ol&gt;&lt;li id=fn1&gt;&lt;p&gt;Jorge Luis Borges. “The House of Asterion.” In&#xA;&lt;em&gt;Labyrinths: Selected Writings &amp; Other Stories.&lt;/em&gt; New York:&#xA;New Directions Books, 1962. Note: all subsequent quotations from Borges&#xA;short stories are from this translation.&lt;a href=#fnref1 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn2&gt;&lt;p&gt;Using a state machine to accept or reject inputs might&#xA;be foreign to readers who use state machines to, for example, determine&#xA;the actions of an agent in a game. In the interest of keeping the&#xA;Borgean allegory as simple as possible—it can be hard to follow as it&#xA;is—this story concerns itself with state machines as they’re used in&#xA;pattern-matching: we give the sorcerer a boolean output.&lt;a href=#fnref2 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn3&gt;&lt;p&gt;Note, there is one &lt;em&gt;acceptable&lt;/em&gt; route to each&#xA;room (that is, a sequence of &lt;code&gt;left&lt;/code&gt;s and&#xA;&lt;code&gt;right&lt;/code&gt;s). There are also innumerable unfollowable&#xA;instructions—which say to pick a door marked &lt;code&gt;axaxaxas mlö&lt;/code&gt;,&#xA;for example—but we won’t sweat that here.&lt;a href=#fnref3 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn4&gt;&lt;p&gt;Minimal notation is really a feature of &lt;em&gt;formal&#xA;grammars.&lt;/em&gt; Diagrams of state machines corresponding to those&#xA;grammars are handy visualizations (I find them much easier to skim).&lt;a href=#fnref4 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn5&gt;&lt;p&gt;This is a cheeky suggestion. There’s an &lt;em&gt;infinite&#xA;number&lt;/em&gt; of finite state machines that admit any sequence of binary&#xA;instructions; they just need to end in some cycle (either a single-node&#xA;cycle, where one constantly reenters the room they’re in, or a&#xA;multi-node cycle like a looped braid).&lt;p&gt;Those terminal cycles can be preceeded by any non-cyclical labyrinth&#xA;(e.g. the tree in figure 1, the braid in figure 2, or some other&#xA;directed acyclic graph) as long as every room has a door labeled&#xA;&lt;code&gt;0&lt;/code&gt; and a door labeled &lt;code&gt;1&lt;/code&gt;.&lt;p&gt;And, finally, there are unexplored dimensions: a finite state machine&#xA;that accepts a &lt;em&gt;superset&lt;/em&gt; of the set of finite binary inputs&#xA;satisfies our requirements.&lt;a href=#fnref5 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn6&gt;&lt;p&gt;After a certain set of instructions, each room can&#xA;either contain or not contain an avatar. Thus, for a maze with &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;n&lt;/em&gt;&lt;/span&gt; rooms, you have &lt;span class=&#34;math inline&#34;&gt;2&lt;sup&gt;&lt;em&gt;n&lt;/em&gt;&lt;/sup&gt;&lt;/span&gt; possible&#xA;configurations of avatars in the maze.&lt;p&gt;Note, though, that this is an upper bound. If the only way into room&#xA;&lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;R&lt;/em&gt;&lt;sub&gt;&lt;em&gt;l&lt;/em&gt;&lt;em&gt;e&lt;/em&gt;&lt;em&gt;f&lt;/em&gt;&lt;em&gt;t&lt;/em&gt;&lt;/sub&gt;&lt;/span&gt;&#xA;is a door marked &lt;code&gt;left&lt;/code&gt; and the only way into room &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;R&lt;/em&gt;&lt;sub&gt;&lt;em&gt;r&lt;/em&gt;&lt;em&gt;i&lt;/em&gt;&lt;em&gt;g&lt;/em&gt;&lt;em&gt;h&lt;/em&gt;&lt;em&gt;t&lt;/em&gt;&lt;/sub&gt;&lt;/span&gt;&#xA;is a door marked &lt;code&gt;right&lt;/code&gt;, then no set of instructions will&#xA;simultaneously put avatars in &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;R&lt;/em&gt;&lt;sub&gt;&lt;em&gt;l&lt;/em&gt;&lt;em&gt;e&lt;/em&gt;&lt;em&gt;f&lt;/em&gt;&lt;em&gt;t&lt;/em&gt;&lt;/sub&gt;&lt;/span&gt;&#xA;and &lt;span class=&#34;math inline&#34;&gt;&lt;em&gt;R&lt;/em&gt;&lt;sub&gt;&lt;em&gt;r&lt;/em&gt;&lt;em&gt;i&lt;/em&gt;&lt;em&gt;g&lt;/em&gt;&lt;em&gt;h&lt;/em&gt;&lt;em&gt;t&lt;/em&gt;&lt;/sub&gt;&lt;/span&gt;&#xA;(because no sequence can end with both &lt;code&gt;left&lt;/code&gt; &lt;em&gt;and&lt;/em&gt;&#xA;&lt;code&gt;right&lt;/code&gt;).&lt;a href=#fnref6 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;/ol&gt;&lt;/aside&gt;</content>
    <link href="https://lukasschwab.me/blog/gen/borges-automata.html" rel="alternate"></link>
    <summary type="html">An introduction to state machines (also called &#34;automata&#34;) via an extended Borgean allegory about an existence in a labyrinth, a sort of infinite gallery of mazes. It introduces several significant concepts---equivalence, finitude, acceptance, determinism, and the powerset construction---as a succession of realizations about that existence.</summary>
  </entry>
  <entry>
    <title>Making Things Worse on Purpose</title>
    <updated>2021-02-13T00:00:00Z</updated>
    <id>https://lukasschwab.me/blog/gen/worse-on-purpose.html</id>
    <content type="html">&lt;html xmlns=http://www.w3.org/1999/xhtml lang xml:lang&gt;&lt;meta charset=utf-8&gt;&lt;meta name=generator content=&#34;pandoc&#34;&gt;&lt;meta name=viewport content=&#34;width=device-width,initial-scale=1,user-scalable=yes&#34;&gt;&lt;meta name=author content=&#34;Lukas Schwab&#34;&gt;&lt;meta name=dcterms.date content=&#34;2021-02-13&#34;&gt;&lt;title&gt;Making Things Worse on Purpose&lt;/title&gt;&lt;style&gt;&#xA;      code{white-space: pre-wrap;}&#xA;      span.smallcaps{font-variant: small-caps;}&#xA;      span.underline{text-decoration: underline;}&#xA;      div.column{display: inline-block; vertical-align: top; width: 50%;}&#xA;          &lt;/style&gt;&lt;link rel=stylesheet href=../styles/common.css&gt;&lt;link rel=stylesheet href=../styles/fence-extensions/frame.css&gt;&lt;script data-goatcounter=https://lukasschwab.goatcounter.com/count async src=//gc.zgo.at/count.js&gt;&lt;/script&gt;&lt;a href=../index.html&gt;&amp;#171; blog&lt;/a&gt; &lt;span class=date&gt;2021-02-13&lt;/span&gt;&lt;header id=title-block-header&gt;&lt;h1 class=title&gt;Making Things Worse on Purpose&lt;/h1&gt;&lt;/header&gt;&lt;p&gt;Lately I’ve enjoyed breaking bad habits and resolving little&#xA;frictions in my life by tweaking apps that cause and enable them.&lt;p&gt;&lt;a href=https://github.com/jordwest/news-feed-eradicator&gt;&lt;code&gt;news-feed-eradicator&lt;/code&gt;&lt;/a&gt;&#xA;specifically eliminates Facebook’s news feed, that erstwhile&#xA;attention-trap. More generically, you can customize an ad blocker to&#xA;block &lt;em&gt;arbitrary content&lt;/em&gt; on a website; I use this to hide&#xA;comment sections which would otherwise waste my time. These social&#xA;features make products “better” inasmuch as they make them more&#xA;profitable (via user engagement), but the psychology of a bottomless&#xA;social feed is such that my engagement doesn’t &lt;em&gt;necessarily&lt;/em&gt;&#xA;correspond to it providing me any value at all.&lt;p&gt;YouTube’s default video quality throttling mismanages my computer’s&#xA;resources rather than my attention: their playback quality throttling&#xA;considers bandwidth, but not the client’s graphics hardware or playback&#xA;speed. The fan on my 2013 laptop is &lt;em&gt;screaming&lt;/em&gt; after a minute of&#xA;streaming 1440p video at 60 frames per second and 2.5x normal speed.&lt;p&gt;Because YouTube’s player controls store a manually-set quality limit&#xA;in Chrome’s Local Storage (as &lt;code&gt;yt-player-quality&lt;/code&gt;), a Chrome&#xA;extension can simulate manually correcting the defaults. I don’t need to&#xA;abide by YouTube’s expectation that I prefer higher-quality video.&lt;p&gt;Unfortunately, we don’t have this degree of flexibility in mobile&#xA;browsers (which lack the extensibility of desktop browsers) or&#xA;closed-source native apps (which don’t share a single customizable&#xA;client). This is the same product-user dynamic that &lt;a href=./json-feed-tools&gt;makes syndication feeds important&lt;/a&gt;: allowing&#xA;end-users to tinker with an interface alleviates some pressure to&#xA;predict what they need.&lt;p&gt;Tinkering is a gratifyingly &lt;em&gt;active&lt;/em&gt; kind of usership, one&#xA;that — at its extreme — cuts against applications’ commercial drive to&#xA;be desirable, to induce dependency.</content>
    <link href="https://lukasschwab.me/blog/gen/worse-on-purpose.html" rel="alternate"></link>
    <summary type="html">Customizing one&#39;s browser is a neat way to cut out app &#34;features&#34; that squeeze value out of users by being more irritating than useful.</summary>
  </entry>
  <entry>
    <title>Hearst Hall</title>
    <updated>2020-11-22T00:00:00Z</updated>
    <id>https://lukasschwab.me/blog/gen/hearst-hall.html</id>
    <content type="html">&lt;html xmlns=http://www.w3.org/1999/xhtml lang xml:lang&gt;&lt;meta charset=utf-8&gt;&lt;meta name=generator content=&#34;pandoc&#34;&gt;&lt;meta name=viewport content=&#34;width=device-width,initial-scale=1,user-scalable=yes&#34;&gt;&lt;meta name=author content=&#34;Lukas Schwab&#34;&gt;&lt;meta name=dcterms.date content=&#34;2020-11-22&#34;&gt;&lt;title&gt;Hearst Hall&lt;/title&gt;&lt;style&gt;&#xA;      code{white-space: pre-wrap;}&#xA;      span.smallcaps{font-variant: small-caps;}&#xA;      span.underline{text-decoration: underline;}&#xA;      div.column{display: inline-block; vertical-align: top; width: 50%;}&#xA;          &lt;/style&gt;&lt;link rel=stylesheet href=../styles/common.css&gt;&lt;link rel=stylesheet href=../styles/fence-extensions/addendum.css&gt;&lt;link rel=stylesheet href=../styles/fence-extensions/frame.css&gt;&lt;link rel=stylesheet href=../styles/fence-extensions/gallery.css&gt;&lt;script data-goatcounter=https://lukasschwab.goatcounter.com/count async src=//gc.zgo.at/count.js&gt;&lt;/script&gt;&lt;a href=../index.html&gt;&amp;#171; blog&lt;/a&gt; &lt;span class=date&gt;2020-11-22&lt;/span&gt;&lt;header id=title-block-header&gt;&lt;h1 class=title&gt;Hearst Hall&lt;/h1&gt;&lt;/header&gt;&lt;p&gt;An urban college campus is an interesting place for architectural&#xA;archaeology. UC Berkeley is dense, and in constant redevelopment; for a&#xA;couple hundred years, layers of Berkeley have been plowed under to make&#xA;way for new buildings, programs, and generations of students.&lt;p&gt;That plowing-under is so complete that, for five years, I lived a&#xA;short walk from what has become one of my favorite buildings in&#xA;California: a hall built and rebuilt by Bernard Maybeck, a star of the&#xA;California Arts &amp; Crafts movement, to house a women’s social&#xA;center-slash-theater-slash-concert hall. Hearst Hall is full of gothic&#xA;drama and prefigurations of modernism. For five years I didn’t realize&#xA;it because Hearst hall no longer exists.&lt;p&gt;I only discovered Hearst Hall after walking past the Swedenborgian&#xA;Church in San Francisco—also a Maybeck building—enough times to remember&#xA;to look it up. That church led me to Maybeck, who designed a great&#xA;number of notable buildings in Berkeley and San Francisco in his&#xA;idiosyncratic medieval-revivalist mashup style.&lt;figure&gt;&lt;img src=../img/hearst-hall/sitting.jpg alt=&#34;The interior of Bernard Maybeck’s Hearst Hall, built in 1899. This photo shows the interior configured as a sort of sitting room or theater space or something; the hall served a myriad of purposes&#34;&gt;&lt;figcaption aria-hidden=true&gt;The interior of Bernard Maybeck’s Hearst&#xA;Hall, built in 1899. This photo shows the interior configured as a sort&#xA;of sitting room or theater space or something; the hall served a myriad&#xA;of purposes&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;Phoebe Anderson Hearst turned to philanthropy, including a long&#xA;patronage of the University of California, after her husband’s death in&#xA;1891. In 1896 she funded a $100,000 competition to design an expanded&#xA;general plan for the University (the third such general design&#xA;commissioned, after an original design by F.L. Olmstead and an 1870&#xA;David Farquarson expansion Olmstead bitterly resented). As the&#xA;competition’s sponsor, she not only moved to Berkeley but commissioned&#xA;from Bernard Maybeck—then a professor—a new building to host competitors&#xA;and judges: Hearst Hall.&lt;a href=#fn1 class=footnote-ref id=fnref1 role=doc-noteref&gt;&lt;sup&gt;1&lt;/sup&gt;&lt;/a&gt;&lt;p&gt;The Hall’s main chamber is striking: the 16-meter-tall vault of a&#xA;gothic arch, extruded like an A-frame but closed at the ends, strung&#xA;with round arches of incandescent lights. It’s as if Maybeck sliced the&#xA;top off of an enormous redwood gothic basilica.&lt;p&gt;The decorators’ clever use of suspension (both in the decor for this&#xA;sitting-room arrangement and the gymnastic equipment pictured below)&#xA;gives the impression of matryoshka volumes, one inside the other. A&#xA;fabric hung unevenly by its four corners looks like it’s caught&#xA;mid-fall. The furniture is eclectic and sparse. Planters flank the&#xA;alcoves. The sense of space in these photos—the nested volumes drawing&#xA;attention to the voids between them—reminds me of this Paris Review line&#xA;about Berlin living rooms:&lt;blockquote&gt;&lt;p&gt;In my romance, the apartment is large enough for a given room to have&#xA;whatever purpose the occupant chooses for it. The point of a Berlin&#xA;apartment of this kind is to enjoy the emptiness.&lt;a href=#fn2 class=footnote-ref id=fnref2 role=doc-noteref&gt;&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt;&lt;/blockquote&gt;&lt;p&gt;Which, to my surprise, fits Maybeck’s architectural philosophy:&lt;blockquote&gt;&lt;p&gt;The house after all is only the shell and the real interest must come&#xA;from those who are to live in it. If this is done carefully and with&#xA;earnestness it will give the inmates a sense of satisfaction and rest&#xA;and will have the same power over the mind as music or poetry or any&#xA;healthy activity in any kind of human experience.&lt;a href=#fn3 class=footnote-ref id=fnref3 role=doc-noteref&gt;&lt;sup&gt;3&lt;/sup&gt;&lt;/a&gt;&lt;/blockquote&gt;&lt;p&gt;Hearst, like Maybeck, wanted the hall to serve a succession of&#xA;different virtuous purposes. Phoebe Hearst brought Berkeley a&#xA;world-class education in classical music—an event series complete with&#xA;individual invitations to students. When she left Berkeley for&#xA;Pleasanton, Hearst had the hall disassembled, moved, and reassembled on&#xA;the university’s campus to serve as a women’s social center (for&#xA;“Saturday afternoon receptions, musicals on Sundays, ‘At Home’&#xA;Wednesdays, dinners three evenings a week”&lt;a href=#fn4 class=footnote-ref id=fnref4 role=doc-noteref&gt;&lt;sup&gt;4&lt;/sup&gt;&lt;/a&gt;).&#xA;Hearst and Maybeck may have had that move in mind from the start: the&#xA;hall is a construction of “laminated arches and diaphragms… sections of&#xA;the building [were] independent, movable, and easily re-assembled&#xA;units.”&lt;a href=#fn5 class=footnote-ref id=fnref5 role=doc-noteref&gt;&lt;sup&gt;5&lt;/sup&gt;&lt;/a&gt; Realizing that “the women’s physical&#xA;education program was almost non-ecistent since the facilities of Harmon&#xA;Gymnasium were available to them only during those few hours a week when&#xA;wthe men were engaged in drill,” Hearst paid to repurpose and expand the&#xA;building as a women’s gymnasium.&lt;a href=#fn6 class=footnote-ref id=fnref6 role=doc-noteref&gt;&lt;sup&gt;6&lt;/sup&gt;&lt;/a&gt;&lt;figure&gt;&lt;img src=../img/hearst-hall/gym.jpg alt=&#34;Hearst Hall reconfigured as a Gym after its relocation to campus in 1901.&#34;&gt;&lt;figcaption aria-hidden=true&gt;Hearst Hall reconfigured as a Gym after&#xA;its relocation to campus in 1901.&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;The Hall was destroyed by a fire in 1922, three years after Phoebe&#xA;Hearst’s death. Its adjoining swimming pools—built when it became a&#xA;gym—were used for swimming and hydrological experiments until they were&#xA;cleared in 1955 to make way for Wurster Hall.&lt;a href=#fn7 class=footnote-ref id=fnref7 role=doc-noteref&gt;&lt;sup&gt;7&lt;/sup&gt;&lt;/a&gt;&lt;p&gt;William Randolph Hearst—Phoebe Hearst’s son—commissioned a new&#xA;women’s gym in his mother’s memory from Maybeck and Julia Morgan;&lt;a href=#fn8 class=footnote-ref id=fnref8 role=doc-noteref&gt;&lt;sup&gt;8&lt;/sup&gt;&lt;/a&gt; that’s the Hearst Memorial Gymnsium&#xA;on Cal’s campus today. Hearst Gym has none of former Hearst Hall’s&#xA;bohemian charm: it trades in the quirky medieval revivalism for a much&#xA;more fireproof stone-and-cinderblock Beaux-Arts. George Hearst’s&#xA;namesake builing, the Hearst Mining building in the northeast corner of&#xA;campus, is much prettier.&lt;p&gt;Wurster is an interesting point of dialog for its predecessor. It&#xA;houses Berkeley’s College of Environmental Design (CED), including Cal’s&#xA;architecture library; people without a taste for concrete joke about it&#xA;driving architecture students crazy. Personally I think Wurster Hall’s&#xA;ugliness is way overrated (Evans is a far more visually offensive, and&#xA;neighboring Law School commits the double-sin of being ugly &lt;em&gt;and&lt;/em&gt;&#xA;boring), but it certainly can’t be mistaken for a Maybeck building:&#xA;there’s nothing in the least bit handcrafted or medieval about it, maybe&#xA;save its general external &lt;em&gt;menace.&lt;/em&gt; Wurster Hall doesn’t exactly&#xA;project whimsy.&lt;p&gt;Beneath this concrete surface, though, Wurster Hall shares Hearst&#xA;Hall’s philosophical bones.&lt;p&gt;The modern hulk is named for William Wurster, founding president of&#xA;the CED. Wurster, a student of Maybeck’s, graduated from Berkeley in&#xA;1919. Like Maybeck, Wurster’s &lt;em&gt;professional&lt;/em&gt; style tended quirky,&#xA;low and modern but quintessentially Californian. Why would he commission&#xA;such a stark building for his department?&lt;figure&gt;&lt;img src=../img/hearst-hall/wurster.jpg alt=&#34;Wurster Hall nearing completion.&#34;&gt;&lt;figcaption aria-hidden=true&gt;Wurster Hall nearing completion.&lt;a href=#fn9 class=footnote-ref id=fnref9 role=doc-noteref&gt;&lt;sup&gt;9&lt;/sup&gt;&lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;&lt;blockquote&gt;&lt;p&gt;Bill Wurster wanted the designers to design what he called a&#xA;&lt;em&gt;ruin,&lt;/em&gt; a building that “achieved timelessness through freedom&#xA;from stylistic quirks.” […] Historian Sally Woodbridge neatly sums up&#xA;both William Wurster’s ideals and the building that was named after him:&#xA;“As Wurster Hall weathered without mellowing, it reflected Wurster’s&#xA;opinion that &lt;em&gt;a school should be a rough place with many cracks in&#xA;it. Perpetually unfinished, Wurster Hall was an open-ended and&#xA;provocative environment&lt;/em&gt; for teaching and questioning.”&lt;a href=#fn10 class=footnote-ref id=fnref10 role=doc-noteref&gt;&lt;sup&gt;10&lt;/sup&gt;&lt;/a&gt;&lt;/blockquote&gt;&lt;div class=addendum data-date=&#34;July 4, 2021&#34;&gt;&lt;p&gt;Woodbridge quotes Wurster’s Oral History:&lt;blockquote&gt;&lt;p&gt;“I wanted it to look like a ruin that no regent would like… It’s&#xA;absolutely unfinished, uncouth, and brilliantly strong.”&lt;a href=#fn11 class=footnote-ref id=fnref11 role=doc-noteref&gt;&lt;sup&gt;11&lt;/sup&gt;&lt;/a&gt;&lt;/blockquote&gt;&lt;/div&gt;&lt;p&gt;“A ruin.” Maybeck’s Palace of Fine Arts in San Francisco was famously&#xA;designed as a “forgotten and overgrown Roman ruin” in 1915.&lt;a href=#fn12 class=footnote-ref id=fnref12 role=doc-noteref&gt;&lt;sup&gt;12&lt;/sup&gt;&lt;/a&gt; Though they’re ruins in very&#xA;different styles, Wurster Hall and the PFA reference ruination to&#xA;signify a similar openness. Wurster talks about achieving timelessness&#xA;through quirklessness, but “ruin” evokes more than just bareness: ruins&#xA;point back at a pre-ruined history. Maybeck’s point back is nostalgic&#xA;for medieval handcrafts and classical arts, a rebellion against&#xA;industrial production. Wurster Hall feels altoghether bleaker; it looks&#xA;skeletonized, burned clean. Whereas the PFA points hauntologically at an&#xA;unrealized enlightened progression out of classicism, Wurster Hall&#xA;points back at biblical warnings: “MENE, MENE, TEKEL, UPHARSIN.”&lt;figure&gt;&lt;img src=../img/hearst-hall/pre-relocation.png alt=&#34;The buildings share some minor external similarities, though I have no evidence they’re intentional. Hearst Hall’s facade, with its towers, is hulking and carceral. The balcony on Wurster’s southern face is like a vestigial remnant of Hearst’s weird turret-obelisk. I’ve waxed poetic about Hearst Hall’s interior, but I’m cherry-picking: nothing about this exterior appeals to me.&#34;&gt;&lt;figcaption aria-hidden=true&gt;The buildings share &lt;em&gt;some&lt;/em&gt; minor&#xA;external similarities, though I have no evidence they’re intentional.&#xA;Hearst Hall’s facade, with its towers, is hulking and carceral. The&#xA;balcony on Wurster’s southern face is like a vestigial remnant of&#xA;Hearst’s weird turret-obelisk. I’ve waxed poetic about Hearst Hall’s&#xA;interior, but I’m cherry-picking: nothing about this exterior appeals to&#xA;me.&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;“Perpetually unfinished.” Wurster’s vision is consistent with&#xA;Maybeck’s philosophy, in which “the house after all is only the shell.”&#xA;Wurster’s bareness leaves it ready for contstant reconfiguration—as an&#xA;event space, as galleries of removable walls for end-of-semester&#xA;showcases, for students working in all kinds of groups on all manner of&#xA;projects. While the building itself can’t be disassembled and moved like&#xA;Hearst Hall was, its interior is constantly disassambled and&#xA;rebuilt.&lt;p&gt;This isn’t surprising for a modern design college, but I’m surprised&#xA;to spot echoes of the 19th century building once built on the same&#xA;ground. I’d go so far as to say that Wurster is the &lt;em&gt;only&lt;/em&gt;&#xA;building at Cal channeling Hearst Hall’s ethos: this openness to&#xA;rearrangement, the deference to the building’s inhabitants to give it&#xA;purpose and interest. Moreover, it’s one of very few buildings (along&#xA;with the Hearst Mining building, donated by Phoebe Hearst in memory of&#xA;her mining magnate husband), that reveals its own structure: whereas&#xA;Hearst Hall bared its ribcage of laminated arches, Wurster Hall flaunts&#xA;slab concrete.&lt;p&gt;The comparison doesn’t only spice up Wurster Hall; it reveals Hearst&#xA;Hall to be, in some respcts, &lt;em&gt;modern.&lt;/em&gt; Modularism, repurposing&#xA;spaces, and the unity of structural and aesthetic elements seem like&#xA;modernism’s innovations, but they’re all present in this 1899 building&#xA;by an architect whose architect was in rebellion &lt;em&gt;against&lt;/em&gt;&#xA;industrialist modernization.&lt;p&gt;Buildings are weird texts; they don’t lend themselvves to comparative&#xA;analysis. So much of architecture—Hearst Hall, for example—is unique:&#xA;they aren’t reproduced like books from an ur-text, which means that when&#xA;Hearst Hall burns down I’ll never really be able to “read” it as I can&#xA;read an extant building. Even extant buildings are complicated. As a&#xA;building’s purpose shifts, so does the way its occupants experience it.&#xA;I like the idea, though, that buildings point into the past like the&#xA;Archaeological Research Facility points, tongue in cheek, at its frat&#xA;origins: “look what used to be where I am now; consider the purpose I&#xA;was given then; consider the ruin I will eventually become.” Like Hearst&#xA;Hall’s nested arches, spaces inside spaces.&lt;div class=addendum data-date=&#34;July 5, 2021&#34;&gt;&lt;p&gt;After some continued reading on Bernard Maybeck, I’ve written &lt;a href=../gen/hearst-hall-continued-reading.html&gt;a follow-up to this&#xA;essay&lt;/a&gt;. It uses stronger evidence to cover much the same ground, and&#xA;extends my discussions of Maybeck’s architectural philosophy and the&#xA;design of Wurster Hall.&lt;/div&gt;&lt;aside id=footnotes class=&#34;footnotes footnotes-end-of-document&#34; role=doc-endnotes&gt;&lt;hr&gt;&lt;ol&gt;&lt;li id=fn1&gt;&lt;p&gt;Corbett, Michael. “Hearst Memorial Gymnasium Historic&#xA;Structure Report.” 2005: 12-13. Note: the most useful detailed, but&#xA;brief, chronology of Phoebe Hearst’s involvement with UC Berkeley I&#xA;could find.&lt;a href=#fnref1 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn2&gt;&lt;p&gt;Nabokov, Dominique &amp; Darryl Pinckney. “&lt;a href=https://www.theparisreview.org/blog/2017/03/02/berlin-living-rooms/&gt;Berlin&#xA;Living Rooms&lt;/a&gt;.” &lt;em&gt;Paris Review,&lt;/em&gt; 2017.&lt;p&gt;Never mind that I’m using this quote in a wildly different&#xA;architectural context. It’s funny to compare International Style&#xA;sparseness with Arts &amp; Crafts sparseness. Two departures from 19th&#xA;century maximalism, but departures towards very different metaphysics:&#xA;medievalism and handcraftsmanship on one hand, modernism and industrial&#xA;production on the other.&lt;a href=#fnref2 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn3&gt;&lt;p&gt;Cardwell, Kenneth H. &lt;em&gt;Bernard Maybeck: artisan,&#xA;architect, artist.&lt;/em&gt; Santa Barbara and Salt Lake City: Peregrine&#xA;Smith, 1977. (157)&lt;p&gt;Also quoted in Le Guin, Ursula K. “Words Are My Matter: Writings&#xA;About Life and Books, 2000-2016.” (52)&lt;p&gt;Le Guin grew up in a Berkeley house designed by Maybeck. Apparently&#xA;quite a quirky one: “our house, for example, originally had no stairs to&#xA;the basement. ‘Maybeck was moody about stairs.’” (53)&lt;p&gt;Le Guin calls Maybeck’s architectural philosophy premodern. I read&#xA;&lt;em&gt;The Left Hand of Darkness&lt;/em&gt; recently; maybe there’s an&#xA;inspiration here for Karhide’s distinctly premodern cities, with their&#xA;unreachable doors?&lt;p&gt;For more on Maybeck’s medievalism, see also Mitchell, James.&#xA;&lt;em&gt;Medieval San Francisco: Neo-Gothic Architecture in Northern&#xA;California, 1900-1940&lt;/em&gt; (2016), 89-96.&lt;a href=#fnref3 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn4&gt;&lt;p&gt;Kantor, J.R.K. “Cora, Jane, &amp; Phoebe: Fin-de-Siècle&#xA;Philanthropy.” &lt;a href=../img/hearst-hall/chronicle-1998.pdf&gt;&lt;em&gt;Chronicle of the&#xA;University of California&lt;/em&gt; 1, no. 2 (1998)&lt;/a&gt;: 6.&lt;a href=#fnref4 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn5&gt;&lt;p&gt;Miller, Leta E. “Practical Idealism: The Musical&#xA;Patronage of Phoebe Apperson Hearst.” &lt;em&gt;Journal of the Society for&#xA;American Music&lt;/em&gt; 10, no. 4 (2016): 397.&lt;a href=#fnref5 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn6&gt;&lt;p&gt;Kantor, J.R.K. “Cora, Jane, &amp; Phoebe: Fin-de-Siècle&#xA;Philanthropy.” &lt;a href=../img/hearst-hall/chronicle-1998.pdf&gt;&lt;em&gt;Chronicle of the&#xA;University of California&lt;/em&gt; 1, no. 2 (1998)&lt;/a&gt;: 6.&lt;p&gt;For a gushing contemporary account of the events and activities to&#xA;which Hearst Hall was home, see the 1904 Blue and Gold excerpt on page&#xA;9. For a detailed discussion of its impact on women’s athletics at Cal,&#xA;see “A Gym of Their Own” (Park) in the same volume, especially pages&#xA;25-26.&lt;a href=#fnref6 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn7&gt;&lt;p&gt;Dougherty, Carolyn. &lt;a href=../img/hearst-hall/loafers-guide-complete.pdf&gt;&lt;em&gt;The Loafer’s&#xA;Guide to UC Berkeley.&lt;/em&gt;&lt;/a&gt;&lt;a href=#fnref7 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn8&gt;&lt;p&gt;Maybeck and Morgan were surprising choices: Phoebe&#xA;Hearst vocally supported John Galen Howard, the Supervising Architect&#xA;hired in the campus expansion competition she sponsored.&lt;p&gt;Howard turned out to be something of a tyrant. From &lt;a href=../img/hearst-hall/loafers-guide-complete.pdf&gt;&lt;em&gt;The Loafer’s&#xA;Guide to UC Berkeley&lt;/em&gt;&lt;/a&gt;, with my emphasis added:&lt;blockquote&gt;&lt;p&gt;[Howard] had always insisted on complete control of campus design, as&#xA;well as the sole right to design campus buildings; this the Regents were&#xA;no longer willing to grant him. In 1922 &lt;em&gt;they awarded the design of&#xA;the new Hearst Gym to Bernard Maybeck and Julia Morgan while Howard was&#xA;in Europe.&lt;/em&gt; This building, and the attached buildings and grounds&#xA;originally planned, were the seed of a “revolt” organized by Bernard&#xA;Maybeck, Julia Morgan and William Randolph Hearst, whose memorial gym&#xA;for his mother had originally been intended as the nucleus of a&#xA;“countercampus” in the style of San Simeon.&lt;/blockquote&gt;&lt;p&gt;There’s probably a much more dramatic way to tell this story: three&#xA;generations of Hearsts (mining, publishing, and bank-robbing); fires;&#xA;rival architects with their respective patrons…&lt;a href=#fnref8 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn9&gt;&lt;p&gt;UC Berkeley College of Environmental Design. &lt;a href=https://ced.berkeley.edu/about-ced/college-history&gt;&lt;em&gt;College&#xA;History.&lt;/em&gt;&lt;/a&gt;&lt;a href=#fnref9 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn10&gt;&lt;p&gt;UC Berkeley College of Environmental Design. &lt;a href=https://ced.berkeley.edu/about-ced/college-history&gt;&lt;em&gt;College&#xA;History.&lt;/em&gt;&lt;/a&gt;&lt;a href=#fnref10 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn11&gt;&lt;p&gt;Woodbridge, Sally. &lt;a href=../img/hearst-hall/woodbridge-1984.pdf&gt;“Reflections on the&#xA;Founding: Wurster Hall and The College of Environmental Design [Two&#xA;Place Tales].”&lt;/a&gt; &lt;em&gt;Places&lt;/em&gt; 1, no. 4 (1984).&lt;a href=#fnref11 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn12&gt;&lt;p&gt;National register of Historic Places. &lt;a href=https://npgallery.nps.gov/GetAsset/51817f34-fdb5-4b0f-b8cd-f7561356cc1e&gt;&lt;em&gt;Registration&#xA;Form: Palace of Fine Arts.&lt;/em&gt;&lt;/a&gt;&lt;a href=#fnref12 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;/ol&gt;&lt;/aside&gt;</content>
    <link href="https://lukasschwab.me/blog/gen/hearst-hall.html" rel="alternate"></link>
    <summary type="html">Hearst Hall is a stunning example of California Arts &amp; Crafts architecture. What does a gothic hall&amp;#8212;built, moved, and built again by a mining baroness, burned down in 1922, and now largely forgotten&amp;#8212;say about the spaces that replace it?</summary>
  </entry>
  <entry>
    <title>Web Syndication with JSON Feeds</title>
    <updated>2020-11-02T00:00:00Z</updated>
    <id>https://lukasschwab.me/blog/gen/json-feed-tools.html</id>
    <content type="html">&lt;html xmlns=http://www.w3.org/1999/xhtml lang xml:lang&gt;&lt;meta charset=utf-8&gt;&lt;meta name=generator content=&#34;pandoc&#34;&gt;&lt;meta name=viewport content=&#34;width=device-width,initial-scale=1,user-scalable=yes&#34;&gt;&lt;meta name=author content=&#34;Lukas Schwab&#34;&gt;&lt;meta name=dcterms.date content=&#34;2020-11-02&#34;&gt;&lt;title&gt;Web Syndication with JSON Feeds&lt;/title&gt;&lt;style&gt;&#xA;      code{white-space: pre-wrap;}&#xA;      span.smallcaps{font-variant: small-caps;}&#xA;      span.underline{text-decoration: underline;}&#xA;      div.column{display: inline-block; vertical-align: top; width: 50%;}&#xA;          &lt;/style&gt;&lt;style&gt;pre &gt; code.sourceCode { white-space: pre; position: relative; }&#xA;pre &gt; code.sourceCode &gt; span { display: inline-block; line-height: 1.25; }&#xA;pre &gt; code.sourceCode &gt; span:empty { height: 1.2em; }&#xA;.sourceCode { overflow: visible; }&#xA;code.sourceCode &gt; span { color: inherit; text-decoration: inherit; }&#xA;div.sourceCode { margin: 1em 0; }&#xA;pre.sourceCode { margin: 0; }&#xA;@media screen {&#xA;div.sourceCode { overflow: auto; }&#xA;}&#xA;@media print {&#xA;pre &gt; code.sourceCode { white-space: pre-wrap; }&#xA;pre &gt; code.sourceCode &gt; span { text-indent: -5em; padding-left: 5em; }&#xA;}&#xA;pre.numberSource code&#xA;  { counter-reset: source-line 0; }&#xA;pre.numberSource code &gt; span&#xA;  { position: relative; left: -4em; counter-increment: source-line; }&#xA;pre.numberSource code &gt; span &gt; a:first-child::before&#xA;  { content: counter(source-line);&#xA;    position: relative; left: -1em; text-align: right; vertical-align: baseline;&#xA;    border: none; display: inline-block;&#xA;    -webkit-touch-callout: none; -webkit-user-select: none;&#xA;    -khtml-user-select: none; -moz-user-select: none;&#xA;    -ms-user-select: none; user-select: none;&#xA;    padding: 0 4px; width: 4em;&#xA;    color: #aaaaaa;&#xA;  }&#xA;pre.numberSource { margin-left: 3em; border-left: 1px solid #aaaaaa;  padding-left: 4px; }&#xA;div.sourceCode&#xA;  {   }&#xA;@media screen {&#xA;pre &gt; code.sourceCode &gt; span &gt; a:first-child::before { text-decoration: underline; }&#xA;}&#xA;code span.al { color: #ff0000; font-weight: bold; } /* Alert */&#xA;code span.an { color: #60a0b0; font-weight: bold; font-style: italic; } /* Annotation */&#xA;code span.at { color: #7d9029; } /* Attribute */&#xA;code span.bn { color: #40a070; } /* BaseN */&#xA;code span.bu { color: #008000; } /* BuiltIn */&#xA;code span.cf { color: #007020; font-weight: bold; } /* ControlFlow */&#xA;code span.ch { color: #4070a0; } /* Char */&#xA;code span.cn { color: #880000; } /* Constant */&#xA;code span.co { color: #60a0b0; font-style: italic; } /* Comment */&#xA;code span.cv { color: #60a0b0; font-weight: bold; font-style: italic; } /* CommentVar */&#xA;code span.do { color: #ba2121; font-style: italic; } /* Documentation */&#xA;code span.dt { color: #902000; } /* DataType */&#xA;code span.dv { color: #40a070; } /* DecVal */&#xA;code span.er { color: #ff0000; font-weight: bold; } /* Error */&#xA;code span.ex { } /* Extension */&#xA;code span.fl { color: #40a070; } /* Float */&#xA;code span.fu { color: #06287e; } /* Function */&#xA;code span.im { color: #008000; font-weight: bold; } /* Import */&#xA;code span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Information */&#xA;code span.kw { color: #007020; font-weight: bold; } /* Keyword */&#xA;code span.op { color: #666666; } /* Operator */&#xA;code span.ot { color: #007020; } /* Other */&#xA;code span.pp { color: #bc7a00; } /* Preprocessor */&#xA;code span.sc { color: #4070a0; } /* SpecialChar */&#xA;code span.ss { color: #bb6688; } /* SpecialString */&#xA;code span.st { color: #4070a0; } /* String */&#xA;code span.va { color: #19177c; } /* Variable */&#xA;code span.vs { color: #4070a0; } /* VerbatimString */&#xA;code span.wa { color: #60a0b0; font-weight: bold; font-style: italic; } /* Warning */&lt;/style&gt;&lt;link rel=stylesheet href=../styles/common.css&gt;&lt;script data-goatcounter=https://lukasschwab.goatcounter.com/count async src=//gc.zgo.at/count.js&gt;&lt;/script&gt;&lt;a href=../index.html&gt;&amp;#171; blog&lt;/a&gt; &lt;span class=date&gt;2020-11-02&lt;/span&gt;&lt;header id=title-block-header&gt;&lt;h1 class=title&gt;Web Syndication with JSON Feeds&lt;/h1&gt;&lt;/header&gt;&lt;p&gt;Sometime in the last ten years, while you were mourning the loss of&#xA;Google Reader, we entered the golden age of content syndication. Our&#xA;social media overlords hit the syndication Comstock Lode. For all their&#xA;&lt;a href=http://www.roughlydrafted.com/Oct05.RSS.html&gt;dystopic&#xA;visioneering&lt;/a&gt;, I doubt the feed-accelerationists at Microsoft and&#xA;Netscape in the mid 90’s foresaw these particular macroeconomics.&lt;p&gt;Arguably, though, we’re also in a golden age of nondystopic&#xA;&lt;em&gt;author-managed&lt;/em&gt; syndication. Free and nearly-free tools for&#xA;hosting static sites are only outnumbered by static site generators; new&#xA;ones are released every week. These tools, like blogosphere-era blogging&#xA;platforms, can generate feeds as side effects of the routine publishing&#xA;activity of their users; many do so by default. Even if it’s only a feed&#xA;of content previews (to draw users onto the publisher’s site), each feed&#xA;is a contribution to the digital commons.&lt;a href=#fn1 class=footnote-ref id=fnref1 role=doc-noteref&gt;&lt;sup&gt;1&lt;/sup&gt;&lt;/a&gt;&lt;p&gt;Syndicated feeds — for which RSS, Atom, and JSON Feed are&#xA;specifications — are essentially different from the feeds turning social&#xA;media users into blue-app-anxiety foie gras. Rather than an&#xA;algorithmically ranked and collated series of texts from a variety of&#xA;sources, syndicated feeds just &lt;em&gt;list items&lt;/em&gt; as a single source;&#xA;the categorizing, collating, and display of those items is left up to&#xA;feeds’ consumers. This has accessibility upsides, makes feeds easy to&#xA;process programmatically, and provides a neat interface for users&#xA;waiting on sparse updates (e.g. a blog that only updates once in a blue&#xA;moon).&lt;p&gt;Providing a feed might mean content loses ad impressions to feed&#xA;readers, but feeds generally align the interests of author-publishers&#xA;who want their work &lt;em&gt;read&lt;/em&gt; with the folks doing the reading.&lt;p&gt;The social challenge: maintaining and checking a feed reader will&#xA;reward users &lt;em&gt;only&lt;/em&gt; if their favorite sources of content provide&#xA;feeds to be followed. For those sources of content, maintaining a feed&#xA;(trickiest during site migrations!) is only worthwhile if readers&#xA;&lt;em&gt;would not otherwise follow them.&lt;/em&gt;&lt;p&gt;An introductory note on feed formats — RSS has the longest history&#xA;and is the most widely-known, but its XML specification is pretty deeply&#xA;janky. I would not recommend writing code for working with RSS feeds.&#xA;Atom, RSS’s successor in the XML feed tradition, is a strict&#xA;improvement. Most feed readers support both.&lt;p&gt;&lt;a href=https://www.jsonfeed.org/&gt;JSON Feed&lt;/a&gt; is a relative&#xA;newcomer, introduced by the authors of NetNewsWire and Micro.blog. It&#xA;has less client support than Atom/RSS, but it’s a sweet format to tinker&#xA;with. I find JSON easier to read than XML, and my languages of choice&#xA;these days (Go, Python, Typescript) have much nicer support for parsing&#xA;and writing JSON objects than for XML (even with Python’s&#xA;&lt;code&gt;feedparser&lt;/code&gt;).&lt;p&gt;JSON feeds make syndication &lt;em&gt;so simple&lt;/em&gt; that I’ve written a&#xA;cluster of interrelated tools for working with them. Here’s a narrative&#xA;breakdown of how they came to be and how I use them together.&lt;hr&gt;&lt;p&gt;Habitually collecting feeds makes one &lt;em&gt;very&lt;/em&gt; aware of how many&#xA;sites don’t (but should!) have them; how many &lt;em&gt;have&lt;/em&gt; feeds but&#xA;don’t prominently list links to them; and how many publications offer&#xA;central aggregate feeds but not feeds broken down by category or author.&#xA;I’ve built myself a few tools to help with this.&lt;p&gt;&lt;a href=https://github.com/lukasschwab/bin/blob/master/feedscan&gt;&lt;code&gt;feedscan&lt;/code&gt;&lt;/a&gt;&#xA;is a bash utility for discovering feeds by checking the routes that&#xA;commonly host them: &lt;code&gt;/feed&lt;/code&gt;, &lt;code&gt;/atom.xml&lt;/code&gt;, and so&#xA;on. If I find a sweet blog at &lt;a href=httpes://lukasschwab.me/blog&gt;lukasschwab.me/blog&lt;/a&gt;, I try&#xA;&lt;code&gt;feedscan https://lukasschwab.me/blog&lt;/code&gt; before digging for a&#xA;link on the site itself. It’s totally disconnected from the other&#xA;projects discussed here, but it has saved me a lot of frantic&#xA;searches.&lt;p&gt;&lt;a href=https://github.com/lukasschwab/jsonfeed&gt;&lt;code&gt;jsonfeed&lt;/code&gt;&lt;/a&gt;&#xA;is a JSON feed parser and constructor package written in Python, the&#xA;backbone to most of my other JSON feed tools. I wrote a Go equivalent,&#xA;&lt;a href=https://github.com/lukasschwab/go-jsonfeed&gt;&lt;code&gt;go-jsonfeed&lt;/code&gt;&lt;/a&gt;,&#xA;but haven’t used it much. This very blog is generated with a fork of &lt;a href=https://github.com/lukasschwab/pandoc-blog&gt;&lt;code&gt;pandoc-blog&lt;/code&gt;&lt;/a&gt;,&#xA;which generates &lt;a href=file:///Users/lukas/Programming/blog/feed.json&gt;a JSON feed&lt;/a&gt;&#xA;using the &lt;code&gt;jsonfeed&lt;/code&gt; package I authored.&lt;p&gt;I discovered a little running project for myself in building and&#xA;hosting public feeds for sites that don’t offer them. I got my start&#xA;with &lt;a href=https://arxiv-feeds.appspot.com/&gt;&lt;code&gt;arxiv-feeds&lt;/code&gt;&lt;/a&gt;,&#xA;which converts Atom to JSON using &lt;code&gt;jsonfeed&lt;/code&gt;, but it’s a&#xA;relatively boring wrapper. Blogs and news sites are more fun because&#xA;they involve scraping feed items from the sites on demand. I wrote&#xA;separate Python scraper/generator apps for a couple of sites, then&#xA;realized those generators shared a certain procedural structure:&lt;ol type=1&gt;&lt;li&gt;Fetch some target page.&lt;li&gt;Parse the target page HTML (with Beautiful Soup).&lt;li&gt;Extract feed items from the parsed HTML, an operation unique to each&#xA;site.&lt;li&gt;Return the constructed feed.&lt;/ol&gt;&lt;p&gt;Steps 1, 2, and 4 were essentially shared, so I factored them out&#xA;into &lt;a href=https://github.com/lukasschwab/jsonfeed-wrapper&gt;&lt;code&gt;jsonfeed-wrapper&lt;/code&gt;&lt;/a&gt;,&#xA;which takes the site-specific HTML-to-feed transform and wraps it with&#xA;the standard fetching and feed-serving logic. I originally designed it&#xA;for use with Google App Engine, but last weekend I rewrote it to expose&#xA;a Google Cloud Function target.&lt;a href=#fn2 class=footnote-ref id=fnref2 role=doc-noteref&gt;&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt; Cloud Functions save me&#xA;a couple bucks a month.&lt;p&gt;I generate and host feeds for &lt;a href=https://github.com/lukasschwab/itsnicethat-feed&gt;It’s Nice&#xA;That&lt;/a&gt;, &lt;a href=https://github.com/lukasschwab/bandcamp-feeds&gt;Bandcamp&#xA;artists&lt;/a&gt;, &lt;a href=https://github.com/lukasschwab/baffler-feeds&gt;The&#xA;Baffler&lt;/a&gt;, and &lt;a href=https://github.com/lukasschwab/atlas-feed&gt;Atlas of Places&lt;/a&gt;.&#xA;Generating feeds from scraped HTML is somewhat brittle, but these have&#xA;been reliable enough for the last few months. Adding a new site to the&#xA;list takes about an hour of filling out a &lt;code&gt;jsonfeed-wrapper&lt;/code&gt;&#xA;template; shortening that time is the &lt;code&gt;jsonfeed-wrapper&lt;/code&gt;&#xA;project’s north star. Everything deserves a feed.&lt;p&gt;The next frontier: feed filters with &lt;a href=https://github.com/google/cel-spec&gt;&lt;code&gt;CEL&lt;/code&gt;&lt;/a&gt;.&#xA;&lt;code&gt;cel-go&lt;/code&gt; works neatly with raw JSON, but the JSON Feed schema&#xA;is well defined — why not create a &lt;code&gt;CEL&lt;/code&gt; environment with&#xA;types and macros for filtering feeds?&lt;p&gt;I have a Cloud Function that does nothing but parse Bruce Schneier’s&#xA;RSS feed, filter out the feed items involing squid (Bruce’s hobby&#xA;outside of security), and re-host the feed. There’s no reason this&#xA;filtered feed should be re-hosted on its own when it could&#xA;&lt;em&gt;instead&lt;/em&gt; compile a CEL expression it receives from a client:&lt;div class=sourceCode id=cb1&gt;&lt;pre class=&#34;sourceCode txt&#34;&gt;&lt;code class=&#34;sourceCode default&#34;&gt;&lt;span id=cb1-1&gt;&lt;a href=#cb1-1 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;!item.tags.exists_one(t, t == &amp;quot;squid&amp;quot;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;…and just return items where that expression returns&#xA;&lt;code&gt;true&lt;/code&gt;. User-defined &lt;code&gt;CEL&lt;/code&gt; expressions are&#xA;non-Turing-complete and safe to execute, so I can use them in lieu of&#xA;parsing and documenting some feed-specific filter API. Different&#xA;requests, passing different &lt;code&gt;CEL&lt;/code&gt; expressions, can fetch&#xA;differently filtered feeds from the same endpoint.&lt;p&gt;I will probably never convince anyone to host a feed that behaves&#xA;this way, but that’s the neat thing about syndication: I can mirror or&#xA;aggregate other feeds in a feed of my own that provides the interface&#xA;&lt;em&gt;I&lt;/em&gt; want. No need to ask anyone else to implement anything.&lt;aside id=footnotes class=&#34;footnotes footnotes-end-of-document&#34; role=doc-endnotes&gt;&lt;hr&gt;&lt;ol&gt;&lt;li id=fn1&gt;&lt;p&gt;Podcasts are a good example of syndication working as&#xA;expected: a system of RSS/Atom feeds means your podcast is available in&#xA;whatever listening environment you like, including as a raw audio file.&#xA;Platform-exclusive podcasts are trendy but somewhat toxic.&lt;a href=#fnref1 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn2&gt;&lt;p&gt;Rewriting &lt;code&gt;jsonfeed-wrapper&lt;/code&gt; from something&#xA;App Engine specific to something that could equivalently provide a Cloud&#xA;Function was super simple because of how each of those Cloud Services&#xA;register the code’s entry points.&lt;p&gt;Google App Engine’s Python runtime requires a WSGI-compatible object&#xA;(in this case, a &lt;code&gt;bottle&lt;/code&gt; application). Google’s Cloud&#xA;Function runtime, on the other hand, asks you to define a function for&#xA;handling requests.&lt;p&gt;The platform-ambivalent rewrite of &lt;code&gt;jsonfeed-wrapper&lt;/code&gt;&#xA;exports interfaces for constructing both, and everything but the&#xA;request/response handling code is shared. Moreover, since instantiating&#xA;a &lt;code&gt;bottle&lt;/code&gt; app doesn’t do the hard work of actually&#xA;&lt;em&gt;running it&lt;/em&gt;, leaving the app definition in the Cloud Function&#xA;code has a minimal impact on performance.&lt;p&gt;The result: each of my &lt;code&gt;jsonfeed-wrapper&lt;/code&gt; applications can&#xA;be deployed either as a Cloud Function or as an App Engine app&#xA;&lt;em&gt;without changing a single line of code.&lt;/em&gt;&lt;a href=#fnref2 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;/ol&gt;&lt;/aside&gt;</content>
    <link href="https://lukasschwab.me/blog/gen/json-feed-tools.html" rel="alternate"></link>
    <summary type="html">Sometime in the last ten years, while you were mourning the loss of Google Reader, we entered the golden age of the feed. A brief pitch for web syndication and an overview of the tools I&#39;ve built for generating and working with JSON Feeds.</summary>
  </entry>
  <entry>
    <title>Processing PDFs with Cloud Functions</title>
    <updated>2020-10-19T00:00:00Z</updated>
    <id>https://lukasschwab.me/blog/gen/cloud-function-pdf-processing.html</id>
    <content type="html">&lt;html xmlns=http://www.w3.org/1999/xhtml lang xml:lang&gt;&lt;meta charset=utf-8&gt;&lt;meta name=generator content=&#34;pandoc&#34;&gt;&lt;meta name=viewport content=&#34;width=device-width,initial-scale=1,user-scalable=yes&#34;&gt;&lt;meta name=author content=&#34;Lukas Schwab&#34;&gt;&lt;meta name=dcterms.date content=&#34;2020-10-19&#34;&gt;&lt;title&gt;Processing PDFs with Cloud Functions&lt;/title&gt;&lt;style&gt;&#xA;      code{white-space: pre-wrap;}&#xA;      span.smallcaps{font-variant: small-caps;}&#xA;      span.underline{text-decoration: underline;}&#xA;      div.column{display: inline-block; vertical-align: top; width: 50%;}&#xA;          &lt;/style&gt;&lt;style&gt;pre &gt; code.sourceCode { white-space: pre; position: relative; }&#xA;pre &gt; code.sourceCode &gt; span { display: inline-block; line-height: 1.25; }&#xA;pre &gt; code.sourceCode &gt; span:empty { height: 1.2em; }&#xA;.sourceCode { overflow: visible; }&#xA;code.sourceCode &gt; span { color: inherit; text-decoration: inherit; }&#xA;div.sourceCode { margin: 1em 0; }&#xA;pre.sourceCode { margin: 0; }&#xA;@media screen {&#xA;div.sourceCode { overflow: auto; }&#xA;}&#xA;@media print {&#xA;pre &gt; code.sourceCode { white-space: pre-wrap; }&#xA;pre &gt; code.sourceCode &gt; span { text-indent: -5em; padding-left: 5em; }&#xA;}&#xA;pre.numberSource code&#xA;  { counter-reset: source-line 0; }&#xA;pre.numberSource code &gt; span&#xA;  { position: relative; left: -4em; counter-increment: source-line; }&#xA;pre.numberSource code &gt; span &gt; a:first-child::before&#xA;  { content: counter(source-line);&#xA;    position: relative; left: -1em; text-align: right; vertical-align: baseline;&#xA;    border: none; display: inline-block;&#xA;    -webkit-touch-callout: none; -webkit-user-select: none;&#xA;    -khtml-user-select: none; -moz-user-select: none;&#xA;    -ms-user-select: none; user-select: none;&#xA;    padding: 0 4px; width: 4em;&#xA;    color: #aaaaaa;&#xA;  }&#xA;pre.numberSource { margin-left: 3em; border-left: 1px solid #aaaaaa;  padding-left: 4px; }&#xA;div.sourceCode&#xA;  {   }&#xA;@media screen {&#xA;pre &gt; code.sourceCode &gt; span &gt; a:first-child::before { text-decoration: underline; }&#xA;}&#xA;code span.al { color: #ff0000; font-weight: bold; } /* Alert */&#xA;code span.an { color: #60a0b0; font-weight: bold; font-style: italic; } /* Annotation */&#xA;code span.at { color: #7d9029; } /* Attribute */&#xA;code span.bn { color: #40a070; } /* BaseN */&#xA;code span.bu { color: #008000; } /* BuiltIn */&#xA;code span.cf { color: #007020; font-weight: bold; } /* ControlFlow */&#xA;code span.ch { color: #4070a0; } /* Char */&#xA;code span.cn { color: #880000; } /* Constant */&#xA;code span.co { color: #60a0b0; font-style: italic; } /* Comment */&#xA;code span.cv { color: #60a0b0; font-weight: bold; font-style: italic; } /* CommentVar */&#xA;code span.do { color: #ba2121; font-style: italic; } /* Documentation */&#xA;code span.dt { color: #902000; } /* DataType */&#xA;code span.dv { color: #40a070; } /* DecVal */&#xA;code span.er { color: #ff0000; font-weight: bold; } /* Error */&#xA;code span.ex { } /* Extension */&#xA;code span.fl { color: #40a070; } /* Float */&#xA;code span.fu { color: #06287e; } /* Function */&#xA;code span.im { color: #008000; font-weight: bold; } /* Import */&#xA;code span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Information */&#xA;code span.kw { color: #007020; font-weight: bold; } /* Keyword */&#xA;code span.op { color: #666666; } /* Operator */&#xA;code span.ot { color: #007020; } /* Other */&#xA;code span.pp { color: #bc7a00; } /* Preprocessor */&#xA;code span.sc { color: #4070a0; } /* SpecialChar */&#xA;code span.ss { color: #bb6688; } /* SpecialString */&#xA;code span.st { color: #4070a0; } /* String */&#xA;code span.va { color: #19177c; } /* Variable */&#xA;code span.vs { color: #4070a0; } /* VerbatimString */&#xA;code span.wa { color: #60a0b0; font-weight: bold; font-style: italic; } /* Warning */&lt;/style&gt;&lt;link rel=stylesheet href=../styles/common.css&gt;&lt;script data-goatcounter=https://lukasschwab.goatcounter.com/count async src=//gc.zgo.at/count.js&gt;&lt;/script&gt;&lt;a href=../index.html&gt;&amp;#171; blog&lt;/a&gt; &lt;span class=date&gt;2020-10-19&lt;/span&gt;&lt;header id=title-block-header&gt;&lt;h1 class=title&gt;Processing PDFs with Cloud Functions&lt;/h1&gt;&lt;/header&gt;&lt;p&gt;Two months ago I found the &lt;a href=http://sunnyday.mit.edu/papers/&gt;trove of papers on Nancy&#xA;Leveson’s MIT homepage&lt;/a&gt;&lt;a href=#fn1 class=footnote-ref id=fnref1 role=doc-noteref&gt;&lt;sup&gt;1&lt;/sup&gt;&lt;/a&gt;, which planted the seed&#xA;of a thought. Academic homepages are full of interesting reading&#xA;(unpublished rambles; administrivia; pre-prints, the &lt;em&gt;published&lt;/em&gt;&#xA;versions of which are inaccessible behind paywalls). Academic homepages&#xA;are also woefully ill-maintained. Why not scrape them?&lt;p&gt;They’re good candidates. Most are hand-written static sites with&#xA;simple DOMs. Most are reachable from central departmental indices.&lt;p&gt;In the days after I found Dr. Leveson’s homepage, in the breaks&#xA;between her papers, I threw together a &lt;code&gt;scrapy&lt;/code&gt; spider for&#xA;pulling PDF URLs a department at a time, and for jotting those URLs down&#xA;into text files… but that’s where the project stalled out.&lt;a href=#fn2 class=footnote-ref id=fnref2 role=doc-noteref&gt;&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt;&lt;div class=sourceCode id=cb2&gt;&lt;pre class=&#34;sourceCode txt&#34;&gt;&lt;code class=&#34;sourceCode default&#34;&gt;&lt;span id=cb2-1&gt;&lt;a href=#cb2-1 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;https://arxiv.org/pdf/1711.02226.pdf&lt;/span&gt;&#xA;&lt;span id=cb2-2&gt;&lt;a href=#cb2-2 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;http://proceedings.mlr.press/v70/carmon17a/carmon17a.pdf&lt;/span&gt;&#xA;&lt;span id=cb2-3&gt;&lt;a href=#cb2-3 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;http://proceedings.mlr.press/v70/namkoong17a/namkoong17a-supp.pdf&lt;/span&gt;&#xA;&lt;span id=cb2-4&gt;&lt;a href=#cb2-4 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;https://web.stanford.edu/~jduchi/projects/SinhaDu16.pdf&lt;/span&gt;&#xA;&lt;span id=cb2-5&gt;&lt;a href=#cb2-5 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;...&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;My scraper was fast, but I had no interest in actually&#xA;&lt;em&gt;downloading&lt;/em&gt; tens of gigabytes of PDFs––tens of thousands of&#xA;files!––that I would, realistically, never read. The code would be&#xA;annoying to write and molassify my home network. I moved on to other&#xA;work.&lt;p&gt;But what is cloud computing for, if not the senseless accumulation of&#xA;&lt;em&gt;online stuff?&lt;/em&gt; Google Cloud Platform (or the competing cloud&#xA;platform of your choice) has everything we need to store &lt;em&gt;and&#xA;index&lt;/em&gt; the papers my scraper discovers:&lt;ul&gt;&lt;li&gt;A dirt-cheap blobstore for stashing files: Cloud Storage. Tens of&#xA;gigabytes? Easy.&lt;li&gt;Ephemeral, networked compute resources: Cloud Functions. These&#xA;achieve two tasks.&lt;ol type=1&gt;&lt;li&gt;Ingestion: coordinate transfering PDFs into Cloud Storage.&lt;li&gt;Processing: once a PDF is stored, extract useful metadata from it&#xA;(e.g. its text contents for search).&lt;/ol&gt;&lt;/ul&gt;&lt;p&gt;I haven’t used Cloud Functions much in the past (I often &lt;a href=https://github.com/lukasschwab/jsonfeed-wrapper&gt;reach for App&#xA;Engine&lt;/a&gt; out of habit, even when Cloud Functions will suffice). My&#xA;impressions from this project:&lt;table&gt;&lt;col style=&#34;width: 50%&#34;&gt;&lt;col style=&#34;width: 50%&#34;&gt;&lt;thead&gt;&lt;tr class=header&gt;&lt;th&gt;Advantages&lt;th&gt;Disadvantages&lt;tbody&gt;&lt;tr class=odd&gt;&lt;td&gt;+ Concurrency without code&lt;br&gt;+ Easy IAM permissioning&lt;br&gt;+&#xA;Built-in event triggers&lt;br&gt;+ Multi-language pipelines&lt;td&gt;- Awkward local debugging&lt;br&gt;- Awkward remote debugging&lt;br&gt;-&#xA;Multi-minute deployments&lt;/table&gt;&lt;p&gt;Instead of focusing on their shortcomings, we’ll walk through what&#xA;they handled well: I’ve indexed more than 20,000 PDFs––just over 20&#xA;gigabytes––and extracted 500 megabytes of plain text.&lt;div class=sourceCode id=cb3&gt;&lt;pre class=&#34;sourceCode bash&#34;&gt;&lt;code class=&#34;sourceCode bash&#34;&gt;&lt;span id=cb3-1&gt;&lt;a href=#cb3-1 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=ex&gt;$&lt;/span&gt; gsutil ls &lt;span class=st&gt;&amp;quot;gs://documents-repository/**&amp;quot;&lt;/span&gt; &lt;span class=kw&gt;|&lt;/span&gt; &lt;span class=fu&gt;grep&lt;/span&gt; &lt;span class=st&gt;&amp;quot;.pdf$&amp;quot;&lt;/span&gt; &lt;span class=kw&gt;|&lt;/span&gt; &lt;span class=fu&gt;wc&lt;/span&gt; &lt;span class=at&gt;-l&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb3-2&gt;&lt;a href=#cb3-2 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;  &lt;span class=ex&gt;20243&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb3-3&gt;&lt;a href=#cb3-3 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;/span&gt;&#xA;&lt;span id=cb3-4&gt;&lt;a href=#cb3-4 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=ex&gt;$&lt;/span&gt; gsutil du &lt;span class=at&gt;-e&lt;/span&gt; &lt;span class=st&gt;&amp;quot;*.txt&amp;quot;&lt;/span&gt; &lt;span class=at&gt;-sh&lt;/span&gt; &lt;span class=st&gt;&amp;quot;gs://documents-repository/&amp;quot;&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb3-5&gt;&lt;a href=#cb3-5 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=ex&gt;20.07&lt;/span&gt; GiB    gs://documents-repository&lt;/span&gt;&#xA;&lt;span id=cb3-6&gt;&lt;a href=#cb3-6 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;/span&gt;&#xA;&lt;span id=cb3-7&gt;&lt;a href=#cb3-7 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=ex&gt;$&lt;/span&gt; gsutil du &lt;span class=at&gt;-e&lt;/span&gt; &lt;span class=st&gt;&amp;quot;*.pdf&amp;quot;&lt;/span&gt; &lt;span class=at&gt;-sh&lt;/span&gt; &lt;span class=st&gt;&amp;quot;gs://documents-repository/&amp;quot;&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb3-8&gt;&lt;a href=#cb3-8 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=ex&gt;498.76&lt;/span&gt; MiB   gs://documents-repository&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;All this without using &lt;code&gt;ssh&lt;/code&gt; to connect to a server,&#xA;configuring a network, sharing a client among concurrent routines, or&#xA;spending more than $1.00.&lt;h1 id=uploading-documents&gt;Uploading documents&lt;/h1&gt;&lt;p&gt;Our first challenge: process the old files of jotted-down URLs and&#xA;persist the PDFs. Several properties of this problem make Cloud&#xA;Functions attractive.&lt;ol type=1&gt;&lt;li&gt;&lt;p&gt;I can pipe the PDFs straight from the network into Cloud Storage&#xA;without holding an entire file in memory.&lt;li&gt;&lt;p&gt;Since there are so many PDFs, I’d like to download as many of&#xA;them concurrently as possible. A highly concurrent program on a single&#xA;device might end up I/O-bottlenecked, but each Cloud Function has its&#xA;own network resources! Need more concurrent uploads? GCP will&#xA;automatically provision more Cloud Function instances.&lt;li&gt;&lt;p&gt;Since invoking a Function is lightweight––a POST request with a&#xA;URL––we can eventually invoke it &lt;em&gt;directly from my scraper&lt;/em&gt;&#xA;instead of writing PDF URLs to an intermediate file. If the scraper code&#xA;had to download and upload files itself, it’d be unworkably&#xA;slow.&lt;li&gt;&lt;p&gt;The scraper doesn’t validate the URLs; they end with&#xA;&lt;code&gt;.pdf&lt;/code&gt;, but they could point at anything. Some may lead to&#xA;sites that no longer exist. Some may point at redirects. Since we have&#xA;few guarantees about the &lt;em&gt;quality of our input&lt;/em&gt;, we don’t have to&#xA;bother managing failures in individual uploads. Let them fail!&lt;/ol&gt;&lt;p&gt;We can split uploading documents into two scripts: a Cloud Function&#xA;which takes a single URL and pipes the PDF to Cloud Storage, and a&#xA;script that turns my scraped files into invocations of that Cloud&#xA;Function.&lt;p&gt;Piping PDF data from a response body into GCS reminded me of Chris&#xA;Roche’s &lt;a href=https://rodaine.com/2015/04/async-split-io-reader-in-golang/&gt;post&#xA;on splitting &lt;code&gt;io.Reader&lt;/code&gt;s&lt;/a&gt; Since this Cloud Function is&#xA;self-contained and provides a neatly defined interface&#xA;(&lt;code&gt;(url) =&gt; pdf&lt;/code&gt;), we don’t have to worry about our upload&#xA;language being suitable for analyzing/indexing the PDFs. We’re free to&#xA;choose whatever language fits our upload style. In Go:&lt;div class=sourceCode id=cb4&gt;&lt;pre class=&#34;sourceCode go&#34;&gt;&lt;code class=&#34;sourceCode go&#34;&gt;&lt;span id=cb4-1&gt;&lt;a href=#cb4-1 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=co&gt;// Package nmt contains a Cloud Function for streaming documents to GCS.&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-2&gt;&lt;a href=#cb4-2 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=kw&gt;package&lt;/span&gt; nmt&lt;/span&gt;&#xA;&lt;span id=cb4-3&gt;&lt;a href=#cb4-3 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-4&gt;&lt;a href=#cb4-4 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=kw&gt;import&lt;/span&gt; &lt;span class=op&gt;(&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-5&gt;&lt;a href=#cb4-5 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    &lt;span class=st&gt;&amp;quot;context&amp;quot;&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-6&gt;&lt;a href=#cb4-6 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    &lt;span class=st&gt;&amp;quot;encoding/json&amp;quot;&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-7&gt;&lt;a href=#cb4-7 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    &lt;span class=st&gt;&amp;quot;fmt&amp;quot;&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-8&gt;&lt;a href=#cb4-8 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    &lt;span class=st&gt;&amp;quot;io&amp;quot;&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-9&gt;&lt;a href=#cb4-9 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    &lt;span class=st&gt;&amp;quot;net/http&amp;quot;&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-10&gt;&lt;a href=#cb4-10 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    &lt;span class=st&gt;&amp;quot;net/url&amp;quot;&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-11&gt;&lt;a href=#cb4-11 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-12&gt;&lt;a href=#cb4-12 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    &lt;span class=st&gt;&amp;quot;cloud.google.com/go/storage&amp;quot;&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-13&gt;&lt;a href=#cb4-13 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=op&gt;)&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-14&gt;&lt;a href=#cb4-14 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-15&gt;&lt;a href=#cb4-15 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=co&gt;// message is the data to parse from an invocation body. At this stage, there&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-16&gt;&lt;a href=#cb4-16 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=co&gt;// is no metadata besides a PDF URL.&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-17&gt;&lt;a href=#cb4-17 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=kw&gt;type&lt;/span&gt; message &lt;span class=kw&gt;struct&lt;/span&gt; &lt;span class=op&gt;{&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-18&gt;&lt;a href=#cb4-18 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    URL &lt;span class=dt&gt;string&lt;/span&gt; &lt;span class=st&gt;`json:&amp;quot;url&amp;quot;`&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-19&gt;&lt;a href=#cb4-19 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=op&gt;}&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-20&gt;&lt;a href=#cb4-20 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-21&gt;&lt;a href=#cb4-21 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=co&gt;// Name for the bucket to which documents should be written. This Cloud&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-22&gt;&lt;a href=#cb4-22 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=co&gt;// Function assumes the bucket is in the same GCP project.&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-23&gt;&lt;a href=#cb4-23 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=kw&gt;const&lt;/span&gt; bucketName &lt;span class=op&gt;=&lt;/span&gt; &lt;span class=st&gt;&amp;quot;documents-repository&amp;quot;&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-24&gt;&lt;a href=#cb4-24 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-25&gt;&lt;a href=#cb4-25 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=co&gt;// Ingest streams the file at a URL to Cloud Storage.&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-26&gt;&lt;a href=#cb4-26 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=kw&gt;func&lt;/span&gt; Ingest&lt;span class=op&gt;(&lt;/span&gt;w http&lt;span class=op&gt;.&lt;/span&gt;ResponseWriter&lt;span class=op&gt;,&lt;/span&gt; r &lt;span class=op&gt;*&lt;/span&gt;http&lt;span class=op&gt;.&lt;/span&gt;Request&lt;span class=op&gt;)&lt;/span&gt; &lt;span class=op&gt;{&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-27&gt;&lt;a href=#cb4-27 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    &lt;span class=co&gt;// Parse invocation body for PDF URL.&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-28&gt;&lt;a href=#cb4-28 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    &lt;span class=kw&gt;var&lt;/span&gt; decoded message&lt;/span&gt;&#xA;&lt;span id=cb4-29&gt;&lt;a href=#cb4-29 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    &lt;span class=cf&gt;if&lt;/span&gt; err &lt;span class=op&gt;:=&lt;/span&gt; json&lt;span class=op&gt;.&lt;/span&gt;NewDecoder&lt;span class=op&gt;(&lt;/span&gt;r&lt;span class=op&gt;.&lt;/span&gt;Body&lt;span class=op&gt;).&lt;/span&gt;Decode&lt;span class=op&gt;(&amp;amp;&lt;/span&gt;decoded&lt;span class=op&gt;);&lt;/span&gt; err &lt;span class=op&gt;!=&lt;/span&gt; &lt;span class=ot&gt;nil&lt;/span&gt; &lt;span class=op&gt;{&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-30&gt;&lt;a href=#cb4-30 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;        http&lt;span class=op&gt;.&lt;/span&gt;Error&lt;span class=op&gt;(&lt;/span&gt;w&lt;span class=op&gt;,&lt;/span&gt; &lt;span class=st&gt;&amp;quot;Failed to decode message&amp;quot;&lt;/span&gt;&lt;span class=op&gt;,&lt;/span&gt; &lt;span class=dv&gt;400&lt;/span&gt;&lt;span class=op&gt;)&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-31&gt;&lt;a href=#cb4-31 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;        &lt;span class=cf&gt;return&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-32&gt;&lt;a href=#cb4-32 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    &lt;span class=op&gt;}&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-33&gt;&lt;a href=#cb4-33 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    filename&lt;span class=op&gt;,&lt;/span&gt; url&lt;span class=op&gt;,&lt;/span&gt; err &lt;span class=op&gt;:=&lt;/span&gt; parseURL&lt;span class=op&gt;(&lt;/span&gt;decoded&lt;span class=op&gt;.&lt;/span&gt;URL&lt;span class=op&gt;)&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-34&gt;&lt;a href=#cb4-34 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    &lt;span class=cf&gt;if&lt;/span&gt; err &lt;span class=op&gt;!=&lt;/span&gt; &lt;span class=ot&gt;nil&lt;/span&gt; &lt;span class=op&gt;{&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-35&gt;&lt;a href=#cb4-35 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;        http&lt;span class=op&gt;.&lt;/span&gt;Error&lt;span class=op&gt;(&lt;/span&gt;w&lt;span class=op&gt;,&lt;/span&gt; &lt;span class=st&gt;&amp;quot;Failed to parse URL&amp;quot;&lt;/span&gt;&lt;span class=op&gt;,&lt;/span&gt; &lt;span class=dv&gt;400&lt;/span&gt;&lt;span class=op&gt;)&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-36&gt;&lt;a href=#cb4-36 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;        &lt;span class=cf&gt;return&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-37&gt;&lt;a href=#cb4-37 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    &lt;span class=op&gt;}&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-38&gt;&lt;a href=#cb4-38 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-39&gt;&lt;a href=#cb4-39 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    resp&lt;span class=op&gt;,&lt;/span&gt; err &lt;span class=op&gt;:=&lt;/span&gt; http&lt;span class=op&gt;.&lt;/span&gt;Get&lt;span class=op&gt;(&lt;/span&gt;url&lt;span class=op&gt;.&lt;/span&gt;String&lt;span class=op&gt;())&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-40&gt;&lt;a href=#cb4-40 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    &lt;span class=cf&gt;if&lt;/span&gt; err &lt;span class=op&gt;!=&lt;/span&gt; &lt;span class=ot&gt;nil&lt;/span&gt; &lt;span class=op&gt;{&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-41&gt;&lt;a href=#cb4-41 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;        http&lt;span class=op&gt;.&lt;/span&gt;Error&lt;span class=op&gt;(&lt;/span&gt;w&lt;span class=op&gt;,&lt;/span&gt; &lt;span class=st&gt;&amp;quot;Failed to request URL&amp;quot;&lt;/span&gt;&lt;span class=op&gt;,&lt;/span&gt; &lt;span class=dv&gt;404&lt;/span&gt;&lt;span class=op&gt;)&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-42&gt;&lt;a href=#cb4-42 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;        &lt;span class=cf&gt;return&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-43&gt;&lt;a href=#cb4-43 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    &lt;span class=op&gt;}&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-44&gt;&lt;a href=#cb4-44 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    &lt;span class=cf&gt;defer&lt;/span&gt; resp&lt;span class=op&gt;.&lt;/span&gt;Body&lt;span class=op&gt;.&lt;/span&gt;Close&lt;span class=op&gt;()&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-45&gt;&lt;a href=#cb4-45 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    &lt;span class=cf&gt;if&lt;/span&gt; resp&lt;span class=op&gt;.&lt;/span&gt;Header&lt;span class=op&gt;.&lt;/span&gt;Get&lt;span class=op&gt;(&lt;/span&gt;&lt;span class=st&gt;&amp;quot;Content-Type&amp;quot;&lt;/span&gt;&lt;span class=op&gt;)&lt;/span&gt; &lt;span class=op&gt;!=&lt;/span&gt; &lt;span class=st&gt;&amp;quot;application/pdf&amp;quot;&lt;/span&gt; &lt;span class=op&gt;{&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-46&gt;&lt;a href=#cb4-46 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;        http&lt;span class=op&gt;.&lt;/span&gt;Error&lt;span class=op&gt;(&lt;/span&gt;w&lt;span class=op&gt;,&lt;/span&gt; &lt;span class=st&gt;&amp;quot;Not a PDF&amp;quot;&lt;/span&gt;&lt;span class=op&gt;,&lt;/span&gt; &lt;span class=dv&gt;404&lt;/span&gt;&lt;span class=op&gt;)&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-47&gt;&lt;a href=#cb4-47 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;        &lt;span class=cf&gt;return&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-48&gt;&lt;a href=#cb4-48 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    &lt;span class=op&gt;}&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-49&gt;&lt;a href=#cb4-49 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-50&gt;&lt;a href=#cb4-50 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    &lt;span class=co&gt;// Create a storage client.&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-51&gt;&lt;a href=#cb4-51 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    ctx &lt;span class=op&gt;:=&lt;/span&gt; context&lt;span class=op&gt;.&lt;/span&gt;Background&lt;span class=op&gt;()&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-52&gt;&lt;a href=#cb4-52 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    storageClient&lt;span class=op&gt;,&lt;/span&gt; err &lt;span class=op&gt;:=&lt;/span&gt; storage&lt;span class=op&gt;.&lt;/span&gt;NewClient&lt;span class=op&gt;(&lt;/span&gt;ctx&lt;span class=op&gt;)&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-53&gt;&lt;a href=#cb4-53 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    &lt;span class=cf&gt;if&lt;/span&gt; err &lt;span class=op&gt;!=&lt;/span&gt; &lt;span class=ot&gt;nil&lt;/span&gt; &lt;span class=op&gt;{&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-54&gt;&lt;a href=#cb4-54 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;        http&lt;span class=op&gt;.&lt;/span&gt;Error&lt;span class=op&gt;(&lt;/span&gt;w&lt;span class=op&gt;,&lt;/span&gt; &lt;span class=st&gt;&amp;quot;Failed to create storage client.&amp;quot;&lt;/span&gt;&lt;span class=op&gt;,&lt;/span&gt; &lt;span class=dv&gt;500&lt;/span&gt;&lt;span class=op&gt;)&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-55&gt;&lt;a href=#cb4-55 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;        &lt;span class=cf&gt;return&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-56&gt;&lt;a href=#cb4-56 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    &lt;span class=op&gt;}&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-57&gt;&lt;a href=#cb4-57 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    &lt;span class=cf&gt;defer&lt;/span&gt; storageClient&lt;span class=op&gt;.&lt;/span&gt;Close&lt;span class=op&gt;()&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-58&gt;&lt;a href=#cb4-58 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    &lt;span class=co&gt;// Initialize a storage object by creating a writer for it.&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-59&gt;&lt;a href=#cb4-59 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    writer &lt;span class=op&gt;:=&lt;/span&gt; storageClient&lt;span class=op&gt;.&lt;/span&gt;Bucket&lt;span class=op&gt;(&lt;/span&gt;bucketName&lt;span class=op&gt;).&lt;/span&gt;Object&lt;span class=op&gt;(&lt;/span&gt;filename&lt;span class=op&gt;).&lt;/span&gt;NewWriter&lt;span class=op&gt;(&lt;/span&gt;ctx&lt;span class=op&gt;)&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-60&gt;&lt;a href=#cb4-60 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    &lt;span class=cf&gt;defer&lt;/span&gt; writer&lt;span class=op&gt;.&lt;/span&gt;Close&lt;span class=op&gt;()&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-61&gt;&lt;a href=#cb4-61 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-62&gt;&lt;a href=#cb4-62 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    &lt;span class=co&gt;// Return a 200 status.&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-63&gt;&lt;a href=#cb4-63 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    w&lt;span class=op&gt;.&lt;/span&gt;WriteHeader&lt;span class=op&gt;(&lt;/span&gt;http&lt;span class=op&gt;.&lt;/span&gt;StatusAccepted&lt;span class=op&gt;)&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-64&gt;&lt;a href=#cb4-64 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    &lt;span class=co&gt;// Pipe PDF body to Cloud Storage.&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-65&gt;&lt;a href=#cb4-65 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    writer&lt;span class=op&gt;.&lt;/span&gt;ContentType &lt;span class=op&gt;=&lt;/span&gt; resp&lt;span class=op&gt;.&lt;/span&gt;Header&lt;span class=op&gt;.&lt;/span&gt;Get&lt;span class=op&gt;(&lt;/span&gt;&lt;span class=st&gt;&amp;quot;Content-Type&amp;quot;&lt;/span&gt;&lt;span class=op&gt;)&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-66&gt;&lt;a href=#cb4-66 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    written&lt;span class=op&gt;,&lt;/span&gt; err &lt;span class=op&gt;:=&lt;/span&gt; io&lt;span class=op&gt;.&lt;/span&gt;Copy&lt;span class=op&gt;(&lt;/span&gt;writer&lt;span class=op&gt;,&lt;/span&gt; resp&lt;span class=op&gt;.&lt;/span&gt;Body&lt;span class=op&gt;)&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-67&gt;&lt;a href=#cb4-67 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    &lt;span class=cf&gt;if&lt;/span&gt; err &lt;span class=op&gt;!=&lt;/span&gt; &lt;span class=ot&gt;nil&lt;/span&gt; &lt;span class=op&gt;{&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-68&gt;&lt;a href=#cb4-68 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;        fmt&lt;span class=op&gt;.&lt;/span&gt;Println&lt;span class=op&gt;(&lt;/span&gt;&lt;span class=st&gt;&amp;quot;Error encountered when piping to Cloud Storage&amp;quot;&lt;/span&gt;&lt;span class=op&gt;,&lt;/span&gt; err&lt;span class=op&gt;)&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-69&gt;&lt;a href=#cb4-69 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    &lt;span class=op&gt;}&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-70&gt;&lt;a href=#cb4-70 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    fmt&lt;span class=op&gt;.&lt;/span&gt;Printf&lt;span class=op&gt;(&lt;/span&gt;&lt;span class=st&gt;&amp;quot;Wrote %d to Cloud Storage&lt;/span&gt;&lt;span class=ch&gt;\n&lt;/span&gt;&lt;span class=st&gt;&amp;quot;&lt;/span&gt;&lt;span class=op&gt;,&lt;/span&gt; written&lt;span class=op&gt;)&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-71&gt;&lt;a href=#cb4-71 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=op&gt;}&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-72&gt;&lt;a href=#cb4-72 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-73&gt;&lt;a href=#cb4-73 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=co&gt;// parseURL parses a URL. The returned filename is the URL host and URL path,&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-74&gt;&lt;a href=#cb4-74 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=co&gt;// concatenated. GCS treats the `/` tokens as folder separators.&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-75&gt;&lt;a href=#cb4-75 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=kw&gt;func&lt;/span&gt; parseURL&lt;span class=op&gt;(&lt;/span&gt;input &lt;span class=dt&gt;string&lt;/span&gt;&lt;span class=op&gt;)&lt;/span&gt; &lt;span class=op&gt;(&lt;/span&gt;filename &lt;span class=dt&gt;string&lt;/span&gt;&lt;span class=op&gt;,&lt;/span&gt; url &lt;span class=op&gt;*&lt;/span&gt;url&lt;span class=op&gt;.&lt;/span&gt;URL&lt;span class=op&gt;,&lt;/span&gt; err &lt;span class=dt&gt;error&lt;/span&gt;&lt;span class=op&gt;)&lt;/span&gt; &lt;span class=op&gt;{&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-76&gt;&lt;a href=#cb4-76 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    url&lt;span class=op&gt;,&lt;/span&gt; err &lt;span class=op&gt;=&lt;/span&gt; url&lt;span class=op&gt;.&lt;/span&gt;Parse&lt;span class=op&gt;(&lt;/span&gt;input&lt;span class=op&gt;)&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-77&gt;&lt;a href=#cb4-77 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    filename &lt;span class=op&gt;=&lt;/span&gt; fmt&lt;span class=op&gt;.&lt;/span&gt;Sprintf&lt;span class=op&gt;(&lt;/span&gt;&lt;span class=st&gt;&amp;quot;%s%s&amp;quot;&lt;/span&gt;&lt;span class=op&gt;,&lt;/span&gt; url&lt;span class=op&gt;.&lt;/span&gt;Host&lt;span class=op&gt;,&lt;/span&gt; url&lt;span class=op&gt;.&lt;/span&gt;Path&lt;span class=op&gt;)&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb4-78&gt;&lt;a href=#cb4-78 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    &lt;span class=cf&gt;return&lt;/span&gt; filename&lt;span class=op&gt;,&lt;/span&gt; url&lt;span class=op&gt;,&lt;/span&gt; err&lt;/span&gt;&#xA;&lt;span id=cb4-79&gt;&lt;a href=#cb4-79 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=op&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Less than a hundred lines, and not a buffer in sight! That&#xA;&lt;code&gt;io.Copy()&lt;/code&gt; call––from the HTTP response to the newly&#xA;initialized GCS object––is doing the bulk of the work. Almost everything&#xA;else is error handling.&lt;a href=#fn3 class=footnote-ref id=fnref3 role=doc-noteref&gt;&lt;sup&gt;3&lt;/sup&gt;&lt;/a&gt; There’s pleasantly little&#xA;tool-specific boilerplate, so this could be refactored into an HTTP&#xA;handler function on a webserver if we saw fit.&lt;p&gt;Once this Cloud Function’s deployed, we can invoke it in a fairly&#xA;tight loop: additional Cloud Function instances are automatically&#xA;provisioned to manage the load. I wrote a short &lt;code&gt;aiohttp&lt;/code&gt;&#xA;Python script for this. Uploading 20 GB of PDFs––hundreds of PDFs at a&#xA;time––takes just minutes. Conveniently, the object names are their&#xA;URLs.&lt;h1 id=extracting-text&gt;Extracting text&lt;/h1&gt;&lt;p&gt;Extracting text from PDFs is &lt;em&gt;hard&lt;/em&gt;. PDFs are essentially&#xA;visual documents; they’re meant to be read visually rather than parsed&#xA;programmatically. There are, broadly, two ways of turning them into&#xA;plain text:&lt;ul&gt;&lt;li&gt;&lt;p&gt;Optical Character Recognition (OCR) programs like &lt;a href=&#34;https://guides.library.illinois.edu/c.php?g=347520&amp;amp;p=4121426&#34;&gt;Tesseract&lt;/a&gt;&#xA;trade off efficiency and ease of configuration to play by this&#xA;expectation: they extract text by considering a PDF &lt;em&gt;visually.&lt;/em&gt;&#xA;With intense computational requirements and long runtimes, OCR programs&#xA;suit persistent compute resources.&lt;li&gt;&lt;p&gt;&lt;em&gt;Some&lt;/em&gt; PDFs (the ones wherein you can highlight text,&#xA;copy/paste, etc.) have text embedded in them; we can use programs to&#xA;extract that text dirextly. Unfortunately, this just doesn’t work if a&#xA;PDF doesn’t include encoded text.&lt;/ul&gt;&lt;p&gt;OCR is overkill for our PDF-indexing use case; extracting a bag of&#xA;words will do just fine, and there’s such a wealth of PDFs on academic&#xA;homepages that we can satisfy ourselves with indexing &lt;em&gt;most&lt;/em&gt; of&#xA;them. That’s not to say this text extraction is a simple thing to build&#xA;yourself, or even easy to solve with libraries in a variety of&#xA;languages: I struggled for hours with a &lt;a href=https://github.com/ledongthuc/pdf&gt;Go module&lt;/a&gt; before giving up&#xA;and switching to Python.&lt;p&gt;This is a sweet feature of a radically modular infrastructure: for a&#xA;given stage of our data pipeline, we’re free to pick the language with&#xA;the best support. When we need to pull text from PDFs, we can pick the&#xA;language with the most effective published tools (like we picked a&#xA;language that suited our ingestion strategy). Python has plenty; &lt;a href=https://github.com/pdfminer/pdfminer.six&gt;pdfminer.six&lt;/a&gt; is&#xA;decent. In a cloud function:&lt;div class=sourceCode id=cb5&gt;&lt;pre class=&#34;sourceCode python&#34;&gt;&lt;code class=&#34;sourceCode python&#34;&gt;&lt;span id=cb5-1&gt;&lt;a href=#cb5-1 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=im&gt;from&lt;/span&gt; google.cloud &lt;span class=im&gt;import&lt;/span&gt; storage&lt;/span&gt;&#xA;&lt;span id=cb5-2&gt;&lt;a href=#cb5-2 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=im&gt;import&lt;/span&gt; tempfile&lt;/span&gt;&#xA;&lt;span id=cb5-3&gt;&lt;a href=#cb5-3 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=im&gt;from&lt;/span&gt; pdfminer.high_level &lt;span class=im&gt;import&lt;/span&gt; extract_text&lt;/span&gt;&#xA;&lt;span id=cb5-4&gt;&lt;a href=#cb5-4 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=im&gt;from&lt;/span&gt; flask &lt;span class=im&gt;import&lt;/span&gt; abort&lt;/span&gt;&#xA;&lt;span id=cb5-5&gt;&lt;a href=#cb5-5 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;/span&gt;&#xA;&lt;span id=cb5-6&gt;&lt;a href=#cb5-6 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;/span&gt;&#xA;&lt;span id=cb5-7&gt;&lt;a href=#cb5-7 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=kw&gt;def&lt;/span&gt; main(request):&lt;/span&gt;&#xA;&lt;span id=cb5-8&gt;&lt;a href=#cb5-8 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    &lt;span class=co&gt;&amp;quot;&amp;quot;&amp;quot;main is theCloud Function entry point. It downloads the specified&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb5-9&gt;&lt;a href=#cb5-9 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=co&gt;    existing PDF from GCS to a temporary location, extracts the text from that&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb5-10&gt;&lt;a href=#cb5-10 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=co&gt;    PDF, then uploads that text to a new GCS object.&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb5-11&gt;&lt;a href=#cb5-11 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;/span&gt;&#xA;&lt;span id=cb5-12&gt;&lt;a href=#cb5-12 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=co&gt;    request -- the Flask request that invoked this Cloud Function.&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb5-13&gt;&lt;a href=#cb5-13 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=co&gt;    &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb5-14&gt;&lt;a href=#cb5-14 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    &lt;span class=co&gt;# Parse object name from request.&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb5-15&gt;&lt;a href=#cb5-15 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    request_json &lt;span class=op&gt;=&lt;/span&gt; request.get_json()&lt;/span&gt;&#xA;&lt;span id=cb5-16&gt;&lt;a href=#cb5-16 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    objectName &lt;span class=op&gt;=&lt;/span&gt; request_json[&lt;span class=st&gt;&amp;#39;object&amp;#39;&lt;/span&gt;]&lt;/span&gt;&#xA;&lt;span id=cb5-17&gt;&lt;a href=#cb5-17 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    &lt;span class=cf&gt;assert&lt;/span&gt; objectName.endswith(&lt;span class=st&gt;&amp;quot;.pdf&amp;quot;&lt;/span&gt;)&lt;/span&gt;&#xA;&lt;span id=cb5-18&gt;&lt;a href=#cb5-18 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    &lt;span class=bu&gt;print&lt;/span&gt;(&lt;span class=st&gt;&amp;quot;Got request to handle&amp;quot;&lt;/span&gt;, objectName)&lt;/span&gt;&#xA;&lt;span id=cb5-19&gt;&lt;a href=#cb5-19 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    &lt;span class=co&gt;# Connect to GCS bucket.&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb5-20&gt;&lt;a href=#cb5-20 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    client &lt;span class=op&gt;=&lt;/span&gt; storage.Client()&lt;/span&gt;&#xA;&lt;span id=cb5-21&gt;&lt;a href=#cb5-21 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    bucket &lt;span class=op&gt;=&lt;/span&gt; client.bucket(&lt;span class=st&gt;&amp;quot;documents-repository&amp;quot;&lt;/span&gt;)&lt;/span&gt;&#xA;&lt;span id=cb5-22&gt;&lt;a href=#cb5-22 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    &lt;span class=co&gt;# Initialize temporary file for downloaded PDF.&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb5-23&gt;&lt;a href=#cb5-23 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    pdf &lt;span class=op&gt;=&lt;/span&gt; tempfile.NamedTemporaryFile()&lt;/span&gt;&#xA;&lt;span id=cb5-24&gt;&lt;a href=#cb5-24 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    &lt;span class=cf&gt;try&lt;/span&gt;:&lt;/span&gt;&#xA;&lt;span id=cb5-25&gt;&lt;a href=#cb5-25 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;        &lt;span class=co&gt;# Download blob into temporary file, extract, and uplaod.&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb5-26&gt;&lt;a href=#cb5-26 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;        bucket.blob(objectName).download_to_filename(pdf.name)&lt;/span&gt;&#xA;&lt;span id=cb5-27&gt;&lt;a href=#cb5-27 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;        &lt;span class=cf&gt;return&lt;/span&gt; extract(bucket, objectName, pdf.name)&lt;/span&gt;&#xA;&lt;span id=cb5-28&gt;&lt;a href=#cb5-28 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    &lt;span class=cf&gt;except&lt;/span&gt; &lt;span class=pp&gt;Exception&lt;/span&gt; &lt;span class=im&gt;as&lt;/span&gt; err:&lt;/span&gt;&#xA;&lt;span id=cb5-29&gt;&lt;a href=#cb5-29 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;        &lt;span class=cf&gt;return&lt;/span&gt; abort(&lt;span class=dv&gt;500&lt;/span&gt;, &lt;span class=st&gt;&amp;quot;Exception while extracting text&amp;quot;&lt;/span&gt;, err)&lt;/span&gt;&#xA;&lt;span id=cb5-30&gt;&lt;a href=#cb5-30 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;/span&gt;&#xA;&lt;span id=cb5-31&gt;&lt;a href=#cb5-31 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;/span&gt;&#xA;&lt;span id=cb5-32&gt;&lt;a href=#cb5-32 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=kw&gt;def&lt;/span&gt; extract(bucket: storage.Bucket, objectName: &lt;span class=bu&gt;str&lt;/span&gt;, pdf: &lt;span class=bu&gt;str&lt;/span&gt;):&lt;/span&gt;&#xA;&lt;span id=cb5-33&gt;&lt;a href=#cb5-33 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    &lt;span class=co&gt;&amp;quot;&amp;quot;&amp;quot;extract pulls text out of a downloaded PDF and uploads the result to a&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb5-34&gt;&lt;a href=#cb5-34 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=co&gt;    new GCS object in the same bucket.&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb5-35&gt;&lt;a href=#cb5-35 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;/span&gt;&#xA;&lt;span id=cb5-36&gt;&lt;a href=#cb5-36 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=co&gt;    bucket -- the GCS bucket to which the resulting text will be uploaded.&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb5-37&gt;&lt;a href=#cb5-37 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=co&gt;    objectName -- the prefix for the uploaded text object. Usually this is the&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb5-38&gt;&lt;a href=#cb5-38 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=co&gt;        object name for the processed PDF.&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb5-39&gt;&lt;a href=#cb5-39 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=co&gt;    pdf -- the filename for a downloaded PDF from which to extract text.&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb5-40&gt;&lt;a href=#cb5-40 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=co&gt;    &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb5-41&gt;&lt;a href=#cb5-41 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    &lt;span class=co&gt;# &lt;/span&gt;&lt;span class=al&gt;TODO&lt;/span&gt;&lt;span class=co&gt;: silence pdfminer noisy logging.&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb5-42&gt;&lt;a href=#cb5-42 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    text &lt;span class=op&gt;=&lt;/span&gt; extract_text(pdf)&lt;/span&gt;&#xA;&lt;span id=cb5-43&gt;&lt;a href=#cb5-43 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    &lt;span class=co&gt;# Upload extracted text to new GCS object.&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb5-44&gt;&lt;a href=#cb5-44 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    dest_blob &lt;span class=op&gt;=&lt;/span&gt; bucket.blob(objectName &lt;span class=op&gt;+&lt;/span&gt; &lt;span class=st&gt;&amp;quot;.txt&amp;quot;&lt;/span&gt;)&lt;/span&gt;&#xA;&lt;span id=cb5-45&gt;&lt;a href=#cb5-45 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    dest_blob.upload_from_string(text)&lt;/span&gt;&#xA;&lt;span id=cb5-46&gt;&lt;a href=#cb5-46 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    &lt;span class=cf&gt;return&lt;/span&gt; text&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Downloading a PDF to a temporary file is clumsy, but the published&#xA;tools expect filenames (and, as I said, they’re complex enough to&#xA;dictate my implementation). Like the upload Cloud Function before it, we&#xA;can invoke this one in a tight loop using the scraped URLs from before…&#xA;et voilà! After a few minutes, the &lt;code&gt;pdfminer&lt;/code&gt; output for each&#xA;stored PDF is tucked next to its corresponding PDF in the bucket.&lt;h2 id=listening-for-storage-events&gt;Listening for storage events&lt;/h2&gt;&lt;p&gt;This extraction strategy works, but it involves manually triggerig&#xA;both stages: first, we trigger PDF ingestion; second, after the upload&#xA;is finished, we separately trigger our text extraction function.&#xA;Instead, we can &lt;em&gt;just&lt;/em&gt; trigger the ingestion, and have the “after&#xA;the upload is completed” event trigger our Function&#xA;automatically/immediately.&lt;p&gt;&lt;a href=https://cloud.google.com/functions/docs/calling/storage&gt;Google&#xA;Cloud Storage Triggers&lt;/a&gt; are a neat Pub/Sub interface for invoking&#xA;Cloud Functions. Instead of manually announcing “hey, I uploaded this&#xA;object, it’s ready for processing,” the Cloud Function can consume the&#xA;GCS-published &lt;code&gt;finalize&lt;/code&gt; event marking the Storage object&#xA;creations and updates. Refactoring our code from before:&lt;div class=sourceCode id=cb6&gt;&lt;pre class=&#34;sourceCode python&#34;&gt;&lt;code class=&#34;sourceCode python&#34;&gt;&lt;span id=cb6-1&gt;&lt;a href=#cb6-1 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=kw&gt;def&lt;/span&gt; on_finalized(event, _):&lt;/span&gt;&#xA;&lt;span id=cb6-2&gt;&lt;a href=#cb6-2 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    &lt;span class=co&gt;&amp;quot;&amp;quot;&amp;quot;on_finalized is the Cloud Function entry point for handling GCS object&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb6-3&gt;&lt;a href=#cb6-3 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=co&gt;    finalized events. It downloads the specified PDF from GCS into a temporary&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb6-4&gt;&lt;a href=#cb6-4 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=co&gt;    file, extracts the text from that PDF, then uploads that text to a new GCS&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb6-5&gt;&lt;a href=#cb6-5 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=co&gt;    object.&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb6-6&gt;&lt;a href=#cb6-6 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;/span&gt;&#xA;&lt;span id=cb6-7&gt;&lt;a href=#cb6-7 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=co&gt;    event -- the received GCS event. Includes the bucket name and the name of&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb6-8&gt;&lt;a href=#cb6-8 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=co&gt;        the finalized object.&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb6-9&gt;&lt;a href=#cb6-9 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=co&gt;    &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb6-10&gt;&lt;a href=#cb6-10 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    bucket &lt;span class=op&gt;=&lt;/span&gt; event[&lt;span class=st&gt;&amp;#39;bucket&amp;#39;&lt;/span&gt;]&lt;/span&gt;&#xA;&lt;span id=cb6-11&gt;&lt;a href=#cb6-11 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    objectName &lt;span class=op&gt;=&lt;/span&gt; event[&lt;span class=st&gt;&amp;#39;name&amp;#39;&lt;/span&gt;]&lt;/span&gt;&#xA;&lt;span id=cb6-12&gt;&lt;a href=#cb6-12 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    &lt;span class=co&gt;# Skip non-PDF files: this function writes to the bucket it watches.&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb6-13&gt;&lt;a href=#cb6-13 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    &lt;span class=cf&gt;if&lt;/span&gt; &lt;span class=kw&gt;not&lt;/span&gt; objectName.endswith(&lt;span class=st&gt;&amp;quot;.pdf&amp;quot;&lt;/span&gt;):&lt;/span&gt;&#xA;&lt;span id=cb6-14&gt;&lt;a href=#cb6-14 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;        &lt;span class=bu&gt;print&lt;/span&gt;(&lt;span class=st&gt;&amp;quot;Skipping request to handle&amp;quot;&lt;/span&gt;, objectName)&lt;/span&gt;&#xA;&lt;span id=cb6-15&gt;&lt;a href=#cb6-15 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;        &lt;span class=cf&gt;return&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb6-16&gt;&lt;a href=#cb6-16 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;/span&gt;&#xA;&lt;span id=cb6-17&gt;&lt;a href=#cb6-17 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    &lt;span class=bu&gt;print&lt;/span&gt;(&lt;span class=st&gt;&amp;quot;Extracting text from&amp;quot;&lt;/span&gt;, objectName)&lt;/span&gt;&#xA;&lt;span id=cb6-18&gt;&lt;a href=#cb6-18 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    &lt;span class=co&gt;# Connect to GCS bucket.&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb6-19&gt;&lt;a href=#cb6-19 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    client &lt;span class=op&gt;=&lt;/span&gt; storage.Client()&lt;/span&gt;&#xA;&lt;span id=cb6-20&gt;&lt;a href=#cb6-20 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    bucket &lt;span class=op&gt;=&lt;/span&gt; client.bucket(bucket)&lt;/span&gt;&#xA;&lt;span id=cb6-21&gt;&lt;a href=#cb6-21 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    &lt;span class=co&gt;# Initialize temporary file for downloaded PDF.&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb6-22&gt;&lt;a href=#cb6-22 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    pdf &lt;span class=op&gt;=&lt;/span&gt; tempfile.NamedTemporaryFile()&lt;/span&gt;&#xA;&lt;span id=cb6-23&gt;&lt;a href=#cb6-23 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    &lt;span class=cf&gt;try&lt;/span&gt;:&lt;/span&gt;&#xA;&lt;span id=cb6-24&gt;&lt;a href=#cb6-24 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;        &lt;span class=co&gt;# Download blob into temporary file, extract, and uplaod.&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb6-25&gt;&lt;a href=#cb6-25 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;        bucket.blob(objectName).download_to_filename(pdf.name)&lt;/span&gt;&#xA;&lt;span id=cb6-26&gt;&lt;a href=#cb6-26 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;        extracted &lt;span class=op&gt;=&lt;/span&gt; extract(bucket, objectName, pdf.name)&lt;/span&gt;&#xA;&lt;span id=cb6-27&gt;&lt;a href=#cb6-27 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;        &lt;span class=bu&gt;print&lt;/span&gt;(&lt;span class=st&gt;&amp;quot;Success: extracted &lt;/span&gt;&lt;span class=sc&gt;{}&lt;/span&gt;&lt;span class=st&gt; characters&amp;quot;&lt;/span&gt;.&lt;span class=bu&gt;format&lt;/span&gt;(&lt;span class=bu&gt;len&lt;/span&gt;(extracted)))&lt;/span&gt;&#xA;&lt;span id=cb6-28&gt;&lt;a href=#cb6-28 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    &lt;span class=cf&gt;except&lt;/span&gt; &lt;span class=pp&gt;Exception&lt;/span&gt; &lt;span class=im&gt;as&lt;/span&gt; err:&lt;/span&gt;&#xA;&lt;span id=cb6-29&gt;&lt;a href=#cb6-29 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;        &lt;span class=bu&gt;print&lt;/span&gt;(&lt;span class=st&gt;&amp;quot;Exception while extracting text&amp;quot;&lt;/span&gt;, err)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;extract&lt;/code&gt; function––and, indeed, everything but the&#xA;code to pull the &lt;code&gt;objectName&lt;/code&gt; from the Pub/Sub message––is&#xA;unchanged. With this update, there’s no need to invoke anything but the&#xA;ingestion function: once a PDF has been streamed into GCS object, the&#xA;text extraction will automatically kick off; we don’t need any second&#xA;pass over scraped URLs.&lt;p&gt;We can daisy-chain this further by writing Cloud Functions invoked by&#xA;&lt;code&gt;finalize&lt;/code&gt; events on .txt objects, or publish events to&#xA;Pub/Sub topics linking successive pipeline stages. TFIDF? Elasticsearch?&#xA;Write the next stage in Typescript? The sky’s the limit.&lt;aside id=footnotes class=&#34;footnotes footnotes-end-of-document&#34; role=doc-endnotes&gt;&lt;hr&gt;&lt;ol&gt;&lt;li id=fn1&gt;&lt;p&gt;I skimmed most of the PDFs listed on Dr. Leveson’s site.&#xA;If you’re interested in software-system safety analysis, my personal&#xA;favorites were&lt;ul&gt;&lt;li&gt;&lt;p&gt;“Systems Theoretic Process Analysis (STPA) of an Offshore Supply&#xA;Vessel Dynamic Positioning System.” I skipped a lot of the&#xA;topic-specific detail, but its STPA overview is one of the best I read&#xA;and its example (maintaining the relative positions of two ships without&#xA;running either aground) is memorable.&lt;li&gt;&lt;p&gt;“Software Deviation Analyis: A ‘Safeware’ Technique.” Discusses&#xA;modeling a software system and, well, kind of &lt;a href=https://netflix.github.io/chaosmonkey/&gt;Chaos Monkey&lt;/a&gt;ing it:&#xA;see what happens when different combinations of the software’s controls&#xA;are violated.&lt;li&gt;&lt;p&gt;“Inside Risks: An Integrated Approach to Safety and Security&#xA;Based on Systems Theory.” A strong case for cross-applying principles&#xA;from system safety (Leveson’s primary focus) into information security&#xA;(my primary focus these days).&lt;/ul&gt;&lt;p&gt;Leveson doesn’t index them by title, but they’re in there!&lt;a href=#fnref1 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn2&gt;&lt;p&gt;The scraper that produces the lists of PDF URLs is&#xA;pretty janky. It requires some tweaking for each academic department.&#xA;The abridged code:&lt;div class=sourceCode id=cb1&gt;&lt;pre class=&#34;sourceCode python&#34;&gt;&lt;code class=&#34;sourceCode python&#34;&gt;&lt;span id=cb1-1&gt;&lt;a href=#cb1-1 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=im&gt;import&lt;/span&gt; scrapy&lt;/span&gt;&#xA;&lt;span id=cb1-2&gt;&lt;a href=#cb1-2 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=im&gt;from&lt;/span&gt; scrapy.linkextractors &lt;span class=im&gt;import&lt;/span&gt; LinkExtractor&lt;/span&gt;&#xA;&lt;span id=cb1-3&gt;&lt;a href=#cb1-3 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;/span&gt;&#xA;&lt;span id=cb1-4&gt;&lt;a href=#cb1-4 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=kw&gt;class&lt;/span&gt; CustomLinkExtractor(LinkExtractor):&lt;/span&gt;&#xA;&lt;span id=cb1-5&gt;&lt;a href=#cb1-5 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;  &lt;span class=kw&gt;def&lt;/span&gt; &lt;span class=fu&gt;__init__&lt;/span&gt;(&lt;span class=va&gt;self&lt;/span&gt;, &lt;span class=op&gt;*&lt;/span&gt;args, &lt;span class=op&gt;**&lt;/span&gt;kwargs):&lt;/span&gt;&#xA;&lt;span id=cb1-6&gt;&lt;a href=#cb1-6 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;      &lt;span class=bu&gt;super&lt;/span&gt;(CustomLinkExtractor, &lt;span class=va&gt;self&lt;/span&gt;).&lt;span class=fu&gt;__init__&lt;/span&gt;(&lt;span class=op&gt;*&lt;/span&gt;args, &lt;span class=op&gt;**&lt;/span&gt;kwargs)&lt;/span&gt;&#xA;&lt;span id=cb1-7&gt;&lt;a href=#cb1-7 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;      &lt;span class=co&gt;# Keep the default values in &amp;quot;deny_extensions&amp;quot; *except* for PDFs.&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb1-8&gt;&lt;a href=#cb1-8 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;      &lt;span class=va&gt;self&lt;/span&gt;.deny_extensions &lt;span class=op&gt;=&lt;/span&gt; [ext &lt;span class=cf&gt;for&lt;/span&gt; ext &lt;span class=kw&gt;in&lt;/span&gt; &lt;span class=va&gt;self&lt;/span&gt;.deny_extensions &lt;span class=cf&gt;if&lt;/span&gt; ext &lt;span class=op&gt;!=&lt;/span&gt; &lt;span class=st&gt;&amp;quot;.pdf&amp;quot;&lt;/span&gt;]&lt;/span&gt;&#xA;&lt;span id=cb1-9&gt;&lt;a href=#cb1-9 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;/span&gt;&#xA;&lt;span id=cb1-10&gt;&lt;a href=#cb1-10 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;span class=kw&gt;class&lt;/span&gt; Spider(scrapy.Spider):&lt;/span&gt;&#xA;&lt;span id=cb1-11&gt;&lt;a href=#cb1-11 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;  name &lt;span class=op&gt;=&lt;/span&gt; &lt;span class=st&gt;&amp;quot;nmt-spider&amp;quot;&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb1-12&gt;&lt;a href=#cb1-12 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;  &lt;span class=co&gt;# allowed_domains limit the domains to be scraped rather than the PDF links&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb1-13&gt;&lt;a href=#cb1-13 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;  &lt;span class=co&gt;# to be extracted.&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb1-14&gt;&lt;a href=#cb1-14 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;  allowed_domains &lt;span class=op&gt;=&lt;/span&gt; [&lt;span class=st&gt;&amp;#39;department.domain.edu&amp;#39;&lt;/span&gt;, &lt;span class=st&gt;&amp;#39;personal.sites.domain.edu&amp;#39;&lt;/span&gt;]&lt;/span&gt;&#xA;&lt;span id=cb1-15&gt;&lt;a href=#cb1-15 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;/span&gt;&#xA;&lt;span id=cb1-16&gt;&lt;a href=#cb1-16 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;  &lt;span class=kw&gt;def&lt;/span&gt; start_requests(&lt;span class=va&gt;self&lt;/span&gt;):&lt;/span&gt;&#xA;&lt;span id=cb1-17&gt;&lt;a href=#cb1-17 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    &lt;span class=co&gt;# An iniial entry point; usually a faculty index.&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb1-18&gt;&lt;a href=#cb1-18 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    &lt;span class=cf&gt;yield&lt;/span&gt; scrapy.Request(url&lt;span class=op&gt;=&lt;/span&gt;&lt;span class=st&gt;&amp;quot;department.domain.edu/faculty&amp;quot;&lt;/span&gt;)&lt;/span&gt;&#xA;&lt;span id=cb1-19&gt;&lt;a href=#cb1-19 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;&lt;/span&gt;&#xA;&lt;span id=cb1-20&gt;&lt;a href=#cb1-20 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;  &lt;span class=kw&gt;def&lt;/span&gt; parse(&lt;span class=va&gt;self&lt;/span&gt;, response):&lt;/span&gt;&#xA;&lt;span id=cb1-21&gt;&lt;a href=#cb1-21 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    pdf_extractor &lt;span class=op&gt;=&lt;/span&gt; CustomLinkExtractor(allow&lt;span class=op&gt;=&lt;/span&gt;&lt;span class=vs&gt;r&amp;#39;.*\.pdf$&amp;#39;&lt;/span&gt;)&lt;/span&gt;&#xA;&lt;span id=cb1-22&gt;&lt;a href=#cb1-22 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    &lt;span class=cf&gt;with&lt;/span&gt; &lt;span class=bu&gt;open&lt;/span&gt;(&lt;span class=st&gt;&amp;quot;jots.txt&amp;quot;&lt;/span&gt;, &lt;span class=st&gt;&amp;quot;a&amp;quot;&lt;/span&gt;) &lt;span class=im&gt;as&lt;/span&gt; f:&lt;/span&gt;&#xA;&lt;span id=cb1-23&gt;&lt;a href=#cb1-23 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;      &lt;span class=cf&gt;for&lt;/span&gt; pdf_link &lt;span class=kw&gt;in&lt;/span&gt; pdf_extractor.extract_links(response):&lt;/span&gt;&#xA;&lt;span id=cb1-24&gt;&lt;a href=#cb1-24 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;        f.write(url &lt;span class=op&gt;+&lt;/span&gt; &lt;span class=st&gt;&amp;quot;&lt;/span&gt;&lt;span class=ch&gt;\n&lt;/span&gt;&lt;span class=st&gt;&amp;quot;&lt;/span&gt;)&lt;/span&gt;&#xA;&lt;span id=cb1-25&gt;&lt;a href=#cb1-25 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;    &lt;span class=cf&gt;for&lt;/span&gt; link &lt;span class=kw&gt;in&lt;/span&gt; LinkExtractor.extract_links(response):&lt;/span&gt;&#xA;&lt;span id=cb1-26&gt;&lt;a href=#cb1-26 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;      &lt;span class=co&gt;# ...filter out problematic links here before yielding.&lt;/span&gt;&lt;/span&gt;&#xA;&lt;span id=cb1-27&gt;&lt;a href=#cb1-27 aria-hidden=true tabindex=-1&gt;&lt;/a&gt;      &lt;span class=cf&gt;yield&lt;/span&gt; response.follow(link, callback&lt;span class=op&gt;=&lt;/span&gt;&lt;span class=va&gt;self&lt;/span&gt;.parse)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Running &lt;code&gt;scrapy runspider scrape.py&lt;/code&gt; yields a file&#xA;&lt;code&gt;jots.txt&lt;/code&gt; full of PDF URLs.&lt;a href=#fnref2 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn3&gt;&lt;p&gt;I made a conscious effort to stick to &lt;a href=https://github.com/lojikil/kyoto-go-nihilism/blob/master/go-nihilism.md&gt;“The&#xA;Kyoto School of Go Nihilism”&lt;/a&gt;––every &lt;code&gt;err&lt;/code&gt; checked, every&#xA;&lt;code&gt;defer&lt;/code&gt;red function executed.&lt;a href=#fnref3 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;/ol&gt;&lt;/aside&gt;</content>
    <link href="https://lukasschwab.me/blog/gen/cloud-function-pdf-processing.html" rel="alternate"></link>
    <summary type="html">A personal project of mine requires storing and processing tens of gigabytes of scraped files. I discuss using polyglot Cloud Functions as a rudimentary data pipeline: first, a function for highly parallel file uploads; second, one for PDF text extraction.</summary>
  </entry>
  <entry>
    <title>Web App Product Ontologies</title>
    <updated>2020-08-22T00:00:00Z</updated>
    <id>https://lukasschwab.me/blog/gen/product-ontology.html</id>
    <content type="html">&lt;html xmlns=http://www.w3.org/1999/xhtml lang xml:lang&gt;&lt;meta charset=utf-8&gt;&lt;meta name=generator content=&#34;pandoc&#34;&gt;&lt;meta name=viewport content=&#34;width=device-width,initial-scale=1,user-scalable=yes&#34;&gt;&lt;meta name=author content=&#34;Lukas Schwab&#34;&gt;&lt;meta name=dcterms.date content=&#34;2020-08-22&#34;&gt;&lt;title&gt;Web App Product Ontologies&lt;/title&gt;&lt;style&gt;&#xA;      code{white-space: pre-wrap;}&#xA;      span.smallcaps{font-variant: small-caps;}&#xA;      span.underline{text-decoration: underline;}&#xA;      div.column{display: inline-block; vertical-align: top; width: 50%;}&#xA;          &lt;/style&gt;&lt;link rel=stylesheet href=../styles/common.css&gt;&lt;link rel=stylesheet href=../styles/fence-extensions/addendum.css&gt;&lt;link rel=stylesheet href=../styles/fence-extensions/frame.css&gt;&lt;link rel=stylesheet href=../styles/fence-extensions/gallery.css&gt;&lt;script data-goatcounter=https://lukasschwab.goatcounter.com/count async src=//gc.zgo.at/count.js&gt;&lt;/script&gt;&lt;a href=../index.html&gt;&amp;#171; blog&lt;/a&gt; &lt;span class=date&gt;2020-08-22&lt;/span&gt;&lt;header id=title-block-header&gt;&lt;h1 class=title&gt;Web App Product Ontologies&lt;/h1&gt;&lt;/header&gt;&lt;p&gt;Web apps with user interfaces lend themselves to object-orientation.&#xA;It’s in the middle of the acronym: Document &lt;em&gt;Object&lt;/em&gt; Model. This&#xA;isn’t to say you a “functional” product model is impossible, but&#xA;&lt;em&gt;visualizing&lt;/em&gt; a function involves expressing it with an object;&#xA;to shoehorn a product model into a UI is to congeal it into an&#xA;assemblage of objects.&lt;p&gt;Articulating a good product model requires picking the most&#xA;expressive classes of objects and describing how they’re conceptually&#xA;related: developing a &lt;em&gt;product ontology.&lt;/em&gt;&lt;a href=#fn1 class=footnote-ref id=fnref1 role=doc-noteref&gt;&lt;sup&gt;1&lt;/sup&gt;&lt;/a&gt;&lt;p&gt;Formal XML-derived syntaxes have been developed for physical product&#xA;ontology, but the primacy of physical components makes them awkward fits&#xA;for describing the product models behind web applications. Whereas&#xA;physical product ontologies describe concrete units—an air purifier with&#xA;one fan assembly and one filter assembly, working together—a web product&#xA;model is essentially nonconcrete until runtime.&lt;figure&gt;&lt;img src=../img/product-ontology/lee-owl.png alt=&#34;Lee and Suh’s meta-product ontology divides entities into physical and abstract entities. Web app product models rarely have equivalents to physical Objects (global and static), but deal heavily in SetOrClass and Datatype entities that are instantiated by users.&#34;&gt;&lt;figcaption aria-hidden=true&gt;Lee and Suh’s meta-product ontology&#xA;divides entities into &lt;em&gt;physical&lt;/em&gt; and &lt;em&gt;abstract&lt;/em&gt;&#xA;entities.&lt;a href=#fn2 class=footnote-ref id=fnref2 role=doc-noteref&gt;&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt; Web app product models rarely have&#xA;equivalents to physical &lt;code&gt;Object&lt;/code&gt;s (global and static), but&#xA;deal heavily in &lt;code&gt;SetOrClass&lt;/code&gt; and &lt;code&gt;Datatype&lt;/code&gt;&#xA;entities that are instantiated by users.&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;Rather than describing a collection of identifiable entities and&#xA;their relations, then, a description of a web product specifies classes&#xA;of entities and the relations between their classes. A user who&#xA;udnerstands those classes and relations can use the product to express&#xA;and control the concrete, identifiable stuff.&lt;p&gt;In some respects, this is an easier task than developing a formal&#xA;language for describing air purifiers! Web product planning can borrow&#xA;the notion of an underlying, describable ontology without falling into&#xA;the mire of rigidity/nonrigidity, subassemblies, and so on. Because the&#xA;product’s implementation in code is an unambiguous and analyzable&#xA;expression of the product’s structure, a web product-modeler doesn’t&#xA;have to be as exhaustive.&lt;p&gt;&lt;strong&gt;What are your product’s primary concepts, its objects under&#xA;manipulation? When a user uses your product, what do they&#xA;control?&lt;/strong&gt;&lt;p&gt;I think of this as nailing down a product model’s &lt;em&gt;noumena,&lt;/em&gt;&#xA;those stubborn concepts that persist independent of their&#xA;representation. Whether or not they’re expressed directly—whether or not&#xA;the user even &lt;em&gt;conceives of the product&lt;/em&gt; in their terms—these are&#xA;the product’s underlying objects. An app for scheduling events, no&#xA;matter what interface exposes, works with some internal notion of what&#xA;constitutes an “event;” this is its primary noumenal class.&lt;a href=#fn3 class=footnote-ref id=fnref3 role=doc-noteref&gt;&lt;sup&gt;3&lt;/sup&gt;&lt;/a&gt;&lt;p&gt;Describing noumena and interdependencies early could kick off data&#xA;model design before, or concurrently with, interface design. A&#xA;well-expressed product model can be the joint precursor to both, rather&#xA;than building one to fit the other.&lt;p&gt;That said, sketching a product ontology shouldn’t be mistaken for&#xA;data model design. The engineering task is to produce a data model that&#xA;can unambiguously represent the space of permitted product model states.&#xA;There may be multiple data models that achieve this. In my experience, a&#xA;single product model will be implemented and reimplemented with a&#xA;&lt;em&gt;succession&lt;/em&gt; of data models before it’s changed.&lt;p&gt;As a result, it’s easy to think through other products’ ontologies&#xA;without thinking about the details of their implementation.&lt;p&gt;An example: Trello is essentially a tool for organizing&#xA;&lt;em&gt;cards&lt;/em&gt; representing records. Their product involves other&#xA;nouns—lists, boards, etc.—these are derivative concepts for organizing&#xA;and displaying cards. Regardless of whether you click on a card on a&#xA;board, in a calendar view, or in the archived query view (when a card&#xA;has been disassociated from its board/list), the card is readable and&#xA;editable in exactly the same popover format. Easy to recall, easy to&#xA;teach.&lt;p&gt;An unpleasant example: I can’t think up a tight product ontology for&#xA;Notion. Is it for organizing records (in table or gallery views)? Is it&#xA;for organizing lightly interactive pages, like a wiki with embedded&#xA;forms? Or am I organizing the &lt;em&gt;sections of content&lt;/em&gt; on those&#xA;pages, which are drag-and-drop rearrangebale? There must be some&#xA;underlying consistent data model, but I can’t find it in the fog of&#xA;concepts and interactions.&lt;p&gt;A good product teaches its users to perceive its abstract product&#xA;model, and to intuit based on their understanding thereof. While the&#xA;noumena in that model &lt;em&gt;may not&lt;/em&gt; be represented directly or&#xA;consistently, users will learn them more effectively when they are.&lt;h1 id=controlling-noumena&gt;Controlling noumena&lt;/h1&gt;&lt;p&gt;We can turn sketched classes of product entities into a fistful of&#xA;representational litmus tests. The product we build around our ontology&#xA;will necessarily involve mechanisms for users to view, organize, and&#xA;modify our noumena. Representing the entities in the product model&#xA;&lt;em&gt;directly and comprehensively&lt;/em&gt; should keep the user’s model of&#xA;the system state close to the system’s internal state; keeping a user’s&#xA;understanding accurate should minimize corrective error.&lt;a href=#fn4 class=footnote-ref id=fnref4 role=doc-noteref&gt;&lt;sup&gt;4&lt;/sup&gt;&lt;/a&gt;&lt;p&gt;Consider your product as a &lt;em&gt;control interface&lt;/em&gt; for each class&#xA;of entities: how ergonomically can a user assess the state of an entity&#xA;and cause precisely the &lt;em&gt;represented change&lt;/em&gt; they intend? To that&#xA;end…&lt;ul&gt;&lt;li&gt;&lt;p&gt;How many instances of a given noumenal class do you expect a user&#xA;to have to manage? If this number is unbounded, consider building query&#xA;and filter interfaces from the jump.&lt;li&gt;&lt;p&gt;Are all the properties on a noumenon represented directly? Is&#xA;there a place in your product where they’re represented together in one&#xA;place, viewable at once on standard screens?&lt;li&gt;&lt;p&gt;Does the user control a value indirectly when they could&#xA;reasonably control it directly?&lt;br&gt;This can lead to miscorrections.&#xA;This is a common control pattern in physical systems—you can’t directly&#xA;control the location of a boat (“position control”), but you can control&#xA;the location indirectly with the throttle (“heading control”)—but not&#xA;for web apps.&lt;a href=#fn5 class=footnote-ref id=fnref5 role=doc-noteref&gt;&lt;sup&gt;5&lt;/sup&gt;&lt;/a&gt;&lt;/ul&gt;&lt;ul&gt;&lt;li&gt;Is the manner of control consistent throughout your application? Can&#xA;a user possibly modify a noumenon without realizing they’re doing it?&#xA;Punishing!&lt;/ul&gt;&lt;div class=addendum data-date=&#34;Oct. 12, 2020&#34;&gt;&lt;p&gt;The Cognitive Dimensions of Notations framework&lt;a href=#fn6 class=footnote-ref id=fnref6 role=doc-noteref&gt;&lt;sup&gt;6&lt;/sup&gt;&lt;/a&gt; is&#xA;a list of digital product usability anti-patterns (with some neat&#xA;properties, e.g. pairwise independence). It may seem a stretch to think&#xA;of web apps as “notations,” but these antipatterns can appear anywhere&#xA;users do &lt;em&gt;composition&lt;/em&gt; (whether they’re composing a vacation,&#xA;their account settings, or a program). Users work to get a product to&#xA;express their intent. Blandford and Green turned the Cognitive&#xA;Dimensions of Notation into a set of usability litmus tests like the&#xA;ones I propose above.&lt;a href=#fn7 class=footnote-ref id=fnref7 role=doc-noteref&gt;&lt;sup&gt;7&lt;/sup&gt;&lt;/a&gt;&lt;/div&gt;&lt;p&gt;Besides this usability-forecasting, a well-specified product model&#xA;offers a nice starting point for breaking a product into reusable&#xA;components. It might be sensible to build a single component to manage&#xA;representing a noumenon in a variety of contexts; “cards” (neatly&#xA;arranged into arrays; vertically more forgiving than table rows) are a&#xA;common pattern for this. If certain contexts require the card to be too&#xA;compact to be a good control surface, clicking on it can pop a more&#xA;articulate modal:&lt;a href=#fn8 class=footnote-ref id=fnref8 role=doc-noteref&gt;&lt;sup&gt;8&lt;/sup&gt;&lt;/a&gt; picking noumena that exist&#xA;independent of outside context should mitigate the context-downside of&#xA;hiding the rest of the page.&lt;h1 id=comparative-advantage&gt;Comparative advantage&lt;/h1&gt;&lt;p&gt;Instead of beginning with ontology, we &lt;em&gt;could&lt;/em&gt; begin with&#xA;functionalism: “what do users want to do with our product?” This doesn’t&#xA;&lt;em&gt;avoid&lt;/em&gt; creating a product ontology, but it defers to an&#xA;imaginary user’s incumbent ontology. If a product would simplify a&#xA;complex workflow, this approach cements into its heart an model just as&#xA;unwieldy as the one it should replace.&lt;a href=#fn9 class=footnote-ref id=fnref9 role=doc-noteref&gt;&lt;sup&gt;9&lt;/sup&gt;&lt;/a&gt;&lt;p&gt;Focusing on user workflows too early also runs the risk of tailoring&#xA;our data model too closely to a &lt;em&gt;particular&lt;/em&gt; representation that&#xA;achieves a very specific function. Since representations are immediately&#xA;user-facing, they’re more likely than the data models to be subject to&#xA;early user feedback and iteration. Better to build a flexible data model&#xA;and leave our UI easy to invalidate.&lt;p&gt;Directness and comprehensiveness are useful heuristics for UI&#xA;options. Conversational interfaces are hip for their humanism, but a&#xA;conversation with intense control requirements is frustrating: it’s&#xA;untransparent, which makes it error prone.&lt;div class=addendum data-date=&#34;Dec. 5, 2020&#34;&gt;&lt;p&gt;Winograd (1979)&lt;a href=#fn10 class=footnote-ref id=fnref10 role=doc-noteref&gt;&lt;sup&gt;10&lt;/sup&gt;&lt;/a&gt; gets at something similar, which he&#xA;terms “the subject domain.” One difference: he treats it as a puzzle for&#xA;&lt;em&gt;programmers&lt;/em&gt; rather than a dedicated “product” function, so he&#xA;places special emphasis on the difference between a product ontology and&#xA;a data model:&lt;blockquote&gt;&lt;p&gt;This system [for organizing room assignments at a university], like&#xA;every practical system, is about some subject. There is a world of rooms&#xA;and classes, times and schedules, that exists completely apart from the&#xA;computer system that is understood as referring to them. […] &lt;em&gt;One of&#xA;the primary tasks in programming is to develop a set of descriptions&#xA;that are adequate for talking about the objects being represented.&lt;/em&gt;&#xA;There are descriptions for things we think of as objects&#xA;(e.g. buildings, rooms, courses, departments) and also for processes&#xA;(e.g. the scheduling of events). These descriptions are relative to the&#xA;goals of the system as a whole and embody a basic view of the problem.&#xA;For example, what it takes to represent a room would be different for&#xA;this system and for a system used by contractors in building&#xA;construction.&lt;p&gt;All too often the development of descriptions in this domain is&#xA;confused with the specification of data structures (which are in the&#xA;domain of implementation). &lt;em&gt;In deciding whether we want a course to&#xA;be associated with a single teacher, or to leave open the potential&#xA;representation of team-teaching, we are not making a data structure&#xA;decision.&lt;/em&gt; The association of a teacher (or teachers) with a course&#xA;may be represented in many different data structures in many different&#xA;components of the system. One of the most common problems in integrating&#xA;systems is that the components are based on different decisions in the&#xA;subject domain, and therefore there is no effective way to translate the&#xA;data structures.&lt;/blockquote&gt;&lt;p&gt;Winograd argues&lt;blockquote&gt;&lt;p&gt;A programming system needs to provide a powerful set of mechanisms&#xA;for building up and maintaining ‘world views’—coherent sets of&#xA;description structures in the subject domain that are independent of any&#xA;implementation. Each component can then implement part or all of this in&#xA;a way that will be consistent with both the structure of that component&#xA;and the assumptions made in other components.&lt;/blockquote&gt;&lt;p&gt;Those nineties product ontology projects might be seen as languages&#xA;for this kind of specification, but with the big ambition of describing&#xA;&lt;em&gt;any system,&lt;/em&gt; any “world-view” or set of relations. Teams seem to&#xA;do this kind of work in documentation tools, but they rely on the total&#xA;flexibility of linked text; Winograd argues docstrings are too local,&#xA;too prone to inconsistency, to count.&lt;/div&gt;&lt;div class=addendum data-date=&#34;June 8, 2022&#34;&gt;&lt;p&gt;I might just be reinventing &lt;a href=https://en.wikipedia.org/wiki/Domain-driven_design&gt;domain-driven&#xA;design&lt;/a&gt;. My “noumena” captured by are classes and individuals, &lt;a href=https://en.wikipedia.org/wiki/Ontology_components&gt;ontology&#xA;components&lt;/a&gt;.&lt;p&gt;I feel strongly that “noumena” was a poor choice, not least because&#xA;it suggests a difference from “phenomena” for which there isn’t a&#xA;product analogy.&lt;/div&gt;&lt;aside id=footnotes class=&#34;footnotes footnotes-end-of-document&#34; role=doc-endnotes&gt;&lt;hr&gt;&lt;ol&gt;&lt;li id=fn1&gt;&lt;p&gt;This goal is complicated by the reflexive relationship&#xA;between a product’s ontology and its functionality. Some ontology might&#xA;achieve a product’s functional intent better than others… but the&#xA;functions users desire are themselves formed through their understanding&#xA;of the product model, and they can more articulately communicate desires&#xA;in terms of the product concepts that are named for them. An&#xA;ontology-axiology bind!&lt;a href=#fnref1 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn2&gt;&lt;p&gt;Lee, Jae-Hyun, and Hyo-Won Suh. “OWL-based product&#xA;ontology architecture and representation for sharing product knowledge&#xA;on a web.” In &lt;em&gt;International Design Engineering Technical Conferences&#xA;and Computers and Information in Engineering Conference,&lt;/em&gt;&#xA;vol. 48035, pp. 853-861. 2007.&lt;a href=#fnref2 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn3&gt;&lt;p&gt;I’m self-conscious about “noumenon” being a pretentious&#xA;term, but there’s a utility for me in setting up an unfamiliar category.&#xA;Terms like “entity” or “object” are overloaded (esp. “object,” in an&#xA;essay that’s essentially about preferring &lt;em&gt;object-oriented&#xA;representations&lt;/em&gt; for noumena).&lt;p&gt;It should be clear that we’re not talking about the instance a user&#xA;perceives and interacts with. A user’s perception of the product model&#xA;is wont to be inaccurate; whether there’s really a &lt;em&gt;correct&lt;/em&gt;&#xA;perception of a product model feels like a question for metaphysics.&lt;p&gt;A user’s perception of the product model may be more or less close to&#xA;the &lt;em&gt;designed&lt;/em&gt; product model, though, and the control systems&#xA;reading I reference supports my opinion that closeness here is good. The&#xA;term “noumenon” emphasizes the fallability of the UI phenomena you&#xA;build.&lt;a href=#fnref3 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn4&gt;&lt;p&gt;I’ve been poking thorugh Åström, Karl Johan, and Richard&#xA;M. Murray. &lt;em&gt;Feedback systems: an introduction for scientists and&#xA;engineers.&lt;/em&gt; Princeton university press, 2010.&lt;a href=#fnref4 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn5&gt;&lt;p&gt;“Position control” and “direct control” are terms taken&#xA;from Abrecht, Blake, and N. G. Leveson. “Systems theoretic process&#xA;analysis (STPA) of an offshore supply vessel dynamic positioning&#xA;system.” &lt;em&gt;Massachusetts Institute of Technology, Cambridge, MA&lt;/em&gt;&#xA;(2016).&lt;a href=#fnref5 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn6&gt;&lt;p&gt;The Cognitive Dimensions are decently &lt;a href=https://en.wikipedia.org/wiki/Cognitive_dimensions_of_notations&gt;summarized&#xA;on Wikipedia&lt;/a&gt;, but more clearly in Blackwell, Alan, and Thomas Green.&#xA;“Notational systems - the cognitive dimensions of notations framework.”&#xA;&lt;em&gt;HCI models, theories, and frameworks: toward an interdisciplinary&#xA;science.&lt;/em&gt; Morgan Kaufmann (2003).&lt;a href=#fnref6 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn7&gt;&lt;p&gt;Blandford, Ann, and Thomas Green. “OSM: an&#xA;ontology-based approach to usability evaluation.” Note: the notion of&#xA;&lt;em&gt;user-private&lt;/em&gt;, &lt;em&gt;device-private&lt;/em&gt;, and &lt;em&gt;shared&lt;/em&gt;&#xA;concepts butts up againts my idea of a “noumenon” here, but more&#xA;value-neutrally. In my mind, the noumena form a starting set of shared&#xA;concepts.&lt;a href=#fnref7 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn8&gt;&lt;p&gt;Trello does this, but Airtable’s modal view for a record&#xA;is a great example from a more complex product. Sometimes the record is&#xA;a row in a table; sometimes it’s a card in a gallery; no matter what,&#xA;clicking on it will reveal all the data for the record and allow you to&#xA;directly modify &lt;em&gt;any&lt;/em&gt; of it. No matter the representation, the&#xA;user is one modal-expanding click away from a consistent control&#xA;experience.&lt;a href=#fnref8 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn9&gt;&lt;p&gt;Additionally, users are nonhomogeneous; overfitting to&#xA;your early users’ mental models might be a mistake. This is especially&#xA;true for teams with the ambition to shift up/down-market or stretch&#xA;their product into new applications, or strong selection biases in their&#xA;initial user base (e.g. a bias towards users from a certain accelerator&#xA;cohort).&lt;a href=#fnref9 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn10&gt;&lt;p&gt;Winograd, Terry. “Beyond programming languages.”&#xA;&lt;em&gt;Communications of the ACM&lt;/em&gt; 22, no. 7 (1979): 395.&lt;a href=#fnref10 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;/ol&gt;&lt;/aside&gt;</content>
    <link href="https://lukasschwab.me/blog/gen/product-ontology.html" rel="alternate"></link>
    <summary type="html">What are a product&#39;s central concepts? I consider planning products around a product model&#39;s &#34;noumena.&#34; Such an object-oriented organizing principle should mitigate corrective error and, hopefully, set you up for interpretable and reusable componentry.</summary>
  </entry>
  <entry>
    <title>Architecture and Organs</title>
    <updated>2020-07-25T00:00:00Z</updated>
    <id>https://lukasschwab.me/blog/gen/architecture-and-organs.html</id>
    <content type="html">&lt;html xmlns=http://www.w3.org/1999/xhtml lang xml:lang&gt;&lt;meta charset=utf-8&gt;&lt;meta name=generator content=&#34;pandoc&#34;&gt;&lt;meta name=viewport content=&#34;width=device-width,initial-scale=1,user-scalable=yes&#34;&gt;&lt;meta name=author content=&#34;Lukas Schwab&#34;&gt;&lt;meta name=dcterms.date content=&#34;2020-07-25&#34;&gt;&lt;title&gt;Architecture and Organs&lt;/title&gt;&lt;style&gt;&#xA;      code{white-space: pre-wrap;}&#xA;      span.smallcaps{font-variant: small-caps;}&#xA;      span.underline{text-decoration: underline;}&#xA;      div.column{display: inline-block; vertical-align: top; width: 50%;}&#xA;          &lt;/style&gt;&lt;link rel=stylesheet href=../styles/common.css&gt;&lt;link rel=stylesheet href=../styles/fence-extensions/addendum.css&gt;&lt;link rel=stylesheet href=../styles/fence-extensions/frame.css&gt;&lt;script data-goatcounter=https://lukasschwab.goatcounter.com/count async src=//gc.zgo.at/count.js&gt;&lt;/script&gt;&lt;a href=../index.html&gt;&amp;#171; blog&lt;/a&gt; &lt;span class=date&gt;2020-07-25&lt;/span&gt;&lt;header id=title-block-header&gt;&lt;h1 class=title&gt;Architecture and Organs&lt;/h1&gt;&lt;/header&gt;&lt;p&gt;Last November, at the end of a long day of walking, I sat in&#xA;Saint-Étienne-du-Mont and tried to sketch its rood loft. Without any&#xA;announcement, the organ started playing. This was a striking experience:&#xA;totally still and dimly-lit (memorably dimmer than it is in photos&#xA;online), the body of the church was suddenly full of sound.&lt;p&gt;I’ll discuss two interplays I’ve picked out of this experience in&#xA;Paris: first, between the organ and its architectural enclosure (its&#xA;&lt;em&gt;place&lt;/em&gt;), and then between the organ and the organist. I’m&#xA;interested, from a theoretical perspective, in how an instrument (in the&#xA;general sense: a tool) can behave as a sort of architecture.&lt;a href=#fn1 class=footnote-ref id=fnref1 role=doc-noteref&gt;&lt;sup&gt;1&lt;/sup&gt;&lt;/a&gt; My typology of organs is sloppy, but&#xA;I’m particularly interested in big religious pipe organs, and especially&#xA;those where the console is obscured.&lt;p&gt;This post covers the first interplay: between organs and their&#xA;enclosures.&lt;hr&gt;&lt;p&gt;In some respects, pipe organs just occupy the space allowed to them.&#xA;In churches designed with pipe organs in mind, this might mean it fills&#xA;an organ loft, or it might hang from an arch or perch on a choir screen&#xA;like the nest of some infernal wasp. Organ case design differs&#xA;dramatically, often to the end of integrating the instruments with their&#xA;surrounds. Baroque cases gild the mechanism; ornate folds and statuettes&#xA;hide the sharply geometric ends of the pipes, blur the instrument’s&#xA;edges, and fill the seams between an organ’s angular guts and the rounds&#xA;of its enclave. Rococo organ cases are suitably ludicrous.&lt;p&gt;In gothic cathedrals (or with more modern organs) this effect might&#xA;be more subtle. At the Basilique Saint-Denis, the organ is connected to&#xA;the surrounding architecture by its case’s mock-tracery. The three&#xA;darkened sections of Cologne Cathedral’s transept organ recall the&#xA;cathedral facade’s black, tripartite bulk. In Reykjavik’s&#xA;Hallgrímskirkja, where &lt;em&gt;both&lt;/em&gt; the instrument and the church&#xA;experiment with gothic forms, the clusters of pipes and their angular&#xA;brackets recall the exterior’s quasi-basalt.&lt;a href=#fn2 class=footnote-ref id=fnref2 role=doc-noteref&gt;&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt;&lt;p&gt;Musically, on the other hand, the enclosure becomes a part of the&#xA;organ. The acoustics of premodern churches might be as much by accident&#xA;as by design, but each is a unique acoustic extension to the instrument&#xA;it contains. This is true of other spaces and other instruments, but a&#xA;given organ is fixed in a particular acoustic context and the range and&#xA;timbre of pipe organs makes their music feel especially&#xA;&lt;em&gt;reverberal&lt;/em&gt;. Inasmuch as its musicality is bound up with its&#xA;space, the same instrument in two different spaces isn’t &lt;em&gt;really&lt;/em&gt;&#xA;the same instrument.&lt;figure&gt;&lt;img src=../img/organs/soissons.png alt=&#34;Soissons Cathedral’s nave was torn open by shelling during World War I. Its organ stayed put—weirdly cosmetically intact—in the loft.&#34;&gt;&lt;figcaption aria-hidden=true&gt;Soissons Cathedral’s nave was torn open&#xA;by shelling during World War I. Its organ stayed put—weirdly&#xA;cosmetically intact—in the loft.&lt;a href=#fn3 class=footnote-ref id=fnref3 role=doc-noteref&gt;&lt;sup&gt;3&lt;/sup&gt;&lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;The organ exterior is so &lt;em&gt;static&lt;/em&gt;, so visually unmoved by the&#xA;sounds it makes, that its music feels like sounds the space makes&#xA;itself. I think this must be, at least to some extent, an association&#xA;born of an organ’s unique capacity to hold notes without modulation or&#xA;end: no quaver of human breath, no percussed dissipation, no reversals&#xA;in the direction of a bow. What else could sound like an edifice?&lt;a href=#fn4 class=footnote-ref id=fnref4 role=doc-noteref&gt;&lt;sup&gt;4&lt;/sup&gt;&lt;/a&gt; This connection between musicality&#xA;and place is especially strong in Áine O’Dwyer’s &lt;em&gt;Music for Church&#xA;Cleaners&lt;/em&gt;: this isn’t an aterritorial instrument carted into a&#xA;recording studio, but rather the music of a particular site of labor and&#xA;ritual, complete with the shuffling of feet and quiet clatter of&#xA;brooms.&lt;/p&gt;&lt;iframe style=&#34;border: 0; width: 100%; height: 120px;&#34; src=&#34;https://bandcamp.com/EmbeddedPlayer/album=142832589/size=large/bgcol=ffffff/linkcol=0687f5/tracklist=false/artwork=small/track=2115213712/transparent=true/&#34; seamless&gt;&lt;a href=http://aineodwyer.bandcamp.com/album/music-for-church-cleaners&gt;Music&#xA;For Church Cleaners by Áine O&#39;Dwyer&lt;/a&gt;&lt;/iframe&gt;&lt;p&gt;By contrast, other pipe organ recordings are engineered in an effort&#xA;to extricate the instrument from its architectural setting. The&#xA;recording does its best to silence the architecture, but comments like&#xA;this from the release notes for Kali Malone’s &lt;em&gt;The Sacrificial&#xA;Code&lt;/em&gt; admit the concomitance of an organ and its setting:&lt;blockquote&gt;&lt;p&gt;The recordings here involved careful close miking of the pipe organ&#xA;in such a way as to eliminate environmental identifiers as far as&#xA;possible—essentially removing the large hall reverb so inextricably&#xA;linked to the instrument.&lt;/blockquote&gt;&lt;iframe style=&#34;border: 0; width: 100%; height: 120px;&#34; src=&#34;https://bandcamp.com/EmbeddedPlayer/album=3681884855/size=large/bgcol=ffffff/linkcol=0687f5/tracklist=false/artwork=small/track=1576051940/transparent=true/&#34; seamless&gt;&lt;a href=http://kalimalone.bandcamp.com/album/the-sacrificial-code&gt;The&#xA;Sacrificial Code by Kali Malone&lt;/a&gt;&lt;/iframe&gt;&lt;p&gt;This close relationship with place also yields an artistic&#xA;association with the church/state/power trinity: there haven’t been that&#xA;many pipe organs, and every one of them is an exceptional mobilization&#xA;of craft and capital. It’s a quintessentially baroque instrument. Along&#xA;with colonially exported baroque architecture, colonial organs signify&#xA;&lt;em&gt;hegemony&lt;/em&gt; by design. Spain razed Tenochtitlan’s Templo Mayor and&#xA;built a cathedral in its place; in that cathedral the Spanish crown&#xA;comissioned a succession of organs, including its &lt;a href=https://en.wikipedia.org/wiki/File:Catedral_Metropolitana,_M%C3%A9xico_D.F.,_M%C3%A9xico,_2013-10-16,_DD_99.JPG&gt;twin&#xA;surviving mahogany behemoths.&lt;/a&gt;&lt;div class=addendum data-date=&#34;Jun. 17, 2020&#34;&gt;&lt;p&gt;That the organs in Mexico City wall the choir off from the&#xA;choir-aisles underscores their social function:&lt;blockquote&gt;&lt;p&gt;The clergy needed some kind of barrier to separate them from the&#xA;laity, in order not to to be disturbed at their prayers, which took&#xA;place seven times a gday, and it was from this need that rood-screens&#xA;and screens dividing choirs from choir aisles developed. […] These high&#xA;screens, which sometimes take the form of walls (for instance, at&#xA;&lt;em&gt;Lugo&lt;/em&gt; in Spain), destroy the open view that was originally&#xA;intended, and, because of this, many rood-screens in France and Germany&#xA;were later removed. The exclusion of the congregation was felt to be too&#xA;aristocratic, and even the clergy probably found this attitude of&#xA;separation presumptuous. […] Their purpose, stated barely and soberly,&#xA;was to express a system of social strata.&lt;a href=#fn5 class=footnote-ref id=fnref5 role=doc-noteref&gt;&lt;sup&gt;5&lt;/sup&gt;&lt;/a&gt;&lt;/blockquote&gt;&lt;/div&gt;&lt;p&gt;The sound itself, even absent a physical organ, has this&#xA;baroque-architectural connotation. &lt;em&gt;Last Year at Marienbad&lt;/em&gt;’s&#xA;opening Möbian catalog—of stucco, carpets, hallways opening onto&#xA;hallways, an architecture too slippery for memory—loops against Francis&#xA;Seyrig’s ornamented organ score.&lt;/p&gt;&lt;iframe width=100% height=315 src=https://www.youtube.com/embed/pFuNm1hS4SA frameborder=0 allow=&#34;accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture&#34; allowfullscreen&gt;&lt;/iframe&gt;&lt;p&gt;I’d call all three of these compositions “ambient.” Not all organ&#xA;music is ambient, but both architecture and the organ have strong&#xA;aesthetic ties to &lt;em&gt;ambience.&lt;/em&gt; My claims about the acoustic and&#xA;visual interplay between an organ and its enclosure are probably&#xA;uncontroversial, but this poking at an aesthetic &lt;em&gt;isometry&lt;/em&gt; via&#xA;ambience really captures my imagination.&lt;div class=addendum data-date=&#34;Oct. 12, 2020&#34;&gt;&lt;p&gt;My assertion that organs &lt;em&gt;as instruments&lt;/em&gt; are tied up with&#xA;architecture makes me nervous. What about organs that have been restored&#xA;and moved from one space to the other? What about the great trends and&#xA;conflicts in organ &lt;a href=http://decouverte.orgue.free.fr/e_esthet.htm&gt;tonal&#xA;design&lt;/a&gt;––have I done the necessary reading to make some totalizing&#xA;claim about the instrument’s connection to place?&lt;p&gt;I’ve done some nervous googling to check that I’m not way off-base.&#xA;&lt;a href=https://www.washingtonpost.com/entertainment/music/2016/07/29/3349abf0-3a47-11e6-8f7c-d4c723a2becb_story.html&gt;One&#xA;article:&lt;/a&gt;&lt;blockquote&gt;&lt;p&gt;The beauty of the organ — or the curse, depending on your point of&#xA;view — is that no two are alike; each is built and voiced to fit the&#xA;acoustics of a particular room. A pipe organ does not travel. […]&#xA;because the pipes can be located several hundred feet from the manual,&#xA;they often “speak” at slightly different intervals from the moment a key&#xA;is pressed.&lt;/blockquote&gt;&lt;p&gt;And &lt;a href=https://www.atlasobscura.com/articles/great-organ-notre-dame&gt;another&lt;/a&gt;:&lt;blockquote&gt;&lt;p&gt;But the organ itself is just one part of the sonic and aesthetic&#xA;equation; the other is the architecture that houses it. To hear an organ&#xA;fully is to experience it in what organists call “the room.” Whether an&#xA;intimate hall or a vast cathedral, the size, shape, and building&#xA;materials are vital factors.&lt;/blockquote&gt;&lt;/div&gt;&lt;aside id=footnotes class=&#34;footnotes footnotes-end-of-document&#34; role=doc-endnotes&gt;&lt;hr&gt;&lt;ol&gt;&lt;li id=fn1&gt;&lt;p&gt;“Consoles” (as in the &lt;a href=https://en.wiktionary.org/wiki/console#Etymology_1&gt;architectural&#xA;brackets&lt;/a&gt;) are a joint etymological ancestor for organ consoles and&#xA;electronic consoles. This is probably the kernel of what I find&#xA;interesting here: the console simultaneously fixes the function of a&#xA;space and constitutes a part of its architectural form.&lt;a href=#fnref1 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn2&gt;&lt;p&gt;I found &lt;a href=https://www.pipelinepress.com/gothic-cases.html&gt;this brief&#xA;overview&lt;/a&gt; of the adaptation of Gothic-architectural motifs to organ&#xA;case designs. Crenellation feels like a distinctly pre-gothic inclusion&#xA;on these original organs, which leads me to wonder if the earlier organs&#xA;at Saint-Denis (a basilica completed in 1144) mimicked their&#xA;surroundings as carefully as its current instrument (the last in a line&#xA;of sevaral organs, built in 1841). If I had to guess, the stylistic&#xA;similarities have probably gotten stronger as successive revivalist&#xA;waves became more self-aware; this probably subjects me to a kind of&#xA;recency bias.&lt;a href=#fnref2 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn3&gt;&lt;p&gt;Gilman, Roger. “The theory of gothic architecture and&#xA;the effect of shellfire at Rheims and Soissons.” &lt;em&gt;American Journal of&#xA;Archaeology&lt;/em&gt; 24, no. 1 (1920): 37-72.&lt;a href=#fnref3 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn4&gt;&lt;p&gt;The impression that an organ’s sounds belong to the&#xA;building itself might be especially dramatic at the S.F. Legion of&#xA;Honor: the &lt;a href=https://legionofhonor.famsf.org/about/skinner-organ&gt;Spreckels&#xA;Organ’s&lt;/a&gt; pipes are hidden in the building’s walls. I haven’t had a&#xA;chance to catch one of their concerts yet, but I probably prefer organs&#xA;I can look at anyway.&lt;a href=#fnref4 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;li id=fn5&gt;&lt;p&gt;Frankl, Paul, and Paul Crossley. Gothic architecture.&#xA;Vol. 58. Yale University Press, 2000. 226.&lt;a href=#fnref5 class=footnote-back role=doc-backlink&gt;↩︎&lt;/a&gt;&lt;/ol&gt;&lt;/aside&gt;</content>
    <link href="https://lukasschwab.me/blog/gen/architecture-and-organs.html" rel="alternate"></link>
    <summary type="html">Exploring the aesthetic relationship between pipe organs and the architecture around them. Organs&#39; aesthetics mirror architecture to effect a sort of embedding, and the room is an inextricable part of the instrument.</summary>
  </entry>
</feed>