<?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>Erlang Ecosystem Foundation - Supporting the BEAM community</title>
<link>https://erlef.org/blog/security/atom-exhaustion</link>
<guid isPermaLink="false">V7vRAJQXDdCFAH-UG1JD5BOx_PmQde2p1b5OHQ==</guid>
<pubDate>Thu, 28 May 2026 00:36:05 +0000</pubDate>
<description>Atom Exhaustion Is Not a Footgun. It&#39;s One Third of Our CVEs.</description>
<content:encoded>&lt;h2&gt;Atom Exhaustion Is Not a Footgun. It&amp;#39;s One Third of Our CVEs.&lt;/h2&gt;&lt;p&gt;
&lt;strong&gt;35.8% of CVEs published by the Erlang Ecosystem Foundation CNA fall into the
category of uncontrolled resource consumption.&lt;/strong&gt; In the BEAM ecosystem, a large
share of those are caused by one recurring issue: atom exhaustion. You can find
the current distribution on the EEF CNA’s &lt;a href=&quot;https://cna.erlef.org/common-weaknesses&quot;&gt;Common Weaknesses&lt;/a&gt;
page.&lt;/p&gt;&lt;p&gt;
Atom exhaustion is a denial-of-service vulnerability. Atoms are not garbage
collected and are stored in a global atom table, and once it fills up, the VM
crashes. Creating atoms from non-finite values, especially user-supplied input,
is therefore a latent DoS waiting to happen.&lt;/p&gt;&lt;p&gt;
This is not limited to obvious calls such as &lt;code class=&quot;inline&quot;&gt;binary_to_atom/1&lt;/code&gt;,
&lt;code class=&quot;inline&quot;&gt;list_to_atom/1&lt;/code&gt;, &lt;code class=&quot;inline&quot;&gt;String.to_atom/1&lt;/code&gt;, or &lt;code class=&quot;inline&quot;&gt;List.to_atom/1&lt;/code&gt;. Some dangerous
patterns are less obvious:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;erlang&quot;&gt;% Erlang: dynamic atom creation through interpolation
list_to_atom(&amp;quot;field_&amp;quot; ++ UserInput)&lt;/code&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code class=&quot;elixir&quot;&gt;# Elixir: decoding JSON with atom keys
Jason.decode(json, keys: :atoms)&lt;/code&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code class=&quot;elixir&quot;&gt;# Elixir: dynamic atom creation through interpolation
:&amp;quot;field_#{user_input}&amp;quot;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;
What makes this class of vulnerability persistent is not carelessness. It often
appears in code where the input was assumed to be controlled or finite. URI
schemes are a good example: it may feel like there are only a few schemes to
handle, but if the value comes from external input, the set is no longer
guaranteed to be finite.&lt;/p&gt;&lt;blockquote&gt;
  &lt;p&gt;
Creating atoms from input is unsafe unless the set of possible values is
finite, known, and enforced.  &lt;/p&gt;
&lt;/blockquote&gt;&lt;p&gt;
The safest approach is to avoid creating new atoms at runtime entirely. Prefer
explicit lookup tables when the accepted values are known:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;erlang&quot;&gt;% Erlang
case Scheme of
  &amp;lt;&amp;lt;&amp;quot;http&amp;quot;&amp;gt;&amp;gt;  -&amp;gt; http;
  &amp;lt;&amp;lt;&amp;quot;https&amp;quot;&amp;gt;&amp;gt; -&amp;gt; https;
  _           -&amp;gt; error
end&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;
When a lookup table is not practical, use the safer existing-atom variants,
which will raise an error instead of creating a new atom:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;erlang&quot;&gt;% Erlang
binary_to_existing_atom(Value)
list_to_existing_atom(Value)&lt;/code&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code class=&quot;elixir&quot;&gt;# Elixir
String.to_existing_atom(value)
List.to_existing_atom(value)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;
Linters can help catch these patterns before they become vulnerabilities. For
Elixir projects, consider enabling Credo’s
&lt;a href=&quot;https://hexdocs.pm/credo/Credo.Check.Warning.UnsafeToAtom.html&quot;&gt;&lt;code class=&quot;inline&quot;&gt;Credo.Check.Warning.UnsafeToAtom&lt;/code&gt;&lt;/a&gt;, which flags unsafe
calls to &lt;code class=&quot;inline&quot;&gt;String.to_atom/1&lt;/code&gt;, &lt;code class=&quot;inline&quot;&gt;List.to_atom/1&lt;/code&gt;, &lt;code class=&quot;inline&quot;&gt;Module.concat/1,2&lt;/code&gt;, and
&lt;code class=&quot;inline&quot;&gt;Jason.decode/2&lt;/code&gt; with &lt;code class=&quot;inline&quot;&gt;keys: :atoms&lt;/code&gt;. The check is disabled by default.&lt;/p&gt;&lt;p&gt;
If you maintain an Erlang or Elixir project, search your codebase for atom
creation from binaries, strings, JSON keys, URI components, headers, and
configuration values. This is one of the easiest vulnerability classes to fix
before it becomes a CVE.&lt;/p&gt;&lt;p&gt;
For more detailed guidance, see the EEF Security Working Group’s
&lt;a href=&quot;https://security.erlef.org/secure_coding_and_deployment_hardening/atom_exhaustion&quot;&gt;guide on preventing atom exhaustion&lt;/a&gt;.&lt;/p&gt;</content:encoded>
</item>
<item>
<title>Highest Random Weight in Elixir | jola.dev</title>
<link>https://jola.dev/posts/highest-random-weight-in-elixir</link>
<enclosure type="image/jpeg" length="0" url="https://jola.dev/images/og/posts/highest-random-weight-in-elixir.png"></enclosure>
<guid isPermaLink="false">8eKqqXRGCUwMI--e2BA3tsLTjE5cK-8DdK6CCg==</guid>
<pubDate>Sat, 23 May 2026 15:49:11 +0000</pubDate>
<description>A description of HRW/rendezvous hashing and the HRW elixir library.</description>
<content:encoded>&lt;p&gt;
Consistent hashing is a common building block for distributed Elixir and enables fairly low complexity and high value design patterns, like the distributed rate limiter or cache. I’ve written about it before.&lt;/p&gt;&lt;p&gt;
The most common way of assigning keys to nodes, ensuring that any node participating in the cluster can figure out which node owns the given key, is Discord’s &lt;a href=&quot;https://github.com/discord/ex_hash_ring&quot;&gt;ExHashRing&lt;/a&gt;. This is an incredibly battle-tested and reliable library with excellent performance characteristics, and I’ve only had good experiences with it.&lt;/p&gt;&lt;p&gt;
That said, it does have a downside. You have to start and manage the ring processes. It’s not a huge downside, you can give them global names and it’s trivial to look them up, but you still want them set up under your supervision tree and they are stateful persistent things that hang around. That state has to be managed. It’s not a big deal at all, but when I found a stateless alternative it did immediately catch my attention.&lt;/p&gt;&lt;h2&gt;
Rendezvous hashing&lt;/h2&gt;&lt;p&gt;
As described by the &lt;a href=&quot;https://en.wikipedia.org/wiki/Rendezvous_hashing&quot;&gt;Wikipedia page&lt;/a&gt;: &lt;em&gt;Rendezvous hashing is both much simpler and more general than consistent hashing.&lt;/em&gt; Also called HRW or Highest Random Weight&lt;em&gt;.&lt;/em&gt; In practice, you can use it very much like you would ExHashRing.&lt;/p&gt;&lt;p&gt;
&lt;code class=&quot;makeup ok&quot;&gt;ExHashRing&lt;/code&gt; example.&lt;/p&gt;&lt;p&gt;
HRW example.&lt;/p&gt;&lt;p&gt;
That’s it. No stateful process, no setup. Just pure functional programming with inputs and outputs. Consistent across multiple machines. Avoids unnecessary drift when changing the list of nodes. You can see why it caught my eye!&lt;/p&gt;&lt;p&gt;
There’s a downside of course. The big O notation for &lt;code class=&quot;makeup ok&quot;&gt;HRW.owner&lt;/code&gt; is linear (O(n)), or in other words, it doesn’t do well with larger lists of nodes. That’s definitely something to take into account when considering using it. But to be honest, looking back at the times I’ve used &lt;code class=&quot;makeup ok&quot;&gt;ExHashRing&lt;/code&gt; I’ve never had more than ~14 nodes to worry about. Here’s a comparison of how each algorithm does on my machine for 14 nodes.&lt;/p&gt;&lt;p&gt;
&lt;code class=&quot;makeup ok&quot;&gt;ExHashRing&lt;/code&gt; is extremely fast, and stays fast as the number of nodes grow. But at a smaller number of nodes, even on a fairly hot path, there’s not much difference here. You’re free to pick whichever one you think reads better.&lt;/p&gt;&lt;h2&gt;
Basic HRW algorithm&lt;/h2&gt;&lt;p&gt;
Let’s dig a bit deeper into rendezvous hashing. The basic implementation is actually incredibly small. What you want to do is apply a scoring function on the key together with each of the nodes separately and then return the highest value. Highest Random Weight. For a scoring function you can use any fast hashing function really. &lt;code class=&quot;makeup ok&quot;&gt;:erlang.phash2&lt;/code&gt; is an obvious candidate in the BEAM ecosystem.&lt;/p&gt;&lt;p&gt;
Here’s what that looks like.&lt;/p&gt;&lt;p&gt;
It’s pretty ingenious!&lt;/p&gt;&lt;h2&gt;
Linear growth&lt;/h2&gt;&lt;p&gt;
Just to demonstrate how that affects performance as &lt;code class=&quot;makeup ok&quot;&gt;nodes&lt;/code&gt; grows, here’s a benchmark run with 10K nodes. 4200x times slower than &lt;code class=&quot;makeup ok&quot;&gt;ExHashRing&lt;/code&gt;. Although to put things into perspective, it’s still just taking ~2 ms on my machine. Depending on your use case, that might actually be just fine. It’s hard to beat the convenience of a pure function.&lt;/p&gt;&lt;p&gt;
But let’s see if we can do better.&lt;/p&gt;&lt;h2&gt;
HRW skeleton&lt;/h2&gt;&lt;p&gt;
Our basic HRW implementation, although actually quite fast, doesn’t behave well as the number of nodes grows. This is because it, for every lookup, has to hash the key against every node. That same Wikipedia page &lt;a href=&quot;https://en.wikipedia.org/wiki/Rendezvous_hashing#O(log_n)_running_time_via_skeleton-based_hierarchical_rendezvous_hashing&quot;&gt;describes&lt;/a&gt; a way around that by arranging the nodes into an efficient data structure and bringing the big O notation of &lt;code class=&quot;makeup ok&quot;&gt;owner&lt;/code&gt; to O(log n).&lt;/p&gt;&lt;p&gt;
At a very (very) high level what we want to do is sort the list of nodes and then chunk them into clusters. Each cluster gets an address and instead of hashing the key against every node, we now just need to calculate the address of the cluster, and then we can hash the key against the nodes inside that cluster to find the correct one. This means significantly less effort, bringing us to a much nicer logarithmic complexity.&lt;/p&gt;&lt;p&gt;
Using it looks something like this.&lt;/p&gt;&lt;p&gt;
Running the same benchmark as above, but with the skeleton created in advance, just like we do for &lt;code class=&quot;makeup ok&quot;&gt;ExHashRing&lt;/code&gt;, this is what we get.&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;makeup ok&quot;&gt;##### With input D: 10_000 #####
Name                                ips        average  deviation         median         99th %
ExHashRing.Ring.find_node        2.17 M     0.00046 ms  ±1791.93%     0.00042 ms     0.00058 ms
HRW.owner (skeleton)             0.71 M     0.00141 ms   ±634.18%     0.00138 ms     0.00183 ms
HRW.owner                     0.00047 M        2.13 ms     ±5.03%        2.10 ms        2.53 ms

Comparison:
ExHashRing.Ring.find_node        2.17 M
HRW.owner (skeleton)             0.71 M - 3.06x slower +0.00095 ms
HRW.owner                     0.00047 M - 4615.43x slower +2.13 ms&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;
We’ve gone from 2 ms per lookup to 141 µs, only ~3x slower than &lt;code class=&quot;makeup ok&quot;&gt;ExHashRing&lt;/code&gt;, with no NIFs and no stateful processes to start up. We do have a struct we have to pass around now, and adding and removing nodes is no longer a stable operation. Adding a node pushes everything that comes after in the sorted list one slot over. I guess nothing in life is free. Still, this is an interesting tradeoff for a lot of use cases.&lt;/p&gt;&lt;h2&gt;
Distribution&lt;/h2&gt;&lt;p&gt;
The other thing you probably want to know about a mechanism for distributing work/keys/load across a set of nodes, is how well it distributes. It wouldn’t be very useful if every key maps to the same node. Here’s a little sample script that demonstrates the distribution.&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;makeup ok&quot;&gt;defmodule Distribution do
  def run do
    keys = Enum.map(1..100_000, fn i -&amp;gt; &amp;quot;key-#{i}&amp;quot; end)

    for n &amp;lt;- [10, 100, 1000] do
      nodes = Enum.map(1..n, &amp;amp;&amp;quot;node#{&amp;amp;1}&amp;quot;)
      ideal = div(length(keys), n)

      counts =
        keys
        |&amp;gt; Enum.map(&amp;amp;HRW.owner(&amp;amp;1, nodes))
        |&amp;gt; Enum.frequencies()
        |&amp;gt; Map.values()

      min_c = Enum.min(counts)
      max_c = Enum.max(counts)
      avg = Enum.sum(counts) / length(counts)
      stddev = :math.sqrt(Enum.sum(Enum.map(counts, fn c -&amp;gt; (c - avg) ** 2 end)) / length(counts))

      IO.puts(&amp;quot;#{n} nodes, #{length(keys)} keys (ideal #{ideal} per node):&amp;quot;)
      IO.puts(&amp;quot;  min: #{min_c}  max: #{max_c}  stddev: #{Float.round(stddev, 1)}  (#{Float.round(stddev/avg*100, 2)}% of mean)&amp;quot;)
    end
  end
end

Distribution.run()&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;
I extended that to add HRW with MurmurHash3, HRW with skeleton, and ExHashRing, for comparison.&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;makeup ok&quot;&gt;10 nodes, 100000 keys (ideal 10000 per node):
    phash2 (HRW)           min: 9691  max: 10639  stddev: 249.9  (2.5% of mean)
    murmur3 x86_32 (HRW)   min: 9859  max: 10192  stddev: 112.2  (1.12% of mean)
    murmur3 x64_128 (HRW)  min: 9864  max: 10170  stddev: 98.1   (0.98% of mean)
    HRW.Skeleton           min: 9691  max: 10639  stddev: 249.9  (2.5% of mean)
    ExHashRing             min: 9526  max: 10513  stddev: 338.5  (3.38% of mean)

  100 nodes, 100000 keys (ideal 1000 per node):
    phash2 (HRW)           min: 920   max: 1075   stddev: 29.7   (2.97% of mean)
    murmur3 x86_32 (HRW)   min: 934   max: 1059   stddev: 27.0   (2.7% of mean)
    murmur3 x64_128 (HRW)  min: 902   max: 1072   stddev: 29.2   (2.92% of mean)
    HRW.Skeleton           min: 877   max: 1124   stddev: 46.6   (4.66% of mean)
    ExHashRing             min: 105   max: 1229   stddev: 279.7  (27.97% of mean)

  1000 nodes, 100000 keys (ideal 100 per node):
    phash2 (HRW)           min: 69    max: 132    stddev: 9.9    (9.91% of mean)
    murmur3 x86_32 (HRW)   min: 72    max: 132    stddev: 9.6    (9.65% of mean)
    murmur3 x64_128 (HRW)  min: 67    max: 144    stddev: 9.8    (9.79% of mean)
    HRW.Skeleton           min: 72    max: 141    stddev: 9.9    (9.85% of mean)
    ExHashRing             min: 0     max: 147    stddev: 31.4   (31.42% of mean)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;
As you can see, we’re doing just fine with &lt;code class=&quot;makeup ok&quot;&gt;:erlang.phash2.&lt;/code&gt; Murmur3 is maybe slightly better at smaller node counts, but that’s not the big takeaway from here. It’s that &lt;code class=&quot;makeup ok&quot;&gt;ExHashRing&lt;/code&gt; is really struggling at larger node counts on the default settings. The solution is to add more vnodes, but that was unexpected to me!&lt;/p&gt;&lt;h2&gt;
Announcing HRW, the library&lt;/h2&gt;&lt;p&gt;
You’re very welcome to try out the &lt;code class=&quot;makeup ok&quot;&gt;hrw&lt;/code&gt; library on &lt;a href=&quot;http://hex.pm&quot;&gt;hex.pm&lt;/a&gt;, or why not take a look at the Github repository at &lt;a href=&quot;https://github.com/joladev/hrw&quot;&gt;https://github.com/joladev/hrw&lt;/a&gt;. For very large number of nodes, you’ll want to use &lt;code class=&quot;makeup ok&quot;&gt;ExHashRing&lt;/code&gt; or &lt;code class=&quot;makeup ok&quot;&gt;HRW.Skeleton&lt;/code&gt;, for anything else, why not stick with plain &lt;code class=&quot;makeup ok&quot;&gt;HRW.owner&lt;/code&gt;?&lt;/p&gt;&lt;p&gt;
The library comes with additional strategies not described here, like &lt;code class=&quot;makeup ok&quot;&gt;HRW.Weighted&lt;/code&gt; which lets you assign more key space to specific nodes, useful for heterogenous clusters where some machines are bigger, and &lt;code class=&quot;makeup ok&quot;&gt;HRW.Bounded&lt;/code&gt;, which gives you greater precision in how keys are distributed when you know the keys up front.&lt;/p&gt;&lt;p&gt;
Let me know how you find it.&lt;/p&gt;</content:encoded>
</item>
<item>
<title>Make Friends With Your AI Assistant</title>
<link>https://rocket-science.ru/hacking/2026/05/18/make-claude-robust</link>
<enclosure type="image/jpeg" length="0" url="https://rocket-science.ru/img/logo/logo-orig.png"></enclosure>
<guid isPermaLink="false">JMW_giP8FEk1my0-Dp3QtGo1x6rwIsTFl4NlcQ==</guid>
<pubDate>Mon, 18 May 2026 10:10:08 +0000</pubDate>
<description>Several rules from my experience which would make your cooperation with AI assistants a charm</description>
<content:encoded>&lt;h2&gt;Typography&lt;/h2&gt;&lt;p&gt;AI assistants do apparently love clean markers. I use ⓪ –⑳  markers for numbering, ▸/▹ markers for itemizing, typographically correct quotes (“”/‘’,) and m-dashes. Compose key makes entering that stuff easier, alternative layout makes it a charm. No tips for poor MacOS/Win users, sorry.&lt;/p&gt;&lt;h2&gt;Workflows&lt;/h2&gt;&lt;p&gt;I have system-wide workflows for my assistant. They all look like “START_WORD → WORKFLOW_RULES → STOP_WORD.”&lt;/p&gt;&lt;p&gt;Here is an example:&lt;/p&gt;&lt;blockquote&gt;
  &lt;p&gt;Let’s define the workflow you must follow every time we work on a new task, named NEW_TASK. The rules are following:&lt;/p&gt;

  &lt;p&gt;① The statement “New task &lt;task_name&gt;!&amp;quot; typed by me starts new NEW_TASK workflow&lt;br/&gt;
② Upon starting new workflow you must pull the main branch from github and create a new branch named &amp;quot;&lt;task_name&gt;&amp;quot;&lt;br/&gt;
③ At this point you have to wait for me to enter the problem statement&lt;br/&gt;
④ You have to analyze the problem statement and immediately ask all the questions regarding it where you might have had any uncertancy and/or lack of clarity&lt;br/&gt;
⑤ After everythig is clarified, you must come up with the plan of implementation and ask for its amendment and/or approval&lt;br/&gt;
⑥ Upon approval, you should start implementing it, asking all the questions you might have appeared&lt;br/&gt;
⑦ Once done, you must create a comprehensive tests for the new functionality, documentation in both the source code as standalone (markdown), changelog entry, and a brief what-has-been-done note in markdown&lt;br/&gt;
⑧ After I accept everything by saying &amp;quot;Task done!&amp;quot;, you should commit everything with a descriptive comprehensive commit message, push it to remote, and create a pull request in github.&lt;/task_name&gt;&lt;/task_name&gt;&lt;/p&gt;
&lt;/blockquote&gt;&lt;p&gt;The key insight here is the &lt;strong&gt;stop-words&lt;/strong&gt;. Without them the assistant will cheerfully barrel through all eight steps in one breath, commit untested code to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;main&lt;/code&gt;, and open a pull request titled “feat: everything.” Stop-words are the leash. Love the leash (and the muzzle, your assistant is ill-mannered and disobedient.)&lt;/p&gt;&lt;h2&gt;Context Is King (And Your Assistant Has Amnesia)&lt;/h2&gt;&lt;p&gt;Every new session your assistant is a freshly hatched duckling. It will imprint on whatever you show it first. Feed it your codebase conventions, your style preferences, your testing philosophy—and do it &lt;em&gt;persistently&lt;/em&gt;, through rules and system prompts, not through hopeful repetition in every chat.&lt;/p&gt;&lt;p&gt;The difference between a rule that says “always use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:json&lt;/code&gt; instead of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:jason&lt;/code&gt;” and manually correcting the same dependency in every generated &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mix.exs&lt;/code&gt; is the difference between engineering and penance.&lt;/p&gt;&lt;p&gt;Persistent rules are not optional. They are the civilizational layer between you and the primordial chaos of a context window that has never heard of your project. Each workflow (see above) might have different persistent rules attached.&lt;/p&gt;&lt;h2&gt;Atomic Tasks, or: Stop Writing Novels&lt;/h2&gt;&lt;p&gt;The temptation to say “implement the entire authentication system” is roughly as productive as asking an intern to “make the app work.” Break it down. Then break it down again. If your task description exceeds one screenful, you have written a novel, not a task. If within the task there are forks in the road possible, the assistant will inevitable choose the wrong path.&lt;/p&gt;&lt;p&gt;The assistant will cheerfully &lt;em&gt;attempt&lt;/em&gt; your novel and &lt;em&gt;wild guess&lt;/em&gt; the direction on each fork. It will produce something that compiles, passes zero tests, and makes you question your career choices. Whereas “add the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;verify_token/2&lt;/code&gt; function that pattern-matches on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{:ok, claims}&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{:error, reason}&lt;/code&gt;”—that it can do in its sleep. If models slept.&lt;/p&gt;&lt;p&gt;The rule of thumb: &lt;strong&gt;if there is more than one correct architectural path, the task is too large.&lt;/strong&gt; Decide the architecture yourself; delegate the typing.&lt;/p&gt;&lt;h2&gt;Feed It Good Code, Get Good Code&lt;/h2&gt;&lt;p&gt;This is not mysticism, it is pattern matching. If you feed your assistant examples of clean, idiomatic code, it will produce clean, idiomatic code. If you feed it your legacy spaghetti written at 3 AM during a production outage, you will get more spaghetti—but with AI-generated parmesan on top.&lt;/p&gt;&lt;p&gt;I keep a curated set of “golden” modules that I attached as context ages ago. The assistant does not need to be told “you are a senior Elixir architect with 300 years of experience.” It needs to &lt;em&gt;see&lt;/em&gt; what good Elixir looks like. Show, don’t pep-talk.&lt;/p&gt;&lt;h2&gt;Trust, But Verify (Especially the Tests)&lt;/h2&gt;&lt;p&gt;The cruelest irony of AI-assisted development: the assistant generates tests that pass. “Green across the board!” you exclaim, and deploy. Three hours later you discover the tests were testing that the function returns &lt;em&gt;something&lt;/em&gt;, not that it returns the &lt;em&gt;right&lt;/em&gt; something.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Read the generated tests more carefully than the implementation.&lt;/strong&gt; The implementation at least looks suspicious when it is wrong. A bad test looks &lt;em&gt;exactly&lt;/em&gt; like a good test, except it asserts nothing useful. Pattern-match on the actual values; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;assert result&lt;/code&gt; is not a test, it is a prayer.&lt;/p&gt;&lt;h2&gt;The Art of Saying “No, Try Again”&lt;/h2&gt;&lt;p&gt;Your assistant is not your therapist. It does not need encouragement. “This is wrong” followed by silence teaches it nothing. “This is wrong because the recursive clause does not handle the empty-list base case, see &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;List.foldl/3&lt;/code&gt; for the pattern I expect”—now you are getting somewhere. Keep your paw right above &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; combination and press it every time you see the assistant’s thinking process drives bonkers.&lt;/p&gt;&lt;p&gt;You always liked interrupting people in person, feel free to interrupt your deputy.&lt;/p&gt;&lt;h2&gt;Know When to Kill the Session&lt;/h2&gt;&lt;p&gt;If you have been going back and forth for more than three iterations on the same function, close the session. Start fresh. The context window is polluted with failed attempts, corrections, and mutual disappointment—a couples therapy transcript, essentially.&lt;/p&gt;&lt;p&gt;A new session with a clean description of what you &lt;em&gt;actually&lt;/em&gt; want will outperform the seventh round of “no, I meant the &lt;em&gt;other&lt;/em&gt; thing” every single time. Sunk cost fallacy applies to token budgets too.&lt;/p&gt;&lt;p&gt;Please aware, that the necessity to close a session vividly implies your original prompt sucked in the first place. Rewrite it from scratch. Make sure it has no freedom for the assistant to choose paths. They are good at walking the straight roads.&lt;/p&gt;&lt;h2&gt;The Checklist&lt;/h2&gt;&lt;p&gt;Distilled to a grocery list for the impatient:&lt;/p&gt;&lt;p&gt;▸ Use persistent rules, not per-session rituals&lt;br/&gt;
▸ Define workflows with explicit start/stop words&lt;br/&gt;
▸ One architectural decision per task, maximum&lt;br/&gt;
▸ Attach good code as examples, skip the motivational preamble&lt;br/&gt;
▸ Run the full validation pipeline (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;format&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;credo&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dialyzer&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;test&lt;/code&gt;) after every step, not at the end&lt;br/&gt;
▸ Read generated tests as if they were written by someone who wants to trick you (they were not, but the effect is the same)&lt;br/&gt;
▸ Three failed iterations = kill the session, rethink the task&lt;br/&gt;
▸ Never let the assistant commit without your explicit say-so&lt;br/&gt;
▸ If you could not solve the problem yourself, the assistant cannot solve it &lt;em&gt;for&lt;/em&gt; you—it can only type faster&lt;/p&gt;&lt;p&gt;The last point deserves a tattoo. An AI assistant is a force multiplier, not a force. Multiply zero by anything you like.&lt;/p&gt;&lt;p&gt;Happy prompting.&lt;/p&gt;</content:encoded>
</item>
<item>
<title>Mocks Are Your Friends, Not Your Servants</title>
<link>https://rocket-science.ru/hacking/2026/05/17/mocks-are-your-friends</link>
<enclosure type="image/jpeg" length="0" url="https://rocket-science.ru/img/logo/logo-orig.png"></enclosure>
<guid isPermaLink="false">24nWcNfiDTqnapHrTllRG-SjwLMwHSxIpCUCdA==</guid>
<pubDate>Sun, 17 May 2026 18:03:59 +0000</pubDate>
<description>On treating mocks as protagonists of the testing narrative rather than disposable stand-ins hired to nod along</description>
<content:encoded>&lt;p&gt;There is a peculiar tradition in our industry: the moment someone says “mock,” half the room recoils as if you have just proposed replacing the database with a spreadsheet. The other half nods enthusiastically, having already replaced the database with a spreadsheet.&lt;/p&gt;&lt;p&gt;Both camps are wrong, and for the same reason: they think of “mock” as a verb.&lt;/p&gt;&lt;h2&gt;Mock Is a Noun&lt;/h2&gt;&lt;p&gt;José Valim wrote a &lt;a href=&quot;https://dashbit.co/blog/mocks-and-explicit-contracts&quot;&gt;splendid piece on the subject&lt;/a&gt; some years ago, and yet the industry continues to collectively ignore its central thesis with the dedication of a cat ignoring an expensive toy. The key insight is disarmingly simple: &lt;strong&gt;consider “mock” to be a noun, never a verb.&lt;/strong&gt; You do not “mock a module.” You create a mock—an entity, a collaborator, a contract participant—that implements a well-defined behaviour.&lt;/p&gt;&lt;p&gt;The difference is not cosmetic. When you “mock” a function (verb), you are lying to your test. You are saying: “pretend this hole in the wall is a door.” When you create a mock (noun), you are building an actual door—one that opens and closes according to the same interface contract as the production door, just perhaps into a smaller room.&lt;/p&gt;&lt;h2&gt;Stubs Are for Candles, Not for Software&lt;/h2&gt;&lt;p&gt;The prevailing use of mocks in the wild amounts to: “I need this function to return 42 so my other function does not crash.” This is not testing. This is bribery. You have paid off a witness to give the testimony you wanted, and now you are surprised when the real trial goes sideways.&lt;/p&gt;&lt;p&gt;A mock that merely returns canned responses is a stub wearing a fake moustache. It tells you nothing about whether your code actually &lt;em&gt;interoperates&lt;/em&gt; with the expected behaviour. It only tells you that, given a universe where everything behaves exactly as you imagined, your code does not crash. Congratulations—that was never the hard part.&lt;/p&gt;&lt;p&gt;The hard part is the contract. Does your code send the right messages? Does it handle the responses defined by the behaviour? Does it &lt;em&gt;respect the protocol&lt;/em&gt;, not just survive it? A proper mock enforces these questions. A stub sweeps them under the carpet and charges you for the cleaning.&lt;/p&gt;&lt;h2&gt;Promote Your Mocks to Lead Actors&lt;/h2&gt;&lt;p&gt;Here is the heresy: mocks should not sit quietly in the corner of your test, responding when spoken to like well-trained waitstaff. They should be protagonists. They should &lt;em&gt;drive&lt;/em&gt; the testing narrative.&lt;/p&gt;&lt;p&gt;Consider a finite state machine. It transitions through states, fires callbacks, notifies listeners. In production, the listener might persist data, send emails, ring bells. In tests, you do not care about the bells. You care about: did the FSM reach state X with payload Y after event Z?&lt;/p&gt;&lt;p&gt;A mock-as-protagonist answers this question directly. It is not merely absorbing calls—it is &lt;em&gt;reporting back&lt;/em&gt; to the test, asserting that the system did what it promised. The mock becomes your embedded journalist, filing reports from inside the process under test.&lt;/p&gt;&lt;p&gt;This is a fundamentally different posture. The mock is no longer a shift worker you hire to stand in for the real employee. It is a first-class participant in the test, with its own responsibilities, its own assertions, its own voice.&lt;/p&gt;&lt;h2&gt;OTP, Race Conditions, and the Debugger You Deserve&lt;/h2&gt;&lt;p&gt;Anyone who has tested concurrent OTP systems knows the special joy of a test that passes ninety-nine times and fails on the hundredth, always at random time, always on CI, never on your machine. The core problem is structural: you fire an asynchronous message and then try to assert about a state that may or may not have been reached yet. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Process.sleep(200)&lt;/code&gt; is not a solution—it is a prayer in milliseconds.&lt;/p&gt;&lt;p&gt;Mocks offer something better. When a mock is registered as a listener, every state transition sends a message back to the test process. You can then use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;assert_receive&lt;/code&gt; to &lt;em&gt;wait&lt;/em&gt; for the transition, with a timeout, deterministically. No sleep. No prayer. No coin flip.&lt;/p&gt;&lt;p&gt;In effect, the mock becomes a breakpoint in a debugger you never had to open. It declares: “I expect to be called with &lt;em&gt;these&lt;/em&gt; arguments, in &lt;em&gt;this&lt;/em&gt; order,” and it sends a message to the test process confirming each call. You get both the expectation (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;expect/3&lt;/code&gt;) and the synchronization (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;assert_receive/2&lt;/code&gt;) in one mechanism. The mock is simultaneously the probe and the signal.&lt;/p&gt;&lt;p&gt;This is not theoretical. In OTP, where processes communicate via message passing and the scheduler is free to interleave execution in whatever order amuses it most, this pattern transforms flaky tests into deterministic ones. You are no longer guessing when the process reached the desired state. You are being &lt;em&gt;told&lt;/em&gt;.&lt;/p&gt;&lt;h2&gt;Listeners, Visibility, and the Joy of Seeing What Happens&lt;/h2&gt;&lt;p&gt;There is a secondary benefit that deserves its own paragraph: visibility.&lt;/p&gt;&lt;p&gt;When you plug a mock in as a listener—not as a replacement for an implementation, but as an &lt;em&gt;observer&lt;/em&gt; of one—the test code becomes radically more readable. Each assertion is a statement about what the system &lt;em&gt;did&lt;/em&gt;, not about what you had to simulate. The reader does not need to mentally reconstruct the production flow from a pile of stubs. The flow is right there, reported by the mock, step by step.&lt;/p&gt;&lt;p&gt;Test code that reads like a narrative of actual system behaviour is test code that people trust. And test code that people trust is test code that people maintain. And test code that people maintain is test code that catches bugs. The chain of causation is longer than a Dickensian sentence, but every link holds.&lt;/p&gt;&lt;h2&gt;Finitomata: A Case Study in Mock-Driven Testing&lt;/h2&gt;&lt;p&gt;All of this is not armchair philosophy. The &lt;a href=&quot;https://github.com/am-kantox/finitomata/blob/main/lib/finitomata/test/ex_unit.ex&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Finitomata.ExUnit&lt;/code&gt;&lt;/a&gt; module is a working testing framework built entirely on this premise. It uses &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Mox&lt;/code&gt; not as a crutch but as the foundation.&lt;/p&gt;&lt;p&gt;The setup declares a mock listener. The mock is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;allow&lt;/code&gt;-ed to the FSM process and given &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;expect&lt;/code&gt;ations for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;after_transition/3&lt;/code&gt; callbacks. Each expectation sends &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{:on_transition, id, state, payload}&lt;/code&gt; back to the test process. The test then walks the FSM through its transitions and asserts each state deterministically via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;assert_receive&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;The result looks like this:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;test_path &amp;quot;respectful passenger&amp;quot;, %{passengers: initial_passengers} do
  :coin_in -&amp;gt;
    assert_state :opened do
      assert_payload do
        data.passengers ~&amp;gt; ^initial_passengers
      end
    end

  :walk_in -&amp;gt;
    assert_state :closed do
      assert_payload do
        data.passengers ~&amp;gt; one_more when one_more == 1 + data.passengers
      end
    end

  :switch_off -&amp;gt;
    assert_state :switched_off
    assert_state :*
end&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;No &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Process.sleep&lt;/code&gt;. No polling. No race conditions. Each transition is confirmed by the mock-listener before the test proceeds. And crucially: &lt;em&gt;intermediate&lt;/em&gt; states—including those triggered by automatic (bang!) transitions that the test never explicitly fires—are observable and assertable, because the mock reports every single one.&lt;/p&gt;&lt;p&gt;The mock here is not a stand-in. It is the entire testing infrastructure. Remove it, and you are back to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Process.sleep(200)&lt;/code&gt; and crossed fingers.&lt;/p&gt;&lt;h2&gt;The Moral&lt;/h2&gt;&lt;p&gt;Mocks have suffered from a branding problem. They were introduced to the mainstream as “a way to avoid calling the real thing,” which is roughly equivalent to introducing a violin as “a way to avoid silence.” Technically correct, but missing the point so thoroughly that it constitutes its own genre of wrongness.&lt;/p&gt;&lt;p&gt;A mock is a collaborator. A contract enforcer. A synchronisation primitive. A visibility layer. A debugger breakpoint. A protagonist.&lt;/p&gt;&lt;p&gt;Treat it accordingly, and your tests will repay you with the one thing no amount of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Process.sleep&lt;/code&gt; can buy: confidence.&lt;/p&gt;</content:encoded>
</item>
<item>
<title>Running local models on an M4 with 24GB memory | jola.dev</title>
<link>https://jola.dev/posts/running-local-models-on-m4</link>
<enclosure type="image/jpeg" length="0" url="https://jola.dev/images/og-image-2b7872671fc7c11e464dac899d8d3068.png?vsn=d"></enclosure>
<guid isPermaLink="false">eciczdkFiUmNnZYmmB-tlkiY4-avgJJsf1_MrQ==</guid>
<pubDate>Tue, 12 May 2026 02:29:47 +0000</pubDate>
<description>Experiments with getting usable outputs out of local models on a standard Macbook</description>
<content:encoded>&lt;p&gt;
I’ve been experimenting with running local models on and off for a bit and I’ve finally found a setup that seems to work reasonably. It’s nothing like the output of a SOTA model, but the excitement of being able to have a local model do basic tasks, research, and planning, more than makes up for it! No internet connection required! Not to mention that it’s a way of reducing your dependence on big US tech, even if just a tiny bit.&lt;/p&gt;&lt;p&gt;
I gotta say though, it’s not easy to get this stuff set up. First you have to choose how you’re running the model: &lt;a href=&quot;https://ollama.com/&quot;&gt;Ollama&lt;/a&gt;, &lt;a href=&quot;https://github.com/ggml-org/llama.cpp&quot;&gt;llama.cpp&lt;/a&gt; or &lt;a href=&quot;https://lmstudio.ai/&quot;&gt;LM Studio&lt;/a&gt;. Each one comes with its own quirks and limitations, and they don’t offer all the same models. Then of course, you have to pick your model. You want the best model available that fits in memory and still gives you enough headroom to run your regular assortment of Electron apps, not to mention something where you can have at least a 64K context window, but ideally 128K or more. Most recently I’ve tried Qwen 3.6 Q3, GPT-OSS 20B, Devstral Small 24B, which all technically fit in memory but were in practice unusable, and Gemma 4B that would run fine but really struggle with tool use.&lt;/p&gt;&lt;p&gt;
Then there’s a plethora of configuration options to tweak. From the more well-known, like temperature, to more esoteric options like K Cache Quantization Type. Many of these tools come with a basic recommended set of options, but the appropriate ones can depend on things like whether you’re enabling thinking or not!&lt;/p&gt;&lt;h2&gt;
Qwen 3.5-9B (4b quant)&lt;/h2&gt;&lt;p&gt;
&lt;code class=&quot;makeup ok&quot;&gt;qwen3.5-9b@q4_k_s&lt;/code&gt; (&lt;a href=&quot;https://huggingface.co/unsloth/Qwen3.5-9B-GGUF&quot;&gt;HuggingFace link&lt;/a&gt;) is the best model I’ve gotten working with a reasonable ~40 tokens per second, thinking enabled, successful tool use, and a 128K context window, running on LM Studio. Compared to a SOTA model, it gets distracted more easily, sometimes it gets stuck in loops, it’ll misinterpret asks etc. But it’s surprisingly good for something that can run on a 24GB Macbook Pro while leaving space for lots of other things running too!&lt;/p&gt;&lt;p&gt;
These are the recommended settings for thinking mode and coding work:&lt;/p&gt;&lt;blockquote&gt;
  &lt;p&gt;
Thinking mode for precise coding tasks (e.g., WebDev):  &lt;/p&gt;
  &lt;p&gt;
temperature=0.6, top_p=0.95, top_k=20, min_p=0.0, presence_penalty=0.0, repetition_penalty=1.0  &lt;/p&gt;
&lt;/blockquote&gt;&lt;p&gt;
To enable thinking I also had to select the model, go to configuration, scroll to the bottom of the Inference tab, and add &lt;code class=&quot;makeup ok&quot;&gt;{%- set enable_thinking = true %}&lt;/code&gt; to the Prompt Template.&lt;/p&gt;&lt;p&gt;
I’ve been using it through both &lt;a href=&quot;https://pi.dev/&quot;&gt;pi&lt;/a&gt; and &lt;a href=&quot;https://opencode.ai/&quot;&gt;OpenCode&lt;/a&gt;. I still haven’t quite made my mind up on with one I prefer. Pi feels a bit snappier, but although I really appreciate the idea of the harness building itself and all that customization, I can’t help but wish it came with some sensible defaults. I feel like you could easily end up spending more time tweaking your pi set up to be just right, than you do on your actual projects!&lt;/p&gt;&lt;h2&gt;
Pi setup&lt;/h2&gt;&lt;p&gt;
Here’s the &lt;code class=&quot;makeup ok&quot;&gt;~/.pi/agent/models.json&lt;/code&gt;:&lt;/p&gt;&lt;h2&gt;
OpenCode setup&lt;/h2&gt;&lt;p&gt;
&lt;code class=&quot;makeup ok&quot;&gt;~/.config/opencode/opencode.json&lt;/code&gt;&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;makeup ok&quot;&gt;{
  &amp;quot;$schema&amp;quot;: &amp;quot;https://opencode.ai/config.json&amp;quot;,
  &amp;quot;provider&amp;quot;: {
    &amp;quot;lmstudio&amp;quot;: {
      &amp;quot;npm&amp;quot;: &amp;quot;@ai-sdk/openai-compatible&amp;quot;,
      &amp;quot;name&amp;quot;: &amp;quot;LM Studio (local)&amp;quot;,
      &amp;quot;options&amp;quot;: {
        &amp;quot;baseURL&amp;quot;: &amp;quot;http://127.0.0.1:1234/v1&amp;quot;
      },
      &amp;quot;models&amp;quot;: {
        &amp;quot;qwen3.5-9b@q4_k_s&amp;quot;: {
          &amp;quot;name&amp;quot;: &amp;quot;Qwen 3.5 9B Q4_K_S&amp;quot;,
          &amp;quot;tools&amp;quot;: true,
          &amp;quot;context_length&amp;quot;: 131072,
          &amp;quot;max_tokens&amp;quot;: 32768
        }
      }
    }
  },
  &amp;quot;model&amp;quot;: &amp;quot;lmstudio/qwen3.5-9b@q4_k_s&amp;quot;
}&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;
Comparison with SOTA models&lt;/h2&gt;&lt;p&gt;
A model like Qwen 3.5 9B (Q4) is nowhere near being able to independently solve complex problems over longer period of time, like SOTA models are. There’s no point in asking it to build an entire app in one go, you’ll just burn your thighs without getting anything out of it.&lt;/p&gt;&lt;p&gt;
What does work is a more interactive workflow where you’re clearly communicating with the model step by step, and giving it a lot of guidance. I’m sure that sounds pointless to many of you, why use a model where you have to babysit it as it works, but I actually found that it encouraged me to be more engaged. The downside to working with SOTA models is that they make it too easy to offload all cognitive effort, even where you’re trying to actively prevent that from happening.&lt;/p&gt;&lt;p&gt;
With the local modal I have to take on a lot more of the thinking and planning, I have to be a lot more specific, but it will still act as a research assistant, a rubber duck, and a savant with instant recall of a lot of programming language details and command line evocations. It’s not the 10x productivity boost that the big AI companies market, but it’s something, and it’s interesting.&lt;/p&gt;&lt;p&gt;
To give you an idea of some things that work and don’t, here are some very basic examples.&lt;/p&gt;&lt;h3&gt;
Example 1:&lt;/h3&gt;&lt;p&gt;
I wanted to bump the Elixir linter &lt;code class=&quot;makeup ok&quot;&gt;credo&lt;/code&gt; to the latest version and I got warnings out of my code. As an experiment, I asked Qwen to take a look.&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;makeup ok&quot;&gt;me:
  run mix credo --strict and suggest how to solve it, don&amp;#39;t edit

Qwen:
    Credo Warnings
    There are 4 warnings in your test files. The issue is using length/1 to check if lists are non-empty. Credo recommends comparing against an empty list instead.
    Warnings:
    1. test/jola_dev_web/controllers/blog_controller_test.exs:11
       - assert length(conn.assigns.posts) &amp;gt; 0
    2. test/jola_dev_web/controllers/blog_controller_test.exs:22
       - assert length(conn.assigns.posts) &amp;gt; 0
    3. test/jola_dev/blog_test.exs:10
       - assert length(posts) &amp;gt; 0
    4. test/jola_dev/blog_test.exs:23
       - assert length(tags) &amp;gt; 0
    How to Fix
    Replace each length(list) &amp;gt; 0 with list != []:
    blog_controller_test.exs:
    # Line 11
    assert conn.assigns.posts != []
    # Line 22
    assert conn.assigns.posts != []
    blog_test.exs:
    # Line 10
    assert posts != []
    # Line 23
    assert tags != []
    This is more idiomatic in Elixir and avoids the overhead of computing the list length.&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;
I asked Qwen to make the edits and it did 4 parallel edits, very clean. Overall this was a very simple task that I could’ve done myself by tabbing back and forth between the terminal and my editor to get the line numbers to edit. Not a big difference in effort, but nice and convenient.&lt;/p&gt;&lt;h3&gt;
Example 2:&lt;/h3&gt;&lt;p&gt;
After some dependency bumps I had a dependabot PR with git conflicts that dependabot for some reason was refusing to rebase. I pulled it down, rebased, and asked Qwen to take a look. It was a very simple conflict to resolve, just take the newer version of each, and the model recognized that.&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;makeup ok&quot;&gt;I can see the conflict. Here&amp;#39;s what&amp;#39;s happening:
HEAD (your current branch):
- sentry: 13.0.1
- tailwind: 0.3.1
Other side (commit being rebased):
- sentry: 12.0.3
- tailwind: 0.4.1
You have divergent dependency bumps. Here are your options:
Option 1: Take both updates (recommended)
- Keep sentry 13.0.1 from HEAD
- Keep tailwind 0.4.1 from the other side
- This gives you the latest of both deps
Option 2: Take only sentry update
- Keep sentry 13.0.1
- Keep tailwind 0.3.1
Option 3: Take only tailwind update
- Keep sentry 12.0.3
- Keep tailwind 0.4.1
Which approach do you want to take?&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;
However, when asked to make the change, it forgot to make the edit and instead just tried to add the changes and continue the rebase, with the conflict markers still in place.&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;makeup ok&quot;&gt;&amp;gt; Thinking: The user wants to take both updates - sentry 13.0.1 and tailwind 0.4.1. I need to resolve the conflict by keeping the HEAD version for sentry and the other side version for tailwind. I&amp;#39;ll use git add to stage the resolved file.
----
# Stage resolved mix.lock and continue rebase
$ git add mix.lock &amp;amp;&amp;amp; git rebase --continue
Vim: Warning: Output is not to a terminal
Vim: Warning: Input is not from a terminal
&amp;quot;~/workspace/mixtli/.git/COMMIT_EDITMSG&amp;quot; 32L, 1047BBump tailwind from 0.3.1 to 0.4.1
Bumps [tailwind](https://github.com/phoenixframework/tailwind) from 0.3.1 to 0.44.1.
- [Changelog](https://github.com/phoenixframework/tailwind/blob/main/CHANGELOG.mmd)
- [Commits](https://github.com/phoenixframework/tailwind/compare/v0.3.1...v0.4.11)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;
Additionally it failed to recognize that &lt;code class=&quot;makeup ok&quot;&gt;git rebase --continue&lt;/code&gt; opens an editor, and OpenCode then proceeded to hang, although that might just have been a one-off.&lt;/p&gt;&lt;h2&gt;
Conclusion&lt;/h2&gt;&lt;p&gt;
Anyway, LLMs are incredibly flexible and there’s a million things even a local model could do. Your imagination is the limit. Local models have serious tradeoffs, but they come with some pretty attractive benefits:&lt;/p&gt;&lt;ul&gt;
  &lt;li&gt;
No internet connection required, you can work on the plane!  &lt;/li&gt;
  &lt;li&gt;
The cost is limited to the electricity you’re using, assuming you were gonna buy a computer anyway. No subscription required.  &lt;/li&gt;
  &lt;li&gt;
There’s still going to be a serious environmental cost of training these models, but the open model companies are nowhere near the top of the list in environmental impact, and using your own hardware means less data centers.  &lt;/li&gt;
  &lt;li&gt;
It’s fun to tinker.  &lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;
LLMs have had a huge impact on our world, and much of it not great, but it’s obvious that they’re here to stay. Experimenting with local models feels like a more sustainable and positive way to interact with this technology. And honestly, it’s a lot of fun, even when it does the wrong thing!&lt;/p&gt;</content:encoded>
</item>
<item>
<title>Elixir versus Python for Data Science - DockYard</title>
<link>https://dockyard.com//blog/2022/07/12/elixir-versus-python-for-data-science</link>
<enclosure type="image/jpeg" length="0" url="https://assets.dockyard.com/images/Elixir versus Python.jpg"></enclosure>
<guid isPermaLink="false">EFhYYmahrkt-Hhp6iYnXXHCHNEbDNgZPOnXdYw==</guid>
<pubDate>Fri, 08 May 2026 19:20:08 +0000</pubDate>
<description>Machine learning capabilities with Elixir have expanded significantly in recent years. Sean Moriarity discusses some of those developments and compares Elixir&#39;s machine learning abilities with Python&#39;s.</description>
<content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;&lt;p&gt;Over the last year, thanks to the efforts of the amazing Elixir community, the Elixir &lt;a href=&quot;https://dockyard.com/blog/2022/05/10/deploying-machine-learning-models-with-elixir&quot;&gt;machine learning&lt;/a&gt; ecosystem has grown at an impressive rate, with more and more libraries filling more and more gaps in the Elixir data science and machine learning ecosystem.&lt;/p&gt;&lt;p&gt;A common argument against using Nx for a new &lt;a href=&quot;https://dockyard.com/blog/2022/06/21/what-is-machine-learning-anyway&quot;&gt;machine learning&lt;/a&gt; project is its perceived lack of a library/support for some common task that is available in Python. In this post, I&amp;#39;ll do my best to highlight areas where this is &lt;strong&gt;not&lt;/strong&gt; the case, and compare and contrast Elixir projects with their Python equivalents. Additionally, I&amp;#39;ll discuss areas where the Elixir ecosystem still comes up short, and using Nx for a new project might not be the best idea.&lt;/p&gt;&lt;h2&gt;Numerical Computing&lt;/h2&gt;&lt;p&gt;The obvious place to start this post is to compare Nx to its Python equivalents. At its core, Nx is intended to serve as a &lt;a href=&quot;https://numpy.org&quot;&gt;NumPy&lt;/a&gt; equivalent with support for automatic differentiation and acceleration via GPUs. In this respect, its main inspiration is &lt;a href=&quot;https://github.com/google/jax&quot;&gt;JAX&lt;/a&gt;—a Python library that supports automatic differentiation and JIT compilation to accelerators via composable function transformations.&lt;/p&gt;&lt;p&gt;The Nx API is (intentionally) considerably smaller than the NumPy API. Because Nx relies on JIT compilation, the API builds around a smaller amount of powerful primitive operations which &lt;a href=&quot;https://dockyard.com/blog/2021/04/08/up-and-running-nx&quot;&gt;can be used to build out more complex functions&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;NumPy does not have the same luxury, instead needing to rely on specialized implementations for most of the functions in its API. JAX also builds on a set of core primitive operations; however, they intentionally provide wrappers around the NumPy API due to its ubiquity in numerical computing community.&lt;/p&gt;&lt;p&gt;There are pros and cons to having a smaller API. From a learning perspective, a beginner can reasonably pick up and understand 90% of the functions in the Nx API rather quickly.&lt;/p&gt;&lt;p&gt;Unfortunately, this trade-off means Nx implementations can at times be more verbose than their NumPy counterparts. Due to the sheer size of the API, there are often times when a few lines of NumPy translate to considerably more lines in Nx. Additionally, the Nx API falls short of the NumPy API in some areas. For example, Nx PRNG support is not as feature complete as JAX/NumPy, the Nx Linear Algebra module is not as in-depth as NumPy, and Nx does not have support for string data types.&lt;/p&gt;&lt;p&gt;The API shortcomings of Nx are mostly active areas of work. Even with these shortcomings, I&amp;#39;ve found I can be just as productive writing Nx as I can writing NumPy.&lt;/p&gt;&lt;p&gt;From a performance perspective, if you&amp;#39;re using the &lt;a href=&quot;https://github.com/elixir-nx/nx/&quot;&gt;EXLA&lt;/a&gt; compiler, Nx will have (mostly) equivalent performance to JAX. Nx/EXLA relies on the same JIT compiler as JAX in XLA. That means that essentially all of the areas that JAX beats NumPy, Nx will also beat NumPy; and in all of the areas that NumPy beats JAX, NumPy will also beat Nx.&lt;/p&gt;&lt;p&gt;One advantage that Nx has over JAX is its first-class support for pluggable compilers and backends. While the JAX project seems to be moving in the direction of supporting multiple pluggable compilers/runtimes, &lt;a href=&quot;https://dockyard.com/blog/2022/03/15/nx-for-absolute-beginners&quot;&gt;Nx was built with this flexibility in mind&lt;/a&gt;, and thus is positioned for rapid integration with any existing/future tensor compilers and backends.&lt;/p&gt;&lt;p&gt;JAX is ahead in terms of parallelization; however, there are plans to integrate parallel primitives into the Nx API on the roadmap. Given that Nx can build on the same parallelism/sharding implementations as JAX in XLA, Nx can catch up to JAX relatively quickly in this respect.&lt;/p&gt;&lt;h2&gt;Deep Learning&lt;/h2&gt;&lt;p&gt;One of the initial ambitions for the Nx project was to support creating and training deep-learning-type models in Elixir. This is now possible with the &lt;a href=&quot;https://github.com/elixir-nx/axon&quot;&gt;Axon&lt;/a&gt; library.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://dockyard.com/blog/2022/01/11/getting-started-with-axon&quot;&gt;Axon is built directly on top of Nx&lt;/a&gt; and thus can take advantage of all of the things Nx offers, including JIT compilation and automatic differentiation. Axon most directly compares to tools like &lt;a href=&quot;https://pytorch.org&quot;&gt;PyTorch&lt;/a&gt; and &lt;a href=&quot;https://tensorflow.org&quot;&gt;TensorFlow&lt;/a&gt;/&lt;a href=&quot;https://keras.io&quot;&gt;Keras&lt;/a&gt; in the Python ecosystem.&lt;/p&gt;&lt;p&gt;From an API perspective, Axon is somewhat even with both PyTorch and TensorFlow/Keras. Aside from Attention/Transformer layers (which are on the roadmap), Axon has an essentially identical offering of model building blocks. Additionally, with its custom layer API, creating and using new layers is as easy as defining an Nx implementation of the layer. More or less any model you can create and perform inference in PyTorch/TensorFlow, you can also create and perform inference in with Axon.&lt;/p&gt;&lt;p&gt;Axon also has a robust training API inspired by libraries in the Python ecosystem such as &lt;a href=&quot;https://pytorch.org/ignite/index.html&quot;&gt;PyTorch Ignite&lt;/a&gt; and &lt;a href=&quot;https://www.pytorchlightning.ai/&quot;&gt;PyTorch Lightning&lt;/a&gt;. The training API supports out-of-the-box callbacks such as model checkpoints, early stopping, and model validation, as well as an API for integrating custom callbacks. Similar to Keras, the Axon training API offers increasing levels of flexibility at increasing levels of complexity. In other words, you can sacrifice simplicity to have more control over training your models.&lt;/p&gt;&lt;p&gt;One area of concern when migrating to Elixir is the ability to make use of pre-trained models. Thanks to &lt;a href=&quot;https://github.com/elixir-nx/axon_onnx&quot;&gt;AxonOnnx&lt;/a&gt; this is possible for (almost) any model you might have.&lt;/p&gt;&lt;p&gt;If you&amp;#39;re able to export an ONNX version of your model (e.g using &lt;code class=&quot;py-px text-sm leading-6 rounded&quot;&gt;torch.onnx&lt;/code&gt; or &lt;a href=&quot;https://github.com/onnx/tensorflow-onnx&quot;&gt;tf2onnx&lt;/a&gt;), you can probably import your model with Axon. AxonOnnx has even been tested to work with pre-trained transformers from the popular &lt;a href=&quot;https://huggingface.co/transformers&quot;&gt;transformers&lt;/a&gt; library.&lt;/p&gt;&lt;p&gt;While Axon supports importing pre-trained models, there are still some aspects of working with pre-trained models that need ironing out. For example, fine-tuning, while possible, does not yet have a first-class API in Axon. Additionally, features such as mixed-precision and multi-device training that make training large models possible are not 100% supported in Axon yet.&lt;/p&gt;&lt;h2&gt;Traditional Machine Learning&lt;/h2&gt;&lt;p&gt;Along with deep learning, gradient boosting and decision tree algorithms are perhaps the most popular machine learning algorithms in use. These classes of algorithms typically outperform deep learning with tabular and time-series data, and are often significantly less expensive to train and deploy.&lt;/p&gt;&lt;p&gt;Unfortunately, this is an area still under active development in the Elixir ecosystem. Python has popular libraries such as &lt;a href=&quot;https://xgboost.readthedocs.io/en/stable/&quot;&gt;XGBoost&lt;/a&gt;, but there is still no Elixir equivalent. I expect this to change over the next six months; however, for the time being, Elixir is behind in this area.&lt;/p&gt;&lt;p&gt;Elixir also falls short of Python in other traditional machine learning applications. While Python has the excellent &lt;a href=&quot;https://scikit-learn.org&quot;&gt;scikit-learn&lt;/a&gt;, Elixir has the relatively new &lt;a href=&quot;https://github.com/elixir-nx/scholar&quot;&gt;Scholar&lt;/a&gt; library. Because Scholar is new, it&amp;#39;s lacking in features that allow it to be a competitive alternative to sklearn. This is another area of active development on the Nx roadmap, and thus I expect things to look significantly different here in the next six months.&lt;/p&gt;&lt;h2&gt;Data Analysis&lt;/h2&gt;&lt;p&gt;Essentially any data scientist that has worked with Python for any amount of time is familiar with the &lt;a href=&quot;https://pandas.pydata.org/&quot;&gt;pandas&lt;/a&gt; library for data analysis. Pandas is a library for working with structured, columnar data. It&amp;#39;s popular as a library for any sort of analysis or munging tasks you might need to perform. The Elixir equivalent to Pandas is &lt;a href=&quot;https://github.com/elixir-nx/explorer&quot;&gt;Explorer&lt;/a&gt;. Explorer is built on top of the &lt;a href=&quot;https://docs.rs/polars/latest/polars/&quot;&gt;polars&lt;/a&gt; library which implements DataFrames in Rust.&lt;/p&gt;&lt;p&gt;From an API perspective, the Explorer API is different from what you might be used to using with Python. Given that Elixir is a functional language, the Explorer library builds on immutable abstractions, which can feel quite different for somebody migrating from Python and pandas&amp;#39; mutability. Explorer, like Nx, is notably more succinct than its Python counterpart. Despite this, there isn&amp;#39;t much you &lt;em&gt;can&amp;#39;t&lt;/em&gt; do in Explorer that you can do in Pandas.&lt;/p&gt;&lt;p&gt;From a performance perspective, Explorer benefits from the speed of Polars. There are a number of &lt;a href=&quot;https://blog.dominodatalab.com/polars-a-lightning-fast-dataframes-library&quot;&gt;articles&lt;/a&gt; that laud Polars as the fastest DataFrame library. Given that Explorer builds on that performance, you might see significant performance improvements migrating from Pandas to Explorer.&lt;/p&gt;&lt;h2&gt;Data Presentation/Visualization&lt;/h2&gt;&lt;p&gt;In data science, presentations and visualizations are where the money is made. Having a good tool for presenting and visualizing data is a must for any language looking to position itself in the data science sphere. Python has a number of excellent visualization libraries such as &lt;a href=&quot;https://plotly.com/python/plotly-express/&quot;&gt;Plotly Express&lt;/a&gt; and &lt;a href=&quot;https://matplotlib.org/&quot;&gt;matplotlib&lt;/a&gt;. The Elixir equivalent is its &lt;a href=&quot;https://github.com/livebook-dev/vega_lite&quot;&gt;VegaLite&lt;/a&gt; library which provides bindings around the VegaLite graphics library.&lt;/p&gt;&lt;p&gt;Functionally, you can get essentially equivalent visualizations from both Elixir and Python. The VegaLite API might feel unfamiliar to users coming from Plotly Express and matplotlib; however, the abstractions are incredibly powerful and allow for composing and creating evermore complex graphics with code.&lt;/p&gt;&lt;p&gt;For the most part, I&amp;#39;ve found it possible to perform equivalent visualizations in both Elixir and Python; however, Python seems to have an edge in network visualizations and geographic visualizations. Elixir has no equivalents to Python&amp;#39;s &lt;a href=&quot;https://networkx.org/&quot;&gt;NetworkX&lt;/a&gt; and the &lt;a href=&quot;https://python-visualization.github.io/folium/&quot;&gt;Folium&lt;/a&gt; library. I suspect with companies like &lt;a href=&quot;https://felt.com/&quot;&gt;Felt&lt;/a&gt; using Elixir in the map-making space that we might see geographic visualizations in Elixir improve (fingers crossed).&lt;/p&gt;&lt;p&gt;The Python ecosystem also has a number of libraries concerned with Dashboard creation. Tools such as &lt;a href=&quot;https://plotly.com/dash/&quot;&gt;Dash&lt;/a&gt; allow for the creation of interactive demos with a few lines of code. There are no direct equivalents in Elixir just yet; however, the direction of &lt;a href=&quot;https://livebook.dev&quot;&gt;Livebook&lt;/a&gt; is promising for the prospects of interactive and shareable demos in Elixir.&lt;/p&gt;&lt;h2&gt;Pipelines / Orchestration&lt;/h2&gt;&lt;p&gt;Whether it be training large models or creating production-ready data ingest/management solutions, the task of data orchestration and pipeline is an important one for data science/machine learning. There are a large number of Python libraries built specifically to create and orechestrate data pipelines. In Elixir, there are a few; however, this is an area I would personally argue Elixir has a strong edge over Python. Given the Elixir is built on the BEAM, which is designed for concurrency, the task of &lt;a href=&quot;https://pragprog.com/titles/sgdpelixir/concurrent-data-processing-in-elixir/&quot;&gt;Concurrent Data Processing in Elixir&lt;/a&gt; is a natural extension of the language. Python is just not designed with concurrency in mind. From simple language-level abstractions such as &lt;code class=&quot;py-px text-sm leading-6 rounded&quot;&gt;Task&lt;/code&gt; to library-level abstractions such as &lt;code class=&quot;py-px text-sm leading-6 rounded&quot;&gt;Flow&lt;/code&gt; and &lt;code class=&quot;py-px text-sm leading-6 rounded&quot;&gt;Broadway&lt;/code&gt;, creating scalable input processing pipelines is incredibly easy with Elixir by default.&lt;/p&gt;&lt;p&gt;That&amp;#39;s not to say that Python doesn&amp;#39;t have some nice libraries for achieving the same results. Both PyTorch and TensorFlow offer nice data loading abstractions in &lt;code class=&quot;py-px text-sm leading-6 rounded&quot;&gt;tf.data&lt;/code&gt; and &lt;code class=&quot;py-px text-sm leading-6 rounded&quot;&gt;DataLoader&lt;/code&gt;. Additionally, there are a number of libraries designed for building and orchestrating data pipelines at scale (e.g. &lt;a href=&quot;https://prefect.io&quot;&gt;Prefect&lt;/a&gt; and &lt;a href=&quot;https://airflow.apache.org/docs/apache-airflow/stable/python-api-ref.html&quot;&gt;AirFlow&lt;/a&gt;). The biggest advantage Elixir has in this space is that it is concurrent and fault-tolerant by design. I don&amp;#39;t think Python can ever beat Elixir in this regard.&lt;/p&gt;&lt;h2&gt;Domain-Specific Libraries&lt;/h2&gt;&lt;p&gt;There are a number of &amp;quot;domain-specific&amp;quot; libraries that don&amp;#39;t neatly fall into any of the categories I&amp;#39;ve written so far, but which are worth a brief mention in this article—namely &lt;strong&gt;computer vision&lt;/strong&gt;, &lt;strong&gt;natural language processing&lt;/strong&gt;, and &lt;strong&gt;signal processing&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;There are a number of computer vision-related libraries in the Python ecosystem that generally streamline the task of working with images. This includes &lt;a href=&quot;https://pillow.readthedocs.io/en/stable/&quot;&gt;Pillow&lt;/a&gt; and &lt;a href=&quot;https://docs.opencv.org/4.x/d6/d00/tutorial_py_root.html&quot;&gt;OpenCV&lt;/a&gt; among others. In the Elixir ecosystem, there is &lt;a href=&quot;https://github.com/elixir-nx/evision&quot;&gt;Evision&lt;/a&gt;, which provides bindings to OpenCV implementations. From both an API and performance perspective, this means working with images in Evision will be somewhat similar to working with images in Python&amp;#39;s OpenCV bindings.&lt;/p&gt;&lt;p&gt;For NLP tasks, Elixir does not have a library that is equivalent to Python&amp;#39;s &lt;a href=&quot;https://spacy.io&quot;&gt;spacy&lt;/a&gt; or &lt;a href=&quot;https://realpython.com/nltk-nlp-python/&quot;&gt;NLTK&lt;/a&gt;. However, it does offer bindings to Huggingface&amp;#39;s &lt;a href=&quot;https://github.com/elixir-nx/tokenizers&quot;&gt;tokenizers&lt;/a&gt;, and the ability to import a significant number of HuggingFace models for performing NLP tasks with neural networks.&lt;/p&gt;&lt;p&gt;The Elixir ecosystem still falls behind Python in the area of signal processing; however, with recent work to add FFT support to Nx, and other dedicated efforts, I expect this area to improve in the near future.&lt;/p&gt;&lt;h2&gt;Conclusion&lt;/h2&gt;&lt;p&gt;This post was meant to serve as a high-level comparison of the Elixir machine learning and data science ecosystem with the Python ecosystem. While there are still many gaps in the Elixir ecosystem, the progress over the last year has been rapid. Almost every library I&amp;#39;ve mentioned in this post is less than two years old, and I suspect there will be many more projects to fill some of the gaps I&amp;#39;ve mentioned in the coming months.&lt;/p&gt;&lt;p&gt;If you&amp;#39;re interested in helping in any of our areas of active development, join us at the &lt;a href=&quot;https://erlef.org/&quot;&gt;EEF ML Working Group&lt;/a&gt; to drive machine learning on the BEAM forward. Until next time!&lt;/p&gt;</content:encoded>
</item>
<item>
<title>Passkey Sign In with Elixir and Phoenix</title>
<link>https://shadowfacts.net/2023/phoenix-passkeys/</link>
<enclosure type="image/jpeg" length="0" url="https://shadowfacts.net/shadowfacts.png"></enclosure>
<guid isPermaLink="false">VczlMu0F-895eAWwo-kF-ZStbBe9ejiafL56TQ==</guid>
<pubDate>Fri, 08 May 2026 09:29:25 +0000</pubDate>
<description>The outer part of a shadow is called the penumbra.</description>
<content:encoded>&lt;p&gt;Passkeys are a replacement for passwords that use public/private cryptographic key pairs for login in a way that can be more user-friendly and resistant to a number of kinds of attacks. Let’s implement account registration and login with passkeys in a simple Phoenix web app.&lt;/p&gt;&lt;p&gt;This work is heavily based on &lt;a href=&quot;https://www.imperialviolet.org/2022/09/22/passkeys.html&quot;&gt;this article&lt;/a&gt; by Adam Langley, which provides a great deal of information about implementing passkeys. My goal here is to fill in some more of the details, and provide some Elixir-specific information. As with Adam’s article, I’m not going to use any WebAuthn libraries (even though that may be advisable from a security/maintenance perspective) since I think it’s interesting and helpful to understand how things actually work.&lt;/p&gt;&lt;p&gt;Providing an exhaustive, production-ready implementation is a non-goal of this post. I’m going to make some slightly odd decisions for pedagogical reasons, and leave some things incomplete. That said, I’ll try to note when I’m doing so.&lt;/p&gt;&lt;p&gt;To start, I’m using the default Phoenix template app (less Tailwind) in which I’ve also generated the default, controller-based &lt;a href=&quot;https://hexdocs.pm/phoenix/mix_phx_gen_auth.html&quot;&gt;authentication system&lt;/a&gt;. Some parts of the password-specific stuff have been stripped out altogether, others will get changed to fit with the passkey authentication setup we’re going to build.&lt;/p&gt;&lt;h2&gt;Database schema §&lt;/h2&gt;&lt;p&gt;The first thing we’ll need is a backend schema for passkeys. The only data we need to store for a particular passkey is a unique identifier, and the public key itself. We also want users to be able to have multiple passkeys associated with their account (since they may want to login from multiple platforms that don’t cross-sync passkeys), so the users schema will have a one-to-many relationship with the passkeys.&lt;/p&gt;&lt;p&gt;The actual model I’ll call &lt;code&gt;UserCredential&lt;/code&gt;, since that’s closer to what the WebAuthn spec calls them. Here’s the schema, it’s pretty simple:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;defmodule PhoenixPasskeys.Accounts.UserCredential do
  use Ecto.Schema
  import Ecto.Changeset

  @primary_key {:id, :binary, []}

  schema &amp;quot;users_credentials&amp;quot; do
    # DER-encoded Subject Public Key Info: https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.7
    field :public_key_spki, :binary
    belongs_to :user, PhoenixPasskeys.Accounts.User
    timestamps()
  end

  def changeset(credential, attrs) do
    credential
    |&amp;gt; cast(attrs, [:id, :public_key_spki, :user_id])
  end
end&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;There are a couple things to note here:&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;First, we’re explicitly specifying that the primary key is a binary, since that’s what the WebAuthn API provides as the credential ID.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;public_key_spki&lt;/code&gt; field contains the data for the public key itself and the algorithm being used. We don’t care about the specific format, though, since we don’t have to parse it ourselves.&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;To the generated &lt;code&gt;User&lt;/code&gt; schema, I also added the &lt;code&gt;has_many&lt;/code&gt; side of the relationship. There’s also a migration to create the credentials table. I won’t show it here, since it’s exactly what you’d expect—just make sure the &lt;code&gt;id&lt;/code&gt; column is a binary.&lt;/p&gt;&lt;h2&gt;Registration JavaScript §&lt;/h2&gt;&lt;p&gt;WebAuthn, being a modern web API, is a JavaScript API. This is a distinct disadvantage, if your users expect to be able to login from JavaScript-less browsers. Nonetheless, we proceed with the JS. Here’s the first bit:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;document.addEventListener(&amp;quot;DOMContentLoaded&amp;quot;, () =&amp;gt; {
    const registrationForm = document.getElementById(&amp;quot;registration-form&amp;quot;);
    if (registrationForm) {
        registrationForm.addEventListener(&amp;quot;submit&amp;quot;, (event) =&amp;gt; {
            event.preventDefault();
            registerWebAuthnAccount(registrationForm);
        });
    }
});&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;If we find the registration &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt; element (note that I’ve added an ID (and also removed the password field)), we add a submit listener to it which prevents the form submission and instead calls the &lt;code&gt;registerWebAuthnAccount&lt;/code&gt; function we’ll create next.&lt;/p&gt;&lt;p&gt;Before we get there, we’ll also write a brief helper function to check whether passkeys are actually available:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;async function supportsPasskeys() {
    if (!window.PublicKeyCredential || !PublicKeyCredential.isConditionalMediationAvailable) {
		return false;
	}
	const [conditional, userVerifiying] = await Promise.all([
		PublicKeyCredential.isConditionalMediationAvailable(),
		PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(),
	]);
	return conditional &amp;amp;&amp;amp; userVerifiying;
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The conditional mediation check establishes that WebAuthn conditional UI is available. This is what we’ll use for login, and is part of what makes using passkeys a nice, slick experience. Conditional UI will let us start a request for a passkey that doesn’t present anything until the user interacts with the browser’s passkey autofill UI.&lt;/p&gt;&lt;p&gt;The user-verifying platform authenticator check establishes that, well, there is a user-verifying platform authenticator. The platform authenticator part means an authenticator that’s part of the user’s device, not removable, and the user-verifying part means that the authenticator verifies the presence of the user (such as via biometrics).&lt;/p&gt;&lt;p&gt;In the &lt;code&gt;registerWebAuthnAccount&lt;/code&gt; function, the first thing we need to do is check that both these conditions are met and passkeys are supported by the browser. If not, we’ll just bail out and registration won’t be possible.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;async function registerWebAuthnAccount(form) {
    if (!(await supportsPasskeys())) {
        return;
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Next, we’ll setup a big ol’ options object that we’ll pass to the WebAuthn API to specify what sort of credential we want. There’s going to be a whole lot of stuff here, so lets take it one piece at a time.&lt;/p&gt;&lt;p&gt;The RP is the Relying Party—that is, us, the party which relies on the credentials for authentication. The ID is the &lt;a href=&quot;https://w3c.github.io/webauthn/#rp-id&quot;&gt;domain of the RP&lt;/a&gt;—just localhost for this example, though in reality you’d need to use your actual domain in production. The name is just a user-facing name for the RP.&lt;/p&gt;&lt;p&gt;Next up is some info about the user who the credential is for. The user’s “name” will just be the email that they entered in the form. The &lt;code&gt;displayName&lt;/code&gt; value is required, per the spec, but we don’t have any other information so we just leave it blank and let the browser display the &lt;code&gt;name&lt;/code&gt; only. The ID is where this gets a little weird, since we’re just generating a random 64-byte value:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;function generateUserID() {
	const userID = new Uint8Array(64);
	crypto.getRandomValues(userID);
	return userID;
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;If you were adding a passkey to an existing user account, you could use a server-specified value for the user ID and the browser would replace any existing credentials with the same user ID and RP ID. But, since the user is registering a new account at this point, we assume they don’t want to do that (and, moreover, we don’t have any ID we can use yet). The user ID will also be returned upon a successful login, which would let us look up the user that’s logging in. However, since we’re only going to allow a credential to belong to a single user, we can use the credential’s ID to uniquely determine the user.&lt;/p&gt;&lt;p&gt;Next up, we specify the types of public keys that we’ll accept. We’re going to support Ed25519, ES256, and RS256, since those are recommended by the WebAuthn spec:&lt;/p&gt;&lt;p&gt;During the login process, the server will generate a challenge value that the client will sign and the server will verify was signed with the user’s private key. The spec also requires that we provide a challenge when creating a credential, but we’re not going to use it for anything (since the user is just now creating the credential, we have nothing trusted that we can verify the initial challenge against), so we just provide an empty value:&lt;/p&gt;&lt;p&gt;Lastly, we give our requirements for authenticators that we want to use:&lt;/p&gt;&lt;p&gt;We want only the platform authenticator, not anything else, like removable security keys. We also specify that we want a resident key. This usage of “resident” is deprecated terminology that’s enshrined in the API. Really it means we want a &lt;em&gt;discoverable&lt;/em&gt; credential, so that the authenticator will surface it during the login process without us having to request the credential by ID. This is important to note, since it’s what prevents needing a separate username entry step.&lt;/p&gt;&lt;p&gt;Now that we (finally) have all the configuration options in place, we can actually proceed with the credential creation. We pass the options to &lt;code&gt;navigator.credentials.create&lt;/code&gt; to actually create the WebAuthn credential. If that fails, we’ll just take the easy way out and alert to inform the user (in an actual service, you’d probably want better error handling).&lt;/p&gt;&lt;p&gt;From the credential we get back, we need a few pieces of information. First is the decoded client data, which is an object that contains information about the credential creation request that occurred.&lt;/p&gt;&lt;p&gt;The &lt;code&gt;clientDataJSON&lt;/code&gt; field of the response object contains the JSON-serialized object in an &lt;code&gt;ArrayBuffer&lt;/code&gt;, so we decode that to text and then parse the JSON. With the decoded object, we do a consistency check with a couple pieces of data: the type of the request, whether or not it was cross-origin, and the actual origin being used.&lt;/p&gt;&lt;p&gt;If it was not a creation request, the request was cross-origin, or the origin doesn’t match, we bail out. Note that in production, the origin should be checked against the actual production origin, not localhost. And again, in reality you’d want better error handling than just an alert.&lt;/p&gt;&lt;p&gt;Next, we need to get the authenticator data which is encoded in a binary format and pull a few pieces of data out of it. You can see the full format of the authenticator data &lt;a href=&quot;https://w3c.github.io/webauthn/#sctn-authenticator-data&quot;&gt;in the spec&lt;/a&gt;, but the parts we’re interested in are the backed-up state and the credential ID, which is part of the &lt;a href=&quot;https://w3c.github.io/webauthn/#attested-credential-data&quot;&gt;attested credential data&lt;/a&gt;.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;async function registerWebAuthnAccount(form) {
    // ...
    const authenticatorData = new Uint8Array(credential.response.getAuthenticatorData());
    const backedUp = (authenticatorData[32] &amp;gt;&amp;gt; 4) &amp;amp; 1;
    const idLength = (authenticatorData[53] &amp;lt;&amp;lt; 8) | authenticatorData[54];
    const id = authenticatorData.slice(55, 55 + idLength);
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;We get the backed-up bit from the flags byte at offset 32. Then we get the length of the credential ID, which is encoded as a big-endian, 16-bit integer at bytes 53 and 54. The ID itself immediately follows the length, thus starting at byte 55.&lt;/p&gt;&lt;p&gt;Before proceeding, we check that the &lt;a href=&quot;https://w3c.github.io/webauthn/#sctn-credential-backup&quot;&gt;backed-up bit&lt;/a&gt; is set, indicating that the credential is backed-up and safe from the user losing the current device. If it’s not, we won’t let the user register with this passkey. I choose to do this since it’s recommended by Adam Langley’s blog post, but whether it’s actually necessary may depend on your specific circumstances.&lt;/p&gt;&lt;p&gt;The last piece of data we need out of the credential is the actual public key:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;async function registerWebAuthnAccount(form) {
    // ...
    const publicKey = credential.response.getPublicKey();
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And with all that in place, we can actually initiate the registration. We’ll assemble a form data payload with all of the requisite values:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;async function registerWebAuthnAccount(form) {
    // ...
    const body = new FormData();
    body.append(&amp;quot;_csrf_token&amp;quot;, form._csrf_token.value);
    body.append(&amp;quot;email&amp;quot;, form[&amp;quot;user[email]&amp;quot;].value);
    body.append(&amp;quot;credential_id&amp;quot;, arrayBufferToBase64(id));
    body.append(&amp;quot;public_key_spki&amp;quot;, arrayBufferToBase64(publicKey));
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;We’ll use the body in a POST request to the registration endpoint. The response we get back will contain a status value to indicate whether the request was successful or not. If it was successful, the backend will have set the session cookie, and we can redirect to the home page and the new user will be logged in. If registration failed, we’ll alert the user.&lt;/p&gt;&lt;p&gt;And with that, we’re done with JavaScript (for now) and can move on to the backend part of registering an account.&lt;/p&gt;&lt;h2&gt;Creating a user §&lt;/h2&gt;&lt;p&gt;In the &lt;code&gt;UserRegistrationController&lt;/code&gt; module that comes with the Phoenix auth template, we’ll change the &lt;code&gt;create&lt;/code&gt; function. By default, it registers the user using the parameters from the signup form and then redirects to the homepage. Instead, we’re going to register using the passkey we created on the client and then respond with the JSON that our JavaScript is expecting.&lt;/p&gt;&lt;p&gt;The first thing we need to do is extract the values that were sent by the frontend and decode the base 64-encoded ones.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;defmodule PhoenixPasskeysWeb.UserRegistrationController do
  def create(conn, %{
        &amp;quot;email&amp;quot; =&amp;gt; email,
        &amp;quot;credential_id&amp;quot; =&amp;gt; credential_id,
        &amp;quot;public_key_spki&amp;quot; =&amp;gt; public_key_spki
      }) do
    credential_id = Base.decode64!(credential_id)
    public_key_spki = Base.decode64!(public_key_spki)
  end
end&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Those get passed to the &lt;code&gt;Accounts.register_user&lt;/code&gt; function, which we’ll update shortly to handle. If account creation succeeded, we’ll still send the confirmation email as the existing code did. After that, instead of redirecting, we’ll log the user in by setting the session cookie and then respond with the “ok” status for the frontend. If account creation fails, we’ll just respond with the “error” status so the frontend can alert the user.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;defmodule PhoenixPasskeysWeb.UserRegistrationController do
  def create(...) do
    # ...
    case Accounts.register_user(email, credential_id, public_key_spki) do
      {:ok, user} -&amp;gt;
        # Send confirmation email...

        conn
        |&amp;gt; UserAuth.log_in_user_without_redirect(user)
        |&amp;gt; json(%{status: :ok})

      {:error, _changeset} -&amp;gt;
        json(conn, %{status: :error})
    end
  end
end&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Let’s update the &lt;code&gt;register_user&lt;/code&gt; function. Instead of just creating a changeset for the user and then inserting it, we need to also create the authenticator. To avoid potentially leaving things in a broken state, we wrap both of these in a database transaction.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;defmodule PhoenixPasskeys.Accounts do
  def register_user(email, credential_id, public_key_spki) do
    Repo.transaction(fn -&amp;gt;
      user =
        %User{}
        |&amp;gt; User.registration_changeset(%{email: email})
        |&amp;gt; Repo.insert()
        |&amp;gt; case do
          {:ok, user} -&amp;gt; user
          {:error, changeset} -&amp;gt; Repo.rollback(changeset)
        end
    end)
  end
end&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;First, we create a user with the given email. If the user creation fails, we abort and rollback the transaction. Then, we can create a credential belonging to the new user with the credential ID and public key we received from the client. As with the user, if creating the credential fails, we rollback the transaction.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;defmodule PhoenixPasskeys.Accounts do
  def register_user(email, credential_id, public_key_spki) do
    Repo.transaction(fn -&amp;gt;
      # ...
      %UserCredential{}
      |&amp;gt; UserCredential.changeset(%{
        id: credential_id,
        public_key_spki: public_key_spki,
        user_id: user.id
      })
      |&amp;gt; Repo.insert()
      |&amp;gt; case do
        {:ok, _credential} -&amp;gt; nil
        {:error, changeset} -&amp;gt; Repo.rollback(changeset)
      end
    end)
  end
end&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;We don’t need to do anything with the newly created credential, so we can just ignore it once it’s been created.&lt;/p&gt;&lt;p&gt;And finally, from the transaction function, we return the user:&lt;/p&gt;&lt;p&gt;The last thing we need to to complete the registration flow is to set the new user’s session cookie so that they’re logged in immediately. The &lt;code&gt;UserAuth&lt;/code&gt; module that’s generated as part of the Phoenix auth template has a &lt;code&gt;log_in_user&lt;/code&gt; function that does exactly this. But it also redirects the connection to another endpoint. We don’t want to do that, since we’re sending a JSON response, so I’ve split the function into two: one that only sets the session, and the existing function that sets the session and then redirects.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;defmodule PhoenixPasskeysWeb.UserAuth do
  def log_in_user(conn, user, params \\ %{}) do
    user_return_to = get_session(conn, :user_return_to)
    conn
    |&amp;gt; log_in_user_without_redirect(user, params)
    |&amp;gt; redirect(to: user_return_to || signed_in_path(conn))
  end

  def log_in_user_without_redirect(conn, user, params \\ %{}) do
    token = Accounts.generate_user_session_token(user)
    conn
    |&amp;gt; renew_session()
    |&amp;gt; put_token_in_session(token)
    |&amp;gt; maybe_write_remember_me_cookie(token, params)
  end
end&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And with that, everything is in place and you can now create an account with a passkey!&lt;/p&gt;&lt;h2&gt;Login form §&lt;/h2&gt;&lt;p&gt;Now that the user’s got an account, they need to be able to login with it. That means once again interacting with the WebAuthn API and writing a bunch of JavaScript. But before we get there, we need some slight changes to the backend.&lt;/p&gt;&lt;p&gt;In the HTML for the login page, we’ll add an ID to the &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt; element so that we can find it from JS. We’ll also remove the password field, which is obviously no longer necessary. Lastly, but certainly not least, we need to send a challenge to the client.&lt;/p&gt;&lt;p&gt;The challenge is a value that the user’s device will cryptographically sign with their private key. The result will get sent back to the server, and we’ll verify it against the public key we have stored thus authenticating them. We’ll send the challenge just in a hidden form field:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;&amp;lt;input type=&amp;quot;hidden&amp;quot; id=&amp;quot;challenge&amp;quot; value={@webauthn_challenge} /&amp;gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;In the session controller, we’ll need to generate and assign the challenge to the connection.&lt;/p&gt;&lt;p&gt;WebAuthn expects the challenge to be a value up to 64 bytes long, so we’ll use the Erlang crypto module to generate one of that length. The value is encoded as &lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc4648#section-5&quot;&gt;URL-safe base 64&lt;/a&gt; (the same as normal base 64, but with dash and underscore rather than plus and slash) without padding. We encode it this way since that’s the format in which it will later be &lt;a href=&quot;https://w3c.github.io/webauthn/#dom-collectedclientdata-challenge&quot;&gt;returned&lt;/a&gt; as part of the &lt;code&gt;clientDataJSON&lt;/code&gt;, so when we extract that value we can directly compare it to the challenge value we generated.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;defmodule PhoenixPasskeysWeb.UserSessionController do
  defp put_webauthn_challenge(conn) do
    challenge =
      :crypto.strong_rand_bytes(64)
      |&amp;gt; Base.url_encode64(padding: false)

    conn
    |&amp;gt; put_session(:webauthn_challenge, challenge)
    |&amp;gt; assign(:webauthn_challenge, challenge)
  end
end&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Note that the challenge string is also stored in the session, so that we can later check that the challenge that the client signed matches the challenge we generated. It’s safe to store this in the session, even though it’s sent to the client, because the cookie is encrypted and signed so the client can’t tamper with it.&lt;/p&gt;&lt;h2&gt;Login JavaScript §&lt;/h2&gt;&lt;p&gt;With the first part of the backend changes taken care of, it’s time for more JavaScript, baby!&lt;/p&gt;&lt;p&gt;We’ll follow a similar outline to the registration setup (and the same caveat applies about error handling).&lt;/p&gt;&lt;pre&gt;&lt;code&gt;document.addEventListener(&amp;quot;DOMContentLoaded&amp;quot;, () =&amp;gt; {
    // ...
    const loginForm = document.getElementById(&amp;quot;login-form&amp;quot;);
	if (loginForm) {
        loginWebAuthnAccount(loginForm);
    }
});

async function loginWebAuthnAccount(loginForm) {
    if (!(await supportsPasskeys())) {
        return;
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The first thing we need to do is grab the challenge from the hidden form field, then we can construct the options object for getting the credential (which is, thankfully, much simpler than for creation).&lt;/p&gt;&lt;p&gt;In the options, we specify that we want conditional mediation. As noted before, this means that the browser won’t display any UI, except for autofill, for this credential request until the user accepts the autofill suggestion. In the public key options, we also give the decoded challenge value and specify the our Relying Party ID (again, this would need to be the actual domain in production).&lt;/p&gt;&lt;p&gt;Now, we can actually make the credential request and then, if we get a credential back, encode and send all the values to the backend. We need to send the ID of the credential, so that the backend can find its public key and the corresponding user. We also need the client data JSON, which we send as text decoded from the &lt;code&gt;ArrayBuffer&lt;/code&gt; it’s returned as. We also need to send the authenticator data as well as the signature itself.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;async function loginWebAuthnAccount(loginForm) {
    // ...
    const credential = await navigator.credentials.get(getOptions);
    if (!credential) {
        alert(&amp;quot;Could not get credential&amp;quot;);
        return;
    }

    const clientDataJSON = new TextDecoder().decode(credential.response.clientDataJSON);

    const body = new FormData();
    body.append(&amp;quot;_csrf_token&amp;quot;, loginForm._csrf_token.value);
    body.append(&amp;quot;raw_id&amp;quot;, arrayBufferToBase64(credential.rawId));
    body.append(&amp;quot;client_data_json&amp;quot;, clientDataJSON);
    body.append(&amp;quot;authenticator_data&amp;quot;, arrayBufferToBase64(credential.response.authenticatorData));
    body.append(&amp;quot;signature&amp;quot;, arrayBufferToBase64(credential.response.signature));
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;We can then send a request to the login endpoint. If the backend request fails (or if we failed to get the credential) we just alert the user (again, you’d probably want something better in reality). If the login attempt was successful, the server will have set the session cookie, and so we can just redirect to the homepage and the user will be logged in.&lt;/p&gt;&lt;p&gt;With that in place, let’s move on to the backend half of the login request.&lt;/p&gt;&lt;h2&gt;Validating a login attempt §&lt;/h2&gt;&lt;p&gt;As with signup, we’ll modify the existing log in endpoint to actually validate the WebAuthn login attempt.&lt;/p&gt;&lt;p&gt;The first step is extracting all of the information the frontend provides in the params and decoding it:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;defmodule PhoenixPasskeysWeb.UserSessionController do
  def create(conn, params) do
    id = params |&amp;gt; Map.get(&amp;quot;raw_id&amp;quot;) |&amp;gt; Base.decode64!()
    authenticator_data = params |&amp;gt; Map.get(&amp;quot;authenticator_data&amp;quot;) |&amp;gt; Base.decode64!()
    client_data_json_str = params |&amp;gt; Map.get(&amp;quot;client_data_json&amp;quot;)
    signature = params |&amp;gt; Map.get(&amp;quot;signature&amp;quot;) |&amp;gt; Base.decode64!()
  end
end&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Next, we’re going to validate all of the information we got from the client. Before we get to that, there are a handful of helper functions we’ll use. First, looking up a credential by its ID:&lt;/p&gt;&lt;p&gt;Next, a function in the controller that verifies the signature against the provided data:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;defmodule PhoenixPasskeysWeb.UserSessionController do
  defp verify_signature(credential, client_data_json_str, authenticator_data, signature) do
    with {:ok, pubkey} &amp;lt;- X509.PublicKey.from_der(credential.public_key_spki),
         client_data_json_hash &amp;lt;- :crypto.hash(:sha256, client_data_json_str),
         signed_message &amp;lt;- authenticator_data &amp;lt;&amp;gt; client_data_json_hash,
         true &amp;lt;- :public_key.verify(signed_message, :sha256, signature, pubkey) do
      true
    else
      _ -&amp;gt;
        false
    end
  end
end&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;X509&lt;/code&gt; comes from the &lt;a href=&quot;https://hex.pm/packages/x509&quot;&gt;&lt;code&gt;x509&lt;/code&gt;&lt;/a&gt; package, which is the only third-party piece of code we’re using. It’s a fairly thin wrapper around the Erlang &lt;code&gt;public_key&lt;/code&gt; and &lt;code&gt;crypto&lt;/code&gt; modules, and mostly serves to save me from having to deal with Erlang records in my code. Its &lt;code&gt;from_der&lt;/code&gt; helper function is used to parse the public key from the encoded format.&lt;/p&gt;&lt;p&gt;Next, we hash the client data JSON and append that hash to the authenticator data from the client. This value is what should match the signature using the public key we’ve got, so finally we check that. If all these steps succeeded, we return true, and false otherwise.&lt;/p&gt;&lt;p&gt;The last helper function will receive the decoded client data make sure it’s got all of the values that we expect. If the &lt;code&gt;crossOrigin&lt;/code&gt; value is present and is not false, the client data is invalid and the login attempt will be rejected.&lt;/p&gt;&lt;p&gt;Otherwise, we check that the data has the expected type and origin, and we extract the challenge value (note that we’re checking the origin again here, and this would need to change in production):&lt;/p&gt;&lt;p&gt;And lastly, if neither of the previous patterns matched, the client data fails validation:&lt;/p&gt;&lt;p&gt;Now, let’s put all those parts together and validate the login attempt.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;defmodule PhoenixPasskeysWeb.UserSessionController do
  def create(conn, params) do
    # ...
    with credential when not is_nil(credential) &amp;lt;- Accounts.get_credential(id),
         true &amp;lt;- verify_signature(credential, client_data_json_str, authenticator_data, signature),
         {:ok, client_data_json} &amp;lt;- Jason.decode(client_data_json_str),
         {:ok, challenge} &amp;lt;- check_client_data_json(client_data_json),
         true &amp;lt;- challenge == get_session(conn, :webauthn_challenge),
         true &amp;lt;- :binary.part(authenticator_data, 0, 32) == :crypto.hash(:sha256, &amp;quot;localhost&amp;quot;),
         true &amp;lt;- (:binary.at(authenticator_data, 32) &amp;amp;&amp;amp;&amp;amp; 1) == 1 do
      conn
      |&amp;gt; delete_session(:webauthn_challenge)
      |&amp;gt; UserAuth.log_in_user_without_redirect(credential.user)
      |&amp;gt; json(%{status: :ok})
    else
      _ -&amp;gt;
        json(conn, %{status: :error})
    end
  end
end&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Here’s everything that we’re doing:&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;Lookup the credential with the given ID.&lt;/li&gt;
&lt;li&gt;Use the public key we had stored to verify the signature on the authenticator data and client data JSON.&lt;/li&gt;
&lt;li&gt;Decode the client data JSON.&lt;/li&gt;
&lt;li&gt;Check that all the values in the client data are what we expect, and extract the challenge that was signed.&lt;/li&gt;
&lt;li&gt;Ensure the challenge that the user signed matches what we previously generated.&lt;/li&gt;
&lt;li&gt;Extract the hash of the origin from the &lt;a href=&quot;https://w3c.github.io/webauthn/#authenticator-data&quot;&gt;authenticator data&lt;/a&gt; and ensure it matches our origin (this would not be localhost in production).&lt;/li&gt;
&lt;li&gt;Check that the authenticator data has the user presence bit set (indicating that a person was actually present on the client’s end).&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;If all of those steps succeeded, we remove the old challenge value from the session (since it’s no longer needed), actually log the user in, and then respond with the “ok” status that the JavaScript is expecting. If any step failed, we’ll respond with the “error” status and the frontend will alert the user.&lt;/p&gt;&lt;p&gt;Since there’s a lot going on here, it’s worth being clear about what exactly in this process lets us authenticate and prove that the user is who they claim to be. Since the signature verification step is using the public key that we stored during the registration process, we know that anyone that can produce a valid signature using that public key must be the user (or at any rate, have their private key). The value that they’re signing is, essentially, the challenge: a securely generated random value. The user isn’t directly signing the challenge, but this is still safe, since the challenge value is included in the client data JSON, that hash of which is included in the signed message.&lt;/p&gt;&lt;p&gt;So: the challenge value that was signed by the user must be in the client data, and the challenge value in the client data must be the one we generated. Given that, we know that the user whose key was used to sign the message is the one trying to log in now. That we’re verifying with the stored public key prevents an attacker from using an arbitrary key to sign the login attempt. And that the signed challenge matches the challenge the server generated means an attacker can’t reuse a previous response to login (a replay attack).&lt;/p&gt;&lt;p&gt;At long last, we finally have the ability to log in to our application using a passkey. Only a few minor things to go, so let’s forge ahead.&lt;/p&gt;&lt;h2&gt;Handling login if the user enters an email §&lt;/h2&gt;&lt;p&gt;Although we’re presenting the conditional UI, there’s nothing preventing the user from typing their email into the field and then clicking “Sign in,” so we should probably handle that to. This can be done fairly simply by reusing our existing code for conditional login.&lt;/p&gt;&lt;p&gt;We’ll change the &lt;code&gt;loginWebAuthnAccount&lt;/code&gt; to take an additional parameter, &lt;code&gt;conditional&lt;/code&gt;, which will be a boolean indicating whether this login attempt is to setup the conditional UI or triggered by submitting the login form.&lt;/p&gt;&lt;p&gt;If it’s false, we won’t request conditional mediation and instead we’ll look up the credentials corresponding to the email the user entered and ask WebAuthn for one of those:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;async function loginWebAuthnAccount(loginForm, conditional) {
    // ...
    let allowCredentials = [];
    if (!conditional) {
		const email = loginForm[&amp;quot;user[email]&amp;quot;].value;
		const resp = await fetch(`/users/log_in/credentials?email=${email}`);
		const respJSON = await resp.json();
		allowCredentials = respJSON.map((id) =&amp;gt; {
			return {
				type: &amp;quot;public-key&amp;quot;,
				id: base64ToArrayBuffer(id),
			};
		});
    }

    const getOptions = {
		mediation: conditional ? &amp;quot;conditional&amp;quot; : &amp;quot;optional&amp;quot;,
		publicKey: {
			challenge: base64URLToArrayBuffer(challenge),
			rpId: &amp;quot;localhost&amp;quot;,
			allowCredentials,
		}
    };
    // ...
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The “optional” value for the &lt;code&gt;mediation&lt;/code&gt; option means that the authenticator isn’t required to display UI, but will do so if its policies dictate that. The &lt;code&gt;allowCredentials&lt;/code&gt; array contains objects describing all of the credentials that we want to accept—specifically, their binary IDs as &lt;code&gt;ArrayBuffer&lt;/code&gt;s.&lt;/p&gt;&lt;p&gt;We look up the user’s credentials so that the one we actually request from the authenticator matches the account that the user is trying to log in with. To handle this, we’ll also wire up an additional route on the backend that returns the base 64-encoded IDs of all the credentials belonging to the user with a given email.&lt;/p&gt;&lt;p&gt;The &lt;code&gt;get_credentials_by_email&lt;/code&gt; function is quite simple. It just looks up a user by email, preloading any credentials they have and then returning them:&lt;/p&gt;&lt;p&gt;Back in the JS, we can tweak the setup code to pass &lt;code&gt;true&lt;/code&gt; for the conditional parameter in the initial request and also register a submit handler on the login form that will invoke it with &lt;code&gt;false&lt;/code&gt;:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;document.addEventListener(&amp;quot;DOMContentLoaded&amp;quot;, () =&amp;gt; {
    // ...
	const loginForm = document.getElementById(&amp;quot;login-form&amp;quot;);
	if (loginForm) {
		loginWebAuthnAccount(loginForm, true);
		loginForm.addEventListener(&amp;quot;submit&amp;quot;, (event) =&amp;gt; {
			event.preventDefault();
			loginWebAuthnAccount(loginForm, false);
		});
	}
});&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And so we’ve handled the case where the user ignores the conditional UI and still types in their email to log in.&lt;/p&gt;&lt;h2&gt;Passkey reset §&lt;/h2&gt;&lt;p&gt;A not-infrequent argument against passkeys is that if your phone falls into a lake, you lose access to all of your passkey-backed accounts. One could argue that this isn’t true because the definition of passkeys means that they’re backed up and thus protected from this sort of event (and indeed, earlier we only permitted registering with backed-up credentials). I think the argument isn’t particularly interesting, however, because you can still have a “Forgot my passkey” option that works just like it does now with passwords.&lt;/p&gt;&lt;p&gt;This is less secure than a passkey implementation that has no “Forgot” option. But it’s no less secure than current password-based systems, and I think the UX/security tradeoff here falls on the UX side—people will, inevitably, lose access to their passkeys while retaining access to their email.&lt;/p&gt;&lt;p&gt;Implementing isn’t too complicated, fortunately, since we can reuse much of the registration code. First, the JavaScript. The only change necessary is attaching the registration function to the reset form as well.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;document.addEventListener(&amp;quot;DOMContentLoaded&amp;quot;, () =&amp;gt; {
    // ...
	const resetForm = document.getElementById(&amp;quot;reset-passkey-form&amp;quot;);
	if (resetForm) {
		resetForm.addEventListener(&amp;quot;submit&amp;quot;, (event) =&amp;gt; {
			event.preventDefault();
			registerWebAuthnAccount(resetForm);
		});
	}
});&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;In the HTML for the reset form, we we need to include the email in the same form field as the registration form (and also add an ID to the &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt; element):&lt;/p&gt;&lt;pre&gt;&lt;code&gt;&amp;lt;input type=&amp;quot;hidden&amp;quot; name=&amp;quot;user[email]&amp;quot; value={@user.email} /&amp;gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;In the function for the reset route (which is changed to POST from PUT, to match the signup route), we take the credential ID and public key and use them to update the user’s credentials:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;defmodule PhoenixPasskeysWeb.UserResetPasswordController do
  def update(conn, %{
        &amp;quot;credential_id&amp;quot; =&amp;gt; credential_id,
        &amp;quot;public_key_spki&amp;quot; =&amp;gt; public_key_spki
      }) do
    credential_id = Base.decode64!(credential_id)
    public_key_spki = Base.decode64!(public_key_spki)
    case Accounts.reset_user_credentials(conn.assigns.user, credential_id, public_key_spki) do
      :ok -&amp;gt;
        conn
        |&amp;gt; put_flash(:info, &amp;quot;Passkey reset successfully.&amp;quot;)
        |&amp;gt; UserAuth.log_in_user_without_redirect(conn.assigns.user)
        |&amp;gt; json(%{status: :ok})

      {:error, _} -&amp;gt;
        conn
        |&amp;gt; put_flash(:error, &amp;quot;Error resetting passkey&amp;quot;)
        |&amp;gt; json(%{status: :error})
    end
  end
end&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;After updating, we log the user in if applicable and return JSON with the appropriate status.&lt;/p&gt;&lt;p&gt;The &lt;code&gt;reset_user_credentials&lt;/code&gt; function works very similarly to the original reset password function that was part of the template: it deletes all the user’s existing sessions, and then removes their existing credentials and creates a new one:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;defmodule PhoenixPasskeys.Accounts do
  def reset_user_credentials(user, credential_id, public_key_spki) do
    Ecto.Multi.new()
    |&amp;gt; Ecto.Multi.delete_all(
      :old_credentials,
      from(a in UserCredential, where: a.user_id == ^user.id)
    )
    |&amp;gt; Ecto.Multi.insert(
      :new_credential,
      UserCredential.changeset(%UserCredential{}, %{
        id: credential_id,
        public_key_spki: public_key_spki,
        user_id: user.id
      })
    )
    |&amp;gt; Ecto.Multi.delete_all(:tokens, UserToken.user_and_contexts_query(user, :all))
    |&amp;gt; Repo.transaction()
    |&amp;gt; case do
      {:ok, _} -&amp;gt; :ok
      {:error, _, changeset, _} -&amp;gt; {:error, changeset}
    end
  end
end&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;It’s worth noting that this does have slightly weaker security properties than the &lt;code&gt;phx.gen.auth&lt;/code&gt; reset password implementation. With that approach, leaking a password reset token does not necessarily result in an account takeover, since whoever obtained the leaked token may not know the target user’s email address. Since the auth template forces the user to re-login after a reset, this prevents someone without the email address from gaining access even if they change the password.&lt;/p&gt;&lt;p&gt;But since logging in with a passkey is functionally a single factor, resetting it means gaining access to the account. So, a leaked reset token gives the bearer control over the account. This is an argument against having a reset option, but whether this is a concern in practice depends on your specific circumstances.&lt;/p&gt;&lt;h2&gt;Conclusion §&lt;/h2&gt;&lt;p&gt;As noted, this is not a complete implementation. There are a handful of places where I’ve left things unfinished since this isn’t meant to be production-level code. There are also a few places where there are security decisions that need to be made on a more contextual basis, that I’ve tried to note. And, of course, you wouldn’t really want to only permit signing in with passkeys and wholesale drop the passwords column from your database.&lt;/p&gt;&lt;p&gt;Nonetheless, I hope this has been a helpful look at how to implement passkeys in an Elixir/Phoenix application. You can find the complete repo &lt;a href=&quot;https://git.shadowfacts.net/shadowfacts/phoenix_passkeys&quot;&gt;here&lt;/a&gt;, and it may also be useful to look at the &lt;a href=&quot;https://git.shadowfacts.net/shadowfacts/phoenix_passkeys/commit/ce4f485dbc4e528bdd13a57b0d379012dd893338&quot;&gt;specific commit&lt;/a&gt; where passkey support was added.&lt;/p&gt;</content:encoded>
</item>
<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>
</channel>
</rss>
