<?xml version="1.0" encoding="UTF-8" ?>
<?xml-stylesheet type="text/xsl" href="/rss.xsl" media="all"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
<channel>
<title>Roastidio.us Tagged with elixir</title>
<link>https://roastidio.us/tag/2771</link>
<atom:link href="https://roastidio.us/tagged_with/elixir" rel="self" type="application/rss+xml"></atom:link>
<description>Roastidio.us Tagged with elixir</description>
<item>
<title>From legacy code to verifiable specifications with Surveyor</title>
<link>https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor</link>
<guid isPermaLink="false">OwKRRarDBvCxLjw8oGuHThV0mW_dK7NcEu5NrA==</guid>
<pubDate>Wed, 29 Apr 2026 20:10:39 +0000</pubDate>
<description>Take ownership of code you don&#39;t trust. Surveyor extracts a verifiable behavioral specification of a legacy system. Rewrite with certainty.</description>
<content:encoded>&lt;h2&gt;What is legacy code?&lt;a href=&quot;https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#what-is-legacy-code&quot;&gt;​&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Sometimes that&amp;#39;s the fifteen-year-old monolith.
Sometimes it&amp;#39;s the service that grew complicated faster than the team could keep up with.
Sometimes it&amp;#39;s that project that started as a shortcut and turned into a roadblock. Legacy code has been exposed to real world demands long enough to gather fixes for edge cases we are not aware of anymore.&lt;/p&gt;&lt;p&gt;And — increasingly — sometimes it&amp;#39;s the repo your CEO vibe-coded last weekend and that they proudly present on Monday.
It runs. Now you have to own it.&lt;/p&gt;&lt;h2&gt;Legacy software is the code we lost confidence in&lt;a href=&quot;https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#legacy-software-is-the-code-we-lost-confidence-in&quot;&gt;​&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;That last category is genuinely new and growing fast. Code written once, never reviewed, tested a bit, deployed one day. Or, worse, released as a library and never maintained after that.&lt;/p&gt;&lt;p&gt;A new kind of technical dept has evolved: &lt;strong&gt;&lt;em&gt;&amp;quot;The code we should review thoroughly one day&lt;/em&gt;&lt;/strong&gt;.&amp;quot;&lt;/p&gt;&lt;h2&gt;&amp;quot;Can we rewrite this?&amp;quot;&lt;a href=&quot;https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#can-we-rewrite-this&quot;&gt;​&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;The actual problem is not the writing of the new code, but the discovery of what the old code does, and the certainty that the new one does the same thing.&lt;/p&gt;&lt;p&gt;We&amp;#39;ve been building two small Elixir tools to help with both halves of that problem.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Surveyor&lt;/strong&gt; scans a codebase in any language and produces an architectural model.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Assay&lt;/strong&gt; runs the same plain-text behavioral specs against both the legacy and the rewrite, so you can prove they behave the same.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Together they cover the workflow: Figure out the structure, capture the behavior, rewrite, prove the rewrite works.&lt;/p&gt;&lt;div&gt;&lt;div&gt;TL;DR&lt;/div&gt;&lt;div&gt;&lt;p&gt;Surveyor produces an &lt;strong&gt;automatically verifiable behavioral specification&lt;/strong&gt; of a legacy system.&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;h2&gt;How we got here&lt;a href=&quot;https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#how-we-got-here&quot;&gt;​&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Before going into either tool, it&amp;#39;s worth walking through the shape of the problem.
The picture builds up step by step, and the final diagram only makes sense once you&amp;#39;ve seen the holes in the earlier ones.&lt;/p&gt;&lt;h3&gt;What we don&amp;#39;t want&lt;a href=&quot;https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#what-we-dont-want&quot;&gt;​&lt;/a&gt;&lt;/h3&gt;&lt;figure&gt;&lt;img src=&quot;https://bitcrowd.dev/assets/images/step-0-158d6135059c381671508607696828b4.png&quot; alt=&quot;Legacy Codebase with a dashed arrow pointing directly at a Target Codebase, with no intermediate steps&quot; title=&quot;&quot;/&gt;&lt;figcaption&gt;A direct rewrite also copies bugs, dead code and unused features.&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;Point an agent at a legacy codebase, ask it to produce a new one, hope for the best.
Call it &amp;quot;the Claude zombie rewrite.&amp;quot;
You end up with a target codebase that nobody understands either, plus a generation gap between the old assumptions and the new. What has the agent overlooked, misunderstood, assumed?
A black box gets replaced with a different black box.
That is not a rewrite, that is a transcoding.&lt;/p&gt;&lt;h3&gt;What we want instead&lt;a href=&quot;https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#what-we-want-instead&quot;&gt;​&lt;/a&gt;&lt;/h3&gt;&lt;figure&gt;&lt;img src=&quot;https://bitcrowd.dev/assets/images/step-1-b16e88b3f686956678092c1d93021641.png&quot; alt=&quot;Legacy Codebase with an arrow into a TXT file labelled &amp;amp;quot;Readable Specification&amp;amp;quot;.&quot; title=&quot;&quot;/&gt;&lt;figcaption&gt;The features of the legacy codebase need to be revised&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;The valuable artifact in the middle is not new code — it&amp;#39;s a
&lt;strong&gt;readable specification&lt;/strong&gt; of what the old code &lt;strong&gt;actually does&lt;/strong&gt;. Plain text,
human-reviewable, version-controllable. The kind of thing you need when you
want to ask the product owner if a feature is &lt;strong&gt;really&lt;/strong&gt; wanted that way. - Or if it is just a legacy quirk.&lt;/p&gt;&lt;p&gt;If you have it, you can use it for estimates. It can be your map to plan the
implementation and might even give you a hint about that stakeholder that you
might have forgotten about otherwise.&lt;/p&gt;&lt;h3&gt;Organised and illustrated&lt;a href=&quot;https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#organised-and-illustrated&quot;&gt;​&lt;/a&gt;&lt;/h3&gt;&lt;figure&gt;&lt;img src=&quot;https://bitcrowd.dev/assets/images/step-2-9f42ae2f41b319470f65ad9ec5928524.png&quot; alt=&quot;Legacy Codebase pointing into an Architecture Map of Module A, Module B, Module C, each with its own .md spec.&quot; title=&quot;&quot;/&gt;&lt;figcaption&gt;The specification&amp;#39;s organisation should mirror the codebase&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;A single big text file describing a whole legacy system is only useful for very small projects. You want the spec broken
down by module — by bounded context — and the modules themselves arranged on a map of the architecture. Once each module
has its own spec associated to it, two things become possible: you can divide the work, and you can reason about
coverage.&lt;/p&gt;&lt;h3&gt;Job done! Or is it?&lt;a href=&quot;https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#job-done-or-is-it&quot;&gt;​&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;We now have and organised map of the system and a set of specifications. We have an overview, and the details. We could
now talk to the stakeholders, the development team, and set to work.&lt;/p&gt;&lt;figure&gt;&lt;img src=&quot;https://bitcrowd.dev/assets/images/step-3-f8cc173a463a67d9b7f04b22acfa704a.png&quot; alt=&quot;A graphic showing rectangles representing the software parts map to rectangles representing the specification&quot; title=&quot;&quot;/&gt;&lt;figcaption&gt;The specification&amp;#39;s organisation should mirror the codebase&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;&lt;strong&gt;However, we have not yet verified our findings.&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;What we are looking at is not yet a specification of the legacy system — it is a &lt;em&gt;speculation&lt;/em&gt; of the legacy system.
A document that &lt;strong&gt;claims&lt;/strong&gt; to describe what the code does, written by reading the code (or by asking an LLM to read the code).
Plausible, well-organised, reviewable. But unverified.&lt;/p&gt;&lt;h4&gt;1. Verifiability&lt;a href=&quot;https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#1-verifiability&quot;&gt;​&lt;/a&gt;&lt;/h4&gt;&lt;p&gt;This is &lt;strong&gt;speculation as specification&lt;/strong&gt;, and it&amp;#39;s the trap most &amp;quot;let&amp;#39;s document the legacy system&amp;quot; projects fall into.
A Word document of &amp;quot;what the system does&amp;quot; can be worse than no document at all, because people start trusting it.
But even if the specification is done as a group effort by all stakeholders involved, and is thoroughly verified, it
still lacks one critical property:&lt;/p&gt;&lt;h4&gt;2. Reproducibility&lt;a href=&quot;https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#2-reproducibility&quot;&gt;​&lt;/a&gt;&lt;/h4&gt;&lt;p&gt;What we actually want is a &lt;strong&gt;verifiable specification&lt;/strong&gt;: Not &amp;quot;verified once,&amp;quot; but verifiable on demand, any time someone
asks the question. You could verify a paper spec by hand, of course — sit down, read it, run the system, tick off the
scenarios — but manual verification is expensive enough that it gets done once at sign-off and never again.&lt;/p&gt;&lt;p&gt;A verification you don&amp;#39;t actually do is verification that doesn&amp;#39;t exist. And unless the legacy codebase is a museum
exhibit, it&amp;#39;s a moving target:&lt;/p&gt;&lt;p&gt;The team is still shipping fixes, the system is still drifting, and a paper spec written on Monday is no longer accurate
by Friday.&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://bitcrowd.dev/assets/images/step-6-ff8b98aeca5f9fdf5732fe355a44fbc0.png&quot; alt=&quot;Same picture but with a red horse galloping across the bottom and the words &amp;amp;quot;BUT SPECS GO STALE&amp;amp;quot;. The Legacy Codebase is now annotated as &amp;amp;quot;A moving target&amp;amp;quot;.&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;h3&gt;Runnable specs&lt;a href=&quot;https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#runnable-specs&quot;&gt;​&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;The fix is to make the specs &lt;strong&gt;executable&lt;/strong&gt; — to wire them into something that can run them against the actual legacy system, on demand, and tell you whether they still hold.&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://bitcrowd.dev/assets/images/step-7-444bd86f1323e81fa52511f1d57830f1.png&quot; alt=&quot;The earlier picture with a new &amp;amp;quot;SOME MAGIC&amp;amp;quot; box added below, with arrows looping the .assay specs back through the magic box and into the legacy codebase.&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;That is the move from speculation to verifiable specification: not a one-shot audit, but a button you can press on every commit, every nightly, every time anyone asks &amp;quot;is this still true?&amp;quot;&lt;/p&gt;&lt;p&gt;A &lt;strong&gt;green&lt;/strong&gt; run proves the spec is still true. A &lt;strong&gt;red&lt;/strong&gt; run is a useful signal: either the legacy drifted, or your spec was wrong.
Either way, you find out before the rewrite, not during it.&lt;/p&gt;&lt;h3&gt;That magic is Assay&lt;a href=&quot;https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#that-magic-is-assay&quot;&gt;​&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;&lt;img src=&quot;https://bitcrowd.dev/assets/images/step-8-c06ddef9a1740cfb355acb10dace6184.png&quot; alt=&quot;The bottom box is now labelled &amp;amp;quot;Assay — A framework to automate assertions&amp;amp;quot; with a Parser that reads .assay files and a &amp;amp;quot;(The parts you fill in)&amp;amp;quot; placeholder beneath it.&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;Assay is the runner.
It parses the &lt;code&gt;.assay&lt;/code&gt; specs Surveyor produced, matches each step against bindings you write, and exercises the real legacy system through whatever interface it actually exposes — HTTP, CLI, message queue, file drop, whatever.
The framework is small.&lt;/p&gt;&lt;p&gt;The runtime contract is &amp;quot;your bindings + your assertions + a deterministic runner.&amp;quot;&lt;/p&gt;&lt;h3&gt;The systemic approach&lt;a href=&quot;https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#the-systemic-approach&quot;&gt;​&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;&lt;img src=&quot;https://bitcrowd.dev/assets/images/overview-110149d565e655cbe254faa9456037a1.png&quot; alt=&quot;The full picture: Surveyor on top with Architecture Map and .assay specs; Assay below with Parser, Pattern Recognition, Bindings, and Given/When/Then automations; arrows looping back to the Legacy Codebase.&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;Open up that &amp;quot;parts you fill in&amp;quot; box and you find the actual surface area of work: pattern recognition for step matching, a binding per target system, and Given/When/Then automations for setup, action, and verification.&lt;/p&gt;&lt;p&gt;The end state has a property that nothing in the earlier pictures had: the problem space has been &lt;strong&gt;dissected into manageable pieces&lt;/strong&gt;.
Architecture, behavior, target adapters, assertions — each is small, each can be reviewed independently, and each can be picked up by a human or an agent without having to hold the whole system in their head at once.&lt;/p&gt;&lt;p&gt;That&amp;#39;s the goal of the toolkit.
Everything below is the detail of how each piece is built.&lt;/p&gt;&lt;h2&gt;Surveyor — discovering the architecture you inherited&lt;a href=&quot;https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#surveyor--discovering-the-architecture-you-inherited&quot;&gt;​&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Surveyor is a Mix project that takes a path to a codebase and produces a &lt;a href=&quot;https://structurizr.com/&quot;&gt;Structurizr&lt;/a&gt; DSL workspace file.
It runs in three phases mapping to the C4 model: C1 (system context), C2 (containers), C3 (components per container).&lt;/p&gt;&lt;p&gt;Crucially, Surveyor does &lt;strong&gt;not&lt;/strong&gt; parse code.
There are no language-specific parsers, no AST walkers, no fragile grammar files for thirty different ecosystems.&lt;/p&gt;&lt;p&gt;It scans the filesystem to identify what&amp;#39;s there — languages, frameworks, project files, entry points, deployment manifests — and then feeds meaningful chunks to an LLM. That sounds trivial, but is the same mechanism that commercial coding agents use when
they analyse your code.&lt;/p&gt;&lt;p&gt;Surveyor&amp;#39;s intelligence is in the prompts and the chunking, not in a stack of half-broken language adapters.&lt;/p&gt;&lt;p&gt;The CLI is interactive at every phase.&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code class=&quot;codeBlockLines_e6Vv&quot;&gt;$ surveyor ./legacy-monolith --phase c1

Phase 1 — System Context
  Scanning codebase...
  Querying LLM...

  System: &amp;quot;Order Management System&amp;quot;
  Description: Manages the full order lifecycle

  Actors:
    ✓ Customer — places and tracks orders via Web UI
    ✓ Warehouse Staff — manages fulfillment via Back Office
    ? Admin — found references in auth config (confidence: low)

  External Systems:
    ✓ Stripe — payment processing (REST API)
    ? SendGrid — found API key in config (confidence: low)

[a]ccept  [e]dit  [r]etry with more context  [q]uit
&amp;gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;The LLM is asked to flag uncertainty.
Anything tagged &lt;code&gt;confidence: low&lt;/code&gt; shows up with a &lt;code&gt;?&lt;/code&gt; and a short reasoning string for human review.
That&amp;#39;s not just nice ergonomics — it&amp;#39;s the bit that makes the tool trustworthy.
An LLM that confidently invents a &lt;code&gt;PaymentReconciliation&lt;/code&gt; container that doesn&amp;#39;t exist is worse than no model at all.
An LLM that says &amp;quot;I see a &lt;code&gt;SENDGRID_API_KEY&lt;/code&gt; in &lt;code&gt;.env.example&lt;/code&gt; but no Sendgrid client in the source, please confirm&amp;quot; is doing the right kind of work.&lt;/p&gt;&lt;p&gt;Each phase is resumable.
Results are saved as JSON in &lt;code&gt;./surveyor/&lt;/code&gt;, so a thirty-container system doesn&amp;#39;t have to finish in one sitting.
You can hop in, accept C1, take a break, come back tomorrow, and resume at C2.&lt;/p&gt;&lt;p&gt;The end product is a &lt;code&gt;workspace.dsl&lt;/code&gt; you can render in Structurizr.
But more importantly, it&amp;#39;s a &lt;code&gt;workspace.dsl&lt;/code&gt; whose components are decorated with &lt;code&gt;assay.specs&lt;/code&gt; and &lt;code&gt;assay.schema&lt;/code&gt; properties, pointing at the behavioral specs and the domain schemas for each bounded context.&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code class=&quot;codeBlockLines_e6Vv&quot;&gt;orderLifecycle = component &amp;quot;Order Lifecycle&amp;quot; &amp;quot;Orders&amp;quot; &amp;quot;Bounded Context&amp;quot; {
    properties {
        &amp;quot;assay.specs&amp;quot; &amp;quot;specs/order-lifecycle&amp;quot;
        &amp;quot;assay.schema&amp;quot; &amp;quot;schemas/order_lifecycle.ex&amp;quot;
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;DDD pattern annotations from the LLM (&lt;code&gt;customer-supplier&lt;/code&gt;, &lt;code&gt;anticorruption-layer&lt;/code&gt;, …) ride along as properties on the relationship.
Every component gets &lt;code&gt;assay.specs&lt;/code&gt; and &lt;code&gt;assay.schema&lt;/code&gt; properties — the contract the next tool reads from.&lt;/p&gt;&lt;h3&gt;Examples&lt;a href=&quot;https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#examples&quot;&gt;​&lt;/a&gt;&lt;/h3&gt;&lt;h4&gt;The System Context of a Medical Application&lt;a href=&quot;https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#the-system-context-of-a-medical-application&quot;&gt;​&lt;/a&gt;&lt;/h4&gt;&lt;p&gt;Surveyor identified the actors and the external systems of the application. Without knowing the code, we would already
know which actor types there are&lt;/p&gt;&lt;figure&gt;&lt;img src=&quot;https://bitcrowd.dev/assets/images/MeditechsystemContext-b7cb1953223f982d281b4b7110677683.png&quot; alt=&quot;&quot; title=&quot;&quot;/&gt;&lt;figcaption&gt;The System Context of a medical application&lt;/figcaption&gt;&lt;/figure&gt;&lt;h4&gt;An example of an assay spec created in the second phase:&lt;a href=&quot;https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#an-example-of-an-assay-spec-created-in-the-second-phase&quot;&gt;​&lt;/a&gt;&lt;/h4&gt;&lt;figure&gt;&lt;img src=&quot;https://bitcrowd.dev/assets/images/MeditechAssay-c4-46cf27120239ca00a3e2a7202cf32936.png&quot; alt=&quot;&quot; title=&quot;&quot;/&gt;&lt;figcaption&gt;The Account Creation Specs of a medical application&lt;/figcaption&gt;&lt;/figure&gt;&lt;h2&gt;Assay — proving the rewrite behaves the same&lt;a href=&quot;https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#assay--proving-the-rewrite-behaves-the-same&quot;&gt;​&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Assay is a minimal behavioral spec runner.
Specs are plain text and look like this:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code class=&quot;codeBlockLines_e6Vv&quot;&gt;Component: [orderLifecycle] Order Lifecycle
Context: Order Placement

Definitions:
  - &amp;quot;a valid customer&amp;quot; means:
      a Customer with status Active, verified email,
      and a credit limit greater than zero

Invariants:
  - total must not exceed customer credit limit
  - at least one line item required

Rule: Customers can place orders for in-stock items

  @critical @phase-1
  Scenario: [OL-001] Place a simple order
    Given a valid customer &amp;quot;Alice&amp;quot; with credit limit €10,000
    And product &amp;quot;Widget&amp;quot; is in stock with 50 units available
    When Alice places an order for 3 units of &amp;quot;Widget&amp;quot;
    Then the order status becomes &amp;quot;Placed&amp;quot;
    And stock for &amp;quot;Widget&amp;quot; is reduced to 47 units&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;The bracketed &lt;code&gt;[orderLifecycle]&lt;/code&gt; is a workspace.dsl identifier — the same identifier Surveyor wrote into the architecture model.
The bracketed &lt;code&gt;[OL-001]&lt;/code&gt; on the scenario is an optional free-form id that survives across rewrites of the spec text and lets you cross-reference from tickets or audit trails.&lt;/p&gt;&lt;p&gt;If that looks like Cucumber or Gherkin, it does — with one large difference.
There is no glue layer, no abstraction over what kind of system you are testing, no framework opinions about HTTP, databases, or browsers.
The bindings are just Elixir.&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code class=&quot;codeBlockLines_e6Vv&quot;&gt;defmoduleTargets.Legacy.OrderLifecycledo
useAssay.Binding,component:&amp;quot;orderLifecycle&amp;quot;

@baseSystem.get_env(&amp;quot;LEGACY_API_URL&amp;quot;)

  step :given,~r/a valid customer &amp;quot;(?P&amp;lt;name&amp;gt;.+)&amp;quot; with credit limit €(?P&amp;lt;limit&amp;gt;[\d,.]+)/do
{:ok, resp}=Req.post(&amp;quot;#{@base}/test/customers&amp;quot;,json:%{
name:params().name,
credit_limit:parse_money(params().limit),
status:&amp;quot;Active&amp;quot;
})
assign(customer_id: resp.body[&amp;quot;id&amp;quot;])
end

  step :action,~r/.+ places an order for (?P&amp;lt;qty&amp;gt;\d+) units? of &amp;quot;(?P&amp;lt;product&amp;gt;.+)&amp;quot;/do
{:ok, resp}=Req.post(&amp;quot;#{@base}/orders&amp;quot;,json:%{
customer_id:var(:customer_id),
lines:[%{product_id:var(:product_id),quantity:to_int(params().qty)}]
})
assign(order_id: resp.body[&amp;quot;id&amp;quot;],http_status: resp.status)
end

  step :expect,~r/the order status becomes &amp;quot;(?P&amp;lt;status&amp;gt;.+)&amp;quot;/do
{:ok, resp}=Req.get(&amp;quot;#{@base}/orders/#{var(:order_id)}&amp;quot;)
    assert resp.body[&amp;quot;status&amp;quot;]==params().status
end
end&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;A binding is just a module that brings whatever it needs — &lt;code&gt;Req&lt;/code&gt; for an HTTP API, &lt;code&gt;AMQP&lt;/code&gt; for a message broker, &lt;code&gt;System.cmd&lt;/code&gt; for a batch processor, &lt;code&gt;File&lt;/code&gt; for a directory-watching pipeline.
Assay itself does not ship an HTTP client or a database adapter.&lt;/p&gt;&lt;p&gt;The framework provides parsing, step matching, scenario lifecycle, and assertions; the rest is your code.
That&amp;#39;s what makes the same spec runnable against an HTTP API, a CLI tool, a batch job, or a message queue, depending on what the legacy system happens to be.&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;p&gt;
🚀 Would you like to run this on your legacy codebase? Sign up for early access! &lt;a href=&quot;https://73f4313d.sibforms.com/serve/MUIFAGf7xJuMm4W5YSsztruKlELH459zdPyB50IhmpgBnS6wyrTGqz7dQ55IJLSVa7CUPFvAHrUEvEYbHEn5tBivo8SQPN_cJIMdBr_O1xiQ9ug64k46sfNBIFuTHK1rNKqvqytuDkTUoh0C5XMyUKOPZy7whNQM8zviGihyCCrZYBTq7uTf5-35fK6Ki2YBOVnEAmV_WwrgoT1-wg==&quot;&gt;Count me in!&lt;/a&gt;&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;The crucial property is target swappability:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code class=&quot;codeBlockLines_e6Vv&quot;&gt;assay run specs/order-lifecycle/ --target legacy
assay run specs/order-lifecycle/ --target new&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;Same specs. Different bindings. Different systems.
The legacy binding hits the old SOAP API; the new binding hits the new Phoenix endpoint.
If both go green, the rewrite is behavior-equivalent for the things the spec covers — which, after a few months of writing specs, is most of the things that matter.&lt;/p&gt;&lt;p&gt;There is no separate &lt;code&gt;.exs&lt;/code&gt; file generated from the specs.
The runner parses &lt;code&gt;.assay&lt;/code&gt; files, matches each step against a regex on a binding function, executes them, and prints pass/fail.
That&amp;#39;s it.&lt;/p&gt;&lt;h3&gt;How Assay actually works&lt;a href=&quot;https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#how-assay-actually-works&quot;&gt;​&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;The runtime is around six hundred lines of Elixir.
Five design choices do most of the work.&lt;/p&gt;&lt;h4&gt;A two-pass parser&lt;a href=&quot;https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#a-two-pass-parser&quot;&gt;​&lt;/a&gt;&lt;/h4&gt;&lt;p&gt;The first pass lifts triple-quoted doc strings out of the file (so the line tokenizer doesn&amp;#39;t have to reason about them).
The second pass dispatches on the first keyword on each line.&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code class=&quot;codeBlockLines_e6Vv&quot;&gt;defparse_string(content)do
{filtered, doc_strings}=
    content
|&amp;gt;String.split(&amp;quot;\n&amp;quot;)
|&amp;gt;Enum.with_index(1)
|&amp;gt;extract_doc_strings()

Process.put(:assay_doc_strings, doc_strings)

  filtered
|&amp;gt;Enum.reject(fn{line, _}-&amp;gt;
    trimmed =String.trim(line)
    trimmed ==&amp;quot;&amp;quot;orString.starts_with?(trimmed,&amp;quot;#&amp;quot;)
end)
|&amp;gt;parse_lines(%Spec{})
end&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;Output is a plain &lt;code&gt;%Spec{}&lt;/code&gt; struct. No AST nodes, no string interpolation.&lt;/p&gt;&lt;h4&gt;The step macro generates real module functions&lt;a href=&quot;https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#the-step-macro-generates-real-module-functions&quot;&gt;​&lt;/a&gt;&lt;/h4&gt;&lt;p&gt;Each &lt;code&gt;step :given, ~r/.../ do ... end&lt;/code&gt; call expands into a uniquely-named function plus a &lt;code&gt;%StepBinding{}&lt;/code&gt; record on an accumulating attribute.&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code class=&quot;codeBlockLines_e6Vv&quot;&gt;defmacrostep(type, regex,do: block)
when type in[:given,:action,:expect]do
  fn_name = :&amp;quot;__step_#{:erlang.unique_integer([:positive])}__&amp;quot;

quotedo
@assay_steps{unquote(type),unquote(regex),&amp;amp;__MODULE__.unquote(fn_name)/0}

defunquote(fn_name)()do
unquote(block)
end
end
end&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;At compile time, &lt;code&gt;__before_compile__&lt;/code&gt; exposes &lt;code&gt;__steps__/0&lt;/code&gt; and &lt;code&gt;__component__/0&lt;/code&gt; for the runner to read.
There is no string codegen, no eval — just normal Elixir function definitions.&lt;/p&gt;&lt;h4&gt;Component-scoped step matching&lt;a href=&quot;https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#component-scoped-step-matching&quot;&gt;​&lt;/a&gt;&lt;/h4&gt;&lt;p&gt;Anyone who has used Cucumber has hit the global step-definition problem: the same step text means different things in different contexts, but Gherkin treats step definitions as one shared namespace.
Assay scopes bindings by component:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code class=&quot;codeBlockLines_e6Vv&quot;&gt;defprun_spec(spec, bindings, target, tags, exclude_tags)do
  component_bindings =
Enum.filter(bindings,fn b -&amp;gt; b.component == spec.component end)

# ... run each scenario against component_bindings only
end&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;This is the direct reason Surveyor and Assay use the same identifier convention.
The architecture model and the spec runner share a vocabulary, and step text never collides across bounded contexts.&lt;/p&gt;&lt;h4&gt;Pre-check, then execute&lt;a href=&quot;https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#pre-check-then-execute&quot;&gt;​&lt;/a&gt;&lt;/h4&gt;&lt;p&gt;Before running any step, the runner walks the whole scenario and finds a binding for every step.
If anything is unbound, the scenario fails before any side effects.&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code class=&quot;codeBlockLines_e6Vv&quot;&gt;defrun_scenario(scenario, bindings)do
  matched_steps =
Enum.map(scenario.steps,fn step -&amp;gt;
casefind_binding(step, bindings)do
nil-&amp;gt;{:unbound, step}
        binding -&amp;gt;{:bound, step, binding}
end
end)

ifEnum.any?(matched_steps,&amp;amp;match?({:unbound, _},&amp;amp;1))do
%ScenarioResult{status::error,error:&amp;quot;Unbound steps found&amp;quot;}
else
execute_matched_steps(scenario, matched_steps, bindings)
end
end&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;This is what stops a scenario from running half its Givens, hitting an unbound When, and leaving a dangling test customer in the database.&lt;/p&gt;&lt;h4&gt;Per-scenario state in the process dictionary&lt;a href=&quot;https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#per-scenario-state-in-the-process-dictionary&quot;&gt;​&lt;/a&gt;&lt;/h4&gt;&lt;p&gt;Each scenario gets a fresh context — a tiny module that stores variables, params, doc strings, and data tables in the process dictionary.&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code class=&quot;codeBlockLines_e6Vv&quot;&gt;def init do
Process.put(@vars_key,%{})
Process.put(@params_key,%{})
end

defget_var(name)do
Process.get(@vars_key,%{})|&amp;gt;Map.get(name)
end

defset_vars(keyword)do
  vars =Process.get(@vars_key,%{})
Process.put(@vars_key,Enum.into(keyword, vars))
end&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;The runner can &lt;code&gt;save/0&lt;/code&gt; and &lt;code&gt;restore/1&lt;/code&gt; the snapshot, which is how Assay tests itself by running its own &lt;code&gt;.assay&lt;/code&gt; specs through itself in the same process.&lt;/p&gt;&lt;p&gt;There is no plugin system, no dependency injection, no scenario hooks beyond &lt;code&gt;cleanup&lt;/code&gt;.
Adding any of those would push complexity into the framework and out of the binding, which is the wrong direction.
The framework exists to be small enough that you can read it on a Friday afternoon and trust it on Monday.&lt;/p&gt;&lt;h2&gt;How they fit together&lt;a href=&quot;https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#how-they-fit-together&quot;&gt;​&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;The flow is straightforward and it maps onto the five phases laid out in the Assay handbook.&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;strong&gt;Discovery (Surveyor).&lt;/strong&gt; Run Surveyor against the legacy codebase. Walk through the C1/C2/C3 output interactively. Edit, retry, accept. Produces &lt;code&gt;workspace.dsl&lt;/code&gt;.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Behavioral extraction.&lt;/strong&gt; For each component in the model, write &lt;code&gt;.assay&lt;/code&gt; specs and an Elixir schema. The &lt;code&gt;assay.specs&lt;/code&gt; and &lt;code&gt;assay.schema&lt;/code&gt; properties on every Structurizr component tell you exactly where they go: &lt;code&gt;specs/&amp;lt;context&amp;gt;/&lt;/code&gt; and &lt;code&gt;schemas/&amp;lt;context&amp;gt;.ex&lt;/code&gt;.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Validation.&lt;/strong&gt; Write bindings against the legacy system in &lt;code&gt;targets/legacy/&lt;/code&gt;. Run &lt;code&gt;assay run specs/ --target legacy&lt;/code&gt;. Iterate until green. You now have a verified, executable specification of the legacy system&amp;#39;s behavior.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Stabilization.&lt;/strong&gt; Review what&amp;#39;s covered, what&amp;#39;s missing, what&amp;#39;s flagged as ambiguous. Get sign-off on scope.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Rewrite.&lt;/strong&gt; Build the new system. Write bindings against it in &lt;code&gt;targets/new/&lt;/code&gt;. Run &lt;code&gt;assay run specs/ --target new&lt;/code&gt;. When that&amp;#39;s green, the rewrite is done — at least for the surface area the specs cover.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;The reason the two tools sit next to each other is that the architecture model is what gives the spec work structure.
Without a model, &amp;quot;write specs for the legacy system&amp;quot; is an open-ended task with no obvious stopping point.
With the model, every component has a &lt;code&gt;specs/&lt;/code&gt; directory and the question becomes &amp;quot;is each context&amp;#39;s behavior covered?&amp;quot;
That&amp;#39;s tractable.&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;p&gt;
🚀 Would you like to run this on your legacy codebase? Sign up for early access! &lt;a href=&quot;https://73f4313d.sibforms.com/serve/MUIFAGf7xJuMm4W5YSsztruKlELH459zdPyB50IhmpgBnS6wyrTGqz7dQ55IJLSVa7CUPFvAHrUEvEYbHEn5tBivo8SQPN_cJIMdBr_O1xiQ9ug64k46sfNBIFuTHK1rNKqvqytuDkTUoh0C5XMyUKOPZy7whNQM8zviGihyCCrZYBTq7uTf5-35fK6Ki2YBOVnEAmV_WwrgoT1-wg==&quot;&gt;Count me in!&lt;/a&gt;&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;The cross-target story is what makes the rewrite verifiable.
The same parsed &lt;code&gt;.assay&lt;/code&gt; scenarios are dispatched, step by step, through two different binding modules at two different points in the project&amp;#39;s life:&lt;/p&gt;&lt;p&gt;Same spec, two bindings, two systems.
The runner doesn&amp;#39;t know or care which target it&amp;#39;s dispatching to — that&amp;#39;s the property that makes the rewrite a measurable thing rather than a leap of faith.&lt;/p&gt;&lt;h2&gt;Four audiences, one artifact&lt;a href=&quot;https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#four-audiences-one-artifact&quot;&gt;​&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Four very different parties end up reading — or executing — the same &lt;code&gt;.assay&lt;/code&gt; file: the &lt;strong&gt;product owner&lt;/strong&gt;, the &lt;strong&gt;developer&lt;/strong&gt;, the &lt;strong&gt;coding agent&lt;/strong&gt;, and the &lt;strong&gt;test runner&lt;/strong&gt;.
The interesting observation is that they don&amp;#39;t conflict on &lt;strong&gt;content&lt;/strong&gt; — they conflict on &lt;strong&gt;form&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Product owners&lt;/strong&gt; want to know what the system does in business terms, what would break if you changed X, and where the risk lives.
They don&amp;#39;t want UML.
They want to read scenarios in their domain vocabulary, in plain language, and trust them.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Developers&lt;/strong&gt; want to know how the system is structured, where the seams are, and what invariants they mustn&amp;#39;t break.
And — crucially — they want the documentation to be wrong less often than the code is.
The moment docs lie, developers stop reading them.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Coding agents&lt;/strong&gt; want machine-parseable, unambiguous, addressable artifacts with stable identifiers.
They want to be able to say &amp;quot;the assertion at &lt;code&gt;assay://orderLifecycle/order-placement/OL-001/step-3&lt;/code&gt; failed&amp;quot; and have that mean something durable.
They want types, schemas, and graphs they can traverse.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Automated tests&lt;/strong&gt; want executable bindings — the layer Assay provides — and they want failures that point back to the spec, not just to a line of code.&lt;/p&gt;&lt;p&gt;Same facts, four presentations.&lt;/p&gt;&lt;p&gt;The &lt;code&gt;.assay&lt;/code&gt; file gives the product owner readable scenarios in domain language.
The &lt;code&gt;workspace.dsl&lt;/code&gt; plus the schema modules give the developer the structural truth.
The bracketed identifiers (&lt;code&gt;Component: [orderLifecycle]&lt;/code&gt;, &lt;code&gt;Scenario: [OL-001]&lt;/code&gt;) give the agent its stable addresses.
The runner turns the whole thing into a regression suite.
None of these audiences is asked to read the others&amp;#39; representation; they all read the same spec, surfaced at the level of detail they need.&lt;/p&gt;&lt;h2&gt;Where the LLM lives, and where it doesn&amp;#39;t&lt;a href=&quot;https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#where-the-llm-lives-and-where-it-doesnt&quot;&gt;​&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;A reasonable question at this point: with all the recent enthusiasm for LLMs, why isn&amp;#39;t more of this LLM-driven?&lt;/p&gt;&lt;p&gt;The split is deliberate.&lt;/p&gt;&lt;p&gt;Surveyor uses an LLM because architectural discovery is genuinely a language task — you&amp;#39;re reading config files, route definitions, deployment manifests, dependency lock files, and inferring &amp;quot;this is the API, that&amp;#39;s the worker, that&amp;#39;s the read model.&amp;quot;
That&amp;#39;s exactly the kind of fuzzy, evidence-weighing job an LLM does well, especially when you tell it to flag uncertainty rather than guess.&lt;/p&gt;&lt;p&gt;Assay does &lt;strong&gt;not&lt;/strong&gt; use an LLM at runtime.
The runner is deterministic: parse the spec, match the regex, execute the binding, assert.
A behavioral spec that sometimes passes and sometimes does not, depending on which model variant happened to answer, is not a spec.&lt;/p&gt;&lt;p&gt;LLMs are very welcome on the authoring side — drafting &lt;code&gt;.assay&lt;/code&gt; files from legacy source is a great agent task, and the handbook has explicit guidance for coding agents about how to do that without inventing behavior.
But the green/red signal at the end has to come from a deterministic runner, or it is not really a signal.&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;p&gt;
🚀 Sign up for early access:&lt;a href=&quot;https://73f4313d.sibforms.com/serve/MUIFAGf7xJuMm4W5YSsztruKlELH459zdPyB50IhmpgBnS6wyrTGqz7dQ55IJLSVa7CUPFvAHrUEvEYbHEn5tBivo8SQPN_cJIMdBr_O1xiQ9ug64k46sfNBIFuTHK1rNKqvqytuDkTUoh0C5XMyUKOPZy7whNQM8zviGihyCCrZYBTq7uTf5-35fK6Ki2YBOVnEAmV_WwrgoT1-wg==&quot;&gt;Count me in!&lt;/a&gt;&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;h2&gt;How you use them on a project&lt;a href=&quot;https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#how-you-use-them-on-a-project&quot;&gt;​&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;In practice, the first day of a legacy rewrite looks something like this:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Clone the repo. Run Surveyor with &lt;code&gt;--phase c1&lt;/code&gt; to get the system in context. Discuss the actors and external systems with the client — this alone surfaces &amp;quot;wait, who is the Admin role?&amp;quot; conversations that would otherwise happen three months in.&lt;/li&gt;&lt;li&gt;Run &lt;code&gt;--phase c2&lt;/code&gt; to map containers. This is where you discover the data plane that nobody documented, the cron job running on a forgotten server, the queue that two services share. Surveyor flags &lt;code&gt;confidence: low&lt;/code&gt; items; the human resolves them.&lt;/li&gt;&lt;li&gt;Run &lt;code&gt;--phase c3&lt;/code&gt; per container. By the end you have a &lt;code&gt;workspace.dsl&lt;/code&gt; that is, for the first time in the project&amp;#39;s history, an accurate picture of what is deployed.&lt;/li&gt;&lt;li&gt;Pick the highest-risk bounded context. Read the legacy source. Draft &lt;code&gt;.assay&lt;/code&gt; specs for its behavior. Write a legacy binding. Run it. Iterate until green.&lt;/li&gt;&lt;li&gt;Repeat per context, in priority order, until coverage is good enough to start the rewrite.&lt;/li&gt;&lt;li&gt;Build the new system, one bounded context at a time, with &lt;code&gt;targets/new/&lt;/code&gt; bindings going green as each context comes online.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;The thing both tools are optimized for is the same: making the work legible.
A legacy rewrite is a long project with shifting personnel and a nervous client.
Both Surveyor and Assay produce artifacts — a &lt;code&gt;workspace.dsl&lt;/code&gt; and a directory of &lt;code&gt;.assay&lt;/code&gt; files — that survive turnover, show progress, and that the client can actually read.&lt;/p&gt;&lt;h2&gt;Status and next steps&lt;a href=&quot;https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#status-and-next-steps&quot;&gt;​&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Both tools are ready to be tested in real world scenarios. &lt;strong&gt;This is where we need your help&lt;/strong&gt;:&lt;/p&gt;&lt;p&gt;If you have a legacy system staring you down and a rewrite on the roadmap, &lt;a href=&quot;https://73f4313d.sibforms.com/serve/MUIFAGf7xJuMm4W5YSsztruKlELH459zdPyB50IhmpgBnS6wyrTGqz7dQ55IJLSVa7CUPFvAHrUEvEYbHEn5tBivo8SQPN_cJIMdBr_O1xiQ9ug64k46sfNBIFuTHK1rNKqvqytuDkTUoh0C5XMyUKOPZy7whNQM8zviGihyCCrZYBTq7uTf5-35fK6Ki2YBOVnEAmV_WwrgoT1-wg==&quot;&gt;get in touch&lt;/a&gt;. We would love to hear from you!&lt;/p&gt;</content:encoded>
</item>
<item>
<title>Elixir: Introduction</title>
<link>https://priver.dev/post/elixir-introduction</link>
<guid isPermaLink="false">hs1NTOkg8A_vQRj0zfvHsjQluwDtepppWf87Bw==</guid>
<pubDate>Sun, 26 Apr 2026 14:28:15 +0000</pubDate>
<description>Elixir: Introduction</description>
<content:encoded>Elixir: Introduction</content:encoded>
</item>
<item>
<title>Head of the Agents and Assistants Department</title>
<link>https://rocket-science.ru/hacking/2026/04/23/developing-as-ai</link>
<enclosure type="image/jpeg" length="0" url="https://rocket-science.ru/img/logo/logo-orig.png"></enclosure>
<guid isPermaLink="false">WkNZM12AigiuASQEvEpBhyPVLPzlG-ZnZzS21g==</guid>
<pubDate>Thu, 23 Apr 2026 10:20:28 +0000</pubDate>
<description>A first-hand account of building the Cure programming language with AI agents as co-developers—what worked, what crashed spectacularly, and the distilled rules that emerged from the wreckage.</description>
<content:encoded>&lt;p&gt;Let me state upfront: my attitude toward AI assistants cannot be expressed as a boolean value. If you need an answer to the question posed point-blank—“New York Yankees or Boston Red Sox?”—I do not watch baseball at all; I’m a Barça fan. That said, I find AI assistants a perfectly legitimate and even liquid asset. The text below is an account of what made my work with agents pleasant and reduced their errors and rough edges to an acceptable minimum.&lt;/p&gt;&lt;p&gt;A little under a year ago I began working on &lt;a href=&quot;https://cure-lang.org&quot;&gt;Cure&lt;/a&gt;, a programming language with dependent types, finite state machines as first-class citizens, SMT verification, and other niceties, compiling to the BEAM.&lt;/p&gt;&lt;p&gt;My first approach to the apparatus ended in ignominious failure. I got tangled in my own architectural decisions, started piling on crutches wherever they fit, turned the code completely into an Italian restaurant menu, and lost heart. In a fit of idiocy I had an assistant generate the website and showed it to the public—the ideas themselves were intriguing enough, dependent types and SMT solvers on the BEAM are hardly redundant, and I harboured a quiet hope for community interest. The community correctly identified the language’s site as slop generated by a language model and received my attempt with arctic indifference. I got a great many “nothing works” responses and not a single coherent suggestion for improvement (no fault of the community’s—against the backdrop of the slop-flood of those days, my project would not have looked like Noah’s Ark even to an extremely charitable observer).&lt;/p&gt;&lt;p&gt;I stepped back, examined my creation from all angles, and was forced to admit: I had produced a monster. I saw no chance of licking it into shape, even through a global refactor. I bought a ream of paper and some pencils and started drawing, in order to understand exactly where I had gone wrong. (Spoiler: I had been so enchanted by the idea itself, and so desperate to get something to launch and run, that I had done literally everything wrong.)&lt;/p&gt;&lt;p&gt;I had no intention of giving up—and before me, full height, loomed the necessity of rewriting everything from scratch without repeating the mistakes. By that point I knew that artificial assistants could significantly accelerate the actual writing of code, so I started by erecting scaffolding around the future project. It was obvious to me then (I can now confirm that former intuition with experience) that all those prompts along the lines of “You are a genius architect with three hundred years of experience designing languages with dependent types and SMT solvers” work no better than the morning pep talk to an intern at standup: “You are a great programmer who has written three hundred million billion lines of code without a debugger.” If an assistant tasked with writing coherent code to a spec needed motivational gibberish to function, it would not be worth using under any circumstances whatsoever. Seriously, think about it: a model is asked to implement a GCD module using the Euclidean algorithm—are you really suggesting its deeply baked-in internal rules will not guide it down the correct branches of the conditional operators without first being told it has the soul of a prima ballerina and an avant-garde poet? What on earth does “You are a &lt;em&gt;senior architect&lt;/em&gt;” actually mean? Do the people who advocate this believe that without such a preamble the training pathways via the memoirs of axolotl breeders will activate instead?&lt;/p&gt;&lt;p&gt;So, instead of all those skills/agents/whatever, I started by feeding this mechanised beast the source code of all my own libraries, lovingly written by hand, with the note: “Here are examples of good code. Write like this. Not like that—do not write like that.” I know it is immodest, but it is my assistant. You are welcome to feed yours your own code.&lt;/p&gt;&lt;p&gt;Then I reclaimed the wasteland: thus were born &lt;a href=&quot;https://hexdocs.pm/metastatic&quot;&gt;Metastatic&lt;/a&gt;, implementing MetaAST for different languages across different paradigms, and &lt;a href=&quot;https://hexdocs.pm/ragex&quot;&gt;Ragex&lt;/a&gt;—a RAG built on AST rather than plain text (my hunch that AST fits into a context window far more easily and is far better structured than raw source code turned out to be correct).&lt;/p&gt;&lt;p&gt;My task was building a new language; constructing a new ecosystem from scratch was not part of the brief. So I analysed existing solutions—Rust, Go, Elm, Gleam—and chose the one I considered most mature (I never promised the project would be neutral with respect to my tastes and preferences). I simply copied the Elixir ecosystem and added to it what I had personally found lacking over the last ten years. Thanks to standing on the shoulders of giants in this regard, the model wrote almost the entire ecosystem for me; I simply told it: “Look how beautifully new project creation is handled in Elixir—do the same for Cure.” Language models are strong at translation, and Elixir is &lt;a href=&quot;https://dashbit.co/blog/why-elixir-best-language-for-ai&quot;&gt;considerably more intelligible&lt;/a&gt; to them compared to almost every other language.&lt;/p&gt;&lt;p&gt;So, before the first line of code, my backpack already held: the right AST—a language in which the assistant and I can communicate far more easily than in homespun English—a handcrafted RAG, and a clear understanding that every step must be a simple, atomic change. &lt;strong&gt;The fewer choices the assistant has to make between two paths, the cleaner the result.&lt;/strong&gt; This principle outweighs the quality of all prompts combined.&lt;/p&gt;&lt;p&gt;Next I had to solve the problem of validating the produced code. My eyes are sharp, but they occasionally miss non-obvious bad decisions in review. Thus was born &lt;a href=&quot;https://hexdocs.pm/oeditus_credo&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;oeditus_credo&lt;/code&gt;&lt;/a&gt;—a set of nearly forty additional &lt;a href=&quot;https://hexdocs.pm/credo&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;credo&lt;/code&gt;&lt;/a&gt; checks covering vulnerabilities, anti-patterns, and the like. The library also ships a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mix oeditus_assistant_rules&lt;/code&gt; command, a rules generator for the soulless assistant. To those rules I also added: after each stage, verify that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mix format &amp;amp;&amp;amp; mix credo --strict &amp;amp;&amp;amp; mix dialyzer &amp;amp;&amp;amp; mix test&lt;/code&gt; passes; update all documentation; add regression tests for new code; then run all regression tests and confirm they are still as green as my face the morning after a party.&lt;/p&gt;&lt;p&gt;Every reasonably significant stage also ends with creating an “example” in the &lt;a href=&quot;https://github.com/am-kantox/cure-lang/tree/main/examples&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;examples&lt;/code&gt;&lt;/a&gt; folder. Something for people to look at, and the regression tests never idle.&lt;/p&gt;&lt;p&gt;At this point I felt ready to start writing actual code. Cure has certain critical parts I wrote by hand from scratch. Every other pull request gets manual edits from me. Every bug I find through manual testing I fix by hand (the obvious ones aside). And yet I reached the desired result considerably faster than if I had written every line in Vim.&lt;/p&gt;&lt;p&gt;Over the course of working on Cure I learned to keep the number of errors—and consequently manual edits—to a minimum. A fairly substantial release, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;v0.26.0&lt;/code&gt;, required not a single correction, for example. Here is the distillate of my rules for communicating with an assistant, in case it proves useful to anyone:&lt;/p&gt;&lt;ul&gt;
  &lt;li&gt;the task must be self-contained but not too large; “first define the types for numeric, then we will write the converters” does not work&lt;/li&gt;
  &lt;li&gt;inside the task there must be no undefined ambiguities that will derail our &lt;em&gt;T9&lt;/em&gt;; there must be exactly one path to the solution&lt;/li&gt;
  &lt;li&gt;before tackling any task, demand an implementation plan and edit it until all ambiguities have vanished&lt;/li&gt;
  &lt;li&gt;the task must be roughly solved in your own head before turning to the assistant; otherwise the odds of agreeing with an incorrect solution are uncomfortably high&lt;/li&gt;
  &lt;li&gt;generated code must be comprehensible and elegant, “rewrite this nicely, I fed you three gigabytes of rules” does not work; if the solution is aesthetically repellent, there is a problem with the task formulation—close the session and start over&lt;/li&gt;
  &lt;li&gt;setting a task and going for coffee is the direct path to the infinite iterations described in the previous point; the stream of unconsciousness must be watched and any attempt to stray from the planned path killed without mercy&lt;/li&gt;
  &lt;li&gt;finally, if it seems to you that the cognitive load is decreasing and that any cook could now implement such a project—you need pharmaceutical intervention; your fingers tire less, yes, but if you could not have implemented the project from scratch in a text editor, the LLM is no help to you; it will generate something suspiciously twitching, no argument there, but the first halfway-serious complexity requiring a considered architectural decision will put a cross on the whole enterprise.&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;That is my experience. Yours may differ; I can bear with you on that matter.&lt;/p&gt;&lt;p&gt;Try &lt;a href=&quot;https://cure-lang.org&quot;&gt;https://cure-lang.org&lt;/a&gt; and maybe you will like it. The site now has a &lt;a href=&quot;https://cure-lang.org/playground&quot;&gt;playground&lt;/a&gt; where you can experiment with types in real time, and an almost &lt;a href=&quot;https://cure-lang.org/repl&quot;&gt;real console&lt;/a&gt; where you can play with the REPL without installing anything locally.&lt;/p&gt;&lt;p&gt;Happy curing!&lt;/p&gt;</content:encoded>
</item>
<item>
<title>Scotty, I need warp speed in three minutes - Hauleth</title>
<link>https://hauleth.dev/post/things-about-elixir-you-probably-will-never-need/</link>
<enclosure type="image/jpeg" length="0" url="https://hauleth.dev/banner.png"></enclosure>
<guid isPermaLink="false">LgEc9iGNIbYFt5qz01nZzQVKxWy9JGcaHjxqjg==</guid>
<pubDate>Mon, 20 Apr 2026 23:02:18 +0000</pubDate>
<description>My journey into optimising Elixir codebase of Ultravisor (my fork of Supabase&#39;s Supavisor). This story is not about a goal, but ~~friends~~ optimisations we met along the way.</description>
<content:encoded>&lt;p&gt;In my last larger gig I worked on fascinating project - &lt;a href=&quot;https://github.com/supabase/supavisor&quot;&gt;Postgres connection pooler written in Elixir&lt;/a&gt;. Unfortunately, due to different circumstances, this project burned me out to the ground. However what doesn&amp;#39;t kill you &lt;del&gt;is crap not a weapon&lt;/del&gt; can become great learning experience.&lt;/p&gt;&lt;p&gt;Most of my achievements in this project were related to performance. This project contains &lt;strong&gt;very&lt;/strong&gt; tight loop in form of query handler, that needed to run hundreds of thousands times per second per user connection. That mean that this functions are &lt;em&gt;very&lt;/em&gt; sensitive to even slightest performance changes. And that was my task - to find potential improvements that can be made to make this codebase be much faster.&lt;/p&gt;&lt;p&gt;After departing from Supabase I liked the project so much (mostly as learning ground) that I have created my own fork, where unrestrained from all business side of the project I could focus purely on squeezing as much of performance as I can. This project now lives as &lt;a href=&quot;https://github.com/Ultravisor/ultravisor&quot;&gt;Ultravisor&lt;/a&gt; - it is still nowhere near being done in a way that I like, but I still go back to work on it from time to time to find potential performance improvements.&lt;/p&gt;&lt;p&gt;This is a story of things that I have done and learned during that journey.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Beware&lt;/strong&gt;: It is a retrospection, so in some places my memory may be not the best.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;Here I need to provide some explanation first, about how Ultravisor works with database connections. It provides 2 modes of operation:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;code&gt;session&lt;/code&gt; - where each connection from user to Ultravisor checks out one connection from Ultravisor to database. It checks out once, at the start of connection, and then holds connection until the end;&lt;/li&gt;&lt;li&gt;&lt;code&gt;transaction&lt;/code&gt; - where on connection there is nothing done. Client connects to the Ultravisor and can keep that connection indefinitely without ever bothering database. Database connection is checked out &lt;em&gt;only&lt;/em&gt; when there is some request from user and is returned to the pool as soon as that result of that query is returned and DB is ready for next one.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;While &lt;code&gt;session&lt;/code&gt; mode is quite on par with other implementations of connection pooling for Postgres, &lt;code&gt;transaction&lt;/code&gt; mode is where performance is lacking and is the main focus is put. In whole article (unless mentioned otherwise) I will speak about &lt;code&gt;transaction&lt;/code&gt; mode of Ultravisor.&lt;/p&gt;&lt;h2&gt;Lesson: Flame graphs and call tracing is essential#&lt;/h2&gt;&lt;p&gt;Pretty obvious thing, but still valuable lesson for any performance optimisation endeavour. For that the great thanks to &lt;a href=&quot;https://github.com/Stratus3D&quot;&gt;Trevor Brown&lt;/a&gt; and his awesome project &lt;a href=&quot;https://github.com/Stratus3D/eflambe&quot;&gt;eFlambè&lt;/a&gt;. This helped a lot in tracing hot points in the running code.&lt;/p&gt;&lt;p&gt;Unfortunately this project seems to be less active recently and has some missing features, like &lt;a href=&quot;https://github.com/Stratus3D/eflambe/issues/48&quot;&gt;listening for given duration instead of function calls count&lt;/a&gt;. This can be partially fixed by simply listening for count of calls to &lt;code&gt;handle_event/4&lt;/code&gt; function given times and then running &lt;code&gt;cat *.bggg&lt;/code&gt; to concatenate all files into larger trace. That has disadvantages, but at least it was workable within &lt;a href=&quot;https://speedoscope.app&quot;&gt;Speedoscope&lt;/a&gt; which I also highly recommend to anyone who needs to work on such optimisation.&lt;/p&gt;&lt;p&gt;While flame graphs are awesome, there is cost to gathering them with eFlambè - it greatly affects performance. Fortunately Erlang has some built in tools that have lesser performance impact, and the &amp;quot;most modern&amp;quot; of these is &lt;a href=&quot;https://erlang.org/doc/man/tprof.html&quot;&gt;&lt;code&gt;tprof&lt;/code&gt;&lt;/a&gt;. This tool is pretty easy to use, but is less detailed than eFlambè. But even with that limitation, it provides superb insight into stuff that has greatest impact on performance, as well as it make it easier to work on long running processes, as it work asynchronously, so you can &amp;quot;manually&amp;quot; decide how long you want to trace your process.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Summary:&lt;/strong&gt; Knowing where your bottlenecks are is essential for performance optimisations.&lt;/p&gt;&lt;h2&gt;Lesson: Doing less can improve performance#&lt;/h2&gt;&lt;p&gt;Obvious thing that need to be stated - doing nothing is faster than doing something. Extracting amount of data sent over given socket using &lt;code&gt;:inet.getstat/2&lt;/code&gt; call is fast, but not free. That involves some waiting for response from either port or process handling connection, which introduces slowdown. Two possible solutions there are:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Do not gather that metric at all - sensible, but not feasible, especially when you use that metric to charge your users.&lt;/li&gt;&lt;li&gt;Gather that data less often.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;The approach I have taken there is obviously 2., and the solution is dumb simple - debouncer.&lt;/p&gt;&lt;p&gt;Debouncing is an interesting technique often used in user interfaces where you accept some event, and then for some period you ignore repeated events. The reason for that is that our interfaces may have flaws that send repeated events one after another.&lt;/p&gt;&lt;p&gt;In this case Ultravisor tries to store amount of sent data after each query, but that can get expensive for many short queries. Instead I have implemented simple per-process debouncer:&lt;/p&gt;&lt;p&gt;This stores returned data in process dictionary (per-process mutable space with quick access) and if there was no call in given time-period, then we process data again. This is safe way to do so, as &lt;code&gt;:inet.getstat/2&lt;/code&gt; will always return amount of data that socket processed since it started, so data between calls will be accounted.&lt;/p&gt;&lt;p&gt;Before:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;tps = 79401.392762 (without initial connection time)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;After (10ms of debouncing):&lt;/p&gt;&lt;pre&gt;&lt;code&gt;tps = 80069.646510 (without initial connection time)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;After (100ms of debouncing):&lt;/p&gt;&lt;pre&gt;&lt;code&gt;tps = 80568.825937 (without initial connection time)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;Summary&lt;/strong&gt;: Doing noting is more performant than doing something. Sometimes doing nothing can be quite easy.&lt;/p&gt;&lt;h2&gt;Lesson: Telemetry is not free#&lt;/h2&gt;&lt;p&gt;When working on most projects, especially Phoenix-based, one can slap &lt;code&gt;:telemetry.execute/3&lt;/code&gt; calls everywhere and notice no performance degradation&lt;sup&gt;1&lt;/sup&gt;. Unfortunately, when you do hundreds of thousands calls a second - that is not a case.&lt;/p&gt;&lt;div&gt;&lt;sup&gt;1&lt;/sup&gt;&lt;p&gt;For unaware readers - &lt;a href=&quot;https://github.com/beam-telemetry/telemetry&quot;&gt;Telemetry&lt;/a&gt; is Erlang event dispatching system for observability events.&lt;/p&gt;&lt;/div&gt;&lt;p&gt;In this project the metrics are exposed in Prometheus/OpenMetrics format, which means that there needs to be collection system within the application. In BEAM applications the standard way to implement that is to use ETS tables to store recorded values. Fortunately there are libraries to handle that for you, and for the longest time &amp;quot;gold standard&amp;quot; for it was &lt;code&gt;telemetry_prometheus_core&lt;/code&gt; library created by Telemetry core team.&lt;/p&gt;&lt;p&gt;While for most projects that library is performant enough (because metrics aren&amp;#39;t recorded in quite tight loops), in case of this project that was not a case. There, metrics gathering is still one of the hottest spot in the codebase, even with all improvements that have been done.&lt;/p&gt;&lt;p&gt;Excerpt from &lt;code&gt;tprof&lt;/code&gt; profile:&lt;/p&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Function&lt;/th&gt;&lt;th&gt;Calls count&lt;/th&gt;&lt;th&gt;Per call (μs)&lt;/th&gt;&lt;th&gt;Percentage&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;Peep.EventHandler.store_metrics/5&lt;/code&gt;&lt;/td&gt;&lt;td&gt;1421911&lt;/td&gt;&lt;td&gt;0.13&lt;/td&gt;&lt;td&gt;4.21%&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;Peep.Storage.Striped.insert_metric/5&lt;/code&gt;&lt;/td&gt;&lt;td&gt;904852&lt;/td&gt;&lt;td&gt;0.26&lt;/td&gt;&lt;td&gt;5.11%&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;This is with awesome library &lt;a href=&quot;https://github.com/rkallos/peep&quot;&gt;Peep&lt;/a&gt; by &lt;a href=&quot;https://github.com/rkallos&quot;&gt;Richard Kallos&lt;/a&gt;. When using &lt;code&gt;telemetry_prometheus_core&lt;/code&gt; it was simply the most expensive thing in whole loop. Just replacing metrics gathering library with Peep gave us about 2x bump in TPS.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Summary:&lt;/strong&gt; Telemetry handler can matter in tight loops. Fast metrics gathering isn&amp;#39;t easy.&lt;/p&gt;&lt;h2&gt;Lesson: Records instead of maps or structs#&lt;/h2&gt;&lt;p&gt;Elixir uses structs for structured data. That gives a lot nice features wrt. hot code reloads, compilation graph dependencies, and other. However, because structs are maps, there is a cost. Maps have O(log n) access time to the fields, this is how maps are constructed in memory. While smaller maps have slightly different (better in most cases) characteristics, there is strict requirement that you keep your structure with less than 31 fields&lt;sup&gt;2&lt;/sup&gt; and it still has slight memory overhead. The alternative is to use &lt;a href=&quot;https://hexdocs.pm/elixir/Record.html&quot;&gt;records&lt;/a&gt;. These have better performance characteristic (always constant) irrelevant of the amount of fields at the cost of being slightly more rigid (records are tuple based) and less convenient to use (experience may vary). Additional advantage in my opinion is that it is harder to add incorrect field by using &lt;code&gt;Map&lt;/code&gt; module.&lt;/p&gt;&lt;div&gt;&lt;sup&gt;2&lt;/sup&gt;&lt;p&gt;Current (OTP 28) limit for small map is 32 keys, but Elixir uses one key for struct name, hence 31 fields is the limit.&lt;/p&gt;&lt;/div&gt;&lt;p&gt;Before you will run and change all structs in your system to records, just remember - most of the time the difference doesn&amp;#39;t matter - just use structures.&lt;/p&gt;&lt;p&gt;Before:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;tps = 81765.266264 (without initial connection time)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;After:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;tps = 82147.855889 (without initial connection time)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;Summary:&lt;/strong&gt; &lt;code&gt;Record&lt;/code&gt;s are super handy when you need to squeeze each bit of performance. It doesn&amp;#39;t provide much, but these adds up.&lt;/p&gt;&lt;h2&gt;Lesson: ETS tables are super fast, but not always#&lt;/h2&gt;&lt;p&gt;&lt;a href=&quot;https://www.erlang.org/doc/apps/stdlib/ets.html&quot;&gt;ETS&lt;/a&gt; is Erlang&amp;#39;s built-in module for storing key-value data in mutable way. Like built-in Redis. This structure allows for sharing some data in a way, that is easy to access from different parts of the system. One example of system that is using ETS for storing their information is Telemetry (mentioned above).&lt;/p&gt;&lt;p&gt;While for 99% of the use cases Telemetry will be fast enough, it has some problems with tight loops. Main problem is that it will always copy data from table to the caller process. That mean that it can put high memory pressure on the process that tries to retrieve data.&lt;/p&gt;&lt;p&gt;Fortunately Erlang supports another mechanism for storing globally accessible data - &lt;code&gt;persistent_term&lt;/code&gt;. Of course, there is no such thing as &amp;quot;free lunch&amp;quot; so it has substantial disadvantage - it works poorly&lt;sup&gt;3&lt;/sup&gt; with data that changes often, as removing or changing data in a key will require walk through all processes to copy data from it to processes that may use it into process memory. However - Telemetry handlers should not change a lot, you should just set them once as soon as your system start, and then ideally they will not change ever again.&lt;/p&gt;&lt;div&gt;&lt;sup&gt;3&lt;/sup&gt;&lt;p&gt;There is slight optimisation that makes it fast in some cases (single word values, like atoms), but that is not the case there, so we can ignore that.&lt;/p&gt;&lt;/div&gt;&lt;p&gt;Before&lt;sup&gt;4&lt;/sup&gt;:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;tps = 76914.004685 (without initial connection time)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;After:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;tps = 78479.006634 (without initial connection time)&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;sup&gt;4&lt;/sup&gt;&lt;p&gt;If you wonder why these results are lower than in previous section, it is because test conditions are identical only per section, not cross sections. In this particular case I have ran benchmark while collecting metrics (to show difference in &lt;code&gt;persistent_term&lt;/code&gt; change) while other are ran without metrics to not pollute results.&lt;/p&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Summary:&lt;/strong&gt; &lt;code&gt;persistent_term&lt;/code&gt; is awesome and super fast, so if you know that you have some data that will probably never change and will be requested &lt;em&gt;constantly&lt;/em&gt;, then it may be good place to store that data.&lt;/p&gt;&lt;h2&gt;Lesson: Calling your &lt;code&gt;GenServer&lt;/code&gt;s is fast, but not 90k times per second fast#&lt;/h2&gt;&lt;p&gt;One of the interesting observations that I have spotted is that if there are longer running queries, ones that send more data over the network than just simple short responses, then the difference between Ultravisor and &amp;quot;state of the art&amp;quot; tools like &lt;a href=&quot;https://www.pgbouncer.org&quot;&gt;PgBouncer&lt;/a&gt; or &lt;a href=&quot;https://pgdog.dev&quot;&gt;PgDog&lt;/a&gt; (that are written in non-managed languages like C and Rust) is much smaller (obviously it is still there, but it is on par, not substantially off).&lt;/p&gt;&lt;p&gt;I needed to dig more, what can be the cause of such strange behaviour. The reason was found in place where I least expected it - checking out database connection to be used.&lt;/p&gt;&lt;p&gt;Flame graph showed that almost third of the time is spent on checking out database connections, and most of that time is spent in 2 function calls, both of them are internally &lt;code&gt;gen_statem&lt;/code&gt; calls and in both most time is spent on sleeping (aka, waiting for reply).&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://hauleth.dev/post/things-about-elixir-you-probably-will-never-need/sleep.png&quot; alt=&quot;Image showing left-heavy flamegraph of the profiled application&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;Now, this one is hard thing to optimise, as in Elixir there is no mutability (almost, we will get there). This mean that if I want some form of shared queue of processes, then I need to use separate process to keep state of the queue for us, and then do &lt;code&gt;GenServer&lt;/code&gt; calls to fetch that state. What I did in such situation? What any unreasonable Elixir developer obsessed with performance would do - NIF&lt;sup&gt;5&lt;/sup&gt;.&lt;/p&gt;&lt;div&gt;&lt;sup&gt;5&lt;/sup&gt;&lt;p&gt;I wanted to use ETS there, but for that to work it lacks function like &lt;code&gt;ets:take/2&lt;/code&gt; that would return only one element from the tables with type &lt;code&gt;bag&lt;/code&gt; or &lt;code&gt;duplicate_bag&lt;/code&gt;. Or any other form of just taking out any (possibly random) element from ETS table in atomic way.&lt;/p&gt;&lt;/div&gt;&lt;p&gt;The implementation is rather basic wrapper over &lt;a href=&quot;https://doc.rust-lang.org/1.94.0/std/collections/struct.VecDeque.html&quot;&gt;&lt;code&gt;VecDeque&lt;/code&gt;s&lt;/a&gt; that allow popping single element from that queue without any message passing. The implementation is very crude, nowhere production ready. It doesn&amp;#39;t provide any form of worker restarts or anything, but works quite well as PoC of what is possible.&lt;/p&gt;&lt;p&gt;New queue also provides a way to store additional &amp;quot;metadata&amp;quot; alongside the worker PID. This allows me to store DB connection socket next to connection process, which removes need for additional call to extract that data to pass requests directly to other DB, without copying data between processes.&lt;/p&gt;&lt;p&gt;Before:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;tps = 83619.640673 (without initial connection time)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;After:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;tps = 94191.475386 (without initial connection time)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;Summary:&lt;/strong&gt; Sometimes one need to get creative to get around platform limitations. This may require some pesky NIFs though.&lt;/p&gt;&lt;h2&gt;Conclusions#&lt;/h2&gt;&lt;p&gt;Optimising such project was enormous fun and I think that at the current state there is nothing extra that can be done to optimise it more without optimising generated JIT-ed native code or optimising Erlang scheduler.&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://hauleth.dev/post/things-about-elixir-you-probably-will-never-need/final.png&quot; alt=&quot;Final flamegraph of application&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;There are some flags, that affect performance, but as it is currently unclear why these work at all (probably it is related more to the OS scheduler rather than Erlang performance), I left them out of this article for now.&lt;/p&gt;&lt;h2&gt;Post Scriptum: Good tooling helps a lot#&lt;/h2&gt;&lt;p&gt;Just after I have started that optimisation project after leaving Supabase I started using &lt;a href=&quot;https://docs.jj-vcs.dev/latest/&quot;&gt;Jujutsu&lt;/a&gt; for version control. That one thing helped me &lt;strong&gt;a lot&lt;/strong&gt; with being able to have separate branches/PRs for each of the changes, while at the same being able to work with &lt;a href=&quot;https://steveklabnik.github.io/jujutsu-tutorial/advanced/simultaneous-edits.html&quot;&gt;mega-merge&lt;/a&gt; of them all.&lt;/p&gt;&lt;p&gt;That allows me to profile code with all other noise removed, while still exposing the changes as separate reviewable units. Without that support I would need to decipher what have already been changed and/or removed from the profile.&lt;/p&gt;&lt;p&gt;Additional feature that I heavily used there is &amp;quot;anonymous branching&amp;quot;. As when working with JJ I do not need to create new name for each branch that I want to try, it was way easier to implement one idea, then just do &lt;code&gt;jj new @-&lt;/code&gt; (which branches off at the commit that is parent of the current one) and just implement alternative idea. I used that constantly to compare ideas and reject failed concepts.&lt;/p&gt;&lt;div&gt;∎&lt;/div&gt;</content:encoded>
</item>
<item>
<title>Resist Vendor Lock-In With Supabase</title>
<link>https://www.thestackcanary.com/supasafe-supabase/</link>
<guid isPermaLink="false">Se4vQzxkzzKERy-Gt6r5mjM4TxDJj9Dpcg6PoA==</guid>
<pubDate>Sun, 19 Apr 2026 04:26:32 +0000</pubDate>
<description>Not only is Supabase the open-source Firebase alternative, it just might be safer too.</description>
<content:encoded>&lt;img src=&quot;https://www.thestackcanary.com/content/images/2024/04/Dark-Blue-Virtual-Tech-Banner--1-.png&quot; alt=&quot;Resist Vendor Lock-In With Supabase&quot; title=&quot;&quot;/&gt;&lt;p&gt;In one of his recent YouTube videos, &lt;a href=&quot;https://twitter.com/t3dotgg?ref=thestackcanary.com&quot;&gt;Theo Browne&lt;/a&gt; highlights the potential pitfalls that comes with using a Platform-as-a-Service. &lt;/p&gt;&lt;p&gt;The video goes over recent work done to fuzz suspected Firebase-backed services looking for leaked credentials. As is shown in the video, this is an all-too common pattern in these platforms, where the &amp;quot;backend&amp;quot; is being directly manipulated from client-side SDKs, such that theres a high likelihood that credentials are carelessly exposed to the client. In addition to potential secret leakage, many of these platforms, including Firestore, do not have great default security settings.  This has obvious MAJOR security concerns is a horrible practice for the industry to standardize on, but yet these platforms still remain extremely popular despite known security concerns due to their ease of use and how they enable rapid development. &lt;/p&gt;&lt;p&gt;In addition to these security concerns, Firebase has long drawn the ire of many developers due to its tight coupling with Google&amp;#39;s proprietary ecosystem, and its cost structure which has lead to more than a few horror stories about unexpectedly high bills (although these issues can often be attributed to poor database practices that lead to exponentially ballooning costs). With much apprehension towards Firebase, many looked for alternatives, and found themselves needing to piece together a solution from many different parts. Here enters &lt;a href=&quot;https://supabase.com/?ref=thestackcanary.com&quot;&gt;Supabase&lt;/a&gt;, which pitches itself as the &amp;quot;open-source Firebase alternative.&amp;quot;  Supabase offers similar solutions as Firebase, such as a realtime database, object storage, user authentication and management, edge functions, and much more.  Supabase, however, is built entirely from open-source software (in fact, they stipulate that &lt;a href=&quot;https://github.com/supabase/supabase?tab=readme-ov-file&amp;amp;ref=thestackcanary.com#how-it-works&quot;&gt;Supabase will only ever include software that comes with an MIT, Apache 2, or equivalent license&lt;/a&gt;).&lt;/p&gt;&lt;p&gt; When asked about his thoughts on Supabase, Theo rightly points out that these problems still exist when using their client SDKs, but he stresses that unlike Firebase, since Supabase is built entirely on open-source software (namely &lt;a href=&quot;https://www.postgresql.org/?ref=thestackcanary.com&quot;&gt;PostgreSQL&lt;/a&gt;) you not only can connect directly to the database, but it is actively encouraged by the CEO himself.&lt;/p&gt;&lt;figure&gt;&lt;div&gt;&lt;blockquote&gt;&lt;p&gt;cc &lt;a href=&quot;https://twitter.com/chasers?ref_src=twsrc%5Etfw&amp;amp;ref=thestackcanary.com&quot;&gt;@chasers&lt;/a&gt;&lt;a href=&quot;https://twitter.com/filipecabaco?ref_src=twsrc%5Etfw&amp;amp;ref=thestackcanary.com&quot;&gt;@filipecabaco&lt;/a&gt;&lt;a href=&quot;https://twitter.com/wenboxie?ref_src=twsrc%5Etfw&amp;amp;ref=thestackcanary.com&quot;&gt;@wenboxie&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;Alternatively: just connect to Postgres&lt;br/&gt;¯\_(ツ)_/¯&lt;/p&gt;— Paul Copplestone — e/postgres (@kiwicopple) &lt;a href=&quot;https://twitter.com/kiwicopple/status/1781161242637602896?ref_src=twsrc%5Etfw&amp;amp;ref=thestackcanary.com&quot;&gt;April 19, 2024&lt;/a&gt;&lt;/blockquote&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;Wow! What a breath of fresh air that is! And as it turns out, even when &amp;quot;just&amp;quot; using Supabase in this way, it is still great! You get an authentication table with Row-Level-Security and robust security practices out of the box, access to their wonderful front-end database management, and the ability to easily integrate their other features such as &lt;a href=&quot;https://supabase.com/edge-functions?ref=thestackcanary.com&quot;&gt;Edge Functions&lt;/a&gt;, &lt;a href=&quot;https://supabase.com/storage?ref=thestackcanary.com&quot;&gt;Object Storage&lt;/a&gt;, all while maintaining tight control over your backend. It just makes sense! It offers the rapid development experience that is all too valuable, while not compromising on security. &lt;/p&gt;&lt;p&gt;Still, you might be wondering what this looks like from the developer perspective. Surely if you&amp;#39;re writing directly to the database this must be more unwieldily than using those client SDKs right? Well, not necessarily! &lt;/p&gt;&lt;p&gt;Supabase still offer &lt;a href=&quot;https://postgrest.org/en/v12/?ref=thestackcanary.com&quot;&gt;PostgREST&lt;/a&gt; out of the box, which exposes a REST API for database operations, but you have control over where you fire off those requests from. And since you have complete control over your database, you can even do a hybrid approach, which is what I prefer. Since Supabase takes care of the &lt;code&gt;auth.users&lt;/code&gt; table (and even makes it read-only through the web frontend), you might want to just use the REST APIs they expose to handle authentication, but then handle everything else yourself.  &lt;/p&gt;&lt;div&gt;&lt;div&gt;💡&lt;/div&gt;&lt;div&gt;If you want to have a table NOT be exposed as an API through PostgREST, just add it to a different schema from the public schema. All tables under the public schema will be exposed.&lt;/div&gt;&lt;/div&gt;&lt;p&gt;Even though you might want Supabase to handle authentication, you still might want to extend features from that &lt;code&gt;users.auth&lt;/code&gt; table. Since they discourage modifying that table directly, the suggested approach is to make a table in the &lt;code&gt;public&lt;/code&gt; schema (which is the default schema – the &lt;code&gt;auth&lt;/code&gt; schema is managed by Supabase) to manage any additional information related to the user. The documentation goes into much more detail about &lt;a href=&quot;https://supabase.com/docs/guides/auth/managing-user-data?ref=thestackcanary.com#creating-user-tables&quot;&gt;Managing User Data&lt;/a&gt;, but that is the gist of the situation. Well, if you want this new table (let&amp;#39;s call it &lt;code&gt;Profiles&lt;/code&gt;) to stay in sync with the &lt;code&gt;auth.users&lt;/code&gt; table, it&amp;#39;s best to have the &lt;code&gt;Profiles&lt;/code&gt; table refer to the &lt;code&gt;auth.users&lt;/code&gt; table, and add a Postgres trigger to run a function to create a new entry in the &lt;code&gt;Profiles&lt;/code&gt; table whenever a new user is registered. &lt;/p&gt;&lt;div&gt;&lt;div&gt;💡&lt;/div&gt;&lt;div&gt;It&amp;#39;s also worth noting that in their recent General Availability Launch Week, Supabase also announced new efforts to increase security practices across projects, including a Postgres Linter and new Security Advisor and Performance Advisor dashboards to help you maintain good security posture. &lt;/div&gt;&lt;/div&gt;&lt;figure&gt;&lt;a href=&quot;https://github.com/supabase/splinter?ref=thestackcanary.com&quot;&gt;&lt;div&gt;&lt;div&gt;GitHub - supabase/splinter: Supabase Postgres Linter&lt;/div&gt;&lt;div&gt;Supabase Postgres Linter. Contribute to supabase/splinter development by creating an account on GitHub.&lt;/div&gt;&lt;div&gt;&lt;img src=&quot;https://github.githubassets.com/assets/pinned-octocat-093da3e6fa40.svg&quot; alt=&quot;Resist Vendor Lock-In With Supabase&quot; title=&quot;&quot;/&gt;&lt;span&gt;GitHub&lt;/span&gt;&lt;span&gt;supabase&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;img src=&quot;https://opengraph.githubassets.com/966032ded7ca25ff60a2b6d2cb729f96ee103f143195ce1c3991b19e2032b39e/supabase/splinter&quot; alt=&quot;Resist Vendor Lock-In With Supabase&quot; title=&quot;&quot;/&gt;&lt;/div&gt;&lt;/a&gt;&lt;/figure&gt;&lt;figure&gt;&lt;a href=&quot;https://supabase.com/blog/security-performance-advisor?ref=thestackcanary.com&quot;&gt;&lt;div&gt;&lt;div&gt;Supabase Security Advisor &amp;amp; Performance Advisor&lt;/div&gt;&lt;div&gt;We’re making it easier to build a secure and high-performing application.&lt;/div&gt;&lt;div&gt;&lt;img src=&quot;https://supabase.com/favicon/mstile-310x310.png&quot; alt=&quot;Resist Vendor Lock-In With Supabase&quot; title=&quot;&quot;/&gt;&lt;span&gt;Supabase&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;img src=&quot;https://supabase.com/images/blog/ga-week/security-peformance-advisor/og.png?v=3&quot; alt=&quot;Resist Vendor Lock-In With Supabase&quot; title=&quot;&quot;/&gt;&lt;/div&gt;&lt;/a&gt;&lt;/figure&gt;&lt;p&gt;Let&amp;#39;s walk through an example of what this might look like using my favorite backend language, &lt;a href=&quot;https://elixir-lang.org/?ref=thestackcanary.com&quot;&gt;Elixir&lt;/a&gt; (it&amp;#39;s also a favorite of Supabase themselves).  I&amp;#39;ll be using Elixir&amp;#39;s very nice ORM, &lt;a href=&quot;https://hexdocs.pm/ecto/Ecto.html?ref=thestackcanary.com&quot;&gt;Ecto&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;One you make your new project (using &lt;code&gt;mix new&lt;/code&gt; or perhaps more commonly in this situation, &lt;code&gt;mix phx.new&lt;/code&gt;) you&amp;#39;ll want to make sure you have Ecto as a dependency, and then you&amp;#39;ll want to model the &lt;code&gt;auth.users&lt;/code&gt; table as well as the new &lt;code&gt;public.profiles&lt;/code&gt; table. &lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-elixir&quot;&gt;defmodule User do
  @moduledoc &amp;quot;&amp;quot;&amp;quot;
  This schema represents the default Supabase users table, which is under the &amp;#39;auth&amp;#39; schema.

  Since we don&amp;#39;t actually manage this schema, we will not make any migrations for it.

  This is mainly for convenience when unmarshalling data and working with users, so we
  can refer to the User struct rather than a generic map.

  Notice that we specify the primary key which will be referred to later

  Ecto schemas do not have to have 1-to-1 fields match the table in the database, so we can use whatever minimal fields we want to mirror in the profiles table (`id` at a minimum).
  &amp;quot;&amp;quot;&amp;quot;
  use Ecto.Schema
  import Ecto.Changeset

  # id is a UUID
  @primary_key {:id, :binary_id, autogenerate: false}
  schema &amp;quot;auth.users&amp;quot; do
    field :created_at, :naive_datetime_usec
    field :updated_at, :naive_datetime_usec
  end

end&lt;/code&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code class=&quot;language-elixir&quot;&gt;defmodule Profile do
  @moduledoc &amp;quot;&amp;quot;&amp;quot;
  This schema holds extra information about users
  &amp;quot;&amp;quot;&amp;quot;
  use Ecto.Schema
  import Ecto.Changeset

  schema &amp;quot;profiles&amp;quot; do
    field :first_name, :string
    field :last_name, :string

    embeds_one :settings, Settings do
      field :default_portfolio, :string
      field :theme, Ecto.Enum, values: [dark: &amp;quot;Dark&amp;quot;, light: &amp;quot;Light&amp;quot;, system: &amp;quot;System&amp;quot;]
    end

    # This is the most important line and the one that is required to
    # properly link this table to `auth.users`
    # Make sure to set the type to :binary_id, which is what the Supabase 
    # auth uses
    belongs_to :user, User, type: :binary_id, references: :id, primary_key: true

    timestamps()
  end
end&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now we would create the necessary migrations, starting with creating the &lt;code&gt;public.profiles&lt;/code&gt; table.&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-elixir&quot;&gt;defmodule CreateProfiles do
  use Ecto.Migration

  def change do
    create table(:profiles, primary_key: false) do
      # notice the `prefix` since `public` is the default prefix
      # notice the specifying the type to match the Supabase defaults
      # make sure to set this as the primary key
      add :id, references(:users, on_delete: :delete_all, prefix: &amp;quot;auth&amp;quot;, type: :uuid),
        primary_key: true

      # These fields should match what you have in your schema
      add :first_name, :string
      add :last_name, :string
      add :settings, :map

      # This represents the `inserted_at` and `updated_at` fields in the 
      # schema, and are required by default
      timestamps()
    end

    # You might also want to add indexes to improve performance and ensure data integrity
    create index(:profiles, [:id])
  end
end&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now we add a migration to add the trigger:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-elixir&quot;&gt;defmodule CreateProfilesTrigger do
  use Ecto.Migration

  def up do
    # Function to insert a new profile
    execute &amp;quot;&amp;quot;&amp;quot;
    CREATE OR REPLACE FUNCTION public.create_profile_for_new_user()
    RETURNS TRIGGER AS $$
    BEGIN
      INSERT INTO public.profiles (id, inserted_at, updated_at)
      VALUES (NEW.id, now(), now());
      RETURN NEW;
    END;
    $$ LANGUAGE plpgsql SECURITY DEFINER;
    &amp;quot;&amp;quot;&amp;quot;

    # Trigger to call the function after a user is inserted
    execute &amp;quot;&amp;quot;&amp;quot;
    CREATE TRIGGER trigger_create_profile_after_user_insert
    AFTER INSERT ON auth.users
    FOR EACH ROW
    EXECUTE FUNCTION public.create_profile_for_new_user();
    &amp;quot;&amp;quot;&amp;quot;
  end

  def down do
    execute &amp;quot;DROP TRIGGER IF EXISTS trigger_create_profile_after_user_insert ON auth.users;&amp;quot;
    execute &amp;quot;DROP FUNCTION IF EXISTS create_profile_for_new_user;&amp;quot;
  end
end&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And that&amp;#39;s it, you now have extended the ability to store information for the users while keeping the security provided by the protected &lt;code&gt;auth.users&lt;/code&gt; table. But now how do you use it?&lt;/p&gt;&lt;p&gt;Well, let me show you how in fewer than 90 lines of code:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-elixir&quot;&gt;defmodule UserManagement do
  @req Req.new(
         base_url: Application.compile_env(:myapp, [:supabase, :base_url]),
         headers: [apikey: Application.compile_env(:myapp, [:supabase, :api_key])],
         url: &amp;quot;/auth/v1/:action&amp;quot;
       )

  def get_current_user(bearer_token) do
    Req.get!(
      @req,
      auth: {:bearer, bearer_token},
      path_params: [action: &amp;quot;user&amp;quot;]
    )
    |&amp;gt; Map.get(:body)
  end

  def signup_with_username_and_password(email, password) do
    Req.post!(
      @req,
      path_params: [action: &amp;quot;signup&amp;quot;],
      json: %{email: email, password: password}
    )
    |&amp;gt; Map.get(:body)
  end

  def login_with_email_and_password(email, password) do
    Req.post!(
      @req,
      path_params: [action: &amp;quot;token&amp;quot;],
      params: [grant_type: &amp;quot;password&amp;quot;],
      json: %{email: email, password: password}
    )
    |&amp;gt; Map.get(:body)
  end

  def send_password_recovery_email(email) do
    Req.post!(
      @req,
      path_params: [action: &amp;quot;recover&amp;quot;],
      json: %{email: email}
    )
    |&amp;gt; Map.get(:body)
  end

  def update_user(bearer_token, data \\ %{}) do
    {email, data} = Map.pop(data, &amp;quot;email&amp;quot;)
    {password, data} = Map.pop(data, &amp;quot;password&amp;quot;)

    body = %{
      &amp;quot;data&amp;quot; =&amp;gt; data
    }

    body = if email, do: Map.put(body, &amp;quot;email&amp;quot;, email), else: body
    body = if password, do: Map.put(body, &amp;quot;password&amp;quot;, password), else: body

    Req.put!(
      @req,
      path_params: [action: &amp;quot;user&amp;quot;],
      auth: {:bearer, bearer_token},
      json: body
    )
    |&amp;gt; Map.get(:body)
  end

  def logout(bearer_token) do
    Req.post!(
      @req,
      auth: {:bearer, bearer_token},
      path_params: [action: &amp;quot;logout&amp;quot;]
    )
    |&amp;gt; Map.get(:body)
  end

  def send_email_invite(bearer_token, email) do
    Req.post!(
      @req,
      auth: {:bearer, bearer_token},
      json: %{email: email},
      path_params: [action: &amp;quot;invite&amp;quot;]
    )
    |&amp;gt; Map.get(:body)
  end
end&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Pretty simple right? You could surely condense this more too if you so choose. &lt;/p&gt;&lt;div&gt;&lt;div&gt;💡&lt;/div&gt;&lt;div&gt;The wonderful &lt;a href=&quot;https://hexdocs.pm/req/readme.html?ref=thestackcanary.com&quot;&gt;Req&lt;/a&gt; library takes care of a lot of the tedious parts of these requests, such as JSON-encoding and Bearer authentication. I highly recommend it over other HTTP clients for these reasons. &lt;/div&gt;&lt;/div&gt;&lt;p&gt;As long as you set your API key and Supabase instance URL into the application environment, this will have you ready to perform all of your user management tasks, and upon registration have the new user reflected in the &lt;code&gt;auth.users&lt;/code&gt; table as well as the &lt;code&gt;public.profiles&lt;/code&gt; table. &lt;/p&gt;&lt;p&gt;And lastly, make sure you connect to your database using the connection string (or you can specify each field if you&amp;#39;d like).&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-elixir&quot;&gt;import Config

config :myapp, MyApp.Repo,
  url:
    &amp;quot;myconnectionstring&amp;quot;,&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This barely scratched the surface of what you can do with Supabase, but I hope it at least demonstrates how quick it is to get started with it and how flexible it is to have complete control over your stack. &lt;/p&gt;&lt;p&gt;Of course this example was in Elixir, but you could extend this to any other backend language and get the same benefits, and avoid falling victim to vendor lock-in! &lt;/p&gt;</content:encoded>
</item>
<item>
<title>Elevate Your Elixir With Sigils</title>
<link>https://www.thestackcanary.com/elevate-your-elixir-with-sigils/</link>
<guid isPermaLink="false">BjZvqJdgixq9fEOBlRwQGOfD0wLBimvMMXCzfw==</guid>
<pubDate>Sun, 19 Apr 2026 04:26:32 +0000</pubDate>
<description>Adding support for real-valued intervals implementing the Enumerable Protocol in Elixir.</description>
<content:encoded>&lt;h2&gt;Motivation&lt;/h2&gt;&lt;img src=&quot;https://www.thestackcanary.com/content/images/2024/01/Elevate_Elxiir_With_Sigils.png&quot; alt=&quot;Elevate Your Elixir With Sigils&quot; title=&quot;&quot;/&gt;&lt;p&gt;In a &lt;a href=&quot;https://www.thestackcanary.com/elixir-nimble-options/&quot;&gt;previous article&lt;/a&gt; of mine, I wrote about using &lt;a href=&quot;https://github.com/dashbitco/nimble_options?ref=thestackcanary.com&quot;&gt;NimbleOptions&lt;/a&gt; to add extremely powerful option handling to your Elixir applications.  One of the custom validations I was using was a function  &lt;code&gt;in_range&lt;/code&gt; that would check if an option fell within a real-valued interval. This differs from Elixir&amp;#39;s built-in &lt;code&gt;Range&lt;/code&gt; in that it needed to be real-valued (rather than discrete integer steps). Additionally, mostly due to aesthetic and personal opinion, I wanted to be able to express the intervals using mathematical notation such as &lt;code&gt;(0,1]&lt;/code&gt; to mean &amp;quot;allow any value greater than 0 and less than or equal to 1&amp;quot;. I find Elixir to be such a beautiful language with a unique capacity for extensions that it felt wrong to use a function such as &lt;code&gt;in_range&lt;/code&gt; or &lt;code&gt;in_interval&lt;/code&gt;. Additionally, some implementations I&amp;#39;ve come across have somewhat unintuitive APIs, such as the following spec:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-elixir&quot;&gt;@spec in_range(float, float, float, bool, bool) :: bool
@doc &amp;quot;&amp;quot;&amp;quot;
  * `:value` - Value to test for inclusion
  * `:min` - Minimum value in range
  * `:max` - Maximum value in range
  * `:left` - Whether the left boundary is inclusive (true) or exclusive (false)
  * `:right` - Whether the right boundary is inclusive (true) or exclusive (false)
&amp;quot;&amp;quot;&amp;quot;
def in_range(value, min \\ 0, max \\ 1, left \\ true, right \\ true)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;There&amp;#39;s nothing expressly wrong with this implementation, but with my use cases and as Elixir is being used more in the domain of Machine Learning which deals with these intervals quite often, I wanted a solution that felt a bit more integrated.&lt;/p&gt;&lt;h2&gt;Solution&lt;/h2&gt;&lt;p&gt;This led me to create a small 1-file library called &lt;a href=&quot;https://github.com/acalejos/exterval?ref=thestackcanary.com&quot;&gt;&lt;code&gt;Exterval&lt;/code&gt;&lt;/a&gt; which is available on &lt;a href=&quot;https://hex.pm/packages/exterval?ref=thestackcanary.com&quot;&gt;Hex&lt;/a&gt; and can be installed with:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-elixir&quot;&gt;def deps do
[
  {:exterval, &amp;quot;~&amp;gt; 0.1.0&amp;quot;}
]
end&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;To make the interval feel more native to Elixir, I implemented it as a sigil that implements the&lt;a href=&quot;https://hexdocs.pm/elixir/1.12/Enumerable.html?ref=thestackcanary.com&quot;&gt;&lt;code&gt;Enumerable&lt;/code&gt; Protocol&lt;/a&gt;, which gives you several nice benefits:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Takes advantage of the &lt;code&gt;member?/2&lt;/code&gt; function which means we can use the &lt;code&gt;in&lt;/code&gt; keyword to check for membership&lt;/li&gt;&lt;li&gt;Allows for checking of sub-interval membership (&lt;a href=&quot;https://github.com/acalejos/exterval?tab=readme-ov-file&amp;amp;ref=thestackcanary.com#membership&quot;&gt;with some caveats&lt;/a&gt;)&lt;/li&gt;&lt;li&gt;Implements an optional &lt;code&gt;step&lt;/code&gt; parameter that allows you to iterate/reduce over the interval&lt;/li&gt;&lt;li&gt;Implements a &lt;code&gt;size&lt;/code&gt; function (remember, &lt;code&gt;size&lt;/code&gt; refers to the ability to count the number of members without reducing over the whole structure, whereas &lt;code&gt;lengths&lt;/code&gt; implies a need to reduce).&lt;/li&gt;&lt;li&gt;Allows for &lt;code&gt;:infinity&lt;/code&gt; and &lt;code&gt;:neg_infinity&lt;/code&gt; to be specified in the interval&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;This lets us write more succinct checks like:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-elixir&quot;&gt;iex&amp;gt; import Exterval
iex&amp;gt; ~i&amp;lt;[1, 10)//2&amp;gt;
[1, 10)//2
iex&amp;gt; ~i&amp;lt;[1, 10)//2&amp;gt; |&amp;gt; Enum.to_list()
[1.0, 3.0, 5.0, 7.0, 9.0]
iex&amp;gt; ~i&amp;lt;[1, 10)//2&amp;gt; |&amp;gt; Enum.sum()
25.0
iex&amp;gt; ~i&amp;lt;[-1, 3)//-0.5&amp;gt; |&amp;gt; Enum.to_list()
[2.5, 2.0, 1.5, 1.0, 0.5, 0.0, -0.5, -1.0]
iex&amp;gt; ~i&amp;lt;[1, 10]&amp;gt; |&amp;gt; Enum.count()
:infinity
iex&amp;gt; ~i&amp;lt;[1, 10)//2&amp;gt; |&amp;gt; Enum.count()
4
iex&amp;gt; ~i&amp;lt;[-2,-2]//1.0&amp;gt; |&amp;gt; Enum.count()
1
iex&amp;gt; ~i&amp;lt;[1,2]//0.5&amp;gt; |&amp;gt; Enum.count()
3
iex&amp;gt; ~i&amp;lt;[-2,-1]//0.75&amp;gt; |&amp;gt; Enum.count()
2
iex&amp;gt; 1 in ~i&amp;lt;[1, 10]&amp;gt;
true
iex&amp;gt; 1 in ~i&amp;lt;[1, 10)//2&amp;gt;
true
iex&amp;gt; 3 in ~i&amp;lt;(1, 10)//2&amp;gt;
true
# You can even do variable substitution using string interpolation syntax, since the sigil parameter is just a string
iex&amp;gt; min = 2
iex&amp;gt; 3 in ~i&amp;lt;(#{min + 1}, 10)//2&amp;gt;
false&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Design Details&lt;/h2&gt;&lt;p&gt;The decision to implement the interval as a sigil was not as straightforward as it might seem. As I mentioned before, Elixir is an extremely extensible language with superior support for meta-programming, so implementing this as a macro was my first instinct. I considered commandeering the opening brackets &lt;code&gt;(&lt;/code&gt; and &lt;code&gt;[&lt;/code&gt; to trigger the macro, or something similar with the comma &lt;code&gt;,&lt;/code&gt; , but fortunately, I hit a brick wall with that effort. I say fortunately not only because it would have been a bad idea from a design perspective, but it certainly would have been a messier implementation and would have overly complicated it in addition to actually making the code less clear. I appreciate the usage of the sigil &lt;code&gt;~I&lt;/code&gt; because it makes it clear that the range that follows is not to be confused with the built-in &lt;code&gt;Range&lt;/code&gt;. &lt;/p&gt;&lt;div&gt;&lt;div&gt;💡&lt;/div&gt;&lt;div&gt;You can read more about Elixir sigils &lt;a href=&quot;https://hexdocs.pm/elixir/main/sigils.html?ref=thestackcanary.com&quot;&gt;here&lt;/a&gt; and see their syntax reference &lt;a href=&quot;https://hexdocs.pm/elixir/main/syntax-reference.html?ref=thestackcanary.com#sigils&quot;&gt;here&lt;/a&gt;. Of note, you can use any of the allowed delimiter pairs that it lists to capture your sigil. I chose [ and ] so as to not conflict with the brackets used in the interval. You could also use something like ~i|[0,1)| if you prefer. &lt;/div&gt;&lt;/div&gt;&lt;p&gt;Once I decided on the usage of the &lt;code&gt;Enumerable&lt;/code&gt; protocol, I knew I wanted to allow some way for an optional step size to be specified so that &lt;code&gt;reduce&lt;/code&gt; could be used on the structure. Elixir sigils allow for parameters to be passed after the closing sigil, so initially,  I considered passing in the step size as a parameter since zero or more ASCII letters and digits can be given as a modifier to the sigil, but this would prohibit having floats as step sizes. Another constraint to consider when using sigils is that string interpolation is only allowed within the sigil when using a lowercase sigil. Sigils start with &lt;code&gt;~&lt;/code&gt; and are followed by one lowercase letter or by one or more uppercase letters, immediately followed by one of the allowed delimiter pairs. Within the context of our use case, we happen to be able to get by without having string interpolation since we use it within pre-defined parameters that are hardcoded, but the library becomes much more useful if we can have dynamically defined intervals, so this limits how the sigil is named.&lt;/p&gt;&lt;p&gt;Another major design decision was how to actually parse the sigil. I ultimately landed on the straightforward answer of just using a regex, but I had a decent back-and-forth with my friend &lt;a href=&quot;https://twitter.com/polvalente?ref=thestackcanary.com&quot;&gt;Paulo&lt;/a&gt; from the &lt;a href=&quot;https://github.com/elixir-nx/nx/tree/main/nx?ref=thestackcanary.com#readme&quot;&gt;Elixir-Nx&lt;/a&gt; core team regarding other options. He provided some nice proofs of concept using binary pattern matching as well as &lt;a href=&quot;https://github.com/dashbitco/nimble_parsec?ref=thestackcanary.com&quot;&gt;NimbleParsec&lt;/a&gt;, but I decided on a regex due to my familiarity, its ability to reduce the amount of code, and because I was not too concerned with performance concerns with what will typically be short patterns. &lt;/p&gt;&lt;p&gt;One of the last design details finalized was how to treat the step size and its effect on item membership. Paulo and I discussed whether it should support ranges where the min and max values did not necessarily have to be in the correct order (e.g. &lt;code&gt;~i&amp;lt;1,-1//0.5&amp;gt;&lt;/code&gt;) which would essentially imply that any iteration would start at &lt;code&gt;1&lt;/code&gt; in this instance and would work towards &lt;code&gt;-1&lt;/code&gt; in steps of &lt;code&gt;0.5&lt;/code&gt;. This was discussed since it can be seen in some other implementations throughout other ecosystems.  We decided that the most clear solution, as well as the solution that fit best within the spirit of the library, was to enforce that the first value specified be less than or equal to the second value, and any desire to iterate starting with the max value could be specified using a negative step size. &lt;/p&gt;&lt;h2&gt;Implementation Details&lt;/h2&gt;&lt;h3&gt;Creation&lt;/h3&gt;&lt;p&gt;An interval is stored as a struct with the following fields:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;code&gt;left&lt;/code&gt; - the left bracket, either &lt;code&gt;[&lt;/code&gt; or &lt;code&gt;(&lt;/code&gt;.&lt;/li&gt;&lt;li&gt;&lt;code&gt;right&lt;/code&gt; - the right bracket, either &lt;code&gt;]&lt;/code&gt; or &lt;code&gt;)&lt;/code&gt;.&lt;/li&gt;&lt;li&gt;&lt;code&gt;min&lt;/code&gt; - the lower bound of the interval. Can be &lt;code&gt;:neg_infinity&lt;/code&gt; or any number.&lt;/li&gt;&lt;li&gt;&lt;code&gt;max&lt;/code&gt; - the upper bound of the interval. Can be &lt;code&gt;:infinity&lt;/code&gt; or any number.&lt;/li&gt;&lt;li&gt;&lt;code&gt;step&lt;/code&gt; - the step size of the interval. If &lt;code&gt;nil&lt;/code&gt;, the interval is continuous.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;To define a sigil, you create a function with the name of the sigil prefixed by &lt;code&gt;sigil_&lt;/code&gt;, so since I wish to use this sigil using &lt;code&gt;~i&lt;/code&gt; I define it as&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-elixir&quot;&gt;def sigil_i(pattern, []) do
end&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The second parameter are the options to the sigil I mentioned earlier. For now these are unused.&lt;/p&gt;&lt;p&gt;I parse the input to the sigil using the following &lt;a href=&quot;https://regex101.com/r/MJyFE4/1?ref=thestackcanary.com&quot;&gt;regex&lt;/a&gt;:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-regex&quot;&gt;^(?P&amp;lt;left&amp;gt;\[|\()\s*(?P&amp;lt;min&amp;gt;[-+]?(?:\d+|\d+\.\d+)(?:[eE][-+]?\d+)?|:neg_infinity)\s*,\s*(?P&amp;lt;max&amp;gt;[-+]?(?:\d+|\d+\.\d+)(?:[eE][-+]?\d+)?|:infinity)\s*(?P&amp;lt;right&amp;gt;]|\))(?:\/\/(?P&amp;lt;step&amp;gt;[-+]?(?:[1-9]+|\d+\.\d+)(?:[eE][-+]?\d+)?))?$&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Using the named capture groups I perform some additional validation such as ensuring that the interval goes from the minimum value to the maximum.&lt;/p&gt;&lt;h3&gt;Enumerable – Size / Count&lt;/h3&gt;&lt;p&gt;The first function I need to implement for the protocol is the &lt;code&gt;Enumerable.count/1&lt;/code&gt; function. Logically, there are three conditions to account for. First are the instances where the size is either zero or infinity. Since &lt;code&gt;Enumerable.count/1&lt;/code&gt; must return a number on success, I choose to return &lt;code&gt;{:error, Infinity}&lt;/code&gt; from &lt;code&gt;Enumerable.count/1&lt;/code&gt; when I wish to return &lt;code&gt;:infinity&lt;/code&gt;. This would normally be used to return a module which can perform a reduction to compute the count, but if we just make a simple helper module &lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-elixir&quot;&gt;defmodule Infinity do
  @moduledoc false
  def reduce(%Exterval{}, _, _), do: {:halt, :infinity}
end&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now I can get my desired behavior. I implement these cases with the following:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-elixir&quot;&gt;def size(interval)
def size(%__MODULE__{step: nil}), do: {:error, Infinity}
def size(%__MODULE__{max: :neg_infinity}), do: 0
def size(%__MODULE__{min: :infinity}), do: 0

def size(%__MODULE__{min: min, max: max})
    when min in [:infinity, :neg_infinity] or max in [:infinity, :neg_infinity],
    do: {:error, Infinity}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Lastly I separate cases where the step size is negative and where its positive since the logic is different.&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-elixir&quot;&gt;def size(%__MODULE__{left: left, right: right, min: min, max: max, step: step}) when step &amp;lt; 0 do
  case {left, right} do
    {&amp;quot;[&amp;quot;, &amp;quot;]&amp;quot;} -&amp;gt;
      abs(trunc((max - min) / step)) + 1

    {&amp;quot;(&amp;quot;, &amp;quot;]&amp;quot;} -&amp;gt;
      abs(trunc((max - (min - step)) / step)) + 1

    {&amp;quot;[&amp;quot;, &amp;quot;)&amp;quot;} -&amp;gt;
      abs(trunc((max + step - min) / step)) + 1

    {&amp;quot;(&amp;quot;, &amp;quot;)&amp;quot;} -&amp;gt;
      abs(trunc((max + step - (min - step)) / step)) + 1
  end
end

def size(%__MODULE__{left: left, right: right, min: min, max: max, step: step}) when step &amp;gt; 0 do
  case {left, right} do
    {&amp;quot;[&amp;quot;, &amp;quot;]&amp;quot;} -&amp;gt;
      abs(trunc((max - min) / step)) + 1

    {&amp;quot;(&amp;quot;, &amp;quot;]&amp;quot;} -&amp;gt;
      abs(trunc((max - (min + step)) / step)) + 1

    {&amp;quot;[&amp;quot;, &amp;quot;)&amp;quot;} -&amp;gt;
      abs(trunc((max - step - min) / step)) + 1

    {&amp;quot;(&amp;quot;, &amp;quot;)&amp;quot;} -&amp;gt;
      abs(trunc((max - step - (min + step)) / step)) + 1
  end
end&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;Enumerable – Reduce&lt;/h3&gt;&lt;p&gt;The implementation for &lt;code&gt;reduce&lt;/code&gt; is a great example of how Elixir&amp;#39;s pattern matching in function headers can reduce visual complexity and even the implementation itself. First, we return &lt;code&gt;:infinity&lt;/code&gt; if  &lt;code&gt;step&lt;/code&gt; is &lt;code&gt;nil&lt;/code&gt;.&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-elixir&quot;&gt;def reduce(%Exterval{step: nil}, acc, _fun) do
  {:done, acc}
end&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Next, we again have different clauses depending on if the &lt;code&gt;step&lt;/code&gt; is positive or negative, since that dictates which direction with respect to the interval the reduction occur.&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-elixir &quot;&gt;def reduce(%Exterval{left: left, right: right, min: min, max: max, step: step}, acc, fun)
    when step &amp;gt; 0 do
  case left do
    &amp;quot;[&amp;quot; -&amp;gt;
      reduce(min, max, right, acc, fun, step)

    &amp;quot;(&amp;quot; -&amp;gt;
      reduce(min + step, max, right, acc, fun, step)
  end
end

def reduce(%Exterval{left: left, right: right, min: min, max: max, step: step}, acc, fun)
    when step &amp;lt; 0 do
  case right do
    &amp;quot;]&amp;quot; -&amp;gt;
      reduce(min, max, left, acc, fun, step)

    &amp;quot;)&amp;quot; -&amp;gt;
      reduce(min, max + step, left, acc, fun, step)
  end
end&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Notice that these clauses to the &lt;code&gt;reduce/3&lt;/code&gt; implementation return a different &lt;code&gt;reduce/6&lt;/code&gt; function which is specific to our module. &lt;/p&gt;&lt;p&gt;Next we handle conditions where the reduction is halted or suspended:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-elixir&quot;&gt;efp reduce(_min, _max, _closing, {:halt, acc}, _fun, _step) do
  {:halted, acc}
end

defp reduce(min, max, closing, {:suspend, acc}, fun, step) do
  {:suspended, acc, &amp;amp;reduce(min, max, closing, &amp;amp;1, fun, step)}
end&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Next we handle edge cases involving &lt;code&gt;:infinity&lt;/code&gt; and &lt;code&gt;:neg_infinity&lt;/code&gt; where we have no way to begin the reduction since we cannot move &lt;code&gt;step&lt;/code&gt; increments away from either of these when they are our starting point:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-elixir&quot;&gt;defp reduce(:neg_infinity, _max, _closing, {:cont, acc}, _fun, step) when step &amp;gt; 0 do
  {:done, acc}
end

defp reduce(_min, :infinity, _closing, {:cont, acc}, _fun, step) when step &amp;lt; 0 do
  {:done, acc}
end&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Interestingly, these are cases where the size of the intervals would be &lt;code&gt;:infinity&lt;/code&gt; but we cannot reduce over them at all, as opposed to other infinitely sized intervals where we can begin iteration which will never end, such as &lt;code&gt;~i&amp;lt;[0,:infinity]//1&amp;gt;&lt;/code&gt; which would effectively be an infinite stream starting at &lt;code&gt;0&lt;/code&gt; and incrementing by &lt;code&gt;1&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;Next we add all of the main logic for the &amp;quot;typical&amp;quot; cases:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-elixir&quot;&gt;defp reduce(min, max, &amp;quot;]&amp;quot; = closing, {:cont, acc}, fun, step)
     when min &amp;lt;= max do
  reduce(min + step, max, closing, fun.(min, acc), fun, step)
end

defp reduce(min, max, &amp;quot;)&amp;quot; = closing, {:cont, acc}, fun, step)
     when min &amp;lt; max do
  reduce(min + step, max, closing, fun.(min, acc), fun, step)
end

defp reduce(min, max, &amp;quot;[&amp;quot; = closing, {:cont, acc}, fun, step)
     when min &amp;lt;= max do
  reduce(min, max + step, closing, fun.(max, acc), fun, step)
end

defp reduce(min, max, &amp;quot;(&amp;quot; = closing, {:cont, acc}, fun, step)
     when min &amp;lt; max do
  reduce(min, max + step, closing, fun.(max, acc), fun, step)
end&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And lastly we add the final case where the condition that &lt;code&gt;min &amp;lt; max&lt;/code&gt; (or &lt;code&gt;min &amp;lt;= max&lt;/code&gt; depending on the brackets) is no longer met, which means the reduction is complete:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-elixir&quot;&gt;defp reduce(_, _, _, {:cont, acc}, _fun, _up) do
  {:done, acc}
end&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Just like that the &lt;code&gt;reduce/3&lt;/code&gt; implementation is complete! As I mentioned before and notes in more detail, there are some opinions inherit to this implementation having to do with &lt;code&gt;:infinity&lt;/code&gt; and &lt;code&gt;:neg_infinity&lt;/code&gt; bounds as well as empty intervals, but I tried to keep the behavior consistent throughout.&lt;/p&gt;&lt;h3&gt;Enumerable – Membership&lt;/h3&gt;&lt;p&gt;Now on to the part that I was most interested in, which is interval membership. First, let&amp;#39;s add support for checking membership between two intervals, which is essentially a check for one interval being a sub-interval of another. &lt;/p&gt;&lt;p&gt;Sub-interval must satisfy the following to be a subset:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;The minimum value of the subset must belong to the superset.&lt;/li&gt;&lt;li&gt;The maximum value of the subset must belong to the superset.&lt;/li&gt;&lt;li&gt;The step size of the subset must be a multiple of the step size of the superset.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;If the superset has no step size, then only the first two conditions must be satisfied.&lt;/p&gt;&lt;p&gt;if the superset has a step size, and the subset doesn&amp;#39;t then membership is &lt;code&gt;false&lt;/code&gt;.&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-elixir&quot;&gt;def member?(%Exterval{step: nil} = outer, %Exterval{} = inner) do
  res = inner.max in outer &amp;amp;&amp;amp; inner.min in outer
  {:ok, res}
end

def member?(%Exterval{}, %Exterval{step: nil}) do
  {:ok, false}
end

def member?(%Exterval{} = outer, %Exterval{} = inner) do
  res = inner.max in outer &amp;amp;&amp;amp; inner.min in outer &amp;amp;&amp;amp; :math.fmod(inner.step, outer.step) == 0
  {:ok, res}
end&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then that just leaves the main implementation for membership checks, which is basically just a &lt;code&gt;case&lt;/code&gt; statement which changes the output depending on the brackets supplied. Additionally, if the interval contains a &lt;code&gt;step&lt;/code&gt; then the value being checked must be a multiple of the &lt;code&gt;step&lt;/code&gt;.&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-elixir&quot;&gt;def member?(%Exterval{} = rang, value) when is_number(value) do
  res =
    if Exterval.size(rang) == 0 do
      {:ok, false}
    else
      case {rang.left, rang.min, rang.max, rang.right} do
        {_, :neg_infinity, :infinity, _} -&amp;gt;
          true

        {_, :neg_inf, max_val, &amp;quot;]&amp;quot;} -&amp;gt;
          value &amp;lt;= max_val

        {_, :neg_infinity, max_val, &amp;quot;)&amp;quot;} -&amp;gt;
          value &amp;lt; max_val

        {&amp;quot;[&amp;quot;, min_val, :infinity, _} -&amp;gt;
          value &amp;gt;= min_val

        {&amp;quot;(&amp;quot;, min_val, :infinity, _} -&amp;gt;
          value &amp;gt; min_val

        {&amp;quot;[&amp;quot;, min_val, max_val, &amp;quot;]&amp;quot;} -&amp;gt;
          value &amp;gt;= min_val and value &amp;lt;= max_val

        {&amp;quot;(&amp;quot;, min_val, max_val, &amp;quot;]&amp;quot;} -&amp;gt;
          value &amp;gt; min_val and value &amp;lt;= max_val

        {&amp;quot;[&amp;quot;, min_val, max_val, &amp;quot;)&amp;quot;} -&amp;gt;
          value &amp;gt;= min_val and value &amp;lt; max_val

        {&amp;quot;(&amp;quot;, min_val, max_val, &amp;quot;)&amp;quot;} -&amp;gt;
          value &amp;gt; min_val and value &amp;lt; max_val

        _ -&amp;gt;
          raise ArgumentError, &amp;quot;Invalid range specification&amp;quot;
      end
    end

  res =
    unless is_nil(rang.step) || rang.min == :neg_infinity || rang.max == :infinity do
      res &amp;amp;&amp;amp; :math.fmod(value - rang.min, rang.step) == 0
    else
      res
    end

  {:ok, res}
end&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;Inspect&lt;/h3&gt;&lt;p&gt;Lastly, to make the user experience a bit better, it&amp;#39;s not too difficult to implement the &lt;a href=&quot;https://hexdocs.pm/elixir/1.16.0/Inspect.html?ref=thestackcanary.com&quot;&gt;&lt;code&gt;Inpect&lt;/code&gt;&lt;/a&gt; Protocol to provide a cleaner output:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-elixir&quot;&gt;defimpl Inspect do
  import Inspect.Algebra
  import Kernel, except: [inspect: 2]

  def inspect(%Exterval{left: left, right: right, min: min, max: max, step: nil}, opts) do
    concat([string(left), to_doc(min, opts), &amp;quot;,&amp;quot;, to_doc(max, opts), string(right)])
  end

  def inspect(%Exterval{left: left, right: right, min: min, max: max, step: step}, opts) do
    concat([
      string(left),
      to_doc(min, opts),
      &amp;quot;,&amp;quot;,
      to_doc(max, opts),
      string(right),
      &amp;quot;//&amp;quot;,
      to_doc(step, opts)
    ])
  end
end&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Future Plans&lt;/h2&gt;&lt;p&gt;Currently I am weighing the options between adding more functionality to the library or keeping it as thin as it currently is. The main additions could be more robust set operations on the intervals, but I currently do not have a need for it so it will probably not make it into the library in the near future. &lt;/p&gt;&lt;p&gt;For now, I hope this provided a detailed look at the process of identifying a problem, and subsequently designing and implementing the solution. I found this to be an elegant solution to the problem, but as I mentioned it was not a straight-line path. I would be interested to hear about any other solutions people have seen!&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;h2&gt;&lt;span&gt;Sign up for The Stack Canary&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;&lt;span&gt;If you enjoyed reading, let me know!&lt;/span&gt;&lt;/p&gt;&lt;div&gt;
                Email sent! Check your inbox to complete your signup.
            &lt;/div&gt;&lt;p&gt;&lt;span&gt;No spam. Unsubscribe anytime.&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</content:encoded>
</item>
<item>
<title>From Python to Elixir Machine Learning</title>
<link>https://www.thestackcanary.com/from-python-pytorch-to-elixir-nx/</link>
<guid isPermaLink="false">k8co6KGJFymIITBZMxGpxvlwk_pc1H1S1bO29w==</guid>
<pubDate>Sun, 19 Apr 2026 04:26:32 +0000</pubDate>
<description>Moving on from Python Machine Learning might seem impossible. Let me break down why and how you can do it.</description>
<content:encoded>&lt;img src=&quot;https://www.thestackcanary.com/content/images/2023/07/Dark-Blue-Simple-Modern-Virtual-Technology-Banner-1.png&quot; alt=&quot;From Python to Elixir Machine Learning&quot; title=&quot;&quot;/&gt;&lt;p&gt;As Elixir&amp;#39;s Machine Learning (ML) ecosystem grows, many Elixir enthusiasts who wish to adopt the new machine learning libraries in their projects are stuck at a crossroads of wanting to move away from their existing ML stack (typically Python) while not having a clear path of how to do so. I would like to take some time to talk to WHY I believe now is a good time to start porting over Machine Learning code into Elixir, and HOW I went about doing just this for two libraries I wrote: &lt;a href=&quot;https://github.com/acalejos/exgboost?ref=thestackcanary.com&quot;&gt;EXGBoost&lt;/a&gt; (from Python XGBoost) and &lt;a href=&quot;https://github.com/acalejos/mockingjay?ref=thestackcanary.com&quot;&gt;Mockingjay&lt;/a&gt; (from Python Hummingbird).&lt;/p&gt;&lt;h2&gt;Why is Python not Sufficient?&lt;/h2&gt;&lt;p&gt;There&amp;#39;s a common saying in programming languages that no language is perfect, but that different languages are suited for different jobs. Languages such as C, Rust, and now even Zig are known for their targeting systems development, while languages such as C++, C#, and Java are more commonly used for application development, and obviously there are the web languages such as JavaScript/TypeScript, PHP, Ruby (on Rails), and more. There are gradations to these rules of course, but more often than not there are good reasons that languages tend to exist within the confines of particular use cases. &lt;/p&gt;&lt;p&gt; Languages such as Elixir and Go tend to be used in large distributed systems because they place an emphasis on having great support for common concurrency patterns, which can come at the cost of supporting other domains. Go, for example, has barely (if any?) support for machine learning libraries, but it&amp;#39;s also not trying to cater to that as a target domain. For a long time, the same could have been said about Elixir, but over the past two or so years, there has been a massive concerted push from the Elixir community to not only have support for machine learning, but to push the envelope with the maintaining state of the art libraries that are beginning to compete with the other dominant machine learning languages - namely Python. &lt;/p&gt;&lt;p&gt;Python has long been the gold standard in the realm of machine learning. The breadth of libraries and the low entry barrier makes Python a great language to work with, but it does create a bit of a bottleneck. Any application that wishes to integrate machine learning has historically had only a couple of options: have a Python component or reach into the underlying libraries that power much of the Python libraries directly. Despite all the good parts of Python I mentioned before, speed and support for concurrency are not on that list. Elixir-Nx is striving to give another option - an option that can take advantage of the native distributed support that Elixir and the BEAM VM have to offer. Nx&amp;#39;s &lt;code&gt;Nx.Serving&lt;/code&gt; construct is a drop-in solution for serving distributed machine-learning models. &lt;/p&gt;&lt;h2&gt;How to Proceed&lt;/h2&gt;&lt;p&gt;Sean Moriarity, the co-creator of &lt;a href=&quot;https://github.com/elixir-nx/nx?ref=thestackcanary.com&quot;&gt;Nx&lt;/a&gt;, creator of &lt;a href=&quot;https://github.com/elixir-nx/axon?ref=thestackcanary.com&quot;&gt;Axon&lt;/a&gt;, and author of &lt;a href=&quot;https://pragprog.com/titles/smelixir/machine-learning-in-elixir/?ref=thestackcanary.com&quot;&gt;Machine Learning in Elixir&lt;/a&gt;, has talked many times about how the initial creation of Nx and Axon involved hours upon hours of reading source code from reference implementations of libraries in Python and C++, namely the TensorFlow source code. While I was writing &lt;a href=&quot;https://github.com/acalejos/exgboost?ref=thestackcanary.com&quot;&gt;EXGBoost&lt;/a&gt; and &lt;a href=&quot;https://github.com/acalejos/mockingjay?ref=thestackcanary.com&quot;&gt;Mockingjay&lt;/a&gt;, much of my time, especially towards the beginning, was spent referencing the Python and C++ implementations of the original libraries.  This builds a great fundamental understanding of the libraries as well as taught me how to identify patterns in Python and C++ and identify the Elixir pattern that could express the same ideas. This skill is invaluable, and the better I got at it the faster I could write. Below is a summary and key takeaways from my process of porting Python / PyTorch to Elixir / Nx. &lt;/p&gt;&lt;h2&gt;Workflow Overview&lt;/h2&gt;&lt;p&gt;Before I get to the examples from the code bases, I would like to briefly explain the high-level cyclical workflow I established while working on this effort, and what I would recommend to anyone pursuing a similar endeavor. &lt;/p&gt;&lt;h3&gt;Understand the Macro System&lt;/h3&gt;&lt;p&gt; Much like how there&amp;#39;s a common strategy to reading comprehension which involves reading through the entire document once to get a high-level understanding and then doing subsequent shorter reads to gain more in-depth understanding with the added context of the entire piece, you can consider doing the same when reading code. My first step was to follow the logical flow from the call of &lt;code&gt;hummingbird.ml.convert&lt;/code&gt; to the final result. You can use tools such as function tracers and callgraph generators to accelerate this part of the process, or manually trace depending on the extent of the codebase. I felt in my case that it was manageable to trace myself. &lt;/p&gt;&lt;h3&gt;Read the Documentation&lt;/h3&gt;&lt;p&gt;Once you have a general understanding of the flow and process of the original system, you can start referring to the documentation for some additional context. In my case, this lead me to the academic paper &lt;a href=&quot;https://scnakandala.github.io/papers/TR_2020_Hummingbird.pdf?ref=thestackcanary.com&quot;&gt;&lt;em&gt;Taming Model Serving Complexity, Performance and Cost: A Compilation to Tensor Computations Approach&lt;/em&gt;&lt;/a&gt;&lt;em&gt;, &lt;/em&gt;which was the underlying ground work and basis for their implementation. I could write a whole other blog post about the process of transcribing algorithms and code from academic papers and pseudocode, but for now just know that these are some of the most important pieces you can refer to while re-implementing or porting over a piece of source code. &lt;/p&gt;&lt;h3&gt;Read the Source Code in Detail&lt;/h3&gt;&lt;p&gt;This is the point in which you want to disambiguate the higher-level ideas from the first step and really gain a fine, high-resolution understanding of what is happening. There might even be some points in which you need to deconflict the source code with its documentation and/or paper reference. In those cases, the source code almost always wins, and if not, then you likely have a bug report you can file. If you see things you don&amp;#39;t fully understand, you don&amp;#39;t necessarily need to address it here, but you should make note of it and keep it in mind while working in case new details help resolve it. &lt;/p&gt;&lt;h3&gt;Implement the New Code&lt;/h3&gt;&lt;p&gt;At this point, you should feel comfortable enough to start implementing the code. I found this to be a very iterative process, meaning I would think I had a grasp on something, then would start working on implementing it, then would realize I did not understand it as well as I had thought and would work my way back through the previous steps. &lt;/p&gt;&lt;h2&gt;Example&lt;/h2&gt;&lt;div&gt;&lt;div&gt;💡&lt;/div&gt;&lt;div&gt;In case you would like to follow along going forward, the Python code I will be referencing is the &lt;a href=&quot;https://github.com/microsoft/hummingbird/blob/main/hummingbird/ml/operator_converters/_tree_implementations.py?ref=thestackcanary.com&quot;&gt;Microsoft Hummingbird source code&lt;/a&gt; (specifically their implementation of Decision Tree Compilation), and the Elixir code is from the &lt;a href=&quot;https://github.com/acalejos/mockingjay/tree/main/lib/mockingjay/strategies?ref=thestackcanary.com&quot;&gt;Mockingjay source code&lt;/a&gt;.&lt;/div&gt;&lt;/div&gt;&lt;h2&gt;Class vs. Behaviour&lt;/h2&gt;&lt;p&gt;As a result of the reading and comprehension I did of the Hummingbird code base, I realized fairly early on that my library was going to have some key differences. One of the main reasons for these differences was the fact that the Hummingbird code base was built as a retroactive library that needed to cater to existing APIs that existed throughout the Python ecosystem. They chose to only add support for converting decision trees according to the SKLearn API. I, conversely, chose to write Mockingjay in such a way that it would be incumbent upon the authors of decision tree libraries to implement a protocol to interface with Mockingjay&amp;#39;s &lt;code&gt;convert&lt;/code&gt; function. This difference meant that I could establish a &lt;code&gt;Mockingjay.Tree&lt;/code&gt; data structure that I would use throughout my library, rather than having to reconstruct tree features from various other APIs as is done in Hummingbird. &lt;/p&gt;&lt;p&gt;Next, Hummingbird approaches its pipeline in a very-object oriented manner, as makes sense when using Python. Here&amp;#39; we are focusing on the implementation of the three decision tree conversion strategies: GEMM, Tree Traversal, and PErfect Tree Traversal.  It implements the following base class for tree conversions as well as PyTorch networks. &lt;/p&gt;&lt;div&gt;&lt;div&gt;💡&lt;/div&gt;&lt;div&gt;Since they&amp;#39;re inheriting from torch.nn.model they must also implement the forward method.&lt;/div&gt;&lt;/div&gt;&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;class AbstracTreeImpl(PhysicalOperator):
    &amp;quot;&amp;quot;&amp;quot;
    Abstract class definig the basic structure for tree-base models.
    &amp;quot;&amp;quot;&amp;quot;

    def __init__(self, logical_operator, **kwargs):
        super().__init__(logical_operator, **kwargs)

    @abstractmethod
    def aggregation(self, x):
        &amp;quot;&amp;quot;&amp;quot;
        Method defining the aggregation operation to execute after the model is evaluated.

        Args:
            x: An input tensor

        Returns:
            The tensor result of the aggregation
        &amp;quot;&amp;quot;&amp;quot;
        pass

class AbstractPyTorchTreeImpl(AbstracTreeImpl, torch.nn.Module):
    &amp;quot;&amp;quot;&amp;quot;
    Abstract class definig the basic structure for tree-base models implemented in PyTorch.
    &amp;quot;&amp;quot;&amp;quot;

    def __init__(
        self, logical_operator, tree_parameters, n_features, classes, n_classes, decision_cond=&amp;quot;&amp;lt;=&amp;quot;, extra_config={}, **kwargs
    ):
        &amp;quot;&amp;quot;&amp;quot;
        Args:
            tree_parameters: The parameters defining the tree structure
            n_features: The number of features input to the model
            classes: The classes used for classification. None if implementing a regression model
            n_classes: The total number of used classes
            decision_cond: The condition of the decision nodes in the x &amp;lt;cond&amp;gt; threshold order. Default &amp;#39;&amp;lt;=&amp;#39;. Values can be &amp;lt;=, &amp;lt;, &amp;gt;=, &amp;gt;
        &amp;quot;&amp;quot;&amp;quot;
        super(AbstractPyTorchTreeImpl, self).__init__(logical_operator, **kwargs)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;They then proceed to inherit from these base classes and have different classes for each of the three decision tree strategies as well as their gradient-boosted counterparts, leaving them with three classes for each strategies (1 base class per strategy, 1 for ensemble implementations, and 1 for normal implementations) and nine total classes. &lt;/p&gt;&lt;p&gt;I chose to approach this using a &lt;code&gt;behaviour&lt;/code&gt;&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-elixir&quot;&gt;defmodule Mockingjay.Strategy do
  @moduledoc false
  @type t :: Nx.Container.t()

  @callback init(data :: any(), opts :: Keyword.t()) :: term()
  @callback forward(x :: Nx.Container.t(), term()) :: Nx.Tensor.t()
  ...
end&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;init&lt;/code&gt; will perform setup functionality depending on the strategy and return the parameters that will need to be passed to &lt;code&gt;forward&lt;/code&gt; later on. This allows for a very simple top-level api. The whole top-level &lt;code&gt;mockingjay.ex&lt;/code&gt; file can fit here:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-elixir&quot;&gt;def convert(data, opts \\ []) do
    {strategy, opts} = Keyword.pop(opts, :strategy, :auto)

    strategy =
      case strategy do
        :gemm -&amp;gt;
          Mockingjay.Strategies.GEMM

        :tree_traversal -&amp;gt;
          Mockingjay.Strategies.TreeTraversal

        :perfect_tree_traversal -&amp;gt;
          Mockingjay.Strategies.PerfectTreeTraversal

        :auto -&amp;gt;
          Mockingjay.Strategy.get_strategy(data, opts)

        _ -&amp;gt;
          raise ArgumentError,
                &amp;quot;strategy must be one of :gemm, :tree_traversal, :perfect_tree_traversal, or :auto&amp;quot;
      end

    {post_transform, opts} = Keyword.pop(opts, :post_transform, nil)
    state = strategy.init(data, opts)

    fn data -&amp;gt;
      result = strategy.forward(data, state)
      {_, n_trees, n_classes} = Nx.shape(result)

      result
      |&amp;gt; aggregate(n_trees, n_classes)
      |&amp;gt; post_transform(post_transform, n_classes)
    end
  end&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;As you can see, the use of a behaviour here allows a strategy-agnostic approach to generating a prediction pipeline. In the object-oriented implementation, each class implements &lt;code&gt;init&lt;/code&gt;, &lt;code&gt;forward&lt;/code&gt;, &lt;code&gt;aggregate&lt;/code&gt;, and &lt;code&gt;post_transform&lt;/code&gt;. We get the same result from a functional pipeline approach, where each step generates the needed information as input parameters for the next step. So, instead of storing intermediate results as object properties or values in an object&amp;#39;s &lt;strong&gt;&lt;code&gt;__dict__&lt;/code&gt;&lt;/strong&gt;, we just pass them along in the pipeline. I would argue this creates a much simpler and easier to follow implementation (but I am also quite biased).&lt;/p&gt;&lt;h2&gt;PyTorch to Nx &lt;/h2&gt;&lt;p&gt;For these examples, we will be looking at porting the implementations of the &lt;code&gt;forward&lt;/code&gt; function for the three conversion strategies from Python to Nx.&lt;/p&gt;&lt;h4&gt;GEMM&lt;/h4&gt;&lt;p&gt;Next, let&amp;#39;s look at the &lt;code&gt;forward&lt;/code&gt; function implementation for GEMM, one of the three conversion strategies. In Hummingbird, they implemented the &lt;code&gt;forward&lt;/code&gt; step in the base class for each strategy. So given three GEMM classes with the signatures of &lt;code&gt;GEMMTreeImpl(AbstractPyTorchTreeImpl)&lt;/code&gt;, &lt;code&gt;GEMMDecisionTreeImpl(GEMMTreeImpl)&lt;/code&gt;, and &lt;code&gt;GEMMGBDTImpl(GEMMTreeImpl)&lt;/code&gt;, the &lt;code&gt;forward&lt;/code&gt; function is defined in the &lt;code&gt;GEMMTreeImpl&lt;/code&gt; class, since both ensemble and non-ensemble decision tree models share the same forward step. &lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def forward(self, x):
      x = x.t()
      x = self.decision_cond(torch.mm(self.weight_1, x), self.bias_1)
      x = x.view(self.n_trees, self.hidden_one_size, -1)
      x = x.float()

      x = torch.matmul(self.weight_2, x)

      x = x.view(self.n_trees * self.hidden_two_size, -1) == self.bias_2
      x = x.view(self.n_trees, self.hidden_two_size, -1)
      if self.tree_op_precision_dtype == &amp;quot;float32&amp;quot;:
          x = x.float()
      else:
          x = x.double()

      x = torch.matmul(self.weight_3, x)
      x = x.view(self.n_trees, self.hidden_three_size, -1)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now, here is the Nx implementation:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-elixir&quot;&gt;@impl true
  deftransform forward(x, {arg, opts}) do
    opts =
      Keyword.validate!(opts, [
        :condition,
        :n_trees,
        :n_classes,
        :max_decision_nodes,
        :max_leaf_nodes,
        :n_weak_learner_classes,
        :custom_forward
      ])

    _forward(x, arg, opts)
  end

  defnp _forward(x, arg, opts \\ []) do
    %{mat_A: mat_A, mat_B: mat_B, mat_C: mat_C, mat_D: mat_D, mat_E: mat_E} = arg

    condition = opts[:condition]
    n_trees = opts[:n_trees]
    n_classes = opts[:n_classes]
    max_decision_nodes = opts[:max_decision_nodes]
    max_leaf_nodes = opts[:max_leaf_nodes]
    n_weak_learner_classes = opts[:n_weak_learner_classes]

    mat_A
    |&amp;gt; Nx.dot([1], x, [1])
    |&amp;gt; condition.(mat_B)
    |&amp;gt; Nx.reshape({n_trees, max_decision_nodes, :auto})
    |&amp;gt; then(&amp;amp;Nx.dot(mat_C, [2], [0], &amp;amp;1, [1], [0]))
    |&amp;gt; Nx.reshape({n_trees * max_leaf_nodes, :auto})
    |&amp;gt; Nx.equal(mat_D)
    |&amp;gt; Nx.reshape({n_trees, max_leaf_nodes, :auto})
    |&amp;gt; then(&amp;amp;Nx.dot(mat_E, [2], [0], &amp;amp;1, [1], [0]))
    |&amp;gt; Nx.reshape({n_trees, n_weak_learner_classes, :auto})
    |&amp;gt; Nx.transpose()
    |&amp;gt; Nx.reshape({:auto, n_trees, n_classes})
  end&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Do not be distracted by the length of this code snippet, as much of the lines are taken up by validating arguments. Let&amp;#39;s look at a more stripped-down version without that:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-elixir&quot;&gt;@impl true
  deftransform forward(x, {arg, opts}) do
    _forward(x, arg, opts)
  end

  defnp _forward(x, arg, opts \\ []) do
    mat_A
    |&amp;gt; Nx.dot([1], x, [1])
    |&amp;gt; condition.(mat_B)
    |&amp;gt; Nx.reshape({n_trees, max_decision_nodes, :auto})
    |&amp;gt; then(&amp;amp;Nx.dot(mat_C, [2], [0], &amp;amp;1, [1], [0]))
    |&amp;gt; Nx.reshape({n_trees * max_leaf_nodes, :auto})
    |&amp;gt; Nx.equal(mat_D)
    |&amp;gt; Nx.reshape({n_trees, max_leaf_nodes, :auto})
    |&amp;gt; then(&amp;amp;Nx.dot(mat_E, [2], [0], &amp;amp;1, [1], [0]))
    |&amp;gt; Nx.reshape({n_trees, n_weak_learner_classes, :auto})
    |&amp;gt; Nx.transpose()
    |&amp;gt; Nx.reshape({:auto, n_trees, n_classes})
  end&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Let&amp;#39;s take a look at some obvious difference:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;The &lt;code&gt;Nx&lt;/code&gt; code does not have to transpose in the first step since &lt;code&gt;Nx.dot/4&lt;/code&gt; allows you to specify the contracting axes.&lt;/li&gt;&lt;li&gt;You can use &lt;code&gt;Nx.dot/6&lt;/code&gt; to get the same behavior as &lt;code&gt;torch.matmul&lt;/code&gt;&lt;ul&gt;&lt;li&gt;&lt;code&gt;torch.matmul&lt;/code&gt; does a lot of wizardry with broadcasting to make this instance work&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;We use functions such as &lt;code&gt;Nx.equal&lt;/code&gt; to fit into the pipeline rather than using the &lt;code&gt;==&lt;/code&gt; oeprator (which would work outside of a pipeline)&lt;/li&gt;&lt;li&gt;&lt;code&gt;torch.view&lt;/code&gt; is equivalent to &lt;code&gt;Nx.reshape&lt;/code&gt;&lt;/li&gt;&lt;li&gt;&lt;code&gt;Nx&lt;/code&gt; uses the &lt;code&gt;:auto&lt;/code&gt; atom to where &lt;code&gt;torch&lt;/code&gt; uses &lt;code&gt;-1&lt;/code&gt; to reference infering the sie of an axis&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Outside of these differences, the code translates fairly easily. Let&amp;#39;s take a look at a bit of a more complex instance.&lt;/p&gt;&lt;h4&gt;Tree Traversal&lt;/h4&gt;&lt;p&gt;Here is the Python implementation:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def _expand_indexes(self, batch_size):
        indexes = self.nodes_offset
        indexes = indexes.expand(batch_size, self.num_trees)
        return indexes.reshape(-1)

def forward(self, x):
        indexes = self.nodes_offset
        indexes = indexes.expand(batch_size, self.num_trees).reshape(-1)

        for _ in range(self.max_tree_depth):
            tree_nodes = indexes
            feature_nodes = torch.index_select(self.features, 0, tree_nodes).view(-1, self.num_trees)
            feature_values = torch.gather(x, 1, feature_nodes)

            thresholds = torch.index_select(self.thresholds, 0, indexes).view(-1, self.num_trees)
            lefts = torch.index_select(self.lefts, 0, indexes).view(-1, self.num_trees)
            rights = torch.index_select(self.rights, 0, indexes).view(-1, self.num_trees)

            indexes = torch.where(self.decision_cond(feature_values, thresholds), lefts, rights).long()
            indexes = indexes + self.nodes_offset
            indexes = indexes.view(-1)

        output = torch.index_select(self.values, 0, indexes).view(-1, self.num_trees, self.n_classes)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And here is the Nx implementation:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-elixir&quot;&gt;defn _forward(x, features, lefts, rights, thresholds, nodes_offset, values, opts \\ []) do
    max_tree_depth = opts[:max_tree_depth]
    num_trees = opts[:num_trees]
    n_classes = opts[:n_classes]
    condition = opts[:condition]
    unroll = opts[:unroll]

    batch_size = Nx.axis_size(x, 0)

    indices =
      nodes_offset
      |&amp;gt; Nx.broadcast({batch_size, num_trees})
      |&amp;gt; Nx.reshape({:auto})

    {indices, _} =
      while {tree_nodes = indices, {features, lefts, rights, thresholds, nodes_offset, x}},
            _ &amp;lt;- 1..max_tree_depth,
            unroll: unroll do
        feature_nodes = Nx.take(features, tree_nodes) |&amp;gt; Nx.reshape({:auto, num_trees})
        feature_values = Nx.take_along_axis(x, feature_nodes, axis: 1)
        local_thresholds = Nx.take(thresholds, tree_nodes) |&amp;gt; Nx.reshape({:auto, num_trees})
        local_lefts = Nx.take(lefts, tree_nodes) |&amp;gt; Nx.reshape({:auto, num_trees})
        local_rights = Nx.take(rights, tree_nodes) |&amp;gt; Nx.reshape({:auto, num_trees})

        result =
          Nx.select(
            condition.(feature_values, local_thresholds),
            local_lefts,
            local_rights
          )
          |&amp;gt; Nx.add(nodes_offset)
          |&amp;gt; Nx.reshape({:auto})

        {result, {features, lefts, rights, thresholds, nodes_offset, x}}
      end

    values
    |&amp;gt; Nx.take(indices)
    |&amp;gt; Nx.reshape({:auto, num_trees, n_classes})
  end&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Here there are some much more striking differences, namely the use of &lt;code&gt;Nx&lt;/code&gt;&amp;#39;s &lt;code&gt;while&lt;/code&gt; expression compared to a &lt;code&gt;for&lt;/code&gt; loop in Python. We use &lt;code&gt;while&lt;/code&gt; in this case since it can achieve the same purpose as the Python &lt;code&gt;for&lt;/code&gt; loop and it is supported by &lt;code&gt;Nx&lt;/code&gt; within a &lt;code&gt;defn&lt;/code&gt; expression. Otherwise, we might have to perform some of the calculations within a &lt;code&gt;deftransform&lt;/code&gt;, as we will see in the next example.  Another obvious difference is that in the Nx implementation, we have to pass the required variables around throughout these operation, whereas Python can use stored class attributes. &lt;/p&gt;&lt;p&gt;Still, the conversion is quite straightforward. I hope you are beginning to see that this is not an impossible effort, and can be accomplished given you have a firm understanding of the source material.&lt;/p&gt;&lt;h4&gt;Perfect Tree Traversal&lt;/h4&gt;&lt;p&gt;Lastly, let&amp;#39;s look at the last conversion strategy. Yet again, this conversion is even slightly more complex, but hopefully seeing this example will help you in your case:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def forward(self, x):
        prev_indices = (self.decision_cond(torch.index_select(x, 1, self.root_nodes), self.root_biases)).long()
        prev_indices = prev_indices + self.tree_indices
        prev_indices = prev_indices.view(-1)

        factor = 2
        for nodes, biases in zip(self.nodes, self.biases):
            gather_indices = torch.index_select(nodes, 0, prev_indices).view(-1, self.num_trees)
            features = torch.gather(x, 1, gather_indices).view(-1)
            prev_indices = (
                factor * prev_indices + self.decision_cond(features, torch.index_select(biases, 0, prev_indices)).long()
            )

        output = torch.index_select(self.leaf_nodes, 0, prev_indices).view(-1, self.num_trees, self.n_classes)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And the Elixir implementation:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-elixir&quot;&gt;defnp _forward(
          x,
          root_features,
          root_thresholds,
          features,
          thresholds,
          values,
          indices,
          opts \\ []
        ) do
    prev_indices =
      x
      |&amp;gt; Nx.take(root_features, axis: 1)
      |&amp;gt; opts[:condition].(root_thresholds)
      |&amp;gt; Nx.add(indices)
      |&amp;gt; Nx.reshape({:auto})
      |&amp;gt; forward_reduce_features(x, features, thresholds, opts)

    Nx.take(values, prev_indices)
    |&amp;gt; Nx.reshape({:auto, opts[:num_trees], opts[:n_classes]})
  end

  deftransformp forward_reduce_features(prev_indices, x, features, thresholds, opts \\ []) do
    Enum.zip_reduce(
      Tuple.to_list(features),
      Tuple.to_list(thresholds),
      prev_indices,
      fn nodes, biases, acc -&amp;gt;
        gather_indices = nodes |&amp;gt; Nx.take(acc) |&amp;gt; Nx.reshape({:auto, opts[:num_trees]})
        features = Nx.take_along_axis(x, gather_indices, axis: 1) |&amp;gt; Nx.reshape({:auto})

        acc
        |&amp;gt; Nx.multiply(@factor)
        |&amp;gt; Nx.add(opts[:condition].(features, Nx.take(biases, acc)))
      end
    )
  end&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;You can see that in this case, we have a function defined in a &lt;code&gt;deftransform&lt;/code&gt; within our &lt;code&gt;forward&lt;/code&gt; pipeline. Why is this so? Well, when writing definitions within &lt;code&gt;defn&lt;/code&gt; you forfeit the use of the default Elixir kernel for the &lt;code&gt;Nx.Kernel&lt;/code&gt; module. If you want full access to all of the normal Elixir modules, you need to use a &lt;code&gt;deftransform&lt;/code&gt;. We needed to use &lt;code&gt;Enum.zip_reduce&lt;/code&gt; in this instance (rather than &lt;code&gt;Nx&lt;/code&gt;&amp;#39;s &lt;code&gt;while&lt;/code&gt; like before) since the &lt;code&gt;features&lt;/code&gt; and &lt;code&gt;thresholds&lt;/code&gt; lists are not of uniform shape. Their shape represents the length of a given depth of a binary tree, so they will be a nested list of lengths &lt;code&gt;[1,2,4,8...]&lt;/code&gt;. This is an optimization as opposed to normal &lt;code&gt;TreeTraversal&lt;/code&gt;, but required a bit of a different approach as opposed to the Python implementation which took advantage of &lt;code&gt;torch.nn.ParameterList&lt;/code&gt; to build out the same lists. You might also notice the use of &lt;code&gt;Tuple.to_list&lt;/code&gt; on lines 25 and 26. This was required since we needed &lt;code&gt;features&lt;/code&gt; and &lt;code&gt;thresholds&lt;/code&gt; to be stored in &lt;code&gt;Nx.container&lt;/code&gt;&amp;#39;s when passed into the &lt;code&gt;deftransform&lt;/code&gt;, and &lt;code&gt;Tuple&lt;/code&gt; implements the &lt;code&gt;Nx.Container&lt;/code&gt; protocol, while lists do not. Even still, given that knowledge of the intricacies of &lt;code&gt;defn&lt;/code&gt; and &lt;code&gt;deftransform&lt;/code&gt;, the final ported solution is very similar to the reference solution.&lt;/p&gt;&lt;h1&gt;Conclusion&lt;/h1&gt;&lt;p&gt;In this post, I tried to accomplish several things at once, and perhaps that lead to a cluttered article, but I felt the need to address all of these points at once. I do not mean to suggest that Machine Learning has no place in Python or that Python will not continue to be the most dominant player in Machine Learning, but that I think some healthy competition is a good thing, and that perhaps Python does have some shortcomings that might give other languages valid reasons to coexist in the space. &lt;/p&gt;&lt;p&gt;Next, I wanted to address some specifics as to what Elixir has to offer to the machine learning space. I think it is uniquely positioned to be quite competitive considering the large community push to support more and more libraries, as well as the large application development community that can benefit from an in-house solution.&lt;/p&gt;&lt;p&gt;Lastly, I wanted to share some practical tips for those looking to move on from Python to Elixir, but feeling somewhat helpless in the process. I think that Sean Moriarity&amp;#39;s book that I mentioned at the beginning of this article is an invaluable resource and great step in the education of machine learning for Elixir developers, but it can nonetheless feel daunting to seemingly throw out existing working solutions for new-fangled, perhaps not as well respected solutions. I hope I showed how anybody can approach this problem, and any existing Elixir developer can be a machine learning developer going forward. The ground work has been laid, and the tools are available. Thank you for reading (especially if you made it to the end)!&lt;/p&gt;</content:encoded>
</item>
<item>
<title>Cure, Four Releases Deep: From FSMs to Furniture</title>
<link>https://rocket-science.ru/hacking/2026/04/18/cure-v016-to-v019</link>
<enclosure type="image/jpeg" length="0" url="https://rocket-science.ru/img/logo/logo-orig.png"></enclosure>
<guid isPermaLink="false">NqWEtqE765iKeAlfG-VPuRWvFXYIkLwy5jAlUA==</guid>
<pubDate>Sat, 18 Apr 2026 11:56:20 +0000</pubDate>
<description>A walk through Cure v0.16.0 through v0.19.1. A Finitomata-inspired FSM rewrite, the dependent-types core finally behaving like it promised, a pattern engine that actually matches, and a stack of ergonomics that should have been there six versions ago.</description>
<content:encoded>&lt;p&gt;There is a particular kind of intellectual cowardice endemic to programming-language design, which consists of writing a feature that almost works, convincing oneself that it works enough, and then moving on to the next item on the roadmap before the cracks start to show. I have indulged in this pastime for years. Cure, for the first dozen or so releases, indulged in it with me. The last four tags—v0.16.0, v0.17.0 with its two patch siblings, v0.18.0, and v0.19.0 followed by v0.19.1—are my belated attempt to stop. Each of them picks one area of the language that had previously been garnished with a thin coat of varnish and strips it down to bare wood.&lt;/p&gt;&lt;p&gt;What follows is the tour. I will try to explain both what changed and why it had to. If the tone reads as exasperated in places, that is because I am mostly exasperated at the person who wrote the earlier versions, who was, as luck would have it, me.&lt;/p&gt;&lt;h2&gt;v0.16.0: the turnstile that could not contain itself&lt;/h2&gt;&lt;p&gt;For fifteen releases Cure had finite state machines as a language primitive, and for fifteen releases the canonical FSM example—the turnstile—looked like this: four lines of beautifully declarative transition graph in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turnstile.cure&lt;/code&gt;, followed by a hundred and twenty lines of Elixir GenServer plumbing in a wrapper module that had nothing to do with turnstiles and everything to do with bridging the gap between a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gen_statem&lt;/code&gt; process and the rest of the application. The FSM definition said what you meant. The wrapper said what the runtime demanded. You read one of them to understand the domain and the other to make the program run. I cannot in good conscience call that a first-class primitive. It was more of a first-class primitive’s slightly embarrassed cousin, the one who turns up at family dinners and talks about his crypto portfolio.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://hexdocs.pm/finitomata&quot;&gt;Finitomata&lt;/a&gt;, my Elixir library for finite automata that has been in production for years, got this right on the first attempt by insisting that the graph and the transition handler belong in the same module and behind the same abstraction. v0.16.0 borrows that insight wholesale, and then some. The turnstile now reads:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-cure&quot;&gt;fsm Turnstile with Integer
  Locked   --coin--&amp;gt;  Unlocked
  Unlocked --push--&amp;gt;  Locked
  Unlocked --coin--&amp;gt;  Unlocked
  Locked   --push--&amp;gt;  Locked

  on_transition
    (:locked, :coin, _payload, data)   -&amp;gt; %[:ok, :unlocked, data + 1]
    (:unlocked, :push, _payload, data) -&amp;gt; %[:ok, :locked,   data]
    (:unlocked, :coin, _payload, data) -&amp;gt; %[:ok, :unlocked, data + 1]
    (_, _, _, data)                    -&amp;gt; %[:ok, :__same__, data]&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The four transition lines on top are the graph you always wrote. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;on_transition&lt;/code&gt; block underneath takes pattern-matching clauses of shape &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(current_state, event, event_payload, state_payload)&lt;/code&gt; and returns either &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;%[:ok, next_state, new_payload]&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;%[:error, reason]&lt;/code&gt;. When the compiler sees an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;on_transition&lt;/code&gt; block it silently changes mode: instead of generating raw &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gen_statem&lt;/code&gt; Erlang abstract forms, it produces a GenServer-based Elixir module with an embedded transition table, pre-dispatch validation, compiled &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;do_on_transition/4&lt;/code&gt; clauses, and the optional lifecycle hooks &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;on_enter&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;on_exit&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;on_failure&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;on_timer&lt;/code&gt;. If you do not write &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;on_transition&lt;/code&gt;, the FSM compiles the way it always did, through Erlang abstract forms to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gen_statem&lt;/code&gt;. No flags, no configuration—the compiler decides based on whether you asked for the new thing.&lt;/p&gt;&lt;p&gt;Finitomata also contributed two conventions that I refused to live without once I tried them. A hard event, written with a trailing exclamation mark, must be the sole outgoing event of its source state, and fires automatically on arrival, so initialisation chains and guaranteed progressions stop requiring baby-sitting from the caller. A soft event, written with a trailing question mark, silently leaves the state alone if it cannot transition, instead of logging a warning and invoking &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;on_failure&lt;/code&gt;. Health checks, optimistic polling, any kind of “try it, and if it does not apply this tick, never mind”—all become one-liners. The lexer grew a small counter that tracks whether it is currently inside an arrow &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--...--&amp;gt;&lt;/code&gt;, and only inside an arrow does it absorb the trailing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;!&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;?&lt;/code&gt; into the identifier. Everywhere else, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;!&lt;/code&gt; stays reserved for effect annotations and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;?&lt;/code&gt; for predicates and holes. The verifier enforces the hard-event rule; the compiler uses &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{:continue, ...}&lt;/code&gt; from the GenServer return tuple to fire hard events without yielding.&lt;/p&gt;&lt;p&gt;The turnstile’s wrapper is now fifty lines rather than one-hundred-and-twenty, and the fifty it retains are real application logic: counting how many people went through today. The twelve turnstile tests pass without modification, which is the only part of this release I will allow myself to take unironic pride in.&lt;/p&gt;&lt;h2&gt;v0.17.0: toward Idris, at last&lt;/h2&gt;&lt;p&gt;For seven versions Cure had been marketing itself as a dependently-typed language while behaving, in every practical respect, like a vaguely refinement-typed one. You could write &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Vector(T, n)&lt;/code&gt; in a signature. The checker would nod politely. It would not, however, verify that the length you promised was the length you produced, because the machinery to do so did not exist. v0.17.0 stops nodding.&lt;/p&gt;&lt;p&gt;Three type shapes that should have been there from the start arrive simultaneously. Sigma types pair a value with a type that depends on it; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Sigma(n: Nat, Vector(T, n))&lt;/code&gt; is “a natural number together with a vector of exactly that length”. Pi types let a function’s return type depend on its arguments, so the canonical example&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-cure&quot;&gt;fn append(xs: Vector(T, m), ys: Vector(T, n)) -&amp;gt; Vector(T, m + n)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;now actually means what it reads as: at each call site the checker substitutes the concrete arguments into the return type, normalises with a tiny terminating reducer called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Cure.Types.Reduce&lt;/code&gt;, and resolves the result. Closed type-level arithmetic never troubles the SMT solver—&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Vector(T, 2 + 3)&lt;/code&gt; is syntactically the same type as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Vector(T, 5)&lt;/code&gt; before Z3 is even woken up. Equality types arrive with the single constructor &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;refl(x) : Eq(T, x, x)&lt;/code&gt; and the single eliminator &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rewrite eq in expr&lt;/code&gt;, both erased at codegen to the atom &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:cure_refl&lt;/code&gt;. The standard library gains a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Std.Equal&lt;/code&gt; module exposing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;refl&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sym&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;trans&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cong&lt;/code&gt;, which is to say the usual suspects.&lt;/p&gt;&lt;p&gt;The second big arrival is implicit arguments with proper first-order unification. Write &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fn id({T}, x: T) -&amp;gt; T = x&lt;/code&gt;, call &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;id(42)&lt;/code&gt;, and at the call site an occurs-check-equipped unifier resolves &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;T&lt;/code&gt; from the explicit argument. When resolution fails—and it does, more often than one would like—the pipeline emits a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:unification_trace&lt;/code&gt; event carrying the argument, the position, and the substitution that killed it. The LSP renders the trace in hover; the CLI prints it in error output. No more staring at “cannot infer &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;T&lt;/code&gt;” the way one stares at a Rorschach blot. Implicit parameters are erased at codegen, so they cost exactly nothing at runtime.&lt;/p&gt;&lt;p&gt;What ties the dependent-type stack together, in practical terms, is hole-driven development. Write&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-cure&quot;&gt;fn safe_head(xs: List(T)) -&amp;gt; T = ?body&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;and compile, and the compiler does not merely tell you “&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;?body&lt;/code&gt; is missing”: it tells you &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;?body : T in scope: xs : List(T)&lt;/code&gt;. Anonymous holes (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;??&lt;/code&gt;) get numbered &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;?_1&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;?_2&lt;/code&gt;, in source order. Every encountered hole emits a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:hole_goal&lt;/code&gt; event with the goal type and the local context, and the REPL’s new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:holes&lt;/code&gt; meta-command lists everything recorded during the last evaluation. This is how Idris programmers write programs. It is now, finally, how Cure programmers can.&lt;/p&gt;&lt;p&gt;Totality joins the party, gently. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Cure.Types.Totality&lt;/code&gt; classifies every function as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:total&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:partial&lt;/code&gt;, or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:unknown&lt;/code&gt;, combining pattern-coverage (via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Cure.Types.PatternChecker&lt;/code&gt;) with a structural-recursion check. By default totality is report-only; decorating a function with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;#[total]&lt;/code&gt; promotes the classification to a hard error if it fails:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-cure&quot;&gt;#[total]
fn factorial(n: Int) -&amp;gt; Int
  | 0 -&amp;gt; 1
  | n -&amp;gt; n * factorial(n - 1)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Only direct structural recursion is caught in v0.17.0. Mutual recursion has to wait for v0.19.0.&lt;/p&gt;&lt;p&gt;Refinement types grow a backbone. Path-sensitive refinement flows along &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;if&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;match&lt;/code&gt; guards, so inside&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-cure&quot;&gt;if x != 0 then 100 / x else 0&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;then&lt;/code&gt; branch sees &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;x : {x: Int | x != 0}&lt;/code&gt;, and the division is safe without an explicit refinement annotation. The new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Std.Refine&lt;/code&gt; module ships drop-in refinements one does not wish to keep rewriting: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NonZero&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Positive&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Negative&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NonNegative&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Percentage&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Probability&lt;/code&gt;, plus predicate helpers. These are the kinds of tiny conveniences that, had they been present in v0.10.0, would have saved everyone a small amount of grief each day for a year.&lt;/p&gt;&lt;p&gt;The tooling catches up in the same release. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Cure.REPL&lt;/code&gt; is a complete rewrite: multi-line input (terminated by a blank line or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;;;&lt;/code&gt;), the meta-commands &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:t&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:doc&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:effects&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:load&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:reload&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:use&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:holes&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:env&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:reset&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:fmt&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:help&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:quit&lt;/code&gt;, and command history persisted to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/.cure_history&lt;/code&gt;. A watch mode, invoked as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cure watch lib/ --action check&lt;/code&gt;, recompiles, type-checks, or tests on every save with a 200 ms debounce, and works without &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:file_system&lt;/code&gt; thanks to a small polling fallback. The LSP server, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Cure.LSP.Server&lt;/code&gt;, acquires inlay hints, signature help, formatting via a round-trip-tested source-preserving printer, prepare-rename and rename, code lenses, semantic tokens, and workspace symbols—seven capabilities in one commit, which is the sort of thing that happens when one has been putting off the LSP for three releases in a row. Doctests arrive, too: any &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;##&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;###&lt;/code&gt; docstring immediately above a function whose body contains &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cure&amp;gt;&lt;/code&gt; / &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;=&amp;gt;&lt;/code&gt; pairs is executed by &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cure test --doctests&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;The patch releases tidied up. v0.17.1 stopped the stdlib preloader from polluting the code path, fixed two LSP crashes around inlay hints and semantic tokens, and retargeted the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vicure&lt;/code&gt; grammar at v0.17.0 syntax. v0.17.2 re-enabled LSP formatting once the new source-preserving formatter proved it would not eat users’ comments for breakfast—a promise the old AST-based formatter had never quite been willing to make.&lt;/p&gt;&lt;h2&gt;v0.18.0: pattern matching grows up&lt;/h2&gt;&lt;p&gt;Here is a question I did not want to answer for a long time. If one writes&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-cure&quot;&gt;match value
  %{list: [h | t]} -&amp;gt; handle(h, t)
  _                -&amp;gt; default&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;and the subject value is a map whose &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;list&lt;/code&gt; field is not present, what does the old compiler do? The correct answer is “fails the match and falls through to the wildcard”. The answer the old compiler produced is “succeeds, because the map pattern is miscompiled into Erlang’s construction form (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;K =&amp;gt; V&lt;/code&gt;) rather than the match form (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;K := V&lt;/code&gt;), so it accepts any subject, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;h&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;t&lt;/code&gt; are bound to whatever happens to be lying around, possibly nothing sensible”. There were tests pointing straight at this. What there was not, was a pattern compiler willing to recurse.&lt;/p&gt;&lt;p&gt;v0.18.0 replaces the pattern layer wholesale. The headline is that this now actually works:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-cure&quot;&gt;match value
  %[_, %{list: [head | tail]}, _]          -&amp;gt; handle(head, tail)
  Person{name, address: Address{city}}     -&amp;gt; greet(name, city)
  [Ok(v) | _]                              -&amp;gt; v
  _                                        -&amp;gt; default&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Every sub-pattern compiles as a real pattern. Nested map patterns use exact matching and actually require the key. Record patterns check the struct tag and each field. The cons pattern binds &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;v&lt;/code&gt; through the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Ok(...)&lt;/code&gt; constructor. The wildcard mops up the rest. There is no magic here; merely a module (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Cure.Compiler.PatternCompiler&lt;/code&gt;) that does the one thing its name promises, and which every other codegen path—&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;compile_multi_clause_function&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;compile_pattern_match&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;compile_assignment&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;compile_comprehension&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;compile_catch_and_finally&lt;/code&gt;—now routes through rather than reinventing pattern handling locally. The original sin, I discovered after far too long, was treating patterns and expressions uniformly because their AST nodes have the same shape. Patterns are not evaluated; they are matched against a subject with bindings as side effects. Nothing good comes of pretending otherwise.&lt;/p&gt;&lt;p&gt;Alongside the compiler rewrite, the type checker’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bind_pattern_vars/3&lt;/code&gt; was rewritten to thread the scrutinee type through every pattern shape, so nested pattern variables now carry the tightest type the structure allows rather than the old &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:any&lt;/code&gt;. Tuple patterns zip element-wise. List and cons patterns bind head and tail at the correct element and list types respectively. Map patterns look up each key through the scrutinee’s schema when it is a known record, or through the map value type otherwise. Record patterns resolve every field against the schema registered at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rec&lt;/code&gt; time, and unknown fields emit a warning under the new error code &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;E021&lt;/code&gt;. The practical consequence is that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;match p { Person{age: a} when a &amp;gt; 17 -&amp;gt; ... }&lt;/code&gt; finally knows &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;a : Int&lt;/code&gt;, so subsequent refinement fires against the right type.&lt;/p&gt;&lt;p&gt;Field punning arrives in both directions: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Point{x, y}&lt;/code&gt; desugars to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Point{x: x, y: y}&lt;/code&gt; in pattern position and in construction position. It is purely a parser change, and if you have ever found yourself writing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Person{name: name, age: age, email: email, ...}&lt;/code&gt; seven fields deep you will be glad of it. Repeated occurrences of the same variable in one pattern now do what a reasonable person would expect: the first occurrence binds, later ones lower to equality guards. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;%[x, x]&lt;/code&gt; matches exactly the pairs whose slots are equal, which I should have supported five releases ago.&lt;/p&gt;&lt;p&gt;The pin operator (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;^name&lt;/code&gt;) lands behind &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--experimental-pin&lt;/code&gt; as the official escape hatch for “compare against this already-bound value”:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-cure&quot;&gt;let target = get_tag()
match event.tag
  ^target -&amp;gt; :hit
  _       -&amp;gt; :miss&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Internally it lowers to a fresh variable plus a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;V_fresh =:= V_target&lt;/code&gt; guard—the same transformation one used to write by hand. Zero runtime cost, considerably fewer characters on screen. It is promoted to default in v0.19.0.&lt;/p&gt;&lt;p&gt;Exhaustiveness checking grows a second pass. The old flat classifier (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:wildcard | :empty_list | :cons | {:literal, ...} | {:constructor, ...} | {:tuple, n}&lt;/code&gt;) is still there, because it is fast and covers the common case; a new Maranget-style column walker, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Cure.Types.PatternChecker.check_nested/2&lt;/code&gt;, now descends into tuple scrutinees whose element types are enumerable and emits concrete, source-shaped witnesses for missing patterns:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;Warning: match expression has nested non-exhaustive cases (E025)
  missing: %[Error(_), _]&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;Five new error codes—&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;E021&lt;/code&gt; through &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;E025&lt;/code&gt;—land in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Cure.Compiler.Errors&lt;/code&gt; for unknown record fields in patterns, record field type mismatches, non-literal map-pattern keys, unbound pin variables, and non-exhaustive nested matches. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cure explain E0xx&lt;/code&gt; works for every one.&lt;/p&gt;&lt;p&gt;There is a caveat worth naming explicitly. Map patterns that used to silently succeed against arbitrary subjects no longer do so. If your code relied on the old broken behaviour—for example, a map pattern whose key was not in the subject but which the compiler accepted anyway—you will now see either a runtime &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;badmatch&lt;/code&gt; or a compile-time non-exhaustive warning. Fix the pattern. The compiler is right and you are wrong; I say this with the humility of the person who wrote the earlier compiler.&lt;/p&gt;&lt;h2&gt;v0.19.0: the furniture arrives&lt;/h2&gt;&lt;p&gt;After the pattern engine settled, a queue of previously-slated features was ready to land. v0.19.0 is called “Bring the Furniture” because every item on it is the kind of thing one should not have to talk about in a release announcement; one should merely discover, pleasantly, that it is there.&lt;/p&gt;&lt;p&gt;Propositions acquire a syntactic home. A &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;proof&lt;/code&gt; container is a module-shaped block whose bindings must return &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Eq(...)&lt;/code&gt; or a refinement witness:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-cure&quot;&gt;proof Std.Proof
  fn plus_zero(_n: Int)       -&amp;gt; Eq(Int, n, n)        = :cure_refl
  fn append_nil(_xs: List(T)) -&amp;gt; Eq(List(T), xs, xs)  = :cure_refl&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The compiler enforces the shape under &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;E026&lt;/code&gt;. Runtime values are plain &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:cure_refl&lt;/code&gt; atoms; the checker does the interesting work; the resulting BEAM module is ordinary-looking code you can load alongside any other. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;assert_type expr : T&lt;/code&gt; arrives as a zero-cost compile-time assertion: the checker verifies the type, the codegen strips the wrapper, and mismatches surface as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;E027&lt;/code&gt;. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fn doubled(n: Int) -&amp;gt; Int = assert_type n * 2 : Int&lt;/code&gt; does what it looks like.&lt;/p&gt;&lt;p&gt;Records gain field defaults, which is the sort of feature whose absence one does not forgive oneself:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-cure&quot;&gt;rec Person
  name: String = &amp;quot;Anonymous&amp;quot;
  age: Int = 0
  active: Bool = true&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Omitted fields fall back to the declared defaults at construction time; any caller-supplied value always wins. Type mismatches between the default and the declared field type are caught as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;E028&lt;/code&gt;. In the same spirit, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@derive(Show, Eq, Ord)&lt;/code&gt; is finally wired end-to-end. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Cure.Types.Derive&lt;/code&gt; module had been sitting on disk since v0.12.0 without a codegen path; v0.19.0 plumbs it through so that decorating&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-cure&quot;&gt;@derive(Show, Eq, Ord)
rec Point
  x: Int
  y: Int&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;synthesises plain &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;show/1&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;eq/2&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;compare/2&lt;/code&gt; exports. The accompanying example, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;examples/derived_show.cure&lt;/code&gt;, constructs two &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Point&lt;/code&gt; values, asks &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;eq(p, q)&lt;/code&gt;, and returns one if they compare equal—which, naturally, they do.&lt;/p&gt;&lt;p&gt;Property-based testing shows up via two cooperating stdlib modules. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Std.Gen&lt;/code&gt; ships tiny stateless generators (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;int_in&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bool&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;list_int&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;list_of_int&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;one_of&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;constant&lt;/code&gt;) backed by &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:rand&lt;/code&gt;. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Std.Test.forall(gen, property, runs)&lt;/code&gt; runs the property against samples and returns &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:ok&lt;/code&gt; or raises &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:property_failed&lt;/code&gt;:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-cure&quot;&gt;mod Laws
  use Std.Gen
  use Std.Test

  fn test_plus_zero() -&amp;gt; Atom =
    forall(fn(_) -&amp;gt; int_in(-100, 100), fn(n) -&amp;gt; n + 0 == n, 100)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Shrinking, histograms, and stateful generators are future work; this is deliberately the minimum viable property tester, and it is enough to catch the kind of bug one always catches with a property tester in the first week.&lt;/p&gt;&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Std.Iter&lt;/code&gt; is the matching minimal lazy iterator protocol. Constructors &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;empty/0&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;from_list/1&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;range/2&lt;/code&gt;; consumers &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fold/3&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;take/2&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;to_list/1&lt;/code&gt;. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;take/2&lt;/code&gt; stops before materialising the tail, so unbounded ranges are safe as long as you only peek at a prefix:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-cure&quot;&gt;use Std.Iter

fn sum_range(n: Int) -&amp;gt; Int =
  let it = range(1, n)
  let add = fn(x) -&amp;gt; fn(acc) -&amp;gt; acc + x
  fold(it, 0, add)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The package-registry groundwork lands as a version parser and a dependency resolver. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Cure.Project.Version&lt;/code&gt; handles SemVer plus compound constraints (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~&amp;gt;&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;gt;=&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;=&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;gt;&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;==&lt;/code&gt;, combined with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;and&lt;/code&gt;), and accepts MAJOR.MINOR as shorthand for MAJOR.MINOR.0. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Cure.Project.Resolver.resolve/2&lt;/code&gt; is a deterministic backtracking resolver over a local registry that picks the newest compatible version and surfaces conflicts as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;E030&lt;/code&gt;. A remote index service, signing, and Hex.pm cross-publishing are what v0.20.0 is for.&lt;/p&gt;&lt;p&gt;Totality catches up with multi-function cycles. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Cure.Types.Totality.check_mutual/1&lt;/code&gt; runs Tarjan’s SCC algorithm on the module call graph, then classifies each non-trivial strongly-connected component as either &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:ok&lt;/code&gt; (structural decrease proven on at least one path) or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:suspect&lt;/code&gt; (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;E029&lt;/code&gt;). It is not a full termination checker—nothing running at compile time on the BEAM is going to be—but it is strictly more than nothing, and it catches the obvious cases.&lt;/p&gt;&lt;p&gt;One last syntactic nicety: multi-head cons patterns,&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-cure&quot;&gt;match xs
  [a, b, c | rest] -&amp;gt; a + b + c
  _                -&amp;gt; 0&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;desugared by the parser into right-associated cons cells. Works in pattern and construction position. It is the sort of feature that every functional language grows eventually; we might as well have grown ours here.&lt;/p&gt;&lt;p&gt;Five new error codes (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;E026&lt;/code&gt; through &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;E030&lt;/code&gt;) round out the error catalog for the release, all of them available via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cure explain&lt;/code&gt;.&lt;/p&gt;&lt;h2&gt;v0.19.1: Dialyzer and the small sins&lt;/h2&gt;&lt;p&gt;The point release should never be the interesting one, and v0.19.1 honours that convention. Its job was to add Dialyzer to the CI matrix (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.github/workflows/ci.yml&lt;/code&gt;), resolve the backlog of specs it turned up across &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Cure.Compiler&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Cure.Compiler.Codegen&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Cure.FSM.Compiler&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Cure.Types.Env&lt;/code&gt;, and friends, and tidy a handful of things that were unclean but not broken: the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mix cure.escript&lt;/code&gt; task got proper CLI integration, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mix.exs&lt;/code&gt; lost a dead dependency, and the stdlib preloader started force-compiling in tests instead of hoping the beams from the last run would still be fresh. None of this is exciting. All of it needed to happen, because a language without Dialyzer in CI is a language whose maintainer is lying to themselves about how robust its internals are.&lt;/p&gt;&lt;p&gt;The small print, for anyone upgrading. The lexer rule for trailing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;?&lt;/code&gt; on identifiers introduced in v0.17.0 means &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;x?y&lt;/code&gt; now tokenises as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;x?&lt;/code&gt; followed by &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;y&lt;/code&gt;; add whitespace or parentheses if you need the old behaviour. Map patterns that relied on the pre-v0.18.0 construction-form bug will now fail matches honestly. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;None&lt;/code&gt; versus &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;None()&lt;/code&gt; inside a record pattern now emits a warning when you probably meant the nullary constructor. And &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Cure.lock&lt;/code&gt; lockfiles produced by v0.19.0 and v0.19.1 remain source-compatible with v0.17.0 and later, but a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mix cure.compile_stdlib &amp;amp;&amp;amp; mix cure.check&lt;/code&gt; after pulling is cheap insurance.&lt;/p&gt;&lt;h2&gt;What all this adds up to&lt;/h2&gt;&lt;p&gt;If you have read this far you may have noticed a pattern. Each of these releases took a subsystem that the earlier Cure treated as decorative and made it load-bearing. FSMs stopped being a transition graph with a separately-authored runtime and became a language primitive one can actually define in one place. The dependent-type core stopped nodding politely at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Vector(T, m + n)&lt;/code&gt; and started checking it. The pattern engine stopped accepting whatever came past and started matching. The furniture release filled in the items one would notice the absence of but would not necessarily think to name: defaults, derive, property tests, a lazy iterator, a version parser, mutual-recursion totality. And the point release added the tool (Dialyzer) that would have prevented some of these embarrassments from landing in the first place.&lt;/p&gt;&lt;p&gt;There is still a long queue. Full bitstring pattern specifiers. Refinement narrowing through nested record and map patterns. The remote package-registry index service with its signing and Hex.pm cross-publishing. True dependent types in their full glory—&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fn len(l: Vector(_, n)) -&amp;gt; NonNegInt = n&lt;/code&gt; remains, for the moment, a thing I write in the v0.2x plan rather than in actual Cure. None of this will be done quickly. All of it will be done, if the last four releases are any indication, by ripping out the almost-works version and replacing it with one that does.&lt;/p&gt;&lt;p&gt;Clone it, build it, break it:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;git clone https://github.com/am-kantox/cure-lang.git
cd cure
mix deps.get &amp;amp;&amp;amp; mix test
mix escript.build
./cure version
./cure run examples/destructuring.cure
./cure run examples/derived_show.cure
./cure run examples/lazy_iter.cure&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;The repository is at &lt;a href=&quot;https://github.com/am-kantox/cure-lang&quot;&gt;github.com/am-kantox/cure-lang&lt;/a&gt;, the site is at &lt;a href=&quot;https://cure-lang.org&quot;&gt;cure-lang.org&lt;/a&gt;, and the furniture, as of v0.19.1, is indoors.&lt;/p&gt;</content:encoded>
</item>
<item>
<title>SAFE: Bringing Real Static Analysis to the BEAM - Erlang Solutions</title>
<link>https://www.erlang-solutions.com/blog/safe-bringing-real-static-analysis-to-the-beam/</link>
<enclosure type="image/jpeg" length="0" url="https://www.erlang-solutions.com/wp-content/uploads/2026/04/SAFE-Bringing-Real-Static-Analysis-to-the-BEAM.png"></enclosure>
<guid isPermaLink="false">vwQg0k1lGSwbkxsGBf4aoBpY58iucyofhyWdsQ==</guid>
<pubDate>Sat, 18 Apr 2026 03:56:15 +0000</pubDate>
<description>Explore SAFE, Erlang Solutions’ BEAM analysis tool, and how data-flow analysis improves security detection with fewer false positives.</description>
<content:encoded>&lt;p&gt;In recent years, software security has become a hot topic due to regulatory pressure (NIS2, EU Cyber Resilience Act, etc.). Beyond regulations, software communities and open source maintainers have also put security into focus, because open source libraries are often part of commercial software supply chains. This has reached the Erlang/Elixir ecosystem as well, and that is a good thing.&lt;/p&gt;&lt;p&gt;In this post, the SAFE team takes us through SAFE, Erlang Solutions’ security analysis tool for the BEAM, covering what it does, how it works, and what makes it effective in practice.&lt;/p&gt;&lt;h1&gt;&lt;strong&gt;The BEAM is secure by default (Up to a point)&lt;/strong&gt;&lt;/h1&gt;&lt;p&gt;The BEAM’s architecture eliminates entire classes of bugs for free. Isolated process memory means processes can’t manipulate each other’s state, they only communicate through messages. Immutable data structures rule out a whole category of aliasing bugs. These are real wins, and they come without any effort from the developer.&lt;/p&gt;&lt;p&gt;But “secure by default” only goes so far. Application-level vulnerabilities, e.g., XSS, SQL injection, CSRF, unsafe deserialization, atom exhaustion, are just as possible in Erlang and Elixir as anywhere else. That’s the gap SAFE exists to cover.&lt;/p&gt;&lt;h2&gt;&lt;strong&gt;What is SAFE?&lt;/strong&gt;&lt;/h2&gt;&lt;p&gt;&lt;a href=&quot;https://www.erlang-solutions.com/services/security-audit-for-erlang-and-elixir/&quot;&gt;SAFE&lt;/a&gt; (Security Analysis for Erlang/Elixir) is Erlang Solutions’ static analysis tool for the BEAM. It analyses compiled BEAM files rather than source code, so it works consistently across Erlang, Elixir, and Phoenix, including mixed-language codebases. SAFE is free for open source projects (subject to approval) and commercially available for other use cases.&lt;/p&gt;&lt;p&gt;It is developed in collaboration with academic research on static analysis from &lt;a href=&quot;https://www.elte.hu/en&quot;&gt;Eötvös Loránd University&lt;/a&gt; , and are aligned with the security recommendations of the &lt;a href=&quot;https://security.erlef.org/&quot;&gt;Erlang Ecosystem Foundation&lt;/a&gt; (EEF).&lt;/p&gt;&lt;p&gt;SAFE detects a broad range of vulnerabilities, including:&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;Cross-Site Scripting (XSS)&lt;/li&gt;



&lt;li&gt;SQL injection&lt;/li&gt;



&lt;li&gt;Command injection&lt;/li&gt;



&lt;li&gt;Remote Code Execution&lt;/li&gt;



&lt;li&gt;Denial of Service (e.g. atom exhaustion)&lt;/li&gt;



&lt;li&gt;Unsafe serialisation&lt;/li&gt;



&lt;li&gt;Cross-Site Request Forgery (CSRF)&lt;/li&gt;



&lt;li&gt;Session hijacking, fixation, and information leakage&lt;/li&gt;



&lt;li&gt;Content Security Policy (CSP) misconfigurations&lt;/li&gt;
&lt;/ul&gt;&lt;h2&gt;&lt;strong&gt;Data-flow analysis: the core of what makes SAFE different&lt;/strong&gt;&lt;/h2&gt;&lt;p&gt;The central feature that sets SAFE apart is &lt;strong&gt;data-flow analysis&lt;/strong&gt;. Most static analysis tools work by pattern matching, they look for known dangerous function calls and flag them. The problem is that not every call to a dangerous function is actually dangerous. Without understanding the possible values flowing through the code, a tool has no way to tell the difference, and the result is a high rate of false positives.&lt;/p&gt;&lt;p&gt;SAFE takes a different approach. Data-flow analysis tracks what values variables can hold at each point in the program. This information is then used to filter the initial list of vulnerability candidates, eliminating findings where the data can be proven safe, and surfacing only the ones that represent real risk.&lt;/p&gt;&lt;p&gt;The practical impact of this is significant. In our tests across 7 popular open source BEAM projects (~70,000 lines of code), SAFE produced a false positive rate of &lt;strong&gt;7.78%&lt;/strong&gt; and that number continues to improve as we refine our analysis. We also manually review findings during development to further sharpen the filtering.&lt;/p&gt;&lt;h3&gt;&lt;strong&gt;How data-flow analysis eliminates false positives&lt;/strong&gt;&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Example 1 — guarded atom creation&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;A common pattern in Elixir is to convert a binary to an atom only after validating it against an allowlist:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;     
    def safe_to_atom(binary, allowed) do    
      if Enum.member?(allowed, binary), do: String.to_atom(binary)    
    end    
&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A pattern-matching tool sees &lt;code&gt;String.to_atom/1&lt;/code&gt; and flags it. SAFE’s data-flow analysis traces the possible values of&lt;code&gt;binary&lt;/code&gt;at the point of the call and determines that it is always a member of a finite, controlled list so it eliminates the finding entirely.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Example 2 — finite compile-time atom generation&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Metaprogramming is common in Elixir. Consider this pattern where atoms are generated at compile time:&lt;/p&gt;&lt;div&gt;&lt;pre&gt; @variants [:case_a, :case_b, :case_c]    
    #    
    # ...    
    #    
    for var &amp;lt;- @variants do    
      defp unquote(var)() do    
        env = Application.get_env(:my_app, :environment)    
        if env == &amp;quot;test&amp;quot; do    
          unquote(Macro.escape(Module.get_attribute(__MODULE__, :&amp;quot;test_#{var}&amp;quot;)))    
        else    
          unquote(Macro.escape(Module.get_attribute(__MODULE__, var)))    
        end    
      end    
    end    
         
&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The number of atoms created here is strictly bounded by the length of &lt;code&gt;@variants&lt;/code&gt;, a compile-time constant. SAFE calculates this and correctly determines the atom count is finite hence no vulnerability. A tool without data-flow analysis cannot make this determination.&lt;/p&gt;&lt;h2&gt;&lt;strong&gt;What SAFE catches in practice&lt;/strong&gt;&lt;/h2&gt;&lt;h3&gt;&lt;strong&gt;Session management vulnerabilities&lt;/strong&gt;&lt;/h3&gt;&lt;p&gt;Session management vulnerabilities allow attackers to gain unauthorised access to user sessions, which can lead to data theft, unauthorised actions, and account takeover. SAFE detects session hijacking, session fixation, and session information leakage. &lt;strong&gt;Session hijacking&lt;/strong&gt; occurs when an attacker gains access to cookie contents. To prevent this, the&lt;code&gt;http_only&lt;/code&gt; and &lt;code&gt;secure&lt;/code&gt; attributes should both be set to &lt;code&gt;true&lt;/code&gt; when setting a cookie. Below is a vulnerable example:&lt;/p&gt;&lt;div&gt;&lt;pre&gt; @spec set_cookie(Plug.Conn.t()) :: Plug.Conn.t()    
    def set_cookie(conn) do    
      Plug.Conn.put_resp_cookie(conn, &amp;quot;my_cookie&amp;quot;, &amp;quot;true&amp;quot;,    
        http_only: false,    
        max_age: @max_age # an integer    
      )    
    end    
         
&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Session fixation&lt;/strong&gt; is an attack where a malicious user plants a session ID for a victim to use, then hijacks their account after login. The &lt;code&gt;Plug.Session&lt;/code&gt; API provides the &lt;code&gt;configure_session/2&lt;/code&gt; function for renewing the session ID. When this function is misconfigured by setting the &lt;code&gt;renew&lt;/code&gt; option to &lt;code&gt;false&lt;/code&gt;, session fixation can occur.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Session information leakage&lt;/strong&gt; can be prevented by encrypting cookie contents. Encryption can be enabled by setting the &lt;code&gt;encryption_salt&lt;/code&gt;in &lt;code&gt;Plug.Session&lt;/code&gt;. For non-session cookies, encryption can be enabled via the &lt;code&gt;encrypt&lt;/code&gt; option in &lt;code&gt;Plug.Conn.put_resp_cookie/4&lt;/code&gt;.&lt;/p&gt;&lt;h3&gt;&lt;strong&gt;Content Security Policy misconfigurations&lt;/strong&gt;&lt;/h3&gt;&lt;p&gt;When it comes to Content Security Policy (CSP), any policy is better than none. Using :&lt;code&gt;put_secure_browser_headers&lt;/code&gt; without a custom policy won’t be enough on its own:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;   plug :put_secure_browser_headers, %{    
      &amp;quot;content-security-policy&amp;quot;: &amp;quot;[Your Policy]&amp;quot;    
    }    
         
&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;There are also dedicated plugs for this, such as &lt;code&gt;PlugContentSecurityPolicy&lt;/code&gt;:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;   plug PlugContentSecurityPolicy,    
&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;SAFE inspects the policy content itself and will flag an overly permissive default. You should define your own policy, keeping it as restrictive as possible to avoid unintentionally whitelisting too much. Beyond that, SAFE checks that all pipelines accepting HTML have CSP protection at all, a gap that is easy to miss during development.&lt;/p&gt;&lt;h2&gt;&lt;strong&gt;Results from the field&lt;/strong&gt;&lt;/h2&gt;&lt;p&gt;The top three vulnerability types are XSS, DoS, and CSP. Notably, a large share of DoS findings trace back to unguarded &lt;code&gt;String.to_atom/1 calls&lt;/code&gt;, a known footgun that is consistently flagged in Erlang/Elixir documentation, yet still appears frequently in practice.&lt;/p&gt;&lt;p&gt;SAFE found a total of 90 vulnerabilities, of which 7 were false positives after manual investigation, a false positive ratio of 7.78%, and an area of active improvement.&lt;/p&gt;&lt;p&gt;The projects tested are anonymized to avoid identification, since the disclosed vulnerabilities may still be present in production systems. While this limits full reproducibility, responsible disclosure takes priority. Contact us at &lt;em&gt;safe@erlang-solutions.com &lt;/em&gt;to discuss the methodology in detail.&lt;/p&gt;&lt;h2&gt;&lt;strong&gt;Closing notes&lt;/strong&gt;&lt;/h2&gt;&lt;p&gt;Security analysis on the BEAM is a genuinely hard problem. The same flexibility that makes Erlang and Elixir so expressive also makes it easy to introduce subtle vulnerabilities without realising it. SAFE is built specifically for this environment, grounded in academic research, and designed to give you signals you can act on rather than noise you have to filter.&lt;/p&gt;&lt;p&gt;If you maintain an open-source project, SAFE is free: reach out to us at&lt;em&gt; safe@erlang-solutions.com &lt;/em&gt;and after a short approval process you’ll receive a licence at no cost. For commercial use or a third-party security review of your system, &lt;a href=&quot;https://www.erlang-solutions.com/contact/&quot;&gt;get in touch with the team.&lt;/a&gt;&lt;/p&gt;</content:encoded>
</item>
<item>
<title>The Long Road to Cure</title>
<link>https://rocket-science.ru/hacking/2026/04/16/long-way-to-cure</link>
<enclosure type="image/jpeg" length="0" url="https://rocket-science.ru/img/logo/logo-orig.png"></enclosure>
<guid isPermaLink="false">9Unqq7pTf_6Hvc4t1YKoeNTe4LperDBfq71MfQ==</guid>
<pubDate>Thu, 16 Apr 2026 09:19:20 +0000</pubDate>
<description>Just under a year ago I started building a BEAM language with dependent types and first-class FSMs—chose the wrong implementation language, wrote spaghetti, had an LLM tell me it was garbage, shipped something embarrassing, then spent months rethinking. Here is what I learned and where Cure stands today.</description>
<content:encoded>&lt;p&gt;Just under a year ago I set out to fulfil a long-standing ambition: building a language that compiles to BEAM and implements two capabilities essential to my daily work—dependent types resolved and verified at compile time, and verifiable finite state machines as a first-class language primitive.&lt;/p&gt;&lt;p&gt;I chose Erlang as the implementation language because it seemed to me that writing a compiler would be easier that way. I piled together a heap of attractive little baubles, many of which were added simply “because it is fun to have them.” Quite quickly I lost my way, and before long I lost control of the resulting spaghetti. I stopped understanding what was breaking and why whenever I added something that looked innocuous. I brought in an LLM, and the blasted thing informed me that the codebase had been authored by a drunk lumberjack with a primary-school education—someone who could not be trusted with anything more complex than bubble sort. Not in those exact words, but close enough.&lt;/p&gt;&lt;p&gt;That upset me. I lost my temper, demanded that the wretched language model knock together a presentation site in five minutes on the spot (without specifying any details whatsoever) and shipped it. The announcements, predictably, were received with polite indifference. People liked the ambition behind the idea (naturally!), but calling the implementation good was something not even my mother would have attempted.&lt;/p&gt;&lt;p&gt;I shelved the coding—and recoding—for several months and set about thinking about what I had done wrong.&lt;/p&gt;&lt;p&gt;First: over the last decade I had worked primarily with Elixir, and choosing Erlang had been a momentary, entirely unjustified whim. One can call &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:compile.forms/2&lt;/code&gt; perfectly well from Elixir. Moreover, the ecosystem allowed me to abandon &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;make&lt;/code&gt;-files and a rather crude build setup.&lt;/p&gt;&lt;p&gt;Second: on my first approach to the apparatus I was trying to hunt ducks, hares, and wild boar simultaneously. There was nothing systematic about that approach—I just kept adding and adding new things, propping up the collapsing frame with sticks as I went. Adding a new operator could break module parsing entirely. In the current version I followed the plan strictly: better slow and coherent than fast and obscure.&lt;/p&gt;&lt;p&gt;And most importantly: in the first version I was in such a hurry that I repeated the mistake of almost every existing language—the AST was present as an annexe, a shed around the back. The lexer and parser could hand control directly to the compiler. That turned out to be the critical error.&lt;/p&gt;&lt;p&gt;Having reconsidered all this, I decided to start the rebuilding from the foundations. That is how the &lt;a href=&quot;https://hexdocs.pm/metastatic&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;metastatic&lt;/code&gt;&lt;/a&gt; library came into being. Put another way: I not only placed the AST at the centre of things but made it tower above every other entity in the system.&lt;/p&gt;&lt;blockquote&gt;
  &lt;p&gt;Metastatic is a library that provides a unified MetaAST (Meta-level Abstract Syntax Tree) intermediate representation for parsing, transforming, and analyzing code across multiple programming languages using a three-layer meta-model architecture.
Build tools once, apply them everywhere. Create a universal meta-model for program syntax that enables cross-language code analysis, transformation, and tooling. Metastatic provides the foundation—the MetaAST meta-model and language adapters. Tools that leverage this foundation (mutation testing, purity analysis, complexity metrics) are built separately.&lt;/p&gt;
&lt;/blockquote&gt;&lt;p&gt;If you look closely, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MetaAST&lt;/code&gt; bears a strong resemblance to Elixir’s own AST, because there is simply no better way to represent a tree than &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{node, meta, children}&lt;/code&gt;. In any case, I invested considerable effort in creating and debugging this new AST. It became the foundation of my second attempt at a “dependently-typed programming language for the BEAM virtual machine with first-class finite state machines and SMT-backed verification,” as it reads on the landing page.&lt;/p&gt;&lt;p&gt;After that I opened the casket marked “things from the previous century,” pulled out an actual pen and a notebook, and wrote out all the features from the first version that I had managed to implement, however imperfectly. I grouped them by importance, utility, and complexity. At this first stage I decided to forgo support for true dependent types—&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fn len(l: Vector(_, n)) -&amp;gt; NonNegInt = n&lt;/code&gt;—but &lt;a href=&quot;https://cure-lang.org/type-system&quot;&gt;a great deal of interesting things survived&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;FSMs as first-class citizens are still in a fairly embryonic state, but I am in no hurry now, and I shall bring them to completion steadily and carefully.&lt;/p&gt;&lt;p&gt;Give &lt;a href=&quot;https://cure-lang.org&quot;&gt;https://cure-lang.org&lt;/a&gt; a try—perhaps you will like it.&lt;/p&gt;</content:encoded>
</item>
<item>
<title>Avoiding Platform Lock-In in Regulated Environments</title>
<link>https://www.erlang-solutions.com/blog/avoiding-platform-lock-in-in-regulated-environments/</link>
<guid isPermaLink="false">pu9AyKEbT4seK_YxzQZuyRmos0LRZzOzMyrwVw==</guid>
<pubDate>Mon, 13 Apr 2026 05:37:46 +0000</pubDate>
<description>Platform lock-in risks in regulated systems and how to avoid vendor dependency with scalable architecture. The post Avoiding Platform Lock-In in Regulated Environments appeared first on Erlang Solutions.</description>
<content:encoded>&lt;p&gt;&lt;strong&gt;Platform lock-in&lt;/strong&gt; is often discussed as a commercial issue. Organisations adopt infrastructure that works well initially and later realise that moving away from those services becomes expensive or operationally disruptive.&lt;/p&gt;&lt;p&gt;For platforms that run continuously under heavy demand, the consequences appear somewhere else first. They appear in architecture.&lt;/p&gt;&lt;p&gt;Infrastructure choices influence how systems scale, how faults are contained, and how easily the platform can evolve as requirements change. In regulated environments those decisions often remain in place for years, which means architectural flexibility matters as much as technical capability.&lt;/p&gt;&lt;p&gt;When infrastructure becomes tightly coupled to a particular provider, systems may still perform well day to day. The real impact usually surfaces later when workloads grow, regulations change, or operational expectations increase. At that point &lt;strong&gt;platform lock-in risks&lt;/strong&gt; begin to affect reliability as well as flexibility.&lt;/p&gt;&lt;h2&gt;&lt;strong&gt;Why Platform Lock-in Matters in Regulated Environments&lt;/strong&gt;&lt;/h2&gt;&lt;p&gt;These architectural constraints become particularly visible in regulated industries where infrastructure decisions cannot be changed casually.&lt;/p&gt;&lt;p&gt;Financial services platforms must maintain traceable transactions and strict audit trails. Betting platforms process large volumes of activity during live sporting events. Streaming platforms deliver real-time content to global audiences who expect uninterrupted interaction.&lt;/p&gt;&lt;p&gt;Systems supporting these environments often remain active for long periods, which means infrastructure decisions made early in the system’s lifecycle can shape how the platform changes years later.&lt;/p&gt;&lt;h3&gt;&lt;strong&gt;The Operational Impact of Vendor Lock-in&lt;/strong&gt;&lt;/h3&gt;&lt;p&gt;Many organisations already recognise the risks associated with vendor lock-in. The &lt;a href=&quot;https://www.flexera.com/blog/finops/cloud-computing-trends-flexera-2024-state-of-the-cloud-report/&quot;&gt;2024 Flexera State of the Cloud Report&lt;/a&gt; found that 89% of organisations now operate multi-cloud strategies, with reducing infrastructure dependency and avoiding vendor concentration cited as key motivations.&lt;/p&gt;&lt;p&gt;The concern goes beyond procurement strategy. When platforms rely heavily on provider-specific services for messaging, orchestration, or event processing, those dependencies begin shaping how the system behaves under load.&lt;/p&gt;&lt;p&gt;In regulated environments that dependency can become a reliability concern. Infrastructure decisions that once simplified development may later restrict how systems scale, evolve, or respond to operational change.&lt;/p&gt;&lt;h2&gt;&lt;strong&gt;Distributed Systems Architecture and Long-Running Platforms&lt;/strong&gt;&lt;/h2&gt;&lt;p&gt;The reason &lt;strong&gt;platform lock-in&lt;/strong&gt; becomes particularly serious in regulated environments is tied to how many of these platforms operate: as long-running distributed systems.&lt;/p&gt;&lt;p&gt;Large-scale entertainment services rarely behave like short-lived workloads that restart frequently. Messaging layers, real-time interaction systems, and event pipelines maintain persistent connections while processing continuous streams of activity.&lt;/p&gt;&lt;h3&gt;&lt;strong&gt;Why Long-Running Systems Behave Differently&lt;/strong&gt;&lt;/h3&gt;&lt;p&gt;Gaming platforms illustrate this clearly. Competitive environments host thousands of players interacting simultaneously, all of whom expect consistent state across the system. Betting platforms experience similar behaviour during major sporting events when users react instantly to changing odds. Streaming platforms see comparable spikes as audiences interact during live broadcasts.&lt;/p&gt;&lt;p&gt;These platforms rely on &lt;strong&gt;distributed systems architecture&lt;/strong&gt; that must coordinate large numbers of connections and events while remaining continuously available.&lt;/p&gt;&lt;p&gt;Research published by &lt;a href=&quot;https://queue.acm.org/detail.cfm?id=3595878&quot;&gt;&lt;strong&gt;ACM Queue&lt;/strong&gt;&lt;/a&gt; examining large-scale distributed systems highlights how persistent connections and real-time workloads increase coordination pressure across system components, particularly during sudden spikes in concurrency.&lt;/p&gt;&lt;p&gt;When coordination layers rely heavily on platform-specific services, architectural dependency gradually builds. Over time the system begins to inherit those infrastructure constraints.&lt;/p&gt;&lt;h3&gt;&lt;strong&gt;Reliability Requirements in High Reliability Systems&lt;/strong&gt;&lt;/h3&gt;&lt;p&gt;Systems operating under these conditions often prioritise stability over rapid iteration. Platforms designed as &lt;strong&gt;high reliability systems&lt;/strong&gt; must remain available while managing constant traffic, evolving workloads, and unpredictable user behaviour.&lt;/p&gt;&lt;p&gt;Infrastructure decisions therefore have long-term consequences. When coordination, messaging, or state management rely on proprietary platform services, architectural flexibility narrows over time.&lt;/p&gt;&lt;h2&gt;&lt;strong&gt;Why Gaming, Betting and Streaming Platforms Reveal Infrastructure Limits&lt;/strong&gt;&lt;/h2&gt;&lt;p&gt;Systems built as long-running distributed environments face their toughest tests during moments of concentrated demand. Entertainment platforms provide a clear example.&lt;/p&gt;&lt;p&gt;Large audiences often react simultaneously. A football match entering extra time can trigger thousands of betting transactions within seconds. A major esports tournament can bring large numbers of players online at once. Streaming platforms experience bursts of interaction as viewers respond together during live broadcasts.&lt;/p&gt;&lt;h3&gt;&lt;strong&gt;Traffic Spikes and Scalable Distributed Systems&lt;/strong&gt;&lt;/h3&gt;&lt;p&gt;Systems supporting these environments must function as &lt;strong&gt;scalable distributed systems&lt;/strong&gt; capable of handling sudden increases in activity without losing consistency or responsiveness.&lt;/p&gt;&lt;p&gt;Instead of steady growth, activity often arrives in waves. Large numbers of users connect, interact, and generate events within very short timeframes. The system must coordinate these interactions across multiple nodes while maintaining reliable communication between services.&lt;/p&gt;&lt;p&gt;Infrastructure that appears sufficient under normal conditions can struggle during these spikes if the surrounding architecture relies too heavily on provider-specific services.&lt;/p&gt;&lt;h3&gt;&lt;strong&gt;Real-World Example: BET Software&lt;/strong&gt;&lt;/h3&gt;&lt;p&gt;These architectural pressures are particularly visible in betting platforms where activity surges during live sporting events.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.erlang-solutions.com/case-studies/bet-software/&quot;&gt;BET Software&lt;/a&gt; operates large-scale betting technology platforms where thousands of users interact with markets simultaneously. During major sporting events systems must process rapid updates, recalculate market information, and distribute new data to users in real time.&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;img src=&quot;https://www.erlang-solutions.com/wp-content/uploads/2026/03/image-2.png&quot; alt=&quot;BET Software:  Avoiding platform lock-in in regulated environments

&quot; title=&quot;&quot;/&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;Their distributed systems illustrate how reliability and responsiveness become essential in environments where activity concentrates around shared moments.&lt;/p&gt;&lt;p&gt;Architectures designed with flexibility across infrastructure layers tend to scale and recover more predictably than those tightly coupled to provider-specific services.&lt;/p&gt;&lt;h2&gt;&lt;strong&gt;Architectural Patterns to Avoid Vendor Lock-in&lt;/strong&gt;&lt;/h2&gt;&lt;p&gt;Recognising the risks of &lt;strong&gt;vendor lock-in&lt;/strong&gt; is useful only if it leads to better architectural decisions. Systems that remain adaptable across infrastructure layers often share several structural characteristics.&lt;/p&gt;&lt;h3&gt;&lt;strong&gt;Decoupling Infrastructure Dependencies&lt;/strong&gt;&lt;/h3&gt;&lt;p&gt;Architectures designed to &lt;strong&gt;avoid vendor lock-in&lt;/strong&gt; typically separate application logic from infrastructure services wherever possible. This allows teams to evolve system components independently without redesigning the entire system,&lt;/p&gt;&lt;h3&gt;&lt;strong&gt;Designing Fault Tolerant Systems&lt;/strong&gt;&lt;/h3&gt;&lt;p&gt;Platforms that must operate continuously also benefit from architectures designed as &lt;strong&gt;fault tolerant systems&lt;/strong&gt;, where failures can be contained locally rather than cascading across the entire platform.&lt;/p&gt;&lt;p&gt;Common patterns include:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Decoupled services that scale independently&lt;/li&gt;&lt;li&gt;Communication through open protocols rather than proprietary messaging layers&lt;/li&gt;&lt;li&gt;Distributed state management instead of provider-specific coordination services&lt;/li&gt;&lt;li&gt;Horizontal scaling across nodes&lt;/li&gt;&lt;li&gt;Infrastructure abstraction layers separating application logic from provider-specific implementations&lt;/li&gt;&lt;li&gt;These approaches help ensure that infrastructure choices support the system rather than define its limitations.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;These patterns help ensure that infrastructure choices support the system rather than define its limitations.&lt;/p&gt;&lt;h2&gt;&lt;strong&gt;Where Elixir Supports High Reliability Systems&lt;/strong&gt;&lt;/h2&gt;&lt;p&gt;Technology choices also influence how easily distributed systems can maintain reliability while remaining adaptable.&lt;/p&gt;&lt;p&gt;Languages built on the Erlang virtual machine, including Elixir, were designed for environments where systems must remain available while handling large numbers of concurrent processes. The runtime emphasises process isolation and supervision structures that allow failures to be contained locally rather than cascading across the system.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Building Fault Tolerant Systems for Long-Running Platforms&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;These characteristics make the platform particularly well suited for &lt;strong&gt;high reliability systems&lt;/strong&gt; that must remain active while managing heavy concurrency.&lt;/p&gt;&lt;p&gt;The advantage lies in the runtime model rather than any single infrastructure provider. Systems built around resilient distributed behaviour are easier to evolve because they remain stable even as infrastructure decisions change around them.&lt;/p&gt;&lt;h2&gt;&lt;strong&gt;Designing Systems That Reduce Platform Lock-in&lt;/strong&gt;&lt;/h2&gt;&lt;p&gt;Looking across these examples reveals a consistent pattern.&lt;/p&gt;&lt;p&gt;Platform lock-in becomes most visible in systems that must operate continuously while adapting to changing demand. Regulated environments amplify the challenge because infrastructure decisions often remain in place for years while platforms continue to evolve.&lt;/p&gt;&lt;p&gt;Gaming, betting, and streaming services make these limits easier to see. Sudden spikes in activity quickly expose architectural weaknesses, and systems designed with flexible infrastructure tend to scale and recover more predictably.&lt;/p&gt;&lt;p&gt;If you are building platforms where reliability and long-running distributed workloads matter, it may be worth assessing how your architecture handles platform lock-in. To explore these challenges further,&lt;a href=&quot;https://www.erlang-solutions.com/contact/&quot;&gt; get in touch&lt;/a&gt; with the Erlang Solutions team.&lt;/p&gt;&lt;p&gt;The post &lt;a href=&quot;https://www.erlang-solutions.com/blog/avoiding-platform-lock-in-in-regulated-environments/&quot;&gt;Avoiding Platform Lock-In in Regulated Environments&lt;/a&gt; appeared first on &lt;a href=&quot;https://www.erlang-solutions.com&quot;&gt;Erlang Solutions&lt;/a&gt;.&lt;/p&gt;</content:encoded>
</item>
<item>
<title>Reliability is a Product Decision</title>
<link>https://www.erlang-solutions.com/blog/reliability-is-a-product-decision/</link>
<guid isPermaLink="false">u4Ho_YMqCrPC-AcCMiV_ZIiEkJUQsCi-th_kaA==</guid>
<pubDate>Mon, 13 Apr 2026 05:37:45 +0000</pubDate>
<description>Erik Schön explains why reliability in distributed systems is shaped by early architecture decisions, not just operational fixes. The post Reliability is a Product Decision appeared first on Erlang Solutions.</description>
<content:encoded>&lt;p&gt;Reliability is often treated as something that can be improved once a system is live. When things break, the focus shifts to monitoring, incident response, and recovery, with the belief that resilience can be strengthened over time as scale reveals weaknesses.&lt;/p&gt;&lt;p&gt;In reality, most of it is set much earlier.&lt;/p&gt;&lt;p&gt;Long before a system faces sustained demand, its underlying design has already shaped how it will respond under pressure. Choices about service boundaries, data handling, deployment models, and fault management influence whether a problem stays contained or spreads.&lt;/p&gt;&lt;p&gt;The conversation is gradually moving from reliability to resilience because distributed systems rarely operate without failure. The more useful question is how a platform continues running when parts of it inevitably fail. The sections that follow explore how early architectural decisions shape that outcome, why their impact becomes more visible at scale, and what it means to build resilience from the beginning rather than react to it later.&lt;/p&gt;&lt;h1&gt;&lt;strong&gt;Early Decisions Create Long-Term Behaviour&lt;/strong&gt;&lt;/h1&gt;&lt;p&gt;Large-scale failures rarely emerge without warning. What appears sudden at scale is often the predictable outcome of structural decisions made earlier, when different commercial pressures shaped priorities. &lt;/p&gt;&lt;p&gt;In the early stages of a product, the focus is understandably on delivering value quickly, reducing development friction, and validating the market. These are rational business decisions. However, architecture chosen primarily for speed can quietly define the operational ceiling of the system, setting limits that only become visible once demand increases.&lt;/p&gt;&lt;h2&gt;&lt;strong&gt;Systems Behave as They Were Built to Behave&lt;/strong&gt;&lt;/h2&gt;&lt;p&gt;Outages are often described as “unexpected events,” but distributed systems typically respond to pressure in ways that reflect their design. How services communicate, how state is shared, where dependencies sit, and how failure is managed all influence whether disruption remains contained within a single component or spreads across the wider platform.&lt;/p&gt;&lt;p&gt;Research from &lt;a href=&quot;https://sre.google/sre-book/introduction/&quot;&gt;Google’s Site Reliability Engineering&lt;/a&gt; work shows that around 70% of outages are caused by changes to a live system, such as configuration updates, deployments, or operational changes, rather than by hardware failures. Similarly, the &lt;a href=&quot;https://uptimeinstitute.com/resources/research-and-reports/annual-outage-analysis-2025&quot;&gt;Uptime Institute’s Annual Outage Analysis&lt;/a&gt; identifies configuration errors and dependency failures as leading causes of major disruption.&lt;/p&gt;&lt;p&gt;These findings are unsurprising. In distributed environments, dependencies increase and recovery paths become harder to trace, which means that architectural shortcuts that once seemed minor can have disproportionate impact under sustained load. Systems tend to fail along the structural lines already drawn into them, and those lines are shaped by early design decisions, even when those decisions were commercially sensible at the time.&lt;/p&gt;&lt;h2&gt;&lt;strong&gt;Trade-offs That Compound Over Time&lt;/strong&gt;&lt;br/&gt;&lt;/h2&gt;&lt;p&gt;Architectural decisions are rarely made under ideal conditions. Early on, speed to market matters, simplicity reduces friction, and shipping is the priority. A tightly coupled service can help teams move faster, a single-region deployment keeps things straightforward, and limited observability may feel acceptable when traffic is still modest.&lt;/p&gt;&lt;p&gt;But overtime, these trade-offs compound.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Limited isolation between services makes it easier for problems in one area to affect others.&lt;/li&gt;&lt;li&gt;Shared infrastructure can create hidden dependencies that only become visible under heavy demand.&lt;br/&gt;Concentrated regional deployments increase the impact of a local outage or cloud disruption.&lt;/li&gt;&lt;li&gt;Observability that felt sufficient at launch can fall short when trying to understand complex behaviour at scale.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;At a smaller scale, these constraints can go largely unnoticed. As usage increases and demand becomes less predictable, they start to shape how the system responds under pressure. What once felt manageable begins to show its limits.&lt;/p&gt;&lt;p&gt;This is rarely about a lack of technical ability. It is simply what happens as complexity builds over time. Every system reflects the trade-offs made in its early stages, whether those choices were deliberate or just practical at the time.&lt;/p&gt;&lt;h2&gt;&lt;strong&gt;When Architecture Becomes Business Exposure&lt;/strong&gt;&lt;/h2&gt;&lt;p&gt;As systems grow in scale and complexity, the way they are built starts to show up in practical ways. When services are tightly connected, recovery takes longer. When failures are not well contained, a problem in one area can disrupt others. Incidents become harder to resolve and more expensive to manage.&lt;/p&gt;&lt;p&gt;The cost of disruption is not abstract.&lt;a href=&quot;https://astecno.com.br/wp-content/uploads/2023/09/ITIC-2023-Global-Server-Hardware-Server-OS-Reliability-Report.pdf&quot;&gt; ITIC’s 2023 Hourly Cost of Downtime Survey &lt;/a&gt;reports that more than 90% of mid-size and large enterprises estimate a single hour of downtime costs over $300,000, and roughly 41% place that figure between $1 million and $5 million per hour. At that level, even short-lived incidents carry material financial impact.&lt;/p&gt;&lt;p&gt;For organisations that rely on digital platforms to generate revenue, those numbers represent missed transactions, operational strain, and damage to customer trust. At that point, system design is no longer just an engineering decision. It becomes a business decision with measurable financial consequences.&lt;/p&gt;&lt;h2&gt;&lt;strong&gt;When Failure Is Public&lt;/strong&gt;&lt;/h2&gt;&lt;p&gt;Some systems fail quietly, disrupting internal workflows or back-office processes with limited external visibility. Others operate in real time, where performance issues are experienced directly by customers, investors, and partners.&lt;/p&gt;&lt;p&gt;In sectors such as entertainment, demand is often synchronised and predictable. Premieres, sporting events, ticket releases, and major launches concentrate traffic into specific windows, placing simultaneous pressure on application layers, databases, and third-party services. These moments are not unusual spikes; they are built into the operating model. Platforms designed for large-scale engagement are expected to handle peak demand as part of normal business activity.&lt;/p&gt;&lt;p&gt;That expectation changes the stakes. When performance degrades in these environments, it is noticed immediately and often publicly. Frustration spreads quickly, confidence can shift in hours, and what might have been an operational issue becomes a visible business problem.&lt;/p&gt;&lt;p&gt;In this context, resilience shapes whether a high-demand event reinforces confidence in the platform or exposes its limits. When failure is experienced directly by users, it moves beyond internal metrics and becomes part of the customer experience itself.&lt;/p&gt;&lt;h2&gt;&lt;strong&gt;Designing for Resilience&lt;/strong&gt;&lt;/h2&gt;&lt;p&gt;If failure is inevitable in distributed systems, then resilience has to be built in from the start. It cannot be something added later when the first serious incident forces the issue.&lt;/p&gt;&lt;p&gt;Resilient systems are structured so that problems stay contained. A fault in one component should not automatically take others down with it, and services should be able to keep operating even when parts of the system are degraded. External dependencies will fail. Traffic will spike. The design needs to account for that reality.&lt;/p&gt;&lt;p&gt;This way of thinking shifts the focus. Instead of trying to prevent every possible issue, teams concentrate on limiting the impact when something goes wrong. Speed still matters, but so does the ability to grow without introducing instability.&lt;/p&gt;&lt;p&gt;Technology choices can support that approach.&lt;a href=&quot;https://www.erlang-solutions.com/technologies/elixir/&quot;&gt; Elixir programming language&lt;/a&gt;, running on the BEAM, was designed for environments where downtime had real consequences. Its structure reflects that:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Applications are made up of many small, independent processes rather than large, tightly connected components.&lt;/li&gt;&lt;li&gt;Failures are expected and handled locally.&lt;/li&gt;&lt;li&gt;Supervision and recovery are built into the runtime so the wider system keeps running.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;No language guarantees reliability, but tools built around fault tolerance make it easier to create systems that continue operating under pressure.&lt;/p&gt;&lt;h2&gt;&lt;strong&gt;To conclude&lt;/strong&gt;&lt;/h2&gt;&lt;p&gt;By the time serious issues appear at scale, most of the important decisions have already been made.&lt;/p&gt;&lt;p&gt;Failure is part of running distributed systems. What matters is whether problems stay contained and whether the platform keeps operating when something goes wrong.&lt;/p&gt;&lt;p&gt;Thinking about resilience early makes growth easier later. It helps protect revenue, maintain trust, and avoid the instability that forces costly redesigns.If you are building distributed platforms where reliability directly affects performance and reputation, now is the time to treat resilience as a core design decision. &lt;a href=&quot;https://www.erlang-solutions.com/contact/&quot;&gt;Get in touch&lt;/a&gt; to discuss how to build it into your architecture from the start.&lt;/p&gt;&lt;p&gt;The post &lt;a href=&quot;https://www.erlang-solutions.com/blog/reliability-is-a-product-decision/&quot;&gt;Reliability is a Product Decision&lt;/a&gt; appeared first on &lt;a href=&quot;https://www.erlang-solutions.com&quot;&gt;Erlang Solutions&lt;/a&gt;.&lt;/p&gt;</content:encoded>
</item>
<item>
<title>The Always-On Economy: Fintech as Critical Infrastructure</title>
<link>https://www.erlang-solutions.com/blog/the-always-on-economy-fintech-as-critical-infrastructure/</link>
<guid isPermaLink="false">FfEF5xaKSjU2qDYk3ZhVmQxm2-H22qf4IpQ4zg==</guid>
<pubDate>Mon, 13 Apr 2026 05:37:45 +0000</pubDate>
<description>Erik Schön explains how the always-on economy is raising the bar for fintech resilience and scalability. The post The Always-On Economy: Fintech as Critical Infrastructure appeared first on Erlang Solutions.</description>
<content:encoded>&lt;p&gt;​​We are living in an economy that rarely sleeps. Payments clear late at night. Payroll runs in the background. Businesses expect every digital touchpoint to work when they need it.&lt;/p&gt;&lt;p&gt;Most people do not think about this shift. They assume the systems behind it will hold.&lt;/p&gt;&lt;p&gt;That assumption carries weight.&lt;/p&gt;&lt;p&gt;Fintechs, including small and mid-sized ones, now sit inside the basic infrastructure of how money moves. Their uptime affects cash flow. Their stability affects trust. When something breaks, real businesses feel it immediately.&lt;/p&gt;&lt;p&gt;Expectations changed faster than most systems did.This follows on from our earlier piece, &lt;a href=&quot;https://www.erlang-solutions.com/blog/from-prototype-to-production-scaling-fintech-for-smes/&quot;&gt;&lt;em&gt;From Prototype to Production&lt;/em&gt;&lt;/a&gt;, which explored how early technical shortcuts surface as systems scale. Here, we look at what happens next, when those systems become part of the infrastructure businesses rely on every day.&lt;/p&gt;&lt;h1&gt;&lt;strong&gt;When “tech downtime” becomes infrastructure failure&lt;/strong&gt;&lt;/h1&gt;&lt;p&gt;Outages no longer feel contained. A single failure can affect services that millions of people and businesses depend on.&lt;/p&gt;&lt;p&gt;Large providers experience this as much as small ones. When a shared platform falters, the impact spreads quickly. More than &lt;a href=&quot;https://www.networkworld.com/article/972102/10-things-to-know-about-data-center-outages.html#:~:text=Third%2Dparty%20providers%20are%20behind,is%20struck%2C%E2%80%9D%20Brown%20said.&quot;&gt;60 percent of outages&lt;/a&gt; now come from third-party providers rather than internal systems, highlighting how tightly connected the ecosystem has become.&lt;/p&gt;&lt;h3&gt;&lt;strong&gt;The true cost is trust&lt;/strong&gt;&lt;/h3&gt;&lt;p&gt;For fintechs, the impact is immediate because money is involved.&lt;/p&gt;&lt;p&gt;A payment delay blocks cash flow. A failed identity check stops onboarding. A stalled platform damages credibility.&lt;/p&gt;&lt;p&gt;For an SME, this can play out over the course of a single day. Payroll does not process in the morning. Supplier payments stall in the afternoon. Customer support queues fill up while teams wait for systems to recover. Even short interruptions create knock-on effects that last far longer than the outage itself.&lt;/p&gt;&lt;p&gt;And the numbers reflect that risk:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://itic-corp.com/itic-2024-hourly-cost-of-downtime-part-2/&quot;&gt;£25,000&lt;/a&gt; is the average cost of downtime for SMEs.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.cloudgateway.co.uk/knowledge-centre/articles/fintech-hidden-infrastructure-pain-points-that-drain-capital/&quot;&gt;40% &lt;/a&gt;of customers consider switching providers after a single outage.&lt;/li&gt;&lt;li&gt;Fintech revenue losses account for approximately &lt;a href=&quot;https://resolvepay.com/blog/statistics-indicating-api-downtimes-cost-to-finance-operations&quot;&gt;US$37 million&lt;/a&gt; of downtime-related costs each year.&lt;br/&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;At this level of dependency, downtime stops being a technical issue. It becomes an infrastructure failure.&lt;/p&gt;&lt;h2&gt;&lt;strong&gt;Fintechs have become infrastructure whether they intended to or not&lt;/strong&gt;&lt;/h2&gt;&lt;p&gt;Fintech services have moved beyond convenience. They now underpin everyday economic activity for businesses that depend on constant access to money, credit, and financial data.&lt;/p&gt;&lt;p&gt;This shift shows up in uptime expectations. Platforms that handle financial activity are measured against standards once reserved for mission-critical systems. Even brief disruption can have outsized consequences when services are expected to remain available throughout the day..&lt;/p&gt;&lt;p&gt;Customers do not adjust expectations based on company size or stage. If money flows through a service, users expect it to be available around the clock. When it is not, the failure feels systemic rather than technical. What breaks is not just functionality, but confidence.&lt;/p&gt;&lt;p&gt;That is the environment fintech leaders are operating in now.&lt;/p&gt;&lt;h2&gt;&lt;strong&gt;Where resilience typically breaks down&lt;/strong&gt;&lt;/h2&gt;&lt;p&gt;Most fintech systems do not fail because the idea was weak. They fail because early decisions prioritised speed over durability.&lt;/p&gt;&lt;p&gt;Teams optimise for launch. They prove demand. They ship quickly. That works early on, but systems designed for experimentation often struggle once demand becomes constant rather than occasional.&lt;/p&gt;&lt;p&gt;Shortcuts that felt harmless early on start to surface under pressure. Shared components become bottlenecks. Manual processes turn into operational risk. Integrations that worked at low volume become fragile at scale. Recent analysis shows average API uptime has fallen year over year, &lt;a href=&quot;https://www.uptrends.com/state-of-api-reliability-2025&quot;&gt;adding more than 18 hours of downtime &lt;/a&gt;annually for systems dependent on third-party APIs.&lt;br/&gt;&lt;a href=&quot;https://www.uptrends.com/state-of-api-reliability-2025?utm_source=chatgpt.com&quot;&gt; &lt;/a&gt;&lt;/p&gt;&lt;p&gt;Common pressure points include:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Shared components that act as single points of failure&lt;br/&gt;&lt;/li&gt;&lt;li&gt;Manual operational work that cannot keep up with growth&lt;br/&gt;&lt;/li&gt;&lt;li&gt;Third-party dependencies with limited visibility or control&lt;br/&gt;&lt;/li&gt;&lt;li&gt;Architecture built for bursts of usage instead of continuous demand&lt;br/&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;These are not accidental outcomes. They are the result of trade-offs made under pressure. Funding milestones, launch timelines, and growth targets shape architecture as much as technical skill does.&lt;/p&gt;&lt;p&gt;When outages happen, they rarely trace back to a single bug. They trace back to earlier choices about what mattered and what could wait.&lt;/p&gt;&lt;h2&gt;&lt;strong&gt;From product thinking to infrastructure grade systems&lt;/strong&gt;&lt;/h2&gt;&lt;p&gt;Product thinking is about features and speed. Infrastructure thinking is about continuity.&lt;/p&gt;&lt;p&gt;Infrastructure-grade systems assume failure will happen. They are built to contain it, recover quickly, and keep the wider platform running. The goal is not perfection. The goal is staying available.&lt;/p&gt;&lt;p&gt;The goal is not perfection. The goal is staying available.&lt;/p&gt;&lt;p&gt;Continuous availability is now expected in financial services. Systems are updated and maintained without noticeable downtime because users do not tolerate interruptions when money is involved.&lt;/p&gt;&lt;p&gt;This approach does not slow teams down. It reduces risk. Deployments feel routine instead of stressful. Engineering effort shifts away from incident response and toward steady improvement.&lt;/p&gt;&lt;p&gt;Over time, this changes how organisations operate. Teams plan differently. Roadmaps become more realistic. Reliability becomes part of delivery rather than a separate concern.&lt;/p&gt;&lt;h2&gt;&lt;strong&gt;Elixir and the always-on economy&lt;/strong&gt;&lt;/h2&gt;&lt;p&gt;&lt;a href=&quot;https://www.erlang-solutions.com/technologies/elixir/&quot;&gt;Elixir programming language&lt;/a&gt; is designed for systems that are expected to stay available. It runs on the BEAM virtual machine, which was built in environments where downtime carried real consequences.&lt;/p&gt;&lt;p&gt;That background shows up in how Elixir applications are structured. Systems are composed of many small, isolated processes rather than large, tightly coupled components. When something fails, it fails locally. Recovery is expected. The wider system continues to operate.&lt;/p&gt;&lt;h3&gt;&lt;strong&gt;Elixir in Fintech&lt;/strong&gt;&lt;/h3&gt;&lt;p&gt;This matters in fintech, where failure is inevitable and interruptions are costly. External services misbehave. Load changes without warning. Elixir applications are built to absorb those conditions and recover quickly without cascading outages.&lt;/p&gt;&lt;p&gt;Teams working in Elixir tend to spend less time managing fragile behaviour and more time improving core functionality. Systems evolve instead of being replaced. Reliability becomes part of the foundation rather than a promise teams struggle to maintain.&lt;/p&gt;&lt;p&gt;For fintechs operating in an always-on economy, that approach aligns with the expectations already placed on them.&lt;/p&gt;&lt;h3&gt;&lt;strong&gt;Reliability as a competitive advantage&lt;/strong&gt;&lt;/h3&gt;&lt;p&gt;Reliable systems can completely change how a fintech operates day to day.&lt;/p&gt;&lt;p&gt;For growing fintechs, uptime supports trust and regulatory confidence. Customers stay because the platform behaves predictably. Growth becomes steadier because teams are not constantly reacting to incidents. Downtime costs make this real, especially for small businesses that lose revenue when systems are unavailable.&lt;/p&gt;&lt;p&gt;For larger providers, reliability reduces operational strain. Fewer incidents mean fewer emergency fixes and fewer difficult conversations with partners and regulators. Teams spend more time improving core services and less time managing fallout.&lt;/p&gt;&lt;p&gt;Reliability also shapes perception. Platforms that stay up become easier to trust with deeper integrations and higher volumes. Over time, that trust compounds and turns stability into a real advantage, even if it is rarely visible from the outside.&lt;/p&gt;&lt;h2&gt;&lt;strong&gt;To conclude&lt;/strong&gt;&lt;/h2&gt;&lt;p&gt;The always-on economy creates real opportunity for fintechs, but it also raises expectations that many platforms were not originally built to meet.&lt;/p&gt;&lt;p&gt;The question is whether your system has the resilience to operate as infrastructure day after day. If you are a fintech and want to build with reliability in mind, &lt;a href=&quot;https://www.erlang-solutions.com/contact/&quot;&gt;get in touch&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;Designing for resilience early makes it far easier to scale without introducing fragility later on.&lt;br/&gt;&lt;/p&gt;&lt;p&gt;&lt;br/&gt;&lt;/p&gt;&lt;p&gt;The post &lt;a href=&quot;https://www.erlang-solutions.com/blog/the-always-on-economy-fintech-as-critical-infrastructure/&quot;&gt;The Always-On Economy: Fintech as Critical Infrastructure&lt;/a&gt; appeared first on &lt;a href=&quot;https://www.erlang-solutions.com&quot;&gt;Erlang Solutions&lt;/a&gt;.&lt;/p&gt;</content:encoded>
</item>
<item>
<title>Building a performance evaluation toolkit and a dataplane PoC for atproto</title>
<link>https://bitcrowd.dev/2026/03/30/building-a-performance-evaluation-toolkit-and-a-dataplane-poc-for-atproto</link>
<guid isPermaLink="false">-SKqIXPjXO-YHUAmfTvjtQPzL0jq7ouh15Xymw==</guid>
<pubDate>Mon, 13 Apr 2026 04:05:45 +0000</pubDate>
<description>There is a new open social ecosystem emerging around atproto.</description>
<content:encoded>&lt;p&gt;There is a new open social ecosystem emerging around atproto.
Never heard of it?
You should check it out, it&amp;#39;s &lt;a href=&quot;https://overreacted.io/open-social/&quot;&gt;cool tech&lt;/a&gt; with an &lt;a href=&quot;https://www.youtube.com/watch?v=1A-0k58TfPo&quot;&gt;ethos&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;The open system is a dream for builders: from day one you can tap into the social graph of more than 40 million Bluesky users.
Building on this userbase, you can create everyones new favorite social network on atproto.
It&amp;#39;s no surprise that there is already a number of alternative communities emerging: Northsky, Eurosky, Blacksky, to name a few.&lt;/p&gt;&lt;p&gt;This is a blog post though, so you already know there is a problem.&lt;/p&gt;&lt;h2&gt;The Bluesky dataplane&lt;a href=&quot;https://bitcrowd.dev/2026/03/30/building-a-performance-evaluation-toolkit-and-a-dataplane-poc-for-atproto#the-bluesky-dataplane&quot;&gt;​&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Bluesky runs almost entirely on open sourced components which enables alternatives to get started quickly.&lt;/p&gt;&lt;p&gt;There is one notable exception: the part of the stack that processes the event stream of the atproto firehose, the AppView, needs one component to digest and store data.
This part is called dataplane, and it does the most heavy lifting in processing the event stream.
However, the open source dataplane implementation is not very performant.
Bluesky uses a closed source dataplane implementation in production for this reason.&lt;/p&gt;&lt;p&gt;The open source dataplane functions on fan-in principle:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;events are streamed into the system (e.g. &amp;quot;new post&amp;quot;)&lt;/li&gt;&lt;li&gt;the dataplane stores and indexes the events in ordinary Postgres tables&lt;/li&gt;&lt;li&gt;on user request, data is queried and presented to the user (fan-in)&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;This approach leads to the classic Twitter timeline problem, and in fact limits the user numbers that can safely be served to 100k users, often less.&lt;/p&gt;&lt;h2&gt;Fan-out to ETS dataplane&lt;a href=&quot;https://bitcrowd.dev/2026/03/30/building-a-performance-evaluation-toolkit-and-a-dataplane-poc-for-atproto#fan-out-to-ets-dataplane&quot;&gt;​&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;The trick to solve this problem is to change the approach.
Instead of querying the data on user request, we prepare data in a dedicated place for each user on write (fan-out).
When the user makes a request, the data is already sitting there, just waiting to be served to the user.&lt;/p&gt;&lt;p&gt;This is the also the approach followed by the Bluesky closed source dataplane which builds on ScyllaDB.
From all publicly available information, it seems to be pretty optimized to their hardware, and pretty expensive to run.&lt;/p&gt;&lt;p&gt;As you know, we are big fans of Elixir at bitcrowd, so we&amp;#39;ve built a Proof of Concept of a dataplane with &lt;a href=&quot;https://www.erlang.org/doc/apps/stdlib/ets.html&quot;&gt;ETS&lt;/a&gt; taking on the role of ScyllaDB.
The idea here is that we can build something that runs on commodity hardware but scales way better than Bluesky&amp;#39;s open source dataplane implementation.&lt;/p&gt;&lt;h2&gt;A peek into performance&lt;a href=&quot;https://bitcrowd.dev/2026/03/30/building-a-performance-evaluation-toolkit-and-a-dataplane-poc-for-atproto#a-peek-into-performance&quot;&gt;​&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;To evaluate the PoC, we measured reading and writing directly to the dataplane.
We compared a Postgres backed version (representing Bluesky&amp;#39;s open source implementation) with an ETS backed version.&lt;/p&gt;&lt;p&gt;We simulated write traffic and let a number of users make requests to load their timeline (&lt;code&gt;get_timeline&lt;/code&gt;).
In the dashboards below, you can see the number of simultaneously active users (&amp;quot;Active Sessions&amp;quot;), the write traffic (&amp;quot;Posts created&amp;quot;), the latencies of &lt;code&gt;get_timeline&lt;/code&gt; requests, and the throughput of &lt;code&gt;get_timeline&lt;/code&gt; requests.&lt;/p&gt;&lt;p&gt;Under heavy load, the Postgres backed fan-in implementation showed significantly larger latencies and lower throughput in comparison to the ETS backed fan-out implementation.&lt;/p&gt;&lt;figure&gt;&lt;img src=&quot;https://bitcrowd.dev/assets/images/Postgres-timeline-55b5a181aaa3c6f81e5d8eaf4cd205c9.png&quot; alt=&quot;Dashboard for Postgres backed fan-in implementation shows p95 latency at almost 1s at 105 get_timeline requests per second&quot; title=&quot;&quot;/&gt;&lt;figcaption&gt;Dashboard showing data for Postgres backed fan-in implementation&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure&gt;&lt;img src=&quot;https://bitcrowd.dev/assets/images/ETS-timeline-3679509482db499737014f46a5b96e09.png&quot; alt=&quot;Dashboard for ETS backed fan-out implementation shows p95 latency of less than 20ms at more than 200 get_timeline requests per second&quot; title=&quot;&quot;/&gt;&lt;figcaption&gt;Dashboard showing data for ETS backed fan-in implementation&lt;/figcaption&gt;&lt;/figure&gt;&lt;h2&gt;Advanced simulations&lt;a href=&quot;https://bitcrowd.dev/2026/03/30/building-a-performance-evaluation-toolkit-and-a-dataplane-poc-for-atproto#advanced-simulations&quot;&gt;​&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;We extended this PoC into a performance toolkit you can use to simulate traffic via atproto events and user requests.&lt;/p&gt;&lt;p&gt;As a basic principle, we want to be able to repeatedly simulate scenarios.
Therefore, you provide the configuration for the scenario as file that can be repeatedly loaded into the system.&lt;/p&gt;&lt;p&gt;We provide three components:&lt;/p&gt;&lt;h3&gt;Base data&lt;a href=&quot;https://bitcrowd.dev/2026/03/30/building-a-performance-evaluation-toolkit-and-a-dataplane-poc-for-atproto#base-data&quot;&gt;​&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;You can quickly create base data, such as users, to prepare your application instance for the simulated scenario.
It makes a difference for your app&amp;#39;s performance whether you already have millions of users in your database or none.&lt;/p&gt;&lt;h3&gt;Traffic simulation&lt;a href=&quot;https://bitcrowd.dev/2026/03/30/building-a-performance-evaluation-toolkit-and-a-dataplane-poc-for-atproto#traffic-simulation&quot;&gt;​&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;Based on the simulation plan, we perform requests or emit events to create read and write traffic.&lt;/p&gt;&lt;h3&gt;Measuring performance&lt;a href=&quot;https://bitcrowd.dev/2026/03/30/building-a-performance-evaluation-toolkit-and-a-dataplane-poc-for-atproto#measuring-performance&quot;&gt;​&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;Based on the simulated traffic, we can measure the performance users would experience.
For instance, how long would it take for their timeline to load?&lt;/p&gt;</content:encoded>
</item>
<item>
<title>A Score for an Invisible Orchestra</title>
<link>https://rocket-science.ru/hacking/2026/04/12/metaast-for-the-rescue</link>
<enclosure type="image/jpeg" length="0" url="https://rocket-science.ru/img/logo/logo-orig.png"></enclosure>
<guid isPermaLink="false">oxxBbD8mt7Twn3t4UyDZx_LA3AO0KUpDHzjwPg==</guid>
<pubDate>Sun, 12 Apr 2026 18:34:09 +0000</pubDate>
<description>Every programming language stores its AST in its own idiosyncratic format. Writing a code-analysis tool means rewriting it from scratch for each new language. MetaAST—a meta-model sitting one level above any concrete AST—solves this by providing a single universal representation. One analyser, all languages, linear instead of quadratic growth. This is the score that every instrument in the orchestra can read.</description>
<content:encoded>&lt;p&gt;Imagine a five-storey building with no lift, erected in the late fifties somewhere on the outskirts of Avtozavodskaya—or better still, in Kupchino. Every floor speaks its own language. Not figuratively but in the most literal sense: the ground floor communicates in Cyrillic, the second in Latin script, the third in ideograms, the fourth in cuneiform, and the fifth, in the manner of Wittgenstein, maintains a principled silence on the grounds that whereof one cannot speak, thereof one must be silent. The postman, delivering the correspondence, is obliged to carry five copies of one and the same letter, translated into each of these tongues, and to knock on the door every time, hoping the addressee has not moved to another floor.&lt;/p&gt;

&lt;p&gt;That is precisely how the world of programming is arranged—if one looks at it from the wings rather than the stalls. Every language has its own internal representation of code. Python stores its AST the way a thrifty housewife stores dry goods: in tidy labelled containers—&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BinOp&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FunctionDef&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Name&lt;/code&gt;. Elixir, with characteristic self-assurance, uses triples &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{atom, metadata, children}&lt;/code&gt; and calls them &lt;em&gt;quoted expressions&lt;/em&gt;, as though what we have before us is not a syntax tree but a collection of quotations from William Blake. Ruby stockpiles its S-expressions the way an antiquarian bookseller stockpiles yellowing volumes, and Erlang, faithful to tradition, converses in tuples and atoms intelligible only to Ericsson engineers and, by a curious coincidence, doctoral students at a handful of Swedish universities.&lt;/p&gt;

&lt;p&gt;The problem is obvious to anyone who has ever attempted to build a code-analysis tool. Suppose you have written a superb cyclomatic-complexity analyser for Python. It is magnificent: it finds nested conditionals, counts branching points, draws control-flow graphs. Then a colleague comes along and asks, “Could you do one for Ruby?” And it transpires that all your work—all those tree walkers, all that pattern matching over Python’s AST—must be rewritten from scratch. From zero. For a different tree, with different nodes, different semantics, and different booby traps. And then a third colleague will turn up and request the same for Haskell.&lt;/p&gt;

&lt;p&gt;Imagine a conductor forced to relearn musical notation every time a new instrument joins the orchestra. Violin—one system of writing. Cello—another. Oboe—a third, with reversed polarity, no less. The trumpet flatly refuses to acknowledge the existence of the staff and insists on a tablature of its own invention. Absurd, of course. In the real world every instrument reads the same score. Notes, rhythm, dynamics are universal. Only the technique of execution differs.&lt;/p&gt;

&lt;p&gt;MetaAST is that score.&lt;/p&gt;

&lt;hr/&gt;

&lt;p&gt;Before we turn to the details (and they deserve attention in the way a well-constructed detective plot deserves it), permit me a brief digression into theory. Fear not: no formulae, only an analogy. Though one formula will appear after all—but it is so elegant that failing to cite it would be a crime against aesthetics.&lt;/p&gt;

&lt;p&gt;In the early two-thousands—when mobile telephones already existed but had not yet taken charge of our lives—the OMG consortium (Object Management Group, bearing no relation whatsoever to the divine or the exclamatory) released a standard called MOF: Meta-Object Facility. Its essence fits into four lines, yet it took the industry two decades to understand those four lines. MOF defines a four-level hierarchy of models:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;M⁰&lt;/strong&gt; is running code. There it goes, spitting out results, crashing with errors, consuming memory. This is reality.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;M¹&lt;/strong&gt; is a model of reality. For programs, it is the AST: the abstract syntax tree. Python’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BinOp(op=Add(), left=Name(&amp;#39;x&amp;#39;), right=Num(5))&lt;/code&gt; is M¹. Elixir’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{:+, [context: Elixir], [{:x, [], Elixir}, 5]}&lt;/code&gt; is also M¹. Every language describes its own code in its own M¹, the way every painter paints an apple in their own way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;M²&lt;/strong&gt; is the model of models. The meta-model. It defines what a node of &lt;em&gt;any&lt;/em&gt; AST &lt;em&gt;can be&lt;/em&gt;. Not a concrete node of a concrete language, but the &lt;em&gt;concept&lt;/em&gt; of a node. A binary operation is neither Python’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BinOp&lt;/code&gt; nor Elixir’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{:+, ...}&lt;/code&gt;. A binary operation is the &lt;em&gt;idea&lt;/em&gt; that two operands are connected by an operator. UML lives at the M² level. And so does MetaAST.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;M³&lt;/strong&gt; is the meta-meta-model. That which defines what meta-models themselves can be. The type system, the rules of composition. MOF lives here. In the context of MetaAST, this role is played by Elixir’s type system—&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@type&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@spec&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The fundamental difference between MetaAST and LLVM IR, Java bytecode, or any other intermediate representation lies precisely here: all of those are models (M¹). They describe concrete code in a concrete format. MetaAST is a meta-model (M²). It describes what descriptions of code &lt;em&gt;can be&lt;/em&gt;. The difference is roughly the same as between a dictionary and a language: a dictionary catalogues words, whereas a language defines the rules by which those words are possible in the first place.&lt;/p&gt;

&lt;hr/&gt;

&lt;p&gt;Metastatic is an Elixir library that implements this idea in code. The name, as befits a respectable technical project, is charged with a double meaning: &lt;strong&gt;Met(a)-AST-atic&lt;/strong&gt;, that is, “pertaining to the meta-level of AST.” The medical connotations are the house’s treat.&lt;/p&gt;

&lt;p&gt;The architecture is three-layered, and this is not caprice but a consequence of theory:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;M².1 — Core.&lt;/strong&gt; Concepts present in &lt;em&gt;every&lt;/em&gt; programming language on the planet. Literals, variables, binary operations, conditionals, function calls, assignments. Nothing exotic here. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;x + 5&lt;/code&gt; in Python, Elixir, Ruby, Erlang, and Haskell is one and the same thing. An identical MetaAST representation. Literally:&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;{:binary_op, [category: :arithmetic, operator: :+],
  [{:variable, [], &amp;quot;x&amp;quot;}, {:literal, [subtype: :integer], 5}]}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Five languages. One tree. One analysis tool. A score legible to any instrument in the orchestra.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;M².2 — Extended.&lt;/strong&gt; Constructs that exist in &lt;em&gt;most&lt;/em&gt; languages but not all. Loops, lambdas, collection operations, pattern matching, exception handling. Haskell knows nothing of imperative loops—so be it: it has recursion at the M².1 level. Ruby knows nothing of guards—no matter: the adapter’s metadata will preserve the context.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;M².3 — Native.&lt;/strong&gt; The emergency exit for constructs that resist generalisation. Rust’s lifetimes, Haskell’s type classes, Elixir’s metaprogramming. They are wrapped in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{:language_specific, :rust, ...}&lt;/code&gt;—like fragile porcelain in bubble wrap—and travel through the system without losing their identity, yet without claiming universality either.&lt;/p&gt;

&lt;hr/&gt;

&lt;p&gt;Every MetaAST node is a triple: type, metadata, children (or value). The format is deliberately borrowed from Elixir’s quoted expressions, because if you already know how to write macros in Elixir, you already know how to work with MetaAST. The difference is semantic: where Elixir uses &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:+&lt;/code&gt; (the operator itself), MetaAST uses &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:binary_op&lt;/code&gt; (the &lt;em&gt;concept&lt;/em&gt; of a binary operation) and tucks the operator into metadata. Where Elixir inlines literals, MetaAST wraps them in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{:literal, [subtype: :integer], 42}&lt;/code&gt;, ensuring structural uniformity.&lt;/p&gt;

&lt;p&gt;Language adapters form the bridge between M¹ and M². The Python adapter takes Python’s AST (obtained via subprocess) and &lt;em&gt;abstracts&lt;/em&gt; it up to MetaAST. The Elixir adapter takes quoted expressions and does the same. The Ruby adapter—likewise. The inverse operation—&lt;em&gt;reification&lt;/em&gt;—turns MetaAST back into the native AST of the target language. This pair of operations, abstraction and reification, constitutes what mathematicians call a Galois connection:&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;Adapter_L = (alpha_L, rho_L)

alpha_L: AS_L -&amp;gt; MetaAST x Metadata    (abstraction: M¹ -&amp;gt; M²)
rho_L:   MetaAST x Metadata -&amp;gt; AS_L    (reification: M² -&amp;gt; M¹)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;What does this mean in practice? Here is a scenario. You have written a function-purity analyser. It operates on MetaAST: traverses the tree, looks for side effects (I/O, state mutation, calls to random-number generators), and renders its verdict. This analyser was written once. Once. And it works with Python, Elixir, Ruby, Erlang, and Haskell. Because it analyses not the Python or Elixir AST but the &lt;em&gt;meta-level&lt;/em&gt;. You write:&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;{:ok, doc} = Metastatic.Adapter.abstract(Python, &amp;quot;print(&amp;#39;hello&amp;#39;)&amp;quot;, :python)
{:ok, result} = Metastatic.Analysis.Purity.analyze(doc)
result.pure?    # =&amp;gt; false — side effect: I/O
result.effects  # =&amp;gt; [:io]&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And precisely the same code, unchanged, works if you replace Python with Ruby and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;print(&amp;#39;hello&amp;#39;)&lt;/code&gt; with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;puts &amp;#39;hello&amp;#39;&lt;/code&gt;. Because both calls are &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{:function_call, [name: &amp;quot;print&amp;quot;], [{:literal, [subtype: :string], &amp;quot;hello&amp;quot;}]}&lt;/code&gt; at the M² level.&lt;/p&gt;

&lt;hr/&gt;

&lt;p&gt;Let us return to MOF and the four-level hierarchy. Why is any of this necessary? Why erect meta-models when one could simply write a converter from one AST to another?&lt;/p&gt;

&lt;p&gt;The answer is simple and brutal, like the truth of life. A converter works pairwise. For five languages you need twenty converters (5 × 4). For ten—ninety. For twenty—three hundred and eighty. A meta-model works through a hub: each language connects once, via its own adapter. Five languages—five adapters. Ten—ten. Twenty—twenty. Linear growth instead of quadratic. A mathematician who had spent half his life as a reporter would appreciate the irony.&lt;/p&gt;

&lt;p&gt;But it is not only a matter of combinatorics. A meta-model provides &lt;em&gt;standardisation&lt;/em&gt;. Every tool written for MetaAST is guaranteed to work with any language for which an adapter exists. This is not “Python support” and “Ruby support” as separate features. It is one feature: MetaAST support. Everything else follows as a consequence.&lt;/p&gt;

&lt;p&gt;OMG understood this in 2002 when it released MOF. The entire UML industry is built on the same principle: the meta-model defines what models &lt;em&gt;can be&lt;/em&gt;, and concrete diagrams are merely instances of that meta-model. MDA (Model-Driven Architecture) took the idea further: transformations between models are defined at the meta-level and applied automatically to any instances.&lt;/p&gt;

&lt;p&gt;Metastatic does the same, but not for class diagrams—for the syntax trees of programs. It is not an IR, not a compiler, not a transpiler. It is the foundation upon which all of the above can be built—once, and for every language at once.&lt;/p&gt;

&lt;hr/&gt;

&lt;p&gt;There is an old story about a farmer, I’ve heard from a stranger on a train. A man spends his entire life breeding different eggplant varieties. In decades, when the farmer is already old and blind, his fields have produced an unprecedented harvest. He calls for each and every agronomist in the world, they come, and the most famous one says, “These aubergines are adorable!” The farmer dies of frustration. The man does not know what &lt;em&gt;aubergines&lt;/em&gt; are but he’s certain the great eggplant nobody would have called ‘aubergine.’ He considers his life wasted.&lt;/p&gt;

&lt;p&gt;The ASTs of different languages are the eggplants. MetaAST is the knowledge that they all grow on the same field. The difference between them is terminological, not semantic. Python’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BinOp(op=Add())&lt;/code&gt;, Elixir’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{:+, [], [...]}&lt;/code&gt;, and Ruby’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;s(:send, ..., :+, ...)&lt;/code&gt; are different names for one and the same thing: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{:binary_op, [category: :arithmetic, operator: :+], [left, right]}&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;One could, of course, spend an entire life rewriting tools for every new language. One could relearn musical notation each time, bow before every new instrument in the orchestra, translate letters into five tongues for five floors of one and the same building. But why, when one can ascend a single level of abstraction—to the place where eggplants and aubergines are indistinguishable, and the score is one for all?&lt;/p&gt;</content:encoded>
</item>
<item>
<title>Dropping Cloudflare for bunny.net | jola.dev</title>
<link>https://jola.dev/posts/dropping-cloudflare</link>
<enclosure type="image/jpeg" length="0" url="https://jola.dev/images/og-image-2b7872671fc7c11e464dac899d8d3068.png?vsn=d"></enclosure>
<guid isPermaLink="false">1M42zLP0OWc6VEZ7PxV89n0bn2eHHFxl7owrmw==</guid>
<pubDate>Tue, 07 Apr 2026 18:13:31 +0000</pubDate>
<description>Dropping Cloudflare and migrating to bunny.net, starting out with my blog.</description>
<content:encoded>&lt;p&gt;
TL;DR my motivation and experience for moving my blog from Cloudflare to bunny.net&lt;/p&gt;
&lt;p&gt;
I’ve been a long time Cloudflare user. They offer a solid service that is free for the vast majority of their users, that’s very generous. Their infrastructure is massive and their feature set is undeniably incredible. &lt;/p&gt;
&lt;p&gt;
One of my biggest concerns though is around how easily I could become heavily dependent on this one single company that then can decide to cut me off and disable all of my websites, for any arbitrary reason. It’s a single point of failure for the internet. Every Cloudflare outage ends up in the news. And I can’t help but feel that the idea of centralizing the internet into a single US corporation feels off. Not to mention the various scandals that have surrounded them. So I was open to alternatives.&lt;/p&gt;
&lt;h2&gt;
Bunny.net&lt;/h2&gt;
&lt;p&gt;
&lt;a href=&quot;https://bunny.net?ref=f0l8865b7g&quot;&gt;Bunny.net&lt;/a&gt; (affiliate link because why not, raw link &lt;a href=&quot;https://bunny.net&quot;&gt;here&lt;/a&gt;) is a Slovenian (EU) company that is building up a lot of momentum. Their CDN-related services rival Cloudflare already, and although their PoP network is smaller than Cloudflare’s, they score highly on performance and speed across the globe. It’s a genuinely competitive alternative to Cloudflare.&lt;/p&gt;
&lt;p&gt;
It has the additional benefit of being a European company, and I like the idea of growing and supporting the European tech scene.&lt;/p&gt;
&lt;h2&gt;
What I was moving away from&lt;/h2&gt;
&lt;p&gt;
I’ve been using various different services, but focusing on this blog, the first thing was Cloudflare as the registrar for the domain name. I did some research on alternative registrars, but I just didn’t find any good European options. The closest I found was INWX, but their lack of free WHOIS Privacy made them a non-option. I ended up with Porkbun. They run on Cloudflare infrastructure, but they have better support. So the remaining thing Cloudflare was doing for me was the “Orange Cloud”: automatic caching, origin hiding, and optional protection features.&lt;/p&gt;
&lt;p&gt;
So that’s what we’re moving over! I’m gonna walk you through how to set up the bunny.net CDN for your website, with some sensible defaults.&lt;/p&gt;
&lt;h2&gt;
Step by step&lt;/h2&gt;
&lt;p&gt;
Setting up your bunny.net account is quick and you get $20 worth of free credits to play around with, those are valid for 14 days. You don’t need to give them a credit card up front to try things out, but if you do, you get another $30 worth of credits. You do need to confirm your email though before you can start setting things up. Once you’re out of the trial, you pay per use, which for most cases is cents a month. However, note that bunny.net require a minimum payment of $1 per month.&lt;/p&gt;
&lt;p&gt;
I guess a cheap price to pay to &lt;em&gt;stop being the product&lt;/em&gt; and start becoming the customer.&lt;/p&gt;
&lt;h3&gt;
Creating your pull zone&lt;/h3&gt;
&lt;p&gt;
The pull zone is the main mechanism for enabling the CDN for your website. You’ll find them under CDN in the left navigation bar. Here’s how to set one up:&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;
Fill in the pull zone name. Just make it something meaningful to you, for example the website name.  &lt;/li&gt;
  &lt;li&gt;
For origin type, select Origin URL.  &lt;/li&gt;
  &lt;li&gt;
Fill in your Origin URL. This would be the address for directly accessing your server. In my case, it’s the public IP of my server.   &lt;/li&gt;
  &lt;li&gt;
If you’re running multiple apps on your server, for example using Dokploy, coolify, or self-hosted PaaSs like that, you’ll want to pass the Host header as well. Here you put in the domain of your app. In my case, that’s jola.dev.  &lt;/li&gt;
  &lt;li&gt;
For tier, select Standard.  &lt;/li&gt;
  &lt;li&gt;
Finally you can select your pricing zones. Note that some zones are more expensive, so you can choose to disable them. This just means that people in those areas will get redirected to the closest zone you do have enabled.  &lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;
And you’re done with the first part!&lt;/p&gt;
&lt;h3&gt;
Configuring your pull zone&lt;/h3&gt;
&lt;p&gt;
Now that you’ve set up the pull zone, it’s time to hook it up to your website and domain. Go to the pull zone you created. You’ll see a “hostnames” screen. Time to connect things.&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;
Under “Add a custom hostname” fill in your website domain name.  &lt;/li&gt;
  &lt;li&gt;
You’ll get a modal with some instructions. You need to follow them to set up the DNS name to point your website to go through the CDN.  &lt;/li&gt;
  &lt;li&gt;
Go to where you manage domain name and add a CNAME record to point your domain to the given CNAME value in the modal, something like website.b-cdn.net.  &lt;/li&gt;
  &lt;li&gt;
Once you’ve done that, wait a few minutes to let it propagate, and then click “Verify &amp;amp; Activate SSL”.   &lt;/li&gt;
  &lt;li&gt;
If it says success, you’re done. Your website is now running through the bunny.net CDN, similar to the Cloudflare orange cloud.  &lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
Configuring caching&lt;/h3&gt;
&lt;p&gt;
This is the part where bunny.net will really shine through!&lt;/p&gt;
&lt;p&gt;
If your website is set up to return the appropriate cache headers for each resource, things will just work. Bunny defaults to respecting the cache control headers when pointing a pull zone at an origin site. To verify, go to Caching → General and check that “Respect origin Cache-Control” is set under “Cache expiration time”. Note that if you set &lt;code class=&quot;makeup ok&quot;&gt;no-cache&lt;/code&gt;, bunny will use that and will not cache at the edge.&lt;/p&gt;
&lt;p&gt;
Alternatively, if you don’t have cache headers set up, and you don’t want to control that yourself, you can instead enable Smart Cache. This will default to caching typically cached resources like images, CSS, JS files etc, while avoiding caching things like HTML pages. This will work for most cases!&lt;/p&gt;
&lt;p&gt;
But I wanted to go &lt;em&gt;faster&lt;/em&gt;. If you’ve read my post about building this website, here’s how I’ve set up my cache headers: I added a new pipeline in the router called &lt;code class=&quot;makeup ok&quot;&gt;public&lt;/code&gt; and added an extra middleware to it. I technically have everything using this pipeline, but leaving the standard &lt;code class=&quot;makeup ok&quot;&gt;browser&lt;/code&gt; pipeline that comes out of the box with Phoenix keeps my options open to add authenticated (uncached) pages in the future. &lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;makeup ok&quot;&gt;pipeline :public do
    plug :accepts, [&amp;quot;html&amp;quot;]
    plug :put_root_layout, html: {JolaDevWeb.Layouts, :root}
    plug :put_secure_browser_headers, @secure_headers
    plug :put_cdn_cache_header
  end
  
  defp put_cdn_cache_header(conn, _opts) do
    put_resp_header(conn, &amp;quot;cache-control&amp;quot;, &amp;quot;public, s-maxage=86400, max-age=0&amp;quot;)
  end&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;
You can see the whole router here &lt;a href=&quot;https://github.com/joladev/jola.dev/blob/main/lib/jola_dev_web/router.ex&quot;&gt;https://github.com/joladev/jola.dev/blob/main/lib/jola_dev_web/router.ex&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;
This setup means I even cache the HTML pages, which makes this ridiculously fast. Here’s the landing page response time from various locations, using the &lt;a href=&quot;https://larm.dev/tools/response-time/r/89374810-dbb3-4227-87d1-9a947be29e49&quot;&gt;Larm response time checker tool&lt;/a&gt;:&lt;/p&gt;
&lt;img src=&quot;https://jola.dev/images/joladev-larm-response-time.png&quot; alt=&quot;&quot; title=&quot;&quot;/&gt;
&lt;p&gt;
Because I’m caching the HTML pages, if I publish a new post I do need to purge the pull zone to reset the cached HTML files.&lt;/p&gt;
&lt;h3&gt;
Setting some sensible defaults&lt;/h3&gt;
&lt;p&gt;
All of these are optional, but nice to have!&lt;/p&gt;
&lt;p&gt;
On your pull zone page, under General → Hostnames, go toggle “Force SSL” on for your domain to ensure that all requests use SSL. SSL/TLS is pretty standard these days, and many TLDs and websites use HSTS to enforce it, but no harm in enabling it here too.&lt;/p&gt;
&lt;p&gt;
DDoS protection comes out of the box, but we can set some other things up. First of all, go to Caching and then Origin Shield in the left menu on your pull zone, and activate Origin Shield. Select the location closest to your origin. This reduces load on your server, as bunny.net will cache everything in the Origin Shield location, and all edge locations will try that location first before hitting your server.&lt;/p&gt;
&lt;p&gt;
Next, go to Caching → General and scroll down. At the bottom of the page you can select Stale Cache: While Origin Offline and While Updating. This means bunny will keep serving cached content even if it is stale, if it can’t reach your origin, and that it will serve stale content while fetching the latest version. Both are nice to haves, nothing you have to enable, but provide a slightly better service to your users!&lt;/p&gt;
&lt;p&gt;
Next, let’s set up an Edge rule to redirect any requests to our automatically generated pull zone domain to our actual domain, to avoid confusing crawlers. On your pull zone, in the left menu, click Edge rules. &lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;
Add edge rule.  &lt;/li&gt;
  &lt;li&gt;
Name it “Default domain redirect”.  &lt;/li&gt;
  &lt;li&gt;
Under actions, select Redirect.  &lt;/li&gt;
  &lt;li&gt;
For URL, input your URL plus the path variable. Eg for me it’s &lt;code class=&quot;makeup ok&quot;&gt;https://jola.dev{{path}}&lt;/code&gt; .  &lt;/li&gt;
  &lt;li&gt;
Status code: use the default 301.  &lt;/li&gt;
  &lt;li&gt;
For conditions, pick Match any and Request URL Match any.  &lt;/li&gt;
  &lt;li&gt;
Input &lt;code class=&quot;makeup ok&quot;&gt;*://&amp;lt;slug&amp;gt;.b-cdn.net/*&lt;/code&gt; replacing &lt;code class=&quot;makeup ok&quot;&gt;&amp;lt;slug&amp;gt;&lt;/code&gt; with the name given to your pull zone.  &lt;/li&gt;
  &lt;li&gt;
Save edge rule!  &lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;
Now you should be able to go to &lt;code class=&quot;makeup ok&quot;&gt;https://slug.b-cdn.net&lt;/code&gt; for your pull zone and get redirected to your proper domain!&lt;/p&gt;
&lt;h2&gt;
Conclusion&lt;/h2&gt;
&lt;p&gt;
This post just covers the very basics of getting set up on bunny.net. I haven’t even scratched the surface of edge rules, cache configuration, the Shield features for security and firewalls, video hosting and streaming, edge scripting and edge distributed containers, and much more.&lt;/p&gt;
&lt;p&gt;
I especially appreciate the great statistics, logs, and metrics you get out of the dashboard. You can even see every single request coming through to help you investigate issues, and clear feedback on what’s getting cached and not. I’m actively moving everything else over and I’m excited for the upcoming S3 compatible storage!&lt;/p&gt;
&lt;p&gt;
You should give &lt;a href=&quot;https://bunny.net?ref=f0l8865b7g&quot;&gt;bunny.net&lt;/a&gt; a try!&lt;/p&gt;</content:encoded>
</item>
<item>
<title>Newres Al Haider</title>
<link>https://www.newresalhaider.com/post/yggdrasil/</link>
<enclosure type="image/jpeg" length="0" url="https://www.newresalhaider.com/post/yggdrasil/featured.png"></enclosure>
<guid isPermaLink="false">9f86ElXSxCWyKIG3n3jqy43jSawpsOe4kIddWQ==</guid>
<pubDate>Mon, 06 Apr 2026 18:15:11 +0000</pubDate>
<description>An introduction to the Ash declarative framework by growing Yggdrasil, the World Tree of Norse mythology.</description>
<content:encoded>&lt;h1&gt;
    Growing Yggdrasil, the World Tree, with Ash
&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;2026-04-05&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Declarative programming can be a powerful paradigm for organizing software systems. By defining the business processes once, we ensure there is a single source of domain knowledge. From this foundation, we can derive other parts of the system such as API endpoints, database schemas, and even user interfaces. This approach reduces repetition and helps prevent bugs caused by misaligned domain models.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://ash-hq.org/&quot;&gt;Ash Framework&lt;/a&gt; seems like an excellent way to see declarative programming in action. Written in &lt;a href=&quot;https://elixir-lang.org/&quot;&gt;Elixir&lt;/a&gt;, it allows you to describe your domain in a consistent and expressive way, from which it can automatically generate data layers, REST or GraphQL APIs, and admin interfaces. With Ash, you define what your application should do, and the framework takes care of how to make it happen. It can derive JSON REST endpoints, handle validation, manage persistence, and provide authorization logic, all from the same declarative definitions.&lt;/p&gt;
&lt;p&gt;I am new to Ash and I tend to learn best by writing things out, so this article is as much for me as it is for you. Rather than trying to understand everything up front, I prefer to get hands-on quickly with a small, self-contained project. We’ll start with the basics here, and if things go well, expand on it in a follow-up or two.&lt;/p&gt;
&lt;p&gt;Given the name Ash, it felt appropriate to build something inspired by the gigantic ash tree of Norse mythology: &lt;a href=&quot;https://en.wikipedia.org/wiki/Yggdrasil&quot;&gt;Yggdrasil, the World Tree&lt;/a&gt;.&lt;/p&gt;
&lt;figure&gt;
    &lt;img src=&quot;https://www.newresalhaider.com/post/yggdrasil/featured.png&quot; alt=&quot;&quot; title=&quot;&quot;/&gt;
    
    &lt;figcaption&gt;
        
        &lt;h4&gt;An image of a Yggdrasil the world tree as a cybernetic Ash tree.&lt;/h4&gt;
        
        
    &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Yggdrasil is said to connect the Nine Worlds of &lt;a href=&quot;https://en.wikipedia.org/wiki/Norse_cosmology&quot;&gt;Norse cosmology&lt;/a&gt;, though the exact number and nature of these worlds vary between sources. Each world has its own nature, inhabitants, and relationships with the others, making it an ideal metaphor for exploring how Ash models resources, attributes, and relationships. In this project, we will create a domain model for these concepts with Ash and derive a JSON REST API from them.&lt;/p&gt;
&lt;p&gt;The first step is getting started with a basic Ash project for which we will use the Igniter tool. (I will assume Elixir is already installed, but if not, see the &lt;a href=&quot;https://elixir-lang.org/install.html&quot;&gt;Elixir Install page&lt;/a&gt; for instructions). This is used for project setup and code generation, which will help us get started a lot quicker.&lt;/p&gt;
&lt;p&gt;To start off following command will install Igniter:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;mix archive.install hex igniter_new&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Once we have Igniter, the next step is creating a new project in the &lt;code&gt;yggdrasil&lt;/code&gt; directory, adding Ash, and moving into it.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;mix igniter.new yggdrasil --install ash &amp;amp;&amp;amp; cd yggdrasil&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;This will land us in a new Elixir project directory with Ash installed. From this seed we will evolve our application to represent the worlds and characters of Norse mythology.&lt;/p&gt;
&lt;p&gt;The newly created project comes with a &lt;code&gt;hello&lt;/code&gt; function in the &lt;code&gt;lib/yggdrasil.ex&lt;/code&gt; module. Let&amp;#39;s try it out in the &lt;code&gt;iex&lt;/code&gt;, the Elixir interactive shell which we can start with:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;iex -S mix&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Running it will bring us into the shell, where we can run the &lt;code&gt;hello&lt;/code&gt; function in the &lt;code&gt;yggdrasil&lt;/code&gt; module:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;iex(1)&amp;gt; Yggdrasil.hello()
:world&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We now have a hello world, but in yggdrasil we want to represent the &lt;em&gt;worlds&lt;/em&gt; of Norse mythology that are linked by Yggdrasil. The first thing we need for this is a &lt;em&gt;Domain&lt;/em&gt;. This will function as a container for the various concepts, such as the worlds, that we will introduce later.&lt;/p&gt;
&lt;p&gt;For simplicity&amp;#39;s sake, first we will replace the contents of &lt;code&gt;lib/Yggdrasil.ex&lt;/code&gt; with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-elixir&quot;&gt;defmodule Yggdrasil do
  @moduledoc &amp;quot;&amp;quot;&amp;quot;
  The Yggdrasil domain — acts as the trunk of the tree
  and organizes all resources like World and Character.
  &amp;quot;&amp;quot;&amp;quot;

  use Ash.Domain

  resources do
    # Resources will be registered here
    resource Yggdrasil.World
  end
end&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We create a file &lt;code&gt;lib/resources/world.ex&lt;/code&gt; with the following contents:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-elixir&quot;&gt;defmodule Yggdrasil.World do
  @moduledoc &amp;quot;&amp;quot;&amp;quot;
  A resource representing a world in Yggdrasil.
  &amp;quot;&amp;quot;&amp;quot;

  use Ash.Resource,
    # in-memory store
    data_layer: Ash.DataLayer.Ets,
    domain: Yggdrasil

  actions do
    create :create do
      accept [:name, :description]
    end

    update :update do
      accept [:description]
    end

    # Provide default actions
    defaults [:read, :destroy]
  end

  attributes do
    # Primary key
    uuid_primary_key :id

    # World name and description
    attribute :name, :string, allow_nil?: false, public?: true
    attribute :description, :string, public?: true
  end
end&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And finally &lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-elixir&quot;&gt;config :yggdrasil, :ash_domains, [Yggdrasil]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With these files in place, we now have a minimal Ash domain containing a single resource: World. Let’s take a moment to unpack what we just created before moving on.&lt;/p&gt;
&lt;p&gt;At the top level, Yggdrasil acts as our domain, the trunk of our system. It brings together all the resources that make up the application and defines how they relate to each other. Right now, our domain only includes one resource, Yggdrasil.World, but we’ll add more later.&lt;/p&gt;
&lt;p&gt;The Yggdrasil.World module itself is declared as a resource. In Ash, a resource is the fundamental building block. It describes a specific type of data and what can be done with it. Instead of writing separate schemas, changesets, and controllers, we declare everything about a resource in one place, and Ash takes care of the details.&lt;/p&gt;
&lt;p&gt;Our World resource uses the Ash.DataLayer.Ets data layer, which stores data in Elixir’s in-memory ETS tables. This setup is fast and simple, making it perfect for early experimentation, though data won’t persist between runs. Later, this can be swapped out for a different data layer to gain full persistence. The argument &lt;code&gt;domain: Yggdrasil&lt;/code&gt; connects the resource back to the domain we just defined so that the framework knows where it belongs.&lt;/p&gt;
&lt;p&gt;Inside the actions block, we declare what operations are available for this resource. The create action accepts a name and a description, while the defaults &lt;code&gt;[:read, :destroy]&lt;/code&gt; line automatically adds the standard read, update, and delete actions. There’s no need to write any manual CRUD logic—Ash generates it for us.&lt;/p&gt;
&lt;p&gt;The attributes block defines the structure of each world. Every world has a UUID primary key (&lt;code&gt;:id&lt;/code&gt;) and two fields, &lt;code&gt;:name&lt;/code&gt; and &lt;code&gt;:description&lt;/code&gt;. The &lt;code&gt;:name&lt;/code&gt; attribute is made required using &lt;code&gt;(allow_nil?: false)&lt;/code&gt;, ensuring that each world must have one. Both attributes are marked &lt;code&gt;public?: true&lt;/code&gt; so they appear in APIs and outputs.&lt;/p&gt;
&lt;p&gt;Finally, the configuration line we added tells Ash which domains to load when the application starts. Without this, the framework wouldn’t know about our new resource.&lt;/p&gt;
&lt;p&gt;At this point, our small Ash tree has already taken root. We’ve declared the first piece of our domain, and Ash now knows how to create, read, update, and delete worlds. Let’s see that in action next by exploring our resource interactively in iex.&lt;/p&gt;
&lt;p&gt;First, start the interactive shell from your project root:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;iex -S mix&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once inside iex, we want to create our first world Asgard, the shining realm of the gods, with the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-elixir&quot;&gt;asgard = (
  Yggdrasil.World
  |&amp;gt; Ash.Changeset.for_create(:create, %{
       name: &amp;quot;Asgard&amp;quot;,
       description: &amp;quot;A shining realm of order and power, suspended high above the clouds.&amp;quot;
     })
  |&amp;gt; Ash.create!()
)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;which would return the world as such: &lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-elixir&quot;&gt;14:29:14.665 [debug] Creating Yggdrasil.World:

Setting %{id: &amp;quot;726b678e-6cb6-4277-b291-85ecfa313d3a&amp;quot;, name: &amp;quot;Asgard&amp;quot;,
 description: &amp;quot;A shining realm of order and power,...}

%Yggdrasil.World{
  id: &amp;quot;726b678e-6cb6-4277-b291-85ecfa313d3a&amp;quot;,
  name: &amp;quot;Asgard&amp;quot;,
  description: &amp;quot;A shining realm of order and power, suspended high above the clouds.&amp;quot;,
  __meta__: #Ecto.Schema.Metadata&amp;lt;:loaded&amp;gt;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There are a multiple things happening here, so let&amp;#39;s unwrap things step by step. &lt;/p&gt;
&lt;p&gt;First we start off with our Ash resource that we have defined &lt;code&gt;Yggdrasil.World&lt;/code&gt;. In Ash, resources describe the structure of our data, including attributes like name and description, as well as the actions that can be performed on them.&lt;/p&gt;
&lt;p&gt;Next we are using the pipe operator. &lt;code&gt;|&amp;gt;&lt;/code&gt;, to pass this result to our next function. Elixir’s pipe operator takes the result of the expression on the left and passes it as the first argument to the function on the right.&lt;/p&gt;
&lt;p&gt;For example instead of writing:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-elixir&quot;&gt;function(value, a, b)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;with the pipe operator we can equivalently write:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-elixir&quot;&gt;value |&amp;gt; function(a, b)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This allows us to write a sequence of operations in a readable, step-by-step style. In our code example, it means &lt;code&gt;Yggdrasil.World&lt;/code&gt; is passed into the &lt;code&gt;Ash.Changeset.for_create&lt;/code&gt; function, as the first parameter. This function also takes the identifier of our create action &lt;code&gt;:create&lt;/code&gt;, as well as the structure representing Asgard, with its name and description. &lt;/p&gt;
&lt;p&gt;What this function returns is a &lt;code&gt;changeset&lt;/code&gt;, a data structure representing the intended change of a resource in Ash (e.g.: creating, updating, etc). This is especially useful when it comes to validation and error checking, as we will see it later down the line. For now we use this changeset and pipe it into the function that executes the actual creation: &lt;code&gt;Ash.create!()&lt;/code&gt;. &lt;/p&gt;
&lt;p&gt;The resulting value is a &lt;code&gt;%Yggdrasil.World{}&lt;/code&gt; struct, which represents the newly created world. Ash also automatically generated a UUID for the id field, which uniquely identifies this world inside the system.&lt;/p&gt;
&lt;p&gt;Before returning the struct, Ash logs the operation it performed. That is why we see the debug output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-elixir&quot;&gt;[debug] Creating Yggdrasil.World&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The final line is the Elixir struct that was created:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-elixir&quot;&gt;%Yggdrasil.World{
  id: &amp;quot;726b678e-6cb6-4277-b291-85ecfa313d3a&amp;quot;,
  name: &amp;quot;Asgard&amp;quot;,
  description: &amp;quot;A shining realm of order and power, suspended high above the clouds.&amp;quot;,
  __meta__: #Ecto.Schema.Metadata&amp;lt;:loaded&amp;gt;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This struct is also stored in the variable &lt;code&gt;asgard&lt;/code&gt;, so we can reference it later in the session.&lt;/p&gt;
&lt;p&gt;Now that we understand how creating a world works, let’s add another one.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-elixir&quot;&gt;midgard = (
  Yggdrasil.World
  |&amp;gt; Ash.Changeset.for_create(:create, %{
       name: &amp;quot;Midgard&amp;quot;,
       description: &amp;quot;The realm of humans, bound to the earth and everyday struggles.&amp;quot;
     })
  |&amp;gt; Ash.create!()
)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This follows the exact same pattern as before. We build a changeset that describes the creation of Midgard, and then execute it with &lt;code&gt;Ash.create!()&lt;/code&gt;. Much simpler than the mythological creation of &lt;a href=&quot;https://en.wikipedia.org/wiki/Midgard&quot;&gt;Midgard&lt;/a&gt;, which involved the slaying of the giant Ymir.&lt;/p&gt;
&lt;p&gt;Now that we have some worlds, let&amp;#39;s read them using the read action: &lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-elixir&quot;&gt;worlds = (
  Yggdrasil.World
  |&amp;gt; Ash.Query.for_read(:read)
  |&amp;gt; Ash.read!()
)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;which would give us our list of worlds:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-elixir&quot;&gt;[
  %Yggdrasil.World{
    id: &amp;quot;some-uuid-1&amp;quot;,
    name: &amp;quot;Asgard&amp;quot;,
    description: &amp;quot;A shining realm of order and power, suspended high above the clouds.&amp;quot;
  },
  %Yggdrasil.World{
    id: &amp;quot;some-uuid-2&amp;quot;,
    name: &amp;quot;Midgard&amp;quot;,
    description: &amp;quot;The realm of humans, bound to the earth and everyday struggles.&amp;quot;
  }
]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As one can expect, we can also do an update call. For example, let’s change the description of Asgard:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-elixir&quot;&gt;asgard = (
  asgard
  |&amp;gt; Ash.Changeset.for_update(:update, %{
       description: &amp;quot;The fortified realm of the Aesir, ruled by Odin.&amp;quot;
     })
  |&amp;gt; Ash.update!()
)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There are a few things to note here. Instead of starting from the &lt;code&gt;Yggdrasil.World&lt;/code&gt; module, we now start from the existing asgard struct. This is because we are modifying a resource that already exists.&lt;/p&gt;
&lt;p&gt;The function &lt;code&gt;for_update&lt;/code&gt; creates a changeset that describes the intended update. Just like with creation, the changeset itself does not perform the update, it only represents the change we want to make.&lt;/p&gt;
&lt;p&gt;We then pass this changeset into &lt;code&gt;Ash.update!()&lt;/code&gt;, which executes the update. Ash applies the changes, runs any validations, and returns the updated &lt;code&gt;%Yggdrasil.World{}&lt;/code&gt; struct.&lt;/p&gt;
&lt;p&gt;We can verify the change by reading the list of worlds again:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-elixir&quot;&gt;worlds = (
  Yggdrasil.World
  |&amp;gt; Ash.Query.for_read(:read)
  |&amp;gt; Ash.read!()
)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;which would give us a result such as: &lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-elixir&quot;&gt;[
  %Yggdrasil.World{
    id: &amp;quot;6b62b3ea-b08b-4387-8539-37e645e53026&amp;quot;,
    name: &amp;quot;Midgard&amp;quot;,
    description: &amp;quot;The realm of humans, bound to the earth and everyday struggles.&amp;quot;,
    __meta__: #Ecto.Schema.Metadata&amp;lt;:loaded&amp;gt;
  },
  %Yggdrasil.World{
    id: &amp;quot;d2646509-6c92-4049-a2db-0555612fc365&amp;quot;,
    name: &amp;quot;Asgard&amp;quot;,
    description: &amp;quot;The fortified realm of the Aesir, ruled by Odin.&amp;quot;,
    __meta__: #Ecto.Schema.Metadata&amp;lt;:loaded&amp;gt;
  }
]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;An interesting thing we could try out is updating the name of a world instead:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-elixir&quot;&gt;asgard2 = (
  asgard
  |&amp;gt; Ash.Changeset.for_update(:update, %{
       name: &amp;quot;Asgard2&amp;quot;
     })
  |&amp;gt; Ash.update!()
)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We get the following error: &lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-elixir&quot;&gt;** (Ash.Error.Invalid)
Invalid Error

* No such input `name` for action Yggdrasil.World.update

The attribute exists on Yggdrasil.World, but is not accepted by Yggdrasil.World.update

Perhaps you meant to add it to the accept list for Yggdrasil.World.update?


Valid Inputs:

* description&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is because when we were defining our update action in our module, the only attribute we accept is &lt;code&gt;:description&lt;/code&gt;, see fragment below:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-elixir&quot;&gt;update :update do
      accept [:description]
    end&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In other words, while the name attribute exists on the resource, it is not allowed to be modified through the update action. This is a domain modelling decision, and gives us fine-grained control over how our data can change. In this case, we decided that a world’s name is fixed after creation, while its description can evolve over time.&lt;/p&gt;
&lt;p&gt;Finally we get to do delete, where we destroy asgard, our Ragnarok action if you will. We can do this by the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-elixir&quot;&gt;Ash.destroy!(asgard)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Ash.destroy!&lt;/code&gt; takes a resource struct, in this case &lt;code&gt;asgard&lt;/code&gt;, and removes it from the data store. Since we’re using an in-memory ETS store, it deletes it from memory immediately. The function should return &lt;code&gt;:ok&lt;/code&gt; on success. We can double check this by requesting our list of worlds again by our usual means:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-elixir&quot;&gt;worlds = (
  Yggdrasil.World
  |&amp;gt; Ash.Query.for_read(:read)
  |&amp;gt; Ash.read!()
)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;which returns only Midgard:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-elixir&quot;&gt;[
  %Yggdrasil.World{
    id: &amp;quot;6b62b3ea-b08b-4387-8539-37e645e53026&amp;quot;,
    name: &amp;quot;Midgard&amp;quot;,
    description: &amp;quot;The realm of humans, bound to the earth and everyday struggles.&amp;quot;,
    __meta__: #Ecto.Schema.Metadata&amp;lt;:loaded&amp;gt;
  }
]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At this point, we’ve taken the first steps in modeling our little piece of Yggdrasil. We have a domain, a resource, and a way to create, read, update, and delete worlds, enough to bring about a small Ragnarok.&lt;/p&gt;
&lt;p&gt;Next, we will explore how we can start connecting resources together. After all, the worlds need their heroes and villains to really come alive.&lt;/p&gt;</content:encoded>
</item>
<item>
<title>How Many Paradigms Does It Take to Screw In a Lightbulb?</title>
<link>https://rocket-science.ru/hacking/2026/04/06/paradigms-for-lightbulb</link>
<guid isPermaLink="false">ysRzvH1jN84Qm_Agm_de_LqeXFor5GTkMeR9hw==</guid>
<pubDate>Mon, 06 Apr 2026 12:52:41 +0000</pubDate>
<description>A developer who knows only one programming paradigm resembles a carpenter whose entire toolbox contains a single hammer. Naturally, a hammer will drive a nail with admirable precision. Or a screw, if sufficient enthusiasm is applied. But try to saw or plane a board with that hammer, and it becomes immediately clear—assuming you’ve encountered a saw or a plane at least once in your life—that the instrument has been chosen poorly. So it is with paradigms: knowledge of nothing but imperati...</description>
<content:encoded>&lt;p&gt;A developer who knows only one programming paradigm resembles a carpenter whose entire toolbox contains a single hammer. Naturally, a hammer will drive a nail with admirable precision. Or a screw, if sufficient enthusiasm is applied. But try to saw or plane a board with that hammer, and it becomes immediately clear—assuming you’ve encountered a saw or a plane at least once in your life—that the instrument has been chosen poorly. So it is with paradigms: knowledge of nothing but imperative programming, or nothing but object-oriented design, transforms a developer into a mechanical executor of tasks, incapable of seeing an elegant solution even when it lies on the surface, waiting to be noticed.&lt;/p&gt;&lt;p&gt;The narrowness of a programmer trapped in a single paradigm manifests in everything. They will erect loops where a single higher-order function would suffice. They will breed classes and inheritance where a pure function and composition would have been more than enough. They will attempt to verify the correctness of an algorithm with a debugger and tests instead of proving it formally at the type level. Such a developer resembles a tourist who knows exactly one word of the foreign language and is attempting, with its help, to explain a route across the entire city to a taxi driver. And it’s a small mercy if the word isn’t obscene.&lt;/p&gt;&lt;p&gt;Let us, for a start, walk through the principal paradigms and see what instruments each offers for solving problems. We’ll begin with the most ancient and familiar—the imperative paradigm.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Imperative programming&lt;/strong&gt; is the world of instructions and mutable state. The programmer tells the machine: do this, then that, change this variable, repeat five times. A classical example in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;C&lt;/code&gt;:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;intsum=0;for(inti=0;i&amp;lt;10;i++){sum+=i;}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;Here we explicitly manage the state of the variable &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sum&lt;/code&gt;, accumulating the result step by step. This is natural for the machine, but tedious for the human. Every step must be spelled out, every mutation tracked. The imperative style serves well when the task reduces to a sequence of actions with side effects: write to a file, update a database, print to the screen. But as soon as the task grows in complexity, the code devolves into a tangle of interrelated variables and conditions.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Procedural programming&lt;/strong&gt; is the imperative approach enriched with structures and functions. We group instructions into procedures to avoid repetition and improve readability. The same example:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;intcalculate_sum(intn){intsum=0;for(inti=0;i&amp;lt;n;i++){sum+=i;}returnsum;}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;Now the logic is packaged into a function that can be reused. The procedural style dominated the era of Pascal and early &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;C&lt;/code&gt;. It taught programmers to think in modules and structure their code, but it never freed them from the problems of mutable state and side effects.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Object-oriented programming&lt;/strong&gt; (in Gosling’s understanding, not Kay’s) promised to solve all problems at once: encapsulation, inheritance, polymorphism—the three pillars upon which the entire world supposedly rests. Data and methods unite into objects, objects assemble into class hierarchies. It sounds splendid, until you begin to examine how the code actually works:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;classCounter{privateintvalue=0;publicvoidincrement(){value++;}publicintgetValue(){returnvalue;}}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;State lives inside the object, convenient methods form the API, full encapsulation achieved. So it would seem, but the state hasn’t gone anywhere—it has merely relocated into a class field. And along with it relocated all the old afflictions: data races in multithreading, the difficulty of testing, the unpredictability of behavior. The object-oriented approach serves well for modeling a domain when you need to describe entities and their interactions. But it transforms into a nightmare when class hierarchies sprawl to dozens of inheritance levels, and half the methods exist solely to pass a call further down the chain.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Functional programming&lt;/strong&gt; looks at the task from an entirely different angle. Here there is no mutable state, no loops, no side effects. There are only functions that receive data and return results. The same summation example in Haskell:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;sum=foldl(+)0[0..9]&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;One line instead of five. No loops, no intermediate variables. The function &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;foldl&lt;/code&gt; takes (1) an addition operation, (2) an initial value, and (3) a list, returning the result. The code reads like a mathematical expression, not a sequence of commands. The functional style is particularly well suited for working with collections, for building data-processing pipelines, for parallel computation. When there is no mutable state, there is no need for locks and synchronization. Functions can be safely launched simultaneously on different processor cores. Though for the domain of &lt;em&gt;Accounting for a liquor store in the suburbs&lt;/em&gt;—it’s a rather dubious ally.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Logic programming&lt;/strong&gt; overturns one’s very notion of how to write code. Instead of explaining &lt;em&gt;how&lt;/em&gt; to solve a task, the programmer describes &lt;em&gt;what&lt;/em&gt; they want to obtain. The system finds the solution on its own. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Prolog&lt;/code&gt; is the classical representative of this paradigm:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;parent(tom,bob).parent(tom,liz).parent(bob,ann).grandparent(X,Z):-parent(X,Y),parent(Y,Z).&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;We described kinship relations and a rule for determining grandparents. Now we can pose the question: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;grandparent(tom, ann)&lt;/code&gt;?—and the system will answer “yes,” having found the path through the facts. Logic programming is indispensable in certain corners of artificial intelligence, expert systems, and task planning. I even &lt;a href=&quot;https://habr.com/ru/articles/885668/&quot;&gt;dragged it into&lt;/a&gt; the consistency validation of finite automata in one of &lt;a href=&quot;https://hexdocs.pm/finitomata&quot;&gt;my libraries&lt;/a&gt;. But an attempt to write a web server in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Prolog&lt;/code&gt; would look rather like an attempt to hammer a mole with a microscope.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Declarative programming&lt;/strong&gt; is a general term for approaches where the programmer describes the desired result rather than the sequence of steps. SQL is the textbook example:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;SELECTnameFROMusersWHEREage&amp;gt;18ORDERBYname;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;We don’t explain how to traverse the table, how to check the condition, how to sort the result. We simply declare: I want the names of users over eighteen, sorted alphabetically. The database will figure out how to do this efficiently on its own. The declarative style dominates in HTML, CSS (for now—I suspect someone will drag recursion into it before long), and configuration files. It allows one to separate the &lt;em&gt;what&lt;/em&gt; from the &lt;em&gt;how&lt;/em&gt;.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Concatenative programming&lt;/strong&gt; is built on the idea of function composition via a stack. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Forth&lt;/code&gt; is its most vivid representative:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;: square dup * ;
5 square .&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;The function &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;square&lt;/code&gt; duplicates the top element of the stack and multiplies it by itself. The number 5 is placed on the stack, the function &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;square&lt;/code&gt; is applied, the result is printed. The code reads right to left, like reverse Polish notation. Concatenative languages are compact and efficient, but they demand a particular cast of mind. They remain popular in embedded systems and wherever code size and execution speed are critical.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Reactive programming&lt;/strong&gt; focuses on data streams and the propagation of changes. When a data source changes, all dependent computations update automatically. An example in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RxJS&lt;/code&gt;:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;constclicks=fromEvent(document,&amp;#39;click&amp;#39;);constpositions=clicks.pipe(map(event=&amp;gt;event.clientX));positions.subscribe(x=&amp;gt;console.log(x));&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;We create a stream of click events, transform it into a stream of coordinates, and subscribe to changes. Each click automatically produces the coordinate in the output. The reactive style is ideal for interfaces, event handling, and working with asynchronous data sources. It liberates you from callback hell and makes the data flow explicit.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Aspect-oriented programming&lt;/strong&gt; addresses the problem of cross-cutting concerns—logging, caching, access control. Instead of smearing these aspects across the entire codebase, they can be described separately:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;@Transactional@LoggedpublicvoidupdateUser(Useruser){repository.save(user);}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;The annotations &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@Transactional&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@Logged&lt;/code&gt; are aspects. They will be automatically “applied” to the method, wrapping it in a transaction and adding logging. The core code remains clean and comprehensible. The aspect-oriented approach is popular in enterprise development, where cross-cutting concerns permeate the entire system.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Metaprogramming&lt;/strong&gt; is the programming of programs that write programs. Macros in LISP allow code to be generated at compile time:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;(defmacrowhen(condition&amp;amp;restbody)`(if,condition(progn,@body)))&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;The macro &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;when&lt;/code&gt; expands into an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;if&lt;/code&gt; construct with a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;progn&lt;/code&gt; block. Metaprogramming grants extraordinary flexibility, enabling the creation of domain-specific languages right inside the host language. But with great power comes great responsibility: poorly written macros turn code into an unreadable mess. If you want to see what metaprogramming looks like when practiced by a sane person—take any of my libraries, or write your own in &lt;em&gt;Elixir&lt;/em&gt;. I know of no other language where macros have been done properly.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Dependently-typed programming&lt;/strong&gt; elevates the type system to a new plane. Types can depend on values, allowing complex invariants to be expressed at the type level.&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;dataVec(A:Set):Nat-&amp;gt;Setwhere[]:VecAzero_::_:{n:Nat}-&amp;gt;A-&amp;gt;VecAn-&amp;gt;VecA(sucn)append:{A:Set}{mn:Nat}-&amp;gt;VecAm-&amp;gt;VecAn-&amp;gt;VecA(m+n)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;The type &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Vec A n&lt;/code&gt; is a vector of elements of type &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;A&lt;/code&gt; with length &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;n&lt;/code&gt;. The function &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;append&lt;/code&gt; takes two vectors of lengths &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;m&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;n&lt;/code&gt; and returns a vector of length &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;m + n&lt;/code&gt;. The compiler verifies correctness at the type level. It is impossible to write a function that violates the length invariant. Dependent types are used for the formal verification of critical systems, where an error costs far too much.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Theorem-proving&lt;/strong&gt; as a paradigm is the proof of program correctness by mathematical methods. &lt;em&gt;Lean&lt;/em&gt; and &lt;em&gt;Coq&lt;/em&gt; allow one to write not merely code, but proofs that the code does precisely what was intended:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;theoremadd_comm(nm:Nat):n+m=m+n:=byinductionnwith|zero=&amp;gt;simp[Nat.zero_add,Nat.add_zero]|succnih=&amp;gt;simp[Nat.succ_add,Nat.add_succ,ih]&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;This is not simply an addition function—it is a proof that addition is commutative. The compiler doesn’t merely check types; it checks the mathematical proof. This approach is employed in cryptography, compilers, and operating systems—domains where the price of an error is measured not in irritated users, but in human lives or millions of dollars in losses.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;The actor model&lt;/strong&gt; views a program as a collection of independent actors that exchange messages. Each actor has its own &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mailbox&lt;/code&gt;, processes messages sequentially, and can create new actors. &lt;em&gt;Erlang&lt;/em&gt; was built upon this idea:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;-module(counter).-export([start/0,loop/1]).start()-&amp;gt;spawn(fun()-&amp;gt;loop(0)end).loop(N)-&amp;gt;receive{increment,Pid}-&amp;gt;Pid!{value,N+1},loop(N+1);{get,Pid}-&amp;gt;Pid!{value,N},loop(N)end.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;The actor &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;counter&lt;/code&gt; receives &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;increment&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;get&lt;/code&gt; messages, modifies its state, and replies. No shared data, no locks. Actors scale horizontally, failures are isolated. This model is ideal for distributed systems, where failures are the norm rather than the exception.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Dataflow programming&lt;/strong&gt; describes computation as a graph of data streams. The nodes of the graph are operations, the edges are data flows between them. A change in one node propagates automatically through the graph. &lt;em&gt;LabVIEW&lt;/em&gt; uses visual dataflow programming for hardware control. The approach is intuitive for engineers accustomed to thinking in schematics and diagrams.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Constraint programming&lt;/strong&gt; describes a task as a set of constraints that must be satisfied. The system searches for a solution by enumerating possibilities and pruning the impossible. &lt;em&gt;MiniZinc&lt;/em&gt; is a language for constraint programming:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;var 1..9: x;
var 1..9: y;
constraint x + y = 10;
constraint x * y = 21;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;Two variables, two constraints. The system will find &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;x = 3, y = 7&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;x = 7, y = 3&lt;/code&gt;. Constraint programming is applied in planning, scheduling, and resource optimization—wherever a task is formulated as finding a solution under constraints.&lt;/p&gt;&lt;p&gt;Phew.&lt;/p&gt;&lt;p&gt;Now let us pose the question: why does any of this matter to an ordinary developer? The answer is simple and simultaneously non-obvious. &lt;strong&gt;Each paradigm is a way of thinking, an approach to solving problems.&lt;/strong&gt; A programmer who knows only imperative programming will solve every task with loops and conditionals. They will see a list-processing task and write a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;for&lt;/code&gt; loop with intermediate variables. A programmer acquainted with the functional paradigm will write &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;map&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fold&lt;/code&gt;—elegantly, concisely, free of side effects. One who has mastered reactive programming will construct an event-processing pipeline where each stage is explicitly described and easily testable.&lt;/p&gt;&lt;p&gt;Knowledge of different paradigms expands one’s arsenal of tools. You won’t write a web server in &lt;em&gt;Prolog&lt;/em&gt; or prove theorems in &lt;em&gt;JavaScript&lt;/em&gt;. But an understanding of logic programming will help you formulate conditions more precisely and build database queries. Familiarity with dependent types will teach you to think in invariants and express constraints at the type-system level. Experience with actors will show you how to build scalable distributed systems without the headaches of synchronization.&lt;/p&gt;&lt;p&gt;In truth, in the modern world all mature languages have long since become multi-paradigm. &lt;em&gt;Scala&lt;/em&gt; combines object-oriented and functional approaches. &lt;em&gt;Rust&lt;/em&gt; adds a powerful ownership and borrowing system to the imperative style. &lt;em&gt;Python&lt;/em&gt; allows one to write procedurally, in an object-oriented fashion, and functionally. &lt;em&gt;F#&lt;/em&gt; unites functional programming with the &lt;em&gt;.NET&lt;/em&gt; ecosystem. &lt;em&gt;Swift&lt;/em&gt; attempts to incorporate elements of all major paradigms at once. A programmer who understands when an aspect is needed (yes, in any language—for instance, I &lt;a href=&quot;https://hexdocs.pm/telemetria&quot;&gt;dragged aspects into&lt;/a&gt;&lt;em&gt;Elixir&lt;/em&gt;) uses the language to its full power. One who knows only a single paradigm writes in any syntax as though it were PHP.&lt;/p&gt;&lt;p&gt;Paradigms are not a religion where you must choose one true faith and wage war on the heretics. They are tools, and a good craftsman knows when to reach for the hammer, when for the saw, and when for the plane. Need to parse something? Take the functional approach with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;map&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fold&lt;/code&gt;. Build a system with thousands of simultaneous connections? Actors are your choice. Formally prove an algorithm’s correctness? Welcome to &lt;em&gt;Lean&lt;/em&gt; or &lt;em&gt;Agda&lt;/em&gt;. Developing an interface with many interactive elements? Reactive programming will make the code comprehensible.&lt;/p&gt;&lt;p&gt;A programmer trapped in a single paradigm is condemned to solve problems inefficiently. They will drag familiar patterns behind them even when those patterns don’t fit. They will write a class where a function would suffice. They will create mutable state where it could be avoided entirely. They will erect a complex hierarchy where composition would have been enough. They resemble a person who knows only one route from home to work and stubbornly waits at the bus stop every day, even though the road has been torn up for a month and the bus now runs on the next street over.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;If a developer claims the badge of mid-level-plus but doesn’t feel at ease in at least the five principal paradigms—they are a pompous fool, and you should show them the door.&lt;/strong&gt;&lt;/p&gt;</content:encoded>
</item>
<item>
<title>Rails on the BEAM</title>
<link>https://intertwingly.net/blog/2026/04/02/Rails-on-the-BEAM.html</link>
<guid isPermaLink="false">fMGmbR7zMWGvMfIgnim4ph5idQqa3KBQCM1_wA==</guid>
<pubDate>Sat, 04 Apr 2026 02:36:04 +0000</pubDate>
<description>Rails on the BEAM</description>
<content:encoded>&lt;header&gt;
&lt;h3&gt;&lt;a href=&quot;https://intertwingly.net/blog/2026/04/02/Rails-on-the-BEAM.html&quot;&gt;Rails on the BEAM&lt;/a&gt;&lt;/h3&gt;
&lt;hr/&gt;&lt;div&gt;&lt;time&gt;2026-04-02T23:21:00Z&lt;/time&gt;&lt;/div&gt;

&lt;/header&gt;
&lt;img src=&quot;https://intertwingly.net/blog/images/elixir.svg&quot; alt=&quot;&quot; title=&quot;&quot;/&gt;

&lt;p&gt;Same blog from the &lt;a href=&quot;https://intertwingly.net/blog/2026/01/28/Twilight-Zone.html&quot;&gt;Twilight Zone post&lt;/a&gt;. Same models, controllers, views. Same Turbo Streams broadcasting. Same Action Cable protocol. But check what&amp;#39;s serving it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx github:ruby2js/juntos --demo blog
cd blog
npx juntos db:prepare
npx juntos up -d sqlite_napi&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Open &lt;a href=&quot;http://localhost:3000&quot;&gt;http://localhost:3000&lt;/a&gt;. Open a second tab. Create an article. Watch it appear in both. That part you&amp;#39;ve seen before.&lt;/p&gt;
&lt;p&gt;Now imagine a bug in a request handler crashes one of the JavaScript runtimes. On Node.js, that takes down the process — connections drop, state is lost, restart from scratch. On the BEAM, the OTP supervisor restarts just that runtime. The other runtimes keep serving. WebSocket connections stay open. Turbo picks up where it left off.&lt;/p&gt;
&lt;h2&gt;What Changed&lt;/h2&gt;
&lt;p&gt;Nothing in the application. The Ruby source is identical. What changed is the target:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Target&lt;/th&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;Database&lt;/th&gt;
&lt;th&gt;Broadcasting&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Browser&lt;/td&gt;
&lt;td&gt;&lt;code&gt;juntos dev -d dexie&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;IndexedDB&lt;/td&gt;
&lt;td&gt;BroadcastChannel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Node.js&lt;/td&gt;
&lt;td&gt;&lt;code&gt;juntos up -d sqlite&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;SQLite&lt;/td&gt;
&lt;td&gt;WebSocket server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;BEAM&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;juntos up -d sqlite_napi&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;SQLite or PostgreSQL&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;OTP :pg&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The browser uses the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Broadcast_Channel_API&quot;&gt;BroadcastChannel API&lt;/a&gt; for cross-tab sync. Node.js runs a WebSocket server. The BEAM uses Erlang&amp;#39;s &lt;a href=&quot;https://www.erlang.org/doc/apps/kernel/pg.html&quot;&gt;process groups&lt;/a&gt; — distributed by default, no external dependencies.&lt;/p&gt;
&lt;h2&gt;QuickBEAM&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/elixir-volt/quickbeam&quot;&gt;QuickBEAM&lt;/a&gt; is a JavaScript runtime for the Erlang VM, built on &lt;a href=&quot;https://github.com/quickjs-ng/quickjs&quot;&gt;QuickJS-NG&lt;/a&gt; — a lightweight, standards-compliant JavaScript engine. Where Node.js embeds V8 in a C++ process, QuickBEAM embeds QuickJS in an Erlang NIF, giving JavaScript access to the BEAM&amp;#39;s concurrency and fault-tolerance primitives.&lt;/p&gt;
&lt;p&gt;Each QuickBEAM runtime is a lightweight isolate — Elixir can spin up a pool and dispatch requests round-robin across them, each running on its own OS thread. If one crashes, the OTP supervisor restarts it. The others keep serving. This is the same model Erlang uses for telecom switches — let it crash, recover instantly.&lt;/p&gt;
&lt;p&gt;QuickJS isn&amp;#39;t V8 — there&amp;#39;s no JIT, so raw compute is slower. But for a web application that&amp;#39;s mostly I/O (database queries, template rendering, HTTP responses), the difference is negligible. What you gain is a 5MB runtime instead of 45MB, sub-millisecond startup, and the entire OTP ecosystem.&lt;/p&gt;
&lt;h2&gt;The Architecture&lt;/h2&gt;
&lt;p&gt;The application runs inside QuickBEAM — a JavaScript runtime embedded in the Erlang VM. Elixir manages everything around it:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Browser (Turbo, Stimulus, Action Cable client)
    ↕ HTTP + WebSocket
Bandit (Elixir HTTP server)
    ↕ Plug router
QuickBEAM (JavaScript runtime pool)
    ↕ Beam.callSync
Elixir (:pg broadcasts, SQLite NIF, OTP supervision)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The JavaScript application handles request routing, controller logic, view rendering, and model operations — the same code that runs on Node.js. Elixir handles what it&amp;#39;s best at: concurrency, fault tolerance, and distributed messaging.&lt;/p&gt;
&lt;h2&gt;Action Cable, Both Sides&lt;/h2&gt;
&lt;p&gt;The browser runs the real &lt;code&gt;@hotwired/turbo-rails&lt;/code&gt; npm package — the same Action Cable client that Rails uses. The &lt;code&gt;&amp;lt;turbo-cable-stream-source&amp;gt;&lt;/code&gt; custom element connects to &lt;code&gt;/cable&lt;/code&gt; and speaks the Action Cable wire protocol.&lt;/p&gt;
&lt;p&gt;On the server, Elixir implements the other side: WebSocket upgrade via Bandit, subscription management via &lt;code&gt;:pg&lt;/code&gt;, and broadcast delivery in Action Cable&amp;#39;s JSON format. When a model&amp;#39;s &lt;code&gt;broadcasts_to&lt;/code&gt; callback fires, it crosses from JavaScript to Elixir via &lt;code&gt;Beam.callSync(&amp;#39;__broadcast&amp;#39;, channel, html)&lt;/code&gt;, and Elixir fans it out to every subscriber.&lt;/p&gt;
&lt;p&gt;Same protocol. Same custom elements. Same Turbo Stream HTML. The client has no idea it&amp;#39;s talking to Elixir instead of Rails.&lt;/p&gt;
&lt;h2&gt;What the BEAM Adds&lt;/h2&gt;
&lt;p&gt;Things you get for free that would require significant infrastructure on Node.js:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Fault tolerance&lt;/strong&gt; — a runtime crash restarts under OTP supervision, not &lt;code&gt;pm2&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Distributed pub/sub&lt;/strong&gt; — &lt;code&gt;:pg&lt;/code&gt; spans clustered BEAM nodes automatically. Add a node, broadcasts reach it. No Redis, no configuration.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;True parallelism&lt;/strong&gt; — pooled QuickBEAM runtimes on OS threads. Not single-threaded-with-cluster.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hot upgrades&lt;/strong&gt; — OTP releases support zero-downtime deployment&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For production, swap SQLite for PostgreSQL — same app, &lt;code&gt;juntos up -d postgrex&lt;/code&gt;. Database connections are pooled on the Elixir side via Postgrex, and combined with &lt;code&gt;:pg&lt;/code&gt; for broadcasting, you get a fully distributed deployment with no external dependencies beyond Postgres.&lt;/p&gt;
&lt;h2&gt;A Path to Phoenix&lt;/h2&gt;
&lt;p&gt;This isn&amp;#39;t just another deployment target. It&amp;#39;s a migration path.&lt;/p&gt;
&lt;p&gt;Your Rails app runs today inside QuickBEAM. The Elixir scaffold is a thin Plug/Bandit layer. But that layer could be Phoenix. At that point:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Rails controllers that need more performance? Rewrite as Phoenix controllers.&lt;/li&gt;
&lt;li&gt;Models that need distributed state? Move to GenServers.&lt;/li&gt;
&lt;li&gt;Views that need real-time interaction? Swap to LiveView.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;One at a time. The rest keeps running in QuickBEAM. No big-bang rewrite.&lt;/p&gt;
&lt;p&gt;No other migration path offers this. Going from Rails to Phoenix today means starting over. Juntos on BEAM gives you a running app on day one and an incremental path forward.&lt;/p&gt;
&lt;h2&gt;Try It&lt;/h2&gt;
&lt;p&gt;Prerequisites: &lt;a href=&quot;https://nodejs.org/&quot;&gt;Node.js&lt;/a&gt; (18+) and &lt;a href=&quot;https://elixir-lang.org/install.html&quot;&gt;Elixir&lt;/a&gt; (1.18+). That&amp;#39;s all you need.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx github:ruby2js/juntos --demo blog
cd blog
npx juntos db:prepare
npx juntos up -d sqlite_napi&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Same code. Same patterns. Different runtime. &lt;a href=&quot;https://github.com/ruby2js/ruby2js&quot;&gt;Source code&lt;/a&gt;. &lt;a href=&quot;https://www.ruby2js.com/docs/juntos/&quot;&gt;Documentation&lt;/a&gt;.&lt;/p&gt;
&lt;hr/&gt;
&lt;p&gt;&lt;em&gt;&lt;a href=&quot;https://www.ruby2js.com/docs/juntos/&quot;&gt;Juntos&lt;/a&gt; is open source: &lt;a href=&quot;https://github.com/ruby2js/ruby2js&quot;&gt;github.com/ruby2js/ruby2js&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;</content:encoded>
</item>
<item>
<title>Our Journey: Building With Generative AI | Revelry</title>
<link>https://revelry.co/insights/artificial-intelligence/building-with-generative-ai/</link>
<enclosure type="image/jpeg" length="0" url="https://revelry.co/wp-content/uploads/2024/01/BLOG-ART-Journey-to-AI-2.2024.jpg"></enclosure>
<guid isPermaLink="false">1PtN8sfBZKQOlgHLVzp7_ByrkZwtb9cDuR6VQQ==</guid>
<pubDate>Mon, 30 Mar 2026 12:23:34 +0000</pubDate>
<description>Blog series by software engineer Daniel Andrews who shares about our product development team&#39;s early experience building with generative AI, including RAG</description>
<content:encoded>&lt;p&gt;Businesses of all sizes and industries are eager to take advantage of generative artificial intelligence (AI), so I’m going to share some details on Revelry’s journey with this emerging technology over the past year. Even if you’re already working in the space, I believe you’ll find something interesting and helpful about our experience. We’ve got a lot of learnings to share, so this will be the first of a series of posts.&lt;/p&gt;&lt;p&gt;In this first post, I’ll cover our early exploration of generative AI – when we were moving as fast as possible to learn as much as we could. Subsequent posts will get deeper into our learnings around building more complex generative AI systems, in particular diving into how to incorporate Retrieval Augmented Generation (RAG) into software systems (and how you don’t need LangChain to do it).&lt;/p&gt;&lt;h2&gt;Some Background&lt;/h2&gt;&lt;p&gt;Here’s a little backstory on Revelry. Since 2013, we’ve been building custom software. Our bread and butter has primarily been web and mobile app development. In the early days, it was all about Rails and Node, but we’ve played around with PHP, .NET, Java, Python, and more.&lt;/p&gt;&lt;p&gt;We’ve always had a bit of a thing for new and emerging tech. Take React, for instance. Back in 2014, when JQuery and Angular were the big names, we were already building apps with React. And we didn’t stop there – we jumped into React Native pretty early, too. Our first app in React Native hit the AppStore when it was just at version 0.21, and now it’s up to 0.73. (By the way, when are we getting a major version update? Looking at you too, LiveView 😉).&lt;/p&gt;&lt;p&gt;We still work across a variety of tech stacks, but have collectively fallen in love with the elegance, performance, and strong community around &lt;a href=&quot;https://elixir-lang.org/&quot;&gt;Elixir&lt;/a&gt; and Phoenix, which we adopted as our preferred stack around 2018. We were building sophisticated &lt;a href=&quot;https://www.phoenixframework.org/&quot;&gt;Phoenix LiveView&lt;/a&gt; apps before there was even an official LiveView hex package published. (Yes, we were just referencing a commit hash in our mix.exs file — don’t judge.) We have done a lot in the blockchain space too, but I’m definitely not going into that in this article.&lt;/p&gt;&lt;p&gt;This is all to give you a glimpse into how we at Revelry dive into new technologies. We’re not shy about exploring the bleeding edge, and it’s really paid off. Our early dives into spaces like React and Elixir have made us the experts we are today.&lt;/p&gt;&lt;h2&gt;Where We Started&lt;/h2&gt;&lt;p&gt;Thinking back to June 2022, when OpenAI released GPT-3, it’s been nothing short of a rollercoaster ride. We at Revelry, like many software development companies, quickly caught on that this was a game-changer for our industry. Sure, we had a bunch of engineers who were into machine learning, but AI-driven apps weren’t really our main gig. Our partners didn’t ask for them much, and they didn’t seem all that necessary… until GPT-3 came along.&lt;/p&gt;&lt;p&gt;By Fall 2022, we were all in, diving deep into the world of these large language models (LLMs). The pace at which things have evolved since then is mind-blowing. Back then, things weren’t quite ready for the big stage, but it was obvious this was just the start.&lt;/p&gt;&lt;p&gt;We saw a golden opportunity to weave generative AI into our tried-and-true software and product delivery processes. This wasn’t about replacing our team, but turbocharging their productivity. Imagine our folks focusing on the creative, problem-solving aspects of product and software design, while AI handles the tedious stuff – like writing user stories, plotting out product roadmaps, or drafting sprint reports. And what if getting up to speed on a new project could be quicker and smoother? If this could work for us, it’d surely catch on elsewhere, right?&lt;/p&gt;&lt;p&gt;So, we rolled up our sleeves and jumped into the nitty-gritty. It started as a research and development adventure, filled with questions, like:&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;Just how far can the capabilities of these LLMs go?&lt;/li&gt;



&lt;li&gt;What’s the engineering effort needed to integrate generative AI into our custom software?&lt;/li&gt;



&lt;li&gt;What does it take to set up an AI-powered app in a live environment?&lt;/li&gt;



&lt;li&gt;Can LLMs genuinely enhance our team’s productivity? If so, in what ways?&lt;/li&gt;



&lt;li&gt;Is it possible to create something other engineering teams would want to use as well?&lt;/li&gt;
&lt;/ul&gt;&lt;h2&gt;Experimentation&lt;/h2&gt;&lt;p&gt;So, everyone at Revelry began dabbling with ChatGPT, mostly just for kicks. Some of us were crafting Eminem-style raps to add a bit of flair to our company-wide All Hands meetings (We’ve got a different Reveler hosting each week.). Meanwhile, our CEO, &lt;a href=&quot;https://www.linkedin.com/in/gerardramos/&quot;&gt;Gerard Ramos&lt;/a&gt; – or G, as we call him – was tinkering with how ChatGPT could enhance our product delivery process.&lt;/p&gt;&lt;p&gt;G found out pretty fast – with some clever prompting – that ChatGPT could whip up some solid product roadmaps and user stories, and even spin out working code examples based on those stories. This was more than just cool – it was promising. So, he proposed we start building tools around these use cases. And that’s how the idea for our first proof of concept came about: an AI-powered app to create user stories from just a few inputs. Sure, it wasn’t a game-changer yet, but it was a great starting point – allowing us to dip our toes in the water, while simultaneously boosting our productivity.&lt;/p&gt;&lt;h3&gt;Our First AI- Powered Toy: StoryBot&lt;/h3&gt;&lt;p&gt;Enter &lt;a href=&quot;https://github.com/revelrylabs/storybot-ai&quot;&gt;StoryBot&lt;/a&gt;. This little gem was a straightforward CLI tool that we ended up releasing as an open-source NPM package. It’s essentially a single JavaScript file, leveraging LangChain to tap into GPT-3 via OpenAI’s API (This was before GPT-4.). We threw in some tailored prompts, injected the user input, and voilà – it started spitting out decent user stories right in the command line.&lt;/p&gt;&lt;p&gt;We went a bit further with it after that, letting the user refine their story through chat, still all in the command line. The cherry on top was the ability to export the story as an issue in a GitHub Repo (At Revelry, we not only use GitHub to store our code, but also for issue tracking, project management, and more.). Ultimately, we ended up with &lt;a href=&quot;https://asciinema.org/a/583539&quot;&gt;this StoryBot iteration&lt;/a&gt;:&lt;/p&gt;&lt;h3&gt;StoryBot Under the Hood&lt;/h3&gt;&lt;p&gt;Diving into the StoryBot &lt;a href=&quot;https://github.com/revelrylabs/storybot-ai&quot;&gt;repo&lt;/a&gt;, you’ll see the core functionality is in &lt;a href=&quot;https://github.com/revelrylabs/storybot-ai/blob/main/bin/story.js&quot;&gt;one JavaScript file&lt;/a&gt;. This file uses &lt;a href=&quot;https://js.langchain.com/docs/get_started/introduction&quot;&gt;LangChain.js&lt;/a&gt; for communicating with the OpenAI API, generating user stories from command line inputs. We could have opted for LangChain’s &lt;a href=&quot;https://python.langchain.com/docs/get_started/introduction&quot;&gt;Python library&lt;/a&gt;, but the two have close to feature parity, and our team works more in JavaScript than Python. At this point, it was all still experimentation, so we opted for whatever we could move the fastest in.&lt;/p&gt;&lt;p&gt;Technically, for such a straightforward use case, direct API calls to OpenAI would have done the trick. However, LangChain offered ease of setup and capabilities beyond just interfacing with an LLM. It’s packed with features for creating AI-powered apps, like &lt;a href=&quot;https://python.langchain.com/docs/modules/data_connection/&quot;&gt;Retrieval&lt;/a&gt;, &lt;a href=&quot;https://python.langchain.com/docs/modules/agents/&quot;&gt;Agents&lt;/a&gt;, and &lt;a href=&quot;https://python.langchain.com/docs/modules/chains&quot;&gt;Chains&lt;/a&gt;, though we didn’t dive deep into any of these for StoryBot.&lt;/p&gt;&lt;p&gt;LangChain simplifies building AI applications, but it’s not without its complexities and limitations. It abstracts a lot, sometimes obscuring the underlying mechanics, and is currently limited to Python and JavaScript ecosystems. There is now an &lt;a href=&quot;https://github.com/brainlid/langchain&quot;&gt;Elixir implementation&lt;/a&gt; of LangChain, which is exciting because we’re huge Elixir fans, but it isn’t nearly as far along as its Python and JS counterparts. This Elixir library also wasn’t around yet at this point in our journey.&lt;/p&gt;&lt;h4&gt;Looking a Bit More Into StoryBot Code&lt;/h4&gt;&lt;p&gt;The first code that actually gets executed when you run &lt;code&gt;npx gen.story&lt;/code&gt; generates the initial prompt:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const generateInitialPrompt = () =&amp;gt; {
  const { featureText, techStackText, contextText } = parseArgs();

  return `Context: Act as a product manager at a software development company. Write a user story for the &amp;#39;Feature&amp;#39; defined below. Explain in detailed steps how to implement this in a section called &amp;#39;Implementation Notes&amp;#39; at the end of the story. Please make sure that the implementation notes are complete; do not leave any incomplete sentences. ${contextText}

  ${featureText}

  ${techStackText}

  User Story Spec:
    overview:
      &amp;quot;The goal is to convert your response into a GitHub Issue that a software engineer can use to implement the feature. Start your response with a &amp;#39;Background&amp;#39; section, with a few sentences about why this feature is valuable to the application and why we want the user story written. Follow with one or more &amp;#39;Scenarios&amp;#39; containing the relevant Acceptance Criteria (AC). Use markdown format, with subheaders (e.g. &amp;#39;##&amp;#39; ) for each section (i.e. &amp;#39;## Background&amp;#39;, &amp;#39;## Scenario - [Scenario 1]&amp;#39;, &amp;#39;## Implementation Notes&amp;#39;).&amp;quot;,
    scenarios:
    &amp;quot;detailed stories covering the core loop of the feature requested&amp;quot;,
    style:
      &amp;quot;Use BDD / gherkin style to describe the user scenarios, prefacing each line of acceptance criteria (AC) with a markdown checkbox (e.g. &amp;#39;- [ ]&amp;#39;).&amp;quot;,
  }`;
};

...

const prompt = generateInitialPrompt()&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;You can see that the prompt has specific instructions to format the story in the way that Revelry prefers, which may be different than a lot of other teams. This said, if anyone wanted to use this with different prompts, they could easily fork it and change the prompts. The most important part here is that we are injecting user input into the prompt before we send it to the LLM. In this case, there are 3 potential user inputs:&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;The feature in question, which comes in as the 3rd command line argument (e.g. &lt;code&gt;npx gen.story [feature]&lt;/code&gt;).&lt;/li&gt;



&lt;li&gt;optional &lt;code&gt;--stack&lt;/code&gt; flag to specify the tech stack that the user story will need to be implemented in.&lt;/li&gt;



&lt;li&gt;optional &lt;code&gt;--context&lt;/code&gt; flag to add some additional context around the feature you are writing a user story for.&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;Next, we take that hydrated prompt and send it to OpenAI using the tools provided by LangChain:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const model = new OpenAI({
  streaming: true,
  modelName: &amp;quot;gpt-3.5-turbo&amp;quot;,
  callbacks: [
    {
      handleLLMNewToken(token) {
        process.stdout.write(token)
      },
    },
  ],
});
const memory = new BufferMemory()
const chain = new ConversationChain({llm: model, memory: memory})
const {response} = await chain.call({input: prompt})&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;A few things happen at this point: we are creating a new &lt;code&gt;ConversationChain&lt;/code&gt; object, which is a wrapper around the LangChain &lt;code&gt;Chain&lt;/code&gt; object that we’ll use to send the prompt to the LLM. We are also creating a &lt;code&gt;BufferMemory&lt;/code&gt; object, which is a LangChain &lt;code&gt;Memory&lt;/code&gt; object that we’ll use to store the results of the conversation.&lt;/p&gt;&lt;p&gt;Sidenote: If we were just using the OpenAI API directly instead of LangChain, it would be easy to pass the chat history alongside the prompt to the API call. (I’m just clarifying that LangChain isn’t &lt;em&gt;necessary&lt;/em&gt; for this, even though it was very easy to set up.)&lt;/p&gt;&lt;p&gt;Because we set &lt;code&gt;streaming: true&lt;/code&gt; when we initialized the &lt;code&gt;OpenAI&lt;/code&gt; object, the &lt;code&gt;chain.call&lt;/code&gt; method will return immediately, and the LLM will start sending responses to the callback we set up earlier. Since StoryBot is a CLI tool, we’re just outputting to &lt;code&gt;process.stdout&lt;/code&gt; here. If you’re thinking about adapting this for a web app, you’d probably need to figure out how to send JSON responses or stream them to the client. We’ll get more into that later. The main takeaway? It doesn’t take much to start seeing some cool results by plugging user inputs into a well-crafted prompt template and sending it off to GPT-3.&lt;/p&gt;&lt;p&gt;So at this point, there is a &lt;code&gt;response&lt;/code&gt; that is the generated user story, but also the entire user story has been streamed into the terminal and could easily be copy pasted to wherever. However, there is no ability to make any followup refinements yet. I’m not going to go line-for-line through the rest of the final result, but the long story short is that after we get the initial generated response back, we pass it to a &lt;a href=&quot;https://github.com/revelrylabs/storybot-ai/blob/main/bin/story.js#L104&quot;&gt;function&lt;/a&gt; that creates a &lt;a href=&quot;https://nodejs.org/api/readline.html#readline&quot;&gt;readline&lt;/a&gt; interface that allows us to prompt the user with questions in the terminal, and then we take the users’ response and &lt;a href=&quot;https://github.com/revelrylabs/storybot-ai/blob/main/bin/story.js#L122&quot;&gt;send it back&lt;/a&gt; as another message to the LLM in the chat history. We also added the ability to &lt;a href=&quot;https://github.com/revelrylabs/storybot-ai/blob/main/bin/story.js#L126C7-L126C23&quot;&gt;export the final result to github&lt;/a&gt; if you had the github API token set.&lt;/p&gt;&lt;p&gt;That’s it, that’s &lt;a href=&quot;https://github.com/revelrylabs/storybot-ai?tab=readme-ov-file#install-storybot&quot;&gt;StoryBot&lt;/a&gt;!&lt;/p&gt;&lt;p&gt;If you want to play around with it, you can install it via npm.&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install -g storybot-ai&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Fun? Absolutely. Somewhat useful? Sure. But let’s be real – it was an experiment. The thing is, not everyone wants to write user stories via the command line. Plus, every team has its own style for these stories. Our hardcoded prompts were great for us, but might not hit the mark for teams outside of Revelry, especially since we often work in staff augmentation, where teams have their own preferences.&lt;/p&gt;&lt;p&gt;Once we had a proof of concept, we started to see the potential. We were able to get a lot of mileage out of it, and it was a great way to get started with generative AI. This got a lot of ideas spinning about how we could get &lt;em&gt;better&lt;/em&gt; user stories based on relevant context, which ultimately led us to the next part of our journey: diving deeper into RAG (Retrieval Augmented Generation) application development.&lt;/p&gt;&lt;hr/&gt;&lt;p&gt;&lt;em&gt;This is the first post in a series about Revelry’s journey exploring and developing custom software powered by generative AI. The next post will dive into our next experiment: building a chatbot to answer questions about Revelry based on our company playbook. Stay tuned!&lt;/em&gt;&lt;br/&gt;&lt;br/&gt;&lt;em&gt;Until then, here are a few other articles we’ve put out about AI:&lt;br/&gt;&lt;/em&gt;&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://revelry.co/insights/artificial-intelligence/llms-large-context-windows/&quot;&gt;Memory Consumption and Limitations in LLMs with Large Context Windows&lt;/a&gt;&lt;/li&gt;



&lt;li&gt;&lt;a href=&quot;https://revelry.co/insights/artificial-intelligence/memory-consumption-and-limitations-in-llms-part-2/&quot;&gt;Memory Consumption and Limitations in LLMs with Large Context Windows, Pt II&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://revelry.co/insights/artificial-intelligence/comparing-openais-assistants-api-custom-gpts-and-chat-completion-api/&quot;&gt;Comparing OpenAI’s Assistants API, Custom GPTs, and Chat Completion API&lt;/a&gt;&lt;/li&gt;



&lt;li&gt;&lt;a href=&quot;https://revelry.co/insights/artificial-intelligence/creating-an-agent-using-openais-functions-api/?ssp=1&amp;amp;darkschemeovr=1&amp;amp;setlang=en-IN&amp;amp;safesearch=moderate&quot;&gt;Creating an “Agent” Using OpenAI’s Functions API&lt;br/&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;section&gt;
  &lt;div&gt;
    &lt;div&gt;
      
      
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/section&gt;&lt;hr/&gt;</content:encoded>
</item>
<item>
<title>Behind the Scenes: Replacing a Black-Box Billing Service</title>
<link>https://bitcrowd.dev/replacing-a-black-box-billing-service</link>
<guid isPermaLink="false">yIVQQMsfuCO1pJ7UZp4-WP3NAJF1LhLcufTM-g==</guid>
<pubDate>Fri, 27 Mar 2026 12:04:11 +0000</pubDate>
<description>In a previous blog post, we looked at the process of migrating 600k users to a new billing service for Steady Media. This was the final step of an internal project called “Charger”. Its aim was to replace the off-the-shelf payment platform they had started with, Chargebee, which had become a roadblock. In this post, we will reveal the behind-the-scenes insights into the process that made that final step a success.</description>
<content:encoded>&lt;p&gt;In a &lt;a href=&quot;https://bitcrowd.dev/migrating-600k-users-to-new-billing-service&quot;&gt;previous blog post&lt;/a&gt;, we looked at the process of migrating 600k users to a new billing service for Steady Media. This was the final step of an internal project called &lt;strong&gt;“Charger”&lt;/strong&gt;. Its aim was to replace the off-the-shelf payment platform they had started with, &lt;a href=&quot;https://www.chargebee.com/&quot;&gt;Chargebee&lt;/a&gt;, which had become a roadblock. In this post, we will reveal the behind-the-scenes insights into the process that made that final step a success.&lt;/p&gt;&lt;h2&gt;The Challenge&lt;a href=&quot;https://bitcrowd.dev/replacing-a-black-box-billing-service#the-challenge&quot;&gt;​&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Steady Media needed to migrate 600,000 users from Chargebee to a new in-house billing system. The complexity was brutal:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Chargebeeʼs billing logic is opaque - we had to reverse-engineer behavior through observation&lt;/li&gt;&lt;li&gt;Historical data had to work in the new system (canʼt rewrite the past, especially not payment transactions!)&lt;/li&gt;&lt;li&gt;Multiple payment gateways (Braintree, GoCardless, PayPal) with different quirks&lt;/li&gt;&lt;li&gt;Complex billing workflows: trial subscriptions, gift subscriptions, mid-term subscription upgrades, subscription cancellations (with or without refunds)&lt;/li&gt;&lt;li&gt;Complex support workflows: issuing refunds, invoice &amp;amp; credit notes document versioning&lt;/li&gt;&lt;li&gt;Legal compliance (invoicing and taxes)&lt;/li&gt;&lt;li&gt;Migration had to happen without downtime or data loss&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Steadyʼs development team asked us to lead the Charger project, so that they could focus on their core application. The tech stack had to be compatible with their team skills: Elixir &amp;amp; Phoenix!&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;p&gt;
🤩 Like what you see? have a look at &lt;a href=&quot;https://bitcrowd.net/en/projects&quot;&gt;the case studies on bitcrowd.net&lt;/a&gt;&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;h2&gt;The Journey&lt;a href=&quot;https://bitcrowd.dev/replacing-a-black-box-billing-service#the-journey&quot;&gt;​&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;bitcrowd built Charger across three iterations:&lt;/p&gt;&lt;h3&gt;2021: Core System&lt;a href=&quot;https://bitcrowd.dev/replacing-a-black-box-billing-service#2021-core-system&quot;&gt;​&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;The first iteration aimed at building the data model and implementing the external communication layer of Charger:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;API layer with authentication between Charger and the Media Makers app&lt;/li&gt;&lt;li&gt;Webhook notifications between Charger and the Media Makers app&lt;/li&gt;&lt;li&gt;Payment providers: integrated Braintree and GoCardless services (API &amp;amp; webhooks)&lt;/li&gt;&lt;li&gt;Multiplexed calls from the Media Makers app to hit Chargebee OR Charger depending on where the user lived&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Once these various components were finalized, we could start the implementation of the core flows: create new subscriptions, bill users and trigger payments, upgrade subscription plans with prorated billing, settle payments, and so on.&lt;/p&gt;&lt;h3&gt;2023: Feature Parity&lt;a href=&quot;https://bitcrowd.dev/replacing-a-black-box-billing-service#2023-feature-parity&quot;&gt;​&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;After the first development cycle ended, new features were added to the Media Makers app that needed to be back-ported to Charger. We added new models and new flows to support trial and gift subscriptions. We took the opportunity to add Paypal as a third payment provider. Finally, we also tackled the generation and design of legal billing PDF documents.&lt;/p&gt;&lt;h3&gt;2025: Admin UI, Support Tools, and Migration&lt;a href=&quot;https://bitcrowd.dev/replacing-a-black-box-billing-service#2025-admin-ui-support-tools-and-migration&quot;&gt;​&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;A missing piece in Charger was a simple, lightweight, workable admin UI, that could be useful for Steadyʼs developers, QA engineers, support staff and financial experts. The goal was to minimize the implementation effort, while preserving a clean UX. We opted for &lt;a href=&quot;https://daisyui.com/&quot;&gt;DaisyUI&lt;/a&gt; since we knew the admin UI would use standard components like index tables, description lists, navigation elements etc.&lt;/p&gt;&lt;h4&gt;Powerful search&lt;a href=&quot;https://bitcrowd.dev/replacing-a-black-box-billing-service#powerful-search&quot;&gt;​&lt;/a&gt;&lt;/h4&gt;&lt;p&gt;One strong requirement was to make sure that the index pages had a powerful and performant search as well as sorting and filtering mechanisms, as one of the pain points of Chargebee had been searches that time out once enough data was in the system. We solved this issue by paying attention to the queries&amp;#39; efficiency. Additionally, for cross-table filtering, we implemented a search-typeahead dropdown in order to avoid expensive joins.&lt;/p&gt;&lt;h4&gt;Activity logs&lt;a href=&quot;https://bitcrowd.dev/replacing-a-black-box-billing-service#activity-logs&quot;&gt;​&lt;/a&gt;&lt;/h4&gt;&lt;p&gt;For such a billing product, it was crucial to allow admin users to see a clear changeset history on any resource. Charger comes with an auditing layer, that tracks changes and versions each relevant resource (like payments, invoices...). We built an abstraction on top of the auditing, that allows to plug any resource in it and view its version history with a UI resembling a Git diff.&lt;/p&gt;&lt;h4&gt;Support staff tooling&lt;a href=&quot;https://bitcrowd.dev/replacing-a-black-box-billing-service#support-staff-tooling&quot;&gt;​&lt;/a&gt;&lt;/h4&gt;&lt;p&gt;At that stage, we had a working product for developers, QA engineers and financial experts. But Steadyʼs support team still needed tailored features to enable them to solve specific situations. For example, when a userʼs credit card expired, and the system could not withdraw money from their account, the user might send the missing amount via a bank transfer. On Steadyʼs bank account, the balance is correct, but Charger does not know about this transaction, and the accounting balance is affected. Similarly, the support team might refund a user via a manual bank transfer. To keep the accounting in check, we implemented a solution to record offline refund &amp;amp; payment, as well as various manual actions &amp;amp; flows to fix any situation that would not auto-heal.&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;p&gt;
💡 Have we got you interested? If you have a question or topic you would like to discuss with us, we would like to hear from you.&lt;a href=&quot;https://cal.eu/bitcrowd/30min&quot;&gt;Book a free call with us&lt;/a&gt;&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;h2&gt;Outcome&lt;a href=&quot;https://bitcrowd.dev/replacing-a-black-box-billing-service#outcome&quot;&gt;​&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;We migrated all 600,000 users to Charger. The lights stayed green. Steady now controls their billing infrastructure and its support team can handle edge cases and requests through the admin UI. The developer team can also leverage the activity logs and powerful search capabilities to debug or resolve errors.&lt;/p&gt;&lt;p&gt;bitcrowd led on self-contained packages: we took topics Steadyʼs team didnʼt have capacity for and delivered them finished. We were able to coordinate with stakeholders (support team, finance, legal) to turn “we need X” into actionable tickets. We applied our standards of excellent documentation and test coverage, high code quality and review, which ensured a smooth handover with Steadyʼs team.&lt;/p&gt;</content:encoded>
</item>
<item>
<title>Migrating 600k users to a new billing service</title>
<link>https://bitcrowd.dev/migrating-600k-users-to-new-billing-service</link>
<guid isPermaLink="false">OOJsEbowS1utV-YB78Bz_ve0rAJ3loq3jak35A==</guid>
<pubDate>Fri, 27 Mar 2026 12:04:11 +0000</pubDate>
<description>In August 2025, we helped our friends at Steady to migrate their 600,000+ users to their own in-house billing system. This was the final step of an internal project called “Charger”. Its aim was to replace the off-the-shelf payment platform they had started with, Chargebee, which had become a roadblock. The work on Charger began already in 2021 in workshops with the Steady team. Together with them, we designed, planned and finally built the internal product that would take over for years ...</description>
<content:encoded>&lt;p&gt;In August 2025, we helped our friends at &lt;strong&gt;&lt;a href=&quot;https://steady.page/en/&quot;&gt;Steady&lt;/a&gt;&lt;/strong&gt; to migrate their 600,000+ users to their own &lt;strong&gt;in-house billing system&lt;/strong&gt;. This was the final step of an internal project called &lt;strong&gt;“Charger”&lt;/strong&gt;. Its aim was to replace the off-the-shelf payment platform they had started with, Chargebee, which had become a roadblock. The work on Charger began already in 2021 in workshops with the Steady team. Together with them, we designed, planned and finally built the internal product that would take over for years later. The process and the implementation happened in multiple iterations over a span of four years.&lt;/p&gt;&lt;p&gt;This blog post explores the strategy that was used to achieve this challenging task.&lt;/p&gt;&lt;h2&gt;The backstory&lt;a href=&quot;https://bitcrowd.dev/migrating-600k-users-to-new-billing-service#the-backstory&quot;&gt;​&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Steady supports Media Makers in building an audience and driving revenue via subscriptions to newsletters, blogs etc. While most of the tools and features for Media Makers live in their main application, they outsourced the subscriptions &amp;amp; billing management to a third-party service (&lt;a href=&quot;https://www.chargebee.com/&quot;&gt;Chargebee&lt;/a&gt;). The Media Makers app was very tightly coupled to Chargebee as it relied on it for core business logic within the app, as well as for accounting.&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://bitcrowd.dev/assets/images/steady-and-chargebee-718dd3eb9e01cb110b9e71ccd42807ec.png&quot; alt=&quot;Graph illustrating the relationship between Steady Media Makers app and Chargebee via an API Layer&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;h2&gt;Introducing: Charger, the fraternal twin&lt;a href=&quot;https://bitcrowd.dev/migrating-600k-users-to-new-billing-service#introducing-charger-the-fraternal-twin&quot;&gt;​&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Although Chargebee was useful initially, it became increasingly painful to work around their standard processes over time. This ultimately led to the development of a custom billing tool.&lt;/p&gt;&lt;p&gt;For this we had to reverse-engineer all of what Chargebee was doing. We started by building the required schemas, and exposing the API endpoints and webhook notifications that were needed by the Media Makers app. Any changes in the Media Makers app usage of the API/Webhooks, or to Chargebee itself, needed to be propagated to Charger. &lt;strong&gt;Both billing systems had to behave identical from the outside.&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;The Media Makers app now needed to forward calls to Charger and/or Chargebee. A “multiplexer“ abstraction close to the API &amp;amp; Webhook layer was added, so that for developers &amp;amp; users of the app, the fact that Charger or Chargebee was used for the calls would be transparent.&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://bitcrowd.dev/assets/images/introducing-charger-d2bc480de0952bd2d36fbd93bcc61c92.png&quot; alt=&quot;Graph illustrating the new billing system Charger, exposing the same API as Chargebee&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;Charger needed to process payments and refunds, with all payment providers supported by the Media Makers app. The first flow of the application was created: subscribe to a publication, generate an invoice, create a payment, and settle that payment 🎉!&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;p&gt;
🔍 Would you like more detail? We have a &amp;#39;behind the scenes&amp;#39; blog post about Steady! &lt;a href=&quot;https://bitcrowd.dev/replacing-a-black-box-billing-service/&quot;&gt;Have a Look!&lt;/a&gt;&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;h2&gt;A bird watching experience 🔭&lt;a href=&quot;https://bitcrowd.dev/migrating-600k-users-to-new-billing-service#a-bird-watching-experience-&quot;&gt;​&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;At this stage, no real users were migrated to Charger. No real users were even created on Charger! The system just worked™... Subscription statuses, invoices, payments processing being crucial to the system meant that we had to come up with a transition plan, that allowed the developers to observe and watch the behaviour of the system for just a handful of users. If any problem arose, it would need to affect only such a small amount of users that their support team could resolve it manually. We handpicked 10 users with rather simple data: a single recent subscription, no cancellations, and various payment providers / currencies to cover as much ground as possible.&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://bitcrowd.dev/assets/images/migrating-to-charger-c698090ddb6d60b2e507132ca9323214.png&quot; alt=&quot;Graph illustrating the migration job between the Media Makers app and Charger&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;Using our beloved &lt;a href=&quot;https://hexdocs.pm/oban/Oban.html&quot;&gt;Oban&lt;/a&gt; for job processing, we built a migration job, that would, for a given user:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Fetch all their data from Chargebee&lt;/li&gt;&lt;li&gt;Format it to please Chargerʼs data model&lt;/li&gt;&lt;li&gt;Send it to a dedicated endpoint in Charger&lt;/li&gt;&lt;li&gt;Charger creates all the given data for the user (subscriptions, invoices, payments, etc.)&lt;/li&gt;&lt;li&gt;Cancel the user subscriptions on Chargebee (so that they donʼt get billed twice!)&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;Of course, &lt;strong&gt;all of this must happen in a transaction on both systems&lt;/strong&gt;, fail gracefully and report meaningful errors to the devs. We leveraged Oban configuration to optimise the parallelisation and retry mechanism of the jobs, as we could be rate-limited by Chargebee. At some point, we would want to migrate users in batches of 100-10k users at a time, so the migration mechanism had to be robust and auto-healing.&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;p&gt;
💡 Have we got you interested? If you have a question or topic you would like to discuss with us, we would like to hear from you.&lt;a href=&quot;https://cal.eu/bitcrowd/30min&quot;&gt;Book a free call with us&lt;/a&gt;&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;h2&gt;Silent bugs&lt;a href=&quot;https://bitcrowd.dev/migrating-600k-users-to-new-billing-service#silent-bugs&quot;&gt;​&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;A main challenge in this process was to validate the integrity of the migrated data. The Media Makers app and Charger run on different databases, and have their own representation of what is an invoice, a subscription, a user, etc. Forgetting to migrate a field, or mapping the wrong timestamp from one system to the other, would happen silently because a part of the migration code lives in the Media Makers app, and the other part lives in Charger. The data could be silently missing or corrupted, and we would only realise down the road that all users were migrated with errors. Luckily, we had the chance of having access to a Chargebee staging instance with users in all kinds of configuration. This helped us a lot in finding migration issues by comparing the user data on both systems before and after the migration, and strengthening our tests to cover all the edge cases we could find. The developers at Steady also added &lt;a href=&quot;https://www.erlang.org/doc/apps/kernel/erpc.html&quot;&gt;ERPC&lt;/a&gt;, which enabled us to run integration tests evaluating the effects of Charger on the Media Makers app.&lt;/p&gt;&lt;h2&gt;Conclusion&lt;a href=&quot;https://bitcrowd.dev/migrating-600k-users-to-new-billing-service#conclusion&quot;&gt;​&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Once we were confident that our 10 users were rolling, we migrated 10 more users with more complex data. And so on, for weeks, increasing the batch size, and deciding to create &lt;strong&gt;new&lt;/strong&gt; users on Charger with a &lt;code&gt;rand()&lt;/code&gt; choice. This allowed Steady to steer the future of their billing strategy for their users, integrating with other payment providers and simplifying some of the user flows for refunds etc. While it looks simple on paper, this project required a lot of patience, collaboration and planning ahead as the stakes were really high and we had to build up confidence in the new system and in the migration approach.&lt;/p&gt;</content:encoded>
</item>
<item>
<title>Building a blog with Elixir and Phoenix | jola.dev</title>
<link>https://jola.dev/posts/building-a-blog-with-elixir-and-phoenix</link>
<enclosure type="image/jpeg" length="0" url="https://jola.dev/images/og-image-2b7872671fc7c11e464dac899d8d3068.png?vsn=d"></enclosure>
<guid isPermaLink="false">-hibchFwAEJ6inLKS9s3riedDTRQov4u9ijPzw==</guid>
<pubDate>Thu, 26 Mar 2026 19:19:26 +0000</pubDate>
<description>Setting up a website using Elixir and Phoenix, leaning on NimblePublisher for the blog posts.</description>
<content:encoded>&lt;p&gt;
TL;DR: it’s an Elixir app using Phoenix server side rendered pages, with the blog post pages generated from Markdown using NimblePublisher. It’s running on a self-hosted Dokploy instance running on &lt;a href=&quot;https://hetzner.cloud/?ref=SjrsM8GhyYOl&quot;&gt;Hetzner&lt;/a&gt;, with &lt;a href=&quot;https://bunny.net?ref=f0l8865b7g&quot;&gt;bunny.net&lt;/a&gt; as a CDN sitting in front of it.&lt;/p&gt;
&lt;p&gt;
This is a very belated write up of how this blog was put together! There’s nothing terribly original here, but I figure it could come in handy for someone out there as a reference. And the world needs more Elixir content.&lt;/p&gt;
&lt;h2&gt;
Why Phoenix&lt;/h2&gt;
&lt;p&gt;
I have used static site generators before to power my blog (shoutout to &lt;a href=&quot;https://jaspervdj.be/hakyll/&quot;&gt;Hakyll&lt;/a&gt;), but I wanted to open the door for myself to also have little experiments on this site, ones that would require more interactivity than a static site allows. Besides, I just like using Phoenix. Although most of my Phoenix projects use LiveView, this felt like a good place to do things old-school with DeadViews.&lt;/p&gt;
&lt;p&gt;
It also means I get full control of what I’m building. Using a tool someone else created means getting a lot for free, but the moment you step outside of the expected you’re having to figure out how to make things work for their tool.&lt;/p&gt;
&lt;p&gt;
So I kept things simple. No Ecto, no DB. Just server-side rendered HTML. It’s blazingly fast, as you can see from this PageSpeed Insights report.&lt;/p&gt;
&lt;img src=&quot;https://jola.dev/images/joladev-speed-test.png&quot; alt=&quot;&quot; title=&quot;&quot;/&gt;
&lt;h2&gt;
NimblePublisher&lt;/h2&gt;
&lt;p&gt;
My setup closely matches the original Dashbit blog post &lt;a href=&quot;https://dashbit.co/blog/welcome-to-our-blog-how-it-was-made&quot;&gt;Welcome to our blog: how it was made!&lt;/a&gt;, which led to the creation of NimblePublisher.&lt;/p&gt;
&lt;p&gt;
The heart of the blog is the &lt;a href=&quot;https://github.com/dashbitco/nimble_publisher&quot;&gt;NimblePublisher&lt;/a&gt; setup, which consists of a &lt;code class=&quot;makeup ok&quot;&gt;use&lt;/code&gt; block:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;makeup ok&quot;&gt;defmodule JolaDev.Blog do

  use NimblePublisher,
    build: JolaDev.Blog.Post,
    from: Application.app_dir(:jola_dev, &amp;quot;priv/posts/**/*.md&amp;quot;),
    as: :posts,
    html_converter: JolaDev.Blog.MarkdownConverter,
    highlighters: [:makeup_elixir]
...&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;
This will load up all the posts, parse the frontmatter, run it through the markdown converter, and compile it into module attributes. This means there’s no work left to be done at runtime, it’s all pre-compiled.&lt;/p&gt;
&lt;p&gt;
Posts are organized by year:  &lt;code class=&quot;makeup ok&quot;&gt;priv/posts/2025/08-18-ruthless-prioritization.md&lt;/code&gt; . We get beautiful code block syntax highlighting through &lt;a href=&quot;https://github.com/elixir-makeup/makeup&quot;&gt;Makeup&lt;/a&gt;. The &lt;code class=&quot;makeup ok&quot;&gt;Blog&lt;/code&gt; module also defines a set of helpers for fetching the posts:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;makeup ok&quot;&gt;@posts Enum.sort_by(@posts, &amp;amp; &amp;amp;1.date, {:desc, Date})

# Let&amp;#39;s also get all tags
@tags @posts
      |&amp;gt; Enum.flat_map(&amp;amp; &amp;amp;1.tags)
      |&amp;gt; Enum.uniq()
      |&amp;gt; Enum.sort()

# And finally export them
def all_posts, do: @posts
def all_tags, do: @tags

def posts_by_tag(tag) do
  Enum.filter(all_posts(), fn post -&amp;gt; tag in post.tags end)
end

def find_by_id(id) do
  Enum.find(all_posts(), fn post -&amp;gt; post.id == id end)
end&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;
The only thing that took a bit of figuring out for me was getting Tailwind classes into the outputted HTML. I’m pretty sure I’ve seen better approaches shared since I wrote this, but this works too. Under &lt;code class=&quot;makeup ok&quot;&gt;earmark_options&lt;/code&gt;, pass:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;makeup ok&quot;&gt;Earmark.Options.make_options!(
  registered_processors: [
    Earmark.TagSpecificProcessors.new([
      {&amp;quot;a&amp;quot;, &amp;amp;Earmark.AstTools.merge_atts_in_node(&amp;amp;1, class: &amp;quot;underline&amp;quot;)},
      {&amp;quot;h1&amp;quot;, &amp;amp;Earmark.AstTools.merge_atts_in_node(&amp;amp;1, class: &amp;quot;text-3xl py-4&amp;quot;)},
      {&amp;quot;h2&amp;quot;, &amp;amp;Earmark.AstTools.merge_atts_in_node(&amp;amp;1, class: &amp;quot;text-2xl py-4&amp;quot;)},
      {&amp;quot;h3&amp;quot;, &amp;amp;Earmark.AstTools.merge_atts_in_node(&amp;amp;1, class: &amp;quot;text-xl py-4&amp;quot;)},
      {&amp;quot;p&amp;quot;, &amp;amp;Earmark.AstTools.merge_atts_in_node(&amp;amp;1, class: &amp;quot;text-md pb-4&amp;quot;)},
      {&amp;quot;code&amp;quot;, &amp;amp;Earmark.AstTools.merge_atts_in_node(&amp;amp;1, class: &amp;quot;&amp;quot;)},
      {&amp;quot;pre&amp;quot;,
       &amp;amp;Earmark.AstTools.merge_atts_in_node(&amp;amp;1,
         class: &amp;quot;mb-4 p-1 py-4 overflow-x-scroll border-y&amp;quot;
       )},
      {&amp;quot;ol&amp;quot;, &amp;amp;Earmark.AstTools.merge_atts_in_node(&amp;amp;1, class: &amp;quot;list-decimal&amp;quot;)},
      {&amp;quot;ul&amp;quot;, &amp;amp;Earmark.AstTools.merge_atts_in_node(&amp;amp;1, class: &amp;quot;list-disc pb-4&amp;quot;)},
      {&amp;quot;blockquote&amp;quot;,
       &amp;amp;Earmark.AstTools.merge_atts_in_node(&amp;amp;1,
         class: &amp;quot;pl-4 border-l-2 mb-4 border-purple-700&amp;quot;
       )}
    ])
  ]
)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;
You probably have your own preferences for how to set up your classes, but this gives you a pattern you can use to ensure that the tags that come out have the appropriate classes.&lt;/p&gt;
&lt;h2&gt;
The Frontend&lt;/h2&gt;
&lt;p&gt;
As mentioned this is all server-side rendered Phoenix templates. It’s using standard Tailwind CSS. It predates DaisyUI and I don’t think there’s a strong reason for me to make the lift of getting it in, although I wouldn’t have minded it being a part of the scaffolding back when I set up the blog!&lt;/p&gt;
&lt;p&gt;
The only JS snippets in here are a mobile menu toggle and the Phoenix topbar. Apart from the Tailwind library, the custom CSS in here is pretty minimal. You get a lot out of the box with a Phoenix project.&lt;/p&gt;
&lt;p&gt;
And of course, dark mode. I know it’s not everyone’s cup of tea, but it is my website after all.&lt;/p&gt;
&lt;h2&gt;
CI&lt;/h2&gt;
&lt;p&gt;
I’ve got Github Actions set up to run on every push and PR, just the basic Elixir quality assurance tools.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;
&lt;code class=&quot;makeup ok&quot;&gt;mix compile --warnings-as-errors&lt;/code&gt;  &lt;/li&gt;
  &lt;li&gt;
&lt;code class=&quot;makeup ok&quot;&gt;mix format --check-formatted&lt;/code&gt;  &lt;/li&gt;
  &lt;li&gt;
&lt;code class=&quot;makeup ok&quot;&gt;mix credo --strict&lt;/code&gt;  &lt;/li&gt;
  &lt;li&gt;
&lt;code class=&quot;makeup ok&quot;&gt;mix test&lt;/code&gt;  &lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;
And then I’ve got Dependabot set up as well. I’ve been hearing and thinking a lot about how it creates a lot of noise, but I feel like that’s less of an issue in the Elixir community. Packages tend to not have a lot of dependencies, and so you don’t get the same waves of bumps going out that npm does. And merging them is satisfying.&lt;/p&gt;
&lt;h2&gt;
Deployment&lt;/h2&gt;
&lt;p&gt;
On the hosting side things get a bit more spicy. The repo includes a &lt;a href=&quot;https://github.com/joladev/jola.dev/blob/main/Dockerfile&quot;&gt;multi-stage Docker file&lt;/a&gt;, roughly based on the Phoenix recommended example file. This means that most of the dependencies are only pulled in at build time, and the image you get out on the other side is a bit smaller. I’m using Elixir &lt;code class=&quot;makeup ok&quot;&gt;1.18.4&lt;/code&gt;, Erlang &lt;code class=&quot;makeup ok&quot;&gt;28.0.2&lt;/code&gt;, and Debian &lt;code class=&quot;makeup ok&quot;&gt;trixie-20250721-slim&lt;/code&gt; at the time of writing this, but that’s likely to change. There’s something very satisfying about bumping dependencies.&lt;/p&gt;
&lt;p&gt;
And now we’re arriving at &lt;a href=&quot;https://dokploy.com/&quot;&gt;Dokploy&lt;/a&gt;, an open source platform as a service (PaaS) for running apps, basically a self-hosted Heroku. It does everything, automatic builds and deploys from Github updates, built-in Docker Swarm, networking, orchestration of replicas across the cluster, rolling deploys, rollbacks, preview builds, and much more.&lt;/p&gt;
&lt;p&gt;
So my publish flow is basically: create a PR and wait for CI to finish (I could skip this but it’s nice to know I didn’t mess something up). When I merge the PR Dokploy automatically picks that up and triggers a checkout and build of the repo. Once that finishes, it starts a rolling deploy to replace the running replicas. And we’re live. With cached layers on the server, deploys can finish in 30s, zero effort.&lt;/p&gt;
&lt;p&gt;
I run this Dokploy instance on &lt;a href=&quot;https://hetzner.cloud/?ref=SjrsM8GhyYOl&quot;&gt;Hetzner&lt;/a&gt; and my experience has been really positive. The pricing is unbeatable, even with the recent increase, and it’s been rock solid for me. Really, with the Dokploy instance, there’s nothing stopping me from packing up and going somewhere else. Having that kind of freedom is very nice. But I’m more than happy to stick with Hetzner.&lt;/p&gt;
&lt;h2&gt;
The Little Things&lt;/h2&gt;
&lt;p&gt;
I’ve set up a few little conveniences for my app so I’ll share some example code for them here.&lt;/p&gt;
&lt;h3&gt;
RSS&lt;/h3&gt;
&lt;p&gt;
RSS is managed by a plain Phoenix controller that looks something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;makeup ok&quot;&gt;defmodule JolaDevWeb.RssXML do
  use JolaDevWeb, :html

  embed_templates &amp;quot;rss_xml/*&amp;quot;

  def format_rfc822(%Date{} = date) do
    date
    |&amp;gt; DateTime.new!(~T[00:00:00], &amp;quot;Etc/UTC&amp;quot;)
    |&amp;gt; format_rfc822()
  end

  def format_rfc822(%DateTime{} = datetime) do
    Calendar.strftime(datetime, &amp;quot;%a, %d %b %Y %H:%M:%S +0000&amp;quot;)
  end
end&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;
and the corresponding XML:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;makeup ok&quot;&gt;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot;?&amp;gt;
&amp;lt;rss version=&amp;quot;2.0&amp;quot; xmlns:atom=&amp;quot;http://www.w3.org/2005/Atom&amp;quot; xmlns:content=&amp;quot;http://purl.org/rss/1.0/modules/content/&amp;quot;&amp;gt;
  &amp;lt;channel&amp;gt;
    &amp;lt;title&amp;gt;jola.dev&amp;lt;/title&amp;gt;
    &amp;lt;link&amp;gt;&amp;lt;%= url(~p&amp;quot;/&amp;quot;) %&amp;gt;&amp;lt;/link&amp;gt;
    &amp;lt;description&amp;gt;Blog posts from jola.dev&amp;lt;/description&amp;gt;
    &amp;lt;language&amp;gt;en-us&amp;lt;/language&amp;gt;
    &amp;lt;lastBuildDate&amp;gt;&amp;lt;%= JolaDevWeb.RssXML.format_rfc822(DateTime.utc_now()) %&amp;gt;&amp;lt;/lastBuildDate&amp;gt;
    &amp;lt;atom:link href=&amp;quot;&amp;lt;%= url(~p&amp;quot;/rss.xml&amp;quot;) %&amp;gt;&amp;quot; rel=&amp;quot;self&amp;quot; type=&amp;quot;application/rss+xml&amp;quot; /&amp;gt;

    &amp;lt;%= for post &amp;lt;- @posts do %&amp;gt;
    &amp;lt;item&amp;gt;
      &amp;lt;title&amp;gt;&amp;lt;%= post.title %&amp;gt;&amp;lt;/title&amp;gt;
      &amp;lt;link&amp;gt;&amp;lt;%= url(~p&amp;quot;/posts/#{post.id}&amp;quot;) %&amp;gt;&amp;lt;/link&amp;gt;
      &amp;lt;description&amp;gt;&amp;lt;![CDATA[&amp;lt;%= post.description %&amp;gt;]]&amp;gt;&amp;lt;/description&amp;gt;
      &amp;lt;content:encoded&amp;gt;&amp;lt;![CDATA[&amp;lt;%= post.body %&amp;gt;]]&amp;gt;&amp;lt;/content:encoded&amp;gt;
      &amp;lt;pubDate&amp;gt;&amp;lt;%= JolaDevWeb.RssXML.format_rfc822(post.date) %&amp;gt;&amp;lt;/pubDate&amp;gt;
      &amp;lt;guid isPermaLink=&amp;quot;true&amp;quot;&amp;gt;&amp;lt;%= url(~p&amp;quot;/posts/#{post.id}&amp;quot;) %&amp;gt;&amp;lt;/guid&amp;gt;
      &amp;lt;author&amp;gt;&amp;lt;%= post.author %&amp;gt;&amp;lt;/author&amp;gt;
    &amp;lt;/item&amp;gt;
    &amp;lt;% end %&amp;gt;
  &amp;lt;/channel&amp;gt;
&amp;lt;/rss&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;
Sitemap&lt;/h3&gt;
&lt;p&gt;
I was a bit surprised not to find a clean little library for generating the sitemap (this may have changed since I wrote the code!), but I guess the implementation is just going to heavily depend on your setup. Anyway, just sharing this for reference.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;makeup ok&quot;&gt;defmodule JolaDevWeb.SitemapController do
  use JolaDevWeb, :controller

  def index(conn, _params) do
    sitemap = JolaDev.Sitemap.generate()

    conn
    |&amp;gt; put_resp_content_type(&amp;quot;text/xml&amp;quot;)
    |&amp;gt; send_resp(200, sitemap)
  end
end

defmodule JolaDev.Sitemap do
  alias JolaDev.Blog

  @host &amp;quot;https://jola.dev&amp;quot;

  def generate do
    &amp;quot;&amp;quot;&amp;quot;
    &amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot;?&amp;gt;
    &amp;lt;urlset xmlns=&amp;quot;http://www.sitemaps.org/schemas/sitemap/0.9&amp;quot;&amp;gt;
    #{generate_static_pages()}#{generate_tag_pages()}#{generate_blog_posts()}
    &amp;lt;/urlset&amp;gt;
    &amp;quot;&amp;quot;&amp;quot;
  end

  defp generate_static_pages do
    pages = [
      %{loc: @host, changefreq: &amp;quot;monthly&amp;quot;, priority: &amp;quot;1.0&amp;quot;},
      %{loc: &amp;quot;#{@host}/about&amp;quot;, changefreq: &amp;quot;monthly&amp;quot;, priority: &amp;quot;0.8&amp;quot;},
      %{loc: &amp;quot;#{@host}/projects&amp;quot;, changefreq: &amp;quot;weekly&amp;quot;, priority: &amp;quot;0.9&amp;quot;},
      %{loc: &amp;quot;#{@host}/talks&amp;quot;, changefreq: &amp;quot;monthly&amp;quot;, priority: &amp;quot;0.7&amp;quot;},
      %{loc: &amp;quot;#{@host}/posts&amp;quot;, changefreq: &amp;quot;weekly&amp;quot;, priority: &amp;quot;0.9&amp;quot;}
    ]

    Enum.map_join(pages, &amp;quot;\n&amp;quot;, &amp;amp;url_entry/1)
  end

  defp generate_tag_pages do
    Blog.all_tags()
    |&amp;gt; Enum.map(fn tag -&amp;gt;
      %{loc: &amp;quot;#{@host}/posts/tag/#{tag}&amp;quot;, changefreq: &amp;quot;weekly&amp;quot;, priority: &amp;quot;0.6&amp;quot;}
    end)
    |&amp;gt; Enum.map_join(&amp;quot;\n&amp;quot;, &amp;amp;url_entry/1)
  end

  defp generate_blog_posts do
    Blog.all_posts()
    |&amp;gt; Enum.map(fn post -&amp;gt;
      %{
        loc: &amp;quot;#{@host}/posts/#{post.id}&amp;quot;,
        lastmod: Date.to_iso8601(post.date),
        changefreq: &amp;quot;monthly&amp;quot;,
        priority: &amp;quot;0.8&amp;quot;
      }
    end)
    |&amp;gt; Enum.map_join(&amp;quot;\n&amp;quot;, &amp;amp;url_entry/1)
  end

  defp url_entry(params) do
    &amp;quot;&amp;quot;&amp;quot;
      &amp;lt;url&amp;gt;
        &amp;lt;loc&amp;gt;#{params.loc}&amp;lt;/loc&amp;gt;
        #{if params[:lastmod], do: &amp;quot;&amp;lt;lastmod&amp;gt;#{params.lastmod}&amp;lt;/lastmod&amp;gt;&amp;quot;, else: &amp;quot;&amp;quot;}
        &amp;lt;changefreq&amp;gt;#{params.changefreq}&amp;lt;/changefreq&amp;gt;
        &amp;lt;priority&amp;gt;#{params.priority}&amp;lt;/priority&amp;gt;
      &amp;lt;/url&amp;gt;
    &amp;quot;&amp;quot;&amp;quot;
  end
end&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;
Blog redirect plug&lt;/h3&gt;
&lt;p&gt;
When I first moved over to this new app I wanted to ensure that I kept my old blog post links alive, so I set up this little plug to rewrite requests to match the new layout.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;makeup ok&quot;&gt;defmodule JolaDevWeb.Plugs.BlogRedirect do
  import Plug.Conn

  def init(_), do: []

  def call(conn, _opts) do
    if conn.host == &amp;quot;blog.jola.dev&amp;quot; do
      ids = JolaDev.Blog.ids()
      path = strip_path(conn.request_path)

      path =
        if path in ids do
          &amp;quot;posts/&amp;quot; &amp;lt;&amp;gt; path
        else
          path
        end

      conn
      |&amp;gt; put_resp_header(&amp;quot;location&amp;quot;, &amp;quot;https://jola.dev/&amp;quot; &amp;lt;&amp;gt; path)
      |&amp;gt; send_resp(:moved_permanently, &amp;quot;&amp;quot;)
      |&amp;gt; halt()
    else
      conn
    end
  end

  defp strip_path(&amp;quot;/&amp;quot; &amp;lt;&amp;gt; path), do: path
  defp strip_path(path), do: path
end&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;
SEO&lt;/h3&gt;
&lt;p&gt;
I went a bit further on this one. Each page has its own meta description, Open Graph tags, and Twitter Card tags — all driven by assigns passed from the controllers. Blog posts automatically get &lt;code class=&quot;makeup ok&quot;&gt;og:type=&amp;quot;article&amp;quot;&lt;/code&gt; with &lt;code class=&quot;makeup ok&quot;&gt;article:published_time&lt;/code&gt; and &lt;code class=&quot;makeup ok&quot;&gt;article:tag&lt;/code&gt; set from the post metadata. The layout just reads from &lt;code class=&quot;makeup ok&quot;&gt;conn.assigns&lt;/code&gt; with sensible fallbacks, so adding SEO to a new page is just a matter of passing the right assigns. Here’s what the blog-post-specific bits look like in the layout:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;makeup ok&quot;&gt;&amp;lt;meta property=&amp;quot;og:type&amp;quot; content={if(@conn.assigns[:post], do: &amp;quot;article&amp;quot;, else: &amp;quot;website&amp;quot;)} /&amp;gt;
&amp;lt;%= if post = @conn.assigns[:post] do %&amp;gt;
  &amp;lt;meta property=&amp;quot;article:published_time&amp;quot; content={Date.to_iso8601(post.date)} /&amp;gt;
  &amp;lt;meta property=&amp;quot;article:author&amp;quot; content=&amp;quot;https://jola.dev/about&amp;quot; /&amp;gt;
  &amp;lt;%= for tag &amp;lt;- post.tags do %&amp;gt;
    &amp;lt;meta property=&amp;quot;article:tag&amp;quot; content={tag} /&amp;gt;
  &amp;lt;% end %&amp;gt;
&amp;lt;% end %&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;
Same idea for the Twitter Card and description tags — one place in the layout, driven entirely by what the controller passes in.&lt;/p&gt;
&lt;p&gt;
I also added &lt;a href=&quot;https://llmstxt.org/&quot;&gt;&lt;code class=&quot;makeup ok&quot;&gt;llms.txt&lt;/code&gt;&lt;/a&gt; and &lt;code class=&quot;makeup ok&quot;&gt;llms-full.txt&lt;/code&gt; endpoints, this is a newer standard that helps AI systems understand your site. It follows the same pattern as the sitemap: a module that generates the content from &lt;code class=&quot;makeup ok&quot;&gt;Blog.all_posts()&lt;/code&gt;, and a controller that serves it as plain text. Whether it actually matters yet, who knows, but it was trivial to add and I figure it can’t hurt.&lt;/p&gt;
&lt;h2&gt;
Wrapping Up&lt;/h2&gt;
&lt;p&gt;
This app is intentionally kept simple but powerful. Everything is set up the way I want it and I have a zero effort and very fast pipeline for publishing new posts. If you’re an Elixir dev thinking about a personal site, consider just using Phoenix. Combined with NimblePublisher you’ve got a really powerful and blazing fast blog framework right there.&lt;/p&gt;
&lt;p&gt;
And while you’re at it, why not host it on Hetzner! If you use the &lt;a href=&quot;https://hetzner.cloud/?ref=SjrsM8GhyYOl&quot;&gt;referral link to sign up you get €20 and I get €10&lt;/a&gt;. If you prefer not to use the referral link, here’s a plain link: &lt;a href=&quot;https://www.hetzner.com/cloud/&quot;&gt;https://www.hetzner.com/cloud/&lt;/a&gt;. Also consider joining me in &lt;a href=&quot;https://github.com/sponsors/Dokploy&quot;&gt;sponsoring Dokploy&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;
Source code is available at: &lt;a href=&quot;https://github.com/joladev/jola.dev&quot;&gt;https://github.com/joladev/jola.dev&lt;/a&gt;. Next up I’ll talk about setting up &lt;a href=&quot;https://bunny.net?ref=f0l8865b7g&quot;&gt;bunny.net&lt;/a&gt; and a separate post on Dokploy on Hetzner.&lt;/p&gt;</content:encoded>
</item>
<item>
<title>Software Development in 2026</title>
<link>https://rocket-science.ru/hacking/2026/03/23/software-development-in-2026</link>
<guid isPermaLink="false">wz0KP8VGMC6EKD0YbSz0XdDPfEn0ZTAf7bmz_g==</guid>
<pubDate>Wed, 25 Mar 2026 16:08:30 +0000</pubDate>
<description>Three years ago I wrote a fairly coherent piece on four key developer skills, but a good deal of water has flowed under the bridge since then, and while the theses laid out there remain sound, they need a slight adjustment in light of the boon that has descended upon us in the form of large language models.</description>
<content:encoded>&lt;p&gt;Three years ago I wrote a fairly coherent piece on &lt;a href=&quot;https://rocket-science.ru/hacking/2023/11/03/software-development-in-2023&quot;&gt;four key developer skills&lt;/a&gt;, but a good deal of water has flowed under the bridge since then, and while the theses laid out there remain sound, they need a slight adjustment in light of the boon that has descended upon us in the form of large language models.&lt;/p&gt;&lt;p&gt;I went through all five stages of the inevitable over the course of a year.&lt;/p&gt;&lt;p&gt;① &lt;strong&gt;Denial&lt;/strong&gt;—I watched my colleagues a year ago raving about autocomplete and hallucinations, and even won a few bets—not unlike the one the Adriano Celentano character &lt;a href=&quot;https://youtu.be/GBFt3FF7i2Q&quot;&gt;proposes to his accountant&lt;/a&gt; in that great film.&lt;/p&gt;&lt;p&gt;② &lt;strong&gt;Anger&lt;/strong&gt;—I kept writing code by hand, but part of my job involves cleaning up colleagues’ messes after shifted concepts (I do a lot of reviews), and the volume of neural slop had crossed every conceivable boundary: even when the code compiled, it looked like Rome from a bird’s-eye view—slovenly fragments of good practices scattered here and there, interspersed with the slums of deeply nested conditionals. I found myself literally rewriting large chunks after the little models, because I take code review seriously and still see it as a tool for teaching apprentices.&lt;/p&gt;&lt;p&gt;③ &lt;strong&gt;Bargaining&lt;/strong&gt;—about seven months ago I tried unleashing a model on an old library of mine that was desperately in need of proper documentation; to my surprise, the documentation turned out coherent, nearly complete, and unquestionably better than nothing. I screwed my eyes shut and asked for tests. Half of them tested the standard library and implementation details—but the other half brought genuine value. Like those gruff stubborn men from the joke, I said: “Welllll, damn.” And paid for &lt;a href=&quot;https://www.warp.dev/&quot;&gt;Warp&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;④ &lt;strong&gt;Depression&lt;/strong&gt;—in the first month of use I finished two personal projects that had been gathering dust for years, equipped all my libraries with detailed documentation and the missing tests, played around with creating my own programming language, and even trusted the model to fully solve a take-home assignment for one of the companies that had written to me directly with an offer (the assignment sailed through, but I never would have taken the job anyway—their HR had violated every conceivable rule of decency in headhunting). I wasn’t afraid, of course, that I’d be thrown out and Claude hired in my place—the models won’t reach my level of expertise before I retire—but writing code is one of my most beloved occupations in life, and I felt it being taken away from me.&lt;/p&gt;&lt;p&gt;⑤ &lt;strong&gt;Acceptance&lt;/strong&gt;—it is now the end of March 2026, and I can say with confidence that language models provide me with substantial help in development, without particularly intruding on the part of my life I cherish: they write excellent documentation, reasonably coherent tests, and—under supervision and with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Ctrl&lt;/code&gt;+&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;C&lt;/code&gt; at the ready—can shuffle JSON around. Complex code I still write myself, and I’m confident this will continue until my death at the wheel of a sports motorcycle.&lt;/p&gt;&lt;hr/&gt;&lt;p&gt;At the same time, the internet is proliferating with success stories from every variety of blowhard—from startup founders with three grades of parochial school, armed with enthusiasm, a narrow worldview, and the free tier of &lt;em&gt;ChatGPT&lt;/em&gt;—to bedroom traders whose imaginary profits already permit them to purchase a paper yacht in Bali. In the hands of people far removed from software development, large language models are doomed to two low-yield applications: you can amuse yourself generating memes of piglets drinking mojitos in the Kremlin, or you can reproduce a product that already exists on the market and that in its niche you’ll never catch up to.&lt;/p&gt;&lt;p&gt;To use models effectively (you can now also deploy agents, but this changes nothing whatsoever about the substance), you need—at minimum—to understand the principles by which they operate. It took the automobile industry a hundred years to give users the joy of driving a vehicle without ever having opened the bonnet. Aircraft haven’t reached that stage yet. I see no reason to suppose that very advanced autocomplete is capable of repairing itself. And this is besides the fact that language models are, in principle, a dead end in the development of &lt;em&gt;artificial intelligence&lt;/em&gt;. Even Yann LeCun has &lt;a href=&quot;https://amilabs.xyz&quot;&gt;understood this&lt;/a&gt;, though unfortunately it remains unclear whether his own ideas aren’t yet another dead end.&lt;/p&gt;&lt;p&gt;Yeah.&lt;/p&gt;&lt;p&gt;If you need to knock together from scratch a shop for the worthless trinkets your wife makes—a modern model will handle it with flying colours. Clean unpretentious design, convenient addition of new bracelets, photos, payment system. The little model will knock you up a website, a mobile app, and lord knows what else. And it will work first try, most likely—because in training it has stared at such dreck in billions of different variations. What everyone finds so astonishing is, in essence, the output of four shell commands: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cp&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;grep&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sed&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;awk&lt;/code&gt;. Last century this technology bore the proud name “snippet.” For cloning already-existing things, then, the model is a perfectly fine assistant. Web studios that build landing pages will probably indeed die (they should never have existed in principle, but that’s a different question).&lt;/p&gt;&lt;p&gt;As for more complex projects—not necessarily more complex &lt;em&gt;per se&lt;/em&gt;, but more &lt;em&gt;unusual&lt;/em&gt;—without an architect to guide it, the model will disgrace itself at the very first hurdle. Because a vaguely described result can be achieved in fifteen different ways, and sooner or later the rough-and-ready decisions of a spring balance (a steelyard, not a precision scale) will lead into a swamp from which there’s no escape—because admitting defeat is not our way, and the model will play roulette to the bitter end, like Dostoevsky in Baden-Baden.&lt;/p&gt;&lt;p&gt;I recently wrote about why elaborate prompts and detailed descriptions cannot produce a good result—see part two of &lt;a href=&quot;https://rocket-science.ru/hacking/2026/03/13/artificial-intelligence&quot;&gt;Artificial ‘Intelligence’&lt;/a&gt;—I won’t repeat myself here, but will quote the key thesis:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;A person capable of breaking a large task down into the minimum number of smaller ones that satisfy the condition of “unambiguous solution” is capable, in today’s reality, of writing a reasonably complex application in a couple of days.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;What I mean is that the ability to distinguish tasks with branching logic from multi-step syllogisms is more in demand than ever. Draw a mental flowchart of the execution—and if there are &lt;a href=&quot;https://en.wikipedia.org/wiki/Flowchart#Common_symbols&quot;&gt;decision diamonds&lt;/a&gt; in it, you need to work them out for the model explicitly (go left here, go right here, snow on head here, very painful). Better still—break the main task into several, to eliminate those “conditions/decisions” entirely. But this is hardly possible if you have no idea how to write such code yourself.&lt;/p&gt;&lt;hr/&gt;&lt;p&gt;Besides the fools who tried to build a website and succeeded—there are also saboteurs. They are considerably more dangerous in that they give the unprepared reader the impression of being “people in the know.” Did I mention that before paying for a subscription to a cloud model I thoroughly understood exactly how they work? I always do. If I get it into my head to drag library &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;xyz&lt;/code&gt; into a project, I start by digging into its source code. When choosing a technology I don’t read adoption success stories and watch benchmarks—I write my own “trinket shop in the Bronx” with it. To use language models in daily work, and actually pay for the privilege, Altman’s feverish ravings and Karpathy’s coquettish napkin-code are not enough for me. I need to understand how it functions under the hood, so as to preemptively avoid disappointment and the collapse of hopes.&lt;/p&gt;&lt;p&gt;In short, those who have learned by instruction to run models and ask them questions pulled from thin air—supposedly testing or even proving something—would frankly be better off keeping quiet. Because everything that has crossed my field of vision resembles hallucinations from the first version of ChatGPT, Joyce’s &lt;em&gt;Ulysses&lt;/em&gt;, Castaneda’s astral flights, and in principle any address at a party congress by the secretary of the Upper Walrusville cell, having gorged himself on fly agaric.&lt;/p&gt;&lt;p&gt;Let me skim glissando—or, as Dovlatov used to say, in dotted lines—across the main points.&lt;/p&gt;&lt;h3&gt;Tests and Benchmarks&lt;/h3&gt;&lt;p&gt;Comparing different models based on sets of cardboard tests and plastic benchmarks is pure, undiluted charlatanism. It’s enough to look at the language summary table at &lt;a href=&quot;https://autocodebench.github.io/&quot;&gt;AutoCodeBench → Experimental Results&lt;/a&gt; (yes, this is sarcasm). Claude Opus 4 fails to hit 50% for TypeScript but clears 80% for Elixir. Translating from accountant-speak into plain language (and exaggerating slightly, of course)—if your project is in TypeScript, the model is more of a hindrance; if it’s in Elixir, you can hand it a 1,000-line refactor.&lt;/p&gt;&lt;h3&gt;Context&lt;/h3&gt;&lt;p&gt;RAG in any moderately complex project (or more precisely, its “RA” part) matters hundreds of times more than the model itself. Why do all these Claudes burrow into your computers with their IDEs (and lately—CLIs), do you think?—It’s simple: shipping the entire context to the server every time is expensive and inefficient (and despite the flagship advertising promises, nobody can actually process more than a hundred thousand tokens without noticeable quality loss). So every model sends some kind of “distillate” to the server.&lt;/p&gt;&lt;p&gt;It’s important to understand that any language model operates as a finite state machine: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;start&lt;/code&gt; → &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;context&lt;/code&gt; → &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;query&lt;/code&gt; → &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;response&lt;/code&gt; → &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;stop&lt;/code&gt;. There are no “sessions.” You cannot “launch” a model and converse with it—every request you make starts from a blank slate. Models as such have no “memory”—which is why preserving context is critically important. But unlike Hoffman’s character from &lt;em&gt;Rain Man&lt;/em&gt;, even we cannot memorize six decks of cards—let alone models, with their context window as narrow as the Strait of Hormuz. My slowly-simmering project &lt;a href=&quot;https://hexdocs.pm/ragex/&quot;&gt;Ragex&lt;/a&gt; is an attempt to somehow formalize—and minimize without loss of generality—the context required for processing sizeable codebases. We’ll see whether I manage it—but the fact that I appear to be alone in this on the visible horizon is not exactly inspiring.&lt;/p&gt;&lt;h3&gt;Plans and Reasoning&lt;/h3&gt;&lt;p&gt;When tackling any reasonably non-trivial task, you need to force the model to sketch a plan and ask all the questions that arose for it while creating that plan. It will gladly surrender to you all the internal decision branches from the flowchart. These questions must be answered as clearly as possible—in blunt, clipped phrases that admit no double interpretation. The requirement to supply each phase of the plan with tests and bring all project documentation fully in line with the current state of the codebase must be baked into the general rules.&lt;/p&gt;&lt;p&gt;Each stage calls for a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git diff&lt;/code&gt; with an informal review. I also always use “thinking” models, and watch those reasoning traces in real time—so that the moment it tries to veer off course (and on non-trivial tasks it will always try)—I can kill the little pest with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Ctrl&lt;/code&gt;+&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;C&lt;/code&gt; and explain why it’s wrong.&lt;/p&gt;&lt;h3&gt;Language&lt;/h3&gt;&lt;p&gt;In my experience, it’s best to use the language in which an answer is easiest to find on the internet. For code—always English; for obscure details of Lorca’s biography—Spanish; for a survey of the sexual services market in Berlin—German.&lt;/p&gt;&lt;p&gt;I have no hard evidence, unfortunately—only a general understanding of how this T9 on steroids operates—but the empirical record is copious. My Spanish hovers somewhere around B2–C1 level, so I still try English first; if the result looks underwhelming, I strain my fingers in Spanish—and in the vast majority of cases, I don’t regret it.&lt;/p&gt;&lt;h3&gt;Politeness&lt;/h3&gt;&lt;p&gt;After each successful task completion I say something like “Awesome,” or “Astonishing,” or “Stunning,” or words to that effect. I phrase every request as a request and always add “please.” My behaviour affects carbon emissions about as much as it affects democratic elections in a country of fifty million—but it makes things simpler and more pleasant for me. Besides, as Niels Bohr once said: “Of course I am not superstitious. But they say a horseshoe brings luck even to those who don’t believe in such nonsense.”&lt;/p&gt;</content:encoded>
</item>
<item>
<title>Horizontal Scaling</title>
<link>https://rocket-science.ru/hacking/2023/12/26/horizontal-scaling</link>
<guid isPermaLink="false">h0lE6LRnT3TqEqyaGkLNnn11FcirlP9cJ3Te4A==</guid>
<pubDate>Wed, 25 Mar 2026 16:08:30 +0000</pubDate>
<description>Part four of four key developer skills.</description>
<content:encoded>&lt;p&gt;&lt;em&gt;Part four of &lt;a href=&quot;https://rocket-science.ru/hacking/2023/11/03/software-development-in-2023&quot;&gt;four key developer skills&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Ability to immediately build a horizontally scalable solution without adding any special code for it in the first version.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;&lt;img src=&quot;https://rocket-science.ru/img/horizontal-scaling.jpg&quot; alt=&quot;Sometimes it’s better to keep quiet than to speak&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;hr/&gt;&lt;p&gt;This turned out to be the hardest piece in the series, because literally a handful of developers actually understand what “horizontal scaling” is. Shown above is a screenshot of an amusing tweet by Tobi Lütke, which demonstrates either his supreme professionalism in sophistry, or his complete technical incompetence.&lt;/p&gt;&lt;p&gt;Serving ten billion clients does not mean your service is scalable. Buses carry people along Nevsky Prospect, and buses carry people along the Garden Ring; anyone who has completed three grades of parochial logistics school will tell you that speaking of “reliable connections between Leningrad and Moscow” on the basis of this incontrovertible fact is premature.&lt;/p&gt;&lt;p&gt;Even within a single city, the situation can easily spiral out of control. Scaling a bus fleet to match the expanding footprint of a metropolis is not about buying and deploying more buses. I lived in Berlin twenty years ago and could not stop marveling at how well the transport was organized. Judging by today’s passenger reviews, as the city spread outward—BVG did not cope.&lt;/p&gt;&lt;p&gt;I invite the thoughtful reader to pause here and think: so what exactly went wrong with the service’s scaling? For the superficial blockheads who can’t be bothered to think through what they’re reading—the answer is: logistics.&lt;/p&gt;&lt;p&gt;Deploying more buses onto the streets of a multi-million-person city doesn’t require much brainpower. But each individual passenger needs exactly one bus: the one that arrives immediately after they step off the previous one. Not an hour later, not a minute too early—within a three-to-five minute transfer window. As long as the bus schedule is built around minimized (and guaranteed) transfer times—we can speak of scaling. If bus A dumps its passengers at the terminal thirty seconds after connecting bus B has already pulled away—scaling has failed.&lt;/p&gt;&lt;p&gt;All right, to hell with Berlin—even with good public transport, living there was impossible. Let’s get back to Tobi.&lt;/p&gt;&lt;p&gt;Each Shopify user requires one persistent connection (a WebSocket) to one server. Adding new users is simply adding servers to the rack. If there are any scaling problems—they’re all clustered around the database, not Rails itself. Rails doesn’t care at all how many total servers are serving users: each server only needs to handle its own load.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;This is not scaling. This is adding isolated capacity. It’s like if the zoom on your phone’s camera did not actually zoom in without unacceptable quality loss, but simply tiled more tiny copies of the image.&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;What the computing industry calls scaling is adding new hardware resources that increase the capacity of a &lt;em&gt;connected&lt;/em&gt; node. Deploying more buses on new streets—no. Changing the schedule so that new buses coordinate with the existing ones—yes.&lt;/p&gt;&lt;p&gt;A chess server, for example, requires no scaling whatsoever. The server overheating?—Put another one next to it; in the end we’re serving disconnected pairs of players anyway. A server for the simultaneous slaughter of a billion sweaty nerds, on the other hand—that one needs scaling.&lt;/p&gt;&lt;p&gt;Let me briefly mention one example from personal practice before getting to the point. We process an incoming stream of currency exchange rates, perform some mathematical manipulations on them, and spit out peculiar results. Two hundred-plus currencies—that’s forty thousand pairs—with values arriving on average roughly once a second (in practice, more often). The mathematical manipulations can involve any number of currencies. All of this happens in real time. Because values must be available at any given moment, we cannot simply partition the pairs and split the streams across multiple servers. And a single server physically cannot handle it. Which means every node in the cluster must be able to exchange information with every other node—and the information is not static, so Redis won’t do. This is where the architecture needs the ability to scale horizontally.&lt;/p&gt;&lt;hr/&gt;&lt;p&gt;The preamble has dragged on a bit. I hope I’ve at least clarified the terminology somewhat. So: if in the course of discussing an architecture you’ve concluded that the project will need genuine horizontal scaling—you cannot do without finite state machines. (In general it’s better to build all business logic on FSMs, but in a standalone system you can hobble along without them—in a cluster, there’s no way.) I rarely recommend reference material, but &lt;a href=&quot;https://en.wikipedia.org/wiki/Introduction_to_the_Theory_of_Computation&quot;&gt;Introduction to the Theory of Computation&lt;/a&gt; by Michael Sipser is well worth a look. A finite state machine—for all its apparent simplicity—is something so powerful it genuinely astonishes.&lt;/p&gt;&lt;p&gt;Unfortunately, many believe that an FSM is simply a set of states—an extremely dangerous misconception that completely negates the entire body of formal mathematics upon which the power of finite state machines rests.&lt;/p&gt;&lt;p&gt;In any case, if you want to be ready to scale horizontally—build critical processes on finite state machines and make them fully asynchronous. If subsystem A must interact with subsystem B—forget about direct calls. In HTTP terms—&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;201&lt;/code&gt; is good, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;200&lt;/code&gt; is dreadful. Under no circumstances will you ever be able to later convert a request→response call sequence into request→acknowledgement→await response (without the destructive force of a complete refactor).&lt;/p&gt;&lt;p&gt;Asynchronous interactions built on top of FSMs, on the other hand, will make future scaling painless—because in this paradigm it makes absolutely no difference on which node the code handling a request actually runs.&lt;/p&gt;&lt;p&gt;There are languages in which this is easier (Elixir, Erlang), and those in which it’s harder. But in principle it is achievable in any environment. Once you get the hang of it, writing asynchronous code becomes no harder than synchronous. I speak from experience.&lt;/p&gt;&lt;hr/&gt;&lt;p&gt;And finally, the perfect litmus test for determining whether your system is truly distributed or just a few servers standing in corners. If you’ve ever had to decide which letter from the &lt;a href=&quot;https://en.wikipedia.org/wiki/CAP_theorem&quot;&gt;CAP theorem&lt;/a&gt; set—‘C,’ ‘A,’ ‘P’—to sacrifice, you most likely have a genuine scalable cluster. If not—forget about horizontal scaling and simply add capacity as the business blooms.&lt;/p&gt;&lt;p&gt;And while I have the opportunity, I can’t resist recommending my own library &lt;a href=&quot;https://hexdocs.pm/finitomata&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Finitomata&lt;/code&gt;&lt;/a&gt;, which I prototyped in &lt;a href=&quot;https://www.idris-lang.org/&quot;&gt;Idris&lt;/a&gt; and which is designed to be completely asynchronous (there is no way to determine from a response whether a state transition succeeded)—it provably prevents the programmer from violating a single law of finite state machine management.&lt;/p&gt;</content:encoded>
</item>
</channel>
</rss>
