<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[Pattern Matched]]></title><description><![CDATA[Essays covering Functional & Systems Programming, and Software Engineering.]]></description><link>https://patternmatched.substack.com</link><image><url>https://substackcdn.com/image/fetch/$s_!ocb0!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe171a870-253d-4ebc-9a74-4ccda69726bb_1200x1200.png</url><title>Pattern Matched</title><link>https://patternmatched.substack.com</link></image><generator>Substack</generator><lastBuildDate>Tue, 02 Jun 2026 07:22:00 GMT</lastBuildDate><atom:link href="https://patternmatched.substack.com/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Desert Thunder]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[patternmatched@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[patternmatched@substack.com]]></itunes:email><itunes:name><![CDATA[Owais]]></itunes:name></itunes:owner><itunes:author><![CDATA[Owais]]></itunes:author><googleplay:owner><![CDATA[patternmatched@substack.com]]></googleplay:owner><googleplay:email><![CDATA[patternmatched@substack.com]]></googleplay:email><googleplay:author><![CDATA[Owais]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[Software as Fan Art]]></title><description><![CDATA[A dev log where I walk through how I'm taking inspiration from great tools.]]></description><link>https://patternmatched.substack.com/p/software-as-fan-art</link><guid isPermaLink="false">https://patternmatched.substack.com/p/software-as-fan-art</guid><dc:creator><![CDATA[Owais]]></dc:creator><pubDate>Sat, 07 Mar 2026 15:07:31 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!q5oV!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F411147f5-9ff4-48ab-b62a-cd6fc6409c7e_3080x1950.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>As the title suggests, the theme of this dev log is &#8220;Software as Fan Art.&#8221; Everything I&#8217;ve worked on in February and March has been inspired by some other piece of software that I appreciate.</p><h2>Writer</h2><p>For the past month or so I&#8217;ve been working on a bunch of tauri apps, each with a different front-end framework. The first one I want to talk about is Writer. It&#8217;s inspired by iaWriter in that it&#8217;s basically my attempt at remaking iaWriter with tools and libraries I know how to use. I also added some features that are a bit like Things, specifically the quick capture system. </p><p>It&#8217;s built with codemirror, React, and React-PDF on the front-end. One of the big things I tried to attempt with this project was to use Rust as much as possible to hold state, though you&#8217;ll see that there are more lines of code in TypeScript, likely due to markup. There&#8217;s a ton of markup and code splitting because I opted to use a lint rule that forced me to keep my JSX nesting under 3-4 levels, aptly called <code>react/jsx-max-depth</code> (I use this in the SolidJS project I&#8217;ll discuss later too).</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!q5oV!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F411147f5-9ff4-48ab-b62a-cd6fc6409c7e_3080x1950.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!q5oV!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F411147f5-9ff4-48ab-b62a-cd6fc6409c7e_3080x1950.png 424w, https://substackcdn.com/image/fetch/$s_!q5oV!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F411147f5-9ff4-48ab-b62a-cd6fc6409c7e_3080x1950.png 848w, https://substackcdn.com/image/fetch/$s_!q5oV!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F411147f5-9ff4-48ab-b62a-cd6fc6409c7e_3080x1950.png 1272w, https://substackcdn.com/image/fetch/$s_!q5oV!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F411147f5-9ff4-48ab-b62a-cd6fc6409c7e_3080x1950.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!q5oV!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F411147f5-9ff4-48ab-b62a-cd6fc6409c7e_3080x1950.png" width="1456" height="922" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/411147f5-9ff4-48ab-b62a-cd6fc6409c7e_3080x1950.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:922,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:2471589,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://patternmatched.substack.com/i/190181277?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F411147f5-9ff4-48ab-b62a-cd6fc6409c7e_3080x1950.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!q5oV!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F411147f5-9ff4-48ab-b62a-cd6fc6409c7e_3080x1950.png 424w, https://substackcdn.com/image/fetch/$s_!q5oV!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F411147f5-9ff4-48ab-b62a-cd6fc6409c7e_3080x1950.png 848w, https://substackcdn.com/image/fetch/$s_!q5oV!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F411147f5-9ff4-48ab-b62a-cd6fc6409c7e_3080x1950.png 1272w, https://substackcdn.com/image/fetch/$s_!q5oV!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F411147f5-9ff4-48ab-b62a-cd6fc6409c7e_3080x1950.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Tauri holds document and file state and emits file-system events, and partial rendering of markdown. This is done via Tauri&#8217;s <code>Emitter</code> struct and the <code>notify</code> crate (it&#8217;s cross-platform, just like Tauri). Through JavaScript bindings, the listens for events and renders the current state of all files. An experiment I tried out with this was to build a system of ports, named after and inspired heavily by the Elm Architecture (TEA). Ports are bindings that Elm code uses to call JavaScript, and similarly, the ports in my project invoke Rust code. The basic flow is pretty simple:</p><blockquote><p>Send a message (command identified by a string), receive an update from the &#8220;back-end,&#8221; and then update the UI accordingly.</p></blockquote><p>One crazy thing I discovered about Tauri while working on this is that Tauri&#8217;s external drag being enabled completely breaks HTML5 drag and drop. I spent days trying to figure out how to properly compute drop zones before finding the <a href="https://github.com/tauri-apps/tauri/issues/14373">github issue</a> that explained this. I made a small website at <a href="https://writer.stormlightlabs.org/">writer.stormlightlabs.org</a> and releases will be hosted on Github. I&#8217;m working on finalizing version 0.2.0 and am excited to share it.</p><p>Source code here: <a href="https://github.com/stormlightlabs/writer">https://github.com/stormlightlabs/writer</a></p><h2>Agent V</h2><p>Agent V (v for visualizer) is a CLI and desktop GUI that ingests logs and session data from AI programming agents/assistants and displays them. Right now it supports Codex, Claude, OpenCode, and Crush. It&#8217;s been interesting to take a peek into how I use agents and the estimated token costs of what I ask them to do. It would be interesting to see how much it costs the companies hosting the models and the carbon footprint of my work. </p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!TgKl!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9615f506-c9cc-413f-826d-f526b9e85a7c_2290x1450.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!TgKl!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9615f506-c9cc-413f-826d-f526b9e85a7c_2290x1450.png 424w, https://substackcdn.com/image/fetch/$s_!TgKl!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9615f506-c9cc-413f-826d-f526b9e85a7c_2290x1450.png 848w, https://substackcdn.com/image/fetch/$s_!TgKl!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9615f506-c9cc-413f-826d-f526b9e85a7c_2290x1450.png 1272w, https://substackcdn.com/image/fetch/$s_!TgKl!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9615f506-c9cc-413f-826d-f526b9e85a7c_2290x1450.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!TgKl!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9615f506-c9cc-413f-826d-f526b9e85a7c_2290x1450.png" width="1456" height="922" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9615f506-c9cc-413f-826d-f526b9e85a7c_2290x1450.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:922,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1273436,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://patternmatched.substack.com/i/190181277?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9615f506-c9cc-413f-826d-f526b9e85a7c_2290x1450.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!TgKl!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9615f506-c9cc-413f-826d-f526b9e85a7c_2290x1450.png 424w, https://substackcdn.com/image/fetch/$s_!TgKl!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9615f506-c9cc-413f-826d-f526b9e85a7c_2290x1450.png 848w, https://substackcdn.com/image/fetch/$s_!TgKl!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9615f506-c9cc-413f-826d-f526b9e85a7c_2290x1450.png 1272w, https://substackcdn.com/image/fetch/$s_!TgKl!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9615f506-c9cc-413f-826d-f526b9e85a7c_2290x1450.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>As you can see from the screenshot that there are still some data quality kinks to work out. Those have been hard to keep up with. This is unconfirmed but it seems like OpenCode switched to a SQLite database to store session data while I was working on this (or I just didn&#8217;t notice it).  As far as OpenCode goes, I had started with the export command from OpenCode&#8217;s CLI by having the system listen for updates to the log directory. When a log file was added it would call export and store the session. Like Writer, it uses notify as its filesystem watcher to look for new sessions in all sessions so that you can watch your sessions in real-time.</p><p>Source code here: <a href="https://github.com/stormlightlabs/agentv">https://github.com/stormlightlabs/agentv</a></p><h2>Thunderus</h2><p>Thunderus is an AI agent and harness I&#8217;m working on that is meant to provide me with a first-class experience using Kimi K2.5 and GLM5. I find that using Claude and OpenCode, though effective doesn&#8217;t work perfectly. GLM with OpenCode in particular will leave off a closing brace and enter a &#8220;death loop&#8221; trying to find and fix the problem. I don&#8217;t yet know if that&#8217;s a model problem or a harness problem but I know that side loading GLM keys into Claude doesn&#8217;t cause this. I want to leave Claude as-is with Anthropic models so I stick to OpenCode despite the occasional headache. </p><p>This is being built with Rust, specifically ratatui. I like Ratatui a lot but compared to libraries like Ink and Bubbletea which borrow patterns from frontend focused tools, it&#8217;s been a bit of a learning curve for me. Writing tools and streaming requests between the client and model hasn&#8217;t been too bad but getting the UI to match the designs I made has been difficult. One thing about designs: HTML and ASCII mockups are the way to go for TUIs.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!WtlS!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2a5a8741-d731-4362-99ac-10635078a92c_2290x1450.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!WtlS!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2a5a8741-d731-4362-99ac-10635078a92c_2290x1450.png 424w, https://substackcdn.com/image/fetch/$s_!WtlS!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2a5a8741-d731-4362-99ac-10635078a92c_2290x1450.png 848w, https://substackcdn.com/image/fetch/$s_!WtlS!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2a5a8741-d731-4362-99ac-10635078a92c_2290x1450.png 1272w, https://substackcdn.com/image/fetch/$s_!WtlS!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2a5a8741-d731-4362-99ac-10635078a92c_2290x1450.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!WtlS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2a5a8741-d731-4362-99ac-10635078a92c_2290x1450.png" width="1456" height="922" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2a5a8741-d731-4362-99ac-10635078a92c_2290x1450.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:922,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:841847,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://patternmatched.substack.com/i/190181277?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2a5a8741-d731-4362-99ac-10635078a92c_2290x1450.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!WtlS!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2a5a8741-d731-4362-99ac-10635078a92c_2290x1450.png 424w, https://substackcdn.com/image/fetch/$s_!WtlS!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2a5a8741-d731-4362-99ac-10635078a92c_2290x1450.png 848w, https://substackcdn.com/image/fetch/$s_!WtlS!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2a5a8741-d731-4362-99ac-10635078a92c_2290x1450.png 1272w, https://substackcdn.com/image/fetch/$s_!WtlS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2a5a8741-d731-4362-99ac-10635078a92c_2290x1450.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>They&#8217;ll look prettier than what comes out in your terminal but they do look cool.</p><p>It&#8217;s not open source&#8230;yet but once it&#8217;s usable I&#8217;ll post an update!</p><h2>Video Editor X</h2><p>Why it&#8217;s called X I don&#8217;t know. It&#8217;s basically untitled. This is the SolidJS + Tauri project I mentioned earlier. I quite like some of the constructs in Solid. It markets itself as a framework for fine grained reactivity but I really think it provides you with more transparency about the computations being done in your projects. The eslint plugin does a good job of telling you when you&#8217;re not properly leveraging signals too. Svelte obscures signals unless you try to type <code>createRawSnippet</code> where in order to use the props you pass into the snippet, you have to call a function. The big thing Solid provides as far as DX goes is no dependency array &#8220;footguns.&#8221; It automatically detects dependencies much like <code>$derived</code> an <code>$effect</code> in Svelte (which by the way, can be a footgun if you don&#8217;t use <code>untrack</code> appropriately) so the compiler reads calls to <code>createEffect</code> and <code>createMemo</code> to construct a list of dependencies. Pretty cool stuff.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!yhk_!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4c52b0dc-4eef-42b8-a9f9-ec5673111a1f_2290x1450.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!yhk_!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4c52b0dc-4eef-42b8-a9f9-ec5673111a1f_2290x1450.png 424w, https://substackcdn.com/image/fetch/$s_!yhk_!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4c52b0dc-4eef-42b8-a9f9-ec5673111a1f_2290x1450.png 848w, https://substackcdn.com/image/fetch/$s_!yhk_!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4c52b0dc-4eef-42b8-a9f9-ec5673111a1f_2290x1450.png 1272w, https://substackcdn.com/image/fetch/$s_!yhk_!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4c52b0dc-4eef-42b8-a9f9-ec5673111a1f_2290x1450.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!yhk_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4c52b0dc-4eef-42b8-a9f9-ec5673111a1f_2290x1450.png" width="1456" height="922" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/4c52b0dc-4eef-42b8-a9f9-ec5673111a1f_2290x1450.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:922,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1044830,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://patternmatched.substack.com/i/190181277?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4c52b0dc-4eef-42b8-a9f9-ec5673111a1f_2290x1450.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!yhk_!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4c52b0dc-4eef-42b8-a9f9-ec5673111a1f_2290x1450.png 424w, https://substackcdn.com/image/fetch/$s_!yhk_!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4c52b0dc-4eef-42b8-a9f9-ec5673111a1f_2290x1450.png 848w, https://substackcdn.com/image/fetch/$s_!yhk_!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4c52b0dc-4eef-42b8-a9f9-ec5673111a1f_2290x1450.png 1272w, https://substackcdn.com/image/fetch/$s_!yhk_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4c52b0dc-4eef-42b8-a9f9-ec5673111a1f_2290x1450.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The core problem this project this solves for me is wanting to take advantage of ffmpeg and its powerful set of features. I want a simple editing setup and a way to draw audio visualizations using it with a GUI. I also want to be able to take markdown slides and turn them into simple video with audio voiceover. It takes advantage of browser APIs exposed by WebViews for audio recording, editing and parsing libraries that JS can provide like codemirror, as well as htmltocanvas for markdown to html to png for the backend to put together in a video.</p><p>Source code here: <a href="https://github.com/stormlightlabs/video-editor">https://github.com/stormlightlabs/video-editor</a></p><h2>Closing Thoughts</h2><p>I think I&#8217;m the poster child for what AI provides engineers, as well as what it exposes in us. My productively has skyrocketed and I&#8217;m building a lot of stuff that I&#8217;m proud of, but I&#8217;m awful at handling releases and promoting my own work. My workflow has changed around this where I don&#8217;t power through implementation at the same rate and instead work on a set of tasks with some level of TDD, then iterate on a feature until I feel ready to move forward. This feels like slowing down but my output is turning out better. I&#8217;m also leaning into desktop and terminal projects because I don&#8217;t need to setup and configure hosting for these projects. They very easily adhere to my local-first ethos.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://patternmatched.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading! Subscribe to get my thoughts straight to your inbox.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p></p>]]></content:encoded></item><item><title><![CDATA[MiniSearch for Astro]]></title><description><![CDATA[How I'm refactoring desertthunder.dev with Astro and more advanced features]]></description><link>https://patternmatched.substack.com/p/minisearch-for-astro</link><guid isPermaLink="false">https://patternmatched.substack.com/p/minisearch-for-astro</guid><dc:creator><![CDATA[Owais]]></dc:creator><pubDate>Tue, 25 Nov 2025 19:26:02 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!n4Yy!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff665ebb3-3b51-4bc4-bc43-17371d8ca0d4_1606x863.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Designing &amp; implementing client-side search for a statically generated site is mostly an integration problem: normalize heterogeneous content, ship a compact index, and hydrate just enough JavaScript to answer queries interactively.</p><p>I chose <a href="https://lucaong.github.io/minisearch/">MiniSearch</a> because it runs entirely in the browser, exposes well-documented ranking hooks, and maintains a reasonable bundle size even after adding conveniences such as fuzzy matching and prefix search.</p><p>Today, I&#8217;d like to walk through the different components of my implementation so that they can be reproduced or extended for your own use without requiring reverse-engineering of the repository. You can follow along with the source code <a href="https://github.com/desertthunder/website">here</a>. As of writing, we&#8217;re in the refactor branch.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!n4Yy!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff665ebb3-3b51-4bc4-bc43-17371d8ca0d4_1606x863.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!n4Yy!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff665ebb3-3b51-4bc4-bc43-17371d8ca0d4_1606x863.png 424w, https://substackcdn.com/image/fetch/$s_!n4Yy!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff665ebb3-3b51-4bc4-bc43-17371d8ca0d4_1606x863.png 848w, https://substackcdn.com/image/fetch/$s_!n4Yy!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff665ebb3-3b51-4bc4-bc43-17371d8ca0d4_1606x863.png 1272w, https://substackcdn.com/image/fetch/$s_!n4Yy!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff665ebb3-3b51-4bc4-bc43-17371d8ca0d4_1606x863.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!n4Yy!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff665ebb3-3b51-4bc4-bc43-17371d8ca0d4_1606x863.png" width="1456" height="782" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f665ebb3-3b51-4bc4-bc43-17371d8ca0d4_1606x863.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:782,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:427583,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://patternmatched.substack.com/i/179950162?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff665ebb3-3b51-4bc4-bc43-17371d8ca0d4_1606x863.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!n4Yy!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff665ebb3-3b51-4bc4-bc43-17371d8ca0d4_1606x863.png 424w, https://substackcdn.com/image/fetch/$s_!n4Yy!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff665ebb3-3b51-4bc4-bc43-17371d8ca0d4_1606x863.png 848w, https://substackcdn.com/image/fetch/$s_!n4Yy!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff665ebb3-3b51-4bc4-bc43-17371d8ca0d4_1606x863.png 1272w, https://substackcdn.com/image/fetch/$s_!n4Yy!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff665ebb3-3b51-4bc4-bc43-17371d8ca0d4_1606x863.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2>Why MiniSearch?</h2><p>The choice of search engine for a static site involves trade-offs between implementation complexity, runtime performance, and user experience.</p><p>MiniSearch addresses the specific use case where full-text search features such as prefix search, fuzzy matching, and field boosting are required to achieve the experience I&#8217;m after. Still, the corpus fits comfortably in browser memory. By storing the index locally rather than querying a remote service, the system eliminates network latency. It enables true offline functionality, a characteristic particularly valuable for progressive web applications or documentation sites.</p><p>MiniSearch ends up at approximately 7 kB gzipped, with zero runtime dependencies, making it suitable for memory-constrained environments such as mobile browsers.</p><p>The library&#8217;s developer emphasizes a space-optimized index structure that balances retrieval speed against storage overhead, avoiding the memory explosion common in na&#239;ve inverted-index implementations, much like the one <a href="https://tangled.org/desertthunder.dev/noteleaf/commit/2fc5eeaac410fd50c5badd24730e596681547e13">I wrote</a> this past week.</p><p>Alternative client-side libraries such as Lunr.js or Elasticlunr offer similar capabilities. Still with different performance profiles: Lunr trades some query speed for a more compact index, while Elasticlunr removes stemming (reducing a word to its root form) to reduce build complexity. MiniSearch&#8217;s distinguishing feature is its tunable ranking system, which exposes BM25-inspired scoring with per-field boosts and configurable fuzzy-match tolerance.</p><h2>Content Modeling &amp; Invariants</h2><p>The project leans on <code>astro:content</code> (Astro&#8217;s amazing content collection system) collections to guarantee that every document conforms to a known schema, with Zod for runtime validation. The content config file defines the fields for blog posts, bookmarks, projects, and pages, normalizing types such as <code>date</code>, <code>tags</code>, and <code>categories</code> before build time.</p><p>Because MiniSearch does not enforce schemas on the client, the quality of the index depends on the data discipline established by the user. Schema validation catches errors at build time rather than runtime, a property especially valuable when onboarding contributors or migrating legacy content. When a new post is created, it must match the schema; otherwise, Astro will throw an error, preventing malformed/invalid documents from entering the index.</p><p>Thanks to this front-loading of validation, the search system can assume structural integrity: every blog post will have a <code>title</code>, <code>description</code>, <code>date</code>, and <code>tags</code> array, eliminating defensive null checks in the indexing pipeline.</p><p>I explicitly mark <code>draft</code> fields on blog entries, capture URL metadata for bookmarks and projects, and expose optional Open Graph images on standalone pages. The search system later relies on these attributes to determine which documents are indexed and what metadata is shown in snippets. Establishing these invariants early simplifies downstream logic. </p><h2>Index Construction Pipeline</h2><p>The document index originates from <code>search-index.json</code>, an Astro API route that runs at build time and during development.</p><p>The handler invokes <code>getCollection</code> for each content type, filters out drafts, and converts markdown bodies into plaintext using a deterministic <code>stripMarkdown</code> helper. The helper removes headings, emphasis markers, inline code fences, and link syntax to ensure MiniSearch ingests only searchable prose. Every record in the resulting array is capped at 500 characters to keep the JSON payload compact.</p><p>The route exports <code>prerender = true</code>, instructing Astro to emit a static <code>/search-index.json</code> artifact during the production build. When running the dev server, the handler still executes dynamically, allowing me or any developer to create a new post, refresh the search panel, and immediately see the document without restarting the dev server. This dual-mode behavior ensures parity between development and production, eliminating the need for separate development &amp; production pipelines.</p><p>To guard against regressions, the indexer tags each document with a stable <code>id</code> that combines the collection name and slug (for example, <code>blog:mini-search-case-study</code>). This identifier later drives result enrichment on the client, allowing the UI to pull auxiliary metadata, such as tags from the original document array, even if MiniSearch returns only a subset of fields for ranking purposes.</p><h2>Field Boosting &amp; Relevance Scoring</h2><p>Boosts add another layer of control. The configuration doubles the weight of matches in <code>title</code>, sets a 1.5 multiplier on <code>description</code>, and leaves other fields at the default weight. These heuristics reflect user expectations: headlines should outrank incidental body matches and echo established search-engine practices.</p><p>The underlying mechanics draw from information retrieval research, specifically the BM25 ranking function that improves upon classical TF-IDF (Term Frequency-Inverse Document Frequency). Where TF-IDF naively multiplies term frequency by inverse document frequency, BM25 introduces term frequency saturation: once a document contains enough occurrences of a query term, additional mentions yield diminishing returns. This prevents keyword-stuffed documents from dominating results purely through repetition. The <code>k&#8321;</code> parameter controls the saturation curve; MiniSearch&#8217;s defaults align with standard search-engine tuning.</p><p>BM25 also normalizes for document length, addressing the bias toward longer texts in TF-IDF. A match in a short document often signals higher relevance than the same match buried in a 5000-word essay. By adjusting scores based on document length relative to the average corpus length, BM25 produces rankings that better match human judgment. Field boosting extends this framework by treating matches in high-value fields, such as titles, as if they appeared multiple times, effectively amplifying their contribution to the final score.</p><p>Fuzzy matching with a tolerance of 0.2 offers resilience to common typos without diluting precision, and prefix search ensures that partially typed terms still surface meaningful results. These features combine to create a forgiving search experience: users can type &#8220;progresive&#8221; instead of &#8220;progressive,&#8221; omit the final letters of a word mid-query, and still receive relevant hits.</p><h2>UI</h2><p>Two separate UI surfaces consume the same index.</p><p>The compact overlay is implemented in a <code>SearchBar</code> component and mounts as part of the global navigation. It fetches the index on first use, initializes MiniSearch once, and caches the resulting instance. Results appear immediately below the input, capped at ten entries to preserve readability inside the constrained panel. Each entry lists the title, an optional description snippet, the content type badge, and up to three tags. The dedicated search page reuses the same index but renders a paginated list with 12 results per page (I may make this configurable), explicit navigation controls, and richer snippets.</p><p>Because the page is route-level content, it also reads and stores state in the URL, with a query parameter to allow creation of shareable search links without additional server support. Both components guard against unnecessary hydration by running their initialization logic inside a document-level event listener.</p><p>Astro&#8217;s client-side router triggers this event whenever navigation occurs, so the search UI stays functional even during client-side transitions without re-downloading scripts. No component uses a <code>client:*</code> directive keeping the scripts inline, and the browser only executes them when the respective page is active.</p><h2>Document Enrichment &amp; Snippets</h2><p>While MiniSearch returns ranking data, it does not know which field triggered the match or how to craft usable snippets. Both UI implementations solve this by searching the original documents array for a record with the matching <code>id</code> and calling helper functions defined within the class.</p><ul><li><p><code>findMatchedField</code> tokenizes the query, checks the title, tags, description, and body in that order, and returns the first field containing a term. <br>This information informs whether the UI should display a description or a body excerpt.</p></li><li><p><code>extractMatchSnippet</code> takes the plaintext body text, locates the earliest occurrence of any query token, and returns approximately 60&#8211;100 characters on either side (depending on the surface). <br>If no term is found, the helper falls back to truncating the body to a fixed length.</p></li><li><p><code>highlightMatch</code> wraps each matched term in <code>&lt;mark&gt;</code> tags. <br>Paired with custom CSS in both files, this produces consistent visual emphasis without re-processing the HTML on the server.</p></li></ul><p>These helpers remain deterministic and pure. They also share identical logic between the overlay and the full page to avoid confusing users with mismatched results.</p><h2>Data Prep &amp; Error Handling</h2><p>The construction pipeline follows indexing best practices by configuring searchable fields before adding documents. Defining which fields contribute to ranking and which weights to apply, ahead of time, allows MiniSearch to optimize its internal data structures during initialization. The alternative, reconfiguring fields after ingestion, would require rebuilding the index from scratch.</p><p>Document batching also matters, though less so for static builds than for real-time indexing. Because the search index route runs during build time, all documents are processed in a single operation. If the system ever migrates to incremental static regeneration, batching updates into larger payloads will outperform many small HTTP requests. A single large batch is processed more quickly than multiple smaller ones due to reduced overhead from request parsing and response serialization.</p><p>To minimize layout thrash, all DOM updates happen through string templates assembled before assignment. Highlighting occurs before the markup is rendered, preventing partial renders. The overlay also limits its vertical space with scroll overflow handling, so it does not push other layout elements around.</p><p>Because the endpoint produces deterministic JSON, there are few moving parts, but the surfacing of load failures still matters during deployments or CDN incidents.</p><p>In development, I often log the number of indexed documents after MiniSearch loads, which provides a quick sanity check that the expected collections were captured.</p><h2>Performance</h2><p>A static JSON index plus a WASM-free search engine results in low overhead.</p><p>The indexer truncates bodies, reducing payload size. This is a practice recommended by search optimization guides that emphasize trimming unnecessary data to accelerate processing. Cached responses mean repeat searches do not refetch data, and both components reuse the same MiniSearch instance across interactions. Because everything stays in the browser, latency hinges on client CPU rather than network round-trips, which keeps search responsive even for users on slow connections once the index is downloaded.</p><h2>Pagination &amp; Accessibility</h2><p>The full search page integrates a small pagination module (<code>paginate</code> and <code>setupPaginationNavigation</code>). The first function slices the result array while returning metadata such as <code>hasNext</code>, <code>hasPrev</code>, and <code>lastPage</code>.</p><p>The second attaches click handlers to the &#8220;Previous&#8221; and &#8220;Next&#8221; buttons and keeps aria attributes in sync with the disabled state. When users navigate across pages, the class scrolls the viewport to the top for context continuity. These behaviors are implemented with minimal DOM mutations and no external dependencies.</p><p>Keyboard and screen-reader support come baked in. Inputs use descriptive placeholder text, results render as anchor elements for native focus management, and pagination controls expose aria-label attributes. The overlay includes logic to close itself when pressing Escape or clicking outside the panel, preventing inadvertent focus trapping.</p><h2>Extensibility</h2><p>Adding a new content collection requires two steps:</p><ol><li><p>Extend your content schema</p></li><li><p>Replicate the indexing loop inside the <code>search-index.json</code> route.</p></li></ol><p>It&#8217;s a good idea to keep naming conventions consistent to ensure the front-end helpers continue to work without modification.</p><p>If the new content introduces a different metadata taxonomy (for example, topics instead of tags), normalize it to the existing tags or categories arrays before serializing the document. MiniSearch configurations can evolve without rewriting the UI. For instance, adding field-specific fuzziness or custom tokenization can happen at instantiation time. However, any change that affects stored fields has to be mirrored in the result enrichment logic, or else the UI might request metadata that no longer exists in the index.</p><h2>Conclusion</h2><p>The search experience on this Astro site balances predictability and flexibility: it is deterministic at build time, lazy at runtime, and straightforward to extend. Adhering to Astro&#8217;s strict &amp; built-in content schemas, normalizing documents before serialization, and instantiating MiniSearch exactly once per surface eliminates many of the pitfalls usually associated with client-side search.</p><p>I think the resulting implementation is fast, index-driven, and maintainable without external services. Let me know in the comments if you have any feedback or questions!</p><h2>References</h2><p>MiniSearch documentation - <a href="https://lucaong.github.io/minisearch/">lucaong.github.io/minisearch</a> <br>Luca Ongaro, &#8220;MiniSearch, a client-side full-text search engine&#8221; - <a href="https://lucaongaro.eu/blog/2019/01/30/minisearch-client-side-fulltext-search-engine.html">lucaongaro.eu/blog</a><br>&#8220;Understanding TF-IDF and BM-25&#8221; - <a href="https://kmwllc.com/index.php/2020/03/20/understanding-tf-idf-and-bm-25/">kmwllc.com</a><br>&#8220;BM25 Explained: A Better Ranking Algorithm than TF-IDF&#8221; - <a href="https://vishwasg.dev/blog/2025/01/20/bm25-explained-a-better-ranking-algorithm-than-tf-idf/">vishwasg.dev/blog</a><br>&#8220;BM25 relevance scoring&#8221; - <a href="https://learn.microsoft.com/en-us/azure/search/index-similarity-and-scoring">Microsoft Learn</a> <br>&#8220;Search indexing best practices for top performance&#8221; - <a href="https://www.algolia.com/blog/engineering/search-indexing-best-practices-for-top-performance-with-code-samples">algolia.com/blog</a><br> Stemming | Elastic Docs. <a href="https://www.elastic.co/docs/manage-data/data-store/text-analysis/stemming">https://www.elastic.co/docs/manage-data/data-store/text-analysis/stemming</a> </p>]]></content:encoded></item><item><title><![CDATA[Graphs and Tarjan’s Algorithm]]></title><description><![CDATA[Untangling Python projects with Rust]]></description><link>https://patternmatched.substack.com/p/graphs-and-tarjans-algorithm</link><guid isPermaLink="false">https://patternmatched.substack.com/p/graphs-and-tarjans-algorithm</guid><dc:creator><![CDATA[Owais]]></dc:creator><pubDate>Thu, 13 Nov 2025 23:43:22 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!VG00!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fae263dd9-f68d-46aa-81e0-a9c6e31fae2c_3540x2216.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Graph theory provides a mathematical foundation for understanding structural relationships between entities. Directed graphs, in particular, model asymmetric relationships such as dependency, ownership, and communication. Within this framework, strong connectivity, &#8202;the property that every node in a subset can reach every other node, &#8202;plays a central role in reasoning about cycles, modularity, and causality. In graph theory, a subgraph is strongly connected if it is directed and if every vertex is reachable from every other, following the edge directions. It&#8217;s defined as weakly connected if it is undirected or if the only connections that exist for some vertex pairs go against the edge directions. </p><p></p><p>Tarjan&#8217;s algorithm is an elegant linear-time method to identify strongly connected components (SCCs) in directed graphs. It underpins essential infrastructure in compilers, static analyzers, and dependency management systems. In software analysis, directed graphs arise naturally from <em>imports</em>, <em>function calls</em>, and <em>data dependencies</em>. Recognizing cycles within these graphs is critical for incremental computation, correctness, and maintainability. </p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!VG00!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fae263dd9-f68d-46aa-81e0-a9c6e31fae2c_3540x2216.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!VG00!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fae263dd9-f68d-46aa-81e0-a9c6e31fae2c_3540x2216.png 424w, https://substackcdn.com/image/fetch/$s_!VG00!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fae263dd9-f68d-46aa-81e0-a9c6e31fae2c_3540x2216.png 848w, https://substackcdn.com/image/fetch/$s_!VG00!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fae263dd9-f68d-46aa-81e0-a9c6e31fae2c_3540x2216.png 1272w, https://substackcdn.com/image/fetch/$s_!VG00!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fae263dd9-f68d-46aa-81e0-a9c6e31fae2c_3540x2216.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!VG00!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fae263dd9-f68d-46aa-81e0-a9c6e31fae2c_3540x2216.png" width="728" height="455.5" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ae263dd9-f68d-46aa-81e0-a9c6e31fae2c_3540x2216.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;normal&quot;,&quot;height&quot;:911,&quot;width&quot;:1456,&quot;resizeWidth&quot;:728,&quot;bytes&quot;:351697,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://patternmatched.substack.com/i/178087253?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fae263dd9-f68d-46aa-81e0-a9c6e31fae2c_3540x2216.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!VG00!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fae263dd9-f68d-46aa-81e0-a9c6e31fae2c_3540x2216.png 424w, https://substackcdn.com/image/fetch/$s_!VG00!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fae263dd9-f68d-46aa-81e0-a9c6e31fae2c_3540x2216.png 848w, https://substackcdn.com/image/fetch/$s_!VG00!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fae263dd9-f68d-46aa-81e0-a9c6e31fae2c_3540x2216.png 1272w, https://substackcdn.com/image/fetch/$s_!VG00!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fae263dd9-f68d-46aa-81e0-a9c6e31fae2c_3540x2216.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Rust Implementation of Tarjan&#8217;s Algorithm (1/2)</figcaption></figure></div><p>Today I&#8217;d like to outline the algorithm&#8217;s structure and theoretical basis, then examine its application in the context of <a href="https://github.com/stormlightlabs/beacon">Beacon</a>, a Rust-based Python language server that I&#8217;ve been building. Beacon&#8217;s workspace module treats source files and imports as a dependency graph. By integrating Tarjan&#8217;s algorithm into this architecture, the system achieves deterministic incremental analysis and reliable cycle detection without resorting to ad hoc heuristics. It offers us a simple yet powerful way to partition such graphs into strongly connected components using a single depth-first search (DFS).</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!flCU!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91a452be-a15b-4576-9643-9f4abb264086_3940x2684.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!flCU!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91a452be-a15b-4576-9643-9f4abb264086_3940x2684.png 424w, https://substackcdn.com/image/fetch/$s_!flCU!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91a452be-a15b-4576-9643-9f4abb264086_3940x2684.png 848w, https://substackcdn.com/image/fetch/$s_!flCU!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91a452be-a15b-4576-9643-9f4abb264086_3940x2684.png 1272w, https://substackcdn.com/image/fetch/$s_!flCU!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91a452be-a15b-4576-9643-9f4abb264086_3940x2684.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!flCU!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91a452be-a15b-4576-9643-9f4abb264086_3940x2684.png" width="1456" height="992" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/91a452be-a15b-4576-9643-9f4abb264086_3940x2684.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:992,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:476063,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://patternmatched.substack.com/i/178087253?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91a452be-a15b-4576-9643-9f4abb264086_3940x2684.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!flCU!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91a452be-a15b-4576-9643-9f4abb264086_3940x2684.png 424w, https://substackcdn.com/image/fetch/$s_!flCU!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91a452be-a15b-4576-9643-9f4abb264086_3940x2684.png 848w, https://substackcdn.com/image/fetch/$s_!flCU!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91a452be-a15b-4576-9643-9f4abb264086_3940x2684.png 1272w, https://substackcdn.com/image/fetch/$s_!flCU!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91a452be-a15b-4576-9643-9f4abb264086_3940x2684.png 1456w" sizes="100vw"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Rust Implementation of Tarjan&#8217;s Algorithm (2/2)</figcaption></figure></div><h3>Theory</h3><h4>Strongly Connected Components</h4><blockquote><p>Given a directed graph G = (V, E), a strongly connected component (SCC) is a maximal subset C &#8838; V such that for any two vertices u, v &#8712; C, there are paths from u to v and from v to u. Collapsing each SCC into a single node produces a condensation graph, which is acyclic by definition. This transformation simplifies reasoning about dependency order and incremental updates.</p></blockquote><p>A directed graph can be thought of as a set of points connected by one-way arrows. A strongly connected component (SCC) is a group of points where you can follow the arrows in some direction and always find your way from any point in the group back to any other point. If you shrink each of these groups down to a single point, you get a simpler version of the graph called a condensation graph. This new graph never has loops that let you return to your starting point. It always moves forward. That property makes it easier to understand how parts of a system depend on each other or how changes can be mutated in order. In software systems, SCCs represent groups of mutually dependent modules, so detecting and collapsing these cycles allows systems to analyze or rebuild only what is necessary.</p><h4>The Algorithm</h4><p>Tarjan&#8217;s algorithm identifies SCCs with linear time complexity (O(V + E) &#8594; O(N)), through a single depth-first traversal. It maintains the state as:</p><ul><li><p><code>index[v]</code>: the order in which node (v) was discovered;</p></li><li><p><code>lowlink[v]</code>: the smallest discovery index reachable from (v), including back edges;</p></li><li><p>A <em>stack</em> to record the current search path.</p></li></ul><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!lZ3k!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdd422b96-88cf-4f44-847f-0b4551a26598_1200x800.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!lZ3k!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdd422b96-88cf-4f44-847f-0b4551a26598_1200x800.png 424w, https://substackcdn.com/image/fetch/$s_!lZ3k!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdd422b96-88cf-4f44-847f-0b4551a26598_1200x800.png 848w, https://substackcdn.com/image/fetch/$s_!lZ3k!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdd422b96-88cf-4f44-847f-0b4551a26598_1200x800.png 1272w, https://substackcdn.com/image/fetch/$s_!lZ3k!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdd422b96-88cf-4f44-847f-0b4551a26598_1200x800.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!lZ3k!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdd422b96-88cf-4f44-847f-0b4551a26598_1200x800.png" width="1200" height="800" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/dd422b96-88cf-4f44-847f-0b4551a26598_1200x800.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:800,&quot;width&quot;:1200,&quot;resizeWidth&quot;:1200,&quot;bytes&quot;:62522,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://patternmatched.substack.com/i/178087253?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdd422b96-88cf-4f44-847f-0b4551a26598_1200x800.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="" srcset="https://substackcdn.com/image/fetch/$s_!lZ3k!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdd422b96-88cf-4f44-847f-0b4551a26598_1200x800.png 424w, https://substackcdn.com/image/fetch/$s_!lZ3k!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdd422b96-88cf-4f44-847f-0b4551a26598_1200x800.png 848w, https://substackcdn.com/image/fetch/$s_!lZ3k!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdd422b96-88cf-4f44-847f-0b4551a26598_1200x800.png 1272w, https://substackcdn.com/image/fetch/$s_!lZ3k!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdd422b96-88cf-4f44-847f-0b4551a26598_1200x800.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>During traversal, each node is pushed onto the stack when it&#8217;s discovered. If we encounter an edge to a node already on the stack, it updates the <code>lowlink</code> value to the smaller index, recording a back edge. When <code>index[v] </code>and<code> lowlink[v] </code>are equal, the node is identified as the root of a strongly connected component; nodes are popped until that root is removed, forming a single SCC.</p><pre><code>procedure strongconnect(v):
    v.index = index
    v.lowlink = index
    index += 1
    stack.push(v)
    v.onStack = true

for each (v, w) in E:
        if w.index is undefined:
            strongconnect(w)
            v.lowlink = min(v.lowlink, w.lowlink)
        else if w.onStack:
            v.lowlink = min(v.lowlink, w.index)
    if v.lowlink == v.index:
        pop vertices from stack until v is popped
        output these as one SCC</code></pre><p>These SCCs can be generated in reverse topological order of the condensation graph. In other words, from components with no outgoing dependencies to those that depend on others.</p><p>This ordering is especially valuable in contexts such as dependency analysis and incremental scheduling, where processing components in <em>dependency-resolved order</em> ensures that each component&#8217;s prerequisites are handled before its own. In practice, this allows systems to update/recompute only the affected parts when changes occur, without reprocessing the entire graph. These features were paramount when traversing and analyzing Python projects with Beacon.</p><h3>Python Workspaces as Graphs</h3><p>A Python project can be interpreted as a directed graph where each <em>module</em> (file) is a node and each <code>import</code> statement forms a directed edge.</p><p>However, Python&#8217;s flexibility complicates this process. Imports can occur at runtime or conditionally within functions, and the distinction between source and stub files (<code>.pyi</code>) adds further complexity. A naive linear scan or heuristic ordering easily leads to inconsistent states or redundant reanalysis.</p><p><strong>Beacon</strong> addresses this challenge by constructing a <em>live dependency graph</em> that represents the workspace as it evolves. Every edit, file addition, or configuration change updates this graph. Tarjan&#8217;s algorithm sits at the center of that process.</p><h3>Beacon&#8217;s Implementation</h3><h4>Workspace Discovery and Graph Construction</h4><p>Beacon begins by traversing the workspace using the <code>ignore</code> crate, to honor <code>.gitignore</code> and virtual environment patterns. Each discovered Python file becomes a node represented by a <code>ModuleInfo</code> record containing its URI, logical module name, source root, and dependency set.</p><p>The system parses each file&#8217;s abstract syntax tree (AST) and extracts both top-level and nested <code>import</code> statements to form directed edges. These edges are stored in an instance of <code>DependencyGraph</code>, which maintains both forward (&#8220;imports&#8221;) and reverse (&#8220;imported by&#8221;) links for constant-time queries.</p><p>Because Beacon&#8217;s document manager also tracks unsaved buffers, the graph always reflects the current editor state rather than the last file-system snapshot.</p><h4>Running Tarjan&#8217;s Algorithm</h4><p>When the dependency graph changes&#8202; after edits, additions, or deletions, the system invokes a routine based on Tarjan&#8217;s algorithm, implemented as the <code>TarjanState</code> struct. The algorithm iterates through all nodes, assigning indices and lowlink values while maintaining an explicit stack.</p><p>Each discovered SCC is a set of URIs representing modules that form a cycle. Single-element SCCs correspond to acyclic modules; multi-element SCCs reveal circular imports. Rather than treating cycles as errors, Beacon uses them as analysis units: type checking and inference occur on a per-SCC basis to ensure internal consistency among mutually dependent files.</p><h4>Condensation and Topological Sorting</h4><p>Once SCCs are identified, Beacon constructs a <strong>condensation graph</strong> where each SCC becomes a node. Edges are reversed&#8202; (if <code>A</code> imports <code>B</code>, then the condensed graph includes <code>B &#8594; A</code>) so that topological sorting can proceed in dependency order.</p><p>Kahn&#8217;s algorithm is then used to produce an analysis schedule: modules without incoming edges are processed first, followed by those whose dependencies are satisfied. The analyzer consumes this order directly to guarantee deterministic results across runs.</p><h4>Incremental Re-analysis</h4><p>When a developer edits a file, the document manager re-parses only the edited module. The graph updates its edges, and Tarjan&#8217;s algorithm re-runs to identify affected SCCs. Downstream dependencies are discovered through reverse edges, limiting invalidation to directly dependent modules.</p><h4>Handling Stubs</h4><p>Python&#8217;s dynamic typing makes stubs (<code>.pyi</code> files) essential for static analysis. Beacon indexes these stubs alongside source files and assigns them module identities in the graph. Tarjan&#8217;s traversal captures relationships between implementations and their type stubs.</p><p>This means that a cycle spanning a stub and its source file remains visible as a single SCC, such that the analyzer maintains a coherent understanding of symbol origins. A shared <code>StubCache</code>, guarded by <code>RwLock</code> (<a href="https://doc.rust-lang.org/std/sync/struct.RwLock.html">link</a>), provides thread-safe access to these parsed artifacts during analysis.</p><h4>Algorithmic and Architectural Interplay</h4><p>Our integration of Tarjan&#8217;s algorithm demonstrates how a theoretically simple procedure can provide architectural clarity in a complex, concurrent system. Several examples of design patterns in practice emerge:</p><ol><li><p><strong>Separation of concerns<br></strong>The dependency graph handles edge management; Tarjan&#8217;s algorithm only classifies structure.</p></li><li><p><strong>Predictable invalidation</strong> <br>SCCs form natural recomputation boundaries.</p></li><li><p><strong>Algorithmic composability.</strong><br>Tarjan&#8217;s results feed directly into Kahn&#8217;s algorithm for topological order, yielding a complete pipeline from discovery to analysis scheduling.</p></li><li><p><strong>Concurrency awareness<br></strong>By keeping Tarjan&#8217;s DFS single-threaded but parallelizing parsing and indexing, Beacon maintains correctness without sacrificing performance.</p></li></ol><h3>Broader Implications</h3><p>Tarjan&#8217;s algorithm continues to find new life in systems engineering because its structure aligns with the needs of dynamic environments:</p><ul><li><p><strong>Incremental computation:</strong> SCCs define minimal recomputation units.</p></li><li><p><strong>Cycle detection:</strong> essential for dependency visualization and modularization.</p></li><li><p><strong>Deterministic ordering:</strong> condensation and topological sorting ensure reproducibility.</p></li></ul><p>Beacon&#8217;s case illustrates that these properties are not confined to compilers or static analyzers; they apply equally to live, concurrent systems where files change continuously. By expressing workspace state as a graph and analysis as traversal, Beacon aligns with a lineage of systems that rely on mathematical invariants for predictability and performance.</p><p>Tarjan&#8217;s algorithm exemplifies the enduring relevance of algorithmic theory in modern software tooling. Its capacity to decompose complex graphs into minimal, interdependent components provides both computational efficiency and conceptual clarity. In the case of Beacon, this algorithm transforms a Python workspace from a collection of files into a <em>structured dependency graph</em>. Each edit, import, or configuration change becomes an update to that graph, and Tarjan&#8217;s logic ensures the system remains consistent. The lesson extends beyond any specific implementation: stable, deterministic software often emerges not from new algorithms but from the disciplined application of old ones. Tarjan&#8217;s algorithm, through its balance of simplicity and rigor, remains one of the best examples of that principle.</p><ul><li><p>&#8220;Tarjan&#8217;s Strongly Connected Components Algorithm.&#8221; <em>Wikipedia</em>, 31 Oct. 2025. <em>Wikipedia</em>, <a href="https://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm">https://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm</a></p></li><li><p>Tarjan, Robert E., and Uri Zwick. &#8220;Finding Strong Components Using Depth-First Search.&#8221; arXiv, 2022. <em>DOI.org (Datacite)</em>, <a href="https://doi.org/10.48550/ARXIV.2201.07197.">https://doi.org/10.48550/ARXIV.2201.07197.</a></p></li></ul>]]></content:encoded></item><item><title><![CDATA[Testing Bubble Tea Interfaces]]></title><description><![CDATA[Noteleaf's hand-rolled TUI test harness]]></description><link>https://patternmatched.substack.com/p/testing-bubble-tea-interfaces</link><guid isPermaLink="false">https://patternmatched.substack.com/p/testing-bubble-tea-interfaces</guid><dc:creator><![CDATA[Owais]]></dc:creator><pubDate>Wed, 05 Nov 2025 23:13:35 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!L2mJ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb15c1dec-398e-4674-b125-b999b9005f2a_2048x1398.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Terminal user interfaces (TUIs) have a way of hiding their complexity behind a calm prompt. Under the surface, you&#8217;re juggling asynchronous updates, listeners for keyboard choreography, and a rendering engine that assumes a real terminal is close at hand. When I started building Noteleaf&#8217;s Bubble Tea interface, I quickly realized that the hard part wouldn&#8217;t be drawing UI elements with LipGloss or wiring in <a href="https://github.com/charmbracelet/bubbles%27">Bubbles</a>. The real challenge would be proving that the whole experience holds together as I expand the app&#8217;s feature set. Today I&#8217;m going to walk through the test infrastructure assembled for Noteleaf, the patterns leaned on when exercising Charmbracelet models, and the little lessons learned while keeping the feedback loop fast. Everything mentioned here is live in the <a href="https://github.com/stormlightlabs/noteleaf">repository</a> so that you can follow along in your editor or in the linked source code.</p><h3>Why Spend Time on TUI Tests?</h3><p>Before we dive into mechanics, it is worth replaying the motivation. Don&#8217;t worry, I&#8217;m not going to evangelize TDD. There are better <a href="https://www.obeythetestinggoat.com/pages/book.html#toc">resources</a> for that (&#128016;). TUIs are notoriously tricky to implement regression tests for. A single missed key event can strand users, and refactors that change layouts often break muscle memory. I needed to be confident that our task editor and data browsers survive quick refactors, and to be able to do so without launching a full terminal in every unit test. The answer was to focus on the Bubble Tea model layer. Bubble Tea already treats input, update logic, and rendering as pure(ish) functions. If we can drive those functions deterministically, we can catch regressions with unit-test speed.</p><h3>The Test Harness</h3><p>The heart of the setup lives in <a href="https://github.com/stormlightlabs/noteleaf/blob/main/internal/ui/test_utilities.go#L18">internal/ui/test_utilities.go</a>. We created <code>TUITestSuite</code>, a struct that encapsulates state for tests that require direct interaction with any <code>tea.Model</code>. This means no real terminal, no threads unless we want them, and no dependence on the default renderer. The suite captures every model update and view string, exposes helpers for sending keyboard messages, and tracks context-based deadlines so tests do not hang when something goes wrong.</p><p>The struct definition and setup code look like this:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!L2mJ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb15c1dec-398e-4674-b125-b999b9005f2a_2048x1398.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!L2mJ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb15c1dec-398e-4674-b125-b999b9005f2a_2048x1398.png 424w, https://substackcdn.com/image/fetch/$s_!L2mJ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb15c1dec-398e-4674-b125-b999b9005f2a_2048x1398.png 848w, https://substackcdn.com/image/fetch/$s_!L2mJ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb15c1dec-398e-4674-b125-b999b9005f2a_2048x1398.png 1272w, https://substackcdn.com/image/fetch/$s_!L2mJ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb15c1dec-398e-4674-b125-b999b9005f2a_2048x1398.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!L2mJ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb15c1dec-398e-4674-b125-b999b9005f2a_2048x1398.png" width="1456" height="994" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b15c1dec-398e-4674-b125-b999b9005f2a_2048x1398.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:994,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:276368,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://patternmatched.substack.com/i/178131996?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb15c1dec-398e-4674-b125-b999b9005f2a_2048x1398.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!L2mJ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb15c1dec-398e-4674-b125-b999b9005f2a_2048x1398.png 424w, https://substackcdn.com/image/fetch/$s_!L2mJ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb15c1dec-398e-4674-b125-b999b9005f2a_2048x1398.png 848w, https://substackcdn.com/image/fetch/$s_!L2mJ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb15c1dec-398e-4674-b125-b999b9005f2a_2048x1398.png 1272w, https://substackcdn.com/image/fetch/$s_!L2mJ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb15c1dec-398e-4674-b125-b999b9005f2a_2048x1398.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><p>You will notice a deliberate omission in the <code>SetupProgram </code>helper: We never call <code>tea.NewProgram</code>. Instead, in tests, we&#8217;ll run <code>Init</code>, track the returned command, and call <code>Update</code> manually inside <code>SendMessage</code> (in class <a href="https://guide.elm-lang.org/architecture/">TEA</a> fashion).</p><p>Bubble Tea models are pure enough that this keeps behavior faithful while giving tests the freedom to inject messages synchronously, suspend time, or inspect intermediate views without wrangling goroutines.</p><p>The harness also ships with a <code>ControlledOutput</code> writer and a dead-simple <code>ControlledInput</code> stub so we can capture renders when we need to assert on raw Lip Gloss strings.</p><p>Two small options make a big difference in practice.</p><ol><li><p><code>WithInitialSize</code> primes the model with a <em>tea.WindowSizeMsg</em>, which mirrors the behavior of real programs resizing themselves on startup.</p></li><li><p><code>WithTimeout</code> wraps the underlying context so that every <code>WaitFor</code> call shares a deterministic deadline.</p></li></ol><p>Both are defined alongside the suite in <a href="https://github.com/stormlightlabs/noteleaf/blob/main/internal/ui/test_utilities.go#L117-L131">test_utilities.go</a>, and they keep tests concise: we express the &#8220;terminal&#8221; dimensions we care about and let the harness enforce timeouts.</p><h3>Driving the Model</h3><p>Once the harness is in place, most tests read like transcripts of user flows. In <a href="https://github.com/stormlightlabs/noteleaf/blob/main/internal/ui/task_edit_interactive_test.go#L12-L181">internal/ui/task_edit_interactive_test.go</a>, we rehearse how a human edits a task, flips between priority schemes, and navigates the status picker. The suite provides <em>SendKey</em>, <code>SendKeyString</code>, and <code>SendMessage</code>, making it easy to express those flows.</p><p>A representative sequence from that file looks like this:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!ybpx!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0442bfb-e598-41ef-97f8-f476751397f7_1490x852.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!ybpx!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0442bfb-e598-41ef-97f8-f476751397f7_1490x852.png 424w, https://substackcdn.com/image/fetch/$s_!ybpx!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0442bfb-e598-41ef-97f8-f476751397f7_1490x852.png 848w, https://substackcdn.com/image/fetch/$s_!ybpx!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0442bfb-e598-41ef-97f8-f476751397f7_1490x852.png 1272w, https://substackcdn.com/image/fetch/$s_!ybpx!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0442bfb-e598-41ef-97f8-f476751397f7_1490x852.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!ybpx!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0442bfb-e598-41ef-97f8-f476751397f7_1490x852.png" width="1456" height="833" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b0442bfb-e598-41ef-97f8-f476751397f7_1490x852.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:833,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:152994,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://patternmatched.substack.com/i/178131996?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0442bfb-e598-41ef-97f8-f476751397f7_1490x852.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!ybpx!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0442bfb-e598-41ef-97f8-f476751397f7_1490x852.png 424w, https://substackcdn.com/image/fetch/$s_!ybpx!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0442bfb-e598-41ef-97f8-f476751397f7_1490x852.png 848w, https://substackcdn.com/image/fetch/$s_!ybpx!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0442bfb-e598-41ef-97f8-f476751397f7_1490x852.png 1272w, https://substackcdn.com/image/fetch/$s_!ybpx!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0442bfb-e598-41ef-97f8-f476751397f7_1490x852.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><p>This snippet highlights two philosophical decisions.</p><p>First, we keep model instances type-safe by casting inside the predicate rather than leaking internal state to the test harness.</p><p>Second, we wait for conditions instead of asserting immediately after a key press. Bubble Tea updates often fan out through commands, so giving the model a heartbeat to settle avoids race conditions without sleeping arbitrarily. The suite&#8217;s <code>WaitFor</code> helper polls the latest model snapshot every 10 milliseconds and respects the per-test context timeout. When conditionals remain false, the helper returns a wrapped error that is easy to debug. The same file uses <code>SimulateKeySequence</code> for longer workflows. By pairing key events with optional delays, we can mimic the rhythm of real navigation&#8202;&#8212;&#8202;for example, tabbing through fields and typing text before hitting <code>Enter</code> to persist a change. Because the harness records every intermediate model, assertions can target either the new view or the final state of the struct, whichever is more meaningful for the scenario under test.</p><h3>Layered Assertions and Helpers</h3><p>To keep assertions readable, we packaged a tiny helper set inside the same utility file. The <code>Expect.AssertViewContains</code> and <code>Expect.AssertModelState</code> helpers (see <a href="https://github.com/stormlightlabs/noteleaf/blob/main/internal/ui/test_utilities.go#L307-L335">internal/ui/test_utilities.go</a>) wrap the repetitive parts of checking the latest view or verifying an invariant on the current model.</p><p>These helpers lean on a shared string utility, <code>shared.ContainsString</code>, which normalizes text checks so we can write assertions without worrying about ANSI color sequences that Lip Gloss might add later.</p><p>We still write vanilla assertions when the outcome is obvious (for example, checking a boolean flag), but being able to say &#8220;assert that the view contains <em>x</em>&#8221; as a single line keeps the tests focused on behavior instead of mechanics. This also makes the intent obvious when reading failures in CI.</p><h3>Pure Model Tests Still Matter</h3><p>Not every test fires up the harness. For pure update logic, we lean on direct model instances, which keep the signal-to-noise ratio high and avoid unnecessary casts in the test body. The task editor tests in <a href="https://github.com/stormlightlabs/noteleaf/blob/main/internal/ui/task_edit_test.go#L81-L235%5C">internal/ui/task_edit_test.go</a> are a great example. In it, we build a fully initialized <code>taskEditModel</code>, feed it key messages by hand, and assert on struct fields immediately. Because Bubble Tea models implement the <code>tea.Model</code>interface, this style works in parallel with the harness-based tests. We tend to start with direct model tests to pin down deterministic rules (e.g., how <code>priorityMode</code> cycles when you press <code>m</code>). Then we add an integration-style harness test when we want to guarantee real key bindings stay wired up. This dual approach also pays off when Charmbracelet releases updates. Bubbles components like <code>textinput</code> or <code>help</code> occasionally change their internal focus behavior.</p><p>Our pure tests cover how we configure those components up front, while the harness-driven tests confirm the Bubble Tea glue code still reacts to real key events the way we expect.</p><h4>Exercising Bubbles Components</h4><p>Many of our models embed &#8220;bubbles.&#8221; The authentication form, for instance, uses two <code>textinput</code><em> </em>fields with custom placeholders, echo modes, and focus rules. Our authentication form in <a href="https://github.com/stormlightlabs/noteleaf/blob/main/internal/ui/auth_form_test.go#L1-L207">internal/ui/auth_form_test.go</a> shows how we verify those pieces without hitting the network. We initialize the model with or without a pre-filled handle, call <code>Init</code> to trigger the focus command Bubble Tea generates, and then lean on the harness to send keys:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!-ej1!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F35a01849-948f-48c8-96c5-7b357c18e605_1490x768.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!-ej1!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F35a01849-948f-48c8-96c5-7b357c18e605_1490x768.png 424w, https://substackcdn.com/image/fetch/$s_!-ej1!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F35a01849-948f-48c8-96c5-7b357c18e605_1490x768.png 848w, https://substackcdn.com/image/fetch/$s_!-ej1!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F35a01849-948f-48c8-96c5-7b357c18e605_1490x768.png 1272w, https://substackcdn.com/image/fetch/$s_!-ej1!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F35a01849-948f-48c8-96c5-7b357c18e605_1490x768.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!-ej1!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F35a01849-948f-48c8-96c5-7b357c18e605_1490x768.png" width="1456" height="750" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/35a01849-948f-48c8-96c5-7b357c18e605_1490x768.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:750,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:116663,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://patternmatched.substack.com/i/178131996?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F35a01849-948f-48c8-96c5-7b357c18e605_1490x768.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!-ej1!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F35a01849-948f-48c8-96c5-7b357c18e605_1490x768.png 424w, https://substackcdn.com/image/fetch/$s_!-ej1!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F35a01849-948f-48c8-96c5-7b357c18e605_1490x768.png 848w, https://substackcdn.com/image/fetch/$s_!-ej1!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F35a01849-948f-48c8-96c5-7b357c18e605_1490x768.png 1272w, https://substackcdn.com/image/fetch/$s_!-ej1!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F35a01849-948f-48c8-96c5-7b357c18e605_1490x768.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Because the harness records the entire model, we can inspect component state without exposing setters just for tests. We can also assert that locked fields stay read-only, that validation messages appear inside the render output, and that submission flags flip when we trigger the shortcuts advertised in the help bubble. The tests run in milliseconds yet cover the interactions most likely to break when we tweak key maps or swap in a different Theme function from LipGloss.</p><h4>Mocking Data Sources for Lists and Tables</h4><p>Our data browser elements (<code>DataList</code> and <code>DataTable</code>) fetch records asynchronously and support filtering, searching, and pagination. Rather than connecting to an external service every time (though, because we use SQLite, there are examples of using a test repository in the codebase), we provide in-memory mocks that satisfy the same interfaces. You can see these in <a href="https://github.com/stormlightlabs/noteleaf/blob/main/internal/ui/data_list_test.go#L1-L181">internal/ui/data_list_test.go</a> and <a href="https://github.com/stormlightlabs/noteleaf/blob/main/internal/ui/data_table_test.go#L1-L204">internal/ui/data_table_test.go</a>. The mocks let us specify canned responses, inject errors, and verify that the models respond by showing toasts or falling back to static rendering. When paired with the harness, we can even step through search flows: focus the search field, type the query, send/type <code>Enter</code>, and wait for the view to update with filtered rows. One happy side effect is that these tests double as documentation for our model interfaces. When someone needs to add a new sortable column, they can read the table tests and see exactly which methods are required on the record type.</p><h4>Handling Time, Commands, and Side Effects</h4><p>Bubble Tea commands (<code>tea.Cmd</code>) are the trickiest part of testing TUIs because they hide asynchronous work behind function closures. In our current models, most commands either quit the program or kick off a repository operation. The harness&#8217;s <code>executeCmd</code> function is intentionally minimal. Right now, it ignores commands unless a test stubs in specific behavior. That gives us complete control over when asynchronous work happens.</p><p>When a new model needs to assert on command results, we wrap the command in a test implementation that captures messages and pushes those messages back into the suite with <code>SendMessage</code>.</p><p>The key is that we never have to start a separate Bubble Tea program or juggle channels ourselves; the harness keeps the execution single-threaded unless we want to test concurrency that we&#8217;ve added.</p><p>Time-sensitive logic is handled the same way. We either pass a deterministic clock to the model or use <code>WaitFor</code> with generous deadlines.</p><p>The <em>WithTimeout</em> option protects us from infinite waits when a predicate is wrong, while still letting tests use <code>time.Sleep</code> for small delays when trying to simulate the cadence of a real user. This is something you can observe in video scripting tools like <a href="https://github.com/charmbracelet/vhs/">VHS</a> (also by charm!)</p><h4>Bringing Charm Libraries Together</h4><p><a href="https://charm.land">Charm</a> publishes more than Bubble Tea, and our tests keep those pieces aligned. We use the <code>help</code> bubble extensively to advertise key bindings, so our harness tests assert on strings like &#8220;<code>enter/ctrl+s: submit</code>&#8221; to guarantee the instructions stay accurate.</p><p>LipGloss powers color and layout, but the harness captures raw strings without ANSI sequences. That means our tests verify semantic content&#8202; like &#8202;titles, selected markers, and validation hints, without flaking because of styling tweaks. When we migrate to a new color palette, we expect the snapshots to stay valid because we are intentionally leaving styling assertions to higher-level smoke tests. LipGloss changes are fun; they should not break sensible tests.</p><h4>Tips for Extending the Suite</h4><p>If you are adding a new Bubble Tea model to Noteleaf, try this playbook:</p><ol><li><p>Start with pure model tests that drive <code>Update</code> directly. <br>They run fast and force you to think about state transitions explicitly.</p></li><li><p>Pull in <code>NewTUITestSuite</code> when you bind real key maps or need to verify window-size handling. Reach for <code>WaitFor</code> and <code>WaitForView</code> instead of custom polling to keep timeouts consistent.</p></li><li><p>Mock repositories and data sources instead of reaching into production packages. The existing mocks for tasks, list items, and table records are flexible enough to reuse or crib from.</p></li><li><p>Assert on intent, not styling. Focus on flags, indices, and the view&#8217;s output.</p></li></ol><h4>Wrapping Up</h4><p>Testing TUIs can feel daunting, but Bubble Tea&#8217;s functional core makes it possible to write expressive, reliable tests without firing up a terminal window. By centralizing the harness in <code>TUITestSuite</code>, leaning on Bubbles components&#8217; predictable APIs, and mocking the data layer, we get rapid feedback that matches real user behavior. The patterns above have scaled from the original task editor to authentication, data browsing, and beyond, and we expect them to keep paying dividends as we explore more of <a href="https://github.com/charmbracelet%5C">Charm</a>&#8217;s ecosystem.</p><p>If you are curious to see these ideas encoded in full, clone the <a href="https://github.com/stormlightlabs/noteleaf">repository</a> and explore the linked files; there is plenty more to borrow, extend, or remix for your own Bubble Tea projects.</p><div><hr></div><h3>teatest</h3><p>Charmbracelet quietly publishes a small experimental package called <a href="https://pkg.go.dev/github.com/charmbracelet/x/exp/teatest">teatest</a>, which aims to provide an official testing harness for Bubble Tea programs. It wraps<code>tea.Program</code> in a controlled environment that behaves like a headless terminal, similar in spirit to our internal <code>TUITestSuite</code>, but with a slightly different philosophy.</p><p>Where our approach isolates the <strong>model layer</strong> and drives it manually, <code>teatest</code> preserves the full Bubble Tea event loop. It spins up a <code>TestModel</code> process that runs a <code>tea.Program</code> under the hood, captures output frames, and exposes helpers for deterministic teardown and output comparison. This makes it a strong option to test rendering fidelity or program-level behavior rather than individual model transitions.</p><h4>What teatest Provides</h4><ul><li><p><strong>Program-level execution</strong>: It executes a complete Bubble Tea program in a virtual terminal, preserving the message queue and renderer.</p></li><li><p><strong>Golden-file testing:</strong> Helpers like <code>RequireEqualOutput</code><em> </em>make it possible to snapshot rendered output and compare it across commits.</p></li><li><p><strong>Timeouts and terminal sizing: </strong>Options such as <code>WithFinalTimeout</code> and <code>WithInitialTermSize</code><em> </em>help reproduce stable terminal environments.</p></li><li><p><strong>Final model capture:</strong> Once the program exits, <code>FinalModel</code> returns the last model state for inspection without reflection hacks.</p></li></ul><p>These features overlap with parts of our own harness but operate at a higher level, closer to how users experience the application.</p><h4>When to choose <code>teatest</code> vs. other harnesses</h4><ul><li><p>Use <code>teatest</code> when you care <em>not just</em> about internal model transitions but about full-program behavior: layout, rendered view, entire event loop.</p></li><li><p>Be aware that because it runs the full event loop (via <code>tea.Program</code>), tests can be heavier, less fine-grained, and potentially slower. The article you referenced even explains why a custom harness (model-only) was chosen instead.</p></li><li><p>If your need is &#8220;did the screen change as expected after a sequence of keys?&#8221; then <code>teatest</code> is a strong fit. If you need &#8220;Did the model change its internal flags correctly when I press these keys?&#8221;, a lighter harness may suffice.</p></li></ul><p>To adopt <code>teatest</code>, take a look at <a href="https://charm.land/blog/teatest/">this</a> article on Charm&#8217;s blog.</p><h4>Why We Chose to Roll Our Own (for Now)</h4><p><code>teatest</code> is elegant, but it carries trade-offs that didn&#8217;t fit Noteleaf&#8217;s priorities early on:</p><ul><li><p><strong>Experimental status:</strong> It lives in the <code>x/exp</code> namespace, meaning API stability is not guaranteed.</p></li><li><p><strong>Full-program semantics:</strong> Because it runs the entire event loop, tests tend to be heavier and less deterministic when commands trigger asynchronous effects.</p></li><li><p><strong>Output focus:</strong> The golden-file strategy excels at catching layout regressions, but we needed fine-grained state inspection and control over command timing.</p></li></ul><p>By contrast, our internal <code>TUITestSuite</code> treats models as pure state machines. There are no threads and no renderer letting us assert on intermediate states as often as we need. That design keeps the test cycle fast and predictable.</p><h4>When <code>teatest </code>Might Be the Better Fit</h4><p>If you are building a simpler TUI or care deeply about pixel-perfect layout regressions in menus, tables, or dashboards whose visual shape must not drift, <code>teatest</code> deserves a look. It will likely continue to grow as the de facto standard for verifying Bubble Tea applications as the Charm ecosystem converges around consistent (and more awesome) tooling.</p><div><hr></div><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://patternmatched.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading! Subscribe to receive new posts straight to your inbox and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Algebraic Data Types]]></title><description><![CDATA[An overview of ADTs with F# & OCaml]]></description><link>https://patternmatched.substack.com/p/algebraic-data-types</link><guid isPermaLink="false">https://patternmatched.substack.com/p/algebraic-data-types</guid><dc:creator><![CDATA[Owais]]></dc:creator><pubDate>Wed, 07 May 2025 04:04:03 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!_HRh!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F87c0d64d-6508-470a-8448-03a2e5e78520_990x500.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Introduction</h2><p>Algebraic Data Types (ADTs) form a cornerstone of type theory and functional programming languages, offering a means to construct robust type systems by giving authors the ability to compose complex data types from simpler ones. When we say algebraic, we mean to define operations between type constructors and algebraic operations. In other words, <em>product types</em> correspond to multiplication (Cartesian products of sets of values), while <em>sum types</em> correspond to addition (disjoint unions of sets of values). By using sums and products to treat types algebraically, domains can be clearly and expressively represented, and handled exhaustively through pattern matching, a control flow construct common in functional languages, particularly those that share roots in the ML syntax and even in languages like Gleam and Rust. This exhaustiveness allows leveraging strong static type systems (such as those based on Hindley-Milner type inference) to catch errors at compile time and ensure correctness.</p><h2>Foundations of ADTs: Sum and Product Types</h2><p>Let&#8217;s dive deeper into the two fundamental constructs of ADTs:</p><ol><li><p>Product Types</p></li><li><p>Sum Types</p></li></ol><h3>Product Types</h3><p>Product types combine multiple values, called fields, into a single composite type. In many languages these are tuples and records (seen most commonly as key value pairs). Using set theory, we say that the set of values that make up a product type is the Cartesian product of those values. For example, a 2-tuple type of an integer and a boolean can represent any ordered pair of an integer and true, as well as an integer and false.</p><h3>Sum Types</h3><p>Sum types are represented by a group of related types, each of which is called a constructor. A constructor is a named variant that can carry any number of values of any valid type. In many programming languages these are called enums, or unions. The total number of values for a sum type is the sum of all possible values of its variants, i.e. the disjoint union of the sets of values of its variants (in set theory).</p><p>By combining sums and products, developers can model intricate data invariants directly in the type system. Consider a commonly implemented data structure: a binary tree. Usually a node, or leaf in a tree, is represented by its data, and pointers to its children. There are two possible cases for a binary tree that we can represent for a tree, either empty, or a root node with two subtrees. The node itself can be a product type which demonstrates the relationship between a type definition and the shape of its data.</p><h2>Pattern Matching and Type Safety</h2><p>Any variant of a sum type (a constructor) and its combinations are valid patterns. We check values against possible patterns using one of functional programming&#8217;s most compelling and powerful features, made possible by ADTs: Pattern Matching. This allows us to deconstruct values of constructors, and then bind inner fields to names (think destructuring assignment or tuple unpacking in JavaScript, and Python, respectively).</p><p>What makes this construct so powerful?</p><ul><li><p>Exhaustiveness checking: The compiler warns if not all variants are handled, preventing runtime &#8220;match failure&#8221; errors, <em>ensuring correctness</em></p></li><li><p>Clarity of intent: Each pattern branch explicitly names the data shape it handles, <em>improving readability</em>.</p></li><li><p>Conciseness: Nested data can be deconstructed succinctly, <em>reducing boilerplate</em>.</p></li></ul><p>Let&#8217;s return to our binary tree from the previous section. When we match against variants of our tree type in OCaml for example, the compiler ensures that any additional variant forces compile time updates to every expression. This is a safety guarantee provided by the compiler that reminds authors to cover every case in every match.</p><h2>ADTs in OCaml</h2><p>Let&#8217;s dive deeper into OCaml&#8217;s support for ADTs. These are provided by variant types (sum types) and records (product types). Here&#8217;s an example of how we might represent a tree in OCaml:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!_HRh!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F87c0d64d-6508-470a-8448-03a2e5e78520_990x500.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!_HRh!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F87c0d64d-6508-470a-8448-03a2e5e78520_990x500.png 424w, https://substackcdn.com/image/fetch/$s_!_HRh!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F87c0d64d-6508-470a-8448-03a2e5e78520_990x500.png 848w, https://substackcdn.com/image/fetch/$s_!_HRh!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F87c0d64d-6508-470a-8448-03a2e5e78520_990x500.png 1272w, https://substackcdn.com/image/fetch/$s_!_HRh!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F87c0d64d-6508-470a-8448-03a2e5e78520_990x500.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!_HRh!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F87c0d64d-6508-470a-8448-03a2e5e78520_990x500.png" width="990" height="500" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/87c0d64d-6508-470a-8448-03a2e5e78520_990x500.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:500,&quot;width&quot;:990,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!_HRh!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F87c0d64d-6508-470a-8448-03a2e5e78520_990x500.png 424w, https://substackcdn.com/image/fetch/$s_!_HRh!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F87c0d64d-6508-470a-8448-03a2e5e78520_990x500.png 848w, https://substackcdn.com/image/fetch/$s_!_HRh!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F87c0d64d-6508-470a-8448-03a2e5e78520_990x500.png 1272w, https://substackcdn.com/image/fetch/$s_!_HRh!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F87c0d64d-6508-470a-8448-03a2e5e78520_990x500.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Here, 'a tree is what we call a <em>parametric ADT</em>, meaning it can hold values of any type 'a. The empty constructor carries no data, whereas Node carries a 3-tuple/triple: a value of type 'a and two subtrees of the same type. Using pattern matching, we can deconstruct the values to the following:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!RlSx!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F842a16b1-12e6-4392-8341-41327f6e7e7c_1404x500.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!RlSx!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F842a16b1-12e6-4392-8341-41327f6e7e7c_1404x500.png 424w, https://substackcdn.com/image/fetch/$s_!RlSx!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F842a16b1-12e6-4392-8341-41327f6e7e7c_1404x500.png 848w, https://substackcdn.com/image/fetch/$s_!RlSx!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F842a16b1-12e6-4392-8341-41327f6e7e7c_1404x500.png 1272w, https://substackcdn.com/image/fetch/$s_!RlSx!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F842a16b1-12e6-4392-8341-41327f6e7e7c_1404x500.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!RlSx!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F842a16b1-12e6-4392-8341-41327f6e7e7c_1404x500.png" width="1404" height="500" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/842a16b1-12e6-4392-8341-41327f6e7e7c_1404x500.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:500,&quot;width&quot;:1404,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!RlSx!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F842a16b1-12e6-4392-8341-41327f6e7e7c_1404x500.png 424w, https://substackcdn.com/image/fetch/$s_!RlSx!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F842a16b1-12e6-4392-8341-41327f6e7e7c_1404x500.png 848w, https://substackcdn.com/image/fetch/$s_!RlSx!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F842a16b1-12e6-4392-8341-41327f6e7e7c_1404x500.png 1272w, https://substackcdn.com/image/fetch/$s_!RlSx!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F842a16b1-12e6-4392-8341-41327f6e7e7c_1404x500.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Records (called structs in languages like Rust and C++) provide named fields, improving self&#8208;documentation:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!-yIc!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd2c909a9-bf68-4c60-85ea-a4e20ee5f36d_1214x628.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!-yIc!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd2c909a9-bf68-4c60-85ea-a4e20ee5f36d_1214x628.png 424w, https://substackcdn.com/image/fetch/$s_!-yIc!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd2c909a9-bf68-4c60-85ea-a4e20ee5f36d_1214x628.png 848w, https://substackcdn.com/image/fetch/$s_!-yIc!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd2c909a9-bf68-4c60-85ea-a4e20ee5f36d_1214x628.png 1272w, https://substackcdn.com/image/fetch/$s_!-yIc!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd2c909a9-bf68-4c60-85ea-a4e20ee5f36d_1214x628.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!-yIc!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd2c909a9-bf68-4c60-85ea-a4e20ee5f36d_1214x628.png" width="1214" height="628" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d2c909a9-bf68-4c60-85ea-a4e20ee5f36d_1214x628.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:628,&quot;width&quot;:1214,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!-yIc!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd2c909a9-bf68-4c60-85ea-a4e20ee5f36d_1214x628.png 424w, https://substackcdn.com/image/fetch/$s_!-yIc!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd2c909a9-bf68-4c60-85ea-a4e20ee5f36d_1214x628.png 848w, https://substackcdn.com/image/fetch/$s_!-yIc!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd2c909a9-bf68-4c60-85ea-a4e20ee5f36d_1214x628.png 1272w, https://substackcdn.com/image/fetch/$s_!-yIc!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd2c909a9-bf68-4c60-85ea-a4e20ee5f36d_1214x628.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>This definition models geometric shapes with explicit field names. OCaml&#8217;s compiler enforces exhaustive matching on these variants, ensuring that any new shape variant must be accounted for in all uses of a match statement&#65532;.</p><h3>Parametric Polymorphism and Functoriality</h3><p>OCaml&#8217;s ADTs support polymorphism, allowing definitions to be generic over types (e.g., the &#8216;a parameter in 'a tree). This parametricity allows for code reuse: the same tree&#8208;processing functions work for trees containing any node type, like integer trees, string trees, or even complex user&#8208;defined types (record or tuples!). Under the hood, the Hindley&#8211;Milner type system infers the most general types for functions operating on ADTs, sparing the programmer from repetitive annotations and preserving type safety.</p><h2>ADTs in F#</h2><p>F# mirrors OCaml&#8217;s ADT capabilities via discriminated unions, coupled with records for product types. In fact F# is often called Microsoft/.NET&#8217;s version of OCaml.</p><p>A binary tree in F# can be defined as:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!LZ7u!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F58158d8a-0f31-426e-a878-b3bbf61e473e_1056x542.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!LZ7u!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F58158d8a-0f31-426e-a878-b3bbf61e473e_1056x542.png 424w, https://substackcdn.com/image/fetch/$s_!LZ7u!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F58158d8a-0f31-426e-a878-b3bbf61e473e_1056x542.png 848w, https://substackcdn.com/image/fetch/$s_!LZ7u!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F58158d8a-0f31-426e-a878-b3bbf61e473e_1056x542.png 1272w, https://substackcdn.com/image/fetch/$s_!LZ7u!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F58158d8a-0f31-426e-a878-b3bbf61e473e_1056x542.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!LZ7u!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F58158d8a-0f31-426e-a878-b3bbf61e473e_1056x542.png" width="1056" height="542" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/58158d8a-0f31-426e-a878-b3bbf61e473e_1056x542.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:542,&quot;width&quot;:1056,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!LZ7u!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F58158d8a-0f31-426e-a878-b3bbf61e473e_1056x542.png 424w, https://substackcdn.com/image/fetch/$s_!LZ7u!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F58158d8a-0f31-426e-a878-b3bbf61e473e_1056x542.png 848w, https://substackcdn.com/image/fetch/$s_!LZ7u!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F58158d8a-0f31-426e-a878-b3bbf61e473e_1056x542.png 1272w, https://substackcdn.com/image/fetch/$s_!LZ7u!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F58158d8a-0f31-426e-a878-b3bbf61e473e_1056x542.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>This declaration is semantically identical to the OCaml variant. Pattern matching in F# uses the <code>match&#8230;with</code> construct:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!sZPG!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F662274df-9d55-46bf-ae4c-133722dcd245_1076x584.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!sZPG!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F662274df-9d55-46bf-ae4c-133722dcd245_1076x584.png 424w, https://substackcdn.com/image/fetch/$s_!sZPG!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F662274df-9d55-46bf-ae4c-133722dcd245_1076x584.png 848w, https://substackcdn.com/image/fetch/$s_!sZPG!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F662274df-9d55-46bf-ae4c-133722dcd245_1076x584.png 1272w, https://substackcdn.com/image/fetch/$s_!sZPG!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F662274df-9d55-46bf-ae4c-133722dcd245_1076x584.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!sZPG!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F662274df-9d55-46bf-ae4c-133722dcd245_1076x584.png" width="1076" height="584" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/662274df-9d55-46bf-ae4c-133722dcd245_1076x584.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:584,&quot;width&quot;:1076,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!sZPG!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F662274df-9d55-46bf-ae4c-133722dcd245_1076x584.png 424w, https://substackcdn.com/image/fetch/$s_!sZPG!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F662274df-9d55-46bf-ae4c-133722dcd245_1076x584.png 848w, https://substackcdn.com/image/fetch/$s_!sZPG!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F662274df-9d55-46bf-ae4c-133722dcd245_1076x584.png 1272w, https://substackcdn.com/image/fetch/$s_!sZPG!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F662274df-9d55-46bf-ae4c-133722dcd245_1076x584.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>For records and discriminated unions with named fields, F# allows:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!cBQQ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F11d690cf-299d-4223-962e-f326093d5fac_1388x584.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!cBQQ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F11d690cf-299d-4223-962e-f326093d5fac_1388x584.png 424w, https://substackcdn.com/image/fetch/$s_!cBQQ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F11d690cf-299d-4223-962e-f326093d5fac_1388x584.png 848w, https://substackcdn.com/image/fetch/$s_!cBQQ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F11d690cf-299d-4223-962e-f326093d5fac_1388x584.png 1272w, https://substackcdn.com/image/fetch/$s_!cBQQ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F11d690cf-299d-4223-962e-f326093d5fac_1388x584.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!cBQQ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F11d690cf-299d-4223-962e-f326093d5fac_1388x584.png" width="1388" height="584" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/11d690cf-299d-4223-962e-f326093d5fac_1388x584.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:584,&quot;width&quot;:1388,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!cBQQ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F11d690cf-299d-4223-962e-f326093d5fac_1388x584.png 424w, https://substackcdn.com/image/fetch/$s_!cBQQ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F11d690cf-299d-4223-962e-f326093d5fac_1388x584.png 848w, https://substackcdn.com/image/fetch/$s_!cBQQ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F11d690cf-299d-4223-962e-f326093d5fac_1388x584.png 1272w, https://substackcdn.com/image/fetch/$s_!cBQQ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F11d690cf-299d-4223-962e-f326093d5fac_1388x584.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Again we see that pattern matching can extract fields by name:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!quOf!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9b020b12-b704-461d-8a09-c721354b3783_1316x670.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!quOf!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9b020b12-b704-461d-8a09-c721354b3783_1316x670.png 424w, https://substackcdn.com/image/fetch/$s_!quOf!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9b020b12-b704-461d-8a09-c721354b3783_1316x670.png 848w, https://substackcdn.com/image/fetch/$s_!quOf!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9b020b12-b704-461d-8a09-c721354b3783_1316x670.png 1272w, https://substackcdn.com/image/fetch/$s_!quOf!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9b020b12-b704-461d-8a09-c721354b3783_1316x670.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!quOf!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9b020b12-b704-461d-8a09-c721354b3783_1316x670.png" width="1316" height="670" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9b020b12-b704-461d-8a09-c721354b3783_1316x670.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:670,&quot;width&quot;:1316,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!quOf!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9b020b12-b704-461d-8a09-c721354b3783_1316x670.png 424w, https://substackcdn.com/image/fetch/$s_!quOf!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9b020b12-b704-461d-8a09-c721354b3783_1316x670.png 848w, https://substackcdn.com/image/fetch/$s_!quOf!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9b020b12-b704-461d-8a09-c721354b3783_1316x670.png 1272w, https://substackcdn.com/image/fetch/$s_!quOf!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9b020b12-b704-461d-8a09-c721354b3783_1316x670.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>FSharp&#8217;s discriminated unions also support recursive definitions, union cases with multiple fields, and can model complex hierarchies without boilerplate classes or inheritance.</p><h3>Enhancements: Active Patterns and Union Case Testing</h3><p>Beyond basic ADTs, F# introduces active patterns, allowing custom deconstruction logic for values that may not be direct ADTs (e.g., strings or numbers). Active patterns further generalize pattern matching and maintain ADT&#8208;like clarity across diverse data sources. Additionally, the<code> :? </code>operator enables union case testing in object&#8208;oriented contexts, bridging ADTs with .NET interoperability. Note that F# is not a functional language, just a functional-<em>first</em> programming language.</p><h3>Comparative Discussion</h3><p>OCaml and F# share the ML-syntax lineage and nearly identical ADT semantics, whereby each language leverages ADTs to eliminate a wide class of runtime errors and to encode domain logic directly in type definitions. This encourages a design style where correctness emerges from the type checker rather than from comprehensive test suites alone. However, there are subtle differences:</p><ul><li><p>Syntax: OCaml uses type ... = &#8230; with constructors preceded by |, whereas F# uses a similar syntax but encourages naming union fields directly.</p></li><li><p>Tooling: F# benefits from the .NET ecosystem, integrating seamlessly with C# libraries and Visual Studio tooling.</p><ul><li><p>OCaml&#8217;s ecosystem is more language&#8208;centric, with jbuilder/dune and opam managing builds and packages.</p></li></ul></li><li><p>Interoperability: F# ADTs compile to corresponding .NET types, which can be consumed by other CLR languages, sometimes requiring attribute annotations. In contrast OCaml ADTs compile to native code optimized for performance in different runtimes. Both languages have alternative compilers that allow compiling to JavaScript with Fable for F# and tools like Melange for OCaml (and js_of_ocaml).</p></li></ul><h2>Parametric ADTs and GADTs</h2><p>Generalized Algebraic Data Types (abbreviated GADTs) extend ADTs by allowing constructors to specify more precise return types, enabling richer invariants and encoding of logical constraints in types. In OCaml, GADT syntax uses type ... = with explicit return type annotations, seen below:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!R0Ot!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba88bc52-8fdc-406a-9cd3-5e70ae5c6111_1316x628.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!R0Ot!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba88bc52-8fdc-406a-9cd3-5e70ae5c6111_1316x628.png 424w, https://substackcdn.com/image/fetch/$s_!R0Ot!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba88bc52-8fdc-406a-9cd3-5e70ae5c6111_1316x628.png 848w, https://substackcdn.com/image/fetch/$s_!R0Ot!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba88bc52-8fdc-406a-9cd3-5e70ae5c6111_1316x628.png 1272w, https://substackcdn.com/image/fetch/$s_!R0Ot!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba88bc52-8fdc-406a-9cd3-5e70ae5c6111_1316x628.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!R0Ot!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba88bc52-8fdc-406a-9cd3-5e70ae5c6111_1316x628.png" width="1316" height="628" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ba88bc52-8fdc-406a-9cd3-5e70ae5c6111_1316x628.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:628,&quot;width&quot;:1316,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!R0Ot!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba88bc52-8fdc-406a-9cd3-5e70ae5c6111_1316x628.png 424w, https://substackcdn.com/image/fetch/$s_!R0Ot!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba88bc52-8fdc-406a-9cd3-5e70ae5c6111_1316x628.png 848w, https://substackcdn.com/image/fetch/$s_!R0Ot!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba88bc52-8fdc-406a-9cd3-5e70ae5c6111_1316x628.png 1272w, https://substackcdn.com/image/fetch/$s_!R0Ot!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba88bc52-8fdc-406a-9cd3-5e70ae5c6111_1316x628.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Here, the type parameter of <em>expr</em> can vary with each constructor, allowing heterogeneous expression trees that preserve type correctness at compile time. Pattern matching on GADTs refines type information within branches, avoiding unsafe casts and facilitating advanced type&#8208;driven programming.</p><p>F# also provides GADT&#8208;like functionality via inline functions and the static resolution of type parameters. Researchers continue to explore seamless GADT integration in F# as part of language evolution efforts.</p><h2>Applications and Benefits</h2><p>ADTs shine in scenarios requiring:</p><ul><li><p>Domain modeling: Encoding business rules and workflows as sum&#8208;of&#8208;product types, such as HTTP response status codes, ASTs for compilers, or finite state machines.</p></li><li><p>Error handling: Representing success/failure cases explicitly via Result&lt;'T,'Error&gt; or custom error unions, avoiding unchecked exceptions.</p></li><li><p>Configuration and parsing: Defining precise schemas for configuration files or network protocols, with the compiler ensuring exhaustive parsing logic. If you&#8217;re familiar with the technique of using recursive descent for parsing syntax trees, you&#8217;ll find that matching against collections of tokens allows for a clean, non-regex based traversal.</p></li><li><p>Data transformations: Building safe, type&#8208;checked pipelines where each stage maps one ADT to another, guaranteeing all cases are addressed.</p></li></ul><p>The algebraic nature of types also aligns with equational reasoning, enabling formal verification techniques and equational shortcuts in refactoring.</p><h2>Conclusion</h2><p>Algebraic Data Types represent a powerful abstraction in typed functional programming, seamlessly combining sum and product constructs to model complex domains. OCaml and F# both offer first&#8208;class support for ADTs&#8212;via variants and discriminated unions respectively&#8212;paired with exhaustive pattern matching and robust type inference. Advanced extensions such as GADTs further blur the line between types and values, unlocking expressive design patterns and compile&#8208;time guarantees. As software systems grow in complexity and correctness becomes paramount, ADTs stand out as an essential tool in the functional programmer&#8217;s toolkit, fostering concise, safe, and declarative code.</p><h2>Summary</h2><ul><li><p>ADTs blend product types (records, tuples) and sum types (variants, unions) to model complex data</p></li><li><p>Pattern matching on ADTs enforces exhaustive handling and clear deconstruction of data &#65532;</p></li><li><p>OCaml defines ADTs via type ... = | ... and supports parametric polymorphism and GADTs natively &#65532; &#65532;</p></li><li><p>F# uses discriminated unions and records, integrates with .NET, and offers advanced features like active patterns &#65532; &#65532;</p></li><li><p>GADTs extend ADTs by refining constructor return types, enabling richer invariants and type&#8208;driven design &#65532;</p></li><li><p>Benefits include safer domain modeling, exhaustive error handling, and support for formal reasoning and verification</p></li></ul><h2>References</h2><ul><li><p>Clarkson, Michael R. . &#8220;3.9. Algebraic Data Types &#8212; OCaml Programming: Correct + Efficient + Beautiful.&#8221; Accessed May 1, 2025. https://cs3110.github.io/textbook/chapters/data/algebraic_data_types.html.</p></li><li><p>Madhavapeddy, Anil, Yaron Minsky, and Yaron Minsky. Real World OCaml: Functional Programming for the Masses. Second edition. Cambridge, United Kingdom New York, NY: Cambridge University Press, 2022.</p></li><li><p>W, Scott. &#8220;Algebraic Type Sizes and Domain Modelling | F# for Fun and Profit.&#8221; Accessed May 6, 2025. https://fsharpforfunandprofit.com/posts/type-size-and-design/.</p></li><li><p>Chrichton, Will. &#8220;CS 242: Algebraic Data Types.&#8221; Accessed May 1, 2025. https://stanford-cs242.github.io/f19/lectures/03-2-algebraic-data-types.html.</p></li><li><p>Dollard, Kathleen. &#8220;Pattern Matching - F#.&#8221; Accessed May 2, 2025. https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/pattern-matching.</p></li></ul>]]></content:encoded></item></channel></rss>