<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.3.4">Jekyll</generator><link href="https://modulovalue.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://modulovalue.com/" rel="alternate" type="text/html" /><updated>2026-07-28T14:03:48+02:00</updated><id>https://modulovalue.com/feed.xml</id><title type="html">modulovalue</title><subtitle>A blog by Modestas Valauskas.</subtitle><entry><title type="html">Proof types in Dart: Using final classes as computational witnesses</title><link href="https://modulovalue.com/blog/proof-types-in-dart/" rel="alternate" type="text/html" title="Proof types in Dart: Using final classes as computational witnesses" /><published>2026-07-16T10:00:00+02:00</published><updated>2026-07-16T10:00:00+02:00</updated><id>https://modulovalue.com/blog/proof-types-in-dart</id><content type="html" xml:base="https://modulovalue.com/blog/proof-types-in-dart/"><![CDATA[<p>Here is a pattern that Dart 3.0 made possible:</p>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">final</span> <span class="kd">class</span> <span class="nc">EmailValidated</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="n">EmailValidated</span><span class="o">.</span><span class="na">_</span><span class="p">();</span>
<span class="p">}</span>

<span class="n">EmailValidated</span><span class="o">?</span> <span class="n">validateEmail</span><span class="p">(</span><span class="kt">String</span> <span class="n">email</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">_isValidEmail</span><span class="p">(</span><span class="n">email</span><span class="p">))</span> <span class="p">{</span>
    <span class="k">return</span> <span class="kd">const</span> <span class="n">EmailValidated</span><span class="o">.</span><span class="na">_</span><span class="p">();</span>
  <span class="p">}</span>
  <span class="k">return</span> <span class="kc">null</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>If you have an instance of <code class="language-plaintext highlighter-rouge">EmailValidated</code>, the validation must have occurred. Nothing outside this library can produce an <code class="language-plaintext highlighter-rouge">EmailValidated</code> without going through <code class="language-plaintext highlighter-rouge">validateEmail</code>. The type system enforces it.</p>

<p>I call these proof types. Others call them witness types or evidence types. Whatever the name, the idea is the same: the existence of a value proves that a computation happened.</p>

<h2 id="table-of-contents">Table of Contents</h2>

<ul>
  <li><a href="#why-this-works">Why This Works</a></li>
  <li><a href="#a-more-complete-example">A More Complete Example</a></li>
  <li><a href="#carrying-data-with-proofs">Carrying Data with Proofs</a></li>
  <li><a href="#authorization-checks">Authorization Checks</a></li>
  <li><a href="#what-about-other-languages">What About Other Languages?</a></li>
  <li><a href="#parse-dont-validate">Parse, Don't Validate</a></li>
  <li><a href="#the-curry-howard-correspondence">The Curry-Howard Correspondence</a></li>
  <li><a href="#when-to-use-proof-types">When to Use Proof Types</a></li>
  <li><a href="#limitations">Limitations</a></li>
  <li><a href="#conclusion">Conclusion</a></li>
  <li><a href="#addendum">Addendum</a></li>
</ul>

<h2 id="why-this-works">Why This Works</h2>

<p>Four things combine to make this possible:</p>

<ol>
  <li><code class="language-plaintext highlighter-rouge">final</code> prevents the class from being extended, implemented, or mixed in outside its library (<a href="/blog/understanding-dart-class-modifiers-lattices/">see my post on class modifiers</a>)</li>
  <li>A library-private constructor (<code class="language-plaintext highlighter-rouge">._()</code>) prevents instantiation outside the library</li>
  <li>Dart's library-based privacy means these restrictions are absolute, not advisory</li>
  <li>A sound type system (both static and dynamic) ensures types cannot be forged. Dart's unsound constructs (like <code class="language-plaintext highlighter-rouge">as</code> casts) fail loudly at runtime rather than silently producing invalid values. You cannot accidentally end up with a fake <code class="language-plaintext highlighter-rouge">EmailValidated</code>.</li>
</ol>

<p>The <em>only</em> way to obtain an <code class="language-plaintext highlighter-rouge">EmailValidated</code> instance, then, is to call <code class="language-plaintext highlighter-rouge">validateEmail()</code> with an input that passes validation.</p>

<h2 id="a-more-complete-example">A More Complete Example</h2>

<p>Let's build a user registration system where the type system enforces that all checks have passed:</p>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// registration_proofs.dart</span>

<span class="c1">/// Proof that an email address has been validated.</span>
<span class="kd">final</span> <span class="kd">class</span> <span class="nc">EmailChecked</span> <span class="p">{</span>
  <span class="kd">final</span> <span class="kt">String</span> <span class="n">email</span><span class="p">;</span>
  <span class="kd">const</span> <span class="n">EmailChecked</span><span class="o">.</span><span class="na">_</span><span class="p">(</span><span class="k">this</span><span class="o">.</span><span class="na">email</span><span class="p">);</span>
<span class="p">}</span>

<span class="c1">/// Proof that a password meets strength requirements.</span>
<span class="kd">final</span> <span class="kd">class</span> <span class="nc">PasswordChecked</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="n">PasswordChecked</span><span class="o">.</span><span class="na">_</span><span class="p">();</span>
<span class="p">}</span>

<span class="c1">/// Proof that the terms of service were accepted.</span>
<span class="kd">final</span> <span class="kd">class</span> <span class="nc">TermsAccepted</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="n">TermsAccepted</span><span class="o">.</span><span class="na">_</span><span class="p">();</span>
<span class="p">}</span>

<span class="c1">/// Proof that all registration requirements have been met.</span>
<span class="kd">final</span> <span class="kd">class</span> <span class="nc">RegistrationReady</span> <span class="p">{</span>
  <span class="kd">final</span> <span class="kt">String</span> <span class="n">email</span><span class="p">;</span>
  <span class="kd">const</span> <span class="n">RegistrationReady</span><span class="o">.</span><span class="na">_</span><span class="p">(</span><span class="k">this</span><span class="o">.</span><span class="na">email</span><span class="p">);</span>
<span class="p">}</span>

<span class="n">EmailChecked</span><span class="o">?</span> <span class="n">checkEmail</span><span class="p">(</span><span class="kt">String</span> <span class="n">email</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">final</span> <span class="n">regex</span> <span class="o">=</span> <span class="n">RegExp</span><span class="p">(</span><span class="sx">r'^[^@]+@[^@]+\.[^@]+$'</span><span class="p">);</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">regex</span><span class="o">.</span><span class="na">hasMatch</span><span class="p">(</span><span class="n">email</span><span class="p">))</span> <span class="p">{</span>
    <span class="k">return</span> <span class="n">EmailChecked</span><span class="o">.</span><span class="na">_</span><span class="p">(</span><span class="n">email</span><span class="p">);</span>
  <span class="p">}</span>
  <span class="k">return</span> <span class="kc">null</span><span class="p">;</span>
<span class="p">}</span>

<span class="n">PasswordChecked</span><span class="o">?</span> <span class="n">checkPassword</span><span class="p">(</span><span class="kt">String</span> <span class="n">password</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">password</span><span class="o">.</span><span class="na">length</span> <span class="p">&gt;</span><span class="o">=</span> <span class="mi">12</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="kd">const</span> <span class="n">PasswordChecked</span><span class="o">.</span><span class="na">_</span><span class="p">();</span>
  <span class="p">}</span>
  <span class="k">return</span> <span class="kc">null</span><span class="p">;</span>
<span class="p">}</span>

<span class="n">TermsAccepted</span><span class="o">?</span> <span class="n">checkTermsAccepted</span><span class="p">(</span><span class="kt">bool</span> <span class="n">accepted</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">accepted</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="kd">const</span> <span class="n">TermsAccepted</span><span class="o">.</span><span class="na">_</span><span class="p">();</span>
  <span class="p">}</span>
  <span class="k">return</span> <span class="kc">null</span><span class="p">;</span>
<span class="p">}</span>

<span class="n">RegistrationReady</span><span class="o">?</span> <span class="n">prepareRegistration</span><span class="p">(</span>
  <span class="n">EmailChecked</span> <span class="n">email</span><span class="p">,</span>
  <span class="n">PasswordChecked</span> <span class="n">password</span><span class="p">,</span>
  <span class="n">TermsAccepted</span> <span class="n">terms</span><span class="p">,</span>
<span class="p">)</span> <span class="p">{</span>
  <span class="c1">// All checks have provably passed. We can trust these values.</span>
  <span class="k">return</span> <span class="n">RegistrationReady</span><span class="o">.</span><span class="na">_</span><span class="p">(</span><span class="n">email</span><span class="o">.</span><span class="na">email</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Now the registration function:</p>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// registration_service.dart</span>
<span class="kn">import</span> <span class="s">'registration_proofs.dart'</span><span class="o">;</span>

<span class="kt">void</span> <span class="nf">register</span><span class="p">(</span><span class="n">RegistrationReady</span> <span class="n">proof</span><span class="p">)</span> <span class="p">{</span>
  <span class="c1">// This function CANNOT be called unless:</span>
  <span class="c1">// 1. Email was validated</span>
  <span class="c1">// 2. Password was checked</span>
  <span class="c1">// 3. Terms were accepted</span>
  <span class="c1">//</span>
  <span class="c1">// The compiler enforces this. Documentation and code review cannot.</span>

  <span class="n">createAccount</span><span class="p">(</span><span class="n">proof</span><span class="o">.</span><span class="na">email</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The caller cannot construct a <code class="language-plaintext highlighter-rouge">RegistrationReady</code> instance directly. They must go through <code class="language-plaintext highlighter-rouge">prepareRegistration()</code>, which requires the three proof types, which can only be obtained by passing the respective checks.</p>

<h2 id="carrying-data-with-proofs">Carrying Data with Proofs</h2>

<p>Proof types can carry validated data:</p>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">final</span> <span class="kd">class</span> <span class="nc">ParsedInt</span> <span class="p">{</span>
  <span class="kd">final</span> <span class="kt">int</span> <span class="n">value</span><span class="p">;</span>
  <span class="kd">const</span> <span class="n">ParsedInt</span><span class="o">.</span><span class="na">_</span><span class="p">(</span><span class="k">this</span><span class="o">.</span><span class="na">value</span><span class="p">);</span>
<span class="p">}</span>

<span class="n">ParsedInt</span><span class="o">?</span> <span class="n">parseInt</span><span class="p">(</span><span class="kt">String</span> <span class="n">input</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">final</span> <span class="n">result</span> <span class="o">=</span> <span class="kt">int</span><span class="o">.</span><span class="na">tryParse</span><span class="p">(</span><span class="n">input</span><span class="p">);</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">result</span> <span class="o">!=</span> <span class="kc">null</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="n">ParsedInt</span><span class="o">.</span><span class="na">_</span><span class="p">(</span><span class="n">result</span><span class="p">);</span>
  <span class="p">}</span>
  <span class="k">return</span> <span class="kc">null</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Now <code class="language-plaintext highlighter-rouge">ParsedInt</code> is both a proof that parsing succeeded and a carrier for the parsed value.</p>

<h2 id="authorization-checks">Authorization Checks</h2>

<p>This pattern works well for authorization:</p>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">final</span> <span class="kd">class</span> <span class="nc">Authorized</span><span class="p">&lt;</span><span class="n">T</span><span class="p">&gt;</span> <span class="p">{</span>
  <span class="kd">final</span> <span class="n">T</span> <span class="n">resource</span><span class="p">;</span>
  <span class="kd">const</span> <span class="n">Authorized</span><span class="o">.</span><span class="na">_</span><span class="p">(</span><span class="k">this</span><span class="o">.</span><span class="na">resource</span><span class="p">);</span>
<span class="p">}</span>

<span class="n">Authorized</span><span class="p">&lt;</span><span class="n">Document</span><span class="p">&gt;</span><span class="o">?</span> <span class="n">authorizeDocumentAccess</span><span class="p">(</span>
  <span class="n">User</span> <span class="n">user</span><span class="p">,</span>
  <span class="n">Document</span> <span class="n">document</span><span class="p">,</span>
<span class="p">)</span> <span class="p">{</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">user</span><span class="o">.</span><span class="na">canAccess</span><span class="p">(</span><span class="n">document</span><span class="p">))</span> <span class="p">{</span>
    <span class="k">return</span> <span class="n">Authorized</span><span class="o">.</span><span class="na">_</span><span class="p">(</span><span class="n">document</span><span class="p">);</span>
  <span class="p">}</span>
  <span class="k">return</span> <span class="kc">null</span><span class="p">;</span>
<span class="p">}</span>

<span class="kt">void</span> <span class="nf">deleteDocument</span><span class="p">(</span><span class="n">Authorized</span><span class="p">&lt;</span><span class="n">Document</span><span class="p">&gt;</span> <span class="n">proof</span><span class="p">)</span> <span class="p">{</span>
  <span class="c1">// The authorization check provably happened.</span>
  <span class="c1">// No need to check again.</span>
  <span class="n">proof</span><span class="o">.</span><span class="na">resource</span><span class="o">.</span><span class="na">delete</span><span class="p">();</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="what-about-other-languages">What About Other Languages?</h2>

<p>This pattern requires the ability to create types that cannot be instantiated outside their defining module. Let's see which languages support this.</p>

<h3 id="javascript">JavaScript</h3>

<p>JavaScript has no mechanism for this. Classes can always be instantiated:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">Validated</span> <span class="p">{</span>
  <span class="nx">#private</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span> <span class="c1">// Private field, but...</span>
  <span class="nf">constructor</span><span class="p">()</span> <span class="p">{}</span> <span class="c1">// Constructor is always accessible</span>
<span class="p">}</span>

<span class="c1">// Anyone can do this:</span>
<span class="k">new</span> <span class="nc">Validated</span><span class="p">();</span>
</code></pre></div></div>

<p>Even with private fields, you cannot prevent construction. The language simply doesn't have the concept of a sealed type.</p>

<h3 id="typescript">TypeScript</h3>

<p>TypeScript's type system is erased at runtime. Private constructors exist but provide no runtime guarantees:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">Validated</span> <span class="p">{</span>
  <span class="k">private</span> <span class="nf">constructor</span><span class="p">()</span> <span class="p">{}</span>

  <span class="k">static</span> <span class="nf">create</span><span class="p">():</span> <span class="nx">Validated</span> <span class="p">{</span>
    <span class="k">return</span> <span class="k">new</span> <span class="nc">Validated</span><span class="p">();</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="c1">// TypeScript prevents this at compile time:</span>
<span class="c1">// new Validated(); // Error</span>

<span class="c1">// But at runtime, it's just JavaScript:</span>
<span class="c1">// Anyone with access to the transpiled code can bypass this</span>
</code></pre></div></div>

<p>More fundamentally, TypeScript uses structural typing. Any object with the right shape satisfies an interface:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">interface</span> <span class="nx">Validated</span> <span class="p">{</span>
  <span class="k">readonly</span> <span class="nx">_brand</span><span class="p">:</span> <span class="nx">unique</span> <span class="nx">symbol</span><span class="p">;</span>
<span class="p">}</span>

<span class="c1">// You can still create objects that match:</span>
<span class="kd">const</span> <span class="nx">fake</span> <span class="o">=</span> <span class="p">{</span> <span class="na">_brand</span><span class="p">:</span> <span class="nc">Symbol</span><span class="p">()</span> <span class="p">}</span> <span class="kd">as </span><span class="nx">Validated</span><span class="p">;</span>
</code></pre></div></div>

<p>Branded types are a workaround, but they're not enforced. They rely on developers not bypassing them.</p>

<h3 id="python">Python</h3>

<p>Python's philosophy is "we're all consenting adults." Nothing is truly private:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Validated</span><span class="p">:</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="n">self</span><span class="p">):</span>
        <span class="k">pass</span>

<span class="c1"># "Private" by convention only
</span><span class="k">class</span> <span class="nc">_Validated</span><span class="p">:</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="n">self</span><span class="p">):</span>
        <span class="k">pass</span>

<span class="c1"># Anyone can still do:
</span><span class="nf">_Validated</span><span class="p">()</span>  <span class="c1"># Works fine
</span></code></pre></div></div>

<p>Even with <code class="language-plaintext highlighter-rouge">__slots__</code>, metaclasses, or <code class="language-plaintext highlighter-rouge">__new__</code> tricks, determined code can always create instances.</p>

<h3 id="java">Java</h3>

<p>Java has <code class="language-plaintext highlighter-rouge">final</code> classes and private constructors:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">final</span> <span class="kd">class</span> <span class="nc">Validated</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="nf">Validated</span><span class="o">()</span> <span class="o">{}</span>

    <span class="kd">public</span> <span class="kd">static</span> <span class="nc">Validated</span> <span class="nf">create</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="k">new</span> <span class="nf">Validated</span><span class="o">();</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>This looks promising, but Java has reflection:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">Constructor</span><span class="o">&lt;</span><span class="nc">Validated</span><span class="o">&gt;</span> <span class="n">constructor</span> <span class="o">=</span>
    <span class="nc">Validated</span><span class="o">.</span><span class="na">class</span><span class="o">.</span><span class="na">getDeclaredConstructor</span><span class="o">();</span>
<span class="n">constructor</span><span class="o">.</span><span class="na">setAccessible</span><span class="o">(</span><span class="kc">true</span><span class="o">);</span> <span class="c1">// Bypass private</span>
<span class="nc">Validated</span> <span class="n">fake</span> <span class="o">=</span> <span class="n">constructor</span><span class="o">.</span><span class="na">newInstance</span><span class="o">();</span> <span class="c1">// Creates instance</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">setAccessible(true)</code> call bypasses all access control. Unless you run with a restrictive SecurityManager (which almost no one does, and which is deprecated since Java 17), private constructors provide no guarantee.</p>

<h3 id="kotlin-scala-c-and-php">Kotlin, Scala, C#, and PHP</h3>

<p>The same story repeats across these languages. Kotlin and Scala run on the JVM and inherit its reflection, C# has .NET reflection, and PHP has its own Reflection API. In every case, the equivalent of Java's <code class="language-plaintext highlighter-rouge">setAccessible(true)</code> forces access to a private constructor, so the guarantee is advisory rather than enforced.</p>

<h3 id="go">Go</h3>

<p>Go's unexported types (lowercase names) can only be used within their package:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">package</span> <span class="n">validation</span>

<span class="k">type</span> <span class="n">validated</span> <span class="k">struct</span> <span class="p">{</span>
    <span class="n">value</span> <span class="kt">string</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">Validate</span><span class="p">(</span><span class="n">input</span> <span class="kt">string</span><span class="p">)</span> <span class="o">*</span><span class="n">validated</span> <span class="p">{</span>
    <span class="k">if</span> <span class="n">isValid</span><span class="p">(</span><span class="n">input</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">return</span> <span class="o">&amp;</span><span class="n">validated</span><span class="p">{</span><span class="n">value</span><span class="o">:</span> <span class="n">input</span><span class="p">}</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="no">nil</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This actually works for the basic case. External packages cannot create <code class="language-plaintext highlighter-rouge">validated</code> instances directly. However, Go's type system has limitations. Interfaces are structural, not nominal, and the <code class="language-plaintext highlighter-rouge">unsafe</code> package can bypass protections. It's closer to what we want but not as clean as Dart's approach.</p>

<h3 id="c">C</h3>

<p>C has no classes, but you can use opaque pointers: declare a struct in a header without defining it, and only expose functions that return pointers to it. External code cannot allocate the struct directly. However, C's lack of type safety means you can cast anything to anything. One <code class="language-plaintext highlighter-rouge">memcpy</code> or pointer cast and your guarantees evaporate.</p>

<h3 id="c-1">C++</h3>

<p>C++ has <code class="language-plaintext highlighter-rouge">final</code> classes (since C++11) and private constructors. Unlike Java, C++ has no runtime reflection that can bypass access control. However, <code class="language-plaintext highlighter-rouge">friend</code> declarations punch holes in encapsulation, and pointer arithmetic or <code class="language-plaintext highlighter-rouge">reinterpret_cast</code> can forge any type. If you trust your codebase not to do unsafe things, C++ can approximate proof types, but the language doesn't enforce it.</p>

<h3 id="ruby">Ruby</h3>

<p>Ruby is dynamic and open by design. Classes can be reopened, methods redefined, and <code class="language-plaintext highlighter-rouge">send</code> can call private methods. Nothing is truly sealed.</p>

<h3 id="swift">Swift</h3>

<p>Swift can achieve this pattern:</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">final</span> <span class="kd">class</span> <span class="kt">Validated</span> <span class="p">{</span>
    <span class="kd">fileprivate</span> <span class="nf">init</span><span class="p">()</span> <span class="p">{}</span>
<span class="p">}</span>

<span class="kd">public</span> <span class="kd">func</span> <span class="nf">validate</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="kt">Validated</span><span class="p">?</span> <span class="p">{</span>
    <span class="c1">// Only this file can create Validated</span>
    <span class="k">return</span> <span class="kt">Validated</span><span class="p">()</span>
<span class="p">}</span>
</code></pre></div></div>

<p>With <code class="language-plaintext highlighter-rouge">final</code> preventing subclassing and <code class="language-plaintext highlighter-rouge">fileprivate</code>/<code class="language-plaintext highlighter-rouge">private</code> preventing construction, Swift provides similar guarantees to Dart. Swift's reflection (Mirror) is read-only, but <code class="language-plaintext highlighter-rouge">unsafeBitCast</code> can still forge an instance out of arbitrary bits. As in Rust, that requires deliberately reaching for an explicitly unsafe API.</p>

<h3 id="rust">Rust</h3>

<p>Rust handles this through its module system:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">mod</span> <span class="n">validation</span> <span class="p">{</span>
    <span class="k">pub</span> <span class="k">struct</span> <span class="n">Validated</span> <span class="p">{</span>
        <span class="n">value</span><span class="p">:</span> <span class="nb">String</span><span class="p">,</span> <span class="c1">// private field</span>
    <span class="p">}</span>

    <span class="k">impl</span> <span class="n">Validated</span> <span class="p">{</span>
        <span class="k">pub</span> <span class="k">fn</span> <span class="nf">new</span><span class="p">(</span><span class="n">input</span><span class="p">:</span> <span class="o">&amp;</span><span class="nb">str</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">Option</span><span class="o">&lt;</span><span class="n">Validated</span><span class="o">&gt;</span> <span class="p">{</span>
            <span class="k">if</span> <span class="nf">is_valid</span><span class="p">(</span><span class="n">input</span><span class="p">)</span> <span class="p">{</span>
                <span class="nf">Some</span><span class="p">(</span><span class="n">Validated</span> <span class="p">{</span>
                    <span class="n">value</span><span class="p">:</span> <span class="n">input</span><span class="nf">.to_string</span><span class="p">()</span>
                <span class="p">})</span>
            <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
                <span class="nb">None</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="c1">// Outside the module, you cannot construct Validated</span>
<span class="c1">// because you cannot access the private field</span>
</code></pre></div></div>

<p>Rust's module system and private fields make this pattern natural. The one escape hatch is <code class="language-plaintext highlighter-rouge">unsafe</code>: <code class="language-plaintext highlighter-rouge">std::mem::transmute</code> or <code class="language-plaintext highlighter-rouge">std::mem::zeroed</code> can fabricate a value with private fields. But that means writing the word <code class="language-plaintext highlighter-rouge">unsafe</code>, a loud, greppable signal that ordinary code never uses.</p>

<h3 id="haskell">Haskell</h3>

<p>Haskell pioneered many of these ideas.</p>

<div class="language-haskell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">module</span> <span class="nn">Validation</span> <span class="p">(</span><span class="kt">Validated</span><span class="p">,</span> <span class="nf">validate</span><span class="p">)</span> <span class="kr">where</span>

<span class="kr">newtype</span> <span class="kt">Validated</span> <span class="o">=</span> <span class="kt">Validated</span> <span class="kt">String</span>

<span class="n">validate</span> <span class="o">::</span> <span class="kt">String</span> <span class="o">-&gt;</span> <span class="kt">Maybe</span> <span class="kt">Validated</span>
<span class="n">validate</span> <span class="n">input</span>
    <span class="o">|</span> <span class="n">isValid</span> <span class="n">input</span> <span class="o">=</span> <span class="kt">Just</span> <span class="p">(</span><span class="kt">Validated</span> <span class="n">input</span><span class="p">)</span>
    <span class="o">|</span> <span class="n">otherwise</span>     <span class="o">=</span> <span class="kt">Nothing</span>
</code></pre></div></div>

<p>By not exporting the <code class="language-plaintext highlighter-rouge">Validated</code> constructor, external code cannot create values of that type. This pattern has been used in Haskell for decades. The one loophole is <code class="language-plaintext highlighter-rouge">unsafeCoerce</code>, which can manufacture a <code class="language-plaintext highlighter-rouge">Validated</code> from anything. Like Rust's <code class="language-plaintext highlighter-rouge">unsafe</code> and Swift's <code class="language-plaintext highlighter-rouge">unsafeBitCast</code>, it is an explicit, unmistakable escape hatch, not something normal code stumbles into.</p>

<h3 id="ocaml">OCaml</h3>

<p><a href="https://www.reddit.com/r/ProgrammingLanguages/comments/1uyv0pv/comment/oy56gyq/">gasche pointed out</a> that OCaml, from the ML family with a richer module system than Haskell's, offers two ways to do this.</p>

<p>The first mirrors the Haskell approach: keep the type abstract in the module signature.</p>

<div class="language-ocaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">module</span> <span class="nc">Email</span> <span class="o">:</span> <span class="k">sig</span>
  <span class="k">type</span> <span class="n">validated</span>
  <span class="k">val</span> <span class="n">validate</span> <span class="o">:</span> <span class="kt">string</span> <span class="o">-&gt;</span> <span class="n">validated</span> <span class="n">option</span>
  <span class="k">val</span> <span class="n">to_string</span> <span class="o">:</span> <span class="n">validated</span> <span class="o">-&gt;</span> <span class="kt">string</span>
<span class="k">end</span> <span class="o">=</span> <span class="k">struct</span>
  <span class="k">type</span> <span class="n">validated</span> <span class="o">=</span> <span class="kt">string</span>
  <span class="k">let</span> <span class="n">validate</span> <span class="n">s</span> <span class="o">=</span> <span class="k">if</span> <span class="n">is_valid_email</span> <span class="n">s</span> <span class="k">then</span> <span class="nc">Some</span> <span class="n">s</span> <span class="k">else</span> <span class="nc">None</span>
  <span class="k">let</span> <span class="n">to_string</span> <span class="n">s</span> <span class="o">=</span> <span class="n">s</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Inside the module <code class="language-plaintext highlighter-rouge">validated</code> is just a <code class="language-plaintext highlighter-rouge">string</code>, but the signature hides that. Outside code needs <code class="language-plaintext highlighter-rouge">Email.to_string</code> to read the payload and has no way to construct a <code class="language-plaintext highlighter-rouge">validated</code> on its own.</p>

<p>The second has no Dart equivalent: a private type abbreviation.</p>

<div class="language-ocaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">module</span> <span class="nc">Email</span> <span class="o">:</span> <span class="k">sig</span>
  <span class="k">type</span> <span class="n">validated</span> <span class="o">=</span> <span class="n">private</span> <span class="kt">string</span>
  <span class="k">val</span> <span class="n">validate</span> <span class="o">:</span> <span class="kt">string</span> <span class="o">-&gt;</span> <span class="n">validated</span> <span class="n">option</span>
<span class="k">end</span> <span class="o">=</span> <span class="k">struct</span>
  <span class="k">type</span> <span class="n">validated</span> <span class="o">=</span> <span class="kt">string</span>
  <span class="k">let</span> <span class="n">validate</span> <span class="n">s</span> <span class="o">=</span> <span class="k">if</span> <span class="n">is_valid_email</span> <span class="n">s</span> <span class="k">then</span> <span class="nc">Some</span> <span class="n">s</span> <span class="k">else</span> <span class="nc">None</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Here <code class="language-plaintext highlighter-rouge">validated</code> is a one-way subtype of <code class="language-plaintext highlighter-rouge">string</code>. You can coerce a <code class="language-plaintext highlighter-rouge">validated</code> down to a <code class="language-plaintext highlighter-rouge">string</code> with <code class="language-plaintext highlighter-rouge">(x :&gt; string)</code> and read the payload for free, but you cannot go the other way, so the only way to obtain a <code class="language-plaintext highlighter-rouge">validated</code> is through <code class="language-plaintext highlighter-rouge">validate</code>. It is the same guarantee with less ceremony than a fully abstract type.</p>

<p>Both are undone only by <code class="language-plaintext highlighter-rouge">Obj.magic</code>, OCaml's <code class="language-plaintext highlighter-rouge">unsafeCoerce</code>. It casts any value to any type and skips the constructor entirely, so as in the other languages here, forging a proof takes that one explicit, unsafe step.</p>

<h3 id="summary">Summary</h3>

<table>
  <thead>
    <tr>
      <th>Language</th>
      <th>Proof Types Possible?</th>
      <th>Why / Why Not</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>JavaScript</td>
      <td>No</td>
      <td>No access control on construction</td>
    </tr>
    <tr>
      <td>TypeScript</td>
      <td>No</td>
      <td>Types erased, structural typing</td>
    </tr>
    <tr>
      <td>Python</td>
      <td>No</td>
      <td>Everything accessible, dynamic typing</td>
    </tr>
    <tr>
      <td>Java</td>
      <td>No</td>
      <td>Reflection bypasses private</td>
    </tr>
    <tr>
      <td>Kotlin</td>
      <td>No</td>
      <td>JVM reflection bypasses private</td>
    </tr>
    <tr>
      <td>C#</td>
      <td>No</td>
      <td>Reflection bypasses private</td>
    </tr>
    <tr>
      <td>Go</td>
      <td>Partially</td>
      <td>Unexported works, but <code class="language-plaintext highlighter-rouge">unsafe</code> exists</td>
    </tr>
    <tr>
      <td>C</td>
      <td>No</td>
      <td>Pointer casts bypass everything</td>
    </tr>
    <tr>
      <td>C++</td>
      <td>Partially</td>
      <td>No reflection, but <code class="language-plaintext highlighter-rouge">friend</code> and casts exist</td>
    </tr>
    <tr>
      <td>Scala</td>
      <td>No</td>
      <td>JVM reflection bypasses private</td>
    </tr>
    <tr>
      <td>Ruby</td>
      <td>No</td>
      <td>Dynamic, classes can be reopened</td>
    </tr>
    <tr>
      <td>PHP</td>
      <td>No</td>
      <td>Reflection bypasses private</td>
    </tr>
    <tr>
      <td>Swift</td>
      <td>Yes</td>
      <td>final + private init, no bypassing reflection</td>
    </tr>
    <tr>
      <td>Rust</td>
      <td>Yes</td>
      <td>Private fields, strong module system</td>
    </tr>
    <tr>
      <td>Haskell</td>
      <td>Yes</td>
      <td>Module exports control construction</td>
    </tr>
    <tr>
      <td>OCaml</td>
      <td>Yes</td>
      <td>Abstract or private types control construction</td>
    </tr>
    <tr>
      <td>Dart</td>
      <td>Yes</td>
      <td>final + private constructor, mirrors deprecated and disabled</td>
    </tr>
  </tbody>
</table>

<p>Dart sits alongside Swift, Rust, Haskell, and OCaml in providing this capability. Dart does have reflection, through <code class="language-plaintext highlighter-rouge">dart:mirrors</code>, but mirrors are deprecated, disabled by default in Flutter, and unavailable when compiling to JavaScript. For all practical purposes, Dart code cannot bypass private constructors.</p>

<p>One honest caveat: the "Yes" languages are not truly airtight. Each has a deliberately unsafe escape hatch, Rust's <code class="language-plaintext highlighter-rouge">transmute</code>, Swift's <code class="language-plaintext highlighter-rouge">unsafeBitCast</code>, Haskell's <code class="language-plaintext highlighter-rouge">unsafeCoerce</code>, OCaml's <code class="language-plaintext highlighter-rouge">Obj.magic</code>, and Dart's disabled <code class="language-plaintext highlighter-rouge">dart:mirrors</code>. The distinction from the "No" languages is not that forging is impossible, but that it requires explicitly invoking an unsafe operation rather than ordinary reflection or normal typed code.</p>

<p>What makes Dart notable is that it's a mainstream, accessible language that runs everywhere (web, mobile, desktop, server) while still providing these guarantees. You don't need to learn a systems language or a functional language to use proof types.</p>

<h2 id="parse-dont-validate">Parse, Don't Validate</h2>

<p>Proof types embody the principle of <a href="https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/">"parse, don't validate"</a>.</p>

<p>Validation checks if data is valid and returns a boolean or throws an exception. The data keeps its original type:</p>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">bool</span> <span class="nf">isValidEmail</span><span class="p">(</span><span class="kt">String</span> <span class="n">email</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">return</span> <span class="n">RegExp</span><span class="p">(</span><span class="sx">r'^[^@]+@[^@]+\.[^@]+$'</span><span class="p">)</span><span class="o">.</span><span class="na">hasMatch</span><span class="p">(</span><span class="n">email</span><span class="p">);</span>
<span class="p">}</span>

<span class="kt">void</span> <span class="nf">processEmail</span><span class="p">(</span><span class="kt">String</span> <span class="n">email</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">isValidEmail</span><span class="p">(</span><span class="n">email</span><span class="p">))</span> <span class="p">{</span>
    <span class="k">throw</span> <span class="n">ArgumentError</span><span class="p">(</span><span class="s">'Invalid email'</span><span class="p">);</span>
  <span class="p">}</span>
  <span class="c1">// email is still just a String</span>
  <span class="c1">// Nothing stops you from passing an unvalidated string here</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The problem: you can forget to call the validator, or call it and ignore the result, or pass an unvalidated string to a function expecting a validated one. The type system doesn't help.</p>

<p>Parsing transforms data into a new type that encodes validity in the type itself:</p>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">final</span> <span class="kd">class</span> <span class="nc">ValidEmail</span> <span class="p">{</span>
  <span class="kd">final</span> <span class="kt">String</span> <span class="n">value</span><span class="p">;</span>
  <span class="kd">const</span> <span class="n">ValidEmail</span><span class="o">.</span><span class="na">_</span><span class="p">(</span><span class="k">this</span><span class="o">.</span><span class="na">value</span><span class="p">);</span>
<span class="p">}</span>

<span class="n">ValidEmail</span><span class="o">?</span> <span class="n">parseEmail</span><span class="p">(</span><span class="kt">String</span> <span class="n">email</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">RegExp</span><span class="p">(</span><span class="sx">r'^[^@]+@[^@]+\.[^@]+$'</span><span class="p">)</span><span class="o">.</span><span class="na">hasMatch</span><span class="p">(</span><span class="n">email</span><span class="p">))</span> <span class="p">{</span>
    <span class="k">return</span> <span class="n">ValidEmail</span><span class="o">.</span><span class="na">_</span><span class="p">(</span><span class="n">email</span><span class="p">);</span>
  <span class="p">}</span>
  <span class="k">return</span> <span class="kc">null</span><span class="p">;</span>
<span class="p">}</span>

<span class="kt">void</span> <span class="nf">processEmail</span><span class="p">(</span><span class="n">ValidEmail</span> <span class="n">email</span><span class="p">)</span> <span class="p">{</span>
  <span class="c1">// Can only be called with a ValidEmail</span>
  <span class="c1">// Which can only be obtained by successfully parsing</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Now the type system enforces that <code class="language-plaintext highlighter-rouge">processEmail</code> only receives validated emails. You cannot forget to validate because the types won't match. You cannot pass raw strings because the compiler rejects them.</p>

<p>Proof types take this further. They need not carry any data at all. The value's existence alone certifies that a computation occurred.</p>

<h2 id="the-curry-howard-correspondence">The Curry-Howard Correspondence</h2>

<p>The pattern in this post rests on a well-known result from theoretical computer science: the <a href="https://en.wikipedia.org/wiki/Curry%E2%80%93Howard_correspondence">Curry-Howard correspondence</a>.</p>

<p>The correspondence states:</p>

<ul>
  <li>Types are propositions</li>
  <li>Programs are proofs</li>
  <li>A value of a type is a proof of that proposition</li>
</ul>

<p>When you declare <code class="language-plaintext highlighter-rouge">final class EmailValidated</code>, you're declaring a proposition: "this email was validated." When you have a value of type <code class="language-plaintext highlighter-rouge">EmailValidated</code>, you have a proof of that proposition.</p>

<p>If a type has no public constructor, the only way to obtain a proof is through the functions that construct it. The function <code class="language-plaintext highlighter-rouge">validateEmail</code> is the only way to prove the proposition <code class="language-plaintext highlighter-rouge">EmailValidated</code>. And that function only produces a proof when validation actually succeeds.</p>

<p>In languages with more expressive type systems (like Agda, Coq, or Idris), you can encode complex propositions and have the compiler verify sophisticated invariants. Dart's type system is simpler (for a good reason, one I might discuss in the future), but the core principle applies: if you control construction, you control what can be proven.</p>

<h2 id="when-to-use-proof-types">When to Use Proof Types</h2>

<p>Proof types are useful when:</p>

<ol>
  <li>Security-critical checks must have occurred (authorization, rate limiting)</li>
  <li>Validation must be enforced before processing</li>
  <li>Multi-step processes require all steps to complete</li>
  <li>APIs should make invalid states unrepresentable</li>
  <li>Machine-generated or third-party code must not be able to skip a required check</li>
</ol>

<p>They're overkill when:</p>

<ol>
  <li>The check is trivial and always passes</li>
  <li>Performance is critical and the extra objects matter</li>
  <li>The codebase is small and single-author</li>
</ol>

<h2 id="limitations">Limitations</h2>

<p>Proof types have some limitations to keep in mind:</p>

<p>Library scope: The guarantee holds only outside the library. Within the library that defines the proof type, any code can construct instances. Keep proof-generating logic focused and correct.</p>

<p>No temporal claims: A proof type proves the check happened, not when. If state can change between obtaining the proof and using it, you may need to revalidate. For example, an <code class="language-plaintext highlighter-rouge">Authorized</code> proof obtained before a permission change might be stale.</p>

<p>No subject binding: <a href="https://www.reddit.com/r/ProgrammingLanguages/comments/1uyv0pv/comment/oy2bcf1/">tsanderdev pointed out</a> that an empty proof like <code class="language-plaintext highlighter-rouge">PasswordChecked</code> is weak on its own. Such an instance proves only that some password was validated somewhere over the app's lifetime, not that this user's password was validated for this request. A proof that carries no data is ambient, so any <code class="language-plaintext highlighter-rouge">PasswordChecked</code> is interchangeable with any other. This is a fair point, and the password example does not survive it cleanly. Passwords are unbounded, and Dart cannot put values into its type system, so "a proof for exactly this password" is not something the compiler can express. That example cannot be made fully safe at compile time, and a better one would carry a bounded, identifiable subject.</p>

<p>You can still make the pattern safer at runtime by storing a username or a session token inside the proof and checking it where the proof is consumed, so a mismatched or reused proof fails at runtime even though the type system will not catch it for you. This is also the motivation for phantom types, a related technique I want to cover in a future post. More generally, generics can bind a proof to a type, the way <code class="language-plaintext highlighter-rouge">Authorized&lt;Document&gt;</code> does, but not to a specific value such as one particular user id, because Dart has no dependent types.</p>

<p>Runtime cost: Each proof is an object allocation. For hot paths, consider whether the safety benefit justifies the cost. Extension types are a middle ground here: free at runtime and safer than a bare value, though as the <a href="#extension-types-are-not-a-replacement">addendum</a> explains, they can be forged, so they are not a true proof.</p>

<h2 id="conclusion">Conclusion</h2>

<p><a href="/blog/understanding-dart-class-modifiers-lattices/">Dart 3.0's class modifiers</a> enabled a pattern that many mainstream languages cannot express. By combining <code class="language-plaintext highlighter-rouge">final</code> with library-private constructors, you can create types whose mere existence proves that computations occurred.</p>

<p>The type system stops being just a way to catch typos. It becomes a tool for encoding your program's invariants and protocols directly. When you see a function that takes a <code class="language-plaintext highlighter-rouge">RegistrationReady</code> parameter, you know, without reading the implementation or the documentation, that all registration checks have passed. The types tell you.</p>

<p>This is the kind of capability that makes me excited about Dart's evolution: a small addition to the type system, and suddenly you can encode guarantees that used to require a much fancier language.</p>

<p><strong>PS:</strong> Of the four languages that support proof types, only Dart aims to have an efficient type system. Rust's macro system is Turing-complete. Swift's type system can express unbounded computation (someone <a href="https://forums.swift.org/t/brainf-in-the-swift-type-system/68301">implemented Brainfuck in Swift's type system</a>). Haskell's type-level programming can loop forever. Once a type system can run arbitrary computation, type checking has no upper bound on how long it can take. The other three languages have all crossed that line. Dart has not, and it shows in practice: Haskell, Rust, and Swift are all notorious for slow compile times. Rust's own <a href="https://blog.rust-lang.org/2025/09/10/rust-compiler-performance-survey-2025-results/">2025 compiler performance survey</a> found slow compilation to be the single most-cited pain point, and roughly 45% of respondents who had abandoned Rust listed long compile times as one of their reasons. Swift ships a dedicated error, <a href="https://danielchasehooper.com/posts/why-swift-is-slow/">"expression was too complex to be solved in reasonable time"</a>, for expressions whose overload resolution blows up exponentially. And in Haskell circles, <a href="https://www.parsonsmatt.org/2019/11/27/keeping_compilation_fast.html">complaining about compile times</a> is practically a rite of passage. Dart gives you proof types without the compile-time tax. That advantage is deliberate. A <a href="https://gist.github.com/paulmillr/1208618">2010 Google memo</a> that made the original case for building Dart listed the "Ability to be Tooled" as one of its three core goals, a design constraint JavaScript's structure could never satisfy. Tooling friendliness, it seems, has been a guiding star for Dart from the very beginning, and that heritage is a large part of why it stands apart from the other proof-type languages here.</p>

<p><strong>PPS:</strong> Everything above assumes a <a href="https://en.wikipedia.org/wiki/Closed-world_assumption">closed world</a>. When your Dart code runs in the VM or is compiled to native code, this assumption holds. When compiled to JavaScript, things get murkier. JavaScript runtimes support all kinds of shenanigans (prototype manipulation, <code class="language-plaintext highlighter-rouge">eval</code>, dynamic property access) that could potentially let adversarial code violate these guarantees. If you're defending against malicious code running in the same JS context, proof types alone won't save you. If you need these guarantees on the web, <a href="https://dart.dev/web/wasm">WebAssembly</a> closes the gap: Dart compiles to WASM, which runs in a sandboxed linear-memory model with none of those escape hatches, so the closed-world assumption holds there too.</p>

<hr />

<p><a href="https://www.reddit.com/r/dartlang/comments/1uydmcw/proof_types_in_dart_using_final_classes_as/">Discuss on r/dartlang</a></p>

<p><a href="https://www.reddit.com/r/ProgrammingLanguages/comments/1uyv0pv/proof_types_in_dart_using_final_classes_as/">Discuss on r/ProgrammingLanguages</a></p>

<hr />

<h2 id="addendum">Addendum</h2>

<h3 id="extension-types-are-not-a-replacement">Extension types are not a replacement</h3>

<p><a href="https://www.reddit.com/r/dartlang/comments/1uydmcw/comment/oxyzafk/">cent-met-een-vin suggested</a> that Dart's extension types could express the same idea. Extension types are a good middle ground: they give you essentially free performance and improved safety guarantees, but we can actually fake them. An extension type is a compile-time-only view over a representation type. At runtime, a value of an extension type is its representation, with no separate identity and no enforced construction.</p>

<p>Take an email proof written as an extension type:</p>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">extension</span> <span class="n">type</span> <span class="n">EmailValidated</span><span class="o">.</span><span class="na">_</span><span class="p">(</span><span class="kt">String</span> <span class="n">value</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">static</span> <span class="n">EmailValidated</span><span class="o">?</span> <span class="n">validate</span><span class="p">(</span><span class="kt">String</span> <span class="n">input</span><span class="p">)</span> <span class="o">=</span><span class="p">&gt;</span>
      <span class="n">_isValidEmail</span><span class="p">(</span><span class="n">input</span><span class="p">)</span> <span class="o">?</span> <span class="n">EmailValidated</span><span class="o">.</span><span class="na">_</span><span class="p">(</span><span class="n">input</span><span class="p">)</span> <span class="o">:</span> <span class="kc">null</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The private constructor looks like it guards construction, but extension types are erased to their representation at runtime. Any string can be cast straight into one:</p>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">final</span> <span class="kt">Object</span> <span class="n">raw</span> <span class="o">=</span> <span class="s">"definitely not an email"</span><span class="p">;</span>
<span class="kd">final</span> <span class="n">forged</span> <span class="o">=</span> <span class="n">raw</span> <span class="k">as</span> <span class="n">EmailValidated</span><span class="p">;</span> <span class="c1">// succeeds</span>
</code></pre></div></div>

<p>Because <code class="language-plaintext highlighter-rouge">EmailValidated</code> erases to <code class="language-plaintext highlighter-rouge">String</code>, the check behind that cast is really <code class="language-plaintext highlighter-rouge">raw is String</code>, so it passes, and you get a "validated" email that was never validated. This is the same weakness as TypeScript's branded types: a static-only wrapper with no runtime guarantee. A proof type needs a real class with a real library-private constructor, precisely so that no cast can conjure one.</p>

<h3 id="sealed-classes-make-proof-types-even-more-powerful">Sealed classes make proof types even more powerful</h3>

<p><a href="https://www.reddit.com/r/dartlang/comments/1uydmcw/comment/oy0i10k/">pavanpodila pointed out</a> that sealed classes push this technique further, and that is exactly right. A <code class="language-plaintext highlighter-rouge">sealed</code> root lets you model a proof that is one of several known cases, and you get exhaustiveness checking in <code class="language-plaintext highlighter-rouge">switch</code> for free, so the compiler forces you to handle every branch.</p>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// A proof that a user's identity was verified, and how.</span>
<span class="kd">sealed</span> <span class="kd">class</span> <span class="nc">IdentityVerified</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="n">IdentityVerified</span><span class="o">.</span><span class="na">_</span><span class="p">();</span>
<span class="p">}</span>

<span class="kd">final</span> <span class="kd">class</span> <span class="nc">VerifiedByEmail</span> <span class="kd">extends</span> <span class="n">IdentityVerified</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="n">VerifiedByEmail</span><span class="o">.</span><span class="na">_</span><span class="p">()</span> <span class="o">:</span> <span class="k">super</span><span class="o">.</span><span class="na">_</span><span class="p">();</span>
<span class="p">}</span>

<span class="kd">final</span> <span class="kd">class</span> <span class="nc">VerifiedByPhone</span> <span class="kd">extends</span> <span class="n">IdentityVerified</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="n">VerifiedByPhone</span><span class="o">.</span><span class="na">_</span><span class="p">()</span> <span class="o">:</span> <span class="k">super</span><span class="o">.</span><span class="na">_</span><span class="p">();</span>
<span class="p">}</span>
</code></pre></div></div>

<p>A function can then demand proof that verification happened and still react to how, with the compiler checking that every case is covered:</p>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="nf">grantAccess</span><span class="p">(</span><span class="n">IdentityVerified</span> <span class="n">proof</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">switch</span> <span class="p">(</span><span class="n">proof</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">case</span> <span class="n">VerifiedByEmail</span><span class="p">()</span><span class="o">:</span>
      <span class="n">print</span><span class="p">(</span><span class="s">'verified by email'</span><span class="p">);</span>
    <span class="k">case</span> <span class="n">VerifiedByPhone</span><span class="p">()</span><span class="o">:</span>
      <span class="n">print</span><span class="p">(</span><span class="s">'verified by phone'</span><span class="p">);</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Two details matter when you do this:</p>

<ul>
  <li>Each branch must be <code class="language-plaintext highlighter-rouge">final</code>, so that no outside code can subclass a branch to forge one.</li>
  <li>The root itself does not need <code class="language-plaintext highlighter-rouge">final</code>. Marking it <code class="language-plaintext highlighter-rouge">sealed</code> already prevents extension outside its library, so it is implicitly closed.</li>
</ul>

<p>This becomes much less verbose once <a href="https://github.com/dart-lang/language/blob/main/accepted/3.13/primary-constructors/feature-specification.md">primary constructors</a> land, as <a href="https://www.reddit.com/r/dartlang/comments/1uydmcw/comment/oxyyyqd/">Comun4 noted</a>. Today each proof carries a small amount of constructor boilerplate, and primary constructors collapse most of it.</p>

<h3 id="the-library-is-the-trusted-core">The library is the trusted core</h3>

<p><a href="https://www.reddit.com/r/ProgrammingLanguages/comments/1uyv0pv/comment/oy2gs4v/">EggplantExtra4946 asked</a>: what stops a function in the same library from returning an <code class="language-plaintext highlighter-rouge">EmailValidated</code> without validating anything?</p>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">EmailValidated</span> <span class="nf">newEmail</span><span class="p">(</span><span class="kt">String</span> <span class="n">email</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">return</span> <span class="kd">const</span> <span class="n">EmailValidated</span><span class="o">.</span><span class="na">_</span><span class="p">();</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Nothing stops it, and that is by design. Dart's privacy is library-scoped. A Dart library is a single compilation unit, not the folder of files that many other languages call a library. The library that declares a proof type is its trusted core: code outside it is held to the guarantee, and code inside it is trusted to mint proofs correctly. It is the author's job to keep that library small and its minting logic correct.</p>

<h3 id="backdoors">Backdoors</h3>

<p>Because the library mints its own proofs, a proof is only as trustworthy as the code that produces it. A malicious or careless proof library can hand out valid proofs without doing the work:</p>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">EmailValidated</span><span class="o">?</span> <span class="n">validateEmail</span><span class="p">(</span><span class="kt">String</span> <span class="n">email</span><span class="p">)</span> <span class="p">{</span>
  <span class="c1">// Backdoor: this exact input is always accepted, even though it is not a valid email.</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">email</span> <span class="o">==</span> <span class="s">"malicious"</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="kd">const</span> <span class="n">EmailValidated</span><span class="o">.</span><span class="na">_</span><span class="p">();</span>
  <span class="p">}</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">_isValidEmail</span><span class="p">(</span><span class="n">email</span><span class="p">))</span> <span class="p">{</span>
    <span class="k">return</span> <span class="kd">const</span> <span class="n">EmailValidated</span><span class="o">.</span><span class="na">_</span><span class="p">();</span>
  <span class="p">}</span>
  <span class="k">return</span> <span class="kc">null</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Every <code class="language-plaintext highlighter-rouge">EmailValidated</code> this returns is a real, unforgeable value of the type, so the compiler and every downstream caller trust it completely. But <code class="language-plaintext highlighter-rouge">malicious</code> is not even an email, and it still gets one. Proof types defend against a caller accidentally skipping a check. They do not defend against the author of the proof library, who sits inside the trust boundary and can define "validated" however they like. If you depend on someone else's proof types, you are trusting their minting code the same way you trust any dependency, so treat that code as security-critical and keep it small enough to audit.</p>

<h3 id="the-email-regex-is-both-unsound-and-incomplete">The email regex is both unsound and incomplete</h3>

<p><a href="https://www.reddit.com/r/dartlang/comments/1uydmcw/comment/oy7nqug/">RandalSchwartz noted</a> that the <code class="language-plaintext highlighter-rouge">checkEmail</code> regex, <code class="language-plaintext highlighter-rouge">^[^@]+@[^@]+\.[^@]+$</code>, is far too naive to recognize <a href="https://www.rfc-editor.org/rfc/rfc5322">the actual email grammar</a>, and that a syntactically correct email regex is <a href="http://www.ex-parrot.com/~pdw/Mail-RFC822-Address.html">famously thousands of characters long</a>.</p>

<p>The regex is wrong in both directions. It is unsound, because it accepts strings that are not valid addresses: <code class="language-plaintext highlighter-rouge">John Doe@example.com</code> passes, even though an unquoted space in the local part is not allowed. It is also incomplete, because it rejects strings that are valid: <code class="language-plaintext highlighter-rouge">"John@Doe"@example.com</code> is a legal address with a quoted local part, but the regex sees two <code class="language-plaintext highlighter-rouge">@</code> characters and gives up. So an <code class="language-plaintext highlighter-rouge">EmailChecked</code> minted this way both over-promises and under-delivers.</p>

<p>This is the trusted-core problem again, without any malice. The type guarantees that <code class="language-plaintext highlighter-rouge">checkEmail</code> produced the value. It cannot guarantee that <code class="language-plaintext highlighter-rouge">checkEmail</code> correctly captures "valid email", and here it does not. As RandalSchwartz suggests, the fix is to mint the proof from a real grammar-based parser.</p>]]></content><author><name>Modestas Valauskas</name></author><category term="technical" /><category term="dart" /><summary type="html"><![CDATA[How Dart 3.0's class modifiers enable a pattern where types prove that computations have occurred. A capability missing from JavaScript, Python, TypeScript, Java, and most mainstream languages.]]></summary></entry><entry><title type="html">243,000 words dictated in 39 days, speech-to-text changed how I work</title><link href="https://modulovalue.com/blog/voxtral-transcribe-and-wispr-flow/" rel="alternate" type="text/html" title="243,000 words dictated in 39 days, speech-to-text changed how I work" /><published>2026-02-05T00:00:00+01:00</published><updated>2026-02-05T00:00:00+01:00</updated><id>https://modulovalue.com/blog/voxtral-transcribe-and-wispr-flow</id><content type="html" xml:base="https://modulovalue.com/blog/voxtral-transcribe-and-wispr-flow/"><![CDATA[<p>I have been dictating everything for the past 39 days. Code prompts, messages, emails, notes. In that time I have spoken 243,554 words, which is roughly the length of two books. I would never have typed that many words in the same timeframe.</p>

<p>This post is about two things: the dictation tool that made this possible, and a new speech-to-text API that I built a test app for.</p>

<h2 id="wispr-flow">Wispr Flow</h2>

<p><a href="https://wisprflow.ai/r?MODESTAS3">Wispr Flow</a>* is a macOS (and Windows, and iOS) dictation app that runs in the background and works in any application. You hold a key, speak, and it types out what you said. It is not a simple transcription tool. It auto-edits filler words, adds punctuation, and matches the tone and formatting of the app you are using. It has a custom dictionary so it learns your terminology, which is important if you work with domain-specific terms.</p>

<p>I have been using it every single day since I got it over a month ago. I cannot recommend it strongly enough. It was genuinely life-changing.</p>

<p><img src="/assets/posts/voxtral-transcribe-and-wispr-flow/wispr-stats.png" alt="My Wispr Flow statistics after 39 days of use." /></p>

<p>The numbers: 39-day daily streak, 129 words per minute (top 2% of all Flow users), 243,554 total words dictated across 58 different apps.</p>

<h3 id="what-surprised-me">What surprised me</h3>

<p><strong>I became more comfortable talking to people.</strong> As a developer, I am not used to talking to people all day. Most of my communication was typed. After five weeks of constant dictation, I noticed I was significantly more comfortable in conversations, meetings, and even casual interactions. Speaking became the default mode of expression rather than something I had to switch into.</p>

<p><strong>I say what I think without filtering.</strong> When you type, there is a natural bottleneck. You think of something, then you figure out how to type it, then you type it. Dictation removes the middle step. I can say whatever comes to mind, and it flows out. This matters more than it sounds, because the bottleneck is not typing speed, it is the cognitive overhead of translating thoughts into keystrokes.</p>

<p><strong>Staying in flow is effortless.</strong> You do not need your hands. You can be moving around. You can be looking at a different monitor. You can have multiple things open and narrate what you are doing without ever breaking your attention to type. I recently bought two additional monitors specifically so I can keep different contexts visible simultaneously instead of switching between desktops.</p>

<p><strong>You do not even need to be at your desk.</strong> I reprogrammed a presentation clicker (a laser pointer with a Karabiner-Elements configuration) so that one button triggers Fn+Space (which activates Wispr Flow) and another button sends Enter. I can walk around my room, press a button, speak, press the button again, and the text appears. If I want to submit, I press the other button. It is the laziest, most effective input method I have ever used. I highly recommend trying something like this.</p>

<p style="text-align: center;"><img src="/assets/posts/voxtral-transcribe-and-wispr-flow/laser-pointer.jpg" alt="A reprogrammed presentation clicker for hands-free dictation." style="max-width: 300px;" /></p>

<p>Here is the <a href="https://karabiner-elements.pqrs.org/">Karabiner-Elements</a> configuration. It remaps the clicker's <code class="language-plaintext highlighter-rouge">tab</code> key to <code class="language-plaintext highlighter-rouge">Fn+Space</code> (which triggers Wispr Flow) and <code class="language-plaintext highlighter-rouge">down_arrow</code> to <code class="language-plaintext highlighter-rouge">Enter</code>, scoped to the clicker's specific device ID so it does not affect any other keyboard:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"description"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Laser Pointer Remaps"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"manipulators"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="p">{</span><span class="w">
      </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"basic"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"from"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"key_code"</span><span class="p">:</span><span class="w"> </span><span class="s2">"tab"</span><span class="w">
      </span><span class="p">},</span><span class="w">
      </span><span class="nl">"to"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
        </span><span class="p">{</span><span class="w">
          </span><span class="nl">"key_code"</span><span class="p">:</span><span class="w"> </span><span class="s2">"spacebar"</span><span class="p">,</span><span class="w">
          </span><span class="nl">"modifiers"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"fn"</span><span class="p">]</span><span class="w">
        </span><span class="p">}</span><span class="w">
      </span><span class="p">],</span><span class="w">
      </span><span class="nl">"conditions"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
        </span><span class="p">{</span><span class="w">
          </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"device_if"</span><span class="p">,</span><span class="w">
          </span><span class="nl">"identifiers"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
            </span><span class="p">{</span><span class="w">
              </span><span class="nl">"vendor_id"</span><span class="p">:</span><span class="w"> </span><span class="mi">4643</span><span class="p">,</span><span class="w">
              </span><span class="nl">"product_id"</span><span class="p">:</span><span class="w"> </span><span class="mi">15975</span><span class="w">
            </span><span class="p">}</span><span class="w">
          </span><span class="p">]</span><span class="w">
        </span><span class="p">}</span><span class="w">
      </span><span class="p">]</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="p">{</span><span class="w">
      </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"basic"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"from"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"key_code"</span><span class="p">:</span><span class="w"> </span><span class="s2">"down_arrow"</span><span class="w">
      </span><span class="p">},</span><span class="w">
      </span><span class="nl">"to"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
        </span><span class="p">{</span><span class="w">
          </span><span class="nl">"key_code"</span><span class="p">:</span><span class="w"> </span><span class="s2">"return_or_enter"</span><span class="w">
        </span><span class="p">}</span><span class="w">
      </span><span class="p">],</span><span class="w">
      </span><span class="nl">"conditions"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
        </span><span class="p">{</span><span class="w">
          </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"device_if"</span><span class="p">,</span><span class="w">
          </span><span class="nl">"identifiers"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
            </span><span class="p">{</span><span class="w">
              </span><span class="nl">"vendor_id"</span><span class="p">:</span><span class="w"> </span><span class="mi">4643</span><span class="p">,</span><span class="w">
              </span><span class="nl">"product_id"</span><span class="p">:</span><span class="w"> </span><span class="mi">15975</span><span class="w">
            </span><span class="p">}</span><span class="w">
          </span><span class="p">]</span><span class="w">
        </span><span class="p">}</span><span class="w">
      </span><span class="p">]</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h3 id="why-this-matters-for-ai">Why this matters for AI</h3>

<p>I pair Wispr Flow with ChatGPT Pro and Claude's 20x plan.</p>

<p>When you dictate prompts instead of typing them, you provide far more context. You can discover what you need to say while you are saying it. You do not need to correct yourself or be precise on the first try, because if you provide more context as you speak, the AI understands how you arrived at your conclusion. It can synthesize a better result than if you had carefully typed out a polished, minimal prompt.</p>

<p>Traditionally, when we speak, we filter ourselves. We are afraid of being redundant or imprecise or embarrassing ourselves. But when you are talking to a machine, there is no social pressure. You can speak like a child, without thinking about presentation, and provide far more context than you would ever type. The machine can handle the noise and extract the signal. The result is better output, consistently, than what I get from typed prompts.</p>

<h2 id="voxtral-transcribe-2">Voxtral Transcribe 2</h2>

<p>Mistral recently released <a href="https://mistral.ai/news/voxtral-transcribe-2">Voxtral Transcribe 2</a>, their speech-to-text API. It offers transcription in 13 languages with speaker diarization, context biasing, and word-level timestamps. The pricing is $0.003 per minute for the batch API and $0.006 per minute for the real-time API. To put that in perspective, transcribing one hour of audio costs $0.18 with the batch API or $0.36 with the real-time API. My 243,554 words at 129 words per minute amount to roughly 1,888 minutes of speech. Transcribing all of that would have cost about $5.66 with the batch API or $11.33 with the real-time API. That is cheaper than a month of Wispr Flow Pro at $15/month, and I used it heavily every single day. You can also self-host Voxtral since the real-time model's weights are open under Apache 2.0. The raw economics make it very practical for any application that needs speech-to-text.</p>

<p>That said, Wispr Flow is worth every cent. It works on macOS and iOS, it auto-edits your speech, it learns your vocabulary, and it just works everywhere. The value is in the polish and integration, not just the transcription. But it is interesting to see that the underlying transcription itself has become so cheap that building on top of these APIs is very accessible.</p>

<p>Wispr Flow currently seems to use <a href="https://openai.com/index/whisper/">Whisper</a> under the hood (the "Wispr" in the name is a play on "Whisper"). I am curious whether they will adopt an API like Voxtral, or whether the competitive landscape will push these models to converge. Voxtral does not yet have the equivalent of Wispr's custom dictionary, but Mistral does offer context biasing, which lets you provide a list of terms to improve recognition accuracy for domain-specific vocabulary.</p>

<h3 id="trying-out-the-api">Trying out the API</h3>

<p>I wanted to try the Voxtral API myself. The demos on Mistral's site did not work for me because they do not support a hold-to-record interaction, which is the main use case I care about. So I threw together a simple browser-based test page to play with it.</p>

<p><img src="/assets/posts/voxtral-transcribe-and-wispr-flow/voxtral-app.png" alt="The Voxtral Transcribe test app." /></p>

<p>It is a single HTML page. You enter your <a href="https://console.mistral.ai">Mistral API key</a>, hold a button, speak, and get the transcription back with timestamps, speaker labels, and raw JSON. There is no backend. Your API key stays in your browser's localStorage and audio is sent directly to Mistral's API.</p>

<p><strong><a href="https://modulovalue.github.io/voxtral-transcribe-test/">Try it here</a></strong> or <a href="https://github.com/modulovalue/voxtral-transcribe-test">view the source on GitHub</a>.</p>

<p>You can also download the page to your desktop and run it locally. Nothing is shared, nothing is collected.</p>

<p>Features:</p>
<ul>
  <li>Hold-to-record and toggle modes</li>
  <li>Speaker diarization (identifies and labels who is speaking at any given moment, so a recording of a conversation comes back with "Speaker 0" and "Speaker 1" labels rather than a single block of text)</li>
  <li>Segment and word-level timestamps</li>
  <li>Language selection (13 languages)</li>
  <li>Context biasing for custom terminology</li>
  <li>Copy text and raw JSON output</li>
</ul>

<p>What I observed by building this is that Voxtral is noticeably faster than what Wispr Flow currently uses. With Wispr Flow, roughly every 30-40 messages I have to wait several seconds for the transcription to come through, and roughly every 60 to 70 messages fail completely. It is annoying, but the overall value Wispr Flow provides is good enough that I keep using it despite these issues.</p>

<p>The Voxtral API felt faster and I am very excited to see how reliable it becomes. If someone builds a dictation tool on top of this API, please let me know. I will be your first paying customer.</p>

<p>I hope more applications adopt APIs like this, and I hope it pushes the entire speech-to-text space forward. I cannot imagine going back to typing everything.</p>

<hr />

<p><strong>Addendum (February 10, 2026):</strong></p>

<p>After this post went live, <a href="https://github.com/efimovnikita">Nikita Efimov</a> reached out to share <a href="https://github.com/efimovnikita/DictationSolutions">DictationSolutions</a>, a collection of dictation tools including WhisperInk. It is a Windows application, so I have not been able to test it, but it is nice to see people building in this space.</p>

<p>I also open-sourced <a href="https://github.com/modulovalue/VoxtralDictate">VoxtralDictate</a>, the macOS menu bar app I built on top of Mistral's Voxtral API. Press a keyboard combination to start recording, press it again to stop, and the transcript is pasted at your cursor. In practice, the API turned out to be noticeably slower than my initial tests suggested. I am not sure if I am doing something wrong, but compared to Wispr Flow the experience is much worse. And beyond raw speed, getting the user experience right for a dictation tool is a lot of work that I do not want to put in, since this is not something I want to build and maintain as an application. What I would love to see is an open-source dictation tool that gets the UI and UX right and makes the underlying speech-to-text API configurable, so you can swap between different providers. I am not aware of such a tool.</p>

<p>In other news, someone <a href="https://news.ycombinator.com/item?id=46954136">posted on Hacker News</a> a <a href="https://github.com/TrevorS/voxtral-mini-realtime-rs">Rust implementation of Voxtral Mini 4B</a> that runs in the browser via WebAssembly and WebGPU. The quantized model is about 2.5 GB. I am curious whether this is how it becomes feasible to run speech-to-text locally, or on a dedicated inexpensive server. I would love to see a dictation tool that performs consistently and runs entirely locally.</p>

<hr />

<p>* The <a href="https://wisprflow.ai/r?MODESTAS3">Wispr Flow link</a> above is a referral link. You get a free month if you use it. I do not care, I am already paying for it. But if you want to use it, feel free.</p>]]></content><author><name>Modestas Valauskas</name></author><category term="productivity" /><summary type="html"><![CDATA[How Wispr Flow and Mistral's Voxtral Transcribe API changed my workflow. I built a browser test app for Voxtral and dictated the equivalent of two books in just over a month.]]></summary></entry><entry><title type="html">Benchmarking my parser generator against LLVM: I have a new target</title><link href="https://modulovalue.com/blog/benchmarking-against-llvm-parser/" rel="alternate" type="text/html" title="Benchmarking my parser generator against LLVM: I have a new target" /><published>2026-01-18T00:00:00+01:00</published><updated>2026-01-18T00:00:00+01:00</updated><id>https://modulovalue.com/blog/benchmarking-against-llvm-parser</id><content type="html" xml:base="https://modulovalue.com/blog/benchmarking-against-llvm-parser/"><![CDATA[<p>This is a follow-up to my previous post, <a href="/blog/syscall-overhead-tar-gz-io-performance/">I built a 2x faster lexer, then discovered I/O was the real bottleneck</a>. In that post, I benchmarked my ARM64 assembly lexer against the official Dart scanner. This time, I wanted to answer a different question: how fast should a parser generator be able to go?</p>

<p>I found my answer by benchmarking against LLVM.</p>

<h2 id="the-setup">The setup</h2>

<p>I have been working on a parser generator that produces LALR(k) parsers. To test it, I wrote a grammar for LLVM's textual IR format, a language complex enough to require LALR(2). My test corpus consists of 12,161 LLVM IR files totaling 112 MB, extracted from the LLVM test suite.</p>

<p>I benchmarked three lexers and three parsers:</p>

<p><strong>Lexers:</strong></p>
<ul>
  <li>A Dart-based lexer (generated from a specification by my lexer generator)</li>
  <li>An ARM64 assembly lexer (generated from a specification by my lexer generator)</li>
  <li>LLVM's official lexer (called via FFI)</li>
</ul>

<p><strong>Parsers:</strong></p>
<ul>
  <li>A table-driven recursive ascent parser in Dart (interpreted, not optimized)</li>
  <li>A table-driven recursive ascent parser (code-generated from a specification by my parser generator)</li>
  <li>LLVM's official parser (called via FFI)</li>
</ul>

<p>A note on terminology: most programmers are familiar with <strong>recursive descent</strong>, a top-down parsing technique where each grammar rule becomes a function that calls other rule functions. <strong><a href="https://www.abubalay.com/blog/2018/04/08/recursive-ascent">Recursive ascent</a></strong> is the bottom-up counterpart: the call stack mirrors the LR parse stack, and functions return by "ascending" to parent rules after reducing a production. Both of my parsers use this technique to implement LALR parsing.</p>

<p>Both of my parsers are fully deterministic. I was able to implement a deterministic LLVM IR parser, which means no backtracking and no non-deterministic steps. This also means that generalized parsing algorithms like GLR or GLL would not help here, as they are designed to handle ambiguous or (more broadly) non-deterministic grammars. Similarly, PEG parsers use ordered choice to implicitly disambiguate grammars, but since my grammar is LALR(2) and is therefore deterministic, this offers no advantage.</p>

<h2 id="the-results">The results</h2>

<p>First, let me note that I/O remains a significant cost. Loading 12,161 files into memory took 2.5 seconds, nearly four times longer than the fastest parser. The lessons from my <a href="/blog/syscall-overhead-tar-gz-io-performance/">previous post</a> still apply.</p>

<h3 id="lexer-comparison">Lexer comparison</h3>

<table>
  <thead>
    <tr>
      <th>Lexer</th>
      <th>Time</th>
      <th>Throughput</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>LLVM (FFI)</td>
      <td>248ms</td>
      <td>451 MB/s</td>
    </tr>
    <tr>
      <td>ARM64 ASM</td>
      <td>293ms</td>
      <td>382 MB/s</td>
    </tr>
    <tr>
      <td>Dart</td>
      <td>653ms</td>
      <td>172 MB/s</td>
    </tr>
  </tbody>
</table>

<p>My assembly lexer is only 1.18x slower than LLVM's. This is encouraging.</p>

<p>The Dart lexer is 2.6x slower, but this is not a reflection of Dart as a language. The Dart lexer is table-driven rather than direct-coded, and it exists as a quick proof of concept so I do not have to run a code generation step during development. There are many optimization techniques I could apply, like compiling state transitions into functions, but that defeats the purpose of having a fast iteration loop.</p>

<h3 id="parser-comparison">Parser comparison</h3>

<p>Here is where things get interesting.</p>

<table>
  <thead>
    <tr>
      <th>Parser</th>
      <th>Time</th>
      <th>Throughput</th>
      <th>Notes</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>LLVM (FFI)</td>
      <td>647ms</td>
      <td>173 MB/s</td>
      <td>Combined lex + parse</td>
    </tr>
    <tr>
      <td>Generated recursive ascent</td>
      <td>3,745ms</td>
      <td>30 MB/s</td>
      <td>Parse only, tokens pre-lexed</td>
    </tr>
    <tr>
      <td>Non-generated recursive ascent</td>
      <td>6,560ms</td>
      <td>17 MB/s</td>
      <td>Parse only, tokens pre-lexed</td>
    </tr>
  </tbody>
</table>

<p><strong>Important caveat:</strong> my parser only collects parse events without building any data structure. It also parses whitespace, which LLVM skips. This is a deliberate design choice: one of my goals is lossless parsing, where the original source can be reconstructed from the parse tree. This enables tools like pretty printers, incremental parsing, and educational tooling that can answer questions like "which grammar rules produced this range of code?" But it does add overhead compared to LLVM's approach.</p>

<p>LLVM's parser does far more in other ways: it builds a complete intermediate representation (IR) with type checking, symbol resolution, and validation. Despite doing less semantic work, my parser is significantly slower. This makes LLVM's speed even more impressive, and it means the real gap is even larger than these numbers suggest. To truly compete, I would need to not only match LLVM's parsing speed but also add IR construction on top.</p>

<p>LLVM lexes and parses 112 MB in 647 milliseconds. My best parser, using pre-lexed tokens, takes 3.7 seconds just to parse. The full pipeline comparison is stark:</p>

<table>
  <thead>
    <tr>
      <th>Pipeline</th>
      <th>Time</th>
      <th>vs LLVM</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>LLVM lex + parse</td>
      <td>647ms</td>
      <td>1x</td>
    </tr>
    <tr>
      <td>Dart lex + generated parser</td>
      <td>4,398ms</td>
      <td>6.8x slower</td>
    </tr>
    <tr>
      <td>Dart lex + non-generated parser</td>
      <td>7,213ms</td>
      <td>11x slower</td>
    </tr>
  </tbody>
</table>

<p>To beat LLVM, I would need to parse in under 400 milliseconds (since my assembly lexer takes about 250ms). My current parser takes 3.7 seconds. That is a 9x gap.</p>

<h2 id="why-llvm-is-so-fast">Why LLVM is so fast</h2>

<p>LLVM's parser is a hand-written recursive descent parser. It does not generate an intermediate parse tree or abstract syntax tree. Instead, it constructs LLVM's intermediate representation (IR) directly during parsing.</p>

<p>This is not a tree. It is a graph. Instructions reference other instructions. Basic blocks reference other basic blocks. Functions reference global values. The parser builds this graph incrementally as it descends through the grammar.</p>

<p>This design has significant implications for parser generators.</p>

<h2 id="the-problem-with-fusing-parsing-and-ir-construction">The problem with fusing parsing and IR construction</h2>

<p>The key insight is not that bottom-up parsers are inherently slow. The problem is that LLVM <strong>fuses</strong> parsing with IR construction, and this fusion is difficult to achieve with bottom-up parsing.</p>

<p>In a <strong>top-down parser</strong> (LL family, including recursive descent), you process nodes in pre-order: parents before children. When you enter a function, you can create the function object immediately. When you encounter instructions, you attach them to the already-existing function. The parent context is always available.</p>

<p>In a <strong>bottom-up parser</strong> (LR family, including recursive ascent), you process nodes in post-order: children before parents. When you reduce a production, all children have been parsed, but the parent does not exist yet. You cannot attach children to a parent that has not been created.</p>

<p>This means a bottom-up parser cannot easily inline the construction of a graph structure during parsing. You have two choices:</p>

<ol>
  <li>
    <p><strong>Build an intermediate tree first</strong>, then transform it into the final representation in a separate pass. This adds object creation overhead and an extra traversal.</p>
  </li>
  <li>
    <p><strong>Collect parse events</strong> and defer construction. This is what my parser does. It avoids intermediate tree allocation, but to build the final IR, you would need a separate pass over those events. Whether it is even practical to build a graph structure like LLVM's IR from bottom-up parse events is unclear.</p>
  </li>
</ol>

<p>LLVM avoids both of these costs by building the IR directly as it parses. The IR is the intermediate representation, but there is no intermediate step between parsing and the IR, no AST, no extra pass. The recursive descent structure naturally provides the parent context needed to build the graph incrementally.</p>

<p>Could a bottom-up parser achieve the same fusion? Perhaps, but it would not be straightforward. The post-order nature of bottom-up parsing means you would need creative workarounds to maintain parent context during reductions. I have not found an elegant solution.</p>

<h2 id="is-llvms-lexer-regular">Is LLVM's lexer regular?</h2>

<p>Here is an interesting observation: LLVM's lexer appears to be mostly regular. There are no nested comments, no string interpolation. In principle, it should be a pure deterministic finite automaton with no additional state.</p>

<p>This is unusual. Consider Dart, which I lexed in my previous post. To correctly lex Dart, you need a stack:</p>

<ul>
  <li>C-style block comments can be nested (<code class="language-plaintext highlighter-rouge">/* outer /* inner */ still outer */</code>)</li>
  <li>String interpolation creates nested lexical contexts (<code class="language-plaintext highlighter-rouge">"Hello ${name.toUpperCase()}"</code>)</li>
</ul>

<p>LLVM IR has neither. The lexical structure should be regular.</p>

<h2 id="the-cost-of-hand-written-lexers">The cost of hand-written lexers</h2>

<p>However, looking at the actual implementation, there are some context-sensitive parts. The lexer has a flag that controls how colons in identifiers are handled:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// If we stopped due to a colon, unless we were directed to ignore it,</span>
<span class="c1">// this really is a label.</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">IgnoreColonInIdentifiers</span> <span class="o">&amp;&amp;</span> <span class="o">*</span><span class="n">CurPtr</span> <span class="o">==</span> <span class="sc">':'</span><span class="p">)</span> <span class="p">{</span>
  <span class="n">StrVal</span><span class="p">.</span><span class="n">assign</span><span class="p">(</span><span class="n">StartChar</span><span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="n">CurPtr</span><span class="o">++</span><span class="p">);</span>
  <span class="k">return</span> <span class="n">lltok</span><span class="o">::</span><span class="n">LabelStr</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p><small><a href="https://github.com/llvm/llvm-project/blob/b0eddb4256a6e584a1779d5901676b94d572fef3/llvm/lib/AsmParser/LLLexer.cpp#L509-L514">llvm/lib/AsmParser/LLLexer.cpp, lines 509-514</a></small></p>

<p>The parser sets this flag when parsing summary entries:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// For summary entries, colons should be treated as distinct tokens,</span>
<span class="c1">// not an indication of the end of a label token.</span>
<span class="n">Lex</span><span class="p">.</span><span class="n">setIgnoreColonInIdentifiers</span><span class="p">(</span><span class="nb">true</span><span class="p">);</span>
<span class="c1">// ... parsing code ...</span>
<span class="n">Lex</span><span class="p">.</span><span class="n">setIgnoreColonInIdentifiers</span><span class="p">(</span><span class="nb">false</span><span class="p">);</span>
</code></pre></div></div>
<p><small><a href="https://github.com/llvm/llvm-project/blob/b0eddb4256a6e584a1779d5901676b94d572fef3/llvm/lib/AsmParser/LLParser.cpp#L1097-L1106">llvm/lib/AsmParser/LLParser.cpp, lines 1097-1106</a></small></p>

<p>And again when parsing memory attributes:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// We use syntax like memory(argmem: read), so the colon should not be</span>
<span class="c1">// interpreted as a label terminator.</span>
<span class="n">Lex</span><span class="p">.</span><span class="n">setIgnoreColonInIdentifiers</span><span class="p">(</span><span class="nb">true</span><span class="p">);</span>
</code></pre></div></div>
<p><small><a href="https://github.com/llvm/llvm-project/blob/b0eddb4256a6e584a1779d5901676b94d572fef3/llvm/lib/AsmParser/LLParser.cpp#L2581-L2588">llvm/lib/AsmParser/LLParser.cpp, lines 2581-2588</a></small></p>

<p>This is unfortunate. It introduces coupling between the parser and lexer that would not exist if the lexical grammar were truly regular.</p>

<p>This is the kind of issue that would not arise if the lexer were generated from a specification. A formal specification forces you to make the lexical grammar explicit, and a generator will not even support context-sensitive rules. Hand-written lexers make it too easy to add "just one flag" to handle an edge case, and over time these accumulate. The LLVM team has full control over the IR format and regularly introduces breaking changes, so this could be cleaned up. But the temptation to add a quick fix is always there.</p>

<h2 id="simd-and-the-case-for-generation">SIMD and the case for generation</h2>

<p>Despite these impurities, the lexer is close enough to regular that SIMD techniques should still apply. Parsing regular languages with SIMD is a well-understood problem. Projects like <a href="https://github.com/intel/hyperscan">Intel Hyperscan</a> and its fork <a href="https://github.com/VectorCamp/vectorscan">Vectorscan</a> (which adds ARM NEON support) demonstrate that SIMD-accelerated regex matching can achieve remarkable throughput. Academic work on <a href="https://dl.acm.org/citation.cfm?id=2933357">SIMD-accelerated regular expression matching</a> shows 2-5x speedups over scalar code. Other projects like <a href="https://github.com/coreperf/rejit">Rejit</a> and <a href="https://github.com/MartinErhardt/RoaringRegex">RoaringRegex</a> explore similar territory.</p>

<p>This is actually an argument for lexer generation. A hand-written lexer is unlikely to use SIMD intrinsics for every token type, and it is prone to accumulating context-sensitive hacks over time. A lexer generator could automatically emit SIMD-optimized code while enforcing that the lexical grammar remains regular. The fact that my current generated lexer is slower than LLVM's hand-written one does not mean generation is a dead end. It means there is room for improvement, and SIMD is one concrete path forward.</p>

<p>Real-world programming languages are messier. They have nested comments, string interpolation, heredocs, and other context-sensitive lexical structures that genuinely require a stack (looking at you, JavaScript, and your <a href="https://tc39.es/ecma262/multipage/ecmascript-language-lexical-grammar.html">context-sensitive regex syntax</a>). But for languages whose lexical structure could be regular, generation offers both performance opportunities and protection against accidental complexity.</p>

<h2 id="what-this-means-for-my-project">What this means for my project</h2>

<p>I now have a clear target: LLVM processes 112 MB of complex IR in 647ms, achieving 173 MB/s for combined lexing and parsing.</p>

<p>If I can match LLVM's performance, I will have done something significant. LLVM is one of the largest and most optimized compiler projects in the world, with contributions from Apple, Google, ARM, Intel, and dozens of other companies. It is not a low bar.</p>

<p>But I think it is achievable. The lexing side is already close (293ms vs 248ms). The parsing side needs work.</p>

<p>To close the gap, I see several paths:</p>

<ol>
  <li>
    <p><strong>Optimize the current parser.</strong> My table-driven parser has not been optimized. Direct-coded parsing instead of table interpretation should offer a significant speedup.</p>
  </li>
  <li>
    <p><strong>SIMD tricks.</strong> Projects like <a href="https://github.com/simdjson/simdjson">simdjson</a> achieve remarkable speeds by processing multiple bytes in parallel. Some of these techniques could be applied to parser generators.</p>
  </li>
  <li>
    <p><strong>Generate recursive descent parsers.</strong> My parser generator currently produces LALR(k) parsers. I could also support generating recursive descent (LL) parsers, which would allow fusing parsing with IR construction. Since LLVM IR requires LALR(2), I suspect I would need at least LL(2) to handle the same language, though I am not entirely certain this would be sufficient.</p>
  </li>
  <li>
    <p><strong>Profile-guided optimization.</strong> If I generate parsers that compile to LLVM IR, I could use <a href="https://llvm.org/docs/HowToBuildWithPGO.html">LLVM's PGO</a> to optimize them. Using LLVM to beat LLVM has a certain poetic appeal.</p>
  </li>
</ol>

<p>The more interesting question is whether I can match LLVM's performance while still generating parsers from grammars, rather than writing them by hand. That would be genuinely useful. Once you have a context-free grammar that corresponds precisely to the implementation, you can generate pretty printers from a simple specification, build IR builders for any programming language that are proven to match the implementation, build IDE tooling, implement incremental parsing, and more. Changes to the grammar could be proven to be additive rather than breaking. Breaking changes could be introduced in a backwards-compatible manner with proper documentation.</p>

<p>Of course, migrating from a hand-written parser to a generated one is a difficult sell when it is several times slower. But I believe this is a solvable problem, and I am actively working on it. If you have ideas, feedback, or just want to follow along, feel free to reach out.</p>

<h2 id="conclusion">Conclusion</h2>

<p>I set out to find a target for parser performance. I found one: LLVM processes 173 MB/s. My generated parser currently achieves 30 MB/s, a 5.8x gap. And since my parser does less work than LLVM's (no IR construction), the effective gap is even larger.</p>

<p>The gap exists not because bottom-up parsing is inherently slow, but because LLVM's architecture fuses parsing with IR construction in a way that requires pre-order traversal. To compete, I may need to generate top-down parsers, or find SIMD tricks that make the parsing paradigm less relevant.</p>

<p>I am not there yet. But I have a target now.</p>]]></content><author><name>Modestas Valauskas</name></author><category term="technical" /><category term="performance" /><category term="parsing" /><summary type="html"><![CDATA[LLVM lexes and parses 112 MB of IR in 647ms. My generated parser takes 4.4 seconds. This gap reveals a fundamental limitation of bottom-up parsing.]]></summary></entry><entry><title type="html">I built a 2x faster lexer, then discovered I/O was the real bottleneck</title><link href="https://modulovalue.com/blog/syscall-overhead-tar-gz-io-performance/" rel="alternate" type="text/html" title="I built a 2x faster lexer, then discovered I/O was the real bottleneck" /><published>2026-01-13T00:00:00+01:00</published><updated>2026-01-13T00:00:00+01:00</updated><id>https://modulovalue.com/blog/syscall-overhead-tar-gz-io-performance</id><content type="html" xml:base="https://modulovalue.com/blog/syscall-overhead-tar-gz-io-performance/"><![CDATA[<p>I built an ARM64 assembly lexer (well, I generated one from my own parser generator, but this post is not about that) that processes Dart code 2x faster than the official scanner, a result I achieved using <a href="/blog/statistical-methods-for-reliable-benchmarks/">statistical methods to reliably measure small performance differences</a>. Then I benchmarked it on 104,000 files and discovered my lexer was not the bottleneck. I/O was. This is the story of how I accidentally learned why <a href="https://pub.dev">pub.dev</a> stores packages as tar.gz files.</p>

<h2 id="the-setup">The setup</h2>

<p>I wanted to benchmark my lexer against the official Dart scanner. The pub cache on my machine had 104,000 Dart files totaling 1.13 GB, a perfect test corpus. I wrote a benchmark that:</p>

<ol>
  <li>Reads each file from disk</li>
  <li>Lexes it</li>
  <li>Measures time separately for I/O and lexing</li>
</ol>

<p>Simple enough.</p>

<h2 id="the-first-surprise-lexing-is-fast">The first surprise: lexing is fast</h2>

<p>Here are the results:</p>

<table>
  <thead>
    <tr>
      <th>Metric</th>
      <th>ASM Lexer</th>
      <th>Official Dart</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Lex time</td>
      <td>2,807 ms</td>
      <td>6,087 ms</td>
    </tr>
    <tr>
      <td>Lex throughput</td>
      <td>402 MB/s</td>
      <td>185 MB/s</td>
    </tr>
  </tbody>
</table>

<p>My lexer was 2.17x faster. Success! But wait:</p>

<table>
  <thead>
    <tr>
      <th>Metric</th>
      <th>ASM Lexer</th>
      <th>Official Dart</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>I/O time</td>
      <td>14,126 ms</td>
      <td>14,606 ms</td>
    </tr>
    <tr>
      <td><strong>Total time</strong></td>
      <td><strong>16,933 ms</strong></td>
      <td><strong>20,693 ms</strong></td>
    </tr>
    <tr>
      <td><strong>Total speedup</strong></td>
      <td><strong>1.22x</strong></td>
      <td>-</td>
    </tr>
  </tbody>
</table>

<p>The total speedup was only 1.22x. My 2.17x lexer improvement was being swallowed by I/O. Reading files took 5x longer than lexing them.</p>

<h2 id="the-second-surprise-the-ssd-is-not-the-bottleneck">The second surprise: the SSD is not the bottleneck</h2>

<p>My MacBook has an NVMe SSD that can read at 5-7 GB/s. I was getting 80 MB/s. That is 1.5% of the theoretical maximum.</p>

<p>The problem was not the disk. It was the syscalls.</p>

<p>For 104,000 files, the operating system had to execute:</p>
<ul>
  <li>104,000 <code class="language-plaintext highlighter-rouge">open()</code> calls</li>
  <li>104,000 <code class="language-plaintext highlighter-rouge">read()</code> calls</li>
  <li>104,000 <code class="language-plaintext highlighter-rouge">close()</code> calls</li>
</ul>

<p>That is over 300,000 syscalls. Each syscall involves:</p>
<ul>
  <li>A context switch from user space to kernel space</li>
  <li>Kernel bookkeeping and permission checks</li>
  <li>A context switch back to user space</li>
</ul>

<p>Each syscall costs roughly 1-5 microseconds. Multiply that by 300,000 and you get 0.3-1.5 seconds of pure overhead, before any actual disk I/O happens. Add filesystem metadata lookups, directory traversal, and you understand where the time goes.</p>

<p>I tried a few things that did not help much. Memory-mapping the files made things worse due to the per-file mmap/munmap overhead. Replacing Dart's file reading with direct FFI syscalls (open/read/close) only gave a 5% improvement. The problem was not Dart's I/O layer, it was the sheer number of syscalls.</p>

<h2 id="the-hypothesis">The hypothesis</h2>

<p>I have mirrored pub.dev several times in the past and noticed that all packages are stored as tar.gz archives. I never really understood why, but this problem reminded me of that fact. If syscalls are the problem, the solution is fewer syscalls. What if instead of 104,000 files, I had 1,351 files (one per package)?</p>

<p>I wrote a script to package each cached package into a tar.gz archive:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>104,000 individual files -&gt; 1,351 tar.gz archives
1.13 GB uncompressed     -&gt; 169 MB compressed (6.66x ratio)
</code></pre></div></div>

<h2 id="the-results">The results</h2>

<table>
  <thead>
    <tr>
      <th>Metric</th>
      <th>Individual Files</th>
      <th>tar.gz Archives</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Files/Archives</td>
      <td>104,000</td>
      <td>1,351</td>
    </tr>
    <tr>
      <td>Data on disk</td>
      <td>1.13 GB</td>
      <td>169 MB</td>
    </tr>
    <tr>
      <td>I/O time</td>
      <td>14,525 ms</td>
      <td>339 ms</td>
    </tr>
    <tr>
      <td>Decompress time</td>
      <td>-</td>
      <td>4,507 ms</td>
    </tr>
    <tr>
      <td>Lex time</td>
      <td>2,968 ms</td>
      <td>2,867 ms</td>
    </tr>
    <tr>
      <td><strong>Total time</strong></td>
      <td><strong>17,493 ms</strong></td>
      <td><strong>7,713 ms</strong></td>
    </tr>
  </tbody>
</table>

<p>The I/O speedup was <strong>42.85x</strong>. Reading 1,351 sequential files instead of 104,000 random files reduced I/O from 14.5 seconds to 339 milliseconds.</p>

<p>The total speedup was <strong>2.27x</strong>. Even with decompression overhead, the archive approach was more than twice as fast.</p>

<h2 id="breaking-down-the-numbers">Breaking down the numbers</h2>

<h3 id="io-14525-ms-to-339-ms">I/O: 14,525 ms to 339 ms</h3>

<p>This is the syscall overhead in action. Going from 300,000+ syscalls to roughly 4,000 syscalls (open/read/close for 1,351 archives) eliminated most of the overhead.</p>

<p>Additionally, reading 1,351 files sequentially is far more cache-friendly than reading 104,000 files scattered across the filesystem. The OS can prefetch effectively, the SSD can batch operations, and the page cache stays warm.</p>

<h3 id="decompression-4507-ms">Decompression: 4,507 ms</h3>

<p>gzip decompression ran at about 250 MB/s using the <code class="language-plaintext highlighter-rouge">archive</code> package from pub.dev. This is now the new bottleneck. I did not put much effort into optimizing decompression, an FFI-based solution using native zlib could be significantly faster. Modern alternatives like lz4 or zstd might also help.</p>

<h3 id="compression-ratio-666x">Compression ratio: 6.66x</h3>

<p>Source code compresses well. The 1.13 GB of Dart code compressed to 169 MB. This means less data to read from disk, which helps even on fast SSDs.</p>

<h2 id="why-pubdev-uses-targz">Why pub.dev uses tar.gz</h2>

<p align="center">
  <img src="/assets/pub-dev-versions-page.png" alt="pub.dev versions page with download button" />
</p>

<p align="center">
  <img src="/assets/pub-dev-tar-gz-download.png" alt="pub.dev package download showing flame-1.34.0.tar.gz" />
</p>

<p>This experiment accidentally explains the pub.dev package format. When you run <code class="language-plaintext highlighter-rouge">dart pub get</code>, you download tar.gz archives, not individual files. The reasons are now obvious:</p>

<ol>
  <li><strong>Fewer HTTP requests.</strong> One request per package instead of hundreds.</li>
  <li><strong>Bandwidth savings.</strong> 6-7x smaller downloads.</li>
  <li><strong>Faster extraction.</strong> Sequential writes beat random writes.</li>
  <li><strong>Reduced syscall overhead.</strong> Both on the server (fewer files to serve) and the client (fewer files to write).</li>
  <li><strong>Atomicity.</strong> A package is either fully downloaded or not. No partial states.</li>
</ol>

<p>The same principles apply to npm (tar.gz), Maven (JAR/ZIP), PyPI (wheel/tar.gz), and virtually every package manager.</p>

<h2 id="the-broader-lesson">The broader lesson</h2>

<p>Modern storage is fast. NVMe SSDs can sustain gigabytes per second. But that speed is only accessible for sequential access to large files. The moment you introduce thousands of small files, syscall overhead dominates.</p>

<p>This matters for:</p>

<ul>
  <li><strong>Build systems.</strong> Compiling a project with 10,000 source files? The filesystem overhead might exceed the compilation time.</li>
  <li><strong>Log processing.</strong> Millions of small log files? Concatenate them. Claude uses <a href="https://jsonlines.org/">JSONL</a> for this reason.</li>
  <li><strong>Backup systems.</strong> This is why rsync and tar exist.</li>
</ul>

<h2 id="what-i-would-do-differently">What I would do differently</h2>

<p>If I were optimizing this further:</p>

<ol>
  <li><strong>Use zstd instead of gzip.</strong> 4-5x faster decompression with similar compression ratios.</li>
  <li><strong>Use uncompressed tar for local caching.</strong> Skip decompression entirely, still get the syscall reduction.</li>
  <li><strong>Parallelize with isolates.</strong> Multiple cores decompressing multiple archives simultaneously.</li>
</ol>

<h2 id="conclusion">Conclusion</h2>

<p>I set out to benchmark a lexer and ended up learning about syscall overhead. The lexer was 2x faster. The I/O optimization was 43x faster.</p>

<hr />

<h2 id="addendum-reader-suggestions">Addendum: Reader Suggestions</h2>

<h3 id="linux-specific-optimizations">Linux-Specific Optimizations</h3>

<p><a href="https://www.reddit.com/r/ProgrammingLanguages/comments/1qbvvpn/comment/nzdms8e/">servermeta_net pointed out</a> two Linux-specific approaches: disabling speculative execution mitigations (which could improve performance in syscall-heavy scenarios) and using io_uring for asynchronous I/O. I ran these benchmarks on macOS, which does not support io_uring, but these Linux capabilities are intriguing. A follow-up post exploring how I/O performance can be optimized on Linux may be in order.</p>

<p><a href="https://news.ycombinator.com/item?id=46755420">king_geedorah elaborated</a> on how io_uring could help with this specific workload: open the directory file descriptor, extract all filenames via readdir, then submit all openat requests as submission queue entries (SQEs) at once. This batches what would otherwise be 104,000 sequential open() syscalls into a single submission, letting the kernel process them concurrently. The io_uring_prep_openat function prepares these batched open operations. This is closer to the "load an entire directory into an array of file descriptors" primitive that this workload really needs.</p>

<h3 id="macos-specific-optimizations">macOS-Specific Optimizations</h3>

<p><a href="https://www.reddit.com/r/ProgrammingLanguages/comments/1qbvvpn/comment/nze8xm7/">tsanderdev pointed out</a> that macOS's <code class="language-plaintext highlighter-rouge">kqueue</code> could potentially improve performance for this workload. While <code class="language-plaintext highlighter-rouge">kqueue</code> is not equivalent to Linux's <code class="language-plaintext highlighter-rouge">io_uring</code> (it lacks the same syscall batching through a shared ring buffer), it may still offer some improvement over synchronous I/O. I have not benchmarked this yet.</p>

<h3 id="macos-vs-linux-syscall-performance">macOS vs Linux Syscall Performance</h3>

<p><a href="https://news.ycombinator.com/item?id=46753360">arter45 noted</a> that macOS may be significantly slower than Linux for certain syscalls, linking to a <a href="https://stackoverflow.com/questions/67483292/why-is-the-c-function-open-4x-slower-on-macos-vs-an-ubuntu-vm">Stack Overflow question</a> showing open() being 4x slower on macOS compared to an Ubuntu VM. <a href="https://news.ycombinator.com/item?id=46753665">jchw explained</a> that Linux's VFS layer is aggressively optimized: it uses RCU (Read-Copy-Update) schemes liberally to make filesystem operations minimally contentious, and employs aggressive dentry caching. Linux also separates dentries and generic inodes, whereas BSD/UNIX systems consolidate these into vnode structures. This suggests my benchmark results on macOS may actually understate the syscall overhead problem on that platform relative to Linux, or alternatively, that Linux users might see smaller gains from the tar.gz approach since their baseline is already faster.</p>

<h3 id="is-it-really-the-syscalls">Is it really the syscalls?</h3>

<p><a href="https://news.ycombinator.com/item?id=46757655">ori_b pushed back</a> on the claim that syscall overhead is the bottleneck. On a Ryzen machine, entering and exiting the kernel takes about 150 cycles (~50ns). Even at 1 microsecond per mode switch, 300,000 syscalls would account for only 0.3 seconds of the 14.5-second I/O time. That is roughly 2%. The remaining time likely comes from filesystem metadata lookups, inode resolution, directory traversal, and random seek latency. Even NVMe SSDs have ~50-100 microseconds of latency per random read, and 300,000 random reads at that latency would account for most of the measured I/O time. So the framing might be more precisely stated as "per-file overhead" rather than "syscall overhead" since the expensive part is the work happening inside each syscall, not the context switch itself. It is also worth noting that ori_b's numbers are from a Linux Ryzen machine, where syscalls are faster than on macOS (as discussed above), adding another variable. I do not currently have tooling to break down where the 14.5 seconds actually goes, so this is something I want to investigate in the future.</p>

<h3 id="avoiding-lstat-with-getdents64">Avoiding lstat with getdents64</h3>

<p><a href="https://news.ycombinator.com/item?id=46755100">stabbles pointed out</a> that when scanning directories, you can avoid separate lstat() calls by using the <code class="language-plaintext highlighter-rouge">d_type</code> field from <code class="language-plaintext highlighter-rouge">getdents64()</code>. On most popular filesystems (ext4, XFS, Btrfs), the kernel populates this field with the file type directly, so you do not need an additional syscall to determine if an entry is a file or directory. The caveat: some filesystems return <code class="language-plaintext highlighter-rouge">DT_UNKNOWN</code>, in which case you still need to call lstat(). For my workload of scanning the pub cache, this could eliminate tens of thousands of stat syscalls during the directory traversal phase, before even getting to the file opens.</p>

<h3 id="go-monorepo-60x-speedup-by-avoiding-disk-io">Go Monorepo: 60x Speedup by Avoiding Disk I/O</h3>

<p><a href="https://news.ycombinator.com/item?id=46752237">ghthor shared</a> a similar experience optimizing dependency graph analysis in a Go monorepo. Initial profiling pointed to GC pressure, but the real bottleneck was I/O from shelling out to <code class="language-plaintext highlighter-rouge">go list</code>, which performed stat calls and disk reads for every file. By replacing <code class="language-plaintext highlighter-rouge">go list</code> with a custom import parser using Go's standard library and reading file contents from git blobs (using <code class="language-plaintext highlighter-rouge">git ls-files</code> instead of disk stat calls), they reduced analysis time from 30-45 seconds to 500 milliseconds. This is a 60-90x improvement from the same fundamental insight: avoid per-file syscalls when you can batch or bypass them entirely.</p>

<h3 id="haikus-packagefs">Haiku's packagefs</h3>

<p><a href="https://news.ycombinator.com/item?id=46756528">smallstepforman described</a> how <a href="https://www.haiku-os.org/">Haiku OS</a> solves this problem at the operating system level. Haiku packages are single compressed files that are never extracted. Instead, the OS uses <a href="https://www.haiku-os.org/docs/develop/packages/Infrastructure.html">packagefs</a>, a virtual filesystem that presents the contents of all activated packages as a unified directory tree. Applications see normal paths like <code class="language-plaintext highlighter-rouge">/usr/local/lib/foo.so</code>, but the data is actually read from compressed package files in <code class="language-plaintext highlighter-rouge">/system/packages</code>. Install and uninstall are instant since you are just adding or removing a single file, not extracting or deleting thousands. This eliminates the syscall overhead entirely at the OS level rather than working around it at the application level. Haiku is an open-source OS recreating BeOS, known for its responsiveness and clean design. While not mainstream, its package architecture demonstrates that the "extract everything to disk" model most package managers use is not the only option.</p>

<h3 id="squashfs-for-container-runtimes">SquashFS for Container Runtimes</h3>

<p><a href="https://news.ycombinator.com/item?id=46752085">stabbles suggested</a> SquashFS with zstd compression as another alternative. It is used by various container runtimes and is popular in HPC environments where filesystems often have high latency. SquashFS can be mounted natively on Linux or via FUSE, letting you access files normally while the data stays compressed on disk. When <a href="https://news.ycombinator.com/item?id=46753468">questioned about syscall overhead</a>, stabbles noted that even though syscall counts remain high, latency is reduced because the SquashFS file ensures files are stored close together, benefiting significantly from filesystem cache. This is a different tradeoff than tar.gz: you still pay per-file syscall costs, but you gain file locality and can use standard file APIs without explicit decompression. <a href="https://news.ycombinator.com/item?id=46757072">One commenter warned</a> that when mounting a SquashFS image via a loop device, you should use <code class="language-plaintext highlighter-rouge">losetup --direct-io=on</code> to avoid double caching (the compressed backing file and the decompressed contents both being cached), which can <a href="https://lwn.net/Articles/654701/">reduce memory usage significantly</a>.</p>

<h3 id="sqlite-as-an-alternative">SQLite as an Alternative</h3>

<p><a href="https://www.reddit.com/r/ProgrammingLanguages/comments/1qbvvpn/comment/nze0jrf/">tsanderdev mentioned</a> that this is also why SQLite can be <a href="https://sqlite.org/fasterthanfs.html">much faster than a directory with lots of small files</a>. I had completely forgotten about SQLite as an option. Storing file contents in a SQLite database would eliminate the syscall overhead while providing random access to individual files, something tar.gz does not offer.</p>

<p>This also explains something I have heard multiple times: Apple uses SQLite extensively for its applications, <a href="https://news.ycombinator.com/item?id=26218218">storing structured data and metadata in SQLite databases</a> rather than as individual files. <a href="https://lobste.rs/c/lv2wga">snej clarified</a> that Apple's SQLite-based APIs (CoreData, SwiftData) are database APIs with an ORM and queries, not filesystem simulations. The Photos app, for example, uses SQLite for metadata and thumbnails, but the actual photos remain as individual files. Still, the principle holds for the data that is stored in SQLite: if 100,000 files on a modern Mac with NVMe storage takes 14 seconds to read, imagine what it was like on older, slower machines. The syscall overhead would have been even more punishing. For workloads where random access to many small records is needed, SQLite avoids those syscalls entirely. This is worth exploring.</p>

<h3 id="skip-the-cleanup-syscalls">Skip the Cleanup Syscalls</h3>

<p><a href="https://www.reddit.com/r/ProgrammingLanguages/comments/1qbvvpn/comment/nzeaegy/">matthieum suggested</a> a common trick used by batch compilers: never call <code class="language-plaintext highlighter-rouge">free</code>, <code class="language-plaintext highlighter-rouge">close</code>, or <code class="language-plaintext highlighter-rouge">munmap</code>, and instead let the OS reap all resources when the process ends. For a one-shot batch process like a compiler (or a lexer benchmark), there is no point in carefully releasing resources that the OS will reclaim anyway.</p>

<p><a href="https://www.reddit.com/r/ProgrammingLanguages/comments/1qbvvpn/comment/nzguiwt/">GabrielDosReis added a caveat</a>: depending on the workload, you might actually need to call <code class="language-plaintext highlighter-rouge">close</code>, or you could run out of file descriptors. On macOS, you can check your limits with:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ launchctl limit maxfiles
maxfiles    256            unlimited

$ sysctl kern.maxfilesperproc
kern.maxfilesperproc: 61440
</code></pre></div></div>

<p>The first number (256) is the soft limit per process, the second is the hard limit. <code class="language-plaintext highlighter-rouge">kern.maxfilesperproc</code> shows the kernel's per-process maximum. With 104,000 files, skipping <code class="language-plaintext highlighter-rouge">close</code> calls would exhaust even the maximum limit. <a href="https://news.ycombinator.com/item?id=46757150">dinosaurdynasty noted</a> that the low default soft limit is <a href="https://0pointer.net/blog/file-descriptor-limits.html">a historical artifact of the select() syscall</a>, which can only handle file descriptors below 1024. Modern programs can simply raise their soft limit to the hard limit and not worry about it.</p>

<p>There is even a further optimization: use a wrapper process. The wrapper launches a worker process that does all the work. When the worker signals completion (via stdout or a pipe), the wrapper terminates immediately without waiting for its detached child. Any script waiting on the wrapper can now proceed, while the OS asynchronously reaps the worker's resources in the background. I had not considered this approach before, but it seems worth trying.</p>

<p><a href="https://news.ycombinator.com/item?id=46757879">Dwedit noted</a> that on Windows, a similar optimization is to call <code class="language-plaintext highlighter-rouge">CloseHandle</code> from a secondary thread, keeping the main thread unblocked while handles are being released.</p>

<h3 id="linker-strategies-for-fast-exits">Linker Strategies for Fast Exits</h3>

<p><a href="https://www.reddit.com/r/ProgrammingLanguages/comments/1qbvvpn/comment/nzrrrnk/">MaskRay added context</a> about how production linkers handle this exact problem. The <a href="https://github.com/rui314/mold">mold linker</a> uses the wrapper process approach mentioned above, forking a child to do all the work while the parent exits immediately after the child signals completion. This lets build systems proceed without waiting for resource cleanup. The <code class="language-plaintext highlighter-rouge">--no-fork</code> flag disables this behavior for debugging. The <a href="https://github.com/nickelpacket/wild">wild linker</a> follows the same pattern.</p>

<p><a href="https://github.com/llvm/llvm-project/tree/main/lld">lld</a> takes a different approach with two targeted hacks: <a href="https://github.com/llvm/llvm-project/blob/a72958a95dcb7d815c01e20cc819532151d1856d/lld/Common/Filesystem.cpp#L44">async unlink</a> to remove old output files in a background thread, and <a href="https://github.com/llvm/llvm-project/blob/a72958a95dcb7d815c01e20cc819532151d1856d/lld/Common/ErrorHandler.cpp#L108">calling <code class="language-plaintext highlighter-rouge">_exit</code> instead of <code class="language-plaintext highlighter-rouge">exit</code></a> to skip the C runtime's cleanup routines (unless <code class="language-plaintext highlighter-rouge">LLD_IN_TEST</code> is set for testing).</p>

<p>MaskRay notes a tradeoff with the wrapper process approach: when the heavy work runs in a child process, the parent process of the linker (typically a build system) cannot accurately track resource usage of the actual work. This matters for build systems that monitor memory consumption or CPU time.</p>

<h3 id="why-pubdev-actually-uses-targz">Why pub.dev Actually Uses tar.gz</h3>

<p><a href="https://www.reddit.com/r/ProgrammingLanguages/comments/1qbvvpn/comment/nzggloc/">Bob Nystrom from the Dart team clarified</a> that my speculation about pub.dev's format choice was partially wrong. Fewer HTTP requests and bandwidth savings definitely factored into the decision, as did reduced storage space on the server. Atomicity is important too, though archives do not fully solve the problem since downloads and extracts can still fail. However, it is unlikely that the I/O performance benefits (faster extraction, reduced syscall overhead) were considered: pub extracts archives immediately after download, the extraction benefit only occurs once during <code class="language-plaintext highlighter-rouge">pub get</code>, that single extraction is a tiny fraction of a fairly expensive process, and pub never reads the files again except for the pubspec. The performance benefit I measured only applies when repeatedly reading from archives, which is not how pub works.</p>

<p>This raises an interesting question: what if pub did not extract archives at all? For a clean (non-incremental) compilation of a large project like the Dart Analyzer with hundreds of dependencies, the compiler needs to access thousands of files across many packages. If packages remained in an archive format with random access support (like ZIP), the syscall overhead from opening and closing all those files could potentially be reduced. Instead of thousands of open/read/close syscalls scattered across the filesystem, you would have one open call per package archive, then seeks within each archive. Whether the decompression overhead would outweigh the syscall savings is unclear, but it might be worth exploring for build systems where clean builds of large dependency trees are common.</p>

<h3 id="use-dartio-for-gzip-instead-of-packagearchive">Use dart:io for gzip Instead of package:archive</h3>

<p><a href="https://www.reddit.com/r/ProgrammingLanguages/comments/1qbvvpn/comment/nzi3muf/">Simon Binder pointed out</a> that dart:io already includes gzip support backed by zlib, so there is no need to use package:archive for decompression. Since dart:io does not support tar archives, I used package:archive for everything and did not think of mixing in dart:io's gzip support separately. Using dart:io's <code class="language-plaintext highlighter-rouge">GZipCodec</code> for decompression while only relying on package:archive for tar extraction could yield better performance. I will try this approach when I attempt to lex a bigger corpus.</p>

<h3 id="tar-vs-zip-sequential-vs-random-access">TAR vs ZIP: Sequential vs Random Access</h3>

<p><a href="https://www.reddit.com/r/ProgrammingLanguages/comments/1qbvvpn/comment/nzih9eh/">vanderZwan pointed out</a> that ZIP files could provide SQLite-like random access benefits. This highlights a fundamental architectural difference between TAR and ZIP:</p>

<p><strong>TAR</strong> was designed in 1979 for sequential tape drives. Each file's metadata is stored in a header immediately before its contents, with no central index. To find a specific file, you must read through the archive sequentially. When compressed as tar.gz, the entire stream is compressed together, so accessing any file requires decompressing everything before it. The format was standardized by POSIX (POSIX.1-1988 for ustar, POSIX.1-2001 for pax), is well-documented, and preserves Unix file attributes fully.</p>

<p><strong>ZIP</strong> was designed in 1989 with a central directory stored at the end of the archive. This directory contains offsets to each file's location, enabling random access: read the central directory once, then seek directly to any file. Each file is compressed individually, so you can decompress just the file you need. This is why JAR files, OpenDocument files, and EPUB files all use the ZIP format internally.</p>

<table>
  <thead>
    <tr>
      <th>Aspect</th>
      <th>TAR</th>
      <th>ZIP</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Random access</td>
      <td>No (sequential only)</td>
      <td>Yes (central directory)</td>
    </tr>
    <tr>
      <td>Standardization</td>
      <td>POSIX standard</td>
      <td>PKWARE-controlled specification</td>
    </tr>
    <tr>
      <td>Unix permissions</td>
      <td>Fully preserved</td>
      <td>Limited support</td>
    </tr>
    <tr>
      <td>Compression</td>
      <td>External (gzip, zstd, etc.)</td>
      <td>Built-in, per-file</td>
    </tr>
  </tbody>
</table>

<p>There seems to be no widely-adopted Unix-native format that combines random access with proper Unix metadata support. TAR handles sequential access with full Unix semantics. ZIP handles random access but originated from MS-DOS and has inconsistent Unix permission support. What we lack is something like "ZIP for Unix": random access with proper ownership, permissions, extended attributes, and ACLs.</p>

<p>The closest answer is <a href="http://dar.linux.free.fr/">dar (Disk ARchive)</a>, designed explicitly as a tar replacement with modern features. It stores a catalogue index at the end of the archive for O(1) file extraction, preserves full Unix metadata including extended attributes and ACLs, supports per-file compression with choice of algorithm, and can isolate the catalogue separately for fast browsing without the full archive. However, dar has not achieved the ubiquity of tar or zip.</p>

<p>For my lexer benchmark, random access would not help since I process all files anyway. But for use cases requiring access to specific files within an archive, this architectural distinction matters.</p>

<h3 id="block-based-compression">Block-Based Compression</h3>

<p><a href="https://news.ycombinator.com/item?id=46756484">cb321 pointed out</a> that there is a middle ground between uncompressed archives (random access but large) and fully compressed streams (small but sequential). Standard gzip compresses everything into a single block, so accessing any byte requires decompressing from the beginning. <a href="https://samtools.github.io/hts-specs/SAMv1.pdf">BGZF</a> (Blocked GNU Zip Format), developed by genomics researchers for tools like <a href="http://www.htslib.org/">samtools</a>, compresses data in independent 64KB blocks. Each block is a valid gzip stream, so the file remains compatible with standard gunzip, but with an index you can seek directly to any block and decompress just that portion. This allows random access to multi-gigabyte genome files without decompressing terabytes of data. <a href="https://github.com/facebook/zstd/blob/dev/contrib/seekable_format/zstd_seekable_compression_format.md">Zstd offers a similar seekable format</a> with better compression ratios and faster decompression. For tar archives, combining block-based compression with an external file offset index could provide random access to individual files while still benefiting from compression.</p>

<h3 id="re2c-a-faster-approach-to-lexer-generation">RE2C: A Faster Approach to Lexer Generation</h3>

<p><a href="https://news.ycombinator.com/item?id=46754081">rurban mentioned</a> that <a href="https://re2c.org/">RE2C</a> generates lexers that are roughly 10x faster than flex. The key difference is architectural: while flex generates table-driven lexers that look up transitions in arrays at runtime, RE2C generates direct-coded lexers where the finite automaton is encoded directly as conditional jumps and comparisons. This eliminates table lookup overhead and produces code that is both faster and easier for CPU branch predictors to handle.</p>

<p>RE2C also supports <a href="https://re2c.org/manual/manual_c.html">computed gotos</a> (via the <code class="language-plaintext highlighter-rouge">-g</code> flag), a GCC/Clang extension that compiles switch statements into indirect jumps through a label address table. For lexers with many states, this can significantly reduce branch mispredictions. Other optimizations include DFA minimization and tunnel automaton construction.</p>

<p>My ARM64 assembly lexer currently uses a table-driven approach, so exploring direct-coded generation is an interesting avenue. Another option is profile-guided optimization: compiling the lexer to LLVM IR and using PGO to optimize hot paths based on real Dart code patterns, something I mentioned as a future direction in my <a href="/blog/benchmarking-against-llvm-parser/">LLVM parser benchmarking post</a>. Part of my lexer's speed advantage over the official Dart scanner likely comes from simplicity: my lexer is pure, maintaining only a stack for lexer states across multiple finite automata, while the Dart scanner must construct a linked list of tokens, handle error recovery, and manage additional bookkeeping. Isolating how much of the performance difference comes from architecture versus feature set is something I want to investigate further.</p>

<h3 id="game-engine-archives-mpq-and-casc">Game Engine Archives: MPQ and CASC</h3>

<p><a href="https://www.reddit.com/r/programming/comments/1qmznm8/comment/o1qtnu5/">Iggyhopper pointed out</a> that Blizzard Entertainment solved this same problem decades ago with their <a href="https://en.wikipedia.org/wiki/MPQ_(file_format)">MPQ</a> archive format (Mo'PaQ, short for Mike O'Brien Pack). First deployed in <a href="https://en.wikipedia.org/wiki/Diablo_(video_game)">Diablo</a> in 1996, MPQ bundles game assets (textures, sounds, models, level data) into large archive files with built-in compression, encryption, and fast random access via <a href="http://www.zezula.net/en/mpq/mpqformat.html">hash table indexing</a>. The format was used across StarCraft, Diablo II, Warcraft III, and World of Warcraft. At <a href="https://www.gamedeveloper.com/game-platforms/gdc-austin-an-inside-look-at-the-universe-of-i-warcraft-i-">GDC Austin 2009</a>, Blizzard co-founder Frank Pearce revealed that WoW contained 1.5 million assets, a number that has only grown across subsequent expansions. In 2014, Blizzard replaced MPQ with <a href="https://wowdev.wiki/CASC">CASC (Content Addressable Storage Container)</a> starting with Warlords of Draenor, adding self-maintaining integrity checks and faster patching. The same principle from this blog post applies: bundling assets into large archives avoids the per-file overhead that would make loading millions of individual files impractical for a real-time game.</p>

<h3 id="amdahls-law">Amdahl's Law</h3>

<p><a href="https://www.reddit.com/r/programming/comments/1qmznm8/comment/o1q0jzb/">fun__friday pointed out</a> that the main takeaway is to measure before you start optimizing something, referencing <a href="https://en.wikipedia.org/wiki/Amdahl%27s_law">Amdahl's law</a>. This is a fair point, and this blog post is a textbook illustration of it: when lexing accounts for only ~17% of total execution time, even a 2x improvement in lexing yields only a 1.22x overall speedup. The theoretical maximum speedup from improving just the lexing component is bounded by the fraction of time spent on everything else. Measure first, optimize second.</p>

<p>That said, from a "business" standpoint it makes sense to focus on the largest bottlenecks (by following, e.g., the <a href="https://en.wikipedia.org/wiki/Critical_path_method">Critical path method</a>) and those parts that take up the most time. However, software can be reused, and making a single component faster can have significant benefits to other consumers of that component. A faster lexer benefits not just this benchmark but every tool that uses it: formatters, linters, analyzers, compilers. I think our software community thrives in part because we don't strictly follow the common sense that business optimization dictates.</p>

<h3 id="the-limits-of-profiling">The Limits of Profiling</h3>

<p><a href="https://www.reddit.com/r/programming/comments/1qmznm8/comment/o1qdle4/">Ameisen expanded</a> on the "measure first" advice with an important caveat: measuring can itself be very difficult or misleading. Three cases stand out. First, "death by a thousand cuts," where many small inefficiencies individually appear as noise in a profiler but collectively add up to significant overhead. No single hotspot dominates, so there is nothing obvious to fix. Second, indirect task dependencies, where speeding up one component has cascading benefits that a profiler will not attribute to it. Ameisen gives the example of a sprite resampling mod where a faster hashing algorithm not only helps the render thread directly but also keeps worker threads fed with data sooner, reducing overall latency in ways that are invisible in a flat profile. Third, profilers show what is slow, not why it is slow. Cache invalidations from <a href="https://en.wikipedia.org/wiki/False_sharing">false sharing</a> are a classic example: the profiler points at a slow memory access, but the actual cause (another thread writing to the same cache line) is hidden. The thing causing the slowdown and the thing made slow by it are different, and only the latter shows up in the profile. In a <a href="https://www.reddit.com/r/programming/comments/1qmznm8/comment/o1q5kkr/">follow-up comment</a>, Ameisen shared concrete examples: concurrent workers running 30% slower because their output data needed its own cache line but did not have it (false sharing), and a render thread gaining a 20% speedup from removing a safety branch that always passed, because the branch triggered an undocumented CPU pipeline flush when followed by a locked instruction. Neither issue showed up meaningfully in a profiler.</p>

<hr />

<p><a href="https://news.ycombinator.com/item?id=46693460">Discuss on Hacker News</a></p>

<p><a href="https://www.reddit.com/r/ProgrammingLanguages/comments/1qbvvpn/i_built_a_2x_faster_lexer_then_discovered_io_was/">Discuss on r/ProgrammingLanguages</a></p>

<p><a href="https://www.reddit.com/r/programming/comments/1qmznm8/i_built_a_2x_faster_lexer_then_discovered_io_was/">Discuss on r/programming</a></p>

<p><a href="https://lobste.rs/s/hqyoa2/i_built_2x_faster_lexer_then_discovered_i_o">Discuss on Lobsters</a></p>]]></content><author><name>Modestas Valauskas</name></author><category term="technical" /><category term="performance" /><category term="parsing" /><summary type="html"><![CDATA[Archiving 104K files into tar.gz reduced I/O time by 43x and total processing time by 2.3x. The bottleneck was not disk speed, it was syscall overhead.]]></summary></entry><entry><title type="html">Statistical Methods for Reliable Benchmarks</title><link href="https://modulovalue.com/blog/statistical-methods-for-reliable-benchmarks/" rel="alternate" type="text/html" title="Statistical Methods for Reliable Benchmarks" /><published>2026-01-06T00:00:00+01:00</published><updated>2026-01-06T00:00:00+01:00</updated><id>https://modulovalue.com/blog/statistical-methods-for-reliable-benchmarks</id><content type="html" xml:base="https://modulovalue.com/blog/statistical-methods-for-reliable-benchmarks/"><![CDATA[<p>Benchmarking is critical for performance-sensitive code. Yet most developers approach it with surprisingly crude methods: run some code, measure the time, compare the average against another piece of code. This approach is fundamentally flawed, and the numbers it produces can be actively misleading.</p>

<p>The good news is that there are simple statistical techniques that give us a much better understanding of how code actually performs. These techniques apply to every language, but for this post I will focus on Dart. I have written a package called <a href="https://pub.dev/packages/benchmark_harness_plus">benchmark_harness_plus</a> that implements everything discussed here.</p>

<h2 id="the-problem-with-averages">The Problem with Averages</h2>

<p>Consider a simple benchmark that runs 10 times:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Run 1:  5.0 us
Run 2:  5.1 us
Run 3:  4.9 us
Run 4:  5.0 us
Run 5:  5.2 us
Run 6:  4.8 us
Run 7:  5.0 us
Run 8:  5.1 us
Run 9:  4.9 us
Run 10: 50.0 us  &lt;- GC pause
</code></pre></div></div>

<p>The <strong>mean (average)</strong> is 9.5 us. But does this represent typical performance? Absolutely not. Nine out of ten runs completed in about 5 us. The mean is nearly double the actual typical performance because a single garbage collection pause skewed everything.</p>

<p>This is not a contrived example. GC pauses, OS scheduling, CPU throttling, and background processes constantly interfere with measurements. In real benchmarks, outliers are the norm, not the exception.</p>

<h2 id="the-solution-median">The Solution: Median</h2>

<p>The <strong>median</strong> is the middle value when samples are sorted. For the data above:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Sorted: [4.8, 4.9, 4.9, 5.0, 5.0, 5.0, 5.1, 5.1, 5.2, 50.0]
Median: 5.0 us  (average of the two middle values)
</code></pre></div></div>

<p>The median correctly reports 5.0 us, completely ignoring the outlier. This is why benchmark_harness_plus uses median as the primary comparison metric.</p>

<p><strong>When to look at mean vs median:</strong></p>

<p>The relationship between mean and median tells you about your data distribution:</p>

<ul>
  <li><strong>Mean ≈ Median</strong>: Symmetric distribution, no significant outliers</li>
  <li><strong>Mean &gt; Median</strong>: High outliers present (common in benchmarks, caused by GC and OS)</li>
  <li><strong>Mean &lt; Median</strong>: Low outliers present (rare, might indicate measurement issues)</li>
</ul>

<h3 id="when-you-still-need-the-mean">When you still need the mean</h3>

<p>As <a href="https://www.reddit.com/user/editor_of_the_beast/">editor_of_the_beast</a> pointed out to me, referencing Marc Brooker's post <a href="https://brooker.co.za/blog/2017/12/28/mean.html">Two Places the Mean Isn't Useless</a>, the mean remains essential for capacity planning and throughput calculations. If you want to know how many requests per second your system can handle, you need the mean latency, outliers and all. Those GC pauses consume real time and affect actual throughput.</p>

<p><a href="https://en.wikipedia.org/wiki/Little%27s_law">Little's Law</a> (<code class="language-plaintext highlighter-rouge">L = λ × W</code>) only works with means, not medians or percentiles. If you need to calculate how many concurrent connections you can sustain, or how much buffer space you need, the mean is irreplaceable.</p>

<p>The distinction is this: for comparing which implementation is faster under typical conditions, use the median. For calculating system capacity where every millisecond counts toward the total, use the mean.</p>

<h2 id="but-how-do-i-know-if-i-can-trust-the-results">But how do I know if I can trust the results?</h2>

<p>This is the question most benchmarking tools fail to answer. You get a number, but is it reliable? Could the next run produce something completely different?</p>

<p>The answer is the <strong>Coefficient of Variation (CV%)</strong>.</p>

<p>CV% expresses the standard deviation as a percentage of the mean:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>CV% = (standard deviation / mean) * 100
</code></pre></div></div>

<p>This normalizes variance across different scales. A standard deviation of 1.0 means very different things for a measurement of 10 us versus 1000 us. CV% makes them comparable.</p>

<p><strong>Trust thresholds:</strong></p>

<table>
  <thead>
    <tr>
      <th>CV%</th>
      <th>Reliability</th>
      <th>What it means</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>&lt; 10%</td>
      <td>Excellent</td>
      <td>Highly reliable. Trust exact ratios.</td>
    </tr>
    <tr>
      <td>10-20%</td>
      <td>Good</td>
      <td>Rankings are reliable. Ratios are approximate.</td>
    </tr>
    <tr>
      <td>20-50%</td>
      <td>Moderate</td>
      <td>Directional only. You know which is faster, but not by how much.</td>
    </tr>
    <tr>
      <td>&gt; 50%</td>
      <td>Poor</td>
      <td>Unreliable. The measurement is mostly noise.</td>
    </tr>
  </tbody>
</table>

<p>When benchmark_harness_plus reports CV% &gt; 50%, it warns you explicitly. You should not trust those numbers.</p>

<h2 id="the-complete-picture">The Complete Picture</h2>

<p>Here is what proper benchmark output looks like:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  Variant      |     median |       mean |    fastest |   stddev |    cv% |  vs base
  --------------------------------------------------------------------------------
  growable     |       1.24 |       1.31 |       1.05 |     0.15 |   11.5 |        -
  fixed-length |       0.52 |       0.53 |       0.50 |     0.02 |    3.8 |    2.38x
  generate     |       0.89 |       0.91 |       0.85 |     0.04 |    4.4 |    1.39x

  (times in microseconds per operation)
</code></pre></div></div>

<p>How to read this:</p>

<ol>
  <li>
    <p><strong>Check CV% first.</strong> All values are under 20%, so these measurements are reliable.</p>
  </li>
  <li>
    <p><strong>Compare medians.</strong> fixed-length (0.52 us) is fastest, growable (1.24 us) is slowest.</p>
  </li>
  <li>
    <p><strong>Look at mean vs median.</strong> The growable variant has mean (1.31) &gt; median (1.24), suggesting some high outliers. The others are close, indicating symmetric distributions.</p>
  </li>
  <li>
    <p><strong>Check the ratios.</strong> fixed-length is 2.38x faster than growable. Because both have good CV%, this ratio is trustworthy.</p>
  </li>
</ol>

<h2 id="what-benchmark_harness_plus-does-differently">What benchmark_harness_plus does differently</h2>

<p>The standard <code class="language-plaintext highlighter-rouge">benchmark_harness</code> package reports a single mean value. benchmark_harness_plus implements several statistical best practices:</p>

<h3 id="1-multiple-samples">1. Multiple Samples</h3>

<p>Instead of one measurement, the package collects multiple independent samples (default: 10). Each sample times many iterations of the code, then records the average time per operation. This gives us enough data points to compute meaningful statistics.</p>

<h3 id="2-proper-warmup">2. Proper Warmup</h3>

<p>Before any measurements, each variant runs through a warmup phase (default: 500 iterations). This allows:</p>

<ul>
  <li>The Dart VM to JIT-compile hot paths</li>
  <li>CPU caches to warm up</li>
  <li>Lazy initialization to complete</li>
</ul>

<p>Warmup results are discarded entirely.</p>

<h3 id="3-randomized-ordering">3. Randomized Ordering</h3>

<p>By default, the order of variants is randomized for each sample. This reduces systematic bias from:</p>

<ul>
  <li>CPU frequency scaling</li>
  <li>Thermal throttling</li>
  <li>Memory pressure changes over time</li>
</ul>

<p>If variant A always runs before variant B, the second variant might consistently benefit from (or suffer from) the state left by the first.</p>

<h3 id="4-reliability-assessment">4. Reliability Assessment</h3>

<p>Every result includes CV%, and the package provides a <code class="language-plaintext highlighter-rouge">reliability</code> property that categorizes results as excellent, good, moderate, or poor. You no longer have to guess whether your numbers are meaningful.</p>

<h2 id="usage">Usage</h2>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="s">'package:benchmark_harness_plus/benchmark_harness_plus.dart'</span><span class="o">;</span>

<span class="kt">void</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
  <span class="kd">final</span> <span class="n">benchmark</span> <span class="o">=</span> <span class="n">Benchmark</span><span class="p">(</span>
    <span class="nl">title:</span> <span class="s">'List Creation'</span><span class="p">,</span>
    <span class="nl">variants:</span> <span class="p">[</span>
      <span class="n">BenchmarkVariant</span><span class="p">(</span>
        <span class="nl">name:</span> <span class="s">'growable'</span><span class="p">,</span>
        <span class="nl">run:</span> <span class="p">()</span> <span class="p">{</span>
          <span class="kd">final</span> <span class="n">list</span> <span class="o">=</span> <span class="p">&lt;</span><span class="kt">int</span><span class="p">&gt;[];</span>
          <span class="k">for</span> <span class="p">(</span><span class="kd">var</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="p">&lt;</span> <span class="mi">100</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
            <span class="n">list</span><span class="o">.</span><span class="na">add</span><span class="p">(</span><span class="n">i</span><span class="p">);</span>
          <span class="p">}</span>
        <span class="p">},</span>
      <span class="p">),</span>
      <span class="n">BenchmarkVariant</span><span class="p">(</span>
        <span class="nl">name:</span> <span class="s">'fixed-length'</span><span class="p">,</span>
        <span class="nl">run:</span> <span class="p">()</span> <span class="p">{</span>
          <span class="kd">final</span> <span class="n">list</span> <span class="o">=</span> <span class="kt">List</span><span class="p">&lt;</span><span class="kt">int</span><span class="p">&gt;</span><span class="o">.</span><span class="na">filled</span><span class="p">(</span><span class="mi">100</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
          <span class="k">for</span> <span class="p">(</span><span class="kd">var</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="p">&lt;</span> <span class="mi">100</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
            <span class="n">list</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="n">i</span><span class="p">;</span>
          <span class="p">}</span>
        <span class="p">},</span>
      <span class="p">),</span>
    <span class="p">],</span>
  <span class="p">);</span>

  <span class="kd">final</span> <span class="n">results</span> <span class="o">=</span> <span class="n">benchmark</span><span class="o">.</span><span class="na">run</span><span class="p">(</span><span class="nl">log:</span> <span class="n">print</span><span class="p">);</span>
  <span class="n">printResults</span><span class="p">(</span><span class="n">results</span><span class="p">,</span> <span class="nl">baselineName:</span> <span class="s">'growable'</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The package includes three configuration presets:</p>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">BenchmarkConfig</span><span class="o">.</span><span class="na">quick</span>     <span class="c1">// Fast feedback during development</span>
<span class="n">BenchmarkConfig</span><span class="o">.</span><span class="na">standard</span>  <span class="c1">// Normal benchmarking (default)</span>
<span class="n">BenchmarkConfig</span><span class="o">.</span><span class="na">thorough</span>  <span class="c1">// Important performance decisions</span>
</code></pre></div></div>

<p>You can also create custom configurations:</p>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">BenchmarkConfig</span><span class="p">(</span>
  <span class="nl">iterations:</span> <span class="mi">5000</span><span class="p">,</span>
  <span class="nl">samples:</span> <span class="mi">20</span><span class="p">,</span>
  <span class="nl">warmupIterations:</span> <span class="mi">1000</span><span class="p">,</span>
  <span class="nl">randomizeOrder:</span> <span class="kc">true</span><span class="p">,</span>
<span class="p">)</span>
</code></pre></div></div>

<h2 id="when-measurements-are-unreliable">When Measurements Are Unreliable</h2>

<p>If you see CV% values above 50%, your measurements are dominated by noise. Common causes:</p>

<p><strong>Sub-microsecond operations.</strong> Very fast code is inherently difficult to measure accurately. Timer resolution becomes a limiting factor. Solution: increase iterations so each sample takes at least 10ms.</p>

<p><strong>System interference.</strong> Background processes, browser tabs, other applications. Solution: close unnecessary programs, or accept that some variance is unavoidable.</p>

<p><strong>Inconsistent input.</strong> If the code under test behaves differently based on input, and you are using random input, variance will be high. Solution: use deterministic test data.</p>

<p><strong>The operation is genuinely variable.</strong> Some code has inherently variable performance (cache-dependent algorithms, I/O, network calls). In these cases, high CV% is not a measurement problem; it is telling you something true about the code.</p>

<h2 id="summary">Summary</h2>

<p>The core techniques are simple:</p>

<ol>
  <li><strong>Use median, not mean.</strong> Median ignores outliers.</li>
  <li><strong>Collect multiple samples.</strong> One measurement tells you almost nothing.</li>
  <li><strong>Report CV%.</strong> Know whether you can trust your results.</li>
  <li><strong>Warm up before measuring.</strong> Let the JIT do its work.</li>
  <li><strong>Randomize variant order.</strong> Reduce systematic bias.</li>
</ol>

<p>These principles apply to any language. For Dart, benchmark_harness_plus implements all of them with sensible defaults.</p>

<p>The package is available at <a href="https://pub.dev/packages/benchmark_harness_plus">pub.dev/packages/benchmark_harness_plus</a>.</p>

<hr />

<h2 id="addendum-the-case-for-the-fastest-time">Addendum: The Case for the Fastest Time</h2>

<p><a href="https://www.reddit.com/r/dartlang/comments/1q5wyr8/comment/ny7gq02/">Bob Nystrom from the Dart language team pointed out</a> that the fastest time has a special property: it is an existence proof. If the machine ran the code that fast once, that represents what the code is actually capable of. Noise from GC, OS scheduling, and other interference can only add time, never subtract it. The minimum filters out that external noise and shows the algorithm's true potential.</p>

<p>This approach works well when comparing pure algorithms where you want to isolate the code's performance from system interference. For more complex cases involving throughput or real-world conditions, the noise is part of what you are measuring and should not be filtered out.</p>

<p>I have added a "fastest" column to benchmark_harness_plus (as of version 1.1.0) so this metric is now visible alongside median and mean.</p>

<h3 id="different-metrics-for-different-questions">Different Metrics for Different Questions</h3>

<p>What has become clear from these discussions is that different metrics answer different questions:</p>

<ul>
  <li>
    <p><strong>Fastest (minimum)</strong>: "How fast can this code run?" An existence proof of capability. Best for comparing pure algorithms where you want to isolate the code from system noise.</p>
  </li>
  <li>
    <p><strong>Median</strong>: "How fast does this code typically run?" Robust against outliers. Best for understanding typical performance under normal conditions.</p>
  </li>
  <li>
    <p><strong>Mean (average)</strong>: "What is the total time cost?" Essential for capacity planning and throughput calculations where every millisecond counts toward the total.</p>
  </li>
</ul>

<p>There seems to be a gap in how we talk about benchmarking. We use the same word for very different activities: comparing algorithm efficiency, measuring system throughput, profiling latency distributions, and capacity planning. Each requires different statistical treatment, yet we often reach for the same crude tools.</p>

<p>Perhaps what we need is a clearer taxonomy of benchmarking types, with explicit guidance on which metrics matter for each. The fastest time, the median, and the mean are all valuable, but they answer fundamentally different questions. Knowing which question you are asking is the first step to getting a meaningful answer.</p>

<h3 id="on-gc-triggering">On GC Triggering</h3>

<p>An earlier version of this package attempted to trigger garbage collection between variants by allocating and discarding memory. <a href="https://www.reddit.com/r/dartlang/comments/1q5wyr8/comment/ny60ysv/">Vyacheslav Egorov from the Dart Compiler team pointed out</a> that this is counterproductive: the GC is a complicated state machine driven by heuristics, and allocations can cause it to start concurrent marking, introducing more noise rather than reducing it.</p>

<p>The GC triggering logic has been removed as of version 1.2.0. A better approach for Dart 3.11+ is to use the <code class="language-plaintext highlighter-rouge">dart:developer</code> NativeRuntime API to record timeline events and check whether any GC occurred during the benchmark run, making GC visibility part of the report rather than trying to prevent it.</p>

<hr />

<p><a href="https://www.reddit.com/r/dartlang/comments/1q5wyr8/benchmark_harness_plus_statistical_methods_for/">Discuss on r/dartlang</a></p>]]></content><author><name>Modestas Valauskas</name></author><category term="technical" /><category term="dart" /><category term="performance" /><summary type="html"><![CDATA[Most developers benchmark code by running it and comparing averages. This approach is fundamentally flawed. Here is how to do it properly.]]></summary></entry><entry><title type="html">The Case for Snake Case: A Kolmogorov Complexity Argument</title><link href="https://modulovalue.com/blog/snake-case-vs-camel-case-kolmogorov-complexity/" rel="alternate" type="text/html" title="The Case for Snake Case: A Kolmogorov Complexity Argument" /><published>2025-12-27T00:00:00+01:00</published><updated>2025-12-27T00:00:00+01:00</updated><id>https://modulovalue.com/blog/snake-case-vs-camel-case-kolmogorov-complexity</id><content type="html" xml:base="https://modulovalue.com/blog/snake-case-vs-camel-case-kolmogorov-complexity/"><![CDATA[<p>Software engineering is drowning in complexity. Much of it is unintended, implicit, and hidden beneath layers of convention we rarely question. Today, I want to examine one of these conventions: identifier naming. Specifically, I will argue that snake_case is objectively superior to camelCase, and I will use Kolmogorov complexity to make this case.</p>

<h2 id="what-is-kolmogorov-complexity">What is Kolmogorov Complexity?</h2>

<p><a href="https://en.wikipedia.org/wiki/Kolmogorov_complexity">Kolmogorov complexity</a> measures the computational resources needed to specify an object. In practical terms, it asks: how much information, how many rules, how many external dependencies do we need to perform a given operation?</p>

<p>When we apply this lens to identifier naming conventions, the results are striking.</p>

<h2 id="parsing-identifiers-where-complexity-hides">Parsing Identifiers: Where Complexity Hides</h2>

<p>Consider the seemingly simple task of splitting an identifier into its component words. This operation is fundamental, both for tooling (linters, refactoring tools, documentation generators) and for human comprehension.</p>

<h3 id="snake-case-minimal-complexity">Snake Case: Minimal Complexity</h3>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">components</span> <span class="o">=</span> <span class="n">identifier</span><span class="p">.</span><span class="nf">split</span><span class="p">(</span><span class="sh">"</span><span class="s">_</span><span class="sh">"</span><span class="p">)</span>
</code></pre></div></div>

<p>That is it. The entire algorithm fits in a single, trivial operation. The delimiter is explicit, unambiguous, and universal. The underscore character has the same meaning in ASCII, in Unicode, in every locale, in every context. No external knowledge is required. No lookup tables. No edge cases.</p>

<h3 id="camel-case-hidden-complexity-explosion">Camel Case: Hidden Complexity Explosion</h3>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Good luck.
</span></code></pre></div></div>

<p>To split a camelCase identifier, you must:</p>

<ol>
  <li>
    <p><strong>Find capitalization boundaries.</strong> This requires knowing which characters are "uppercase" and which are "lowercase."</p>
  </li>
  <li>
    <p><strong>Consult the Unicode standard.</strong> Capitalization is not a property of characters in isolation. It is defined by the Unicode Standard, a specification that spans thousands of pages and is updated regularly. The uppercase/lowercase mapping for a single character can depend on locale, context, and version of the standard you are using.</p>
  </li>
  <li>
    <p><strong>Handle abbreviations.</strong> Is <code class="language-plaintext highlighter-rouge">XMLParser</code> split as <code class="language-plaintext highlighter-rouge">[XML, Parser]</code> or <code class="language-plaintext highlighter-rouge">[X, M, L, Parser]</code>? What about <code class="language-plaintext highlighter-rouge">parseHTTPSURL</code>? The answer depends on implicit human knowledge, conventions that vary by codebase, team, and era. There is no algorithm that can reliably determine this without external context.</p>
  </li>
  <li>
    <p><strong>Account for edge cases.</strong> What about <code class="language-plaintext highlighter-rouge">iPhone</code>? Or <code class="language-plaintext highlighter-rouge">eBay</code>? These are valid identifiers that violate the "rules" entirely.</p>
  </li>
</ol>

<p>The Kolmogorov complexity of camelCase parsing is not merely higher. It is unbounded in a practical sense, because it depends on an external, evolving standard (Unicode) and on implicit cultural knowledge that cannot be formalized.</p>

<h2 id="constructing-identifiers-the-same-story">Constructing Identifiers: The Same Story</h2>

<p>Suppose you have a list of words and want to form an identifier.</p>

<h3 id="snake-case">Snake Case</h3>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">identifier</span> <span class="o">=</span> <span class="sh">"</span><span class="s">_</span><span class="sh">"</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="n">components</span><span class="p">)</span>
</code></pre></div></div>

<p>Done. Append underscores between components. No transformation of the components themselves is required.</p>

<h3 id="camel-case">Camel Case</h3>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">identifier</span> <span class="o">=</span> <span class="n">components</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nf">lower</span><span class="p">()</span> <span class="o">+</span> <span class="sh">""</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="n">c</span><span class="p">.</span><span class="nf">title</span><span class="p">()</span> <span class="k">for</span> <span class="n">c</span> <span class="ow">in</span> <span class="n">components</span><span class="p">[</span><span class="mi">1</span><span class="p">:])</span>
</code></pre></div></div>

<p>This looks simple until you ask: what does <code class="language-plaintext highlighter-rouge">title()</code> actually do? The answer: it calls into Unicode case mapping. For the character "i", the uppercase form is "I" in most locales, but in Turkish it is "I" (with a dot). The <code class="language-plaintext highlighter-rouge">title()</code> function must either choose a locale, consult environment variables, or produce inconsistent results.</p>

<p>You have now introduced a dependency on:</p>
<ul>
  <li>The Unicode standard</li>
  <li>Locale settings</li>
  <li>Runtime environment configuration</li>
</ul>

<p>Your identifier construction algorithm is no longer self-contained. Its Kolmogorov complexity has ballooned.</p>

<h2 id="why-this-matters">Why This Matters</h2>

<p>Some might argue this is academic. Who cares about edge cases with Turkish "i" or abbreviations?</p>

<p>I argue that this matters deeply, for several reasons:</p>

<p><strong>1. Tooling reliability.</strong> Every refactoring tool, every linter, every code search engine that works with identifiers must solve this problem. The ambiguity in camelCase means these tools are either incomplete, inconsistent, or carry massive hidden complexity.</p>

<p><strong>2. Internationalization.</strong> Software is global. Identifiers increasingly contain Unicode characters. A naming convention that relies on capitalization is fundamentally tied to the Western alphabet's peculiar property of having case distinctions, a property that most of the world's writing systems do not share.</p>

<p><strong>3. Cognitive load.</strong> When a human reads <code class="language-plaintext highlighter-rouge">parseHTTPSURL</code>, they must mentally segment it. Different readers will segment it differently. This ambiguity consumes cognitive resources that could be spent on understanding the actual logic.</p>

<p><strong>4. The principle of least complexity.</strong> Unintended complexity is one of the greatest problems in software engineering today. It accumulates silently, manifesting as bugs, maintenance burden, and developer frustration. We should actively seek to minimize it.</p>

<h2 id="an-objective-argument">An Objective Argument</h2>

<p>I am not claiming snake_case is more aesthetically pleasing. Aesthetics are subjective. I am claiming that, by the objective measure of Kolmogorov complexity, snake_case requires fundamentally less information to parse and construct.</p>

<ul>
  <li>Snake case parsing: one operation, one delimiter, no external dependencies.</li>
  <li>Camel case parsing: character classification, Unicode case mapping, abbreviation heuristics, cultural conventions.</li>
</ul>

<p>The difference is not marginal. It is categorical.</p>

<h2 id="what-do-popular-languages-recommend">What Do Popular Languages Recommend?</h2>

<p>Given the complexity argument above, one might wonder: how do major programming languages handle this? I surveyed the official style guides of twelve popular languages.</p>

<table>
  <thead>
    <tr>
      <th>Language</th>
      <th>Variables/Functions</th>
      <th>Official Source</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>C++</td>
      <td><code class="language-plaintext highlighter-rouge">snake_case</code></td>
      <td><a href="https://google.github.io/styleguide/cppguide.html">Google C++ Style Guide</a></td>
    </tr>
    <tr>
      <td>Python</td>
      <td><code class="language-plaintext highlighter-rouge">snake_case</code></td>
      <td><a href="https://peps.python.org/pep-0008/">PEP 8</a></td>
    </tr>
    <tr>
      <td>Rust</td>
      <td><code class="language-plaintext highlighter-rouge">snake_case</code></td>
      <td><a href="https://rust-lang.github.io/rfcs/0430-finalizing-naming-conventions.html">RFC 430</a></td>
    </tr>
    <tr>
      <td>Ruby</td>
      <td><code class="language-plaintext highlighter-rouge">snake_case</code></td>
      <td><a href="https://rubystyle.guide/">Ruby Style Guide</a></td>
    </tr>
    <tr>
      <td>Java</td>
      <td><code class="language-plaintext highlighter-rouge">camelCase</code></td>
      <td><a href="https://www.oracle.com/java/technologies/javase/codeconventions-namingconventions.html">Oracle Code Conventions</a></td>
    </tr>
    <tr>
      <td>JavaScript</td>
      <td><code class="language-plaintext highlighter-rouge">camelCase</code></td>
      <td><a href="https://developer.mozilla.org/en-US/docs/MDN/Writing_guidelines/Code_style_guide/JavaScript">MDN Guidelines</a></td>
    </tr>
    <tr>
      <td>Go</td>
      <td><code class="language-plaintext highlighter-rouge">camelCase</code></td>
      <td><a href="https://go.dev/doc/effective_go">Effective Go</a></td>
    </tr>
    <tr>
      <td>C#</td>
      <td><code class="language-plaintext highlighter-rouge">camelCase</code></td>
      <td><a href="https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/identifier-names">Microsoft Naming Guidelines</a></td>
    </tr>
    <tr>
      <td>Swift</td>
      <td><code class="language-plaintext highlighter-rouge">camelCase</code></td>
      <td><a href="https://www.swift.org/documentation/api-design-guidelines/">Swift API Design Guidelines</a></td>
    </tr>
    <tr>
      <td>Kotlin</td>
      <td><code class="language-plaintext highlighter-rouge">camelCase</code></td>
      <td><a href="https://kotlinlang.org/docs/coding-conventions.html">Kotlin Coding Conventions</a></td>
    </tr>
    <tr>
      <td>PHP</td>
      <td><code class="language-plaintext highlighter-rouge">camelCase</code></td>
      <td><a href="https://www.php-fig.org/psr/psr-1/">PSR-1</a></td>
    </tr>
    <tr>
      <td>Dart</td>
      <td><code class="language-plaintext highlighter-rouge">camelCase</code></td>
      <td><a href="https://dart.dev/effective-dart/style">Effective Dart: Style</a></td>
    </tr>
  </tbody>
</table>

<p>The score is 8-4 in favor of camelCase. Does this invalidate my argument?</p>

<p>No. Popularity is not an argument for correctness. Many of these conventions were established decades ago, when ASCII dominance made capitalization seem trivial, when tooling was primitive, and when the hidden costs of implicit complexity were not yet understood.</p>

<p>Consider that Python, one of the most widely adopted languages of the past decade, chose snake_case. Rust, designed with modern sensibilities about safety and correctness, also chose snake_case. Ruby, known for developer happiness, chose snake_case.</p>

<p>The camelCase languages reveal a pattern of convention inheritance rather than deliberate design. Java popularized camelCase in the 1990s. JavaScript adopted "Java" in its name <a href="https://en.wikipedia.org/wiki/JavaScript">for marketing reasons</a> (Brendan Eich himself considered it "a marketing ploy by Netscape") and likely copied the convention. C# was <a href="https://en.wikipedia.org/wiki/Microsoft_Java_Virtual_Machine">Microsoft's answer to Java</a>, developed after Sun's lawsuit forced them to abandon their Java implementation. Dart was <a href="https://gist.github.com/paulmillr/1208618">Google's attempt to replace JavaScript</a>, as revealed in a leaked 2010 internal memo where the language (then called "Dash") was designed to "ultimately replace JavaScript as the lingua franca of web development." Go was designed for <a href="https://go.dev/talks/2012/splash.article">programmers "early in their careers"</a> who are "most familiar with procedural languages, particularly from the C family." Swift inherited from Objective-C, which had used camelCase since the NeXT era. PHP started as a personal project ("Personal Home Page") and grew organically. In most cases, the choice was made to fit existing convention, not because someone analyzed the complexity tradeoffs.</p>

<h2 id="conclusion">Conclusion</h2>

<p>The next time someone dismisses naming conventions as "just style," consider the hidden complexity beneath the surface. Snake case is not merely a preference. It is the convention with lower Kolmogorov complexity, fewer external dependencies, and less room for ambiguity.</p>

<p>In an industry that struggles daily with accidental complexity, choosing the simpler encoding for something as fundamental as identifiers is not pedantry. It is engineering discipline.</p>

<p><code class="language-plaintext highlighter-rouge">use_snake_case</code>. Your future self, your tools, and your international colleagues will have one less thing to worry about.</p>

<hr />

<h2 id="addendum">Addendum</h2>

<p>I received quite a lot of pushback on this post, which ironically gave me even better arguments to support my case.</p>

<p><strong>1. Snake case eliminates the need for abbreviation style guides.</strong> With camelCase, every organization needs rules for handling abbreviations. The .NET guidelines differ from legacy Java guidelines, which differ from Google's guidelines. Is it <code class="language-plaintext highlighter-rouge">HttpUrl</code> or <code class="language-plaintext highlighter-rouge">HTTPURL</code> or <code class="language-plaintext highlighter-rouge">HTTPUrl</code>? With snake_case, it is simply <code class="language-plaintext highlighter-rouge">http_url</code>. No style guide needed. No debates.</p>

<p><strong>2. I am arguing against camelCase, not PascalCase.</strong> Snake case and PascalCase can coexist peacefully. Use <code class="language-plaintext highlighter-rouge">snake_case</code> for variables and functions, <code class="language-plaintext highlighter-rouge">PascalCase</code> for types. The problem is specifically lowerCamelCase, which should be snake_case instead.</p>

<p><strong>3. Snake case and camelCase can be combined meaningfully.</strong> Within snake_case, underscores serve as the primary separator. This frees up camelCase to denote something else entirely within each component. For example, <code class="language-plaintext highlighter-rouge">parse_XMLDocument</code> or <code class="language-plaintext highlighter-rouge">get_userId</code> could use camelCase to preserve domain-specific casing while still having unambiguous word boundaries. You gain an additional layer of expressiveness.</p>

<p><strong>4. The complexity argument is real at the machine level.</strong> Try implementing split and join in assembly. For snake_case, you need a few bytes of code and a loop: scan for underscore, done. For camelCase, you need a Unicode parser and lookup tables that can exceed hundreds of kilobytes. The Kolmogorov complexity difference is not abstract theory; it manifests directly in binary size and execution complexity.</p>

<p><strong>5. Snake case helps with semantic search.</strong> Someone suggested that splitting <code class="language-plaintext highlighter-rouge">parseHTTPSURL</code> into <code class="language-plaintext highlighter-rouge">p a r s e h t t p s u r l</code> would enable fuzzy matching. This does not hold up. LLMs are trained on tokens from real-world text, not individual characters. They can infer that <code class="language-plaintext highlighter-rouge">https_url</code> relates to HTTPS and URL, but <code class="language-plaintext highlighter-rouge">h t t p s u r l</code> introduces uncertainty. Vector databases using metric spaces like Levenshtein distance will rank <code class="language-plaintext highlighter-rouge">p</code> lower than <code class="language-plaintext highlighter-rouge">https</code>. Vector embeddings have the same problem: <code class="language-plaintext highlighter-rouge">p</code> is nowhere close to being a synonym of <code class="language-plaintext highlighter-rouge">https</code>. With snake_case, the components are already explicitly separated as meaningful tokens: <code class="language-plaintext highlighter-rouge">parse_https_url</code>. No character-level decomposition needed.</p>

<p><strong>6. Yes, camelCase saves space.</strong> Someone noted that "technically camelCase is better in terms of space utilization but that is it." Well, I agree. That is indeed the one advantage.</p>]]></content><author><name>Modestas Valauskas</name></author><category term="technical" /><summary type="html"><![CDATA[Snake_case is objectively superior to camelCase. I use Kolmogorov complexity to make this case.]]></summary></entry><entry><title type="html">IIFEs are Dart&apos;s most underrated feature</title><link href="https://modulovalue.com/blog/iifes-are-darts-most-underrated-feature/" rel="alternate" type="text/html" title="IIFEs are Dart&apos;s most underrated feature" /><published>2025-12-23T12:00:00+01:00</published><updated>2025-12-23T12:00:00+01:00</updated><id>https://modulovalue.com/blog/iifes-are-darts-most-underrated-feature</id><content type="html" xml:base="https://modulovalue.com/blog/iifes-are-darts-most-underrated-feature/"><![CDATA[<p>IIFEs in Dart are severely underrated and barely anyone seems to agree with me. This is a hill I'm willing to die on, and I've decided to collect my thoughts into a blog post that will hopefully get you on this hill, too.</p>

<p>IIFE stands for Immediately Invoked Function Expression. What does that even mean? Let's start at the beginning. Bear with me. If you already know what IIFEs are, feel free to <a href="#iife-use-cases">skip to the use cases</a>.</p>

<p><strong>Table of contents:</strong></p>
<ul>
  <li><a href="#whats-an-expression">What's an expression?</a> - Immediately Invoked Function <strong>Expressions</strong></li>
  <li><a href="#whats-a-function">What's a function?</a> - Immediately Invoked <strong>Function</strong> Expressions</li>
  <li><a href="#whats-a-function-expression">What's a function expression?</a> - Immediately Invoked <strong>Function Expressions</strong></li>
  <li><a href="#whats-an-invocation">What's an invocation?</a> - Immediately <strong>Invoked</strong> Function Expressions</li>
  <li><a href="#whats-immediacy">What's immediacy?</a> - <strong>Immediately</strong> Invoked Function Expressions</li>
  <li><a href="#iife-use-cases">IIFE use cases</a>
    <ul>
      <li><a href="#dart-has-no-if-expression">Dart has no if-expression</a></li>
      <li><a href="#iifes-reduce-mental-load-by-scoping-things-locally">IIFEs reduce mental load by scoping things locally</a></li>
      <li><a href="#region-indicators">Region indicators</a></li>
      <li><a href="#iifes-give-you-statements-everywhere">IIFEs give you statements everywhere</a></li>
      <li><a href="#iifes-can-return-null-in-widget-lists">IIFEs can return null in widget lists</a></li>
      <li><a href="#try-catch-as-an-expression">Try-catch as an expression</a></li>
      <li><a href="#late-final-with-complex-initialization">Late final with complex initialization</a></li>
      <li><a href="#null-safe-value-extraction">Null-safe value extraction</a></li>
      <li><a href="#switch-expressions-only-support-expressions">Switch expressions only support expressions</a></li>
      <li><a href="#debug-only-code-with-assert">Debug-only code with assert</a></li>
    </ul>
  </li>
  <li><a href="#performance">Performance</a>
    <ul>
      <li><a href="#if-else-benchmark">If-else benchmark</a></li>
      <li><a href="#switch-expression-benchmark">Switch expression benchmark</a></li>
      <li><a href="#dart2js-output">dart2js output</a></li>
    </ul>
  </li>
  <li><a href="#conclusion">Conclusion</a></li>
</ul>

<h2 id="whats-an-expression">What's an expression?</h2>

<p>A programming language consists of different entities of abstraction. An expression is one such entity that fulfills the purpose of describing what values your program is going to produce. Examples include literals like <code class="language-plaintext highlighter-rouge">123</code> or <code class="language-plaintext highlighter-rouge">'hello'</code>, variable references, arithmetic like <code class="language-plaintext highlighter-rouge">1 + 2</code>, and function calls.</p>

<h2 id="whats-a-function">What's a function?</h2>

<p>A function is a collection of statements (and since expressions can be statements, also a collection of expressions). Functions have a name, parameters, a return type, and a body where statements live:</p>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">String</span> <span class="nf">greet</span><span class="p">(</span><span class="kt">String</span> <span class="n">name</span><span class="p">)</span> <span class="p">{</span>
  <span class="mi">123</span><span class="p">;</span> <span class="c1">// expression statement</span>
  <span class="kd">final</span> <span class="n">message</span> <span class="o">=</span> <span class="s">'Hello, </span><span class="si">$name</span><span class="s">!'</span><span class="p">;</span> <span class="c1">// expression on the right-hand side</span>
  <span class="k">return</span> <span class="n">message</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Here, <code class="language-plaintext highlighter-rouge">greet</code> is the name, <code class="language-plaintext highlighter-rouge">String name</code> is the parameter, the return type is <code class="language-plaintext highlighter-rouge">String</code>, and everything between the curly braces is the body. The body contains three statements: an expression statement (<code class="language-plaintext highlighter-rouge">123;</code>), a variable declaration with an expression on the right-hand side, and a return statement.</p>

<p>There are many other places where expressions can exist, but these are the most relevant for now.</p>

<h2 id="whats-a-function-expression">What's a function expression?</h2>

<p>Dart supports anonymous functions by supporting expressions that are functions. Anonymous means no name, and the return type is implicit, it can't be specified and will be inferred automatically:</p>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">final</span> <span class="n">a</span> <span class="o">=</span> <span class="p">()</span> <span class="p">{};</span>

<span class="kd">final</span> <span class="n">b</span> <span class="o">=</span> <span class="p">(</span><span class="kt">String</span> <span class="n">name</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">final</span> <span class="n">message</span> <span class="o">=</span> <span class="s">'Hello, </span><span class="si">$name</span><span class="s">!'</span><span class="p">;</span>
  <span class="k">return</span> <span class="n">message</span><span class="p">;</span>
<span class="p">};</span>
</code></pre></div></div>

<h2 id="whats-an-invocation">What's an invocation?</h2>

<p>To "invoke" something means essentially to call or execute something.</p>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
  <span class="n">print</span><span class="p">(</span><span class="s">"Hello World"</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>In that example, <code class="language-plaintext highlighter-rouge">print</code> is a function that was invoked, or in other words, called. You can also be very explicit about calling something in Dart and call the <code class="language-plaintext highlighter-rouge">call</code> method of a <code class="language-plaintext highlighter-rouge">Function</code>:</p>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
  <span class="n">print</span><span class="o">.</span><span class="na">call</span><span class="p">(</span><span class="s">"Hello World"</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Think of calling and invoking something as being one and the same thing.</p>

<h2 id="whats-immediacy">What's immediacy?</h2>

<p>What happens if we <strong>immediately</strong> invoke (call) a function expression? Let me present to you, an immediately invoked function expression:</p>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
<span class="c1">//vvvvv function expression</span>
  <span class="p">()</span> <span class="p">{}();</span>
  <span class="c1">//   ^^ invocation</span>
<span class="p">}</span>
</code></pre></div></div>

<p>That's an expression that is a function expression that is being invoked directly where it was defined.</p>

<p>Admittedly, this looks weird at first sight, but everything in programming does the first time you see it. The question is: what does it give us? And IIFEs give us <em>a whole lot</em>.</p>

<h2 id="iife-use-cases">IIFE use cases</h2>

<p>There are many common annoyances in Dart that are immediately (no pun intended) solved by using IIFEs.</p>

<h3 id="dart-has-no-if-expression">Dart has no if-expression</h3>

<p>Dart only supports if statements. Ternary expressions work for simple cases, but they become unreadable with multiple conditions or when you need to execute statements.</p>

<p>With an IIFE, you get an if-expression:</p>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">final</span> <span class="n">result</span> <span class="o">=</span> <span class="p">()</span> <span class="p">{</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">condition1</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="s">"a"</span><span class="p">;</span>
  <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">condition2</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="s">"b"</span><span class="p">;</span>
  <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
    <span class="k">return</span> <span class="s">"c"</span><span class="p">;</span>
  <span class="p">}</span>
<span class="p">}();</span>
</code></pre></div></div>

<p>This is especially useful when initializing final variables that depend on complex logic.</p>

<h3 id="iifes-reduce-mental-load-by-scoping-things-locally">IIFEs reduce mental load by scoping things locally</h3>

<p>Consider:</p>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="nf">foo</span><span class="p">()</span> <span class="p">{</span>
  <span class="c1">// Many lines of unrelated code</span>
  <span class="c1">// ...</span>
  <span class="p">()</span> <span class="p">{</span>
    <span class="kd">final</span> <span class="n">a</span> <span class="o">=</span> <span class="mi">123</span><span class="p">;</span>
    <span class="kd">final</span> <span class="n">b</span> <span class="o">=</span> <span class="s">"abc"</span><span class="p">;</span>
    <span class="c1">// ...</span>
  <span class="p">}();</span>
  <span class="c1">// Many lines of other unrelated code</span>
  <span class="c1">// ...</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Anything defined in the IIFE doesn't pollute the namespace that comes after it. Without it, you'd have to be careful not to declare or reuse/overwrite values that have been declared before, and you avoid problems where you accidentally shadow names used elsewhere.</p>

<p>Note: You might wonder why you need the <code class="language-plaintext highlighter-rouge">()()</code> at all. Dart does support plain <a href="https://github.com/dart-lang/sdk/blob/89250d64b93e1c0c278f7768e502f18144c4596f/tools/spec_parser/dart_spec_parser/Dart.g4#L370-L372">block statements</a> <code class="language-plaintext highlighter-rouge">{ ... }</code> for scoping without the function wrapper. But blocks are statements, not expressions, so you can only use them where statements are allowed. IIFEs give you scoping <em>and</em> an expression you can use anywhere.</p>

<p>This, for example, is an invalid program since <code class="language-plaintext highlighter-rouge">foo</code> is declared locally, but the intention is to use the global <code class="language-plaintext highlighter-rouge">foo</code>:</p>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="nf">foo</span><span class="p">()</span> <span class="p">{}</span>

<span class="kt">void</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
  <span class="kd">final</span> <span class="n">foo</span> <span class="o">=</span> <span class="mi">123</span><span class="p">;</span>
  <span class="n">print</span><span class="p">(</span><span class="n">foo</span><span class="p">);</span>
  <span class="n">foo</span><span class="p">();</span> <span class="c1">// Error: 'foo' isn't a function</span>
<span class="p">}</span>
</code></pre></div></div>

<p>However, this is fine, since <code class="language-plaintext highlighter-rouge">foo</code> only exists within the IIFE:</p>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="nf">foo</span><span class="p">()</span> <span class="p">{}</span>

<span class="kt">void</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
  <span class="p">()</span> <span class="p">{</span>
    <span class="kd">final</span> <span class="n">foo</span> <span class="o">=</span> <span class="mi">123</span><span class="p">;</span>
    <span class="n">print</span><span class="p">(</span><span class="n">foo</span><span class="p">);</span>
  <span class="p">}();</span>
  <span class="n">foo</span><span class="p">();</span> <span class="c1">// Works: calls the global foo</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="region-indicators">Region indicators</h3>

<p>Most IDEs support collapsible regions. IIFEs are a natural way to tell your IDE that you want something to be collapsible.</p>

<p style="text-align: center;">
  <img src="/assets/posts/iifes-are-darts-most-underrated-feature/collapsible.png" alt="Collapsible IIFE in IDE" />
</p>

<p>The markers on the left give you the opportunity to collapse the whole body of a function expression. Why is that useful? If you need to make sense of what's going on in your codebase, it helps to ignore things you've already determined are irrelevant to the problem at hand. Collapsible regions let you do exactly that.</p>

<h3 id="iifes-give-you-statements-everywhere">IIFEs give you statements everywhere</h3>

<p>For Flutter to be fun, you should know what an IIFE is.</p>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Widget</span> <span class="nf">build</span><span class="p">(</span><span class="n">BuildContext</span> <span class="n">context</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">return</span> <span class="n">Column</span><span class="p">(</span>
    <span class="nl">children:</span> <span class="p">[</span>
      <span class="p">()</span> <span class="p">{</span>
        <span class="c1">// Complex logic here</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">isLoading</span><span class="p">)</span> <span class="p">{</span>
          <span class="k">return</span> <span class="n">CircularProgressIndicator</span><span class="p">();</span>
        <span class="p">}</span>
        <span class="k">return</span> <span class="n">Text</span><span class="p">(</span><span class="n">data</span><span class="p">);</span>
      <span class="p">}(),</span>
      <span class="c1">// More widgets...</span>
    <span class="p">],</span>
  <span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Flutter critics tend to complain about deeply nested widget trees. Well, if you use an IIFE, that's no longer a problem. You can always exchange a Flutter widget (which is an expression) for an IIFE, flatten the logic, and return the widget you need.</p>

<p>This also allows you to copy and paste a list of statements into places that support expressions and places that support statements interchangeably.</p>

<p>IIFEs also help reduce widget duplication. If multiple branches of a conditional return the same outer widget, an IIFE lets you factor it out:</p>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Before: duplicated Card across three branches</span>
<span class="k">if</span> <span class="p">(</span><span class="n">user</span><span class="o">.</span><span class="na">isPremium</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">return</span> <span class="n">Card</span><span class="p">(</span>
    <span class="nl">elevation:</span> <span class="mi">4</span><span class="p">,</span>
    <span class="nl">margin:</span> <span class="n">EdgeInsets</span><span class="o">.</span><span class="na">all</span><span class="p">(</span><span class="mi">8</span><span class="p">),</span>
    <span class="nl">child:</span> <span class="n">Column</span><span class="p">(</span><span class="nl">children:</span> <span class="p">[</span>
      <span class="n">Icon</span><span class="p">(</span><span class="n">Icons</span><span class="o">.</span><span class="na">star</span><span class="p">,</span> <span class="nl">color:</span> <span class="n">user</span><span class="o">.</span><span class="na">tier</span><span class="o">.</span><span class="na">color</span><span class="p">),</span>
      <span class="n">Text</span><span class="p">(</span><span class="n">user</span><span class="o">.</span><span class="na">displayName</span><span class="p">),</span>
      <span class="n">Text</span><span class="p">(</span><span class="s">'Member since </span><span class="si">${user.joinDate.year}</span><span class="s">'</span><span class="p">),</span>
    <span class="p">]),</span>
  <span class="p">);</span>
<span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">user</span><span class="o">.</span><span class="na">isTrialActive</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">return</span> <span class="n">Card</span><span class="p">(</span>
    <span class="nl">elevation:</span> <span class="mi">4</span><span class="p">,</span>
    <span class="nl">margin:</span> <span class="n">EdgeInsets</span><span class="o">.</span><span class="na">all</span><span class="p">(</span><span class="mi">8</span><span class="p">),</span>
    <span class="nl">child:</span> <span class="n">Column</span><span class="p">(</span><span class="nl">children:</span> <span class="p">[</span>
      <span class="n">Icon</span><span class="p">(</span><span class="n">Icons</span><span class="o">.</span><span class="na">hourglass_top</span><span class="p">),</span>
      <span class="n">Text</span><span class="p">(</span><span class="n">user</span><span class="o">.</span><span class="na">displayName</span><span class="p">),</span>
      <span class="n">Text</span><span class="p">(</span><span class="s">'</span><span class="si">${user.trialDaysLeft}</span><span class="s"> days left'</span><span class="p">),</span>
    <span class="p">]),</span>
  <span class="p">);</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
  <span class="k">return</span> <span class="n">Card</span><span class="p">(</span>
    <span class="nl">elevation:</span> <span class="mi">4</span><span class="p">,</span>
    <span class="nl">margin:</span> <span class="n">EdgeInsets</span><span class="o">.</span><span class="na">all</span><span class="p">(</span><span class="mi">8</span><span class="p">),</span>
    <span class="nl">child:</span> <span class="n">Column</span><span class="p">(</span><span class="nl">children:</span> <span class="p">[</span>
      <span class="n">Icon</span><span class="p">(</span><span class="n">Icons</span><span class="o">.</span><span class="na">person</span><span class="p">),</span>
      <span class="n">Text</span><span class="p">(</span><span class="n">user</span><span class="o">.</span><span class="na">displayName</span><span class="p">),</span>
      <span class="n">Text</span><span class="p">(</span><span class="s">'Upgrade to premium'</span><span class="p">),</span>
    <span class="p">]),</span>
  <span class="p">);</span>
<span class="p">}</span>

<span class="c1">// After: Card and Column factored out with IIFE</span>
<span class="n">Card</span><span class="p">(</span>
  <span class="nl">elevation:</span> <span class="mi">4</span><span class="p">,</span>
  <span class="nl">margin:</span> <span class="n">EdgeInsets</span><span class="o">.</span><span class="na">all</span><span class="p">(</span><span class="mi">8</span><span class="p">),</span>
  <span class="nl">child:</span> <span class="n">Column</span><span class="p">(</span>
    <span class="nl">children:</span> <span class="p">()</span> <span class="p">{</span>
      <span class="k">if</span> <span class="p">(</span><span class="n">user</span><span class="o">.</span><span class="na">isPremium</span><span class="p">)</span> <span class="p">{</span>
        <span class="kd">final</span> <span class="n">memberDuration</span> <span class="o">=</span> <span class="n">DateTime</span><span class="o">.</span><span class="na">now</span><span class="p">()</span><span class="o">.</span><span class="na">difference</span><span class="p">(</span><span class="n">user</span><span class="o">.</span><span class="na">joinDate</span><span class="p">);</span>
        <span class="kd">final</span> <span class="n">years</span> <span class="o">=</span> <span class="n">memberDuration</span><span class="o">.</span><span class="na">inDays</span> <span class="o">~/</span> <span class="mi">365</span><span class="p">;</span>
        <span class="kd">final</span> <span class="n">badge</span> <span class="o">=</span> <span class="n">years</span> <span class="p">&gt;</span><span class="o">=</span> <span class="mi">5</span> <span class="o">?</span> <span class="n">Icons</span><span class="o">.</span><span class="na">diamond</span> <span class="o">:</span> <span class="n">Icons</span><span class="o">.</span><span class="na">star</span><span class="p">;</span>
        <span class="k">return</span> <span class="p">[</span>
          <span class="n">Icon</span><span class="p">(</span><span class="n">badge</span><span class="p">,</span> <span class="nl">color:</span> <span class="n">user</span><span class="o">.</span><span class="na">tier</span><span class="o">.</span><span class="na">color</span><span class="p">),</span>
          <span class="n">Text</span><span class="p">(</span><span class="n">user</span><span class="o">.</span><span class="na">displayName</span><span class="p">),</span>
          <span class="n">Text</span><span class="p">(</span><span class="s">'Member for </span><span class="si">$years</span><span class="s"> years'</span><span class="p">),</span>
        <span class="p">];</span>
      <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">user</span><span class="o">.</span><span class="na">isTrialActive</span><span class="p">)</span> <span class="p">{</span>
        <span class="kd">final</span> <span class="n">daysLeft</span> <span class="o">=</span> <span class="n">user</span><span class="o">.</span><span class="na">trialEnd</span><span class="o">.</span><span class="na">difference</span><span class="p">(</span><span class="n">DateTime</span><span class="o">.</span><span class="na">now</span><span class="p">())</span><span class="o">.</span><span class="na">inDays</span><span class="p">;</span>
        <span class="kd">final</span> <span class="n">isUrgent</span> <span class="o">=</span> <span class="n">daysLeft</span> <span class="p">&lt;</span><span class="o">=</span> <span class="mi">3</span><span class="p">;</span>
        <span class="k">return</span> <span class="p">[</span>
          <span class="n">Icon</span><span class="p">(</span><span class="n">Icons</span><span class="o">.</span><span class="na">hourglass_top</span><span class="p">,</span> <span class="nl">color:</span> <span class="n">isUrgent</span> <span class="o">?</span> <span class="n">Colors</span><span class="o">.</span><span class="na">red</span> <span class="o">:</span> <span class="kc">null</span><span class="p">),</span>
          <span class="n">Text</span><span class="p">(</span><span class="n">user</span><span class="o">.</span><span class="na">displayName</span><span class="p">),</span>
          <span class="n">Text</span><span class="p">(</span><span class="s">'</span><span class="si">$daysLeft</span><span class="s"> days left'</span><span class="p">),</span>
        <span class="p">];</span>
      <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
        <span class="k">return</span> <span class="p">[</span>
          <span class="n">Icon</span><span class="p">(</span><span class="n">Icons</span><span class="o">.</span><span class="na">person</span><span class="p">),</span>
          <span class="n">Text</span><span class="p">(</span><span class="n">user</span><span class="o">.</span><span class="na">displayName</span><span class="p">),</span>
          <span class="n">Text</span><span class="p">(</span><span class="s">'Upgrade to premium'</span><span class="p">),</span>
        <span class="p">];</span>
      <span class="p">}</span>
    <span class="p">}(),</span>
  <span class="p">),</span>
<span class="p">)</span>
</code></pre></div></div>

<p>Yes, you could put the outer widget in a new function, but that adds complexity and increases the mental load. Do you make it public/private? What do you call it? Where do you put it? In my view, having such helper functions is unhelpful since they will only be used in one place. You don't need them at all when you use IIFEs.</p>

<h3 id="iifes-can-return-null-in-widget-lists">IIFEs can return null in widget lists</h3>

<p>As <a href="https://www.reddit.com/r/FlutterDev/comments/1pttn8j/comment/nvkio79/">u/Dustlay pointed out</a>, IIFEs have an advantage over <code class="language-plaintext highlighter-rouge">Builder</code> widgets: they can return <code class="language-plaintext highlighter-rouge">null</code>. A <code class="language-plaintext highlighter-rouge">WidgetBuilder</code> must return a <code class="language-plaintext highlighter-rouge">Widget</code>, so you'd need at least an empty <code class="language-plaintext highlighter-rouge">SizedBox()</code>. But in a <code class="language-plaintext highlighter-rouge">Row</code> or <code class="language-plaintext highlighter-rouge">Column</code>, that empty widget can mess with spacing.</p>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Column</span><span class="p">(</span>
  <span class="nl">children:</span> <span class="p">[</span>
    <span class="n">Text</span><span class="p">(</span><span class="s">'Header'</span><span class="p">),</span>
    <span class="c1">// Builder can't return null - you'd need SizedBox() which affects spacing</span>
    <span class="c1">// Builder(builder: (context) =&gt; showExtra ? ExtraWidget() : SizedBox()),</span>

    <span class="c1">// IIFE with ?() can return null</span>
    <span class="o">?</span><span class="p">()</span> <span class="p">{</span>
      <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">showExtra</span><span class="p">)</span> <span class="k">return</span> <span class="kc">null</span><span class="p">;</span>
      <span class="kd">final</span> <span class="n">data</span> <span class="o">=</span> <span class="n">computeSomething</span><span class="p">();</span>
      <span class="k">return</span> <span class="n">ExtraWidget</span><span class="p">(</span><span class="nl">data:</span> <span class="n">data</span><span class="p">);</span>
    <span class="p">}(),</span>
    <span class="n">Text</span><span class="p">(</span><span class="s">'Footer'</span><span class="p">),</span>
  <span class="p">],</span>
<span class="p">)</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">?</code> is the <a href="https://dart.dev/language/operators#other-operators">null-aware expression</a> operator applied to an IIFE. When the IIFE returns <code class="language-plaintext highlighter-rouge">null</code>, the element is omitted from the list entirely. No phantom <code class="language-plaintext highlighter-rouge">SizedBox</code> taking up space or interfering with <code class="language-plaintext highlighter-rouge">MainAxisAlignment.spaceBetween</code>.</p>

<h3 id="try-catch-as-an-expression">Try-catch as an expression</h3>

<p>Dart has no try-catch expression. With an IIFE, you can handle errors and return a value in one go:</p>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">final</span> <span class="n">config</span> <span class="o">=</span> <span class="p">()</span> <span class="p">{</span>
  <span class="k">try</span> <span class="p">{</span>
    <span class="k">return</span> <span class="n">jsonDecode</span><span class="p">(</span><span class="n">configString</span><span class="p">);</span>
  <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="n">e</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="n">defaultConfig</span><span class="p">;</span>
  <span class="p">}</span>
<span class="p">}();</span>
</code></pre></div></div>

<p>This is particularly useful for parsing, file operations, or any fallible initialization where you want a guaranteed value.</p>

<h3 id="late-final-with-complex-initialization">Late final with complex initialization</h3>

<p>When a <code class="language-plaintext highlighter-rouge">late final</code> field needs more than a simple expression to initialize:</p>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">DataProcessor</span> <span class="p">{</span>
  <span class="kd">late</span> <span class="kd">final</span> <span class="kt">Map</span><span class="p">&lt;</span><span class="kt">String</span><span class="p">,</span> <span class="n">Handler</span><span class="p">&gt;</span> <span class="n">_handlers</span> <span class="o">=</span> <span class="p">()</span> <span class="p">{</span>
    <span class="kd">final</span> <span class="n">map</span> <span class="o">=</span> <span class="p">&lt;</span><span class="kt">String</span><span class="p">,</span> <span class="n">Handler</span><span class="p">&gt;{};</span>
    <span class="k">for</span> <span class="p">(</span><span class="kd">final</span> <span class="n">type</span> <span class="k">in</span> <span class="n">supportedTypes</span><span class="p">)</span> <span class="p">{</span>
      <span class="n">map</span><span class="p">[</span><span class="n">type</span><span class="o">.</span><span class="na">name</span><span class="p">]</span> <span class="o">=</span> <span class="n">type</span><span class="o">.</span><span class="na">createHandler</span><span class="p">();</span>
      <span class="n">map</span><span class="p">[</span><span class="s">'</span><span class="si">${type.name}</span><span class="s">_legacy'</span><span class="p">]</span> <span class="o">=</span> <span class="n">type</span><span class="o">.</span><span class="na">createLegacyHandler</span><span class="p">();</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="n">map</span><span class="p">;</span>
  <span class="p">}();</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The alternative would be initializing in a constructor or a separate method, but the IIFE keeps the initialization logic right where the field is declared.</p>

<p>More advanced: together with <code class="language-plaintext highlighter-rouge">late</code>, IIFEs help you define Excel-style data flow graphs inside of classes without having to add a ton of constructor, initialization, or function declaration boilerplate.</p>

<h3 id="null-safe-value-extraction">Null-safe value extraction</h3>

<p>When you need to safely extract a value through multiple nullable layers:</p>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">final</span> <span class="n">userName</span> <span class="o">=</span> <span class="p">()</span> <span class="p">{</span>
  <span class="kd">final</span> <span class="n">user</span> <span class="o">=</span> <span class="n">response</span><span class="o">.</span><span class="na">data</span><span class="o">?.</span><span class="na">user</span><span class="p">;</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">user</span> <span class="o">==</span> <span class="kc">null</span><span class="p">)</span> <span class="k">return</span> <span class="s">'Anonymous'</span><span class="p">;</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">user</span><span class="o">.</span><span class="na">displayName</span><span class="o">?.</span><span class="na">isNotEmpty</span> <span class="o">==</span> <span class="kc">true</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="n">user</span><span class="o">.</span><span class="na">displayName</span><span class="o">!</span><span class="p">;</span>
  <span class="p">}</span>
  <span class="k">return</span> <span class="n">user</span><span class="o">.</span><span class="na">email</span><span class="o">?.</span><span class="na">split</span><span class="p">(</span><span class="s">'@'</span><span class="p">)</span><span class="o">.</span><span class="na">first</span> <span class="o">??</span> <span class="s">'User </span><span class="si">${user.id}</span><span class="s">'</span><span class="p">;</span>
<span class="p">}();</span>
</code></pre></div></div>

<p>This is cleaner than deeply nested ternaries or spreading the logic across multiple statements that pollute your scope.</p>

<h3 id="switch-expressions-only-support-expressions">Switch expressions only support expressions</h3>

<p>Dart 3's switch expressions have a limitation: <a href="https://github.com/dart-lang/sdk/blob/583fbe5962309d6305fc4855f52ec807b84f4aed/tools/spec_parser/Dart.g#L881-L883">each arm can only contain a single expression</a>. You can't execute statements, declare variables, or add debug logging inside a switch arm.</p>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// This doesn't work - statements aren't allowed in switch expressions</span>
<span class="kd">final</span> <span class="n">result</span> <span class="o">=</span> <span class="k">switch</span> <span class="p">(</span><span class="n">status</span><span class="p">)</span> <span class="p">{</span>
  <span class="n">Status</span><span class="o">.</span><span class="na">loading</span> <span class="o">=</span><span class="p">&gt;</span> <span class="p">{</span>
    <span class="n">print</span><span class="p">(</span><span class="s">'Loading...'</span><span class="p">);</span> <span class="c1">// Error: statements not allowed</span>
    <span class="k">return</span> <span class="n">LoadingWidget</span><span class="p">();</span>
  <span class="p">},</span>
  <span class="n">Status</span><span class="o">.</span><span class="na">error</span> <span class="o">=</span><span class="p">&gt;</span> <span class="n">ErrorWidget</span><span class="p">(),</span>
  <span class="n">Status</span><span class="o">.</span><span class="na">success</span> <span class="o">=</span><span class="p">&gt;</span> <span class="n">SuccessWidget</span><span class="p">(),</span>
<span class="p">};</span>
</code></pre></div></div>

<p>IIFEs solve this elegantly:</p>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">final</span> <span class="n">result</span> <span class="o">=</span> <span class="k">switch</span> <span class="p">(</span><span class="n">status</span><span class="p">)</span> <span class="p">{</span>
  <span class="n">Status</span><span class="o">.</span><span class="na">loading</span> <span class="o">=</span><span class="p">&gt;</span> <span class="p">()</span> <span class="p">{</span>
    <span class="n">print</span><span class="p">(</span><span class="s">'Loading state entered'</span><span class="p">);</span>
    <span class="kd">final</span> <span class="n">message</span> <span class="o">=</span> <span class="n">computeLoadingMessage</span><span class="p">();</span>
    <span class="k">return</span> <span class="n">LoadingWidget</span><span class="p">(</span><span class="nl">message:</span> <span class="n">message</span><span class="p">);</span>
  <span class="p">}(),</span>
  <span class="n">Status</span><span class="o">.</span><span class="na">error</span> <span class="o">=</span><span class="p">&gt;</span> <span class="p">()</span> <span class="p">{</span>
    <span class="n">logError</span><span class="p">(</span><span class="n">status</span><span class="o">.</span><span class="na">error</span><span class="p">);</span>
    <span class="k">return</span> <span class="n">ErrorWidget</span><span class="p">(</span><span class="nl">retry:</span> <span class="n">handleRetry</span><span class="p">);</span>
  <span class="p">}(),</span>
  <span class="n">Status</span><span class="o">.</span><span class="na">success</span> <span class="o">=</span><span class="p">&gt;</span> <span class="n">SuccessWidget</span><span class="p">(</span><span class="nl">data:</span> <span class="n">status</span><span class="o">.</span><span class="na">data</span><span class="p">),</span>
<span class="p">};</span>
</code></pre></div></div>

<p>Without IIFEs, you'd need to extract each complex arm into a separate function, scattering related logic across your codebase. The IIFE keeps everything inline and readable.</p>

<h3 id="debug-only-code-with-assert">Debug-only code with assert</h3>

<p>Dart's <code class="language-plaintext highlighter-rouge">assert</code> statements are removed entirely in production builds. Since <code class="language-plaintext highlighter-rouge">assert</code> takes an expression, you can use an IIFE to run arbitrary debug-only code with zero production overhead. This is a pattern commonly used by Flutter. Thanks to <a href="https://www.reddit.com/r/FlutterDev/comments/1pttn8j/comment/nvowh51/">u/SchandalRwartz for pointing this out</a>.</p>

<p>Here's <a href="https://github.com/flutter/flutter/blob/4e4b5d0e84ad4e030b39e10bf6cca35cc20a1de7/engine/src/flutter/lib/ui/setup_hooks.dart#L9-L17">an example from the Flutter engine</a>:</p>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">assert</span><span class="p">(()</span> <span class="p">{</span>
  <span class="c1">// In debug mode, register the schedule frame extension.</span>
  <span class="n">developer</span><span class="o">.</span><span class="na">registerExtension</span><span class="p">(</span><span class="s">'ext.ui.window.scheduleFrame'</span><span class="p">,</span> <span class="n">_scheduleFrame</span><span class="p">);</span>

  <span class="c1">// In debug mode, allow shaders to be reinitialized.</span>
  <span class="n">developer</span><span class="o">.</span><span class="na">registerExtension</span><span class="p">(</span><span class="s">'ext.ui.window.reinitializeShader'</span><span class="p">,</span> <span class="n">_reinitializeShader</span><span class="p">);</span>

  <span class="k">return</span> <span class="kc">true</span><span class="p">;</span>
<span class="p">}());</span>
</code></pre></div></div>

<p>The IIFE returns <code class="language-plaintext highlighter-rouge">true</code> so the assertion passes, but the real purpose is executing the statements inside. In production, the entire <code class="language-plaintext highlighter-rouge">assert</code> statement disappears, including the IIFE and all its side effects.</p>

<p>This is useful for debug logging, registering development tools, or running expensive validation that you only want during development.</p>

<h2 id="performance">Performance</h2>

<p>A common concern: "Don't IIFEs create overhead?" I analyzed this by examining what both the Dart VM and dart2js produce.</p>

<h3 id="if-else-benchmark">If-else benchmark</h3>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
  <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="p">&lt;</span> <span class="mi">100000</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span> <span class="n">getX</span><span class="p">();</span> <span class="n">getY</span><span class="p">();</span> <span class="p">}</span>
  <span class="n">print</span><span class="p">(</span><span class="s">'Done warming up'</span><span class="p">);</span>
  <span class="kd">final</span> <span class="n">sw1</span> <span class="o">=</span> <span class="n">Stopwatch</span><span class="p">().</span><span class="o">.</span><span class="na">start</span><span class="p">();</span>
  <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="p">&lt;</span> <span class="mi">10000000</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span> <span class="n">getX</span><span class="p">();</span> <span class="p">}</span>
  <span class="n">sw1</span><span class="o">.</span><span class="na">stop</span><span class="p">();</span>
  <span class="kd">final</span> <span class="n">sw2</span> <span class="o">=</span> <span class="n">Stopwatch</span><span class="p">().</span><span class="o">.</span><span class="na">start</span><span class="p">();</span>
  <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="p">&lt;</span> <span class="mi">10000000</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span> <span class="n">getY</span><span class="p">();</span> <span class="p">}</span>
  <span class="n">sw2</span><span class="o">.</span><span class="na">stop</span><span class="p">();</span>
  <span class="n">print</span><span class="p">(</span><span class="s">'IIFE: </span><span class="si">${sw1.elapsedMicroseconds}</span><span class="s">us'</span><span class="p">);</span>
  <span class="n">print</span><span class="p">(</span><span class="s">'Traditional: </span><span class="si">${sw2.elapsedMicroseconds}</span><span class="s">us'</span><span class="p">);</span>
<span class="p">}</span>

<span class="nd">@pragma</span><span class="p">(</span><span class="s">'vm:never-inline'</span><span class="p">)</span>
<span class="kt">int</span> <span class="nf">getX</span><span class="p">()</span> <span class="p">{</span>  <span class="c1">// IIFE version</span>
  <span class="kd">final</span> <span class="n">result</span> <span class="o">=</span> <span class="p">()</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">condition</span><span class="p">())</span> <span class="p">{</span> <span class="k">return</span> <span class="n">expensive</span><span class="p">()</span> <span class="o">*</span> <span class="mi">2</span><span class="p">;</span> <span class="p">}</span>
    <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="mi">42</span><span class="p">;</span> <span class="p">}</span>
  <span class="p">}();</span>
  <span class="k">return</span> <span class="n">result</span><span class="p">;</span>
<span class="p">}</span>

<span class="nd">@pragma</span><span class="p">(</span><span class="s">'vm:never-inline'</span><span class="p">)</span>
<span class="kt">int</span> <span class="nf">getY</span><span class="p">()</span> <span class="p">{</span>  <span class="c1">// Traditional version</span>
  <span class="kd">final</span> <span class="kt">int</span> <span class="n">result</span><span class="p">;</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">condition</span><span class="p">())</span> <span class="p">{</span> <span class="n">result</span> <span class="o">=</span> <span class="n">expensive</span><span class="p">()</span> <span class="o">*</span> <span class="mi">2</span><span class="p">;</span> <span class="p">}</span>
  <span class="k">else</span> <span class="p">{</span> <span class="n">result</span> <span class="o">=</span> <span class="mi">42</span><span class="p">;</span> <span class="p">}</span>
  <span class="k">return</span> <span class="n">result</span><span class="p">;</span>
<span class="p">}</span>

<span class="nd">@pragma</span><span class="p">(</span><span class="s">'vm:prefer-inline'</span><span class="p">)</span>
<span class="kt">bool</span> <span class="nf">condition</span><span class="p">()</span> <span class="o">=</span><span class="p">&gt;</span> <span class="n">DateTime</span><span class="o">.</span><span class="na">now</span><span class="p">()</span><span class="o">.</span><span class="na">millisecondsSinceEpoch</span> <span class="o">%</span> <span class="mi">2</span> <span class="o">==</span> <span class="mi">0</span><span class="p">;</span>
<span class="nd">@pragma</span><span class="p">(</span><span class="s">'vm:prefer-inline'</span><span class="p">)</span>
<span class="kt">int</span> <span class="nf">expensive</span><span class="p">()</span> <span class="o">=</span><span class="p">&gt;</span> <span class="n">DateTime</span><span class="o">.</span><span class="na">now</span><span class="p">()</span><span class="o">.</span><span class="na">microsecondsSinceEpoch</span><span class="p">;</span>
</code></pre></div></div>

<p>Run with <code class="language-plaintext highlighter-rouge">dart --print-flow-graph-optimized benchmark.dart</code> to see the optimized IL:</p>

<p><strong>getX (IIFE):</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>B1[function entry]:2
    CheckStackOverflow:8(stack=0, loop=0)
    v75 &lt;- StaticCall:16( _getCurrentMicros@0150898&lt;0&gt; ) T{int}
    Branch if RelationalOp:12(&lt;, v75 T{_Smi}, v41) T{bool} goto (28, 29)
    ...
    Branch if TestInt(v87, v26) goto (5, 6)
B5[target]:20  // condition() returned true
    v68 &lt;- StaticCall:16( _getCurrentMicros@0150898&lt;0&gt; ) T{int}
    v18 &lt;- BinarySmiOp:24(&lt;&lt;, v68 T{_Smi}, v26) T{_Smi}  // expensive() * 2
    goto B7
B6[target]:30  // condition() returned false
    goto B7
B7[join]:19 pred(B5, B6) {
    v27 &lt;- phi(v18 T{_Smi}, v24 T{_Smi})  // v24 is constant 42
}
    DartReturn:22(v27)
</code></pre></div></div>

<p><strong>getY (Traditional):</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>B1[function entry]:2
    CheckStackOverflow:8(stack=0, loop=0)
    v57 &lt;- StaticCall:16( _getCurrentMicros@0150898&lt;0&gt; ) T{int}
    Branch if RelationalOp:12(&lt;, v57 T{_Smi}, v22) T{bool} goto (26, 27)
    ...
    Branch if TestInt(v69, v23) goto (3, 4)
B3[target]:22  // condition() returned true
    v50 &lt;- StaticCall:16( _getCurrentMicros@0150898&lt;0&gt; ) T{int}
    v78 &lt;- BinarySmiOp:26(&lt;&lt;, v50 T{_Smi}, v23) T{_Smi}  // expensive() * 2
    goto B5
B4[target]:28  // condition() returned false
    goto B5
B5[join]:32 pred(B3, B4) {
    v5 &lt;- phi(v78 T{_Smi}, v4)  // v4 is constant 42
}
    DartReturn:40(v5)
</code></pre></div></div>

<p>The structure is identical. The IIFE is completely inlined with no closure allocation or call overhead.</p>

<h3 id="switch-expression-benchmark">Switch expression benchmark</h3>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">enum</span> <span class="n">Status</span> <span class="p">{</span> <span class="n">loading</span><span class="p">,</span> <span class="n">error</span><span class="p">,</span> <span class="n">success</span> <span class="p">}</span>

<span class="kt">void</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
  <span class="kd">final</span> <span class="n">statuses</span> <span class="o">=</span> <span class="p">[</span><span class="n">Status</span><span class="o">.</span><span class="na">loading</span><span class="p">,</span> <span class="n">Status</span><span class="o">.</span><span class="na">error</span><span class="p">,</span> <span class="n">Status</span><span class="o">.</span><span class="na">success</span><span class="p">];</span>
  <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="p">&lt;</span> <span class="mi">100000</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">getWithIIFE</span><span class="p">(</span><span class="n">statuses</span><span class="p">[</span><span class="n">i</span> <span class="o">%</span> <span class="mi">3</span><span class="p">]);</span>
    <span class="n">getTraditional</span><span class="p">(</span><span class="n">statuses</span><span class="p">[</span><span class="n">i</span> <span class="o">%</span> <span class="mi">3</span><span class="p">]);</span>
  <span class="p">}</span>
  <span class="n">print</span><span class="p">(</span><span class="s">'Done warming up'</span><span class="p">);</span>
  <span class="kd">final</span> <span class="n">sw1</span> <span class="o">=</span> <span class="n">Stopwatch</span><span class="p">().</span><span class="o">.</span><span class="na">start</span><span class="p">();</span>
  <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="p">&lt;</span> <span class="mi">10000000</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span> <span class="n">getWithIIFE</span><span class="p">(</span><span class="n">statuses</span><span class="p">[</span><span class="n">i</span> <span class="o">%</span> <span class="mi">3</span><span class="p">]);</span> <span class="p">}</span>
  <span class="n">sw1</span><span class="o">.</span><span class="na">stop</span><span class="p">();</span>
  <span class="kd">final</span> <span class="n">sw2</span> <span class="o">=</span> <span class="n">Stopwatch</span><span class="p">().</span><span class="o">.</span><span class="na">start</span><span class="p">();</span>
  <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="p">&lt;</span> <span class="mi">10000000</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span> <span class="n">getTraditional</span><span class="p">(</span><span class="n">statuses</span><span class="p">[</span><span class="n">i</span> <span class="o">%</span> <span class="mi">3</span><span class="p">]);</span> <span class="p">}</span>
  <span class="n">sw2</span><span class="o">.</span><span class="na">stop</span><span class="p">();</span>
  <span class="n">print</span><span class="p">(</span><span class="s">'Switch+IIFE: </span><span class="si">${sw1.elapsedMicroseconds}</span><span class="s">us'</span><span class="p">);</span>
  <span class="n">print</span><span class="p">(</span><span class="s">'Traditional: </span><span class="si">${sw2.elapsedMicroseconds}</span><span class="s">us'</span><span class="p">);</span>
<span class="p">}</span>

<span class="nd">@pragma</span><span class="p">(</span><span class="s">'vm:never-inline'</span><span class="p">)</span>
<span class="kt">String</span> <span class="nf">getWithIIFE</span><span class="p">(</span><span class="n">Status</span> <span class="n">status</span><span class="p">)</span> <span class="p">{</span>  <span class="c1">// Switch expression with IIFEs</span>
  <span class="k">return</span> <span class="k">switch</span> <span class="p">(</span><span class="n">status</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">Status</span><span class="o">.</span><span class="na">loading</span> <span class="o">=</span><span class="p">&gt;</span> <span class="p">()</span> <span class="p">{</span>
      <span class="kd">final</span> <span class="n">msg</span> <span class="o">=</span> <span class="n">computeMessage</span><span class="p">(</span><span class="s">'load'</span><span class="p">);</span>
      <span class="k">return</span> <span class="s">'Loading: </span><span class="si">$msg</span><span class="s">'</span><span class="p">;</span>
    <span class="p">}(),</span>
    <span class="n">Status</span><span class="o">.</span><span class="na">error</span> <span class="o">=</span><span class="p">&gt;</span> <span class="p">()</span> <span class="p">{</span>
      <span class="kd">final</span> <span class="n">code</span> <span class="o">=</span> <span class="n">getErrorCode</span><span class="p">();</span>
      <span class="k">return</span> <span class="s">'Error </span><span class="si">$code</span><span class="s">: </span><span class="si">${computeMessage('err')}</span><span class="s">'</span><span class="p">;</span>
    <span class="p">}(),</span>
    <span class="n">Status</span><span class="o">.</span><span class="na">success</span> <span class="o">=</span><span class="p">&gt;</span> <span class="s">'OK'</span><span class="p">,</span>
  <span class="p">};</span>
<span class="p">}</span>

<span class="nd">@pragma</span><span class="p">(</span><span class="s">'vm:never-inline'</span><span class="p">)</span>
<span class="kt">String</span> <span class="nf">getTraditional</span><span class="p">(</span><span class="n">Status</span> <span class="n">status</span><span class="p">)</span> <span class="p">{</span>  <span class="c1">// Traditional switch statement</span>
  <span class="k">switch</span> <span class="p">(</span><span class="n">status</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">case</span> <span class="n">Status</span><span class="o">.</span><span class="na">loading</span><span class="o">:</span> <span class="k">return</span> <span class="s">'Loading: </span><span class="si">${computeMessage('load')}</span><span class="s">'</span><span class="p">;</span>
    <span class="k">case</span> <span class="n">Status</span><span class="o">.</span><span class="na">error</span><span class="o">:</span> <span class="k">return</span> <span class="s">'Error </span><span class="si">${getErrorCode()}</span><span class="s">: </span><span class="si">${computeMessage('err')}</span><span class="s">'</span><span class="p">;</span>
    <span class="k">case</span> <span class="n">Status</span><span class="o">.</span><span class="na">success</span><span class="o">:</span> <span class="k">return</span> <span class="s">'OK'</span><span class="p">;</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="nd">@pragma</span><span class="p">(</span><span class="s">'vm:prefer-inline'</span><span class="p">)</span>
<span class="kt">String</span> <span class="nf">computeMessage</span><span class="p">(</span><span class="kt">String</span> <span class="n">prefix</span><span class="p">)</span> <span class="o">=</span><span class="p">&gt;</span> <span class="s">'</span><span class="si">$prefix</span><span class="s">-</span><span class="si">${DateTime.now().millisecond}</span><span class="s">'</span><span class="p">;</span>
<span class="nd">@pragma</span><span class="p">(</span><span class="s">'vm:prefer-inline'</span><span class="p">)</span>
<span class="kt">int</span> <span class="nf">getErrorCode</span><span class="p">()</span> <span class="o">=</span><span class="p">&gt;</span> <span class="n">DateTime</span><span class="o">.</span><span class="na">now</span><span class="p">()</span><span class="o">.</span><span class="na">second</span><span class="p">;</span>
</code></pre></div></div>

<p>The optimized IL for both versions is structurally identical, just like the if-else case. The VM inlines IIFEs in switch expression arms just as effectively.</p>

<h3 id="dart2js-output">dart2js output</h3>

<p>Compile with <code class="language-plaintext highlighter-rouge">dart compile js -O2 -o out.js file.dart</code> and inspect the output:</p>

<p><strong>Traditional version</strong> (inlined directly):</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Cleaned up:</span>
<span class="nf">getY</span><span class="p">()</span> <span class="p">{</span>
  <span class="kd">var</span> <span class="nx">result</span><span class="p">;</span>
  <span class="k">if </span><span class="p">(</span><span class="nf">condition</span><span class="p">())</span> <span class="p">{</span>
    <span class="nx">result</span> <span class="o">=</span> <span class="nf">expensive</span><span class="p">()</span> <span class="o">*</span> <span class="mi">2</span><span class="p">;</span>
  <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
    <span class="nx">result</span> <span class="o">=</span> <span class="mi">42</span><span class="p">;</span>
  <span class="p">}</span>
  <span class="k">return</span> <span class="nx">result</span><span class="p">;</span>
<span class="p">}</span>

<span class="c1">// Actual minified output:</span>
<span class="c1">// cg(){var t,s=A.cb()</span>
<span class="c1">// if(A.ak(new A.H(Date.now(),0,!1))&gt;500){Date.now()</span>
<span class="c1">// t=0}else t=42</span>
<span class="c1">// A.ci("IIFE: "+s+", Traditional: "+t)}</span>
</code></pre></div></div>

<p><strong>IIFE version</strong> (closure as prototype method):</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Cleaned up:</span>
<span class="nf">getX</span><span class="p">()</span> <span class="p">{</span>
  <span class="k">return</span> <span class="k">new</span> <span class="nx">A</span><span class="p">.</span><span class="nf">ai</span><span class="p">().</span><span class="nf">$0</span><span class="p">();</span>  <span class="c1">// &lt;-- allocates closure, calls $0</span>
<span class="p">}</span>

<span class="nx">A</span><span class="p">.</span><span class="nx">ai</span><span class="p">.</span><span class="nx">prototype</span> <span class="o">=</span> <span class="p">{</span>
  <span class="nf">$0</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">if </span><span class="p">(</span><span class="nf">condition</span><span class="p">())</span> <span class="p">{</span>
      <span class="k">return</span> <span class="nf">expensive</span><span class="p">()</span> <span class="o">*</span> <span class="mi">2</span><span class="p">;</span>
    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
      <span class="k">return</span> <span class="mi">42</span><span class="p">;</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="c1">// Actual minified output:</span>
<span class="c1">// cb(){return new A.ai().$0()},</span>
<span class="c1">// A.ai.prototype={</span>
<span class="c1">// $0(){if(A.ak(new A.H(Date.now(),0,!1))&gt;500){Date.now()</span>
<span class="c1">// return 0}else return 42},</span>
<span class="c1">// $S:0}</span>
</code></pre></div></div>

<p>The IIFE version has a small overhead: <code class="language-plaintext highlighter-rouge">new A.ai().$0()</code> allocates a closure object and calls through <code class="language-plaintext highlighter-rouge">$0</code>. However, V8 and other modern JS engines inline these aggressively, so benchmarks show no measurable difference in practice.</p>

<p><strong>Bottom line:</strong> IIFEs have zero runtime cost in optimized Dart VM code, and negligible cost in JavaScript. Use them freely.</p>

<h2 id="conclusion">Conclusion</h2>

<p>IIFEs are a simple concept with broad applications. They give you expressions where Dart only offers statements, scoping where Dart gives you a flat namespace, and flexibility where the language is rigid.</p>

<p>The syntax <code class="language-plaintext highlighter-rouge">() {}()</code> might look odd at first, but once you internalize it, you'll start seeing opportunities everywhere: that complex ternary that's getting out of hand, that variable leaking into scope where it shouldn't, that widget tree begging for a bit of logic.</p>

<p>Dart 3 added switch expressions and if-case patterns, which cover some of the ground IIFEs used to own. But IIFEs remain more general. They're not a feature the language gave you; they're a pattern that emerges from first principles. And patterns that emerge from first principles tend to stick around.</p>

<p>Give IIFEs a chance. Your code will thank you.</p>

<hr />

<p><strong>PS:</strong> IIFEs emerged as a concept in the JavaScript world (<a href="https://developer.mozilla.org/en-US/docs/Glossary/IIFE">MDN reference</a>) where they are very common. The thing about Dart is that IIFEs are even cleaner than in JS. To use a function expression with a function body, you don't need any <code class="language-plaintext highlighter-rouge">function</code> keywords in Dart, they just work. And you can immediately call them without wrapping parentheses. That's actually very cool, and I can't imagine a cleaner syntax for IIFEs than what Dart offers. I'd love to see the Dart community not reinvent the wheel, but actually use the wheel we have, because our wheel is even better.</p>

<p><strong>PPS:</strong> IIFEs increase the expressivity of statements and expressions by providing a way to merge them. Let's assume we don't know what IIFEs are and our language consists only of statements and expressions: We can't put arbitrary statements into arbitrary expressions with statements and expressions in our language only. Once we add IIFEs to our vocabulary, we can <strong>always</strong> put arbitrary statements into arbitrary expressions. Similar to how expression statements allow you to put expressions into statements, IIFEs can be seen as a "Statement Expression" because they allow you to put statements into expressions.</p>

<hr />

<p><a href="https://www.reddit.com/r/FlutterDev/comments/1pttn8j/comment/nvkf8bk/">Discuss on Reddit</a></p>]]></content><author><name>Modestas Valauskas</name></author><category term="technical" /><category term="dart" /><category term="flutter" /><summary type="html"><![CDATA[Immediately Invoked Function Expressions unlock powerful patterns in Dart that most developers overlook. From if-expressions to scoped variables to Flutter widgets, IIFEs deserve more attention.]]></summary></entry><entry><title type="html">I failed to run Dart on the web (but FYI you can run Linux on the web)</title><link href="https://modulovalue.com/blog/i-failed-to-run-dart-on-the-web/" rel="alternate" type="text/html" title="I failed to run Dart on the web (but FYI you can run Linux on the web)" /><published>2025-12-21T14:00:00+01:00</published><updated>2025-12-21T14:00:00+01:00</updated><id>https://modulovalue.com/blog/i-failed-to-run-dart-on-the-web</id><content type="html" xml:base="https://modulovalue.com/blog/i-failed-to-run-dart-on-the-web/"><![CDATA[<p>I had what I thought was a fun idea: what if you could compile and run Dart code entirely in the browser? No server, no cloud VM, just pure client-side execution.</p>

<p>The plan was simple:</p>

<ol>
  <li>Use <a href="https://github.com/copy/v86">v86</a>, a JavaScript x86 emulator, to boot Linux in the browser</li>
  <li>Use <a href="https://microsoft.github.io/monaco-editor/">Monaco Editor</a> for a nice code editing experience</li>
  <li>Download the Dart SDK into the virtual Linux environment</li>
  <li>Compile and run Dart code</li>
</ol>

<p>I got steps 1 and 2 working. Step 3 is where it all fell apart.</p>

<h2 id="what-i-built">What I Built</h2>

<p>Here's the working prototype, the actual demo running live right here in this blog post:</p>

<div style="margin: 2rem 0;">
  <iframe src="https://modulovalue.com/linux_web/" style="width: 100%; height: 500px; border: none; border-radius: 8px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);"></iframe>
</div>

<p>Go ahead, try it. On the left, you have a code editor. On the right, a Linux terminal. Write a script, click "Run Script", and watch it execute in a real Linux environment, all running in your browser.</p>

<p>The fascinating part? That's not a terminal emulator pretending to be Linux. That's actual Linux. The output shows a real root filesystem with <code class="language-plaintext highlighter-rouge">/bin</code>, <code class="language-plaintext highlighter-rouge">/dev</code>, <code class="language-plaintext highlighter-rouge">/proc</code>, and all the standard directories you'd expect. (<a href="https://modulovalue.com/linux_web/">Open in a new tab</a> if you prefer a full-page view.)</p>

<h2 id="the-v86-magic">The v86 Magic</h2>

<p><a href="https://github.com/copy/v86">v86</a> is an x86 emulator written in JavaScript and WebAssembly. It can boot real operating systems: Linux, Windows 98, FreeDOS, and others. The emulation is accurate enough to run unmodified operating system images.</p>

<p>If you've never seen this before, take a moment to appreciate how wild it is. Your browser is running a complete x86 CPU emulation, which is running a Linux kernel, which is running a shell, which is executing your commands. All of this happens entirely client-side.</p>

<h2 id="why-it-failed">Why It Failed</h2>

<p>The Dart SDK requires glibc (the GNU C Library). Most tiny Linux distributions use musl libc instead because it's much smaller. The distributions that support glibc are significantly larger and take much longer to boot.</p>

<p>I tried several approaches:</p>

<p><strong>Alpine Linux</strong>: Fast and small, but uses musl. Dart doesn't work.</p>

<p><strong>Buildroot with glibc</strong>: I could configure it to use glibc, but the resulting image was too large and boot times were unacceptable for a web demo.</p>

<p><strong>Debian/Ubuntu minimal</strong>: Way too large. Boot times measured in minutes, not seconds.</p>

<p>The fundamental problem is that the Dart SDK is designed for real machines with real resources. It expects glibc, it expects fast disk I/O, and it expects more memory than a browser-based x86 emulator can reasonably provide.</p>

<h2 id="what-i-learned">What I Learned</h2>

<p>Even though the project didn't achieve its goal, I found the exploration worthwhile.</p>

<p><strong>v86 is impressive.</strong> The fact that you can boot Linux in a browser is remarkable. For educational purposes, lightweight Linux tools, or just showing off what WebAssembly can do, it's a fantastic project.</p>

<p><strong>The web platform keeps surprising me.</strong> Between WebAssembly, SharedArrayBuffer, and modern JavaScript APIs, browsers can do things that would have seemed impossible a decade ago.</p>

<p><strong>Some tools just aren't meant for constrained environments.</strong> The Dart SDK is designed for development machines. Trying to squeeze it into a browser-emulated Linux with limited resources was always going to be a stretch.</p>

<h2 id="a-challenge">A Challenge</h2>

<p>I'm setting this aside for now, but maybe you can get it to work. If you manage to get Dart running in the browser via v86 (or any other way), I'd love to hear about it.</p>

<p><strong>Addendum:</strong> It was brought to my attention by <a href="https://x.com/norbertkozsir">Norbert Kozsir</a> that <a href="https://x.com/mikediarmid">Mike Diarmid</a> managed to compile the Dart VM to WASM: <a href="https://x.com/mikediarmid/status/1928822145888444821">see his tweet</a>. Maybe he'll actually share his approach with the world? The challenge remains: there's no open source way to do this.</p>

<p><strong>Addendum 2:</strong> <a href="https://www.reddit.com/r/linux/comments/1psdpuc/comment/nv8ne6b/">u/Journeyj012 pointed out</a> that Linux also runs in a PDF. Few people know, but PDF supports JavaScript. Check out <a href="https://github.com/ading2210/linuxpdf">linuxpdf on GitHub</a>.</p>

<p><strong>Discuss on Reddit:</strong> <a href="https://www.reddit.com/r/linux/comments/1psdpuc/its_possible_to_run_linux_in_the_browser/">r/linux</a></p>

<p><strong>Links:</strong></p>
<ul>
  <li><a href="https://github.com/modulovalue/linux_web">Source code on GitHub</a></li>
  <li><a href="https://github.com/copy/v86">v86 on GitHub</a></li>
  <li><a href="https://copy.sh/v86/">v86 live demos</a></li>
  <li><a href="https://microsoft.github.io/monaco-editor/">Monaco Editor</a></li>
</ul>]]></content><author><name>Modestas Valauskas</name></author><category term="technical" /><summary type="html"><![CDATA[An attempt to run Dart in the browser using v86 x86 emulator and Linux. Includes a working demo of Linux running entirely client-side in WebAssembly.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://modulovalue.com/assets/posts/i-failed-to-run-dart-on-the-web/preview.png" /><media:content medium="image" url="https://modulovalue.com/assets/posts/i-failed-to-run-dart-on-the-web/preview.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Concepts Reader: a backup plan for my life</title><link href="https://modulovalue.com/blog/concepts-reader-a-backup-plan-for-my-life/" rel="alternate" type="text/html" title="Concepts Reader: a backup plan for my life" /><published>2025-12-20T14:00:00+01:00</published><updated>2025-12-20T14:00:00+01:00</updated><id>https://modulovalue.com/blog/concepts-reader-a-backup-plan-for-my-life</id><content type="html" xml:base="https://modulovalue.com/blog/concepts-reader-a-backup-plan-for-my-life/"><![CDATA[<p>I manage my entire life in <a href="https://concepts.app/">Concepts</a>, the infinite canvas drawing app for iPad.</p>

<div style="margin: 2rem 0;">
  <iframe src="https://modulovalue.com/concepts_reader/" style="width: 100%; height: 600px; border: none; border-radius: 8px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);"></iframe>
</div>

<p style="text-align: center; margin-top: -1rem;"><a href="https://modulovalue.com/concepts_reader/" target="_blank">Open full app in new tab</a> | <a href="https://github.com/modulovalue/concepts_reader" target="_blank">Source code on GitHub</a></p>

<p>Not notes. Not lists. Graphs.</p>

<p>I draw what I call TODO graphs: dependency diagrams where tasks, ideas, and projects connect to each other visually. Every area of my life, work, personal projects, long-term goals, exists as interconnected nodes on an infinite canvas. This isn't a productivity hack. It's how my brain works.</p>

<div style="margin: 2rem 0;">
  <img src="/assets/posts/concepts-reader/todo-graph-example.png" alt="Example of a TODO graph in Concepts" style="max-width: 100%; height: auto; border-radius: 8px;" />
</div>

<p style="text-align: center; margin-top: -1rem; font-size: 0.9em; color: #666;">An example board. Center: current TODOs. Periphery: research papers I'm reading or implementing. The graphs have topmost nodes with no dependencies, those are tasks I can work on today.</p>

<p>The Concepts team has been stellar. Responsive support, thoughtful updates, a product that genuinely feels like it was made for people who think visually. I've been a happy user for years.</p>

<p>But recently, I've been worried.</p>

<h2 id="the-problem-with-infinite">The Problem with "Infinite"</h2>

<p>My boards have grown. A lot. Some of them are now gigabytes in size. I've already had to buy a new iPad once because older devices couldn't handle the memory requirements. The app occasionally struggles with rendering performance on my largest canvases.</p>

<p>This isn't a complaint about Concepts. They're doing impressive work pushing the boundaries of what's possible on an iPad. But the reality is that my use case is extreme, and there's always a chance that future updates, iOS changes, or simply the accumulation of more data will eventually break things beyond repair.</p>

<p>And if that happens? My "life" is trapped in a proprietary format.</p>

<p>Well, it happened before.</p>

<p>One day my board simply wouldn't open on my iPad anymore. I felt lost. Luckily, you can AirDrop boards to your computer, and I discovered that <code class="language-plaintext highlighter-rouge">.concept</code> files are just ZIP archives. I tried compressing the embedded images to reduce the file size. That didn't work. The runtime apparently uses raw pixel data, so compression doesn't help. I had to manually replace images with smaller versions until the board would load again.</p>

<p>That was the first time I started to have trust issues.</p>

<p>Recently, performance problems appeared. Scrolling became sluggish. I couldn't reproduce the issues in other boards. Support asked me to share my board so they could investigate.</p>

<p>Share my board? Share my <em>life</em> with them? No.</p>

<h2 id="building-a-safety-net">Building a Safety Net</h2>

<p>So I wrote a viewer. Just a proof of concept to answer one question: if Concepts stops working for me tomorrow, can I still access my data?</p>

<p>The answer is yes.</p>

<p>The app above is written in Flutter. Drop a <code class="language-plaintext highlighter-rouge">.concept</code> file onto it, and it renders all your strokes and images. Pan around, zoom in, see your work.</p>

<p>It's not Concepts. You can't edit anything. But you can <em>see</em> everything. And sometimes that's enough.</p>

<h2 id="whats-inside-a-concept-file">What's Inside a .concept File</h2>

<p>Turns out, <code class="language-plaintext highlighter-rouge">.concept</code> files are just ZIP archives with a specific structure:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">Strokes.plist</code> contains all the stroke data in Apple's binary plist format</li>
  <li><code class="language-plaintext highlighter-rouge">Resources.plist</code> maps image references to actual files</li>
  <li><code class="language-plaintext highlighter-rouge">Drawing.plist</code> stores the document's transform matrix</li>
  <li><code class="language-plaintext highlighter-rouge">ImportedImages/</code> holds embedded images</li>
</ul>

<p>The stroke data itself is straightforward: arrays of points with pressure, color, and brush information. Parsing it required writing a binary plist parser, but the format is well-documented.</p>

<h2 id="why-flutter">Why Flutter?</h2>

<p>I wanted this to run everywhere. Web, desktop, mobile. Flutter made that trivial. The same codebase works on all platforms, and the web version means anyone can try it without installing anything.</p>

<h2 id="work-in-progress">Work in Progress</h2>

<p>The viewer isn't feature complete yet. Some strokes render incorrectly. Concepts is a complex app that supports many use cases, and supporting them all is not straightforward.</p>

<p>If you find bugs, please <a href="https://github.com/modulovalue/concepts_reader/issues">report them on GitHub</a>. A minimal repro file helps a lot.</p>

<h2 id="the-lesson">The Lesson</h2>

<p>This isn't about incompatibility or Concepts doing something wrong. Concepts was never designed to be a production-grade research tool or a graph visualization system. I've been surprised time and time again by how far it goes, how much it handles, how well it scales. The team has exceeded every reasonable expectation.</p>

<p>But the real question is: what happens when you reach the limits of your tool and there's no alternative that supports your use case?</p>

<p>That's why having a backup plan matters. Not because your tool will fail you, but because you might outgrow it. A read-only viewer won't replace what you've built your workflow around, but it means your data isn't trapped if you ever need to move on.</p>

<h2 id="addendum-pdf-support">Addendum: PDF Support</h2>

<p>I've added PDF rendering support to the viewer, and I'm impressed.</p>

<p>Flutter renders PDFs much better than Concepts does, without the stutters I experience in Concepts when navigating large boards with many PDF pages. This surprised me. Concepts claims to have a custom renderer, so I assumed they would be highly optimized for exactly these use cases. Apparently, you don't need a custom renderer if you're using a good framework like Flutter.</p>

<p>There are a couple of possible explanations. I'm rendering PDF pages at a lower resolution than Concepts does. Additionally, on a macOS desktop, I have all my RAM available, whereas iPads heavily limit RAM usage per app, even if the device has more RAM than is currently being used. There's an upper limit that each app can use, regardless of what's actually available.</p>

<p><strong>Links:</strong></p>
<ul>
  <li><a href="https://modulovalue.com/concepts_reader/">Live demo</a></li>
  <li><a href="https://github.com/modulovalue/concepts_reader">Source code on GitHub</a></li>
  <li><a href="https://concepts.app/">Concepts app</a></li>
</ul>]]></content><author><name>Modestas Valauskas</name></author><category term="technical" /><category term="flutter" /><category term="tools" /><summary type="html"><![CDATA[An open-source Flutter app that reads .concept files from the Concepts drawing app. Built as a data recovery safety net for when proprietary formats become a risk.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://modulovalue.com/assets/posts/concepts-reader/preview.png" /><media:content medium="image" url="https://modulovalue.com/assets/posts/concepts-reader/preview.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Understanding Dart class modifiers by using lattices</title><link href="https://modulovalue.com/blog/understanding-dart-class-modifiers-lattices/" rel="alternate" type="text/html" title="Understanding Dart class modifiers by using lattices" /><published>2025-12-18T10:00:00+01:00</published><updated>2025-12-18T10:00:00+01:00</updated><id>https://modulovalue.com/blog/understanding-dart-class-modifiers-lattices</id><content type="html" xml:base="https://modulovalue.com/blog/understanding-dart-class-modifiers-lattices/"><![CDATA[<p>Dart 3.0 introduced class modifiers, and at first glance, the combinations can feel overwhelming. <code class="language-plaintext highlighter-rouge">base</code>, <code class="language-plaintext highlighter-rouge">final</code>, <code class="language-plaintext highlighter-rouge">interface</code>, <code class="language-plaintext highlighter-rouge">mixin</code>. How do they all fit together? What combinations are valid? Which ones are redundant?</p>

<p>It turns out there's an elegant way to understand the entire system: <a href="https://en.wikipedia.org/wiki/Lattice_(order)">lattice theory</a>.</p>

<h2 id="the-four-capabilities">The Four Capabilities</h2>

<p>Every Dart type has some combination of four fundamental capabilities:</p>

<ol>
  <li><strong>Extendable</strong>: can be used with <code class="language-plaintext highlighter-rouge">extends</code></li>
  <li><strong>Implementable</strong>: can be used with <code class="language-plaintext highlighter-rouge">implements</code></li>
  <li><strong>Mixinable</strong>: can be used with <code class="language-plaintext highlighter-rouge">with</code></li>
  <li><strong>Constructable</strong>: can be instantiated directly</li>
</ol>

<p>Each class modifier combination enables or restricts these capabilities. The lattice below shows how all valid combinations relate to each other.</p>

<h2 id="the-class-modifiers-lattice">The Class Modifiers Lattice</h2>

<div style="overflow-x: auto; margin: 2rem 0;">
  <img src="/assets/posts/understanding-dart-class-modifiers-lattices/lattice.svg" alt="Dart Class Modifiers Lattice" style="max-width: 100%; height: auto;" />
</div>

<p style="text-align: center; margin-top: -1rem;"><a href="/assets/posts/understanding-dart-class-modifiers-lattices/lattice.svg" target="_blank">Open full diagram in new tab ↗</a></p>

<h2 id="reading-the-lattice">Reading the Lattice</h2>

<p>The lattice flows from bottom to top:</p>

<ul>
  <li><strong>Bottom</strong>: <code class="language-plaintext highlighter-rouge">Nothing</code> (no capabilities)</li>
  <li><strong>Top</strong>: <code class="language-plaintext highlighter-rouge">mixin class</code> (all four capabilities)</li>
</ul>

<p>Each arrow represents adding one capability. Follow arrows upward to see how adding capabilities transforms one modifier combination into another.</p>

<h3 id="color-coding">Color Coding</h3>

<p><strong>Node backgrounds:</strong></p>
<ul>
  <li><strong>Yellow</strong>: Existed before Dart 3.0</li>
  <li><strong>Green</strong>: New with class modifiers</li>
  <li><strong>Red</strong>: Impossible combinations</li>
</ul>

<p><strong>Arrow colors</strong> represent which capability is being added:</p>
<ul>
  <li><strong>Orange</strong>: Mixinable</li>
  <li><strong>Teal</strong>: Extendable</li>
  <li><strong>Blue</strong>: Implementable</li>
  <li><strong>Brown</strong>: Constructable</li>
</ul>

<h2 id="key-insights">Key Insights</h2>

<h3 id="1-mixin-class-is-maximum-capability">1. <code class="language-plaintext highlighter-rouge">mixin class</code> is Maximum Capability</h3>

<p>A plain <code class="language-plaintext highlighter-rouge">mixin class</code> has all four capabilities. It's the most permissive type you can declare. This is why it sits at the top of the lattice.</p>

<h3 id="2-some-combinations-are-impossible">2. Some Combinations Are Impossible</h3>

<p>Notice the red nodes. For example, <code class="language-plaintext highlighter-rouge">final base mixin class</code> is impossible because mixin classes <em>must</em> be extendable (that's how mixins work), but <code class="language-plaintext highlighter-rouge">final</code> prevents extension.</p>

<h3 id="3-pre-dart-30-types-were-limited">3. Pre-Dart 3.0 Types Were Limited</h3>

<p>The yellow nodes show what existed before: <code class="language-plaintext highlighter-rouge">class</code>, <code class="language-plaintext highlighter-rouge">abstract class</code>, and <code class="language-plaintext highlighter-rouge">mixin</code>. The green nodes are all the new combinations that class modifiers enable.</p>

<h2 id="practical-examples">Practical Examples</h2>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// All four capabilities</span>
<span class="kd">mixin</span> <span class="nc">class</span> <span class="n">A</span> <span class="p">{}</span>

<span class="c1">// Remove implementability, must extend, can't just implement</span>
<span class="kd">base</span> <span class="kd">mixin</span> <span class="nc">class</span> <span class="n">B</span> <span class="p">{}</span>

<span class="c1">// Remove extendability and mixinability, can only implement</span>
<span class="kd">interface</span> <span class="kd">class</span> <span class="nc">C</span> <span class="p">{}</span>

<span class="c1">// Remove everything except constructability</span>
<span class="kd">final</span> <span class="kd">class</span> <span class="nc">D</span> <span class="p">{}</span>
</code></pre></div></div>

<h2 id="why-lattices">Why Lattices?</h2>

<p>Lattices aren't just a visualization trick. They reveal the <em>algebraic structure</em> of the type system. The fact that Dart's class modifiers form a clean lattice means the design is internally consistent. There are no arbitrary restrictions or special cases.</p>

<p>Understanding this structure helps you:</p>
<ul>
  <li>Remember which combinations are valid</li>
  <li>Predict what capabilities a type has</li>
  <li>Choose the right modifier for your use case</li>
</ul>

<p>The next time you're unsure which class modifier to use, think about which capabilities you want to allow, and find the corresponding node in the lattice.</p>

<h2 id="conclusion">Conclusion</h2>

<p>Complex systems with many interacting options are hard to reason about. When you have four independent boolean capabilities, you get 2^4 = 16 possible combinations. Trying to understand these combinations through documentation alone quickly becomes overwhelming.</p>

<p>Lattices provide a way to model these combinations visually and algebraically. Instead of memorizing rules, you can <em>see</em> the relationships. Each node is a valid state, each edge is a transition, and the structure itself encodes the constraints.</p>

<p>As a fun aside: this hierarchy is actually a 4-dimensional cube (a <a href="https://en.wikipedia.org/wiki/Tesseract">tesseract</a>). Each of the four capabilities corresponds to one dimension, and moving along an edge means toggling that capability on or off. But since we can't intuitively grasp 4-dimensional geometric objects (at least I can't, though <a href="https://www.reddit.com/r/math/comments/iibkbp/mathematician_who_claimed_to_be_able_to_visualize/">some claim they can</a>), the lattice representation serves as a more accessible algebraic structure to aid intuition.</p>

<p><strong>PS:</strong> To be complete when it comes to class modifiers, we would also have to discuss <a href="https://dart.dev/language/modifier-reference">sealed classes</a>. They don't fit into this system. In my view, they are a separate feature and should be discussed separately.</p>

<p><strong>PPS:</strong> Here's the <a href="https://dart.dev/language/class-modifiers">official documentation for class modifiers</a>, the <a href="https://github.com/dart-lang/language/blob/main/accepted/3.0/class-modifiers/feature-specification.md">spec</a>, and the <a href="https://github.com/dart-lang/sdk/blob/583fbe5962309d6305fc4855f52ec807b84f4aed/tools/spec_parser/Dart.g#L458-L465">officially maintained ANTLR grammar</a> showing the syntax of modifiers.</p>

<p><strong>Addendum:</strong> <a href="https://journal.stuffwithstuff.com/">Robert Nystrom</a>, the lead designer of this feature, <a href="https://www.reddit.com/r/dartlang/comments/1pqimnr/comment/nuvfxqh/">pointed out</a> that despite <code class="language-plaintext highlighter-rouge">mixin class</code> having the most capabilities, the Dart team doesn't think it should be used often. It is mostly there for backwards compatibility.</p>

<p><strong>Discuss on Reddit:</strong> <a href="https://www.reddit.com/r/dartlang/comments/1pqimnr/understanding_dart_class_modifiers_by_using/">r/dartlang</a></p>]]></content><author><name>Modestas Valauskas</name></author><category term="technical" /><category term="dart" /><category term="visualisation" /><summary type="html"><![CDATA[A visual guide to Dart 3.0 class modifiers using lattice theory. Learn how base, final, interface, and mixin modifiers relate to each other through an interactive diagram.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://modulovalue.com/assets/posts/understanding-dart-class-modifiers-lattices/lattice-preview.png" /><media:content medium="image" url="https://modulovalue.com/assets/posts/understanding-dart-class-modifiers-lattices/lattice-preview.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>