<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><generator>Reify</generator><language>en</language><title>yakov.codes</title><description>Articles published on yakov.codes blog.</description><link>https://yakov.codes</link><copyright>2024 Yakov Karpov. All rights reserved.</copyright><ttl>60</ttl><atom:link href="https://yakov.codes/rss.xml" rel="self" type="application/rss+xml"/><item><author>hi@yakov.codes (Yakov Karpov)</author><title><![CDATA[ Hermetic and portable CI in a flake ]]></title><link>https://yakov.codes/articles/hermetic-and-portable-ci-in-a-flake</link><guid>https://yakov.codes/articles/hermetic-and-portable-ci-in-a-flake</guid><category>nix</category><category>dart</category><category>flutter</category><category>devops</category><category>ci</category><pubDate>Thu, 28 May 2026 00:00:00 GMT</pubDate><description><![CDATA[ A full-Nix CI setup for a Dart/Flutter monorepo where the GitHub Actions workflow YAML is itself generated from Nix, the checks run with no network access in the sandbox, and `nix flake check` is the entire CI surface — the same primitive locally and on the runner. ]]></description><content:encoded><![CDATA[ <h2 id="why">Why</h2>
<p>Almost every project I've worked on has a <code>.github/workflows/ci.yml</code> that does roughly the same thing. Install the SDK with a setup action written and maintained by someone else, run <code>pub get</code> against pub.dev on a fresh runner, run the tests, push the result. The green check at the bottom of the PR is technically a proof that the tests passed — in a slightly different toolchain than the one on my laptop, against a slightly different snapshot of the dependencies, on a machine that trusted three external systems to behave the same way they did the last time.</p>
<p>For a small library that's fine. For a small library you want to publish to pub.dev and ask other people to depend on, it is also fine, in the way that a lot of things are fine until they aren't.</p>
<p>I just shipped <a href="https://pub.dev/packages/caffeine">caffeine 3.0</a> and redid the CI as <mark>strictly Nix</mark>. Not Nix wrapping setup-dart, but Nix end to end. The dev shell, the test runner, the lockfile handling, the platform deps, the workflow YAML itself — all materialized by the flake. The GitHub Actions runner does the smallest possible job: install Nix, evaluate a matrix from the flake, fan out one runner per check, go home.</p>
<h2 id="what-runs">What runs</h2>
<p><code>flake show</code>, trimmed:</p>
<pre><code>github:purplenoodlesoop/caffeine
├───apps.x86_64-linux.sync-ci
├───checks.x86_64-linux
│   ├───caffeine
│   ├───flutter-caffeine
│   └───ci-up-to-date
├───devShells.x86_64-linux.default
├───githubActions
└───packages.x86_64-linux.ci-yaml
</code></pre>
<p>Three checks (one per package, plus a drift check on the workflow file itself), one dev shell, one package that is the rendered workflow YAML, one app that copies the rendered YAML into the working tree. <code>githubActions</code> is the matrix the workflow consumes — generated by <a href="https://github.com/nix-community/nix-github-actions">nix-community/nix-github-actions</a> from the <code>checks</code> attribute set, so the matrix and the actual list of checks share one source.</p>
<p>The workflow definition lives in <code>nix/ci.nix</code>:</p>
<pre><code class="language-nix">let
  checkoutStep = { name = &quot;Checkout&quot;; uses = &quot;actions/checkout@v4&quot;; };
  installNixStep = {
    name = &quot;Install Nix&quot;;
    uses = &quot;DeterminateSystems/nix-installer-action@v22&quot;;
    &quot;with&quot;.determinate = false;
  };
  magicCacheStep = {
    name = &quot;Magic Nix Cache&quot;;
    uses = &quot;DeterminateSystems/magic-nix-cache-action@v13&quot;;
  };
in
{
  name = &quot;ci&quot;;
  on = {
    push.branches = [ &quot;master&quot; ];
    pull_request = { };
    workflow_dispatch = { };
  };
  jobs = {
    matrix = {
      runs-on = &quot;ubuntu-latest&quot;;
      outputs.matrix = &quot;\${{ steps.set-matrix.outputs.matrix }}&quot;;
      steps = [
        checkoutStep
        installNixStep
        {
          id = &quot;set-matrix&quot;;
          name = &quot;Generate matrix&quot;;
          run = ''
            matrix=&quot;$(nix eval --json '.#githubActions.matrix')&quot;
            echo &quot;matrix=$matrix&quot; &gt;&gt; &quot;$GITHUB_OUTPUT&quot;
          '';
        }
      ];
    };

    check = {
      name = &quot;\${{ matrix.name }}&quot;;
      needs = &quot;matrix&quot;;
      runs-on = &quot;\${{ matrix.os }}&quot;;
      strategy = {
        fail-fast = false;
        matrix = &quot;\${{ fromJSON(needs.matrix.outputs.matrix) }}&quot;;
      };
      steps = [
        checkoutStep
        installNixStep
        magicCacheStep
        { name = &quot;Build check&quot;; run = &quot;nix build -L '.#\${{ matrix.attr }}'&quot;; }
      ];
    };
  };
}
</code></pre>
<p>A Nix attrset. Triggers, concurrency, jobs, steps — all just fields. The matrix job evaluates the flake and emits one entry per check. The check job fans out — <code>caffeine</code>, <code>flutter-caffeine</code>, <code>ci-up-to-date</code> each on its own runner, each as its own status check in the PR. Adding a new check to <code>flake.nix</code> adds a status line to the next PR without touching this file.</p>
<p>The runner has zero direct knowledge of <code>dart</code>, <code>flutter</code>, <code>pub</code>, <code>package_config.json</code> or the workspace layout. It does <code>nix build -L .#&lt;attr&gt;</code> and reports the exit code.</p>
<h2 id="the-check-derivation-is-the-interesting-part">The check derivation is the interesting part</h2>
<p><code>checks.&lt;system&gt;.caffeine</code> is a <code>buildDartApplication</code> derivation whose <code>checkPhase</code> runs the tests inside the Nix sandbox. The sandbox has <mark>no network</mark>. Everything has to be in the closure already.</p>
<p>Three things had to be true at the same time.</p>
<p><mark>First</mark>, <code>pub2nix</code> needs the workspace lockfile as a Nix value, and it is YAML on disk. Convert it inline:</p>
<pre><code class="language-nix">runCommand &quot;${pname}-pubspec.lock.json&quot;
  { nativeBuildInputs = [ yq-go ]; }
  &quot;yq -o=json . ${src}/pubspec.lock &gt; $out&quot;
</code></pre>
<p>This is import-from-derivation, so <code>nix flake check</code> pauses briefly to materialize the JSON before evaluating the rest. Slightly slower, never wrong, no committed <code>pubspec.lock.json</code> to keep in sync with the YAML one.</p>
<p><mark>Second</mark>, <code>dart test</code> calls <code>pub.Entrypoint.ensureUpToDate</code> at startup. That function compares the on-disk lockfile against the generated <code>package_config.json</code> and reaches for pub.dev if it doesn't like what it sees. There is no <code>--no-pub</code> flag on <code>dart test</code>. The trick is a tiny bash helper that <code>dartConfigHook</code> already installs into the build shell:</p>
<pre><code class="language-nix">checkPhase = ''
  cp -rL . &quot;$TMPDIR/src&quot;
  chmod -R u+w &quot;$TMPDIR/src&quot;
  cp -rL &quot;$TMPDIR/src/.dart_tool&quot; &quot;$TMPDIR/src/packages/caffeine/&quot;
  cd &quot;$TMPDIR/src/packages/caffeine&quot;
  HOME=$TMPDIR packageRun test --reporter expanded
'';
</code></pre>
<p><code>packageRun test</code> resolves to <code>dart --packages=.dart_tool/package_config.json &lt;test-pkg&gt;/bin/test.dart</code>. No pub on the call path, no <code>ensureUpToDate</code>, no network probe. Just the Dart VM running the test runner with an explicit package config.</p>
<p><mark>Third</mark>, the workspace has to be invisible at test time. Otherwise <code>dart test</code> walks into the Flutter example as a sibling workspace member and demands the Flutter SDK. After <code>dartConfigHook</code> has built <code>package_config.json</code> (which needs the workspace declaration so workspace members get added to the config), strip both keys:</p>
<pre><code class="language-nix">sed -i '/^workspace:$/,$d' pubspec.yaml
sed -i '/^resolution: workspace$/d' packages/caffeine/pubspec.yaml
</code></pre>
<p>Order matters. Strip in <code>postPatch</code> and <code>workspacePackageConfigScript</code> sees no workspace and never adds <code>caffeine</code> to the config (<code>package:caffeine</code> resolves to nothing, tests fail to compile). Strip in <code>checkPhase</code> and you keep the config but lose the workspace at runtime. The fix is the second option.</p>
<h2 id="the-workflow-itself-is-also-nix">The workflow itself is also Nix</h2>
<p>The <code>.github/workflows/ci.yml</code> file is a generated artifact:</p>
<pre><code class="language-nix">ci-yaml-body = (pkgs.formats.yaml { }).generate &quot;ci-body.yml&quot;
  (import ./nix/ci.nix);

ci-yaml = pkgs.runCommand &quot;ci.yml&quot; { } ''
  cat &gt; $out &lt;&lt;'HEADER'
  # AUTO-GENERATED from nix/ci.nix — DO NOT EDIT BY HAND.
  # Regenerate with: nix run .#sync-ci
  HEADER
  cat ${ci-yaml-body} &gt;&gt; $out
'';
</code></pre>
<p><code>nix/ci.nix</code> is the workflow <mark>as a Nix attrset</mark>. Triggers, concurrency group, steps — all <code>name = &quot;...&quot;; on = { ... };</code>-style fields. <code>pkgs.formats.yaml</code> renders it. The wrapping <code>runCommand</code> prepends the do-not-edit header so the rendered file is self-explaining about how it got there.</p>
<p><code>apps.sync-ci</code> is a <code>writeShellApplication</code> that copies the rendered YAML into <code>.github/workflows/ci.yml</code> at the working tree's root. It has to be a real committed file — GitHub Actions runners can't follow <code>/nix/store/...</code> paths, so we generate and commit, not symlink.</p>
<p>Drift is caught by another check. <code>checks.&lt;sys&gt;.ci-up-to-date</code> is a <code>runCommand</code> that diffs the committed file against the rendered one:</p>
<pre><code class="language-nix">ci-up-to-date = pkgs.runCommand &quot;ci-up-to-date&quot; { } ''
  if ! diff -u ${./.github/workflows/ci.yml} ${ci-yaml}; then
    echo &quot;❌ .github/workflows/ci.yml is out of sync with nix/ci.nix.&quot;
    echo &quot;   Run: nix run .#sync-ci&quot;
    exit 1
  fi
  touch $out
'';
</code></pre>
<p>If somebody hand-edits the workflow, the next CI run fails with a message telling them to regenerate. If somebody edits <code>nix/ci.nix</code> without running <code>sync-ci</code>, same failure. There is no third option, which is the whole point.</p>
<h2 id="what-it-costs">What it costs</h2>
<p>Honest numbers. Cold run, empty cache: ~10–12 minutes. About 8 of those go to the post-job step uploading freshly realized <code>/nix/store</code> paths to GitHub Actions Cache (the Flutter SDK alone, even though we never <code>flutter build</code>, contributes ~150 MB of pre-bundled Linux engine artifacts — <code>buildFlutterApplication</code>'s Linux variant pulls in <code>wrapGAppsHook3</code> and friends regardless of whether you actually build anything for Linux). Warm run, no dependency changes: each check finishes in ~1–2 minutes on its own runner, ~3 minutes wall time including the matrix-evaluation job.</p>
<p>For comparison, the same project with <code>subosito/flutter-action</code> and <code>dart test</code> runs in ~1–2 minutes cold and ~45 seconds warm. <mark>Strictly Nix is roughly 5× slower cold and 3× slower warm.</mark> That cost is real and not going away — sandboxed builds, plus each matrix runner installing Nix from scratch, plus magic-nix-cache's upload phase, are the price.</p>
<p>What you get for the price is small but specific. The same <code>nix build .#&lt;attr&gt;</code> invocation runs identically on my Mac and on ubuntu-latest. Setting up the project on a fresh machine is <code>git clone &amp;&amp; nix develop</code> and the toolchain is at the version pinned in <code>flake.lock</code>, not at whatever <code>setup-dart@v1</code> resolves to today. When something breaks, the breakage is reproducible — which is the only kind of breakage worth fixing.</p>
<p>The value of doing CI this way only shows up the day something does break. If your hermetic test never tells you anything you didn't already know, it isn't hermetic. The whole pipeline has to be tight enough that the next surprise it produces is reproducible.</p>
<h2 id="where-to-look">Where to look</h2>
<ul>
<li><a href="https://github.com/purplenoodlesoop/caffeine/blob/master/flake.nix"><code>flake.nix</code></a> — top-level wiring, dev shell, checks, packages, apps, <code>githubActions</code> matrix</li>
<li><a href="https://github.com/purplenoodlesoop/caffeine/blob/master/nix/mk-check.nix"><code>nix/mk-check.nix</code></a> — shared check builder used by both packages</li>
<li><a href="https://github.com/purplenoodlesoop/caffeine/blob/master/nix/ci.nix"><code>nix/ci.nix</code></a> — workflow as a Nix attrset</li>
<li><a href="https://github.com/purplenoodlesoop/caffeine/blob/master/.github/workflows/ci.yml"><code>.github/workflows/ci.yml</code></a> — the generated file (do not edit)</li>
</ul> ]]></content:encoded></item><item><author>hi@yakov.codes (Yakov Karpov)</author><title><![CDATA[ Calling Haskell from Flutter for fun (and some profit) ]]></title><link>https://yakov.codes/articles/calling-haskell-from-flutter-for-fun-(and-some-profit)</link><guid>https://yakov.codes/articles/calling-haskell-from-flutter-for-fun-(and-some-profit)</guid><category>haskell</category><category>dart</category><category>flutter</category><category>nix</category><category>ffi</category><category>open-source</category><category>release</category><pubDate>Thu, 21 May 2026 00:00:00 GMT</pubDate><description><![CDATA[ A pilot micro-framework for calling Haskell from Dart over FFI, where the same C ABI is written exactly once via a tiny Template Haskell splice and a Dart codegen step that reads an embedded manifest. With a worked Flutter example and benchmarks. ]]></description><content:encoded><![CDATA[ <h2 id="open-source-tldr">Open source TL;DR</h2>
<ul>
<li><a href="https://github.com/purplenoodlesoop/hsdart">https://github.com/purplenoodlesoop/hsdart</a></li>
</ul>
<h2 id="why">Why?</h2>
<p>There's a Dart package on pub.dev called <a href="https://pub.dev/packages/pixer"><code>pixer</code></a>. It's a Flutter image manipulation library, and the interesting part isn't the API. It's that the implementation is <mark>Rust</mark>, called from Dart through FFI, with a build hook that stages prebuilt binaries during the Flutter build. The Dart developer writes nothing about Rust. They just <code>import</code> and call.</p>
<p>A nice pattern. The question that has been bugging me is whether we can do the same thing with <mark>Haskell</mark>. Not &quot;is it possible to call Haskell from Dart&quot;, which is trivially yes (<code>foreign export ccall</code> on one side, <code>@Native</code> on the other, you're done), but &quot;can we make it pleasant enough that the user just writes Haskell functions and they show up in Dart, without spelling out the same C ABI three times by hand&quot;.</p>
<p>That's what <a href="https://github.com/purplenoodlesoop/hsdart"><code>hsdart</code></a> is. A pilot micro-framework, a worked Flutter example, and a benchmark that says the answer is &quot;kinda yes&quot;.</p>
<p><img src="https://raw.githubusercontent.com/purplenoodlesoop/hsdart/main/docs/screenshot.png" alt="hsdart demo" /></p>
<h2 id="what-you-write">What you write</h2>
<p>The whole Haskell side of the example is this:</p>
<pre><code class="language-haskell">{-# LANGUAGE TemplateHaskell #-}
module Core where

import HsDart
import Data.Text (Text)
import qualified Data.Text as T

square :: Int -&gt; Int
square x = x * x

greet :: Text -&gt; Text
greet name = &quot;Hello, &quot; &lt;&gt; name &lt;&gt; &quot;!&quot;

shout :: Text -&gt; Text
shout = T.toUpper

sumPrimesBelow :: Int -&gt; Int
sumPrimesBelow n = … -- sieve via STUArray Word8 + unsafeRead/Write

exportAll ['square, 'greet, 'shout, 'sumPrimesBelow]
</code></pre>
<p>That is it. <mark>One splice</mark>. No <code>foreign export ccall</code>, no <code>CInt</code>, no manual C wrapper. The TH macro reifies each name, picks a marshaling recipe based on the type, and emits the matching wrapper plus the foreign export. It also embeds a tiny JSON manifest of the API into the dylib as another foreign-exported function (<code>hsdart_manifest :: IO CString</code>), and that manifest is what the Dart side reads to generate its bindings.</p>
<p>The Dart side is just as boring:</p>
<pre><code class="language-dart">import 'haskell_example.g.dart';

void main() {
  print(square(12));                  // 144
  print(greet('world'));              // Hello, world!
  print(shout('μετά'));               // ΜΕΤΆ
  print(sumPrimesBelow(100000));      // 454396537
}
</code></pre>
<p><code>haskell_example.g.dart</code> is generated by <code>dart run hsdart_dart:gen</code> before Flutter compiles. Bindings are statically typed, the marshaling wrappers (UTF-8 in/out for strings, <code>int↔CInt</code> for ints) are written once in the codegen, and <code>hs_init</code> is hidden inside a lazy <code>_ensureRts()</code> helper that fires on the first call.</p>
<h2 id="what-actually-happens">What actually happens</h2>
<p>Four moving parts:</p>
<pre><code>Haskell source  →  GHC + cabal  →  libhaskell-example.dylib
                                   (with embedded hsdart_manifest)
                                          │
                                          ↓
                              dart run hsdart_dart:gen
                              (dlopen, read manifest, emit Dart)
                                          │
                                          ↓
                              lib/haskell_example.g.dart
                                          │
                                          ↓
                                  flutter run / build
                                          │
                                          ↓
                            build hook stages dylib as a CodeAsset
                              → Flutter bundles it in the .app
</code></pre>
<ol>
<li><code>exportAll</code> does the heavy lifting on the Haskell side. For each name it <code>reify</code>s the type, dispatches on <code>(from, to)</code> (currently <code>Int</code> and <code>Text</code>), and produces a wrapper <code>hsdartFFI_&lt;name&gt;</code>, a <code>foreign export ccall &lt;name&gt; ...</code>, and contributes the function's signature to a JSON blob that's bundled into the dylib as a foreign-exported function returning a freshly-<code>malloc</code>'d C string.</li>
<li>Building is a <a href="https://github.com/purplenoodlesoop/core-flake"><mark>core-flake</mark></a>-based Nix flake. The Haskell side is two cabal packages, <code>hsdart-haskell</code> (the TH framework, publishable to Hackage) and <code>haskell-example</code> (the user code, depending on it).</li>
<li><code>dart run hsdart_dart:gen</code> <code>nix build</code>s the foreign library, <code>dlopen</code>s it from a regular Dart CLI, calls <code>hsdart_manifest</code>, parses the JSON, and writes <code>lib/haskell_example.g.dart</code> with one <code>@Native</code> declaration plus a marshaling wrapper per function. Plus the <code>_ensureRts()</code> helper around <code>hs_init</code>.</li>
<li>The Flutter build hook is now <mark>two lines</mark>: <code>hsdartBuild(args, flakeAttr: 'haskell-example', flakeDir: '../..')</code>. The helper lives in the <code>hsdart_dart</code> package, <code>nix build</code>s the foreign library, stages it under the right SONAME and registers it as a <code>CodeAsset</code> so Flutter bundles it into the <code>.app</code>.</li>
</ol>
<p>Codegen and the build hook are <mark>deliberately split</mark>. Flutter's hook protocol runs hooks <em>after</em> the Dart kernel snapshot is compiled — codegen from inside the hook would write the bindings, but the snapshot was already built against the previous version (or the stub on first run), so the regenerated bindings don't reach the running app. Codegen has to be a pre-build step.</p>
<h2 id="how-fast-is-it-actually">How fast is it, actually</h2>
<p>The repo ships a benchmark package that compares Haskell-via-FFI to pure-Dart equivalents of the same four functions. It uses <a href="https://pub.dev/packages/benchmark_harness"><code>package:benchmark_harness</code></a> with the standard 100 ms warmup and 2 s measurement window. The task runs the same logic twice, once with <code>dart run</code> (JIT) and once after <code>dart compile exe</code> (AOT), so we get four numbers per function. The foreign library is built with <code>-O2 -funbox-strict-fields</code> and the GHC runtime statically linked in (<code>options: standalone</code>).</p>
<table>
<thead>
<tr>
<th>Function</th>
<th align="right">iters</th>
<th align="right">Dart (JIT)</th>
<th align="right">Dart (AOT)</th>
<th align="right">Haskell+FFI (JIT)</th>
<th align="right">Haskell+FFI (AOT)</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>square(i)</code></td>
<td align="right">100k</td>
<td align="right">5.1 ns</td>
<td align="right">4.8 ns</td>
<td align="right">122.9 ns</td>
<td align="right">117.9 ns</td>
</tr>
<tr>
<td><code>greet(name)</code></td>
<td align="right">10k</td>
<td align="right">36.5 ns</td>
<td align="right">72.1 ns</td>
<td align="right">889.0 ns</td>
<td align="right">860.8 ns</td>
</tr>
<tr>
<td><code>shout(name)</code></td>
<td align="right">10k</td>
<td align="right">27.0 ns</td>
<td align="right">24.5 ns</td>
<td align="right">783.2 ns</td>
<td align="right">731.9 ns</td>
</tr>
<tr>
<td><code>sumPrimesBelow(500k)</code></td>
<td align="right">5</td>
<td align="right">3.16 ms</td>
<td align="right">2.69 ms</td>
<td align="right">1.40 ms</td>
<td align="right">1.40 ms</td>
</tr>
</tbody>
</table>
<p>Three things jump out.</p>
<p><mark>First</mark>, the two <code>Haskell+FFI</code> columns are essentially identical. The Dart-side call site is compiled by either the JIT or the AOT compiler, but in both cases it boils down to &quot;marshal arg, do an indirect call into the native lib, marshal return&quot;. That cost is <mark>structural</mark> and doesn't care which mode you compiled the rest of your Dart in. Roughly ~120 ns for a numeric round trip and ~800 ns for a string round trip (which includes the four-step <code>String → malloc'd UTF-8 → Haskell Text → malloc'd C string → Dart String → free</code> dance).</p>
<p><mark>Second</mark>, Dart AOT is not consistently faster than Dart JIT on hot microbenchmarks. For <code>square</code> and <code>shout</code> AOT shaves off a fraction, for <code>greet</code> JIT is twice as fast because it specialises the string-interpolation path harder, for the sieve they are within noise. The general rule &quot;JIT wins steady-state, AOT wins startup and consistency&quot; holds.</p>
<p><mark>Third</mark>, and this is the actual lesson, the FFI overhead is <mark>fixed per call</mark>. ~120 ns of overhead is 24× the cost of a 5 ns multiplication, but only 0.009% of a 1.4 ms sieve. Spend that fee on a millisecond of real CPU work, like a Sieve of Eratosthenes (<code>sumPrimesBelow</code> does ~520k cache-friendly stores in the marking pass plus ~500k reads and adds in the summing pass), and Haskell beats Dart by ~2× in either compilation mode. Spend it on a multiply and you have paid 20× the cost of the work itself.</p>
<p>One catch on that last row: the sieve had to use <code>STUArray Int Word8</code> with <code>unsafeRead</code>/<code>unsafeWrite</code> to beat Dart. With a <code>Bool</code> payload and bounds-checked reads, the Haskell version was 3.3 ms, barely tied. The <code>Word8</code> array packs the sieve into a byte per entry, the unsafe variants skip a per-access conditional, and together they cut the time by roughly 60%. The full discussion is in <a href="https://github.com/purplenoodlesoop/hsdart/blob/main/BENCHMARKS.md"><code>BENCHMARKS.md</code></a>.</p>
<p>The mental model is just that. <mark>Chunky work amortizes the FFI tax, trivial work cannot</mark>. Reach for Haskell-over-FFI when your function is measured in microseconds or milliseconds, not nanoseconds, and the choice of JIT vs AOT on the Dart side doesn't change the conclusion.</p>
<h2 id="credits">Credits</h2>
<ul>
<li>The pattern is a direct steal from <a href="https://pub.dev/packages/pixer"><code>pixer</code></a>, which does the same thing with Rust. The &quot;Flutter app that calls native code through a build hook&quot; architecture is theirs.</li>
<li>The Nix orchestration is built on top of <a href="https://github.com/purplenoodlesoop/core-flake"><code>purplenoodlesoop/core-flake</code></a>. The <code>tasks</code> module turns flake attributes into <code>nix run .#name</code> apps, and the <code>tasks-help</code> task is auto-generated.</li>
</ul> ]]></content:encoded></item><item><author>hi@yakov.codes (Yakov Karpov)</author><title><![CDATA[ Horizon - a personal AI assistant that lives in your Obsidian vault ]]></title><link>https://yakov.codes/articles/horizon---a-personal-ai-assistant-that-lives-in-your-obsidian-vault</link><guid>https://yakov.codes/articles/horizon---a-personal-ai-assistant-that-lives-in-your-obsidian-vault</guid><category>ai</category><category>tools</category><category>obsidian</category><category>dart</category><pubDate>Thu, 14 May 2026 00:00:00 GMT</pubDate><description><![CDATA[ Horizon is a personal AI assistant that runs as a daemon, receives Telegram messages and cron ticks, and stores everything as plain markdown files in an Obsidian vault. Here's how it works and why the design holds up. ]]></description><content:encoded><![CDATA[ <h2 id="what-it-is">What it is</h2>
<p>Horizon is a personal AI assistant I built that stores all its state in an Obsidian vault — a directory of plain markdown files. There is no database, no hidden state, no proprietary format. The vault is the database. Open Obsidian, and you see everything.</p>
<p>It runs as a daemon on my Linux box. One Dart process, one event loop, one LLM call per event. It accepts input via Telegram messages, fires on cron schedules, and watches vault files for changes. Each event goes into a pipeline: load relevant context, call the model, execute any resulting tool calls, write results back to vault.</p>
<h2 id="architecture">Architecture</h2>
<p>The core loop is straightforward. An event arrives — a Telegram message, a heartbeat tick, a vault file appearing — and the harness:</p>
<ol>
<li>Loads the standing system prompt from <code>_horizon/system/standing.md</code></li>
<li>Reads the capability manifest and selects capabilities relevant to the event</li>
<li>Makes one LLM call with the assembled context</li>
<li>Executes validated bash commands from a YAML allowlist</li>
<li>Writes results back to the vault</li>
</ol>
<p>Tools are bash command templates defined in <code>_horizon/system/allowlist.yaml</code>. Appending a new entry makes it available on the next event — no restart, no code changes. The validation layer handles path traversal and shell escaping, so the allowlist file itself is safe to edit from a mobile Obsidian client.</p>
<p>The LLM endpoint is configurable. Default is Kimi K2 via CrofAI, though anything with an OpenAI-compatible chat completion API works. Every call goes through the same standing prompt, which keeps prefix caching effective — roughly 70–85% cache hit rate at providers that support it.</p>
<h2 id="capabilities">Capabilities</h2>
<p>Behavior is defined in markdown files that live inside the vault itself, in <code>_horizon/capabilities/</code>. Each capability is a <code>.md</code> file with YAML frontmatter:</p>
<pre><code class="language-yaml">---
id: todo-manager
description: Load when the user mentions tasks, todos, or wants to track something to do later
schedule: 1d
---
</code></pre>
<p>The <code>description</code> field is what the orchestrator uses to decide which capabilities to load for a given event. The <code>schedule</code> field, when present, makes the capability eligible for heartbeat ticks at that interval.</p>
<p>What ships out of the box:</p>
<ul>
<li><strong>Journaler</strong> — appends every interaction to <code>journal/YYYY-MM-DD.md</code></li>
<li><strong>Knowledge base</strong> — maintains structured entries at <code>knowledge/&lt;slug&gt;.md</code> for facts worth keeping</li>
<li><strong>Todo manager</strong> — creates and updates tasks at <code>todos/&lt;slug&gt;.md</code> with YAML frontmatter tracking status, due dates, and context</li>
<li><strong>Relationships</strong> — tracks people at <code>people/&lt;slug&gt;.md</code>, bidirectional connections via [[wikilinks]]</li>
<li><strong>Pot tasks</strong> — delegates long-running autonomous work (coding, research, design) to separate Claude agents via the <a href="https://github.com/purplenoodlesoop/potentiality">Potentiality</a> orchestrator; results come back into the vault</li>
<li><strong>Skill reflector</strong> — audits recent activity and proposes new capability files when patterns emerge; promotion is manual</li>
<li><strong>Metacognitive monitor</strong> — scans turn records for repeated misses and inefficiencies, surfaces them for review</li>
<li><strong>Lint capabilities</strong> — LLM-driven audit of the capability descriptions themselves for semantic overlap</li>
</ul>
<p>Editing any of these in Obsidian takes effect on the next event. No restart.</p>
<h2 id="the-vault-as-database">The vault as database</h2>
<p>Every piece of state is a markdown file:</p>
<pre><code>journal/2026-05-14.md
knowledge/nix-flakes.md
todos/write-horizon-article.md
people/alice.md
_horizon/capabilities/todo-manager.md
_horizon/turns/01HXK2...json
</code></pre>
<p>This has a few practical consequences:</p>
<ul>
<li><strong>Full audit trail.</strong> Every turn is recorded in <code>_horizon/turns/</code> as a structured JSON file. The whole conversation history is on disk.</li>
<li><strong>Obsidian-native.</strong> [[Wikilinks]] work. Dataview queries work. You can build a custom dashboard over your todos, a graph view of your relationships, a calendar over your journal entries.</li>
<li><strong>No lock-in.</strong> The files are plain text. If I want to stop using Horizon, the vault stays — notes, todos, journal, all of it.</li>
<li><strong>Mobile editing.</strong> Capabilities, the system prompt, and the tool allowlist are all vault files. Obsidian on iOS edits them. Changes propagate on the next event.</li>
</ul>
<h2 id="signal-driven-heartbeat">Signal-driven heartbeat</h2>
<p>The heartbeat fires every five minutes by default, but it doesn't always do anything. Before making an LLM call, the harness checks whether any capability with a <code>schedule:</code> field is due. If none are, the tick costs zero tokens.</p>
<p>When a capability is due, the orchestrator sees only that capability and an instruction to reply with <code>HEARTBEAT_OK</code> if nothing actionable is found. Skill-reflector runs weekly. Lint runs daily. The metacognitive monitor runs every few hours. Most heartbeats are silent.</p>
<h2 id="what-it-wont-do">What it won't do</h2>
<p>By design:</p>
<ul>
<li>It won't serve multiple users — one vault, one owner</li>
<li>It won't host MCP or be an MCP client — tools are bash commands in a YAML file, nothing more</li>
<li>It won't self-modify — skill-reflector proposes, I promote</li>
<li>It won't hold sub-second real-time state — it's async by nature</li>
</ul>
<p>These aren't compromises. They're the constraints that make everything else tractable.</p>
<h2 id="running-it">Running it</h2>
<pre><code class="language-sh">nix run github:purplenoodlesoop/horizon
</code></pre>
<p>Requires a <code>.env</code> with <code>TELEGRAM_TOKEN</code>, <code>TELEGRAM_USERNAME</code>, and <code>LLM_TOKEN</code>. Vault defaults to <code>./vault</code> in the working directory. The bootstrap creates the <code>_horizon/</code> subtree on first run.</p>
<p>Source at <a href="https://github.com/purplenoodlesoop/horizon">github.com/purplenoodlesoop/horizon</a>.</p> ]]></content:encoded></item><item><author>hi@yakov.codes (Yakov Karpov)</author><title><![CDATA[ Dart is kinda amazing ]]></title><link>https://yakov.codes/articles/dart-is-kinda-amazing</link><guid>https://yakov.codes/articles/dart-is-kinda-amazing</guid><category>dart</category><category>dev</category><category>open-source</category><pubDate>Wed, 22 Jan 2025 00:00:00 GMT</pubDate><description><![CDATA[ Dart is simple, yet elegant, expressive and complete. Talking about Dart, language features, and mostly the good. ]]></description><content:encoded><![CDATA[ <h2 id="what">What?</h2>
<p>Dart is a multi-purpose, multi-paradigm, cross-platform, and JIT/AOT-compilation runnable language. You mostly hear about it in the context of Flutter, but I want to talk about the language itself, in isolation.</p>
<h2 id="why">Why?</h2>
<p>I'll list the parts I like about the language, not in any particular order. There are of course things that I don't like, but they are not for this post. In general, the ugly parts are mostly non-fundamental, even if they are annoying (semicolons, looking at you.)</p>
<h3 id="familiarity">Familiarity</h3>
<p>The post will contain <em>a lot</em> of Dart examples, but they should be pretty universally understandable. Given the initial &quot;replace JS&quot; purpose of the language and the later focus on the mobile, I believe the team intentionally made it so innately simple to understand.</p>
<pre><code class="language-dart">class Dog {
  void bark() {
    print('Woof!');
  }
}

int fibonacci(int n) {
  if (n == 0 || n == 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

final result = fibonacci(20);
</code></pre>
<blockquote>
<p>Almost <a href="https://interslavic-language.org/">Interslavic</a>.</p>
</blockquote>
<h3 id="type-system">Type system</h3>
<p>Dart has a really <em>tasty</em> type system, in a way that it is possible to express any reasonable constraint, model most scenarios with data that can be modeled with data, and not find yourself choosing from multiple &quot;right&quot; ways of doing the same thing or fighting with a synthetic roadblock.</p>
<pre><code class="language-dart">extension type Meters(int value) implements int {}

interface class Symmetric {
  Meters get val;
}

sealed class Form {}

final class Circle extends Form implements Symmetric {
  @override
  final Meters val;

  Circle(this.val);
}
</code></pre>
<p>On top of all that, it is safer than something like TypeScript and gives reasonable type errors.</p>
<h4 id="sound">Sound</h4>
<p>Dart 2.0 introduced soundness that obliterated the last of the previous dynamic Dart legacy – Dart made a significant journey.</p>
<p>Starting as a dynamic language, to making types opt-out, to being sound and null-safe – currently, we have a very &quot;checked&quot; language that is protecting the developer as much as possible without being over the top.</p>
<pre><code class="language-dart">String? doubleName() =&gt; 'Yakov' * 2;

final name = doubleName();
final String nope = doubleName(); 
</code></pre>
<p>Because of that, both writing new code and creating abstractions AND refactoring is a process of little friction – the compiler holds your hand and screams at you in the right proportions, at the right time.</p>
<h4 id="complete-oop">Complete OOP</h4>
<p>OOP is a base paradigm for Dart – even though multiparadigm and very functional approaches are allowed, on the fundamental level everything is object-oriented, and I don't think that is a bad thing, with this particular flavor.</p>
<pre><code class="language-dart">interface class HasAge {
  int get age;
}

interface class HasName {
  String get name;
}

base class Creature implements HasAge, HasName {
  @override
  final String name;
  @override
  final int age;

  Creature(this.name, this.age);
}

interface class CanSpeak {
  void speak();
}
</code></pre>
<p>The main points about Dart's OOP are:</p>
<ol>
<li>Primitives are classes</li>
<li>The Diamond problem is solved through <a href="https://dart.dev/language/mixins">mixins</a></li>
<li>Classes are open by default</li>
<li>There are no data classes, classes are co-data by default</li>
<li>Classes are highly tunable by a list of modifications</li>
<li>Objects have an implicit interface, taking every interaction to some level of abstraction by default</li>
</ol>
<p><a href="https://dart.dev/language/classes">Documentation has a great entry on the specifics</a>, but in essence, OOP is of &quot;true&quot; flavor for the current meaning of the paradigm and without any significant rough edges.</p>
<pre><code class="language-dart">mixin SelfDescribingAbility on HasAge, HasName implements CanSpeak {
  @override
  void speak() {
    print(&quot;My name is ${name} and I'm ${age}&quot;);
  }
}

class Human = Creature with SelfDescribingAbility;
</code></pre>
<p>Flexible, yet safe and complete – once again, boring and tasty.</p>
<h4 id="anonymous-records">Anonymous Records</h4>
<p>The latest significant feature, marking a new era in data abstractions and even some first whiff of (almost)polymorphism.</p>
<p>Destructuring, ad-hoc data polymorphism, named and unnamed fields – all the standard features. However, Records open a few interesting doors.</p>
<p><a href="https://github.com/purplenoodlesoop/sym">Creating anonymous objects with full inference</a></p>
<pre><code class="language-dart">final counter = Module(($) {
  final increment = $.trigger&lt;()&gt;();
  
  return (
    increment: increment,
    value: $.store(($) {
      $.on(increment, (_) =&gt; $.self + 1); 
      return 0;
    })
  );
});
</code></pre>
<p><a href="https://github.com/purplenoodlesoop/cord">Performing operations on multiple inner values</a></p>
<pre><code class="language-dart">final $Order = (
  ('city', City.new.$).with defaults(City('Haifa')),
  ('comment', Comment.new.$).optional(),
  ('buildings', $List(Building.new.$)),
).jsonSchema();
</code></pre>
<p>Or having some level of (pseudo)polymorphism</p>
<pre><code class="language-dart">String doubleString(String val) =&gt; val * 2;

(
  doubleString,
  print
)
  .compose('Hello, world')
</code></pre>
<h4 id="zero-cost-abstractions">Zero-cost abstractions</h4>
<p>New, relatively niche feature, but another nod to the completeness and relative power of the type system.</p>
<p>Extension types allow for both increase of the correctness and well, extending types, by composition.</p>
<p><a href="https://dart.dev/language/extension-types">Once again, the documentation does the job best of explaining</a>.</p>
<p>Examples can be rather simple.</p>
<pre><code class="language-dart">extension type Meters(int value) {}
</code></pre>
<p>Or rather complex</p>
<pre><code class="language-dart">extension type Behavior&lt;B, D&gt;((B, D) _self) {
  B get behavior =&gt; _self.$1;
  
  D get data =&gt; _self.$2;
}

extension type Identity&lt;T&gt;(T _self) {
  T get id =&gt; _self;
}
  
typedef Described&lt;T&gt; = Behavior&lt;String, Identity&lt;T&gt;&gt;;

extension type Describable&lt;T&gt;(Described&lt;T&gt; self) implements Described&lt;T&gt; {
  String describe() =&gt; '${behavior} ${data.id}';
}

typedef Logged&lt;T&gt; = Describable&lt;Behavior&lt;void Function(String), T&gt;&gt;;

extension type Loggable&lt;T&gt;(Logged&lt;T&gt; self) implements Logged&lt;T&gt; {
  void log() {
    data.id.behavior(describe());
  }
}
</code></pre>
<h4 id="not-only-oop">Not only OOP</h4>
<p>After everything written above, it is obvious that Dart not only has a complete OOP system but also offers a list of multi-paradigm features that even further remove restraints and allow for the choice of approach of a fitting paradigm for a job.</p>
<p>In my experience, having a strong OOP-based abstraction under the hood on the library level with a mostly functional interface is one of the most organic approaches, and Flutter demonstrates it the best.</p>
<pre><code class="language-dart">/// I'm a function in disguise!
class CounterWidget extends StatelessWidget {
  const CounterWidget({
    super.key,
  });

  @override
  Widget build(BuildContext context) {
    final (:increment, :value) = context.use(counter);

    return Text(
	  context.use(value).toString(),
	  style: Theme.of(context).textTheme.headlineMedium,
	);
  }
}
</code></pre>
<p>The underlying <code>Element</code> system that powers descriptions like in the above is a classic, inheritance-based OOP.</p>
<h3 id="tooling">Tooling</h3>
<p>Another selling point by offering minimal friction – every single piece of needed tooling is available out of the box, working, fast and simple.</p>
<p>Take a look at <a href="https://dart.dev/tools/dart-compile">compilation</a>, <a href="https://docs.flutter.dev/reference/flutter-cli#flutter-commands">Flutter CLI</a> and <a href="https://dart.dev/tools/dart-doc">documentation tooling</a>.</p>
<p>This solid toolkit allows for some magic like JIT/AOT compilation, hot reload, small output binaries, and other features that just feel and work correctly.</p>
<h3 id="pubdev">pub.dev</h3>
<p>An <code>npm</code> for Dart – accounting for <code>isEven</code>-style packages, the current choice of packages is not less in terms of functionality. Most of your common problems are solved and nicely packages, either as an official <a href="https://pub.dev/publishers/tools.dart.dev/packages">dart tools package</a> or any <a href="https://pub.dev/packages?sort=downloads">of 51k public packages</a>.</p>
<p><a href="https://pub.dev/packages/is_odd">There are some silly ones, of course</a>.</p>
<h3 id="standard-library">Standard Library</h3>
<p>Even not touching on the pub.dev, those are some of the in-built (= canonical, efficient, and idiomatic) libraries.</p>
<ul>
<li><a href="https://api.flutter.dev/flutter/dart-convert/dart-convert-library.html">dart:convert</a></li>
<li><a href="https://api.flutter.dev/flutter/dart-typed_data/dart-typed_data-library.html">dart:typed_data</a></li>
<li><a href="https://api.flutter.dev/flutter/dart-ffi/dart-ffi-library.html">dart:ffi</a></li>
<li><a href="https://api.flutter.dev/flutter/dart-io/dart-io-library.html">dart:io</a></li>
</ul>
<h3 id="async-and-concurrency">Async and Concurrency</h3>
<p>Once again, no particular innovations, but a very complete and pragmatic approach with just the right level of abstraction. TL;DR is <a href="https://api.flutter.dev/flutter/dart-io/dart-io-library.html">BEAM processs/Actor model</a> + <a href="https://en.wikipedia.org/wiki/Futures_and_promises">Futures</a> and <a href="https://en.wikipedia.org/wiki/Async/await">async/await</a>.</p>
<p>Also, <a href="https://dart.dev/language/functions#generators">generators</a>.</p>
<p>In practice, working with Dart's models in this domain feels natural, pragmatic, and straightforward. There is enough syntactical sugar to freely express the desired scenario, and concurrency is a blast.</p>
<p>Some time ago, <a href="https://github.com/dart-lang/sdk/issues/36097">Isolates got much lighter and faster</a>, and currently, it is possible to have the full benefit of running a piece of code in a separate process simply by spawning a green thread</p>
<pre><code class="language-dart">int fib(int n) =&gt; n &lt;= 1 ? 1 : fib(n - 1) + fib(n - 2); 

await Isolate.run(() =&gt; fib(40));
</code></pre>
<h3 id="spicy-features">Spicy features</h3>
<p>Dart has some interesting features that are unique or almost unique to the language. They are rarely used by the end developer but can be leveraged by library code to achieve elegant solutions to complex problems.</p>
<p>Some of those features are:</p>
<ul>
<li><a href="https://dart.dev/libraries/async/zones">Zones</a></li>
<li><a href="https://api.dart.dev/dart-core/Expando-class.html">Expando</a></li>
<li><a href="https://api.flutter.dev/flutter/dart-core/Finalizer-class.html">Finalizer</a></li>
<li><a href="https://dart.dev/language/extension-types">Extension types</a></li>
<li><a href="https://dart.dev/language/functions#generators">Immutable generators</a></li>
</ul>
<h2 id="what-next">What next?</h2>
<p>Please explore Dart more! It can be in the form of Flutter, contributing to the Dart OSS community, or simply by looking at the documentation. I see the language as a really interesting example of how limited innovation and an extreme commitment to each layer produced such a sturdy worktool.</p>
<p><a href="https://github.com/purplenoodlesoop">I have a list of open-source Dart projects on my profile</a>, which can be one of the starting points of interaction with the Dart ecosystem :)</p> ]]></content:encoded></item><item><author>hi@yakov.codes (Yakov Karpov)</author><title><![CDATA[ gRPC, Haskell, Nix, love, hate ]]></title><link>https://yakov.codes/articles/grpc,-haskell,-nix,-love,-hate</link><guid>https://yakov.codes/articles/grpc,-haskell,-nix,-love,-hate</guid><category>haskell</category><category>nix</category><category>gRPC</category><category>open-source</category><category>release</category><pubDate>Tue, 26 Nov 2024 00:00:00 GMT</pubDate><description><![CDATA[ Discuss the state of the gRPC ecosystem in Haskell in the context of Nix, presenting some open-source fixes, and making sense of the current (reproducibly) half-functioning solutions. ]]></description><content:encoded><![CDATA[ <h2 id="open-source-tldr">Open source TL;DR</h2>
<ul>
<li><a href="https://github.com/purplenoodlesoop/http2-grpc-haskell">https://github.com/purplenoodlesoop/http2-grpc-haskell</a></li>
<li><a href="https://github.com/purplenoodlesoop/gRPC-haskell">https://github.com/purplenoodlesoop/gRPC-haskell</a></li>
</ul>
<h2 id="structure-of-a-grpc-toolkit">Structure of a gRPC toolkit</h2>
<p>A toolkit/suite for a <code>proto</code>/<code>gRPC</code> for a specific language is a concept that this article is talking about. A toolkit consists of two mandatory public-facing parts that enable its functioning – (1) a language-specific implementation of the API with core types and functions, and (2) a code-generator from <code>.proto</code> files.</p>
<pre><code class="language-protobuf">service VoiceService {
  rpc Call(stream InputPacket) returns (stream OutputPacket);
}
</code></pre>
<p>Each implementation would differ, as every part can be represented very differently – arguably, some better than others.</p>
<h2 id="ecosystem-overview">Ecosystem Overview</h2>
<p>On Hackage, complete implementations are http2-grpc-haskell and gRPC-haskell. Both packages are heavily undermaintained, but mostly not outdated and fully usable.</p>
<p>This results in some of the toolkits' packages being broken both in vanilla Haskell and Nix ecosystems or requiring obscure, outdated dependencies themselves. Given that the implementations are very much useful, it makes sense to provide the fixes.</p>
<h3 id="on-mu">On mu</h3>
<p><a href="https://github.com/higherkindness/mu-haskell">mu-haskell</a> was an interesting project! Searching for gRPC lands on it as one of the solutions, but the project was archived on Oct 19, 2024. The project received its last commit almost two years ago, and the proto/grpc part is especially outdated. Their <a href="https://github.com/higherkindness/mu-haskell/tree/master/examples">Examples</a>, and <a href="https://github.com/higherkindness/mu-haskell/tree/master/core">source code</a> are of a very much value, though.</p>
<p>mu-haskell uses <a href="https://github.com/yesodweb/wai">WAI</a>, so does <code>http2-grpc-haskell</code></p>
<h2 id="http2-grpc-haskell">http2-grpc-haskell</h2>
<p>A full, full-Haskell implementation of the protocol. Does not rely on external C, and implements everything on top of native abstractions. Flexible, correct, and idiomatic.</p>
<h3 id="implementation">Implementation</h3>
<p>A higher-level implementation of <a href="https://hackage.haskell.org/package/warp-grpc">warp-grpc</a> is the most likely part of the library for an interaction. The toolkit uses <code>wai</code> and <a href="https://hackage.haskell.org/package/warp">warp</a> to implement the <code>rpc</code> part, and the <code>.proto</code>s are generated using an idiomatic <code>gRPC</code> compilation plugin that generates <a href="https://github.com/google/proto-lens/tree/master/proto-lens-tutorial">proto-lens</a> message and service definitions.</p>
<pre><code class="language-nix">''
protoc \
 --plugin=protoc-gen-haskell-protolens=${getExe pkgs.haskellPackages.proto-lens-protoc} \
 --haskell-protolens_out=packages/backend/gen \
 packages/proto/core.proto
''
</code></pre>
<p>The API is somewhat of a trap, however. It poses a similar flaw to <code>grpc-web</code> in missing an important part of functionality – asynchronous, bidirectional stream communications are impossible, as each event can be met with a 0-1 response in the borders of the same message.</p>
<pre><code class="language-haskell">handleIndex :: UnaryHandler GRPCBin &quot;index&quot;
handleIndex _ input = do
 print (&quot;index&quot;::[Char], input)
 return $ defMessage &amp; description .~ &quot;desc&quot;
                        &amp; endpoints   .~ [ defMessage &amp; path .~ &quot;/path1&quot;
                                                      &amp; description .~ &quot;ill-supported&quot;
 ]
</code></pre>
<h3 id="nixpkgs"><code>nixpkgs</code></h3>
<p>The toolkit is mostly broken in the Nix ecosystem, but it is easily fixed in the form of an overlay. After bumping dependencies everywhere to:</p>
<pre><code class="language-yaml">- bytestring &gt;= 0.10.8 &amp;&amp; &lt; 0.13
- http2 &gt;= 3.0 &amp;&amp; &lt; 5.3
- warp &gt;=3.3.15 &amp;&amp; &lt;3.5
- text &gt;= 1.2 &amp;&amp; &lt; 2.2
- tls &gt;= 1.4 &amp;&amp; &lt; 2.1
- zlib &gt;=0.6.2 &amp;&amp; &lt;0.8
</code></pre>
<p>Running <code>cabal2nix</code> on updated <code>.cabal</code> files generates valid <code>.nix</code> files that can be used in an overlay drop-in.</p>
<pre><code class="language-nix">{ lib }:
self: super:
let
  packages = [
    &quot;warp-grpc&quot;
    &quot;http2-grpc-types&quot;
    &quot;http2-grpc-proto3-wire&quot;
    &quot;http2-grpc-proto-lens&quot;
    &quot;http2-client-grpc&quot;
 ];
  genName = &quot;package.gen.nix&quot;;
  foldl = f: lib.foldl' (packages: name: packages // { ${name} = f name; }) { } packages;
in
{
  haskellPackages = super.haskellPackages.override {
    overrides = hSelf: hSuper: with data; foldl (name: hSelf.callPackage ./${name}/${genName} { });
 };
}
</code></pre>
<p><a href="https://github.com/purplenoodlesoop/http2-grpc-haskell">The fix can be found here</a> and applied as a default overlay. The fork is based on <a href="https://github.com/alexfmpe/http2-grpc-haskell">another fork</a> with dependency bumps.</p>
<h2 id="grpc-haskell">gRPC-haskell</h2>
<h3 id="implementation-2">Implementation</h3>
<p>Shared C usage. This implementation is really a wrapper around a mostly canonical implementation, which at the same time makes it a much less flexible implementation that does not allow room for using idiomatic underlying implementations AND provides a full API.</p>
<p>This solution allows for all of the standard protocol functionality.</p>
<pre><code class="language-haskell">addHandler ::
  ServerRequest 'Normal TwoInts OneInt -&gt;
  IO (ServerResponse 'Normal OneInt)
addHandler (ServerNormalRequest _metadata (TwoInts x y)) = do
  let answer = OneInt (x + y)
 return
 ( ServerNormalResponse
 answer
 [(&quot;metadata_key_one&quot;, &quot;metadata_value&quot;)]
        StatusOk
        &quot;addition is easy!&quot;
 )
</code></pre>
<p>The API departs from idiomatic context integrations in one more way – <code>.hs</code> code is generated by a separate program and not as a plugin in the <code>protoc</code> compiler.</p>
<pre><code class="language-nix">{
env = with pkgs; [ haskellPackages.proto3-suite ];
text = ''
 compile-proto-file \
 --includeDir packages/proto \
 --proto core.proto \
 --out packages/backend/gen
'';
}
</code></pre>
<p>The resulting code is somewhat more verbose, but it is a matter of utility functions and types, which are absent in the core implementation.</p>
<h3 id="nixpkgs-2"><code>nixpkgs</code></h3>
<p>Again, broken, again, fixable with an overlay. The project does not use flakes, and using the latest <code>unstable</code> channel would result in compilation errors. The fix is two-fold.</p>
<p>Bumping <code>bytestring</code>:</p>
<pre><code class="language-yaml">- bytestring &gt;= 0.10 &amp;&amp; &lt;=0.12
</code></pre>
<p>And utilizing an older version of gRPC</p>
<pre><code class="language-nix">nixpkgs-grpc.url = &quot;github:NixOS/nixpkgs?rev=d59a6c12647f8a31dda38599c2fde734ade198a8&quot;; # gRPC 1.45.2
</code></pre>
<p>Running <code>cabal2nix</code> and building <code>nixpkgs</code> with an overlay that overrides Haskell packages and <code>grpc</code> the project is built.</p>
<h3 id="favoring-over-other-solutions">Favoring over other solutions</h3>
<p>The differences might seem subtle, but they are quite important and I see a choice as somewhat lacking.</p>
<h4 id="guarantees-of-implementation">Guarantees of implementation</h4>
<p><code>gRPC-haskell</code> expects a record with a service implementation, which allows the compiler to check its completeness of it, and <code>http2-grpc-haskell</code> would allow the compilation to run, even on incomplete, patchy service implementations.</p>
<pre><code class="language-haskell">voiceService state = 
 voiceServiceServer VoiceService {..} defaultServiceOptions
  where
    voiceServiceCall :: RPC 'BiDiStreaming InputPacket OutputPacket
 voiceServiceCall (ServerBiDiRequest _ req resp) = do
-- ...
</code></pre>
<h4 id="type-inference">Type inference</h4>
<p>Not using fully abstract, <code>lens</code>-powered records allows for a neater type-completion, with the compiler being able to interact with the records closer.</p>
<h4 id="bi-directional-streaming">Bi-directional streaming</h4>
<p>Already been mentioned, but this is one of the reasons that pushed me to fix a second library – the imperative, persistent interface for receiving and requesting events asynchronously in independent directions.</p> ]]></content:encoded></item><item><author>hi@yakov.codes (Yakov Karpov)</author><title><![CDATA[ Focus on Data ]]></title><link>https://yakov.codes/articles/focus-on-data</link><guid>https://yakov.codes/articles/focus-on-data</guid><category>dev</category><category>architecture</category><category>dart</category><category>haskell</category><category>typescript</category><pubDate>Wed, 12 Jun 2024 00:00:00 GMT</pubDate><description><![CDATA[ Data-oriented approaches in modern software engineering are widely used and have different implementations, meanings, and uses. Here are discussed some of them with their values and applications. ]]></description><content:encoded><![CDATA[ <h2 id="commonly-confused-paradigms">Commonly confused paradigms</h2>
<p>Two paradigms unfortunately share a similar name, and it is important to differentiate them. Everything in this article is related to the first one, as most of the things discussed can be categorized as a part of it. Not to paint the second one insignificant thought – it is just a very different concept that solves very different problems.</p>
<h3 id="data-driven-programming">Data-driven programming</h3>
<p>A quote from the Wiki:</p>
<blockquote>
<p>In computer programming, data-driven programming is <strong>a programming paradigm in which the program statements describe the data to be matched and the processing required rather than defining a sequence of steps to be taken</strong>.</p>
</blockquote>
<p>A broad definition, I know. It can be fit onto simple <code>map</code>-<code>reduce</code> list transformations that describe the logic of the application, as well as on complex asynchronous <code>Stream</code> transformations, and onto something akin to dynamic programming's attribute set manipulations, like in Nix.</p>
<p>The main point is, that the <mark>data itself describes</mark> both the <mark>control flow</mark> and the <mark>logic</mark>, where the whole application becomes a collection of high-level and abstract data transformation. The transformations themselves are generic and the program becomes a set of rules that command those transformations.</p>
<p>The beautiful <a href="https://bioinf.comav.upv.es/courses/linux/_downloads/829bd103b8372fa78e8c5837e34700c0/the_art_of_unix_programming.pdf">The Art of Unix Programming dives deeper</a> with examples on, surprisingly, Unix systems.</p>
<h3 id="data-oriented-design">Data-oriented design</h3>
<p>A completely different approach from data-driven programming, focusing on optimizing data structures by grouping related fields into a multi-array, composite object, making the object of interest implicit.</p>
<p>So, instead of having</p>
<pre><code class="language-dart">typedef User = (
  String name,
  int age,
);

typedef Users = List&lt;User&gt;;
</code></pre>
<p>The list of users is represented as</p>
<pre><code class="language-dart">typedef Users = (
  List&lt;String&gt; name,
  List&lt;int&gt; age,
);
</code></pre>
<h2 id="representing-effects-as-data">Representing effects as data</h2>
<p>One example of a data-focused approach can be representing impure logic as pure data that gets evaluated later. Both FP and OOP have some sort of it that differs slightly, maintaining the main idea.</p>
<h3 id="data-as-a-description-of-an-effect">Data as a description of an Effect</h3>
<p>In OOP and closer-to-OOP-than-others languages it is called <mark>Interpreter Pattern</mark>, somewhere in the middle it can be viewed as a simple <mark>effectful function over pure data types</mark>, and in FP it is a core of it, as the written code must be pure and every effect must be represented as some kind of data.</p>
<p>Below is an example of a pure function with async effects and logging in TypeScript.</p>
<pre><code class="language-typescript">const run = () async* =&gt; {
  yield {type: 'log', data: 'Starting'}
  yield {type: 'value', data: 10}
  yield {type: 'log', data: 'Waiting for 1 second'}
  yield {type: 'wait', data: 1}
  yield {type: 'log', data: 'Done!'}
}
</code></pre>
<p>The calling site would process those values and evaluate them, while the source function just returns plain values.</p>
<h3 id="sandwiches">Sandwiches!</h3>
<p>There is a whole architectural approach that focuses on this kind of design for non-strictly-functional languages, which might sound limiting, but all &quot;strict&quot; approaches have their benefits, as well as being used widely, either by different names or more explicitly/as a part of a bigger combination of guidelines.</p>
<p><a href="https://blog.ploeh.dk/2020/03/02/impureim-sandwich/">An impure-pure-impure sandwich.</a></p>
<h2 id="dataflow-and-reactive-programming">Dataflow and Reactive programming</h2>
<p>Yet another data-centric approach, and arguably the most straightforward one in terms of the closeness of the problem to the nature of the solution, is a set of reactive-like paradigms.</p>
<p>Both deal with transformations of data as it flows through the system, and Reactive programming can be considered a subset of Dataflow, where both systems are concerned with the propagation of data through a set of transformers with the former operating on the data itself, and the latter operating on data changes.</p>
<p>Reactive programming can be further classified into synchronous/asynchronous and push/pull. Even further, even closely classified system implementation can differ greatly, and something like <a href="https://effector.dev/">Effector</a> is very, very different from <a href="https://reactivex.io/">ReactiveX</a>, even though they share a lot in common.</p>
<p>Dataflow's definition is much more vague, but other than operating on the data itself it is more concerned with graphs, and <a href="https://www.tensorflow.org/">TensorFlow</a> builds upon this very notion. <a href="https://www.datasciencecentral.com/understanding-dataflow-graphs-in-tensorflow/">This article explains it much more in-depth</a>.</p>
<h3 id="on-frp">On FRP</h3>
<p>One very specific implementation of reactive programming is Functional Reactive Programming, initially proposed in the paper <a href="http://conal.net/papers/icfp97/">Functional Reactive Animation</a>. It is not widely used due to its implementation details, but it is very influential nonetheless, and the aforementioned ReactiveX can be thought of as something that resulted from FRP.</p>
<pre><code class="language-haskell">type Behavior a = Time → a
type Event a = [(Time, a)]
</code></pre>
<p>As usual, there are a lot of flavors. I can pinpoint <a href="https://hackage.haskell.org/package/reactive-banana">Reactive Banana</a> as a typical, yet elegant implementation that can be used as an example for understanding the paradigm. <a href="https://wiki.haskell.org/FRP_explanation_using_reactive-banana">Their unofficial tutorial is also nice.</a></p>
<h2 id="ui-as-a-function-of-state">UI as a function of State</h2>
<p>In current component-based UI systems, the configuration of the UI itself is represented as a function of a state, which can be categorized as just another function over data that accepts immutable inputs and outputs a configuration, a description of the resulting UI, and the behavior of application-level code.</p>
<p>This approach was popularized by React, but currently, it is used pretty much universally – Flutter uses it, SwiftUI uses it, Kotlin Compose uses it, and so on.</p>
<p>I really like <a href="https://docs.flutter.dev/ui">Flutters</a> definition of this approach, which makes the idea a bit more radical, representing <mark>everything as a Widget</mark>. This means that data, the Widgets, describes every aspect of application-level behavior, from lifecycle events, such as showing a snack bar on opening a specific page, to the configuration of a resulting UI, such as the color of a button.</p>
<h3 id="on-hooks">On hooks</h3>
<blockquote>
<p>Funnily enough, React gave birth to this <mark>UI = f(State)</mark> paradigm, and React abandoned it by introducing hooks into the equation.</p>
</blockquote>
<p><a href="https://labs.factorialhr.com/posts/hooks-considered-harmful">Lots of disagreement with the concept of Hooks exist</a>, but one thing is a fact – hooks break the paradigm by making Components/Widgets impure. By passing the same props to the Component that uses hooks we won't necessarily obtain the same output, as they introduce state bound to the circumstances of the caller.</p>
<p>Flutter solves this problem by explicitly passing to each and every widget <mark>BuildContext</mark> – a wrapper object that contains the current state of a context that this Widget exists in. Given that the context is used with <a href="https://api.flutter.dev/flutter/widgets/InheritedWidget-class.html">InheritedWidgets</a>, it is possible to achieve stateful behavior without making a widget impure.</p>
<h2 id="composition-vs-inheritance">Composition vs Inheritance</h2>
<p>Prioritizing data in terms of creating collections of grouped functionality also has its applications. Composition over Inheritance is a well-used approach that is considered a &quot;best practice&quot; in a lot of cases, and pretty much as data-centric as it gets in a very data-opposed (and behavior-oriented) setting of combining functionality and achieving polymorphism.</p>
<p>Returning to Flutter as an example, it favors very strongly <a href="https://docs.flutter.dev/resources/architectural-overview#widgets">composition over inheritance</a>, with a vast majority of widgets achieving only a single level of inheritance from their abstract parent class. The widgets are described as a composed, immutable data structure as well, with a polymorphic interface of the <code>Widget</code> class.</p>
<p>This demonstrates not only the extent to which the compositional approach can be pushed but also the fact that it is indeed handy to combine approaches, as stated in the <a href="https://en.wikipedia.org/wiki/Design_Patterns">Design Patterns</a>.</p>
<h3 id="multiple-inheritance">Multiple inheritance</h3>
<p>A very short tangent regarding Multiple Inheritance – CoI is an example of the simplest solution for a problem that is traditionally solved by using different &quot;flavors&quot; of inheritance instead.</p>
<p>Take a look at Dart's <a href="https://dart.dev/language/mixins">Mixins</a> and Swift's <a href="https://www.vadimbulavin.com/multiple-inheritance-swift/">Protocols</a> and see how composition can be used to solve the very same problem, without stepping away from data as a mean for implementation of functionality.</p> ]]></content:encoded></item><item><author>hi@yakov.codes (Yakov Karpov)</author><title><![CDATA[ Explaining flavors of Unidirectional Data Flows ]]></title><link>https://yakov.codes/articles/explaining-flavors-of-unidirectional-data-flows</link><guid>https://yakov.codes/articles/explaining-flavors-of-unidirectional-data-flows</guid><category>dev</category><category>architecture</category><pubDate>Wed, 08 May 2024 00:00:00 GMT</pubDate><description><![CDATA[ Unidirectional state management/business logic implementation is a classic approach that keeps on living. Stemming from state machines, combined with modern languages and modern concepts, the result is interesting and many-faced. ]]></description><content:encoded><![CDATA[ <h2 id="what-are-they">What are they?</h2>
<p>Business logic can be stateful in settings where an application is long-running and changes based on external conditions. This could apply to applications where those conditions are a user or some stream of data. Prices of currencies, for example.</p>
<p>However, the example with a user is a standard application of some kind of abstraction that helps structure, control, and implement the logic behind it. Unidirectionals act as one of the many groups of those abstractions, following principles of immutability, data-first approach, and declarativity.</p>
<p>Data flows in one way, which usually can be summarised as <mark>Events/Intents in -&gt; States out</mark>, and the cycle repeats – based on the states new events can be added, and so on. This can be contrasted with bidirectional approaches, like two-way bindings, for example.</p>
<p>A core part of all can be summarised as a reducer function</p>
<pre><code>(State, Event) -&gt; State
</code></pre>
<h2 id="why">Why?</h2>
<p>Dealing with a state is hard, and dealing with a reactive state is even harder. Unidirectional approaches abstract out most of the low-level stuff – actual mutations, and allow for declaratively describing the relationship between intents and resulting states.</p>
<p>This approach can be generalized on (side) effects as well. This is tricky, however, as reducers, the functions that actually transform the current state and an incoming event to a new state, are pure, which obviously contradicts the whole point of side effects.</p>
<p>Let's not get ahead, as the main value is still present – declarative, seemingly immutable, data-first, simple functions that can fully encapsulate all the possible state changes. But that's not all – given that both states and events are represented as immutable data, algebraic data types, can be transformed and manipulated as any other data. Either by simple functions over data or as complex descriptions of asynchronous data streams – for filtering, manipulating concurrency, and much more.</p>
<p>For games, this is basically a must, as those complex state machines are most naturally represented as unidirectional, reducer-based abstractions. And for UI applications, such as websites and mobile applications, user behavior and some other parts are pretty naturally represented as well.</p>
<h2 id="what-about-the-boilerplate">What about the boilerplate?</h2>
<p>Yep, lots of it. Unidirectional approaches absolutely bring relief to some of the hardest to deal with, and most time-consuming problems of user applications, but they also come with a tax of the boilerplate.</p>
<p>Redux is infamous for it, Elm is infamous for it, and a lot of libraries of the Android world are infamous for it. Boilerplate raises the complexity pointlessly, negating most of the benefits, and that's not even mentioning the raising of cognitive load, code duplication, the un-ergonomic and hard-to-debug</p>
<p>This is one of the reasons for an abundance of all the flavors, which is valid, but is not less of a pain to deal with. Naturally, developers try to maximize the benefits while minimizing the &quot;price&quot; of usage, and there have been quite a lot of improvements and development in the past n years.</p>
<h2 id="flavors">Flavors</h2>
<p>Here are briefly listed most of the existing flavors, with some thoughts and code examples alongside. Examples are in Dart, but being a general-looking C-like language, this does not really matter; don't let that discourage you if you are not familiar with it.</p>
<p>I will use Events for describing Events/Actions/Intents for consistency, as they are (almost) the same thing in most of the flavors.</p>
<h3 id="direct-reducer">Direct reducer</h3>
<p>The simplest, and pioneering approach jumpstarted into the JS world and other worlds by <a href="https://redux.js.org/">Redux</a>, fully resembling the pseudocode example above. Current state in, Event/Action in, new State out.</p>
<pre><code class="language-dart">typedef Reducer1&lt;State, Event&gt; = State Function(State state, Event event);
</code></pre>
<p>Original Redux uses a single store which is not really relevant, as unidirectional approaches can utilize both multi-store and single-store approaches.</p>
<p>Side effects can be represented in many ways when using direct reducers, and many are listed below in their concept, just with different, arguably more ergonomic implementations. Currently, Redux evolved into a somewhat hybrid approach through <a href="https://redux-toolkit.js.org/">RTK</a>, but it is out of the scope of this article.</p>
<p>Redux and other such simple approaches make it hard to model side effects, even though there are a <a href="https://redux.js.org/usage/side-effects-approaches">lot of ways that try to implement the less painful way. Not really effective.</a></p>
<h3 id="effects-as-data">Effects as data</h3>
<p>The next iteration starts the route of encoding side effects directly in the reducer. The <a href="https://elm-lang.org/">second generation Elm</a> pioneered this approach, which eventually seeped into <a href="https://blog.javascripting.com/2018/09/26/why-elmish-side-effect-model-is-inferior/">Redux as well</a>, and can be implemented in pretty much any language that allows passing functions as data in one way or another.</p>
<p>Just as JS has a native type for async data, Elm does this nicely, as it has a native type for side effects. Not necessarily asynchronous, but side-effectful. Simplifying this approach and encoding it in Dart, it can be formulated as something like this</p>
<pre><code class="language-dart">typedef Output2&lt;State, Event&gt; = (State state, Future&lt;Event&gt; Function() effect);

typedef Reducer2&lt;State, Event&gt; = Output2&lt;State, Event&gt; Function(
  State state,
  Event event,
);
</code></pre>
<p>Reducer returns not only a new state but also a <a href="https://en.wikipedia.org/wiki/Thunk">thunk</a> of a side effect that returns a new, that is then evaluated and the resulting event is added back.</p>
<p>Works, works predictably, looks simple, and the concept itself is relatively easy to understand. Lots of boilerplate though, even in the Elm itself that supports this approach on the language level. Trying to port it to other languages yields even more verbose code which in turn leads to bloated, hard-to-understand, and repetitive codebases.</p>
<h3 id="effects-as-abstract-data">Effects as abstract data</h3>
<p>Even a more scary approach in terms of the amount of code, but beautiful in terms of abstraction power – reducers become very dumb and return very simple data structures that are then evaluated by interpreters.</p>
<p>This approach goes the full data-first route, modeling everything as data. Look at the definition of a reducer:</p>
<pre><code class="language-dart">typedef Effect3&lt;State, Effect, Event&gt; = Future&lt;Event&gt; Function(
  State state,
  Effect effect,
);

typedef Output3&lt;State, Effect&gt; = (State state, List&lt;Effect&gt; effects);

typedef Reducer3&lt;State, Event, Effect&gt; = Output3&lt;State, Effect&gt; Function(
  State state,
  Event event,
);
</code></pre>
<p>Both sync and async effects are represented as synchronous, abstract data structures! Colored functions said hello, <a href="https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/">IYKYK</a>.</p>
<p>It makes sense to give an example, but it will take an unimaginable amount of symbols to actually show even the simplest implementation in Dart, so pseudocode it is. Let's model some logging for a counter.</p>
<p>First, interfaces/data declaration</p>
<pre><code>data State = Int

data LogLevel = Info or Warning

data Effect = (String message, LogLevel level)

data Event = Increment or Reset or None
</code></pre>
<p>After everything is encoded into data structures, they can be &quot;evaluated&quot; or &quot;interpreted&quot; by two functions, one pure – for state changes</p>
<pre><code>State reducer(State state, Event event) {
  return when event
    Increment =&gt; (state + 1, [(&quot;Incremented&quot;, Info)])
    Reset =&gt; (0, [(&quot;Reset state&quot;, Warning)])
    Nothing =&gt; (state, [])
}
</code></pre>
<p>And one impure – for effect evaluation.</p>
<pre><code>Event effect(State state, Effect effect) {
  print(effect)
  
  return None
}
</code></pre>
<p>This is a very powerful abstraction that fully encapsulates all the implementation details, but even in this hypothetical pseudocode language, it is verbose and scales as good as Redux. Not good.</p>
<h3 id="effects-as-a-part-of-a-reducer">Effects as a part of a reducer</h3>
<p>A logical extension of those approaches is just mashing side effects and state changes together. Sounds a bit sad, as we are losing the determinative aspect of the reducer, but it is the most ergonomic approach of all the mentioned previously.</p>
<p>Two examples come to mind, which are BLoC as defined <a href="https://www.flutteris.com/blog/en/reactive-programming-streams-bloc-practical-use-cases">in the awesome article</a> of Didier Boelens, and <a href="https://purescript-halogen.github.io/purescript-halogen/guide/03-Performing-Effects.html">PureScript's Halogen</a>.</p>
<p>The main idea can be formulated as &quot;after the reducer receives a new event, it can perform any asynchronous, effectful actions while returning as many states in the process as required&quot;. This makes implementing complex effectfull flows, such as pagination, very easy and ergonomic, while maintaining the data-first approach, and consequentially, all the benefits from treating both intents and resulting states as values.</p>
<p>Returning to BLoC, the reducer type definition looks far simpler than other approaches</p>
<pre><code class="language-dart">typedef Reducer5&lt;State, Event&gt; = Stream&lt;State&gt; Function(
  State state,
  Event event,
);
</code></pre>
<p>Stream is just an asynchronous collection, which fits the definition of a reducer that can perform async actions and return multiple states in the process.</p>
<h3 id="effects-as-reactive-units">Effects as reactive units</h3>
<p>Lastly, an approach that was prevalent in the Android world some few years ago, effects can be described as reactions to state changes or events, decoupling them from the otherwise pure reducer that does not know absolutely anything about side effects being performed.</p>
<p>This approach is conceptually similar to <a href="https://redux.js.org/understanding/history-and-design/middleware">Redux's Middlewares</a>, which can be categorized as the same MVI.</p>
<p>This is a bit of a recursive definition since the effect evaluation is an effect in itself in this paradigm – a subscription to internal events of the &quot;Store&quot;. ClojureScript's <a href="http://day8.github.io/re-frame/">Re-frame</a> generalizes this even further, and <a href="https://redux-saga.js.org/">Redux's Saga</a> is similar in the sense that it returns an asynchronous sequence of events to be added back.</p>
<p>Going back to Dart, this definition looks similar to the previous one and the very first – it combines a pure reducer with an impure async collection, with the exception that the collection describes not new States, but new Events</p>
<pre><code class="language-dart">typedef Reducer6&lt;State, Event&gt; = Reducer1&lt;State, Event&gt;;

typedef Watcher6&lt;State, Event&gt; = Stream&lt;Event&gt; Function(
  State previousState,
  State nextState,
  Event event,
);
</code></pre>
<h2 id="closing-words">Closing words</h2>
<p>What is the conclusion here? Mostly, I would say that the diversity of unidirectional flow-centered approaches says a few main things about their current state:</p>
<ol>
<li>They are battles tested and proven to work and work well</li>
<li>Developers are dissatisfied with current approaches, particularly with <strong>D</strong>eveloper E<strong>x</strong>perience that they offer</li>
<li>Some languages are more fit for some flavors, particularly if they support parts on the language level. Command in Elm and async generators in Dart and JS are examples of that</li>
<li>Some pioneering approaches combine the flavors, which is a topic for another article</li>
<li>Immutability, declarativity, and data-first approaches are useful in many ways but come with a cost</li>
</ol> ]]></content:encoded></item></channel></rss>