<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.9.5">Jekyll</generator><link href="https://www.joekoski.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://www.joekoski.com/" rel="alternate" type="text/html" /><updated>2026-02-09T20:50:27+00:00</updated><id>https://www.joekoski.com/feed.xml</id><title type="html">Joseph Koski’s Blog</title><subtitle>Blog by Joseph Koski, author of Advanced Functional Programming with Elixir (PragProg)  and creator of the Funx library.</subtitle><author><name>Joseph Koski</name></author><entry><title type="html">Funx: Reducing Degrees of Freedom</title><link href="https://www.joekoski.com/blog/2026/02/08/funx-pred-dsl-degree.html" rel="alternate" type="text/html" title="Funx: Reducing Degrees of Freedom" /><published>2026-02-08T14:16:06+00:00</published><updated>2026-02-08T14:16:06+00:00</updated><id>https://www.joekoski.com/blog/2026/02/08/funx-pred-dsl-degree</id><content type="html" xml:base="https://www.joekoski.com/blog/2026/02/08/funx-pred-dsl-degree.html"><![CDATA[<p>Reducing degrees of freedom to make rules more dependable.</p>

<p><a href="https://livebook.dev/run?url=https%3A%2F%2Fwww.joekoski.com%2Fassets%2Flivebooks%2Fblogs%2Ffunx-pred-dsl.livemd"><img src="https://livebook.dev/badge/v1/black.svg" alt="Run in Livebook" /></a></p>

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

<h3 id="is-this-user-an-admin">Is this user an admin?</h3>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">defmodule</span> <span class="no">User</span> <span class="k">do</span>

  <span class="k">defstruct</span> <span class="p">[</span><span class="ss">:id</span><span class="p">,</span> <span class="ss">:name</span><span class="p">,</span> <span class="ss">:admin</span><span class="p">,</span> <span class="ss">:owner</span><span class="p">]</span>

  <span class="c1"># Pattern matching</span>
  <span class="k">def</span> <span class="n">admin?</span><span class="p">(%{</span><span class="ss">admin:</span> <span class="no">true</span><span class="p">}),</span> <span class="k">do</span><span class="p">:</span> <span class="no">true</span>
  <span class="k">def</span> <span class="n">admin?</span><span class="p">(</span><span class="n">_</span><span class="p">),</span> <span class="k">do</span><span class="p">:</span> <span class="no">false</span>

  <span class="c1"># Guard</span>
  <span class="k">def</span> <span class="n">is_admin</span><span class="p">(</span><span class="n">user</span><span class="p">)</span> <span class="ow">when</span> <span class="n">user</span><span class="o">.</span><span class="n">admin</span> <span class="o">==</span> <span class="no">true</span><span class="p">,</span> <span class="k">do</span><span class="p">:</span> <span class="no">true</span>

  <span class="c1"># Conditional</span>
  <span class="k">def</span> <span class="n">admin_user</span><span class="p">(</span><span class="n">user</span><span class="p">)</span> <span class="k">do</span>
     <span class="k">case</span> <span class="n">user</span><span class="o">.</span><span class="n">admin</span> <span class="k">do</span>
        <span class="no">true</span> <span class="o">-&gt;</span> <span class="no">true</span><span class="p">;</span> 
        <span class="n">_</span> <span class="o">-&gt;</span> <span class="no">false</span> 
      <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">user</span> <span class="o">=</span> <span class="p">%{</span><span class="ss">admin:</span> <span class="no">true</span><span class="p">}</span>

<span class="no">User</span><span class="o">.</span><span class="n">admin?</span><span class="p">(</span><span class="n">user</span><span class="p">)</span> <span class="c1"># true</span>
<span class="no">User</span><span class="o">.</span><span class="n">is_admin</span><span class="p">(</span><span class="n">user</span><span class="p">)</span> <span class="c1"># true</span>
<span class="no">User</span><span class="o">.</span><span class="n">admin_user</span><span class="p">(</span><span class="n">user</span><span class="p">)</span> <span class="c1"># true</span>
<span class="n">!!</span><span class="no">Map</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">user</span><span class="p">,</span> <span class="ss">:admin</span><span class="p">)</span> <span class="c1"># true</span>
</code></pre></div></div>

<p>These all answer the question of whether a user is an admin, and they are all syntactically correct. But each carries different assumptions:</p>

<ul>
  <li>whether a missing key is <code class="language-plaintext highlighter-rouge">false</code> or an error,</li>
  <li>whether <code class="language-plaintext highlighter-rouge">nil</code> is distinct from <code class="language-plaintext highlighter-rouge">false</code>,</li>
  <li>whether “admin” means boolean <code class="language-plaintext highlighter-rouge">true</code> or merely truthy.</li>
</ul>

<p>When solving problems, we tend to focus on the happy path, so it’s the unhappy path where we accidentally introduce semantic assumptions that show up as downstream bugs.</p>

<p>The problem hits LLMs as well; they are good at syntax, but less reliable at choosing between semantically different implementations. This means small changes in prompt wording will produce code with different edge-case behaviour (bugs).</p>

<p>We want to reduce the degrees of freedom. Instead of inventing new shapes for each rule, prefer composing from a smaller set of primitives with known explicit semantics.</p>

<h2 id="predicate-dsl">Predicate DSL</h2>

<p>Elixir has a notion of truthy (<code class="language-plaintext highlighter-rouge">!!</code>), where anything except <code class="language-plaintext highlighter-rouge">nil</code> and <code class="language-plaintext highlighter-rouge">false</code> counts as true. The default for the <code class="language-plaintext highlighter-rouge">pred</code> DSL is truthy:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">use</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Predicate</span>

<span class="n">truthy_name</span> <span class="o">=</span> 
  <span class="n">pred</span> <span class="k">do</span>
    <span class="n">check</span> <span class="ss">:name</span>
  <span class="k">end</span>

<span class="n">truthy_name</span><span class="o">.</span><span class="p">(%{</span><span class="ss">name:</span> <span class="s2">"John"</span><span class="p">})</span> <span class="c1"># true</span>
<span class="n">truthy_name</span><span class="o">.</span><span class="p">(%{</span><span class="ss">name:</span> <span class="no">true</span><span class="p">})</span> <span class="c1"># true</span>
<span class="n">truthy_name</span><span class="o">.</span><span class="p">(%{</span><span class="ss">name:</span> <span class="no">false</span><span class="p">})</span> <span class="c1"># false</span>
<span class="n">truthy_name</span><span class="o">.</span><span class="p">(%{</span><span class="ss">name:</span> <span class="no">nil</span><span class="p">})</span> <span class="c1"># false</span>
<span class="n">truthy_name</span><span class="o">.</span><span class="p">(%{</span><span class="ss">name:</span> <span class="s2">""</span><span class="p">})</span> <span class="c1"># true</span>
</code></pre></div></div>

<p>When the key is missing:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">truthy_name</span><span class="o">.</span><span class="p">(%{</span><span class="ss">test:</span> <span class="s2">""</span><span class="p">})</span> <span class="c1"># false</span>
</code></pre></div></div>

<p>Behind the scenes, <code class="language-plaintext highlighter-rouge">check :name</code> uses the Prism optic. The projection either finds something (Just the value) or it doesn’t (Nothing). Funx treats <code class="language-plaintext highlighter-rouge">Nothing</code> as false, so missing data behaves like a failed check instead of an exception.</p>

<p>If we want to treat missing as an exception, we use the Lens optic:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">alias</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Optics</span><span class="o">.</span><span class="no">Lens</span>

<span class="n">truthy_name</span> <span class="o">=</span> 
  <span class="n">pred</span> <span class="k">do</span>
    <span class="n">check</span> <span class="no">Lens</span><span class="o">.</span><span class="n">key</span><span class="p">(</span><span class="ss">:name</span><span class="p">)</span>
  <span class="k">end</span>

<span class="n">truthy_name</span><span class="o">.</span><span class="p">(%{</span><span class="ss">name:</span> <span class="s2">"John"</span><span class="p">})</span> <span class="c1"># true</span>
<span class="n">truthy_name</span><span class="o">.</span><span class="p">(%{</span><span class="ss">name:</span> <span class="no">false</span><span class="p">})</span> <span class="c1"># false</span>
<span class="n">truthy_name</span><span class="o">.</span><span class="p">(%{</span><span class="ss">name:</span> <span class="no">nil</span><span class="p">})</span> <span class="c1"># false</span>
<span class="n">truthy_name</span><span class="o">.</span><span class="p">(%{</span><span class="ss">name:</span> <span class="s2">""</span><span class="p">})</span> <span class="c1"># true</span>
</code></pre></div></div>

<p>The lens enforces the key invariant:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">truthy_name</span><span class="o">.</span><span class="p">(%{</span><span class="ss">test:</span> <span class="s2">""</span><span class="p">})</span> <span class="c1"># ** (KeyError) key :name not found in:</span>
</code></pre></div></div>

<p>Here, a missing <code class="language-plaintext highlighter-rouge">name</code> key raises immediately (fail fast). This is useful when absence is a bug, not a business rule.</p>

<p>We can also invert a predicate with <code class="language-plaintext highlighter-rouge">negate</code>:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">falsy_name</span> <span class="o">=</span> 
  <span class="n">pred</span> <span class="k">do</span>
    <span class="n">negate</span> <span class="n">check</span> <span class="ss">:name</span>
  <span class="k">end</span>

<span class="n">falsy_name</span><span class="o">.</span><span class="p">(%{</span><span class="ss">name:</span> <span class="s2">"John"</span><span class="p">})</span> <span class="c1"># false</span>
<span class="n">falsy_name</span><span class="o">.</span><span class="p">(%{</span><span class="ss">name:</span> <span class="no">false</span><span class="p">})</span> <span class="c1"># true</span>
<span class="n">falsy_name</span><span class="o">.</span><span class="p">(%{</span><span class="ss">name:</span> <span class="no">nil</span><span class="p">})</span> <span class="c1"># true</span>
<span class="n">falsy_name</span><span class="o">.</span><span class="p">(%{</span><span class="ss">name:</span> <span class="s2">""</span><span class="p">})</span> <span class="c1"># false</span>

<span class="n">falsy_name</span><span class="o">.</span><span class="p">(%{</span><span class="ss">test:</span> <span class="s2">""</span><span class="p">})</span> <span class="c1"># true</span>
</code></pre></div></div>

<p>Often we want an empty string included in our definition of falsy. We can supply this more restrictive logic in a predicate.</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">required_name</span> <span class="o">=</span> 
  <span class="n">pred</span> <span class="k">do</span>
    <span class="n">check</span> <span class="ss">:name</span><span class="p">,</span> <span class="k">fn</span> <span class="n">value</span> <span class="o">-&gt;</span> <span class="n">!!value</span> <span class="ow">and</span> <span class="n">value</span> <span class="o">!=</span> <span class="s2">""</span> <span class="k">end</span>
  <span class="k">end</span>

<span class="n">required_name</span><span class="o">.</span><span class="p">(%{</span><span class="ss">name:</span> <span class="s2">""</span><span class="p">})</span> <span class="c1"># false</span>
</code></pre></div></div>

<p>That works, but Funx includes <code class="language-plaintext highlighter-rouge">Required</code> out of the box:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">alias</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Predicate</span><span class="o">.</span><span class="no">Required</span>

<span class="n">required_name</span> <span class="o">=</span> 
  <span class="n">pred</span> <span class="k">do</span>
    <span class="n">check</span> <span class="ss">:name</span><span class="p">,</span> <span class="no">Required</span>
  <span class="k">end</span>

<span class="n">required_name</span><span class="o">.</span><span class="p">(%{</span><span class="ss">name:</span> <span class="s2">""</span><span class="p">})</span> <span class="c1"># false</span>
</code></pre></div></div>

<p>And if we want the literal boolean <code class="language-plaintext highlighter-rouge">true</code>, not truthiness, we can use <code class="language-plaintext highlighter-rouge">IsTrue</code>:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">alias</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Predicate</span><span class="o">.</span><span class="no">IsTrue</span>

<span class="n">admin?</span> <span class="o">=</span> 
  <span class="n">pred</span> <span class="k">do</span>
    <span class="n">check</span> <span class="ss">:admin</span><span class="p">,</span> <span class="no">IsTrue</span>
  <span class="k">end</span>

<span class="n">admin?</span><span class="o">.</span><span class="p">(%{</span><span class="ss">admin:</span> <span class="s2">"Yes"</span><span class="p">})</span> <span class="c1"># false</span>
<span class="n">admin?</span><span class="o">.</span><span class="p">(%{</span><span class="ss">admin:</span> <span class="no">false</span><span class="p">})</span> <span class="c1"># false</span>
<span class="n">admin?</span><span class="o">.</span><span class="p">(%{</span><span class="ss">admin:</span> <span class="no">true</span><span class="p">})</span> <span class="c1"># true</span>
</code></pre></div></div>

<p>The goal of the DSL is to make intent clear and easy to read:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">admin_or_owner?</span> <span class="o">=</span> 
  <span class="n">pred</span> <span class="k">do</span>
    <span class="n">any</span> <span class="k">do</span>
      <span class="n">check</span> <span class="ss">:owner</span><span class="p">,</span> <span class="no">IsTrue</span>
      <span class="n">check</span> <span class="ss">:admin</span><span class="p">,</span> <span class="no">IsTrue</span>
    <span class="k">end</span>
  <span class="k">end</span>

<span class="n">admin_or_owner?</span><span class="o">.</span><span class="p">(%{</span><span class="ss">admin:</span> <span class="no">true</span><span class="p">})</span> <span class="c1"># true</span>
<span class="n">admin_or_owner?</span><span class="o">.</span><span class="p">(%{</span><span class="ss">admin:</span> <span class="no">true</span><span class="p">,</span> <span class="ss">owner:</span> <span class="no">false</span><span class="p">})</span> <span class="c1"># true</span>
<span class="n">admin_or_owner?</span><span class="o">.</span><span class="p">(%{</span><span class="ss">admin:</span> <span class="no">false</span><span class="p">,</span> <span class="ss">owner:</span> <span class="no">false</span><span class="p">})</span> <span class="c1"># false</span>
</code></pre></div></div>

<p>Six months from now, we want to be able to read the rule and immediately understand what it does.</p>

<p>The real payoff comes when we model a more complex domain.</p>

<h2 id="role-playing-game">Role-playing Game</h2>

<p>Let’s revisit our original rules for the role-playing game:</p>

<!-- livebook:{"force_markdown":true} -->

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">defmodule</span> <span class="no">Status</span> <span class="k">do</span>
  <span class="k">defstruct</span> <span class="p">[</span><span class="ss">:poison</span><span class="p">,</span> <span class="ss">:bleeding</span><span class="p">,</span> <span class="ss">:exposure</span><span class="p">,</span> <span class="ss">:stamina</span><span class="p">,</span> <span class="ss">:blessing</span><span class="p">,</span> <span class="ss">:inventory</span><span class="p">]</span>
  <span class="kn">use</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Predicate</span>

  <span class="k">def</span> <span class="n">poisoned?</span> <span class="k">do</span>
    <span class="n">pred</span> <span class="k">do</span>
      <span class="n">check</span> <span class="p">[</span><span class="ss">:poison</span><span class="p">,</span> <span class="ss">:active</span><span class="p">],</span> <span class="k">fn</span> <span class="n">active</span> <span class="o">-&gt;</span> <span class="n">active</span> <span class="o">==</span> <span class="no">true</span> <span class="k">end</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">bleeding?</span> <span class="k">do</span>
    <span class="n">pred</span> <span class="k">do</span>
      <span class="n">check</span> <span class="p">[</span><span class="ss">:bleeding</span><span class="p">,</span> <span class="ss">:staunched</span><span class="p">],</span> <span class="k">fn</span> <span class="n">staunched</span> <span class="o">-&gt;</span> <span class="n">staunched</span> <span class="o">==</span> <span class="no">false</span> <span class="k">end</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">poison_resistant?</span> <span class="k">do</span>
    <span class="n">pred</span> <span class="k">do</span>
      <span class="n">check</span> <span class="p">[</span><span class="ss">:blessing</span><span class="p">,</span> <span class="ss">:grants</span><span class="p">],</span> <span class="k">fn</span> <span class="n">grants</span> <span class="o">-&gt;</span> <span class="ss">:poison_resistance</span> <span class="ow">in</span> <span class="n">grants</span> <span class="k">end</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">poison_danger?</span> <span class="k">do</span>
    <span class="n">pred</span> <span class="k">do</span>
      <span class="n">poisoned?</span><span class="p">()</span>
      <span class="n">negate</span> <span class="n">poison_resistant?</span><span class="p">()</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">severe_bleeding?</span> <span class="k">do</span>
    <span class="n">pred</span> <span class="k">do</span>
      <span class="n">bleeding?</span><span class="p">()</span>
      <span class="n">check</span> <span class="p">[</span><span class="ss">:bleeding</span><span class="p">,</span> <span class="ss">:severity</span><span class="p">],</span> <span class="k">fn</span> <span class="n">severity</span> <span class="o">-&gt;</span> <span class="n">severity</span> <span class="ow">in</span> <span class="p">[</span><span class="ss">:moderate</span><span class="p">,</span> <span class="ss">:severe</span><span class="p">,</span> <span class="ss">:critical</span><span class="p">]</span> <span class="k">end</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">wet?</span> <span class="k">do</span>
    <span class="n">pred</span> <span class="k">do</span>
      <span class="n">check</span> <span class="p">[</span><span class="ss">:exposure</span><span class="p">,</span> <span class="ss">:water</span><span class="p">],</span> <span class="k">fn</span> <span class="n">water</span> <span class="o">-&gt;</span> <span class="n">water</span> <span class="ow">in</span> <span class="p">[</span><span class="ss">:wet</span><span class="p">,</span> <span class="ss">:soaked</span><span class="p">]</span> <span class="k">end</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">charge_building?</span> <span class="k">do</span>
    <span class="n">pred</span> <span class="k">do</span>
      <span class="n">check</span> <span class="p">[</span><span class="ss">:exposure</span><span class="p">,</span> <span class="ss">:electricity</span><span class="p">],</span> <span class="k">fn</span> <span class="n">electricity</span> <span class="o">-&gt;</span> <span class="n">electricity</span> <span class="o">==</span> <span class="ss">:building</span> <span class="k">end</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">electrocution_danger?</span> <span class="k">do</span>
    <span class="n">pred</span> <span class="k">do</span>
      <span class="n">wet?</span><span class="p">()</span>
      <span class="n">charge_building?</span><span class="p">()</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">exhausted?</span> <span class="k">do</span>
    <span class="n">pred</span> <span class="k">do</span>
      <span class="n">check</span> <span class="ss">:stamina</span><span class="p">,</span> <span class="k">fn</span> <span class="n">s</span> <span class="o">-&gt;</span> <span class="n">s</span><span class="o">.</span><span class="n">current</span> <span class="o">/</span> <span class="n">s</span><span class="o">.</span><span class="n">max</span> <span class="o">&lt;</span> <span class="mf">0.25</span> <span class="k">end</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">collapsed?</span> <span class="k">do</span>
    <span class="n">pred</span> <span class="k">do</span>
      <span class="n">check</span> <span class="ss">:stamina</span><span class="p">,</span> <span class="k">fn</span> <span class="n">s</span> <span class="o">-&gt;</span> <span class="n">s</span><span class="o">.</span><span class="n">current</span> <span class="o">/</span> <span class="n">s</span><span class="o">.</span><span class="n">max</span> <span class="o">&lt;</span> <span class="mf">0.1</span> <span class="k">end</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">death_spiral?</span> <span class="k">do</span>
    <span class="n">pred</span> <span class="k">do</span>
      <span class="n">exhausted?</span><span class="p">()</span>
      <span class="n">bleeding?</span><span class="p">()</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">mortal_danger?</span> <span class="k">do</span>
    <span class="n">pred</span> <span class="k">do</span>
      <span class="n">any</span> <span class="k">do</span>
        <span class="n">electrocution_danger?</span><span class="p">()</span>
        <span class="n">death_spiral?</span><span class="p">()</span>
        <span class="n">severe_bleeding?</span><span class="p">()</span>
        <span class="n">collapsed?</span><span class="p">()</span>
      <span class="k">end</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">can_staunch?</span> <span class="k">do</span>
    <span class="n">pred</span> <span class="k">do</span>
      <span class="n">bleeding?</span><span class="p">()</span>
      <span class="n">check</span> <span class="p">[</span><span class="ss">:inventory</span><span class="p">,</span> <span class="ss">:bandage</span><span class="p">],</span> <span class="k">fn</span> <span class="n">count</span> <span class="o">-&gt;</span> <span class="n">count</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="k">end</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">can_cure_poison?</span> <span class="k">do</span>
    <span class="n">pred</span> <span class="k">do</span>
      <span class="n">poisoned?</span><span class="p">()</span>
      <span class="n">check</span> <span class="p">[</span><span class="ss">:inventory</span><span class="p">,</span> <span class="ss">:antidote</span><span class="p">],</span> <span class="k">fn</span> <span class="n">count</span> <span class="o">-&gt;</span> <span class="n">count</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="k">end</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>This works, but we’re still hand-coding all the predicates.</p>

<p>Let’s start by extracting that repeated ratio logic using the DSL’s behaviour:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">defmodule</span> <span class="no">RatioLessThan</span> <span class="k">do</span>
  <span class="nv">@behaviour</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Predicate</span><span class="o">.</span><span class="no">Dsl</span><span class="o">.</span><span class="no">Behaviour</span>

  <span class="nv">@impl</span> <span class="no">true</span>
  <span class="k">def</span> <span class="n">pred</span><span class="p">(</span><span class="n">opts</span><span class="p">)</span> <span class="k">do</span>
    <span class="n">threshold</span> <span class="o">=</span> <span class="no">Keyword</span><span class="o">.</span><span class="n">fetch!</span><span class="p">(</span><span class="n">opts</span><span class="p">,</span> <span class="ss">:value</span><span class="p">)</span>

    <span class="k">fn</span> <span class="p">%{</span><span class="ss">current:</span> <span class="n">current</span><span class="p">,</span> <span class="ss">max:</span> <span class="n">max</span><span class="p">}</span> <span class="o">-&gt;</span>
      <span class="n">max</span> <span class="o">!=</span> <span class="mi">0</span> <span class="ow">and</span> <span class="n">current</span> <span class="o">/</span> <span class="n">max</span> <span class="o">&lt;</span> <span class="n">threshold</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>And we can use Funx’s built-in predicates for the rest.</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">defmodule</span> <span class="no">Status</span> <span class="k">do</span>
  <span class="k">defstruct</span> <span class="p">[</span><span class="ss">:poison</span><span class="p">,</span> <span class="ss">:bleeding</span><span class="p">,</span> <span class="ss">:exposure</span><span class="p">,</span> <span class="ss">:stamina</span><span class="p">,</span> <span class="ss">:blessing</span><span class="p">,</span> <span class="ss">:inventory</span><span class="p">]</span>
  <span class="kn">use</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Predicate</span>
  <span class="n">alias</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Predicate</span><span class="o">.</span><span class="p">{</span><span class="no">Contains</span><span class="p">,</span> <span class="no">Eq</span><span class="p">,</span> <span class="no">GreaterThan</span><span class="p">,</span> <span class="no">In</span><span class="p">,</span> <span class="no">IsFalse</span><span class="p">,</span> <span class="no">IsTrue</span><span class="p">}</span>

  <span class="k">def</span> <span class="n">poisoned?</span> <span class="k">do</span>
    <span class="n">pred</span> <span class="k">do</span>
      <span class="n">check</span> <span class="p">[</span><span class="ss">:poison</span><span class="p">,</span> <span class="ss">:active</span><span class="p">],</span> <span class="no">IsTrue</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">bleeding?</span> <span class="k">do</span>
    <span class="n">pred</span> <span class="k">do</span>
      <span class="n">check</span> <span class="p">[</span><span class="ss">:bleeding</span><span class="p">,</span> <span class="ss">:staunched</span><span class="p">],</span> <span class="no">IsFalse</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">poison_resistant?</span> <span class="k">do</span>
    <span class="n">pred</span> <span class="k">do</span>
      <span class="n">check</span> <span class="p">[</span><span class="ss">:blessing</span><span class="p">,</span> <span class="ss">:grants</span><span class="p">],</span> <span class="p">{</span><span class="no">Contains</span><span class="p">,</span> <span class="ss">value:</span> <span class="ss">:poison_resistance</span><span class="p">}</span> 
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">poison_danger?</span> <span class="k">do</span>
    <span class="n">pred</span> <span class="k">do</span>
      <span class="n">poisoned?</span><span class="p">()</span>
      <span class="n">negate</span> <span class="n">poison_resistant?</span><span class="p">()</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">severe_bleeding?</span> <span class="k">do</span>
    <span class="n">pred</span> <span class="k">do</span>
      <span class="n">bleeding?</span><span class="p">()</span>
      <span class="n">check</span> <span class="p">[</span><span class="ss">:bleeding</span><span class="p">,</span> <span class="ss">:severity</span><span class="p">],</span> <span class="p">{</span><span class="no">In</span><span class="p">,</span> <span class="ss">values:</span> <span class="p">[</span><span class="ss">:moderate</span><span class="p">,</span> <span class="ss">:severe</span><span class="p">,</span> <span class="ss">:critical</span><span class="p">]}</span> 
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">wet?</span> <span class="k">do</span>
    <span class="n">pred</span> <span class="k">do</span>
      <span class="n">check</span> <span class="p">[</span><span class="ss">:exposure</span><span class="p">,</span> <span class="ss">:water</span><span class="p">],</span> <span class="p">{</span><span class="no">In</span><span class="p">,</span> <span class="ss">values:</span> <span class="p">[</span><span class="ss">:wet</span><span class="p">,</span> <span class="ss">:soaked</span><span class="p">]}</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">charge_building?</span> <span class="k">do</span>
    <span class="n">pred</span> <span class="k">do</span>
      <span class="n">check</span> <span class="p">[</span><span class="ss">:exposure</span><span class="p">,</span> <span class="ss">:electricity</span><span class="p">],</span> <span class="p">{</span><span class="no">Eq</span><span class="p">,</span> <span class="ss">value:</span> <span class="ss">:building</span><span class="p">}</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">electrocution_danger?</span> <span class="k">do</span>
    <span class="n">pred</span> <span class="k">do</span>
      <span class="n">wet?</span><span class="p">()</span>
      <span class="n">charge_building?</span><span class="p">()</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">exhausted?</span> <span class="k">do</span>
    <span class="n">pred</span> <span class="k">do</span>
      <span class="n">check</span> <span class="ss">:stamina</span><span class="p">,</span> <span class="p">{</span><span class="no">RatioLessThan</span><span class="p">,</span> <span class="ss">value:</span> <span class="mf">0.25</span><span class="p">}</span> 
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">collapsed?</span> <span class="k">do</span>
    <span class="n">pred</span> <span class="k">do</span>
      <span class="n">check</span> <span class="ss">:stamina</span><span class="p">,</span> <span class="p">{</span><span class="no">RatioLessThan</span><span class="p">,</span> <span class="ss">value:</span> <span class="mf">0.1</span><span class="p">}</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">death_spiral?</span> <span class="k">do</span>
    <span class="n">pred</span> <span class="k">do</span>
      <span class="n">exhausted?</span><span class="p">()</span>
      <span class="n">bleeding?</span><span class="p">()</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">mortal_danger?</span> <span class="k">do</span>
    <span class="n">pred</span> <span class="k">do</span>
      <span class="n">any</span> <span class="k">do</span>
        <span class="n">electrocution_danger?</span><span class="p">()</span>
        <span class="n">death_spiral?</span><span class="p">()</span>
        <span class="n">severe_bleeding?</span><span class="p">()</span>
        <span class="n">collapsed?</span><span class="p">()</span>
      <span class="k">end</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">can_staunch?</span> <span class="k">do</span>
    <span class="n">pred</span> <span class="k">do</span>
      <span class="n">bleeding?</span><span class="p">()</span>
      <span class="n">check</span> <span class="p">[</span><span class="ss">:inventory</span><span class="p">,</span> <span class="ss">:bandage</span><span class="p">],</span> <span class="p">{</span><span class="no">GreaterThan</span><span class="p">,</span> <span class="ss">value:</span> <span class="mi">0</span><span class="p">}</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">can_cure_poison?</span> <span class="k">do</span>
    <span class="n">pred</span> <span class="k">do</span>
      <span class="n">poisoned?</span><span class="p">()</span>
      <span class="n">check</span> <span class="p">[</span><span class="ss">:inventory</span><span class="p">,</span> <span class="ss">:antidote</span><span class="p">],</span> <span class="p">{</span><span class="no">GreaterThan</span><span class="p">,</span> <span class="ss">value:</span> <span class="mi">0</span><span class="p">}</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>The predicates now read like their definitions. The anonymous functions are gone, replaced by named parts that carry their semantics.</p>

<p>Note that <code class="language-plaintext highlighter-rouge">Eq</code>, <code class="language-plaintext highlighter-rouge">In</code>, and <code class="language-plaintext highlighter-rouge">Contains</code> all build on Funx’s <code class="language-plaintext highlighter-rouge">Eq.Protocol</code>, so they respect custom notions of equality. Also, <code class="language-plaintext highlighter-rouge">GreaterThan</code> is order logic, which is built on Funx’s <code class="language-plaintext highlighter-rouge">Ord.Protocol</code>.</p>

<h2 id="the-character">The Character</h2>

<p>A character has a name and a status:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">defmodule</span> <span class="no">Character</span> <span class="k">do</span>
  <span class="n">alias</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Optics</span><span class="o">.</span><span class="no">Lens</span>

  <span class="k">defstruct</span> <span class="p">[</span><span class="ss">:name</span><span class="p">,</span> <span class="ss">:status</span><span class="p">]</span>

  <span class="k">def</span> <span class="n">status_lens</span><span class="p">,</span> <span class="k">do</span><span class="p">:</span> <span class="no">Lens</span><span class="o">.</span><span class="n">key</span><span class="p">(</span><span class="ss">:status</span><span class="p">)</span>

  <span class="k">def</span> <span class="n">status_check</span><span class="p">(%</span><span class="bp">__MODULE__</span><span class="p">{}</span> <span class="o">=</span> <span class="n">character</span><span class="p">)</span> <span class="k">do</span>
    <span class="n">status</span> <span class="o">=</span> <span class="no">Lens</span><span class="o">.</span><span class="n">view!</span><span class="p">(</span><span class="n">character</span><span class="p">,</span> <span class="n">status_lens</span><span class="p">())</span>

    <span class="p">%{</span>
      <span class="ss">poisoned:</span> <span class="no">Status</span><span class="o">.</span><span class="n">poisoned?</span><span class="o">.</span><span class="p">(</span><span class="n">status</span><span class="p">),</span>
      <span class="ss">poison_resistant:</span> <span class="no">Status</span><span class="o">.</span><span class="n">poison_resistant?</span><span class="o">.</span><span class="p">(</span><span class="n">status</span><span class="p">),</span>
      <span class="ss">poison_danger:</span> <span class="no">Status</span><span class="o">.</span><span class="n">poison_danger?</span><span class="o">.</span><span class="p">(</span><span class="n">status</span><span class="p">),</span>
      <span class="ss">bleeding:</span> <span class="no">Status</span><span class="o">.</span><span class="n">bleeding?</span><span class="o">.</span><span class="p">(</span><span class="n">status</span><span class="p">),</span>
      <span class="ss">severe_bleeding:</span> <span class="no">Status</span><span class="o">.</span><span class="n">severe_bleeding?</span><span class="o">.</span><span class="p">(</span><span class="n">status</span><span class="p">),</span>
      <span class="ss">electrocution_danger:</span> <span class="no">Status</span><span class="o">.</span><span class="n">electrocution_danger?</span><span class="o">.</span><span class="p">(</span><span class="n">status</span><span class="p">),</span>
      <span class="ss">exhausted:</span> <span class="no">Status</span><span class="o">.</span><span class="n">exhausted?</span><span class="o">.</span><span class="p">(</span><span class="n">status</span><span class="p">),</span>
      <span class="ss">collapsed:</span> <span class="no">Status</span><span class="o">.</span><span class="n">collapsed?</span><span class="o">.</span><span class="p">(</span><span class="n">status</span><span class="p">),</span>
      <span class="ss">death_spiral:</span> <span class="no">Status</span><span class="o">.</span><span class="n">death_spiral?</span><span class="o">.</span><span class="p">(</span><span class="n">status</span><span class="p">),</span>
      <span class="ss">mortal_danger:</span> <span class="no">Status</span><span class="o">.</span><span class="n">mortal_danger?</span><span class="o">.</span><span class="p">(</span><span class="n">status</span><span class="p">)</span>
    <span class="p">}</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">actions</span><span class="p">(%</span><span class="bp">__MODULE__</span><span class="p">{}</span> <span class="o">=</span> <span class="n">character</span><span class="p">)</span> <span class="k">do</span>
    <span class="n">status</span> <span class="o">=</span> <span class="no">Lens</span><span class="o">.</span><span class="n">view!</span><span class="p">(</span><span class="n">character</span><span class="p">,</span> <span class="n">status_lens</span><span class="p">())</span>

    <span class="p">%{</span>
      <span class="ss">can_staunch:</span> <span class="no">Status</span><span class="o">.</span><span class="n">can_staunch?</span><span class="o">.</span><span class="p">(</span><span class="n">status</span><span class="p">),</span>
      <span class="ss">can_cure_poison:</span> <span class="no">Status</span><span class="o">.</span><span class="n">can_cure_poison?</span><span class="o">.</span><span class="p">(</span><span class="n">status</span><span class="p">)</span>
    <span class="p">}</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Here is a character in trouble:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">warrior</span> <span class="o">=</span> <span class="p">%</span><span class="no">Character</span><span class="p">{</span>
  <span class="ss">name:</span> <span class="s2">"Wounded Warrior"</span><span class="p">,</span>
  <span class="ss">status:</span> <span class="p">%</span><span class="no">Status</span><span class="p">{</span>
    <span class="ss">poison:</span> <span class="p">%{</span><span class="ss">active:</span> <span class="no">true</span><span class="p">,</span> <span class="ss">source:</span> <span class="ss">:spider</span><span class="p">,</span> <span class="ss">severity:</span> <span class="ss">:moderate</span><span class="p">},</span>
    <span class="ss">bleeding:</span> <span class="p">%{</span><span class="ss">severity:</span> <span class="ss">:light</span><span class="p">,</span> <span class="ss">staunched:</span> <span class="no">false</span><span class="p">},</span>
    <span class="ss">exposure:</span> <span class="p">%{</span><span class="ss">water:</span> <span class="ss">:soaked</span><span class="p">,</span> <span class="ss">electricity:</span> <span class="ss">:building</span><span class="p">},</span>
    <span class="ss">stamina:</span> <span class="p">%{</span><span class="ss">current:</span> <span class="mi">20</span><span class="p">,</span> <span class="ss">max:</span> <span class="mi">100</span><span class="p">},</span>
    <span class="ss">blessing:</span> <span class="p">%{</span><span class="ss">grants:</span> <span class="p">[</span><span class="ss">:poison_resistance</span><span class="p">]},</span>
    <span class="ss">inventory:</span> <span class="p">%{</span><span class="ss">antidote:</span> <span class="mi">1</span><span class="p">,</span> <span class="ss">bandage:</span> <span class="mi">2</span><span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>When we apply the <code class="language-plaintext highlighter-rouge">status_check/1</code>:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Character</span><span class="o">.</span><span class="n">status_check</span><span class="p">(</span><span class="n">warrior</span><span class="p">)</span>

<span class="c1"># %{</span>
<span class="c1">#   bleeding: true,</span>
<span class="c1">#   poisoned: true,</span>
<span class="c1">#   poison_resistant: true,</span>
<span class="c1">#   poison_danger: false,</span>
<span class="c1">#   severe_bleeding: false,</span>
<span class="c1">#   electrocution_danger: true,</span>
<span class="c1">#   exhausted: true,</span>
<span class="c1">#   collapsed: false,</span>
<span class="c1">#   death_spiral: true,</span>
<span class="c1">#   mortal_danger: true</span>
<span class="c1"># }</span>
</code></pre></div></div>

<p>We find our character is:</p>

<ul>
  <li>Poisoned, but resistant: no poison danger</li>
  <li>Bleeding, but light: no severe bleeding</li>
  <li>Soaked + charge building: electrocution danger</li>
  <li>Exhausted + bleeding: death spiral</li>
  <li>Mortal danger: true</li>
</ul>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Character</span><span class="o">.</span><span class="n">actions</span><span class="p">(</span><span class="n">warrior</span><span class="p">)</span>

<span class="c1"># %{can_staunch: true, can_cure_poison: true}</span>
</code></pre></div></div>

<p>Fortunately, our warrior has some options: they <code class="language-plaintext highlighter-rouge">can_staunch</code> and <code class="language-plaintext highlighter-rouge">can_cure_poison</code>.</p>

<p>Let’s have them escape the water and apply a bandage:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">updated_warrior</span> <span class="o">=</span> <span class="p">%</span><span class="no">Character</span><span class="p">{</span>
  <span class="ss">name:</span> <span class="s2">"Wounded Warrior"</span><span class="p">,</span>
  <span class="ss">status:</span> <span class="p">%</span><span class="no">Status</span><span class="p">{</span>
    <span class="ss">poison:</span> <span class="p">%{</span><span class="ss">active:</span> <span class="no">true</span><span class="p">,</span> <span class="ss">source:</span> <span class="ss">:spider</span><span class="p">,</span> <span class="ss">severity:</span> <span class="ss">:moderate</span><span class="p">},</span>
    <span class="ss">bleeding:</span> <span class="p">%{</span><span class="ss">severity:</span> <span class="ss">:light</span><span class="p">,</span> <span class="ss">staunched:</span> <span class="no">true</span><span class="p">},</span>
    <span class="ss">exposure:</span> <span class="p">%{</span><span class="ss">water:</span> <span class="ss">:dry</span><span class="p">,</span> <span class="ss">electricity:</span> <span class="ss">:building</span><span class="p">},</span>
    <span class="ss">stamina:</span> <span class="p">%{</span><span class="ss">current:</span> <span class="mi">15</span><span class="p">,</span> <span class="ss">max:</span> <span class="mi">100</span><span class="p">},</span>
    <span class="ss">blessing:</span> <span class="p">%{</span><span class="ss">grants:</span> <span class="p">[</span><span class="ss">:poison_resistance</span><span class="p">]},</span>
    <span class="ss">inventory:</span> <span class="p">%{</span><span class="ss">antidote:</span> <span class="mi">1</span><span class="p">,</span> <span class="ss">bandage:</span> <span class="mi">1</span><span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Now when we check their status:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Character</span><span class="o">.</span><span class="n">status_check</span><span class="p">(</span><span class="n">updated_warrior</span><span class="p">)</span>

<span class="c1"># %{</span>
<span class="c1">#   bleeding: false,</span>
<span class="c1">#   poisoned: true,</span>
<span class="c1">#   poison_resistant: true,</span>
<span class="c1">#   poison_danger: false,</span>
<span class="c1">#   severe_bleeding: false,</span>
<span class="c1">#   electrocution_danger: false,</span>
<span class="c1">#   exhausted: true,</span>
<span class="c1">#   collapsed: false,</span>
<span class="c1">#   death_spiral: false,</span>
<span class="c1">#   mortal_danger: false</span>
<span class="c1"># }</span>
</code></pre></div></div>

<p>They are no longer in immediate danger:</p>

<ul>
  <li>Electrocution danger: false</li>
  <li>Death spiral: false</li>
  <li>Mortal danger: false</li>
</ul>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Character</span><span class="o">.</span><span class="n">actions</span><span class="p">(</span><span class="n">updated_warrior</span><span class="p">)</span>

<span class="c1"># %{can_staunch: false, can_cure_poison: true}</span>
</code></pre></div></div>

<p>Even though they still have a bandage available, they are no longer bleeding, so <code class="language-plaintext highlighter-rouge">can_staunch</code> is false. They still have an antidote, so <code class="language-plaintext highlighter-rouge">can_cure_poison</code> remains true.</p>

<h2 id="why-it-matters">Why It Matters</h2>

<p>Reducing degrees of freedom is how we make rules dependable.</p>

<p>The DSL describes intent, not implementation. Every rule follows the same shape, so review becomes about meaning, not edge cases.</p>

<p>The predicates become our ubiquitous language. Names like <code class="language-plaintext highlighter-rouge">poisoned?</code>, <code class="language-plaintext highlighter-rouge">mortal_danger?</code>, and <code class="language-plaintext highlighter-rouge">can_staunch?</code> match how our domain experts talk about the problem. The code becomes the spec.</p>

<p>For LLMs, it is the same advantage. The DSL removes the hardest choice, picking between implementations that look similar but behave differently. Instead, the model selects and composes known parts. Generation becomes assembly, not invention.</p>

<h2 id="resources">Resources</h2>

<div style="display: flex; gap: 20px; margin: 20px 0;">
  <div style="flex-shrink: 0;">
    <a href="https://pragprog.com/titles/jkelixir/advanced-functional-programming-with-elixir">
      <img src="/assets/images/jkelixir_small.jpg" alt="Advanced Functional Programming with Elixir book cover" width="150" />
    </a>
  </div>
  <div>
    <h3><a href="https://pragprog.com/titles/jkelixir/advanced-functional-programming-with-elixir">Advanced Functional Programming with Elixir</a></h3>
    <p>Dive deeper into functional programming patterns and advanced Elixir techniques. Learn how to build robust, maintainable applications using functional programming principles.</p>
  </div>
</div>

<div style="display: flex; gap: 20px; margin: 20px 0;">
  <div style="flex-shrink: 0;">
    <a href="https://www.funxlib.com">
      <img src="/assets/images/funx-social.jpg" alt="Funx functional programming library" width="150" />
    </a>
  </div>
  <div>
    <h3><a href="https://www.funxlib.com">Funx - Functional Programming for Elixir</a></h3>
    <p>A library of functional programming abstractions for Elixir, including monads, monoids, Eq, Ord, and more. Built as an ecosystem where learning is the priority from the start.</p>
  </div>
</div>]]></content><author><name>Joseph Koski</name></author><category term="elixir" /><category term="funx" /><summary type="html"><![CDATA[Reducing degrees of freedom to make rules more dependable.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.joekoski.com/assets/images/funx-social.jpg" /><media:content medium="image" url="https://www.joekoski.com/assets/images/funx-social.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Funx: Free Your Predicates</title><link href="https://www.joekoski.com/blog/2026/02/01/funx-pred-dsl.html" rel="alternate" type="text/html" title="Funx: Free Your Predicates" /><published>2026-02-01T14:16:06+00:00</published><updated>2026-02-01T14:16:06+00:00</updated><id>https://www.joekoski.com/blog/2026/02/01/funx-pred-dsl</id><content type="html" xml:base="https://www.joekoski.com/blog/2026/02/01/funx-pred-dsl.html"><![CDATA[<blockquote>
  <p>“A complex system that works is invariably found to have evolved from a simple system that worked.” — John Gall</p>
</blockquote>

<p><a href="https://livebook.dev/run?url=https%3A%2F%2Fwww.joekoski.com%2Fassets%2Flivebooks%2Fblogs%2Ffunx-pred-dsl.livemd"><img src="https://livebook.dev/badge/v1/black.svg" alt="Run in Livebook" /></a></p>

<h2 id="what-is-free">What is Free?</h2>

<p>Functional programming likes to borrow mathematical terms, one of which is <code class="language-plaintext highlighter-rouge">free</code>.</p>

<p>In FP, <code class="language-plaintext highlighter-rouge">free</code> means: build a description first, interpret it later.</p>

<p>Here are a couple of basic boolean functions:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">positive?</span> <span class="o">=</span> <span class="k">fn</span> <span class="n">x</span> <span class="o">-&gt;</span> <span class="n">x</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="k">end</span>
<span class="n">even?</span> <span class="o">=</span> <span class="k">fn</span> <span class="n">x</span> <span class="o">-&gt;</span> <span class="n">rem</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span> <span class="k">end</span>
</code></pre></div></div>

<p>When we apply them, they collapse to booleans:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">positive?</span><span class="o">.</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span>  <span class="c1"># true</span>
<span class="n">positive?</span><span class="o">.</span><span class="p">(</span><span class="o">-</span><span class="mi">2</span><span class="p">)</span> <span class="c1"># false</span>
<span class="n">even?</span><span class="o">.</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span>      <span class="c1"># true</span>
<span class="n">even?</span><span class="o">.</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>      <span class="c1"># false</span>
</code></pre></div></div>

<p>We can combine them:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">positive_and_even?</span> <span class="o">=</span> <span class="k">fn</span> <span class="n">x</span> <span class="o">-&gt;</span> <span class="n">positive?</span><span class="o">.</span><span class="p">(</span><span class="n">x</span><span class="p">)</span> <span class="ow">and</span> <span class="n">even?</span><span class="o">.</span><span class="p">(</span><span class="n">x</span><span class="p">)</span> <span class="k">end</span>
<span class="n">positive_or_even?</span>  <span class="o">=</span> <span class="k">fn</span> <span class="n">x</span> <span class="o">-&gt;</span> <span class="n">positive?</span><span class="o">.</span><span class="p">(</span><span class="n">x</span><span class="p">)</span> <span class="ow">or</span>  <span class="n">even?</span><span class="o">.</span><span class="p">(</span><span class="n">x</span><span class="p">)</span> <span class="k">end</span>

<span class="n">positive_and_even?</span><span class="o">.</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>  <span class="c1"># false</span>
<span class="n">positive_and_even?</span><span class="o">.</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span>  <span class="c1"># true</span>
<span class="n">positive_and_even?</span><span class="o">.</span><span class="p">(</span><span class="o">-</span><span class="mi">2</span><span class="p">)</span> <span class="c1"># false</span>

<span class="n">positive_or_even?</span><span class="o">.</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>   <span class="c1"># true</span>
<span class="n">positive_or_even?</span><span class="o">.</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span>   <span class="c1"># true</span>
<span class="n">positive_or_even?</span><span class="o">.</span><span class="p">(</span><span class="o">-</span><span class="mi">2</span><span class="p">)</span>  <span class="c1"># true</span>
<span class="n">positive_or_even?</span><span class="o">.</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">)</span>  <span class="c1"># false</span>
</code></pre></div></div>

<p>We run the checks inline and bake in the grouping (<code class="language-plaintext highlighter-rouge">and</code>/<code class="language-plaintext highlighter-rouge">or</code>).</p>

<p>Funx has <code class="language-plaintext highlighter-rouge">p_all/1</code> and <code class="language-plaintext highlighter-rouge">p_any/1</code>, which take lists of predicates and lets us declare the grouping logic:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">positive_and_even?</span> <span class="o">=</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Predicate</span><span class="o">.</span><span class="n">p_all</span><span class="p">([</span><span class="n">positive?</span><span class="p">,</span> <span class="n">even?</span><span class="p">])</span>
<span class="n">positive_or_even?</span> <span class="o">=</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Predicate</span><span class="o">.</span><span class="n">p_any</span><span class="p">([</span><span class="n">positive?</span><span class="p">,</span> <span class="n">even?</span><span class="p">])</span>

<span class="n">positive_and_even?</span><span class="o">.</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span> <span class="c1"># false</span>
<span class="n">positive_or_even?</span><span class="o">.</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>  <span class="c1"># true</span>
</code></pre></div></div>

<p>Funx also has a predicate DSL, which can be easier to read, particularly for more complex logic.</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">use</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Predicate</span>

<span class="n">positive_and_even?</span> <span class="o">=</span>
  <span class="n">pred</span> <span class="k">do</span>
    <span class="n">positive?</span>
    <span class="n">even?</span>
  <span class="k">end</span>

<span class="n">positive_or_even?</span> <span class="o">=</span>
  <span class="n">pred</span> <span class="k">do</span>
    <span class="n">any</span> <span class="k">do</span>
      <span class="n">positive?</span>
      <span class="n">even?</span>
    <span class="k">end</span>
  <span class="k">end</span>

<span class="n">positive_and_even?</span><span class="o">.</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span> <span class="c1"># false</span>
<span class="n">positive_or_even?</span><span class="o">.</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>  <span class="c1"># true</span>
</code></pre></div></div>

<p>Let’s look at a role-playing game.</p>

<h2 id="the-rules">The Rules</h2>

<p>First, we define our game’s rules:</p>

<table>
  <thead>
    <tr>
      <th>Predicate</th>
      <th>Rule</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">poisoned?</code></td>
      <td>Poison is active</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">poison_resistant?</code></td>
      <td>Blessing grants poison resistance</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">poison_danger?</code></td>
      <td>Poisoned AND NOT resistant</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">bleeding?</code></td>
      <td>Bleeding is NOT staunched</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">severe_bleeding?</code></td>
      <td>Bleeding AND moderate+</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">wet?</code></td>
      <td>Water exposure is wet OR soaked</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">charge_building?</code></td>
      <td>Electrical charge building</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">electrocution_danger?</code></td>
      <td>Wet AND charge building</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">exhausted?</code></td>
      <td>Stamina below 25%</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">collapsed?</code></td>
      <td>Stamina below 10%</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">death_spiral?</code></td>
      <td>Exhausted AND bleeding</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">mortal_danger?</code></td>
      <td>Any mortal danger</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">can_staunch?</code></td>
      <td>Bleeding AND has bandage</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">can_cure_poison?</code></td>
      <td>Poisoned AND has antidote</td>
    </tr>
  </tbody>
</table>

<p>We can express these with predicates:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">defmodule</span> <span class="no">Status</span> <span class="k">do</span>
  <span class="k">defstruct</span> <span class="p">[</span><span class="ss">:poison</span><span class="p">,</span> <span class="ss">:bleeding</span><span class="p">,</span> <span class="ss">:exposure</span><span class="p">,</span> <span class="ss">:stamina</span><span class="p">,</span> <span class="ss">:blessing</span><span class="p">,</span> <span class="ss">:inventory</span><span class="p">]</span>
  <span class="kn">use</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Predicate</span>

  <span class="k">def</span> <span class="n">poisoned?</span> <span class="k">do</span>
    <span class="n">pred</span> <span class="k">do</span>
      <span class="n">check</span> <span class="p">[</span><span class="ss">:poison</span><span class="p">,</span> <span class="ss">:active</span><span class="p">],</span> <span class="k">fn</span> <span class="n">active</span> <span class="o">-&gt;</span> <span class="n">active</span> <span class="o">==</span> <span class="no">true</span> <span class="k">end</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">bleeding?</span> <span class="k">do</span>
    <span class="n">pred</span> <span class="k">do</span>
      <span class="n">check</span> <span class="p">[</span><span class="ss">:bleeding</span><span class="p">,</span> <span class="ss">:staunched</span><span class="p">],</span> <span class="k">fn</span> <span class="n">staunched</span> <span class="o">-&gt;</span> <span class="n">staunched</span> <span class="o">==</span> <span class="no">false</span> <span class="k">end</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">poison_resistant?</span> <span class="k">do</span>
    <span class="n">pred</span> <span class="k">do</span>
      <span class="n">check</span> <span class="p">[</span><span class="ss">:blessing</span><span class="p">,</span> <span class="ss">:grants</span><span class="p">],</span> <span class="k">fn</span> <span class="n">grants</span> <span class="o">-&gt;</span> <span class="ss">:poison_resistance</span> <span class="ow">in</span> <span class="n">grants</span> <span class="k">end</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">poison_danger?</span> <span class="k">do</span>
    <span class="n">pred</span> <span class="k">do</span>
      <span class="n">poisoned?</span><span class="p">()</span>
      <span class="n">negate</span> <span class="n">poison_resistant?</span><span class="p">()</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">severe_bleeding?</span> <span class="k">do</span>
    <span class="n">pred</span> <span class="k">do</span>
      <span class="n">bleeding?</span><span class="p">()</span>
      <span class="n">check</span> <span class="p">[</span><span class="ss">:bleeding</span><span class="p">,</span> <span class="ss">:severity</span><span class="p">],</span> <span class="k">fn</span> <span class="n">severity</span> <span class="o">-&gt;</span> <span class="n">severity</span> <span class="ow">in</span> <span class="p">[</span><span class="ss">:moderate</span><span class="p">,</span> <span class="ss">:severe</span><span class="p">,</span> <span class="ss">:critical</span><span class="p">]</span> <span class="k">end</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">wet?</span> <span class="k">do</span>
    <span class="n">pred</span> <span class="k">do</span>
      <span class="n">check</span> <span class="p">[</span><span class="ss">:exposure</span><span class="p">,</span> <span class="ss">:water</span><span class="p">],</span> <span class="k">fn</span> <span class="n">water</span> <span class="o">-&gt;</span> <span class="n">water</span> <span class="ow">in</span> <span class="p">[</span><span class="ss">:wet</span><span class="p">,</span> <span class="ss">:soaked</span><span class="p">]</span> <span class="k">end</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">charge_building?</span> <span class="k">do</span>
    <span class="n">pred</span> <span class="k">do</span>
      <span class="n">check</span> <span class="p">[</span><span class="ss">:exposure</span><span class="p">,</span> <span class="ss">:electricity</span><span class="p">],</span> <span class="k">fn</span> <span class="n">electricity</span> <span class="o">-&gt;</span> <span class="n">electricity</span> <span class="o">==</span> <span class="ss">:building</span> <span class="k">end</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">electrocution_danger?</span> <span class="k">do</span>
    <span class="n">pred</span> <span class="k">do</span>
      <span class="n">wet?</span><span class="p">()</span>
      <span class="n">charge_building?</span><span class="p">()</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">exhausted?</span> <span class="k">do</span>
    <span class="n">pred</span> <span class="k">do</span>
      <span class="n">check</span> <span class="ss">:stamina</span><span class="p">,</span> <span class="k">fn</span> <span class="n">s</span> <span class="o">-&gt;</span> <span class="n">s</span><span class="o">.</span><span class="n">current</span> <span class="o">/</span> <span class="n">s</span><span class="o">.</span><span class="n">max</span> <span class="o">&lt;</span> <span class="mf">0.25</span> <span class="k">end</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">collapsed?</span> <span class="k">do</span>
    <span class="n">pred</span> <span class="k">do</span>
      <span class="n">check</span> <span class="ss">:stamina</span><span class="p">,</span> <span class="k">fn</span> <span class="n">s</span> <span class="o">-&gt;</span> <span class="n">s</span><span class="o">.</span><span class="n">current</span> <span class="o">/</span> <span class="n">s</span><span class="o">.</span><span class="n">max</span> <span class="o">&lt;</span> <span class="mf">0.1</span> <span class="k">end</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">death_spiral?</span> <span class="k">do</span>
    <span class="n">pred</span> <span class="k">do</span>
      <span class="n">exhausted?</span><span class="p">()</span>
      <span class="n">bleeding?</span><span class="p">()</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">mortal_danger?</span> <span class="k">do</span>
    <span class="n">pred</span> <span class="k">do</span>
      <span class="n">any</span> <span class="k">do</span>
        <span class="n">electrocution_danger?</span><span class="p">()</span>
        <span class="n">death_spiral?</span><span class="p">()</span>
        <span class="n">severe_bleeding?</span><span class="p">()</span>
        <span class="n">collapsed?</span><span class="p">()</span>
      <span class="k">end</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">can_staunch?</span> <span class="k">do</span>
    <span class="n">pred</span> <span class="k">do</span>
      <span class="n">bleeding?</span><span class="p">()</span>
      <span class="n">check</span> <span class="p">[</span><span class="ss">:inventory</span><span class="p">,</span> <span class="ss">:bandage</span><span class="p">],</span> <span class="k">fn</span> <span class="n">count</span> <span class="o">-&gt;</span> <span class="n">count</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="k">end</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">can_cure_poison?</span> <span class="k">do</span>
    <span class="n">pred</span> <span class="k">do</span>
      <span class="n">poisoned?</span><span class="p">()</span>
      <span class="n">check</span> <span class="p">[</span><span class="ss">:inventory</span><span class="p">,</span> <span class="ss">:antidote</span><span class="p">],</span> <span class="k">fn</span> <span class="n">count</span> <span class="o">-&gt;</span> <span class="n">count</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="k">end</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Take a minute to look at this code. These are free predicates. They describe our domain rules without being embedded in control flow, and they are not executed until we interpret them.</p>

<p>When we return in six months, that separation matters. We can quickly see what facts exist, how they build on one another, and where to make a change when the rules evolve, without having to hunt through application logic.</p>

<p>If we have done our job correctly, our subject matter experts should be able to read through these functions and confirm the rules.</p>

<h2 id="the-character">The Character</h2>

<p>A character has a name and a status:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">defmodule</span> <span class="no">Character</span> <span class="k">do</span>
  <span class="n">alias</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Optics</span><span class="o">.</span><span class="no">Lens</span>

  <span class="k">defstruct</span> <span class="p">[</span><span class="ss">:name</span><span class="p">,</span> <span class="ss">:status</span><span class="p">]</span>

  <span class="k">def</span> <span class="n">status_lens</span><span class="p">,</span> <span class="k">do</span><span class="p">:</span> <span class="no">Lens</span><span class="o">.</span><span class="n">key</span><span class="p">(</span><span class="ss">:status</span><span class="p">)</span>

  <span class="k">def</span> <span class="n">status_check</span><span class="p">(%</span><span class="bp">__MODULE__</span><span class="p">{}</span> <span class="o">=</span> <span class="n">character</span><span class="p">)</span> <span class="k">do</span>
    <span class="n">status</span> <span class="o">=</span> <span class="no">Lens</span><span class="o">.</span><span class="n">view!</span><span class="p">(</span><span class="n">character</span><span class="p">,</span> <span class="n">status_lens</span><span class="p">())</span>

    <span class="p">%{</span>
      <span class="ss">poisoned:</span> <span class="no">Status</span><span class="o">.</span><span class="n">poisoned?</span><span class="o">.</span><span class="p">(</span><span class="n">status</span><span class="p">),</span>
      <span class="ss">poison_resistant:</span> <span class="no">Status</span><span class="o">.</span><span class="n">poison_resistant?</span><span class="o">.</span><span class="p">(</span><span class="n">status</span><span class="p">),</span>
      <span class="ss">poison_danger:</span> <span class="no">Status</span><span class="o">.</span><span class="n">poison_danger?</span><span class="o">.</span><span class="p">(</span><span class="n">status</span><span class="p">),</span>
      <span class="ss">bleeding:</span> <span class="no">Status</span><span class="o">.</span><span class="n">bleeding?</span><span class="o">.</span><span class="p">(</span><span class="n">status</span><span class="p">),</span>
      <span class="ss">severe_bleeding:</span> <span class="no">Status</span><span class="o">.</span><span class="n">severe_bleeding?</span><span class="o">.</span><span class="p">(</span><span class="n">status</span><span class="p">),</span>
      <span class="ss">electrocution_danger:</span> <span class="no">Status</span><span class="o">.</span><span class="n">electrocution_danger?</span><span class="o">.</span><span class="p">(</span><span class="n">status</span><span class="p">),</span>
      <span class="ss">exhausted:</span> <span class="no">Status</span><span class="o">.</span><span class="n">exhausted?</span><span class="o">.</span><span class="p">(</span><span class="n">status</span><span class="p">),</span>
      <span class="ss">collapsed:</span> <span class="no">Status</span><span class="o">.</span><span class="n">collapsed?</span><span class="o">.</span><span class="p">(</span><span class="n">status</span><span class="p">),</span>
      <span class="ss">death_spiral:</span> <span class="no">Status</span><span class="o">.</span><span class="n">death_spiral?</span><span class="o">.</span><span class="p">(</span><span class="n">status</span><span class="p">),</span>
      <span class="ss">mortal_danger:</span> <span class="no">Status</span><span class="o">.</span><span class="n">mortal_danger?</span><span class="o">.</span><span class="p">(</span><span class="n">status</span><span class="p">)</span>
    <span class="p">}</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">actions</span><span class="p">(%</span><span class="bp">__MODULE__</span><span class="p">{}</span> <span class="o">=</span> <span class="n">character</span><span class="p">)</span> <span class="k">do</span>
    <span class="n">status</span> <span class="o">=</span> <span class="no">Lens</span><span class="o">.</span><span class="n">view!</span><span class="p">(</span><span class="n">character</span><span class="p">,</span> <span class="n">status_lens</span><span class="p">())</span>

    <span class="p">%{</span>
      <span class="ss">can_staunch:</span> <span class="no">Status</span><span class="o">.</span><span class="n">can_staunch?</span><span class="o">.</span><span class="p">(</span><span class="n">status</span><span class="p">),</span>
      <span class="ss">can_cure_poison:</span> <span class="no">Status</span><span class="o">.</span><span class="n">can_cure_poison?</span><span class="o">.</span><span class="p">(</span><span class="n">status</span><span class="p">)</span>
    <span class="p">}</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Here, we are interpreting all of our predicates in two functions, <code class="language-plaintext highlighter-rouge">status_check/1</code> and <code class="language-plaintext highlighter-rouge">actions/1</code>.</p>

<h3 id="a-character-in-trouble">A character in trouble</h3>

<p>Let’s start with a character:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">warrior</span> <span class="o">=</span> <span class="p">%</span><span class="no">Character</span><span class="p">{</span>
  <span class="ss">name:</span> <span class="s2">"Wounded Warrior"</span><span class="p">,</span>
  <span class="ss">status:</span> <span class="p">%</span><span class="no">Status</span><span class="p">{</span>
    <span class="ss">poison:</span> <span class="p">%{</span><span class="ss">active:</span> <span class="no">true</span><span class="p">,</span> <span class="ss">source:</span> <span class="ss">:spider</span><span class="p">,</span> <span class="ss">severity:</span> <span class="ss">:moderate</span><span class="p">},</span>
    <span class="ss">bleeding:</span> <span class="p">%{</span><span class="ss">severity:</span> <span class="ss">:light</span><span class="p">,</span> <span class="ss">staunched:</span> <span class="no">false</span><span class="p">},</span>
    <span class="ss">exposure:</span> <span class="p">%{</span><span class="ss">water:</span> <span class="ss">:soaked</span><span class="p">,</span> <span class="ss">electricity:</span> <span class="ss">:building</span><span class="p">},</span>
    <span class="ss">stamina:</span> <span class="p">%{</span><span class="ss">current:</span> <span class="mi">20</span><span class="p">,</span> <span class="ss">max:</span> <span class="mi">100</span><span class="p">},</span>
    <span class="ss">blessing:</span> <span class="p">%{</span><span class="ss">grants:</span> <span class="p">[</span><span class="ss">:poison_resistance</span><span class="p">]},</span>
    <span class="ss">inventory:</span> <span class="p">%{</span><span class="ss">antidote:</span> <span class="mi">1</span><span class="p">,</span> <span class="ss">bandage:</span> <span class="mi">2</span><span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>When we apply the <code class="language-plaintext highlighter-rouge">status_check/1</code>:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Character</span><span class="o">.</span><span class="n">status_check</span><span class="p">(</span><span class="n">warrior</span><span class="p">)</span>

<span class="c1"># %{</span>
<span class="c1">#   bleeding: true,</span>
<span class="c1">#   poisoned: true,</span>
<span class="c1">#   poison_resistant: true,</span>
<span class="c1">#   poison_danger: false,</span>
<span class="c1">#   severe_bleeding: false,</span>
<span class="c1">#   electrocution_danger: true,</span>
<span class="c1">#   exhausted: true,</span>
<span class="c1">#   collapsed: false,</span>
<span class="c1">#   death_spiral: true,</span>
<span class="c1">#   mortal_danger: true</span>
<span class="c1"># }</span>
</code></pre></div></div>

<p>We find our character is in danger:</p>

<ul>
  <li>Poisoned, but resistant: no poison danger</li>
  <li>Bleeding, but light: no severe bleeding</li>
  <li>Soaked + charge building: electrocution danger</li>
  <li>Exhausted + bleeding: death spiral</li>
  <li>Mortal danger: true</li>
</ul>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Character</span><span class="o">.</span><span class="n">actions</span><span class="p">(</span><span class="n">warrior</span><span class="p">)</span>

<span class="c1"># %{can_staunch: true, can_cure_poison: true}</span>
</code></pre></div></div>

<p>Fortunately, our warrior has some options: they <code class="language-plaintext highlighter-rouge">can_staunch</code> and <code class="language-plaintext highlighter-rouge">can_cure_poison</code>.</p>

<p>Let’s have them escape the water and apply a bandage:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">warrior_after</span> <span class="o">=</span> <span class="p">%</span><span class="no">Character</span><span class="p">{</span>
  <span class="ss">name:</span> <span class="s2">"Wounded Warrior"</span><span class="p">,</span>
  <span class="ss">status:</span> <span class="p">%</span><span class="no">Status</span><span class="p">{</span>
    <span class="ss">poison:</span> <span class="p">%{</span><span class="ss">active:</span> <span class="no">true</span><span class="p">,</span> <span class="ss">source:</span> <span class="ss">:spider</span><span class="p">,</span> <span class="ss">severity:</span> <span class="ss">:moderate</span><span class="p">},</span>
    <span class="ss">bleeding:</span> <span class="p">%{</span><span class="ss">severity:</span> <span class="ss">:light</span><span class="p">,</span> <span class="ss">staunched:</span> <span class="no">true</span><span class="p">},</span>
    <span class="ss">exposure:</span> <span class="p">%{</span><span class="ss">water:</span> <span class="ss">:dry</span><span class="p">,</span> <span class="ss">electricity:</span> <span class="ss">:building</span><span class="p">},</span>
    <span class="ss">stamina:</span> <span class="p">%{</span><span class="ss">current:</span> <span class="mi">20</span><span class="p">,</span> <span class="ss">max:</span> <span class="mi">100</span><span class="p">},</span>
    <span class="ss">blessing:</span> <span class="p">%{</span><span class="ss">grants:</span> <span class="p">[</span><span class="ss">:poison_resistance</span><span class="p">]},</span>
    <span class="ss">inventory:</span> <span class="p">%{</span><span class="ss">antidote:</span> <span class="mi">1</span><span class="p">,</span> <span class="ss">bandage:</span> <span class="mi">1</span><span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Now when we check their status:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Character</span><span class="o">.</span><span class="n">status_check</span><span class="p">(</span><span class="n">warrior_after</span><span class="p">)</span>

<span class="c1"># %{</span>
<span class="c1">#   bleeding: false,</span>
<span class="c1">#   poisoned: true,</span>
<span class="c1">#   poison_resistant: true,</span>
<span class="c1">#   poison_danger: false,</span>
<span class="c1">#   severe_bleeding: false,</span>
<span class="c1">#   electrocution_danger: false,</span>
<span class="c1">#   exhausted: true,</span>
<span class="c1">#   collapsed: false,</span>
<span class="c1">#   death_spiral: false,</span>
<span class="c1">#   mortal_danger: false</span>
<span class="c1"># }</span>
</code></pre></div></div>

<p>They are no longer in immediate danger:</p>

<ul>
  <li>Electrocution danger: false</li>
  <li>Death spiral: false</li>
  <li>Mortal danger: false</li>
</ul>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Character</span><span class="o">.</span><span class="n">actions</span><span class="p">(</span><span class="n">warrior_after</span><span class="p">)</span>

<span class="c1"># %{can_staunch: false, can_cure_poison: true}</span>
</code></pre></div></div>

<p>Even though they still have a bandage available, they are no longer bleeding, so <code class="language-plaintext highlighter-rouge">can_staunch</code> is false. They still have an antidote, so <code class="language-plaintext highlighter-rouge">can_cure_poison</code> remains true.</p>

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

<p>We want our rules to read like rules. We want them named, composed, and grouped in a way we can scan quickly. We want our rules to reflect our domain’s shared language. And when the rules change, we want to be able to quickly dive into the code, make the change, and trust everything built on top will hold.</p>

<h2 id="resources">Resources</h2>

<div style="display: flex; gap: 20px; margin: 20px 0;">
  <div style="flex-shrink: 0;">
    <a href="https://pragprog.com/titles/jkelixir/advanced-functional-programming-with-elixir">
      <img src="/assets/images/jkelixir_small.jpg" alt="Advanced Functional Programming with Elixir book cover" width="150" />
    </a>
  </div>
  <div>
    <h3><a href="https://pragprog.com/titles/jkelixir/advanced-functional-programming-with-elixir">Advanced Functional Programming with Elixir</a></h3>
    <p>Dive deeper into functional programming patterns and advanced Elixir techniques. Learn how to build robust, maintainable applications using functional programming principles.</p>
  </div>
</div>

<div style="display: flex; gap: 20px; margin: 20px 0;">
  <div style="flex-shrink: 0;">
    <a href="https://www.funxlib.com">
      <img src="/assets/images/funx-social.jpg" alt="Funx functional programming library" width="150" />
    </a>
  </div>
  <div>
    <h3><a href="https://www.funxlib.com">Funx - Functional Programming for Elixir</a></h3>
    <p>A library of functional programming abstractions for Elixir, including monads, monoids, Eq, Ord, and more. Built as an ecosystem where learning is the priority from the start.</p>
  </div>
</div>]]></content><author><name>Joseph Koski</name></author><category term="elixir" /><category term="funx" /><summary type="html"><![CDATA[“A complex system that works is invariably found to have evolved from a simple system that worked.” — John Gall]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.joekoski.com/assets/images/funx-social.jpg" /><media:content medium="image" url="https://www.joekoski.com/assets/images/funx-social.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Funx: Equality as a Domain Rule</title><link href="https://www.joekoski.com/blog/2026/01/26/funx-eq-dsl.html" rel="alternate" type="text/html" title="Funx: Equality as a Domain Rule" /><published>2026-01-26T14:16:06+00:00</published><updated>2026-01-26T14:16:06+00:00</updated><id>https://www.joekoski.com/blog/2026/01/26/funx-eq-dsl</id><content type="html" xml:base="https://www.joekoski.com/blog/2026/01/26/funx-eq-dsl.html"><![CDATA[<blockquote>
  <p>“A model is a selectively simplified and consciously structured form of knowledge.” — Eric Evans</p>
</blockquote>

<p>Equality quietly drives a lot of Elixir code: de-duplication, membership checks, grouping, rule matching, and validation all hinge on what it means for two things to be “the same.” Most code leaves that definition implicit, relying on structural <code class="language-plaintext highlighter-rouge">==</code> or scattering projections like <code class="language-plaintext highlighter-rouge">uniq_by/2</code> throughout the codebase.</p>

<p>Funx provides <code class="language-plaintext highlighter-rouge">Eq</code>. By default it delegates to Elixir’s structural equality. However, we can define a default via Elixir’s protocol, or pass an explicit <code class="language-plaintext highlighter-rouge">Eq</code> when a different comparison is required.</p>

<p><a href="https://livebook.dev/run?url=https%3A%2F%2Fwww.joekoski.com%2Fassets%2Flivebooks%2Fblogs%2Ffunx-eq-dsl.livemd"><img src="https://livebook.dev/badge/v1/black.svg" alt="Run in Livebook" /></a></p>

<h2 id="playing-card">Playing Card</h2>

<p>Let’s look at <code class="language-plaintext highlighter-rouge">Eq</code> from the perspective of a playing card:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">defmodule</span> <span class="no">Card</span> <span class="k">do</span>
  <span class="k">defstruct</span> <span class="p">[</span><span class="ss">:id</span><span class="p">,</span> <span class="ss">:rank</span><span class="p">,</span> <span class="ss">:suit</span><span class="p">]</span>

  <span class="k">def</span> <span class="n">new</span><span class="p">(</span><span class="n">rank</span><span class="p">,</span> <span class="n">suit</span><span class="p">)</span> <span class="k">do</span>
    <span class="p">%</span><span class="bp">__MODULE__</span><span class="p">{</span>
      <span class="ss">id:</span> <span class="ss">:erlang</span><span class="o">.</span><span class="n">unique_integer</span><span class="p">([</span><span class="ss">:positive</span><span class="p">])</span> <span class="o">|&gt;</span> <span class="no">Integer</span><span class="o">.</span><span class="n">to_string</span><span class="p">(),</span>
      <span class="ss">rank:</span> <span class="n">rank</span><span class="p">,</span>
      <span class="ss">suit:</span> <span class="n">suit</span>
    <span class="p">}</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>First, we need some <code class="language-plaintext highlighter-rouge">Card</code> data:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">four_heart</span> <span class="o">=</span> <span class="no">Card</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="s2">"4"</span><span class="p">,</span> <span class="s2">"H"</span><span class="p">)</span>
<span class="n">ten_heart</span> <span class="o">=</span> <span class="no">Card</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="s2">"10"</span><span class="p">,</span> <span class="s2">"H"</span><span class="p">)</span>

<span class="n">four_spade</span> <span class="o">=</span> <span class="no">Card</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="s2">"4"</span><span class="p">,</span> <span class="s2">"S"</span><span class="p">)</span>
<span class="n">ten_spade</span> <span class="o">=</span> <span class="no">Card</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="s2">"10"</span><span class="p">,</span> <span class="s2">"S"</span><span class="p">)</span>
</code></pre></div></div>

<p>Elixir uses <code class="language-plaintext highlighter-rouge">==</code>:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">four_heart</span> <span class="o">==</span> <span class="n">four_heart</span> <span class="c1"># true</span>
<span class="n">four_heart</span> <span class="o">==</span> <span class="n">four_spade</span> <span class="c1"># false</span>
</code></pre></div></div>

<p>Funx has <code class="language-plaintext highlighter-rouge">Eq.eq?/2</code>, which defaults to Elixir’s <code class="language-plaintext highlighter-rouge">==</code>:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">alias</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Eq</span>

<span class="no">Eq</span><span class="o">.</span><span class="n">eq?</span><span class="p">(</span><span class="n">four_heart</span><span class="p">,</span> <span class="n">four_heart</span><span class="p">)</span>

<span class="c1"># true</span>
</code></pre></div></div>

<p>Where a card is equal to itself.</p>

<p>And different cards are not equal:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Eq</span><span class="o">.</span><span class="n">eq?</span><span class="p">(</span><span class="n">four_heart</span><span class="p">,</span> <span class="n">four_spade</span><span class="p">)</span>

<span class="c1"># false</span>
</code></pre></div></div>

<p>But we can go further, Funx’s <code class="language-plaintext highlighter-rouge">Eq.eq?/3</code> accepts an <code class="language-plaintext highlighter-rouge">Eq</code> instance.</p>

<p>It’s possible to construct an <code class="language-plaintext highlighter-rouge">Eq</code> instance by hand, but it’s easier using Funx’s <code class="language-plaintext highlighter-rouge">Eq</code> DSL:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">use</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Eq</span>

<span class="n">rank_eq</span> <span class="o">=</span>
  <span class="n">eq</span> <span class="k">do</span>
    <span class="n">on</span> <span class="ss">:rank</span>
  <span class="k">end</span>

<span class="no">Eq</span><span class="o">.</span><span class="n">eq?</span><span class="p">(</span><span class="n">four_heart</span><span class="p">,</span> <span class="n">four_spade</span><span class="p">,</span> <span class="n">rank_eq</span><span class="p">)</span>

<span class="c1"># true</span>
</code></pre></div></div>

<p>Now the same card rank evaluates to <code class="language-plaintext highlighter-rouge">true</code>.</p>

<p>And different ranks evaluate to <code class="language-plaintext highlighter-rouge">false</code>:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Eq</span><span class="o">.</span><span class="n">eq?</span><span class="p">(</span><span class="n">four_heart</span><span class="p">,</span> <span class="n">ten_heart</span><span class="p">,</span> <span class="n">rank_eq</span><span class="p">)</span>

<span class="c1"># false</span>
</code></pre></div></div>

<p>Cards can also be equal by suit:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">suit_eq</span> <span class="o">=</span>
  <span class="n">eq</span> <span class="k">do</span>
    <span class="n">on</span> <span class="ss">:suit</span>
  <span class="k">end</span>

<span class="no">Eq</span><span class="o">.</span><span class="n">eq?</span><span class="p">(</span><span class="n">four_heart</span><span class="p">,</span> <span class="n">ten_heart</span><span class="p">,</span> <span class="n">suit_eq</span><span class="p">)</span>

<span class="c1"># true</span>
</code></pre></div></div>

<p>Where different suits evaluate to <code class="language-plaintext highlighter-rouge">false</code>:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Eq</span><span class="o">.</span><span class="n">eq?</span><span class="p">(</span><span class="n">four_heart</span><span class="p">,</span> <span class="n">four_spade</span><span class="p">,</span> <span class="n">suit_eq</span><span class="p">)</span>

<span class="c1"># false</span>
</code></pre></div></div>

<p>Our deck can contain duplicates. Here is another four of hearts with a different <code class="language-plaintext highlighter-rouge">id</code>:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">four_heart_b</span> <span class="o">=</span> <span class="no">Card</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="s2">"4"</span><span class="p">,</span> <span class="s2">"H"</span><span class="p">)</span> <span class="c1"># different id</span>

<span class="no">Eq</span><span class="o">.</span><span class="n">eq?</span><span class="p">(</span><span class="n">four_heart</span><span class="p">,</span> <span class="n">four_heart_b</span><span class="p">)</span>

<span class="c1"># false</span>
</code></pre></div></div>

<p>Again, by default <code class="language-plaintext highlighter-rouge">Eq.eq?/2</code> uses Elixir’s structural equality, so the <code class="language-plaintext highlighter-rouge">id</code> matters.</p>

<p>But in our domain, the <code class="language-plaintext highlighter-rouge">id</code> is not part of card identity. We only care about suit and rank:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">card_eq</span> <span class="o">=</span>
  <span class="n">eq</span> <span class="k">do</span>
    <span class="n">on</span> <span class="ss">:suit</span>
    <span class="n">on</span> <span class="ss">:rank</span>
  <span class="k">end</span>

<span class="no">Eq</span><span class="o">.</span><span class="n">eq?</span><span class="p">(</span><span class="n">four_heart</span><span class="p">,</span> <span class="n">four_heart_b</span><span class="p">,</span> <span class="n">card_eq</span><span class="p">)</span>

<span class="c1"># true</span>
</code></pre></div></div>

<p>With our <code class="language-plaintext highlighter-rouge">card_eq</code> Funx understands that two cards with the same rank and suit are equal, regardless of their id.</p>

<p>We can express other equality, such as game rules:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">playable_eq</span> <span class="o">=</span>
  <span class="n">eq</span> <span class="k">do</span>
    <span class="n">any</span> <span class="k">do</span>
      <span class="n">on</span> <span class="ss">:suit</span>
      <span class="n">on</span> <span class="ss">:rank</span>
    <span class="k">end</span>
  <span class="k">end</span>

<span class="no">Eq</span><span class="o">.</span><span class="n">eq?</span><span class="p">(</span><span class="n">four_heart</span><span class="p">,</span> <span class="n">four_spade</span><span class="p">,</span> <span class="n">playable_eq</span><span class="p">)</span>

<span class="c1"># true</span>
</code></pre></div></div>

<p>Here, two cards with matching ranks are playable.</p>

<p>And two cards of the same suit are also playable:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Eq</span><span class="o">.</span><span class="n">eq?</span><span class="p">(</span><span class="n">four_heart</span><span class="p">,</span> <span class="n">ten_heart</span><span class="p">,</span> <span class="n">playable_eq</span><span class="p">)</span>

<span class="c1"># true</span>
</code></pre></div></div>

<p>But a card with a different suit and rank is not playable:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Eq</span><span class="o">.</span><span class="n">eq?</span><span class="p">(</span><span class="n">four_heart</span><span class="p">,</span> <span class="n">ten_spade</span><span class="p">,</span> <span class="n">playable_eq</span><span class="p">)</span>

<span class="c1"># false</span>
</code></pre></div></div>

<p>When we use atoms in the DSL, Funx is using the <code class="language-plaintext highlighter-rouge">Prism</code> optic behind the scenes.</p>

<p>Let’s see this with an incomplete record:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">incomplete_four</span> <span class="o">=</span> <span class="p">%{</span><span class="ss">rank:</span> <span class="s2">"4"</span><span class="p">}</span>

<span class="no">Eq</span><span class="o">.</span><span class="n">eq?</span><span class="p">(</span><span class="n">four_heart</span><span class="p">,</span> <span class="n">incomplete_four</span><span class="p">,</span> <span class="n">playable_eq</span><span class="p">)</span>

<span class="c1"># true</span>
</code></pre></div></div>

<p>A Prism on <code class="language-plaintext highlighter-rouge">:suit</code> focuses to <code class="language-plaintext highlighter-rouge">Nothing</code> when the key is missing, but the <code class="language-plaintext highlighter-rouge">any</code> block passes because <code class="language-plaintext highlighter-rouge">:rank</code> still matches.</p>

<p>If our domain requires that only a <code class="language-plaintext highlighter-rouge">Card</code> can match another <code class="language-plaintext highlighter-rouge">Card</code>, we can use <code class="language-plaintext highlighter-rouge">Prism.path/1</code> to explicitly include the <code class="language-plaintext highlighter-rouge">Card</code> struct:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">alias</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Optics</span><span class="o">.</span><span class="no">Prism</span>

<span class="n">playable_eq</span> <span class="o">=</span>
  <span class="n">eq</span> <span class="k">do</span>
    <span class="n">any</span> <span class="k">do</span>
      <span class="n">on</span> <span class="no">Prism</span><span class="o">.</span><span class="n">path</span><span class="p">([{</span><span class="no">Card</span><span class="p">,</span> <span class="ss">:suit</span><span class="p">}])</span>
      <span class="n">on</span> <span class="no">Prism</span><span class="o">.</span><span class="n">path</span><span class="p">([{</span><span class="no">Card</span><span class="p">,</span> <span class="ss">:rank</span><span class="p">}])</span>
    <span class="k">end</span>
  <span class="k">end</span>

<span class="no">Eq</span><span class="o">.</span><span class="n">eq?</span><span class="p">(</span><span class="n">four_heart</span><span class="p">,</span> <span class="n">incomplete_four</span><span class="p">,</span> <span class="n">playable_eq</span><span class="p">)</span>

<span class="c1"># false</span>
</code></pre></div></div>

<p>This narrows equality so that only a <code class="language-plaintext highlighter-rouge">Card</code> can match a <code class="language-plaintext highlighter-rouge">Card</code>.</p>

<p>But let’s see what happens when we compare two incomplete maps:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">incomplete_five</span> <span class="o">=</span> <span class="p">%{</span><span class="ss">rank:</span> <span class="s2">"5"</span><span class="p">}</span>
<span class="no">Eq</span><span class="o">.</span><span class="n">eq?</span><span class="p">(</span><span class="n">incomplete_four</span><span class="p">,</span> <span class="n">incomplete_five</span><span class="p">,</span> <span class="n">playable_eq</span><span class="p">)</span>

<span class="c1"># true</span>
</code></pre></div></div>

<p>The result is <code class="language-plaintext highlighter-rouge">true</code>, which is expected. In the context of <code class="language-plaintext highlighter-rouge">Card</code>, these values are <code class="language-plaintext highlighter-rouge">Nothing</code>, and two <code class="language-plaintext highlighter-rouge">Nothing</code> values are equal.</p>

<p>If our domain requires a focus to exist, we should switch to a <code class="language-plaintext highlighter-rouge">Lens</code> optic:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">alias</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Optics</span><span class="o">.</span><span class="no">Lens</span>

<span class="n">playable_eq</span> <span class="o">=</span>
  <span class="n">eq</span> <span class="k">do</span>
    <span class="n">any</span> <span class="k">do</span>
      <span class="n">on</span> <span class="no">Lens</span><span class="o">.</span><span class="n">key</span><span class="p">(</span><span class="ss">:suit</span><span class="p">)</span>
      <span class="n">on</span> <span class="no">Lens</span><span class="o">.</span><span class="n">key</span><span class="p">(</span><span class="ss">:rank</span><span class="p">)</span>
    <span class="k">end</span>
  <span class="k">end</span>
</code></pre></div></div>

<p>Now missing keys raise, enforcing the invariant at runtime (fail fast):</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Eq</span><span class="o">.</span><span class="n">eq?</span><span class="p">(</span><span class="n">four_heart</span><span class="p">,</span> <span class="n">incomplete_four</span><span class="p">,</span> <span class="n">playable_eq</span><span class="p">)</span>

<span class="c1"># ** (KeyError) key :suit not found in: %{rank: "4"}</span>
</code></pre></div></div>

<p>A <code class="language-plaintext highlighter-rouge">Lens</code> enforces the focus; it does not care if the values are <code class="language-plaintext highlighter-rouge">Card</code> structs:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">four_club_map</span> <span class="o">=</span> <span class="p">%{</span><span class="ss">rank:</span> <span class="s2">"4"</span><span class="p">,</span> <span class="ss">suit:</span> <span class="s2">"C"</span><span class="p">}</span>

<span class="no">Eq</span><span class="o">.</span><span class="n">eq?</span><span class="p">(</span><span class="n">four_heart</span><span class="p">,</span> <span class="n">four_club_map</span><span class="p">,</span> <span class="n">playable_eq</span><span class="p">)</span>

<span class="c1"># true</span>
</code></pre></div></div>

<p>Again, this is expected. A <code class="language-plaintext highlighter-rouge">Lens</code> works on a product type. If we needed to differentiate between two types (<code class="language-plaintext highlighter-rouge">Card</code> and <code class="language-plaintext highlighter-rouge">Map</code>), that’s a sum type problem, which is a job for the optic <code class="language-plaintext highlighter-rouge">Prism</code>.</p>

<h2 id="model-the-domain">Model the Domain</h2>

<p>When we model a domain, we don’t want our equality rules scattered throughout the code.</p>

<p>Instead, we can keep them within a module:</p>

<!-- livebook:{"force_markdown":true} -->

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">defmodule</span> <span class="no">Game</span><span class="o">.</span><span class="no">Card</span> <span class="k">do</span>
  <span class="kn">use</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Eq</span>
  <span class="kn">use</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Ord</span>
  <span class="kn">import</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Macros</span><span class="p">,</span> <span class="ss">only:</span> <span class="p">[</span><span class="ss">eq_for:</span> <span class="mi">2</span><span class="p">,</span> <span class="ss">ord_for:</span> <span class="mi">2</span><span class="p">]</span>
  <span class="n">alias</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Optics</span><span class="o">.</span><span class="no">Lens</span>

  <span class="k">defstruct</span> <span class="p">[</span><span class="ss">:id</span><span class="p">,</span> <span class="ss">:rank</span><span class="p">,</span> <span class="ss">:suit</span><span class="p">]</span>

  <span class="k">def</span> <span class="n">new</span><span class="p">(</span><span class="n">rank</span><span class="p">,</span> <span class="n">suit</span><span class="p">)</span> <span class="k">do</span>
    <span class="p">%</span><span class="bp">__MODULE__</span><span class="p">{</span>
      <span class="ss">id:</span> <span class="ss">:erlang</span><span class="o">.</span><span class="n">unique_integer</span><span class="p">([</span><span class="ss">:positive</span><span class="p">])</span> <span class="o">|&gt;</span> <span class="no">Integer</span><span class="o">.</span><span class="n">to_string</span><span class="p">(),</span>
      <span class="ss">rank:</span> <span class="n">rank</span><span class="p">,</span>
      <span class="ss">suit:</span> <span class="n">suit</span>
    <span class="p">}</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">suit_eq</span> <span class="k">do</span>
    <span class="n">eq</span> <span class="k">do</span>
      <span class="n">on</span> <span class="no">Lens</span><span class="o">.</span><span class="n">key</span><span class="p">(</span><span class="ss">:suit</span><span class="p">)</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">rank_eq</span> <span class="k">do</span>
    <span class="n">eq</span> <span class="k">do</span>
      <span class="n">on</span> <span class="no">Lens</span><span class="o">.</span><span class="n">key</span><span class="p">(</span><span class="ss">:rank</span><span class="p">)</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">suit_ord</span> <span class="k">do</span>
    <span class="n">ord</span> <span class="k">do</span>
      <span class="n">asc</span> <span class="no">Lens</span><span class="o">.</span><span class="n">key</span><span class="p">(</span><span class="ss">:suit</span><span class="p">)</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="n">eq_for</span><span class="p">(</span>
    <span class="no">Game</span><span class="o">.</span><span class="no">Card</span><span class="p">,</span>
    <span class="n">eq</span> <span class="k">do</span>
      <span class="n">on</span> <span class="no">Lens</span><span class="o">.</span><span class="n">key</span><span class="p">(</span><span class="ss">:rank</span><span class="p">)</span>
      <span class="n">on</span> <span class="no">Lens</span><span class="o">.</span><span class="n">key</span><span class="p">(</span><span class="ss">:suit</span><span class="p">)</span>
    <span class="k">end</span>
  <span class="p">)</span>

  <span class="n">ord_for</span><span class="p">(</span>
    <span class="no">Game</span><span class="o">.</span><span class="no">Card</span><span class="p">,</span>
    <span class="n">ord</span> <span class="k">do</span>
      <span class="n">asc</span> <span class="no">Lens</span><span class="o">.</span><span class="n">key</span><span class="p">(</span><span class="ss">:suit</span><span class="p">)</span>
      <span class="n">desc</span> <span class="no">Lens</span><span class="o">.</span><span class="n">key</span><span class="p">(</span><span class="ss">:rank</span><span class="p">)</span>
    <span class="k">end</span>
  <span class="p">)</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Let’s take a closer look at <code class="language-plaintext highlighter-rouge">eq_for/2</code>. This is how we tell Funx what the domain’s default equality is for a <code class="language-plaintext highlighter-rouge">Card</code>. Funx will use this rule instead of Elixir’s structural equality.</p>

<p>Let’s regenerate our cards, but this time using <code class="language-plaintext highlighter-rouge">Game.Card</code>:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">alias</span> <span class="no">Game</span><span class="o">.</span><span class="no">Card</span>

<span class="n">four_heart</span> <span class="o">=</span> <span class="no">Card</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="s2">"4"</span><span class="p">,</span> <span class="s2">"H"</span><span class="p">)</span>
<span class="n">ten_heart</span> <span class="o">=</span> <span class="no">Card</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="s2">"10"</span><span class="p">,</span> <span class="s2">"H"</span><span class="p">)</span>

<span class="n">four_spade</span> <span class="o">=</span> <span class="no">Card</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="s2">"4"</span><span class="p">,</span> <span class="s2">"S"</span><span class="p">)</span>
<span class="n">ten_spade</span> <span class="o">=</span> <span class="no">Card</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="s2">"10"</span><span class="p">,</span> <span class="s2">"S"</span><span class="p">)</span>
</code></pre></div></div>

<p>And we need the duplicate four of hearts (same suit and rank, different <code class="language-plaintext highlighter-rouge">id</code>):</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">four_heart_b</span> <span class="o">=</span> <span class="no">Card</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="s2">"4"</span><span class="p">,</span> <span class="s2">"H"</span><span class="p">)</span>
</code></pre></div></div>

<p>Now that <code class="language-plaintext highlighter-rouge">Game.Card</code> has a protocol <code class="language-plaintext highlighter-rouge">Eq</code>, Funx knows the domain rule, which is to ignore <code class="language-plaintext highlighter-rouge">id</code> and only focus on rank and suit:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Eq</span><span class="o">.</span><span class="n">eq?</span><span class="p">(</span><span class="n">four_heart</span><span class="p">,</span> <span class="n">four_heart_b</span><span class="p">)</span>

<span class="c1"># true</span>
</code></pre></div></div>

<p>Let’s see how that helps us with list operations.</p>

<h2 id="lists">Lists</h2>

<p>First, we need a list of cards:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">cards</span> <span class="o">=</span> <span class="p">[</span><span class="n">four_heart</span><span class="p">,</span> <span class="n">four_heart_b</span><span class="p">,</span> <span class="n">four_spade</span><span class="p">,</span> <span class="n">four_heart</span><span class="p">]</span>
</code></pre></div></div>

<p>Elixir’s <code class="language-plaintext highlighter-rouge">Enum.uniq/1</code> removes identical terms, but it doesn’t recognize that <code class="language-plaintext highlighter-rouge">four_heart_b</code> is semantically the same card:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Enum</span><span class="o">.</span><span class="n">uniq</span><span class="p">(</span><span class="n">cards</span><span class="p">)</span>

<span class="c1"># [</span>
<span class="c1">#   %Game.Card{id: "13859", rank: "4", suit: "H"},</span>
<span class="c1">#   %Game.Card{id: "13987", rank: "4", suit: "H"},</span>
<span class="c1">#   %Game.Card{id: "13923", rank: "4", suit: "S"}</span>
<span class="c1"># ]</span>
</code></pre></div></div>

<p>We could use Elixir’s <code class="language-plaintext highlighter-rouge">Enum.uniq_by/2</code>, where we can inject a projection:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Enum</span><span class="o">.</span><span class="n">uniq_by</span><span class="p">(</span><span class="n">cards</span><span class="p">,</span> <span class="k">fn</span> <span class="p">%</span><span class="no">Game</span><span class="o">.</span><span class="no">Card</span><span class="p">{</span><span class="ss">rank:</span> <span class="n">rank</span><span class="p">,</span> <span class="ss">suit:</span> <span class="n">suit</span><span class="p">}</span> <span class="o">-&gt;</span> <span class="p">{</span><span class="n">rank</span><span class="p">,</span> <span class="n">suit</span><span class="p">}</span> <span class="k">end</span><span class="p">)</span>

<span class="c1"># [</span>
<span class="c1">#   %Game.Card{id: "13859", rank: "4", suit: "H"}, </span>
<span class="c1">#   %Game.Card{id: "13923", rank: "4", suit: "S"}</span>
<span class="c1"># ]</span>
</code></pre></div></div>

<p>This works, but it tends to spread the domain rule across the codebase.</p>

<p>With Funx, <code class="language-plaintext highlighter-rouge">uniq</code> already understands the domain rules for a <code class="language-plaintext highlighter-rouge">Card</code>:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Funx</span><span class="o">.</span><span class="no">List</span><span class="o">.</span><span class="n">uniq</span><span class="p">(</span><span class="n">cards</span><span class="p">)</span>

<span class="c1"># [</span>
<span class="c1">#   %Game.Card{id: "13859", rank: "4", suit: "H"}, </span>
<span class="c1">#   %Game.Card{id: "13923", rank: "4", suit: "S"}</span>
<span class="c1"># ]</span>
</code></pre></div></div>

<p>Elixir also has <code class="language-plaintext highlighter-rouge">Enum.member?/2</code>, which checks whether an equal term exists in the list:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Enum</span><span class="o">.</span><span class="n">member?</span><span class="p">([</span><span class="n">four_heart</span><span class="p">,</span> <span class="n">four_spade</span><span class="p">],</span> <span class="n">four_heart_b</span><span class="p">)</span>

<span class="c1"># false</span>
</code></pre></div></div>

<p>Here, Elixir is using its default structural equality, so it does not recognize that a four of hearts exists in the list. There is no <code class="language-plaintext highlighter-rouge">member_by?</code> where we can inject our projection.</p>

<p>Funx provides <code class="language-plaintext highlighter-rouge">Funx.List.elem?</code>, which respects the domain’s equality definition:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Funx</span><span class="o">.</span><span class="no">List</span><span class="o">.</span><span class="n">elem?</span><span class="p">([</span><span class="n">four_heart</span><span class="p">,</span> <span class="n">four_spade</span><span class="p">],</span> <span class="n">four_heart_b</span><span class="p">)</span>

<span class="c1"># true</span>
</code></pre></div></div>

<p>It also accepts custom equality logic, such as equality by suit:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Funx</span><span class="o">.</span><span class="no">List</span><span class="o">.</span><span class="n">elem?</span><span class="p">([</span><span class="n">four_heart</span><span class="p">,</span> <span class="n">four_spade</span><span class="p">],</span> <span class="n">ten_spade</span><span class="p">,</span> <span class="no">Card</span><span class="o">.</span><span class="n">suit_eq</span><span class="p">)</span>
</code></pre></div></div>

<p>Where a four of diamonds doesn’t match by suit:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">four_diamond</span> <span class="o">=</span> <span class="no">Card</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="s2">"4"</span><span class="p">,</span> <span class="s2">"D"</span><span class="p">)</span>
<span class="no">Funx</span><span class="o">.</span><span class="no">List</span><span class="o">.</span><span class="n">elem?</span><span class="p">([</span><span class="n">four_heart</span><span class="p">,</span> <span class="n">four_spade</span><span class="p">],</span> <span class="n">four_diamond</span><span class="p">,</span> <span class="no">Card</span><span class="o">.</span><span class="n">suit_eq</span><span class="p">)</span>

<span class="c1"># false</span>
</code></pre></div></div>

<p>But it does match by rank:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Funx</span><span class="o">.</span><span class="no">List</span><span class="o">.</span><span class="n">elem?</span><span class="p">([</span><span class="n">four_heart</span><span class="p">,</span> <span class="n">four_spade</span><span class="p">],</span> <span class="n">four_diamond</span><span class="p">,</span> <span class="no">Card</span><span class="o">.</span><span class="n">rank_eq</span><span class="p">)</span>

<span class="c1"># true</span>
</code></pre></div></div>

<p>Instead of injecting projection functions, Funx uses <code class="language-plaintext highlighter-rouge">Eq</code>.</p>

<h2 id="managing-game-rules">Managing Game Rules</h2>

<p>Let’s start with a hand of cards:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">hand</span> <span class="o">=</span> <span class="p">[</span> 
  <span class="n">ten_heart</span><span class="p">,</span> 
  <span class="n">four_heart</span><span class="p">,</span> 
  <span class="n">four_heart_b</span><span class="p">,</span>
  <span class="n">ten_spade</span><span class="p">,</span>
  <span class="n">four_spade</span><span class="p">,</span> 
  <span class="n">four_diamond</span>
<span class="p">]</span>
</code></pre></div></div>

<p>And we can place our eq rule in our Game aggregate:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">defmodule</span> <span class="no">Game</span> <span class="k">do</span>
  <span class="n">alias</span> <span class="no">Game</span><span class="o">.</span><span class="no">Card</span>

  <span class="k">def</span> <span class="n">playable_card_eq</span> <span class="k">do</span>
    <span class="n">eq</span> <span class="k">do</span>
      <span class="n">any</span> <span class="k">do</span>
        <span class="no">Card</span><span class="o">.</span><span class="n">rank_eq</span><span class="p">()</span>
        <span class="no">Card</span><span class="o">.</span><span class="n">suit_eq</span><span class="p">()</span>
      <span class="k">end</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>In our game, a 10 of clubs was played:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">played_card</span> <span class="o">=</span> <span class="no">Card</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="s2">"10"</span><span class="p">,</span> <span class="s2">"C"</span><span class="p">)</span>
</code></pre></div></div>

<p>We can use <code class="language-plaintext highlighter-rouge">partition/3</code> to split our hand into playable and non-playable cards:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Funx</span><span class="o">.</span><span class="no">List</span><span class="o">.</span><span class="n">partition</span><span class="p">(</span><span class="n">hand</span><span class="p">,</span> <span class="n">played_card</span><span class="p">,</span> <span class="no">Game</span><span class="o">.</span><span class="n">playable_card_eq</span><span class="p">)</span>

<span class="c1"># {</span>
<span class="c1">#   [</span>
<span class="c1">#     %Game.Card{id: "13891", rank: "10", suit: "H"},</span>
<span class="c1">#     %Game.Card{id: "13955", rank: "10", suit: "S"}</span>
<span class="c1">#   ],</span>
<span class="c1">#   [</span>
<span class="c1">#     %Game.Card{id: "13859", rank: "4", suit: "H"},</span>
<span class="c1">#     %Game.Card{id: "13987", rank: "4", suit: "H"},</span>
<span class="c1">#     %Game.Card{id: "13923", rank: "4", suit: "S"},</span>
<span class="c1">#     %Game.Card{id: "14019", rank: "4", suit: "D"}</span>
<span class="c1">#   ]</span>
<span class="c1"># }</span>

</code></pre></div></div>

<p>The playable cards in our hand are the 10 of hearts or the 10 of spades.</p>

<p>We can also group a hand using <code class="language-plaintext highlighter-rouge">Eq</code>:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Funx</span><span class="o">.</span><span class="no">List</span><span class="o">.</span><span class="n">group</span><span class="p">(</span><span class="n">hand</span><span class="p">)</span>

<span class="c1"># [</span>
<span class="c1">#   [%Game.Card{id: "13891", rank: "10", suit: "H"}],</span>
<span class="c1">#   [</span>
<span class="c1">#     %Game.Card{id: "13859", rank: "4", suit: "H"}, </span>
<span class="c1">#     %Game.Card{id: "14435", rank: "4", suit: "H"}</span>
<span class="c1">#   ],</span>
<span class="c1">#   [%Game.Card{id: "13955", rank: "10", suit: "S"}],</span>
<span class="c1">#   [%Game.Card{id: "13923", rank: "4", suit: "S"}],</span>
<span class="c1">#   [%Game.Card{id: "14467", rank: "4", suit: "D"}]</span>
<span class="c1"># ]</span>
</code></pre></div></div>

<p>This groups our duplicate 4 of hearts.</p>

<p>Let’s group by rank instead:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Funx</span><span class="o">.</span><span class="no">List</span><span class="o">.</span><span class="n">group</span><span class="p">(</span><span class="n">hand</span><span class="p">,</span> <span class="no">Card</span><span class="o">.</span><span class="n">rank_eq</span><span class="p">)</span>

<span class="c1"># [</span>
<span class="c1">#   [%Game.Card{id: "13891", rank: "10", suit: "H"}],</span>
<span class="c1">#   [</span>
<span class="c1">#     %Game.Card{id: "13859", rank: "4", suit: "H"}, </span>
<span class="c1">#     %Game.Card{id: "14435", rank: "4", suit: "H"}</span>
<span class="c1">#   ],</span>
<span class="c1">#   [%Game.Card{id: "13955", rank: "10", suit: "S"}],</span>
<span class="c1">#   [</span>
<span class="c1">#     %Game.Card{id: "13923", rank: "4", suit: "S"}, </span>
<span class="c1">#     %Game.Card{id: "14467", rank: "4", suit: "D"}</span>
<span class="c1">#   ]</span>
<span class="c1"># ]</span>
</code></pre></div></div>

<p>Or by suit:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Funx</span><span class="o">.</span><span class="no">List</span><span class="o">.</span><span class="n">group</span><span class="p">(</span><span class="n">hand</span><span class="p">,</span> <span class="no">Card</span><span class="o">.</span><span class="n">suit_eq</span><span class="p">)</span>

<span class="c1"># [</span>
<span class="c1">#   [</span>
<span class="c1">#     %Game.Card{id: "13891", rank: "10", suit: "H"},</span>
<span class="c1">#     %Game.Card{id: "13859", rank: "4", suit: "H"},</span>
<span class="c1">#     %Game.Card{id: "14435", rank: "4", suit: "H"}</span>
<span class="c1">#   ],</span>
<span class="c1">#   [</span>
<span class="c1">#     %Game.Card{id: "13955", rank: "10", suit: "S"},</span>
<span class="c1">#     %Game.Card{id: "13923", rank: "4", suit: "S"}</span>
<span class="c1">#   ],</span>
<span class="c1">#   [%Game.Card{id: "14467", rank: "4", suit: "D"}]</span>
<span class="c1"># ]</span>

</code></pre></div></div>

<p>We can combine grouping with sorting using <code class="language-plaintext highlighter-rouge">group_sort/2</code>:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Funx</span><span class="o">.</span><span class="no">List</span><span class="o">.</span><span class="n">group_sort</span><span class="p">(</span><span class="n">hand</span><span class="p">)</span>

<span class="c1"># [</span>
<span class="c1">#   [%Game.Card{id: "14467", rank: "4", suit: "D"}],</span>
<span class="c1">#   [</span>
<span class="c1">#     %Game.Card{id: "13859", rank: "4", suit: "H"}, </span>
<span class="c1">#     %Game.Card{id: "14435", rank: "4", suit: "H"}</span>
<span class="c1">#   ],</span>
<span class="c1">#   [%Game.Card{id: "13891", rank: "10", suit: "H"}],</span>
<span class="c1">#   [%Game.Card{id: "13923", rank: "4", suit: "S"}],</span>
<span class="c1">#   [%Game.Card{id: "13955", rank: "10", suit: "S"}]</span>
<span class="c1"># ]</span>
</code></pre></div></div>

<p>And again, we can implement a different <code class="language-plaintext highlighter-rouge">Ord</code>, such as  order by suit:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Funx</span><span class="o">.</span><span class="no">List</span><span class="o">.</span><span class="n">group_sort</span><span class="p">(</span><span class="n">hand</span><span class="p">,</span> <span class="no">Card</span><span class="o">.</span><span class="n">suit_ord</span><span class="p">)</span>

<span class="c1"># [</span>
<span class="c1">#   [%Game.Card{id: "14467", rank: "4", suit: "D"}],</span>
<span class="c1">#   [</span>
<span class="c1">#     %Game.Card{id: "13891", rank: "10", suit: "H"},</span>
<span class="c1">#     %Game.Card{id: "13859", rank: "4", suit: "H"},</span>
<span class="c1">#     %Game.Card{id: "14435", rank: "4", suit: "H"}</span>
<span class="c1">#   ],</span>
<span class="c1">#   [</span>
<span class="c1">#     %Game.Card{id: "13955", rank: "10", suit: "S"}, </span>
<span class="c1">#     %Game.Card{id: "13923", rank: "4", suit: "S"}</span>
<span class="c1">#   ]</span>
<span class="c1"># ]</span>
</code></pre></div></div>

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

<p>Equality should be a domain rule. Define a default equality at the type level using <code class="language-plaintext highlighter-rouge">eq_for/2</code> so the rest of the code inherits it, use <code class="language-plaintext highlighter-rouge">Prism</code> when structure is optional and <code class="language-plaintext highlighter-rouge">Lens</code> when invariants are required, and use Funx list operations like <code class="language-plaintext highlighter-rouge">elem?</code> and <code class="language-plaintext highlighter-rouge">uniq</code> to respect domain semantics.</p>

<h2 id="resources">Resources</h2>

<div style="display: flex; gap: 20px; margin: 20px 0;">
  <div style="flex-shrink: 0;">
    <a href="https://pragprog.com/titles/jkelixir/advanced-functional-programming-with-elixir">
      <img src="/assets/images/jkelixir_small.jpg" alt="Advanced Functional Programming with Elixir book cover" width="150" />
    </a>
  </div>
  <div>
    <h3><a href="https://pragprog.com/titles/jkelixir/advanced-functional-programming-with-elixir">Advanced Functional Programming with Elixir</a></h3>
    <p>Dive deeper into functional programming patterns and advanced Elixir techniques. Learn how to build robust, maintainable applications using functional programming principles.</p>
  </div>
</div>

<div style="display: flex; gap: 20px; margin: 20px 0;">
  <div style="flex-shrink: 0;">
    <a href="https://www.funxlib.com">
      <img src="/assets/images/funx-social.jpg" alt="Funx functional programming library" width="150" />
    </a>
  </div>
  <div>
    <h3><a href="https://www.funxlib.com">Funx - Functional Programming for Elixir</a></h3>
    <p>A library of functional programming abstractions for Elixir, including monads, monoids, Eq, Ord, and more. Built as an ecosystem where learning is the priority from the start.</p>
  </div>
</div>]]></content><author><name>Joseph Koski</name></author><category term="elixir" /><category term="funx" /><summary type="html"><![CDATA[“A model is a selectively simplified and consciously structured form of knowledge.” — Eric Evans]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.joekoski.com/assets/images/funx-social.jpg" /><media:content medium="image" url="https://www.joekoski.com/assets/images/funx-social.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Funx: The Optic Iso</title><link href="https://www.joekoski.com/blog/2026/01/14/funx-optics-iso.html" rel="alternate" type="text/html" title="Funx: The Optic Iso" /><published>2026-01-14T14:16:06+00:00</published><updated>2026-01-14T14:16:06+00:00</updated><id>https://www.joekoski.com/blog/2026/01/14/funx-optics-iso</id><content type="html" xml:base="https://www.joekoski.com/blog/2026/01/14/funx-optics-iso.html"><![CDATA[<blockquote>
  <p>“I see dead people.” — The Sixth Sense (1999)</p>
</blockquote>

<p><a href="https://livebook.dev/run?url=https%3A%2F%2Fwww.joekoski.com%2Fassets%2Flivebooks%2Fblogs%2Ffunx-optics-iso-1.livemd"><img src="https://livebook.dev/badge/v1/black.svg" alt="Run in Livebook" /></a></p>

<h2 id="whats-an-iso">What’s an Iso?</h2>

<p>An iso represents a reversible change of representation. Nothing is added, nothing is lost.</p>

<p>Let’s start simple. Here is an iso that adds one.</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">alias</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Optics</span><span class="o">.</span><span class="p">{</span><span class="no">Iso</span><span class="p">,</span> <span class="no">Lens</span><span class="p">,</span> <span class="no">Prism</span><span class="p">}</span>

<span class="n">add_one_iso</span> <span class="o">=</span>
  <span class="no">Iso</span><span class="o">.</span><span class="n">make</span><span class="p">(</span>
    <span class="k">fn</span> <span class="n">n</span> <span class="o">-&gt;</span> <span class="n">n</span> <span class="o">+</span> <span class="mi">1</span> <span class="k">end</span><span class="p">,</span>
    <span class="k">fn</span> <span class="n">n</span> <span class="o">-&gt;</span> <span class="n">n</span> <span class="o">-</span> <span class="mi">1</span> <span class="k">end</span>
  <span class="p">)</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">view/2</code> applies the forward transformation.</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Iso</span><span class="o">.</span><span class="n">view</span><span class="p">(</span><span class="mi">42</span><span class="p">,</span> <span class="n">add_one_iso</span><span class="p">)</span>

<span class="c1"># 43</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">review/2</code> applies the inverse transformation.</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Iso</span><span class="o">.</span><span class="n">review</span><span class="p">(</span><span class="mi">43</span><span class="p">,</span> <span class="n">add_one_iso</span><span class="p">)</span>

<span class="c1"># 42</span>
</code></pre></div></div>

<p>An iso always round-trips. The inverse is exact, not a fallback.</p>

<h3 id="composing-isos">Composing Isos</h3>

<p>Isomorphisms are composable:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">add_two_iso</span> <span class="o">=</span> <span class="no">Iso</span><span class="o">.</span><span class="n">compose</span><span class="p">(</span><span class="n">add_one_iso</span><span class="p">,</span> <span class="n">add_one_iso</span><span class="p">)</span>
<span class="no">Iso</span><span class="o">.</span><span class="n">view</span><span class="p">(</span><span class="mi">42</span><span class="p">,</span> <span class="n">add_two_iso</span><span class="p">)</span>

<span class="c1"># 44</span>
</code></pre></div></div>

<p>And we still have reversibility:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Iso</span><span class="o">.</span><span class="n">review</span><span class="p">(</span><span class="mi">44</span><span class="p">,</span> <span class="n">add_two_iso</span><span class="p">)</span>

<span class="c1"># 42</span>
</code></pre></div></div>

<p>Because the inverse remains valid, composition can scale arbitrarily.</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">add_five_iso</span> <span class="o">=</span> <span class="no">Iso</span><span class="o">.</span><span class="n">compose</span><span class="p">([</span><span class="n">add_two_iso</span><span class="p">,</span> <span class="n">add_two_iso</span><span class="p">,</span> <span class="n">add_one_iso</span><span class="p">])</span>
<span class="n">add_ten_iso</span> <span class="o">=</span> <span class="no">Iso</span><span class="o">.</span><span class="n">compose</span><span class="p">([</span><span class="n">add_five_iso</span><span class="p">,</span> <span class="n">add_five_iso</span><span class="p">])</span>
<span class="n">add_fifty_iso</span> <span class="o">=</span> <span class="no">Iso</span><span class="o">.</span><span class="n">compose</span><span class="p">([</span><span class="n">add_ten_iso</span><span class="p">,</span> <span class="n">add_ten_iso</span><span class="p">,</span> <span class="n">add_ten_iso</span><span class="p">,</span> <span class="n">add_ten_iso</span><span class="p">,</span> <span class="n">add_ten_iso</span><span class="p">])</span>
<span class="n">add_one_hundred_three_iso</span> <span class="o">=</span> <span class="no">Iso</span><span class="o">.</span><span class="n">compose</span><span class="p">([</span><span class="n">add_fifty_iso</span><span class="p">,</span> <span class="n">add_fifty_iso</span><span class="p">,</span> <span class="n">add_two_iso</span><span class="p">,</span> <span class="n">add_one_iso</span><span class="p">])</span>

<span class="n">updated_value</span> <span class="o">=</span> <span class="no">Iso</span><span class="o">.</span><span class="n">view</span><span class="p">(</span><span class="mi">42</span><span class="p">,</span> <span class="n">add_one_hundred_three_iso</span><span class="p">)</span>

<span class="c1"># 145</span>
</code></pre></div></div>

<p>And the original value can always be recovered.</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Iso</span><span class="o">.</span><span class="n">review</span><span class="p">(</span><span class="n">updated_value</span><span class="p">,</span> <span class="n">add_one_hundred_three_iso</span><span class="p">)</span>

<span class="c1"># 42</span>
</code></pre></div></div>

<p>Within the context of <code class="language-plaintext highlighter-rouge">add_one_hundred_three_iso</code>, <code class="language-plaintext highlighter-rouge">145</code> and <code class="language-plaintext highlighter-rouge">42</code> are different representations of the same thing.</p>

<h2 id="unit-conversion">Unit conversion</h2>

<p>In 1999, <a href="https://science.nasa.gov/mission/mars-climate-orbiter/">NASA lost the Mars Climate Orbiter</a> because one team’s software output thrust data in pound-force seconds while another team’s navigation software expected newton-seconds, and the orbiter approached Mars at the wrong altitude and burned up in the atmosphere.</p>

<p>That wasn’t a calculation error. It was a representation error. Two teams looking at the same data, but seeing different things.</p>

<p>That’s a job for <code class="language-plaintext highlighter-rouge">Iso</code>.</p>

<p>Pound-force seconds and newton-seconds are two representations of impulse (thrust over time). Let’s start with an <code class="language-plaintext highlighter-rouge">Iso</code> to switch between them:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">lbf_seconds_to_newton_seconds_iso</span> <span class="o">=</span>
  <span class="no">Iso</span><span class="o">.</span><span class="n">make</span><span class="p">(</span>
    <span class="k">fn</span> <span class="n">lbf_s</span> <span class="o">-&gt;</span> <span class="n">lbf_s</span> <span class="o">*</span> <span class="mf">4.44822</span> <span class="k">end</span><span class="p">,</span>
    <span class="k">fn</span> <span class="n">n_s</span> <span class="o">-&gt;</span> <span class="n">n_s</span> <span class="o">/</span> <span class="mf">4.44822</span> <span class="k">end</span>
  <span class="p">)</span>
</code></pre></div></div>

<p>Here, we have the basic conversion logic, but it is not quite an <code class="language-plaintext highlighter-rouge">Iso</code>:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">newtons</span> <span class="o">=</span> <span class="no">Iso</span><span class="o">.</span><span class="n">view</span><span class="p">(</span><span class="mi">120</span><span class="p">,</span> <span class="n">lbf_seconds_to_newton_seconds_iso</span><span class="p">)</span>

<span class="c1"># 533.7864</span>
</code></pre></div></div>

<p>Notice what happens when we review:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Iso</span><span class="o">.</span><span class="n">review</span><span class="p">(</span><span class="n">newtons</span><span class="p">,</span> <span class="n">lbf_seconds_to_newton_seconds_iso</span><span class="p">)</span>

<span class="c1"># 119.99999999999999</span>
</code></pre></div></div>

<p>Here we have a bit of <code class="language-plaintext highlighter-rouge">Float</code> rounding. Without reversibility, we lose information as we pass it back and forth.</p>

<p>We could, for the sake of this demonstration, pretend that is an <code class="language-plaintext highlighter-rouge">Iso</code>, but it’s not. Instead we need a different strategy, such as using <code class="language-plaintext highlighter-rouge">Rational</code> numbers:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">defmodule</span> <span class="no">Rational</span> <span class="k">do</span>
  <span class="k">defstruct</span> <span class="p">[</span><span class="ss">:num</span><span class="p">,</span> <span class="ss">:den</span><span class="p">]</span>

  <span class="k">def</span> <span class="n">from_integer</span><span class="p">(</span><span class="n">n</span><span class="p">),</span> <span class="k">do</span><span class="p">:</span> <span class="p">%</span><span class="no">Rational</span><span class="p">{</span><span class="ss">num:</span> <span class="n">n</span><span class="p">,</span> <span class="ss">den:</span> <span class="mi">1</span><span class="p">}</span>

  <span class="k">def</span> <span class="n">add</span><span class="p">(%</span><span class="no">Rational</span><span class="p">{</span><span class="ss">num:</span> <span class="n">n1</span><span class="p">,</span> <span class="ss">den:</span> <span class="n">d1</span><span class="p">},</span> <span class="p">%</span><span class="no">Rational</span><span class="p">{</span><span class="ss">num:</span> <span class="n">n2</span><span class="p">,</span> <span class="ss">den:</span> <span class="n">d2</span><span class="p">})</span> <span class="k">do</span>
    <span class="n">normalize</span><span class="p">(%</span><span class="no">Rational</span><span class="p">{</span>
      <span class="ss">num:</span> <span class="n">n1</span> <span class="o">*</span> <span class="n">d2</span> <span class="o">+</span> <span class="n">n2</span> <span class="o">*</span> <span class="n">d1</span><span class="p">,</span>
      <span class="ss">den:</span> <span class="n">d1</span> <span class="o">*</span> <span class="n">d2</span>
    <span class="p">})</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">subtract</span><span class="p">(%</span><span class="no">Rational</span><span class="p">{</span><span class="ss">num:</span> <span class="n">n1</span><span class="p">,</span> <span class="ss">den:</span> <span class="n">d1</span><span class="p">},</span> <span class="p">%</span><span class="no">Rational</span><span class="p">{</span><span class="ss">num:</span> <span class="n">n2</span><span class="p">,</span> <span class="ss">den:</span> <span class="n">d2</span><span class="p">})</span> <span class="k">do</span>
    <span class="n">normalize</span><span class="p">(%</span><span class="no">Rational</span><span class="p">{</span>
      <span class="ss">num:</span> <span class="n">n1</span> <span class="o">*</span> <span class="n">d2</span> <span class="o">-</span> <span class="n">n2</span> <span class="o">*</span> <span class="n">d1</span><span class="p">,</span>
      <span class="ss">den:</span> <span class="n">d1</span> <span class="o">*</span> <span class="n">d2</span>
    <span class="p">})</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">multiply</span><span class="p">(%</span><span class="no">Rational</span><span class="p">{</span><span class="ss">num:</span> <span class="n">n1</span><span class="p">,</span> <span class="ss">den:</span> <span class="n">d1</span><span class="p">},</span> <span class="p">%</span><span class="no">Rational</span><span class="p">{</span><span class="ss">num:</span> <span class="n">n2</span><span class="p">,</span> <span class="ss">den:</span> <span class="n">d2</span><span class="p">})</span> <span class="k">do</span>
    <span class="n">normalize</span><span class="p">(%</span><span class="no">Rational</span><span class="p">{</span><span class="ss">num:</span> <span class="n">n1</span> <span class="o">*</span> <span class="n">n2</span><span class="p">,</span> <span class="ss">den:</span> <span class="n">d1</span> <span class="o">*</span> <span class="n">d2</span><span class="p">})</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">divide</span><span class="p">(%</span><span class="no">Rational</span><span class="p">{</span><span class="ss">num:</span> <span class="n">n1</span><span class="p">,</span> <span class="ss">den:</span> <span class="n">d1</span><span class="p">},</span> <span class="p">%</span><span class="no">Rational</span><span class="p">{</span><span class="ss">num:</span> <span class="n">n2</span><span class="p">,</span> <span class="ss">den:</span> <span class="n">d2</span><span class="p">})</span> <span class="k">do</span>
    <span class="n">normalize</span><span class="p">(%</span><span class="no">Rational</span><span class="p">{</span><span class="ss">num:</span> <span class="n">n1</span> <span class="o">*</span> <span class="n">d2</span><span class="p">,</span> <span class="ss">den:</span> <span class="n">d1</span> <span class="o">*</span> <span class="n">n2</span><span class="p">})</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">normalize</span><span class="p">(%</span><span class="no">Rational</span><span class="p">{</span><span class="ss">num:</span> <span class="n">n</span><span class="p">,</span> <span class="ss">den:</span> <span class="n">d</span><span class="p">})</span> <span class="k">do</span>
    <span class="n">g</span> <span class="o">=</span> <span class="no">Integer</span><span class="o">.</span><span class="n">gcd</span><span class="p">(</span><span class="n">n</span><span class="p">,</span> <span class="n">d</span><span class="p">)</span>
    <span class="p">%</span><span class="no">Rational</span><span class="p">{</span>
      <span class="ss">num:</span> <span class="n">div</span><span class="p">(</span><span class="n">n</span><span class="p">,</span> <span class="n">g</span><span class="p">),</span>
      <span class="ss">den:</span> <span class="n">div</span><span class="p">(</span><span class="n">d</span><span class="p">,</span> <span class="n">g</span><span class="p">)</span>
    <span class="p">}</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">to_float</span><span class="p">(%</span><span class="no">Rational</span><span class="p">{</span><span class="ss">num:</span> <span class="n">n</span><span class="p">,</span> <span class="ss">den:</span> <span class="n">d</span><span class="p">})</span> <span class="k">do</span>
    <span class="n">n</span> <span class="o">/</span> <span class="n">d</span>
  <span class="k">end</span>
<span class="k">end</span>

</code></pre></div></div>

<p>Here is that conversion with <code class="language-plaintext highlighter-rouge">Rational</code>:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">factor</span> <span class="o">=</span>
  <span class="p">%</span><span class="no">Rational</span><span class="p">{</span>
    <span class="ss">num:</span> <span class="mi">4_448_221_615_260_5</span><span class="p">,</span>
    <span class="ss">den:</span> <span class="mi">1_000_000_000_000_0</span>
  <span class="p">}</span>

<span class="c1"># %Rational{num: 44482216152605, den: 10000000000000}</span>
</code></pre></div></div>

<p>And we can use that for our <code class="language-plaintext highlighter-rouge">Iso</code>:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">lbf_seconds_to_newton_seconds_iso</span> <span class="o">=</span>
  <span class="no">Iso</span><span class="o">.</span><span class="n">make</span><span class="p">(</span>
    <span class="k">fn</span> <span class="n">lbf</span> <span class="o">-&gt;</span> <span class="no">Rational</span><span class="o">.</span><span class="n">multiply</span><span class="p">(</span><span class="n">lbf</span><span class="p">,</span> <span class="n">factor</span><span class="p">)</span> <span class="k">end</span><span class="p">,</span>
    <span class="k">fn</span> <span class="n">n</span>   <span class="o">-&gt;</span> <span class="no">Rational</span><span class="o">.</span><span class="n">divide</span><span class="p">(</span><span class="n">n</span><span class="p">,</span> <span class="n">factor</span><span class="p">)</span> <span class="k">end</span>
  <span class="p">)</span>
</code></pre></div></div>

<p>Next, we need a couple of structs to represent our data:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">defmodule</span> <span class="no">LbfSeconds</span> <span class="k">do</span>
  <span class="k">defstruct</span> <span class="p">[</span><span class="ss">:value</span><span class="p">]</span>
<span class="k">end</span>

<span class="k">defmodule</span> <span class="no">NewtonSeconds</span> <span class="k">do</span>
  <span class="k">defstruct</span> <span class="p">[</span><span class="ss">:value</span><span class="p">]</span>
<span class="k">end</span>
</code></pre></div></div>

<p>And a <code class="language-plaintext highlighter-rouge">Lens</code> to focus on the structs’ value:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">value_lens</span> <span class="o">=</span> <span class="no">Lens</span><span class="o">.</span><span class="n">key</span><span class="p">(</span><span class="ss">:value</span><span class="p">)</span>
</code></pre></div></div>

<p>Next, let’s leverage <code class="language-plaintext highlighter-rouge">lbf_seconds_to_newton_seconds</code> for an <code class="language-plaintext highlighter-rouge">Iso</code> to manage the relationship between our structs:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">impulse_iso</span> <span class="o">=</span>
  <span class="no">Iso</span><span class="o">.</span><span class="n">make</span><span class="p">(</span>
    <span class="k">fn</span> <span class="p">%</span><span class="no">LbfSeconds</span><span class="p">{</span><span class="ss">value:</span> <span class="n">lbf</span><span class="p">}</span> <span class="o">-&gt;</span>
      <span class="p">%</span><span class="no">NewtonSeconds</span><span class="p">{</span>
        <span class="ss">value:</span> <span class="no">Iso</span><span class="o">.</span><span class="n">view</span><span class="p">(</span><span class="n">lbf</span><span class="p">,</span> <span class="n">lbf_seconds_to_newton_seconds_iso</span><span class="p">)</span>
      <span class="p">}</span>
    <span class="k">end</span><span class="p">,</span>
    <span class="k">fn</span> <span class="p">%</span><span class="no">NewtonSeconds</span><span class="p">{</span><span class="ss">value:</span> <span class="n">ns</span><span class="p">}</span> <span class="o">-&gt;</span>
      <span class="p">%</span><span class="no">LbfSeconds</span><span class="p">{</span>
        <span class="ss">value:</span> <span class="no">Iso</span><span class="o">.</span><span class="n">review</span><span class="p">(</span><span class="n">ns</span><span class="p">,</span> <span class="n">lbf_seconds_to_newton_seconds_iso</span><span class="p">)</span>
      <span class="p">}</span>
    <span class="k">end</span>
  <span class="p">)</span>
</code></pre></div></div>

<p>Starting with 100 pound-force seconds of impulse:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">lbf</span> <span class="o">=</span> <span class="p">%</span><span class="no">LbfSeconds</span><span class="p">{</span><span class="ss">value:</span> <span class="no">Rational</span><span class="o">.</span><span class="n">from_integer</span><span class="p">(</span><span class="mi">100</span><span class="p">)}</span>

<span class="c1"># %LbfSeconds{value: %Rational{num: 100, den: 1}}</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">Iso.view/2</code> will convert that to newtons.</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">newton</span> <span class="o">=</span> <span class="no">Iso</span><span class="o">.</span><span class="n">view</span><span class="p">(</span><span class="n">lbf</span><span class="p">,</span> <span class="n">impulse_iso</span><span class="p">)</span>

<span class="c1"># %NewtonSeconds{value: %Rational{num: 8896443230521, den: 20000000000}}</span>
</code></pre></div></div>

<p>And we can <code class="language-plaintext highlighter-rouge">review/2</code> to return to the initial value:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Iso</span><span class="o">.</span><span class="n">review</span><span class="p">(</span><span class="n">newton</span><span class="p">,</span> <span class="n">impulse_iso</span><span class="p">)</span>

<span class="c1"># %LbfSeconds{value: %Rational{num: 100, den: 1}}</span>
</code></pre></div></div>

<p>If we need to know the float value, we can convert <code class="language-plaintext highlighter-rouge">NewtonSeconds</code> to a float:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">newton</span> 
<span class="o">|&gt;</span> <span class="no">Lens</span><span class="o">.</span><span class="n">view!</span><span class="p">(</span><span class="n">value_lens</span><span class="p">)</span> 
<span class="o">|&gt;</span> <span class="no">Rational</span><span class="o">.</span><span class="n">to_float</span><span class="p">()</span>

<span class="c1"># 444.82216152605</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">100 LbfSeconds</code> are about equal to <code class="language-plaintext highlighter-rouge">444.822 Newton Seconds</code>.</p>

<p>When dealing with conversions, always perform the float conversion at the edge, where we don’t have the expectation of reversibility.</p>

<h3 id="update-an-iso">Update an Iso</h3>

<p>Let’s add 10 units to our values.</p>

<p>First, a function that adds 10:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">add_ten</span> <span class="o">=</span>
  <span class="k">fn</span> <span class="n">n</span> <span class="o">-&gt;</span>
    <span class="no">Rational</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="n">n</span><span class="p">,</span> <span class="no">Rational</span><span class="o">.</span><span class="n">from_integer</span><span class="p">(</span><span class="mi">10</span><span class="p">))</span>
  <span class="k">end</span>
</code></pre></div></div>

<p>With this, we can use <code class="language-plaintext highlighter-rouge">Lens.over/3</code> to update the internal value:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Lens</span><span class="o">.</span><span class="n">over!</span><span class="p">(</span><span class="n">lbf</span><span class="p">,</span> <span class="n">value_lens</span><span class="p">,</span> <span class="n">add_ten</span><span class="p">)</span>

<span class="c1"># %LbfSeconds{value: %Rational{num: 110, den: 1}}</span>
</code></pre></div></div>

<p>Here, we get <code class="language-plaintext highlighter-rouge">110 LbfSeconds</code>.</p>

<p>And for newtons:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">newton_10</span> <span class="o">=</span> <span class="no">Lens</span><span class="o">.</span><span class="n">over!</span><span class="p">(</span><span class="n">newton</span><span class="p">,</span> <span class="n">value_lens</span><span class="p">,</span> <span class="n">add_ten</span><span class="p">)</span>

<span class="c1"># %NewtonSeconds{value: %Rational{num: 9096443230521, den: 20000000000}}</span>
</code></pre></div></div>

<p>Let’s convert that back to a float:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">newton_10</span> 
<span class="o">|&gt;</span> <span class="no">Lens</span><span class="o">.</span><span class="n">view!</span><span class="p">(</span><span class="n">value_lens</span><span class="p">)</span> 
<span class="o">|&gt;</span> <span class="no">Rational</span><span class="o">.</span><span class="n">to_float</span><span class="p">()</span>

<span class="c1"># 454.82216152605</span>
</code></pre></div></div>

<p>And we have <code class="language-plaintext highlighter-rouge">454.822 NewtonSeconds</code>, ten more than we started with.</p>

<p>But what is ten here? Is it ten lbf or ten newtons?</p>

<p>That’s what crashed the Mars Climate Orbiter.</p>

<h3 id="protect-our-boundary">Protect our Boundary</h3>

<p>Let’s leverage a <code class="language-plaintext highlighter-rouge">Prism</code> to draw boundaries we can use to protect ourselves from making the mistake:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">lbf_prism</span> <span class="o">=</span> <span class="no">Prism</span><span class="o">.</span><span class="n">path</span><span class="p">([{</span><span class="no">LbfSeconds</span><span class="p">,</span> <span class="ss">:value</span><span class="p">}])</span>
<span class="n">newton_prism</span> <span class="o">=</span> <span class="no">Prism</span><span class="o">.</span><span class="n">path</span><span class="p">([{</span><span class="no">NewtonSeconds</span><span class="p">,</span> <span class="ss">:value</span><span class="p">}])</span>
</code></pre></div></div>

<p>Now, within the context of <code class="language-plaintext highlighter-rouge">LbfSeconds</code>:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Prism</span><span class="o">.</span><span class="n">preview</span><span class="p">(</span><span class="n">lbf</span><span class="p">,</span> <span class="n">lbf_prism</span><span class="p">)</span>

<span class="c1"># %Funx.Monad.Maybe.Just{value: %Rational{num: 100, den: 1}}</span>
</code></pre></div></div>

<p>We have <code class="language-plaintext highlighter-rouge">Just</code> the value.</p>

<p>But with newtons:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Prism</span><span class="o">.</span><span class="n">preview</span><span class="p">(</span><span class="n">newton</span><span class="p">,</span> <span class="n">lbf_prism</span><span class="p">)</span>

<span class="c1"># %Funx.Monad.Maybe.Nothing{}</span>
</code></pre></div></div>

<p>We have <code class="language-plaintext highlighter-rouge">Nothing</code>. Our boundary will not let us update an <code class="language-plaintext highlighter-rouge">lbf</code> with a <code class="language-plaintext highlighter-rouge">newton</code>.</p>

<p>Let’s use this boundary in the <code class="language-plaintext highlighter-rouge">Maybe</code> dsl:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">use</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Monad</span><span class="o">.</span><span class="no">Maybe</span>

<span class="n">maybe</span> <span class="n">lbf</span> <span class="k">do</span>
  <span class="n">bind</span> <span class="no">Prism</span><span class="o">.</span><span class="n">preview</span><span class="p">(</span><span class="n">lbf_prism</span><span class="p">)</span>
  <span class="n">map</span> <span class="n">add_ten</span>
  <span class="n">map</span> <span class="no">Rational</span><span class="o">.</span><span class="n">to_float</span>
<span class="k">end</span>

<span class="c1"># %Funx.Monad.Maybe.Just{value: 110.0}</span>
</code></pre></div></div>

<p>To add ten newtons, we need to switch over to the <code class="language-plaintext highlighter-rouge">NewtonSeconds</code> boundary:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">maybe</span> <span class="n">newton</span> <span class="k">do</span>
  <span class="n">bind</span> <span class="no">Prism</span><span class="o">.</span><span class="n">preview</span><span class="p">(</span><span class="n">newton_prism</span><span class="p">)</span>
  <span class="n">map</span> <span class="n">add_ten</span>
  <span class="n">map</span> <span class="no">Rational</span><span class="o">.</span><span class="n">to_float</span>
<span class="k">end</span>

<span class="c1"># %Funx.Monad.Maybe.Just{value: 454.82216152605}</span>
</code></pre></div></div>

<p>If we try to add ten <code class="language-plaintext highlighter-rouge">LbfSeconds</code> to our <code class="language-plaintext highlighter-rouge">NewtonSeconds</code>:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">maybe</span> <span class="n">newton</span> <span class="k">do</span>
  <span class="n">bind</span> <span class="no">Prism</span><span class="o">.</span><span class="n">preview</span><span class="p">(</span><span class="n">lbf_prism</span><span class="p">)</span>
  <span class="n">map</span> <span class="n">add_ten</span>
  <span class="n">map</span> <span class="no">Rational</span><span class="o">.</span><span class="n">to_float</span>
<span class="k">end</span>

<span class="c1"># %Funx.Monad.Maybe.Nothing{}</span>
</code></pre></div></div>

<p>Our boundary is stating that in the context of <code class="language-plaintext highlighter-rouge">LbfSeconds</code> a newton doesn’t exist, so it is <code class="language-plaintext highlighter-rouge">Nothing</code>.</p>

<p>But that’s not true: <code class="language-plaintext highlighter-rouge">LbfSeconds</code> and <code class="language-plaintext highlighter-rouge">NewtonSeconds</code> are NOT two separate things, they are the SAME thing. Just being viewed in two different ways. <code class="language-plaintext highlighter-rouge">Impulse</code> isn’t a <code class="language-plaintext highlighter-rouge">Prism</code>, it is an <code class="language-plaintext highlighter-rouge">Iso</code>.</p>

<h3 id="swap-dont-fail">Swap, don’t fail</h3>

<p>With an <code class="language-plaintext highlighter-rouge">Iso</code>, we don’t fail when we’re in the <em>wrong</em> representation. We just swap to the correct context, do our work, and swap back:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">lbf_add_10_newton</span> <span class="o">=</span>
<span class="no">Iso</span><span class="o">.</span><span class="n">view</span><span class="p">(</span><span class="n">lbf</span><span class="p">,</span> <span class="n">impulse_iso</span><span class="p">)</span>
<span class="o">|&gt;</span> <span class="no">Lens</span><span class="o">.</span><span class="n">over!</span><span class="p">(</span><span class="n">value_lens</span><span class="p">,</span> <span class="n">add_ten</span><span class="p">)</span>
<span class="o">|&gt;</span> <span class="no">Iso</span><span class="o">.</span><span class="n">review</span><span class="p">(</span><span class="n">impulse_iso</span><span class="p">)</span>

<span class="c1"># %LbfSeconds{value: %Rational{num: 909644323052100, den: 8896443230521}}</span>
</code></pre></div></div>

<p>Here, we are swapping <code class="language-plaintext highlighter-rouge">LbfSeconds</code> over to the <code class="language-plaintext highlighter-rouge">NewtonSeconds</code>, then adding ten units, and swapping back to <code class="language-plaintext highlighter-rouge">LbfSeconds</code>.</p>

<p>Here is the float value:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">lbf_add_10_newton</span>
<span class="o">|&gt;</span> <span class="no">Lens</span><span class="o">.</span><span class="n">view!</span><span class="p">(</span><span class="n">value_lens</span><span class="p">)</span> 
<span class="o">|&gt;</span> <span class="no">Rational</span><span class="o">.</span><span class="n">to_float</span><span class="p">()</span>

<span class="c1"># 102.2480894309971</span>
</code></pre></div></div>

<p>After adding ten <code class="language-plaintext highlighter-rouge">NewtonSeconds</code>, we now have about <code class="language-plaintext highlighter-rouge">102.248 LbfSeconds</code>.</p>

<p>An <code class="language-plaintext highlighter-rouge">Iso</code> already has a function for this, named <code class="language-plaintext highlighter-rouge">over/3</code>:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">lbf_add_10_newton</span> <span class="o">=</span>
<span class="no">Iso</span><span class="o">.</span><span class="n">over</span><span class="p">(</span>
  <span class="n">lbf</span><span class="p">,</span>
  <span class="n">impulse_iso</span><span class="p">,</span>
  <span class="k">fn</span> <span class="n">newton</span> <span class="o">-&gt;</span> <span class="no">Lens</span><span class="o">.</span><span class="n">over!</span><span class="p">(</span><span class="n">newton</span><span class="p">,</span> <span class="n">value_lens</span><span class="p">,</span> <span class="n">add_ten</span><span class="p">)</span> <span class="k">end</span>
<span class="p">)</span>

<span class="c1"># %LbfSeconds{value: %Rational{num: 909644323052100, den: 8896443230521}}</span>
</code></pre></div></div>

<p>And it has the opposite with <code class="language-plaintext highlighter-rouge">under/3</code>, where we can add ten <code class="language-plaintext highlighter-rouge">LbfSeconds</code> to our <code class="language-plaintext highlighter-rouge">NewtonSeconds</code>:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">newton_add_10_lbf</span> <span class="o">=</span>
<span class="no">Iso</span><span class="o">.</span><span class="n">under</span><span class="p">(</span>
  <span class="n">newton</span><span class="p">,</span>
  <span class="n">impulse_iso</span><span class="p">,</span>
  <span class="k">fn</span> <span class="n">lbf</span> <span class="o">-&gt;</span> <span class="no">Lens</span><span class="o">.</span><span class="n">over!</span><span class="p">(</span><span class="n">lbf</span><span class="p">,</span> <span class="n">value_lens</span><span class="p">,</span> <span class="n">add_ten</span><span class="p">)</span> <span class="k">end</span>
<span class="p">)</span>

<span class="c1"># %NewtonSeconds{value: %Rational{num: 97860875535731, den: 200000000000}}</span>
</code></pre></div></div>

<p>If we get the difference:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="no">Lens</span><span class="o">.</span><span class="n">view!</span><span class="p">(</span><span class="n">newton_add_10_lbf</span><span class="p">,</span> <span class="n">value_lens</span><span class="p">)</span>
<span class="o">|&gt;</span> <span class="no">Rational</span><span class="o">.</span><span class="n">subtract</span><span class="p">(</span><span class="no">Lens</span><span class="o">.</span><span class="n">view!</span><span class="p">(</span><span class="n">newton</span><span class="p">,</span><span class="n">value_lens</span><span class="p">))</span>
<span class="o">|&gt;</span> <span class="no">Rational</span><span class="o">.</span><span class="n">to_float</span><span class="p">()</span>

<span class="c1"># 44.482216152605</span>
</code></pre></div></div>

<p>We find <code class="language-plaintext highlighter-rouge">10 LbfSeconds</code> is equal to about <code class="language-plaintext highlighter-rouge">44.4822 NewtonSeconds</code>.</p>

<h2 id="wrapping-up">Wrapping Up</h2>

<p>An iso is a statement that two representations are the same information.</p>

<p>That’s why isos don’t have bang variants. An iso models no failure. If the transformation can lose information or fail, we don’t have an iso. A crash is a broken invariant, not an expected outcome.</p>

<p>The Mars Climate Orbiter failed because the boundary between representations was implicit. With an iso, we can make that boundary explicit. Same thing, two views, guaranteed round-tripping.</p>

<h2 id="resources">Resources</h2>

<div style="display: flex; gap: 20px; margin: 20px 0;">
  <div style="flex-shrink: 0;">
    <a href="https://pragprog.com/titles/jkelixir/advanced-functional-programming-with-elixir">
      <img src="/assets/images/jkelixir_small.jpg" alt="Advanced Functional Programming with Elixir book cover" width="150" />
    </a>
  </div>
  <div>
    <h3><a href="https://pragprog.com/titles/jkelixir/advanced-functional-programming-with-elixir">Advanced Functional Programming with Elixir</a></h3>
    <p>Dive deeper into functional programming patterns and advanced Elixir techniques. Learn how to build robust, maintainable applications using functional programming principles.</p>
  </div>
</div>

<div style="display: flex; gap: 20px; margin: 20px 0;">
  <div style="flex-shrink: 0;">
    <a href="https://www.funxlib.com">
      <img src="/assets/images/funx-social.jpg" alt="Funx functional programming library" width="150" />
    </a>
  </div>
  <div>
    <h3><a href="https://www.funxlib.com">Funx - Functional Programming for Elixir</a></h3>
    <p>A library of functional programming abstractions for Elixir, including monads, monoids, Eq, Ord, and more. Built as an ecosystem where learning is the priority from the start.</p>
  </div>
</div>]]></content><author><name>Joseph Koski</name></author><category term="elixir" /><category term="funx" /><summary type="html"><![CDATA[“I see dead people.” — The Sixth Sense (1999)]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.joekoski.com/assets/images/funx-social.jpg" /><media:content medium="image" url="https://www.joekoski.com/assets/images/funx-social.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Funx: Optics Working Together</title><link href="https://www.joekoski.com/blog/2026/01/14/funx-optics-iso-2.html" rel="alternate" type="text/html" title="Funx: Optics Working Together" /><published>2026-01-14T14:16:06+00:00</published><updated>2026-01-14T14:16:06+00:00</updated><id>https://www.joekoski.com/blog/2026/01/14/funx-optics-iso-2</id><content type="html" xml:base="https://www.joekoski.com/blog/2026/01/14/funx-optics-iso-2.html"><![CDATA[<blockquote>
  <p>“You’re looking at it wrong.” — The Big Lebowski (1998)</p>
</blockquote>

<p>In the previous post, we built an iso between <code class="language-plaintext highlighter-rouge">LbfSeconds</code> and <code class="language-plaintext highlighter-rouge">NewtonSeconds</code>, flat structs with a single value. Our spacecraft has nested data: impulse values inside thruster telemetry and navigation.</p>

<p>This post shows how Iso composes with Lens, Prism, and Traversal to work with nested and optional data. Then we’ll apply the same pattern to a different problem: isolating vendor decisions at system boundaries.</p>

<p><a href="https://livebook.dev/run?url=https%3A%2F%2Fwww.joekoski.com%2Fassets%2Flivebooks%2Fblogs%2Ffunx-optics-iso-2.livemd"><img src="https://livebook.dev/badge/v1/black.svg" alt="Run in Livebook" /></a></p>

<h2 id="nested-data">Nested Data</h2>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Spacecraft
│
├─ id
│
├─ thrusters
│  ├─ impulse
│  │  └─ LbfSeconds
│  │     └─ value
│  ├─ fuel_remaining
│  └─ status
│
└─ navigation
   ├─ target_impulse
   │  └─ NewtonSeconds
   │     └─ value
   ├─ trajectory
   └─ eta
</code></pre></div></div>

<p>Our spacecraft has thrusters and navigation, two subsystems that work with impulse values.</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">defmodule</span> <span class="no">ThrusterData</span> <span class="k">do</span>
  <span class="k">defstruct</span> <span class="p">[</span><span class="ss">:impulse</span><span class="p">,</span> <span class="ss">:fuel_remaining</span><span class="p">,</span> <span class="ss">:status</span><span class="p">]</span>
<span class="k">end</span>

<span class="k">defmodule</span> <span class="no">NavData</span> <span class="k">do</span>
  <span class="k">defstruct</span> <span class="p">[</span><span class="ss">:target_impulse</span><span class="p">,</span> <span class="ss">:trajectory</span><span class="p">,</span> <span class="ss">:eta</span><span class="p">]</span>
<span class="k">end</span>

<span class="k">defmodule</span> <span class="no">Spacecraft</span> <span class="k">do</span>
  <span class="k">defstruct</span> <span class="p">[</span><span class="ss">:id</span><span class="p">,</span> <span class="ss">:thrusters</span><span class="p">,</span> <span class="ss">:navigation</span><span class="p">]</span>
<span class="k">end</span>
</code></pre></div></div>

<p>The thruster team uses pound-force seconds, and the navigation team uses newton-seconds:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">spacecraft</span> <span class="o">=</span> <span class="p">%</span><span class="no">Spacecraft</span><span class="p">{</span>
  <span class="ss">id:</span> <span class="s2">"MCO-1999"</span><span class="p">,</span>
  <span class="ss">thrusters:</span> <span class="p">%</span><span class="no">ThrusterData</span><span class="p">{</span>
    <span class="ss">impulse:</span> <span class="p">%</span><span class="no">LbfSeconds</span><span class="p">{</span><span class="ss">value:</span> <span class="no">Rational</span><span class="o">.</span><span class="n">from_integer</span><span class="p">(</span><span class="mi">100</span><span class="p">)},</span>
    <span class="ss">fuel_remaining:</span> <span class="mf">75.5</span><span class="p">,</span>
    <span class="ss">status:</span> <span class="ss">:active</span>
  <span class="p">},</span>
  <span class="ss">navigation:</span> <span class="p">%</span><span class="no">NavData</span><span class="p">{</span>
    <span class="ss">target_impulse:</span> <span class="p">%</span><span class="no">NewtonSeconds</span><span class="p">{</span><span class="ss">value:</span> <span class="no">Rational</span><span class="o">.</span><span class="n">from_integer</span><span class="p">(</span><span class="mi">501</span><span class="p">)},</span>
    <span class="ss">trajectory:</span> <span class="ss">:mars_orbit</span><span class="p">,</span>
    <span class="ss">eta:</span> <span class="sx">~U[1999-09-23 09:01:00Z]</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="c1"># %Spacecraft{</span>
<span class="c1">#   id: "MCO-1999",</span>
<span class="c1">#   thrusters: %ThrusterData{</span>
<span class="c1">#     impulse: %LbfSeconds{value: %Rational{num: 100, den: 1}},</span>
<span class="c1">#     fuel_remaining: 75.5,</span>
<span class="c1">#     status: :active</span>
<span class="c1">#   },</span>
<span class="c1">#   navigation: %NavData{</span>
<span class="c1">#     target_impulse: %NewtonSeconds{value: %Rational{num: 501, den: 1}},</span>
<span class="c1">#     trajectory: :mars_orbit,</span>
<span class="c1">#     eta: ~U[1999-09-23 09:01:00Z]</span>
<span class="c1">#   }</span>
<span class="c1"># }</span>
</code></pre></div></div>

<p>Let’s get the thruster impulse first:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">alias</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Optics</span><span class="o">.</span><span class="p">{</span><span class="no">Lens</span><span class="p">,</span> <span class="no">Iso</span><span class="p">,</span> <span class="no">Prism</span><span class="p">,</span> <span class="no">Traversal</span><span class="p">}</span>


<span class="n">thrusters_impulse_lens</span> <span class="o">=</span> <span class="no">Lens</span><span class="o">.</span><span class="n">path</span><span class="p">([</span><span class="ss">:thrusters</span><span class="p">,</span> <span class="ss">:impulse</span><span class="p">])</span>
<span class="n">spacecraft</span> <span class="o">|&gt;</span> <span class="no">Lens</span><span class="o">.</span><span class="n">view!</span><span class="p">(</span><span class="n">thrusters_impulse_lens</span><span class="p">)</span>

<span class="c1"># %LbfSeconds{value: %Rational{num: 100, den: 1}}</span>
</code></pre></div></div>

<p>This gives us <code class="language-plaintext highlighter-rouge">LbfSeconds</code>. The navigation subsystem works in <code class="language-plaintext highlighter-rouge">NewtonSeconds</code>.</p>

<p>An <code class="language-plaintext highlighter-rouge">Iso</code> can be converted to <code class="language-plaintext highlighter-rouge">Lens</code> with <code class="language-plaintext highlighter-rouge">as_lens/1</code>:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">thruster_impulse_ns_lens</span> <span class="o">=</span>
  <span class="no">Lens</span><span class="o">.</span><span class="n">compose</span><span class="p">([</span>
    <span class="n">thrusters_impulse_lens</span><span class="p">,</span>
    <span class="no">Iso</span><span class="o">.</span><span class="n">as_lens</span><span class="p">(</span><span class="no">Impulse</span><span class="o">.</span><span class="n">impulse_iso</span><span class="p">())</span>
  <span class="p">])</span>
</code></pre></div></div>

<p>Which lets us view the thruster impulse as <code class="language-plaintext highlighter-rouge">NewtonSeconds</code>:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">spacecraft</span> <span class="o">|&gt;</span> <span class="no">Lens</span><span class="o">.</span><span class="n">view!</span><span class="p">(</span><span class="n">thruster_impulse_ns_lens</span><span class="p">)</span>

<span class="c1"># %NewtonSeconds{value: %Rational{num: 8896443230521, den: 20000000000}}</span>
</code></pre></div></div>

<p>We can get the navigation target impulse using a lens as well:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">navigation_impulse_ns_lens</span> <span class="o">=</span> <span class="no">Lens</span><span class="o">.</span><span class="n">path</span><span class="p">([</span><span class="ss">:navigation</span><span class="p">,</span> <span class="ss">:target_impulse</span><span class="p">])</span>
<span class="n">spacecraft</span> <span class="o">|&gt;</span> <span class="no">Lens</span><span class="o">.</span><span class="n">view!</span><span class="p">(</span><span class="n">navigation_impulse_ns_lens</span><span class="p">)</span>

<span class="c1"># %NewtonSeconds{value: %Rational{num: 501, den: 1}}</span>
</code></pre></div></div>

<p>And we can get both with <code class="language-plaintext highlighter-rouge">Traversal</code>:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">alias</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Optics</span><span class="o">.</span><span class="no">Traversal</span>

<span class="n">thrust_trav</span> <span class="o">=</span> 
  <span class="no">Traversal</span><span class="o">.</span><span class="n">combine</span><span class="p">([</span>
    <span class="n">thruster_impulse_ns_lens</span><span class="p">,</span>
    <span class="n">navigation_impulse_ns_lens</span>
  <span class="p">])</span>

<span class="p">[</span><span class="n">current_thrust</span><span class="p">,</span> <span class="n">intended_thrust</span><span class="p">]</span> <span class="o">=</span> <span class="no">Traversal</span><span class="o">.</span><span class="n">to_list</span><span class="p">(</span><span class="n">spacecraft</span><span class="p">,</span> <span class="n">thrust_trav</span><span class="p">)</span>

<span class="c1"># [</span>
<span class="c1">#   %NewtonSeconds{value: %Rational{num: 8896443230521, den: 20000000000}},</span>
<span class="c1">#   %NewtonSeconds{value: %Rational{num: 501, den: 1}}</span>
<span class="c1"># ]</span>
</code></pre></div></div>

<p>The values don’t match. This is the gap between what the thrusters are actually doing and what navigation believes is happening:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">diff</span> <span class="o">=</span> <span class="no">Rational</span><span class="o">.</span><span class="n">subtract</span><span class="p">(</span><span class="n">intended_thrust</span><span class="o">.</span><span class="n">value</span><span class="p">,</span> <span class="n">current_thrust</span><span class="o">.</span><span class="n">value</span><span class="p">)</span>
<span class="no">Rational</span><span class="o">.</span><span class="n">to_float</span><span class="p">(</span><span class="n">diff</span><span class="p">)</span>

<span class="c1"># 56.17783847395</span>
</code></pre></div></div>

<p>Our current thrust is about <code class="language-plaintext highlighter-rouge">56.2 NewtonSeconds</code> short.</p>

<p>We can update the thruster impulse to match the target by updating through the lens path that includes the newton-second view.</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">thruster_impulse_ns_value_lens</span> <span class="o">=</span>
  <span class="no">Lens</span><span class="o">.</span><span class="n">compose</span><span class="p">(</span><span class="n">thruster_impulse_ns_lens</span><span class="p">,</span> <span class="no">Lens</span><span class="o">.</span><span class="n">key</span><span class="p">(</span><span class="ss">:value</span><span class="p">))</span>

 <span class="n">updated_spacecraft</span> <span class="o">=</span>
   <span class="no">Lens</span><span class="o">.</span><span class="n">over!</span><span class="p">(</span>
    <span class="n">spacecraft</span><span class="p">,</span>
    <span class="n">thruster_impulse_ns_value_lens</span><span class="p">,</span>
    <span class="k">fn</span> <span class="n">current</span> <span class="o">-&gt;</span> <span class="no">Rational</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="n">current</span><span class="p">,</span> <span class="n">diff</span><span class="p">)</span> <span class="k">end</span><span class="p">)</span>

<span class="c1"># %Spacecraft{</span>
<span class="c1">#   id: "MCO-1999",</span>
<span class="c1">#   thrusters: %ThrusterData{</span>
<span class="c1">#     impulse: %LbfSeconds{value: %Rational{num: 1002000000000000, den: 8896443230521}},</span>
<span class="c1">#     fuel_remaining: 75.5,</span>
<span class="c1">#     status: :active</span>
<span class="c1">#   },</span>
<span class="c1">#   navigation: %NavData{</span>
<span class="c1">#     target_impulse: %NewtonSeconds{value: %Rational{num: 501, den: 1}},</span>
<span class="c1">#     trajectory: :mars_orbit,</span>
<span class="c1">#     eta: ~U[1999-09-23 09:01:00Z]</span>
<span class="c1">#   }</span>
<span class="c1"># }</span>
</code></pre></div></div>

<p>Notice we can’t just add the float <code class="language-plaintext highlighter-rouge">56.177...</code>, since it has lost information. Instead, we use <code class="language-plaintext highlighter-rouge">diff</code>:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="n">current_thrust</span><span class="p">,</span> <span class="n">intended_thrust</span><span class="p">]</span> <span class="o">=</span> <span class="no">Traversal</span><span class="o">.</span><span class="n">to_list</span><span class="p">(</span><span class="n">updated_spacecraft</span><span class="p">,</span> <span class="n">thrust_trav</span><span class="p">)</span>

<span class="c1"># [</span>
<span class="c1">#   %NewtonSeconds{value: %Rational{num: 501, den: 1}},</span>
<span class="c1">#   %NewtonSeconds{value: %Rational{num: 501, den: 1}}</span>
<span class="c1"># ]</span>
</code></pre></div></div>

<p>And now our values match.</p>

<p>The <code class="language-plaintext highlighter-rouge">Lens</code> and <code class="language-plaintext highlighter-rouge">Iso</code> ensure that unit conversions are explicit, reversible, and mechanically correct.</p>

<h2 id="working-with-optional-data">Working with Optional Data</h2>

<p>Space communications are unreliable. Solar flares, distance, and hardware faults mean our navigation module sometimes drops out. When it’s online, we have navigation data. When it’s not, the field is <code class="language-plaintext highlighter-rouge">nil</code>.</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">spacecraft_connected</span> <span class="o">=</span> <span class="p">%</span><span class="no">Spacecraft</span><span class="p">{</span>
  <span class="ss">id:</span> <span class="s2">"MCO-1999"</span><span class="p">,</span>
  <span class="ss">thrusters:</span> <span class="p">%</span><span class="no">ThrusterData</span><span class="p">{</span>
    <span class="ss">impulse:</span> <span class="p">%</span><span class="no">LbfSeconds</span><span class="p">{</span><span class="ss">value:</span> <span class="no">Rational</span><span class="o">.</span><span class="n">from_integer</span><span class="p">(</span><span class="mi">45</span><span class="p">)},</span>
    <span class="ss">fuel_remaining:</span> <span class="mf">75.5</span><span class="p">,</span>
    <span class="ss">status:</span> <span class="ss">:active</span>
  <span class="p">},</span>
  <span class="ss">navigation:</span> <span class="p">%</span><span class="no">NavData</span><span class="p">{</span>
    <span class="ss">target_impulse:</span> <span class="p">%</span><span class="no">NewtonSeconds</span><span class="p">{</span><span class="ss">value:</span> <span class="no">Rational</span><span class="o">.</span><span class="n">from_integer</span><span class="p">(</span><span class="mi">375</span><span class="p">)},</span>
    <span class="ss">trajectory:</span> <span class="ss">:mars_orbit</span><span class="p">,</span>
    <span class="ss">eta:</span> <span class="sx">~U[1999-09-23 09:01:00Z]</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="n">spacecraft_disconnected</span> <span class="o">=</span> <span class="p">%</span><span class="no">Spacecraft</span><span class="p">{</span>
  <span class="ss">id:</span> <span class="s2">"MCO-1999"</span><span class="p">,</span>
  <span class="ss">thrusters:</span> <span class="p">%</span><span class="no">ThrusterData</span><span class="p">{</span>
    <span class="ss">impulse:</span> <span class="p">%</span><span class="no">LbfSeconds</span><span class="p">{</span><span class="ss">value:</span> <span class="no">Rational</span><span class="o">.</span><span class="n">from_integer</span><span class="p">(</span><span class="mi">76</span><span class="p">)},</span>
    <span class="ss">fuel_remaining:</span> <span class="mf">75.5</span><span class="p">,</span>
    <span class="ss">status:</span> <span class="ss">:active</span>
  <span class="p">},</span>
  <span class="ss">navigation:</span> <span class="no">nil</span>  <span class="c1"># Communication lost</span>
<span class="p">}</span>
</code></pre></div></div>

<p>We need to check whether thruster impulse matches the navigation target, but we can’t assume navigation data exists.</p>

<p>A <code class="language-plaintext highlighter-rouge">Prism</code> handles optional access: it may or may not find a focus. An <code class="language-plaintext highlighter-rouge">Iso</code> witnesses type equivalence: the conversion is total and reversible. We can compose them, with the prism handling the optional data and the iso managing the unit conversion.</p>

<p>An <code class="language-plaintext highlighter-rouge">Iso</code> can act as a <code class="language-plaintext highlighter-rouge">Prism</code>, so we convert it with <code class="language-plaintext highlighter-rouge">as_prism/1</code>:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">nav_target_prism</span> <span class="o">=</span>
  <span class="no">Prism</span><span class="o">.</span><span class="n">path</span><span class="p">([</span><span class="ss">:navigation</span><span class="p">,</span> <span class="ss">:target_impulse</span><span class="p">])</span>
</code></pre></div></div>

<p>With a good connection, we get <code class="language-plaintext highlighter-rouge">Just</code> the target:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Prism</span><span class="o">.</span><span class="n">preview</span><span class="p">(</span><span class="n">spacecraft_connected</span><span class="p">,</span> <span class="n">nav_target_prism</span><span class="p">)</span>

<span class="c1"># %Funx.Monad.Maybe.Just{value: %NewtonSeconds{value: %Rational{num: 375, den: 1}}}</span>
</code></pre></div></div>

<p>With a lost connection, we get <code class="language-plaintext highlighter-rouge">Nothing</code>:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Prism</span><span class="o">.</span><span class="n">preview</span><span class="p">(</span><span class="n">spacecraft_disconnected</span><span class="p">,</span> <span class="n">nav_target_prism</span><span class="p">)</span>

<span class="c1"># %Funx.Monad.Maybe.Nothing{}</span>
</code></pre></div></div>

<p>But our thruster module needs pound-force seconds. <code class="language-plaintext highlighter-rouge">Iso.from/1</code> flips the direction converting <code class="language-plaintext highlighter-rouge">NewtonSeconds</code> into <code class="language-plaintext highlighter-rouge">LbfSeconds</code>:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">nav_target_lbf_prism</span> <span class="o">=</span>
  <span class="no">Prism</span><span class="o">.</span><span class="n">compose</span><span class="p">(</span>
    <span class="n">nav_target_prism</span><span class="p">,</span>
    <span class="no">Iso</span><span class="o">.</span><span class="n">as_prism</span><span class="p">(</span><span class="no">Iso</span><span class="o">.</span><span class="n">from</span><span class="p">(</span><span class="no">Impulse</span><span class="o">.</span><span class="n">impulse_iso</span><span class="p">())))</span>

<span class="no">Prism</span><span class="o">.</span><span class="n">preview</span><span class="p">(</span><span class="n">spacecraft_connected</span><span class="p">,</span> <span class="n">nav_target_lbf_prism</span><span class="p">)</span>

<span class="c1"># %Funx.Monad.Maybe.Just{</span>
<span class="c1">#   value: %LbfSeconds{value: %Rational{num: 750000000000000, den: 8896443230521}}</span>
<span class="c1"># }</span>
</code></pre></div></div>

<p>Now that we are in the context of <code class="language-plaintext highlighter-rouge">Maybe</code>, we can use <code class="language-plaintext highlighter-rouge">Maybe.traverse</code>:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">alias</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Monad</span><span class="o">.</span><span class="no">Maybe</span>

<span class="n">fleet</span> <span class="o">=</span> <span class="p">[</span><span class="n">spacecraft</span><span class="p">,</span> <span class="n">spacecraft_connected</span><span class="p">]</span>

<span class="n">fleet</span>
<span class="o">|&gt;</span> <span class="no">Maybe</span><span class="o">.</span><span class="n">traverse</span><span class="p">(</span><span class="k">fn</span> <span class="n">ship</span> <span class="o">-&gt;</span> <span class="no">Prism</span><span class="o">.</span><span class="n">preview</span><span class="p">(</span><span class="n">ship</span><span class="p">,</span> <span class="n">nav_target_lbf_prism</span><span class="p">)</span> <span class="k">end</span><span class="p">)</span>

<span class="c1"># %Funx.Monad.Maybe.Just{</span>
<span class="c1">#   value: [</span>
<span class="c1">#     %LbfSeconds{value: %Rational{num: 1002000000000000, den: 8896443230521}},</span>
<span class="c1">#     %LbfSeconds{value: %Rational{num: 750000000000000, den: 8896443230521}}</span>
<span class="c1">#   ]</span>
<span class="c1"># }</span>
</code></pre></div></div>

<p>When every spacecraft in our fleet has nav data, we get <code class="language-plaintext highlighter-rouge">Just</code> the list of nav targets.</p>

<p>If we add our disconnected spacecraft:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">fleet</span> <span class="o">=</span> <span class="p">[</span><span class="n">spacecraft_connected</span><span class="p">,</span> <span class="n">spacecraft_disconnected</span><span class="p">]</span>

<span class="n">fleet</span>
<span class="o">|&gt;</span> <span class="no">Maybe</span><span class="o">.</span><span class="n">traverse</span><span class="p">(</span><span class="k">fn</span> <span class="n">ship</span> <span class="o">-&gt;</span> <span class="no">Prism</span><span class="o">.</span><span class="n">preview</span><span class="p">(</span><span class="n">ship</span><span class="p">,</span> <span class="n">nav_target_prism</span><span class="p">)</span> <span class="k">end</span><span class="p">)</span>

<span class="c1"># %Funx.Monad.Maybe.Nothing{}</span>
</code></pre></div></div>

<p>Now the traversal fails because one spacecraft has no nav data. We get <code class="language-plaintext highlighter-rouge">Nothing</code>.</p>

<p>The prism handles optionality. The iso handles type equivalence.</p>

<h2 id="isolating-decisions">Isolating Decisions</h2>

<p>We’re in the early stages of our project. The team wants to use SuperGIS. It gets us moving quickly and has a free tier. But if we succeed, it gets expensive. The worst case: moderate success, where SuperGIS costs shorten our runway before we can switch.</p>

<p>We need to choose now without losing the ability to change later.</p>

<p><code class="language-plaintext highlighter-rouge">SuperGis.Feature</code> and <code class="language-plaintext highlighter-rouge">GeoJson.Feature</code> are isomorphic types. They encode the same geographic information in different schemas. If we make that isomorphism explicit, we can defer the vendor decision.</p>

<p>The data from SuperGIS looks like this:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">defmodule</span> <span class="no">SuperGis</span><span class="o">.</span><span class="no">Point</span> <span class="k">do</span>
  <span class="k">defstruct</span> <span class="p">[</span><span class="ss">:x</span><span class="p">,</span> <span class="ss">:y</span><span class="p">]</span>
<span class="k">end</span>

<span class="k">defmodule</span> <span class="no">SuperGis</span><span class="o">.</span><span class="no">Attributes</span> <span class="k">do</span>
  <span class="k">defstruct</span> <span class="p">[</span>
    <span class="ss">:object_id</span><span class="p">,</span>
    <span class="ss">:name</span><span class="p">,</span>
    <span class="ss">:category</span><span class="p">,</span>
    <span class="ss">:rating</span><span class="p">,</span>
    <span class="ss">:created_at</span>
  <span class="p">]</span>
<span class="k">end</span>

<span class="k">defmodule</span> <span class="no">SuperGis</span><span class="o">.</span><span class="no">Feature</span> <span class="k">do</span>
  <span class="k">defstruct</span> <span class="p">[</span>
    <span class="ss">geometry:</span> <span class="no">SuperGis</span><span class="o">.</span><span class="no">Point</span><span class="p">,</span>
    <span class="ss">attributes:</span> <span class="no">SuperGis</span><span class="o">.</span><span class="no">Attributes</span>
  <span class="p">]</span>
<span class="k">end</span>
</code></pre></div></div>

<p>We don’t know what we’ll choose later, so we pick GeoJson as our internal type: a general schema that most GIS platforms can map to.</p>

<p>Here is our GeoJson:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">defmodule</span> <span class="no">GeoJson</span><span class="o">.</span><span class="no">Point</span> <span class="k">do</span>
  <span class="k">defstruct</span> <span class="p">[</span><span class="ss">:lon</span><span class="p">,</span> <span class="ss">:lat</span><span class="p">]</span>
<span class="k">end</span>

<span class="k">defmodule</span> <span class="no">GeoJson</span><span class="o">.</span><span class="no">Properties</span> <span class="k">do</span>
  <span class="k">defstruct</span> <span class="p">[</span>
    <span class="ss">:id</span><span class="p">,</span>
    <span class="ss">:name</span><span class="p">,</span>
    <span class="ss">:category</span><span class="p">,</span>
    <span class="ss">:rating</span><span class="p">,</span>
    <span class="ss">:created_at</span>
  <span class="p">]</span>
<span class="k">end</span>

<span class="k">defmodule</span> <span class="no">GeoJson</span><span class="o">.</span><span class="no">Feature</span> <span class="k">do</span>
  <span class="k">defstruct</span> <span class="p">[</span>
    <span class="ss">geometry:</span> <span class="no">GeoJson</span><span class="o">.</span><span class="no">Point</span><span class="p">,</span>
    <span class="ss">properties:</span> <span class="no">GeoJson</span><span class="o">.</span><span class="no">Properties</span>
  <span class="p">]</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Now we define the isomorphism. <code class="language-plaintext highlighter-rouge">SuperGis.Point</code> and <code class="language-plaintext highlighter-rouge">GeoJson.Point</code> are isomorphic: same coordinates, different field names:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">point_iso</span> <span class="o">=</span>
  <span class="no">Iso</span><span class="o">.</span><span class="n">make</span><span class="p">(</span>
    <span class="k">fn</span> <span class="p">%</span><span class="no">SuperGis</span><span class="o">.</span><span class="no">Point</span><span class="p">{</span><span class="ss">x:</span> <span class="n">x</span><span class="p">,</span> <span class="ss">y:</span> <span class="n">y</span><span class="p">}</span> <span class="o">-&gt;</span>
      <span class="p">%</span><span class="no">GeoJson</span><span class="o">.</span><span class="no">Point</span><span class="p">{</span><span class="ss">lon:</span> <span class="n">x</span><span class="p">,</span> <span class="ss">lat:</span> <span class="n">y</span><span class="p">}</span>
    <span class="k">end</span><span class="p">,</span>
    <span class="k">fn</span> <span class="p">%</span><span class="no">GeoJson</span><span class="o">.</span><span class="no">Point</span><span class="p">{</span><span class="ss">lon:</span> <span class="n">lon</span><span class="p">,</span> <span class="ss">lat:</span> <span class="n">lat</span><span class="p">}</span> <span class="o">-&gt;</span>
      <span class="p">%</span><span class="no">SuperGis</span><span class="o">.</span><span class="no">Point</span><span class="p">{</span><span class="ss">x:</span> <span class="n">lon</span><span class="p">,</span> <span class="ss">y:</span> <span class="n">lat</span><span class="p">}</span>
    <span class="k">end</span>
  <span class="p">)</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">SuperGis.Attributes</code> and <code class="language-plaintext highlighter-rouge">GeoJson.Properties</code> are also isomorphic: same data, different names for the container and the id field:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">attributes_iso</span> <span class="o">=</span>
  <span class="no">Iso</span><span class="o">.</span><span class="n">make</span><span class="p">(</span>
    <span class="k">fn</span> <span class="p">%</span><span class="no">SuperGis</span><span class="o">.</span><span class="no">Attributes</span><span class="p">{</span>
         <span class="ss">object_id:</span> <span class="n">id</span><span class="p">,</span>
         <span class="ss">name:</span> <span class="n">name</span><span class="p">,</span>
         <span class="ss">category:</span> <span class="n">category</span><span class="p">,</span>
         <span class="ss">rating:</span> <span class="n">rating</span><span class="p">,</span>
         <span class="ss">created_at:</span> <span class="n">created_at</span>
       <span class="p">}</span> <span class="o">-&gt;</span>
      <span class="p">%</span><span class="no">GeoJson</span><span class="o">.</span><span class="no">Properties</span><span class="p">{</span>
        <span class="ss">id:</span> <span class="n">id</span><span class="p">,</span>
        <span class="ss">name:</span> <span class="n">name</span><span class="p">,</span>
        <span class="ss">category:</span> <span class="n">category</span><span class="p">,</span>
        <span class="ss">rating:</span> <span class="n">rating</span><span class="p">,</span>
        <span class="ss">created_at:</span> <span class="n">created_at</span>
      <span class="p">}</span>
    <span class="k">end</span><span class="p">,</span>
    <span class="k">fn</span> <span class="p">%</span><span class="no">GeoJson</span><span class="o">.</span><span class="no">Properties</span><span class="p">{</span>
         <span class="ss">id:</span> <span class="n">id</span><span class="p">,</span>
         <span class="ss">name:</span> <span class="n">name</span><span class="p">,</span>
         <span class="ss">category:</span> <span class="n">category</span><span class="p">,</span>
         <span class="ss">rating:</span> <span class="n">rating</span><span class="p">,</span>
         <span class="ss">created_at:</span> <span class="n">created_at</span>
       <span class="p">}</span> <span class="o">-&gt;</span>
      <span class="p">%</span><span class="no">SuperGis</span><span class="o">.</span><span class="no">Attributes</span><span class="p">{</span>
        <span class="ss">object_id:</span> <span class="n">id</span><span class="p">,</span>
        <span class="ss">name:</span> <span class="n">name</span><span class="p">,</span>
        <span class="ss">category:</span> <span class="n">category</span><span class="p">,</span>
        <span class="ss">rating:</span> <span class="n">rating</span><span class="p">,</span>
        <span class="ss">created_at:</span> <span class="n">created_at</span>
      <span class="p">}</span>
    <span class="k">end</span>
  <span class="p">)</span>
</code></pre></div></div>

<p>Finally, the feature iso composes the point and attribute isos. <code class="language-plaintext highlighter-rouge">SuperGis.Feature</code> and <code class="language-plaintext highlighter-rouge">GeoJson.Feature</code> are isomorphic because their components are:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">feature_iso</span> <span class="o">=</span>
  <span class="no">Iso</span><span class="o">.</span><span class="n">make</span><span class="p">(</span>
    <span class="k">fn</span> <span class="p">%</span><span class="no">SuperGis</span><span class="o">.</span><span class="no">Feature</span><span class="p">{</span><span class="ss">geometry:</span> <span class="n">geom</span><span class="p">,</span> <span class="ss">attributes:</span> <span class="n">attrs</span><span class="p">}</span> <span class="o">-&gt;</span>
      <span class="p">%</span><span class="no">GeoJson</span><span class="o">.</span><span class="no">Feature</span><span class="p">{</span>
        <span class="ss">geometry:</span> <span class="no">Iso</span><span class="o">.</span><span class="n">view</span><span class="p">(</span><span class="n">geom</span><span class="p">,</span> <span class="n">point_iso</span><span class="p">),</span>
        <span class="ss">properties:</span> <span class="no">Iso</span><span class="o">.</span><span class="n">view</span><span class="p">(</span><span class="n">attrs</span><span class="p">,</span> <span class="n">attributes_iso</span><span class="p">)</span>
      <span class="p">}</span>
    <span class="k">end</span><span class="p">,</span>
    <span class="k">fn</span> <span class="p">%</span><span class="no">GeoJson</span><span class="o">.</span><span class="no">Feature</span><span class="p">{</span><span class="ss">geometry:</span> <span class="n">geom</span><span class="p">,</span> <span class="ss">properties:</span> <span class="n">props</span><span class="p">}</span> <span class="o">-&gt;</span>
      <span class="p">%</span><span class="no">SuperGis</span><span class="o">.</span><span class="no">Feature</span><span class="p">{</span>
        <span class="ss">geometry:</span> <span class="no">Iso</span><span class="o">.</span><span class="n">review</span><span class="p">(</span><span class="n">geom</span><span class="p">,</span> <span class="n">point_iso</span><span class="p">),</span>
        <span class="ss">attributes:</span> <span class="no">Iso</span><span class="o">.</span><span class="n">review</span><span class="p">(</span><span class="n">props</span><span class="p">,</span> <span class="n">attributes_iso</span><span class="p">)</span>
      <span class="p">}</span>
    <span class="k">end</span>
  <span class="p">)</span>
</code></pre></div></div>

<p>Now when we receive a SuperGIS feature:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">super_gis_feature</span> <span class="o">=</span>
  <span class="p">%</span><span class="no">SuperGis</span><span class="o">.</span><span class="no">Feature</span><span class="p">{</span>
    <span class="ss">geometry:</span> <span class="p">%</span><span class="no">SuperGis</span><span class="o">.</span><span class="no">Point</span><span class="p">{</span>
      <span class="ss">x:</span> <span class="o">-</span><span class="mf">122.335167</span><span class="p">,</span>
      <span class="ss">y:</span> <span class="mf">47.608013</span>
    <span class="p">},</span>
    <span class="ss">attributes:</span> <span class="p">%</span><span class="no">SuperGis</span><span class="o">.</span><span class="no">Attributes</span><span class="p">{</span>
      <span class="ss">object_id:</span> <span class="mi">1</span><span class="p">,</span>
      <span class="ss">name:</span> <span class="s2">"Coffee Shop"</span><span class="p">,</span>
      <span class="ss">category:</span> <span class="s2">"Retail"</span><span class="p">,</span>
      <span class="ss">rating:</span> <span class="mf">4.6</span><span class="p">,</span>
      <span class="ss">created_at:</span> <span class="sx">~U[2023-08-15 00:00:00Z]</span>
    <span class="p">}</span>
  <span class="p">}</span>

<span class="c1"># %SuperGis.Feature{</span>
<span class="c1">#   geometry: %SuperGis.Point{x: -122.335167, y: 47.608013},</span>
<span class="c1">#   attributes: %SuperGis.Attributes{</span>
<span class="c1">#     object_id: 1,</span>
<span class="c1">#     name: "Coffee Shop",</span>
<span class="c1">#     category: "Retail",</span>
<span class="c1">#     rating: 4.6,</span>
<span class="c1">#     created_at: ~U[2023-08-15 00:00:00Z]</span>
<span class="c1">#   }</span>
<span class="c1"># }</span>
</code></pre></div></div>

<p>We can <code class="language-plaintext highlighter-rouge">view/2</code> it as GeoJson:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">geo_json_feature</span> <span class="o">=</span> <span class="no">Iso</span><span class="o">.</span><span class="n">view</span><span class="p">(</span><span class="n">super_gis_feature</span><span class="p">,</span> <span class="n">feature_iso</span><span class="p">)</span>

<span class="c1"># %GeoJson.Feature{</span>
<span class="c1">#   geometry: %GeoJson.Point{lon: -122.335167, lat: 47.608013},</span>
<span class="c1">#   properties: %GeoJson.Properties{</span>
<span class="c1">#     id: 1,</span>
<span class="c1">#     name: "Coffee Shop",</span>
<span class="c1">#     category: "Retail",</span>
<span class="c1">#     rating: 4.6,</span>
<span class="c1">#     created_at: ~U[2023-08-15 00:00:00Z]</span>
<span class="c1">#   }</span>
<span class="c1"># }</span>
</code></pre></div></div>

<p>And we can go back:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Iso</span><span class="o">.</span><span class="n">review</span><span class="p">(</span><span class="n">geo_json_feature</span><span class="p">,</span> <span class="n">feature_iso</span><span class="p">)</span>

<span class="c1"># %SuperGis.Feature{</span>
<span class="c1">#   geometry: %SuperGis.Point{x: -122.335167, y: 47.608013},</span>
<span class="c1">#   attributes: %SuperGis.Attributes{</span>
<span class="c1">#     object_id: 1,</span>
<span class="c1">#     name: "Coffee Shop",</span>
<span class="c1">#     category: "Retail",</span>
<span class="c1">#     rating: 4.6,</span>
<span class="c1">#     created_at: ~U[2023-08-15 00:00:00Z]</span>
<span class="c1">#   }</span>
<span class="c1"># }</span>
</code></pre></div></div>

<p>Inside our system, we work with <code class="language-plaintext highlighter-rouge">GeoJson.Feature</code>. At the boundary with SuperGIS, the iso converts. If we later add a different vendor, we write a new iso between that vendor’s types and GeoJson. Our internal code doesn’t change.</p>

<p>More importantly, we don’t need to migrate all at once. Because the isomorphism is explicit, we can replace SuperGIS calls incrementally: the most expensive ones first, while keeping the system coherent.</p>

<h2 id="wrapping-up">Wrapping Up</h2>

<p>Lens focuses on nested data and lets you convert it through an iso. Prism handles optional data, with the iso performing the conversion only when a focus exists. Traversal lets you work with multiple foci across a structure, each one viewed through the same iso.</p>

<p>Whether you’re converting units between subsystems or schemas between vendors, the iso does one job: it makes the equivalence explicit and provides total, reversible functions to move between representations.</p>

<h2 id="resources">Resources</h2>

<p><a href="https://pragprog.com/titles/jkelixir/advanced-functional-programming-with-elixir">Advanced Functional Programming with Elixir</a>
Dive deeper into functional programming patterns and advanced Elixir techniques. Learn how to build robust, maintainable applications using functional programming principles.</p>

<p><a href="https://www.funxlib.com">Funx: Functional Programming for Elixir</a>
A library of functional programming abstractions for Elixir, including monads, monoids, Eq, Ord, and more. Built as an ecosystem where learning is the priority from the start.</p>

<h2 id="resources-1">Resources</h2>

<div style="display: flex; gap: 20px; margin: 20px 0;">
  <div style="flex-shrink: 0;">
    <a href="https://pragprog.com/titles/jkelixir/advanced-functional-programming-with-elixir">
      <img src="/assets/images/jkelixir_small.jpg" alt="Advanced Functional Programming with Elixir book cover" width="150" />
    </a>
  </div>
  <div>
    <h3><a href="https://pragprog.com/titles/jkelixir/advanced-functional-programming-with-elixir">Advanced Functional Programming with Elixir</a></h3>
    <p>Dive deeper into functional programming patterns and advanced Elixir techniques. Learn how to build robust, maintainable applications using functional programming principles.</p>
  </div>
</div>

<div style="display: flex; gap: 20px; margin: 20px 0;">
  <div style="flex-shrink: 0;">
    <a href="https://www.funxlib.com">
      <img src="/assets/images/funx-social.jpg" alt="Funx functional programming library" width="150" />
    </a>
  </div>
  <div>
    <h3><a href="https://www.funxlib.com">Funx - Functional Programming for Elixir</a></h3>
    <p>A library of functional programming abstractions for Elixir, including monads, monoids, Eq, Ord, and more. Built as an ecosystem where learning is the priority from the start.</p>
  </div>
</div>]]></content><author><name>Joseph Koski</name></author><category term="elixir" /><category term="funx" /><summary type="html"><![CDATA[“You’re looking at it wrong.” — The Big Lebowski (1998)]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.joekoski.com/assets/images/funx-social.jpg" /><media:content medium="image" url="https://www.joekoski.com/assets/images/funx-social.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Funx: Adding the Optic Traversal</title><link href="https://www.joekoski.com/blog/2026/01/04/funx-optics-traversal.html" rel="alternate" type="text/html" title="Funx: Adding the Optic Traversal" /><published>2026-01-04T14:16:06+00:00</published><updated>2026-01-04T14:16:06+00:00</updated><id>https://www.joekoski.com/blog/2026/01/04/funx-optics-traversal</id><content type="html" xml:base="https://www.joekoski.com/blog/2026/01/04/funx-optics-traversal.html"><![CDATA[<blockquote>
  <p>“You’re either in or you’re out.” — Ocean’s Eleven (2001)</p>
</blockquote>

<p><a href="https://livebook.dev/run?url=https%3A%2F%2Fwww.joekoski.com%2Fassets%2Flivebooks%2Fblogs%2Ffunx-optics-traversal.livemd"><img src="https://livebook.dev/badge/v1/black.svg" alt="Run in Livebook" /></a></p>

<h2 id="why-traversal">Why Traversal?</h2>

<p>A <code class="language-plaintext highlighter-rouge">Lens</code> is a required focus: if the focus does not exist, that is an invariant violation.</p>

<p>A <code class="language-plaintext highlighter-rouge">Prism</code> is a <code class="language-plaintext highlighter-rouge">Maybe</code> focus: the branch either matches and yields a focus, or it does not.</p>

<p>A <code class="language-plaintext highlighter-rouge">Lens</code> and a <code class="language-plaintext highlighter-rouge">Prism</code> define a single focus, but sometimes we need multiple foci. That is a job for <code class="language-plaintext highlighter-rouge">Traversal</code>.</p>

<p><em>When we hear “traversal” we usually think “iterate a list” or “walk a tree.” That is not the right mental model for an optic traversal. A traversal does not describe how to walk the structure. It names multiple foci in the same structure.</em></p>

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

<p>Let’s continue our system for processing transactions. A transaction can be a charge or a refund, and it can be paid by check or credit card.</p>

<p>What’s new is that each transaction now has an item with a price:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Transaction
├─ item
│  ├─ name
│  └─ price
└─ type
   ├─ Charge
   │  ├─ payment
   │  │  ├─ CreditCard
   │  │  │  └─ amount   ← cc_payment
   │  │  └─ Check
   │  │     └─ amount   ← check_payment
   │  └─ status
   │
   └─ Refund
      ├─ payment
      │  ├─ CreditCard
      │  │  └─ amount   ← cc_refund
      │  └─ Check
      │     └─ amount   ← check_refund
      └─ status
</code></pre></div></div>

<h2 id="building-the-domain-model">Building the Domain Model</h2>

<p>First, let’s define our domain structures:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">alias</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Optics</span><span class="o">.</span><span class="p">{</span><span class="no">Lens</span><span class="p">,</span> <span class="no">Prism</span><span class="p">}</span>
<span class="kn">require</span> <span class="no">Logger</span>
<span class="kn">use</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Monad</span><span class="o">.</span><span class="no">Maybe</span>

<span class="k">defmodule</span> <span class="no">CreditCard</span> <span class="k">do</span>
  <span class="k">defstruct</span> <span class="p">[</span><span class="ss">:name</span><span class="p">,</span> <span class="ss">:number</span><span class="p">,</span> <span class="ss">:expiry</span><span class="p">,</span> <span class="ss">:amount</span><span class="p">]</span>
  <span class="n">alias</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Optics</span><span class="o">.</span><span class="no">Prism</span>

  <span class="k">def</span> <span class="n">amount_prism</span> <span class="k">do</span>
    <span class="no">Prism</span><span class="o">.</span><span class="n">path</span><span class="p">([{</span><span class="bp">__MODULE__</span><span class="p">,</span> <span class="ss">:amount</span><span class="p">}])</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="k">defmodule</span> <span class="no">Check</span> <span class="k">do</span>
  <span class="k">defstruct</span> <span class="p">[</span><span class="ss">:name</span><span class="p">,</span> <span class="ss">:routing_number</span><span class="p">,</span> <span class="ss">:account_number</span><span class="p">,</span> <span class="ss">:amount</span><span class="p">]</span>
  <span class="n">alias</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Optics</span><span class="o">.</span><span class="no">Prism</span>

  <span class="k">def</span> <span class="n">amount_prism</span> <span class="k">do</span>
    <span class="no">Prism</span><span class="o">.</span><span class="n">path</span><span class="p">([{</span><span class="bp">__MODULE__</span><span class="p">,</span> <span class="ss">:amount</span><span class="p">}])</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="k">defmodule</span> <span class="no">Item</span> <span class="k">do</span>
  <span class="k">defstruct</span> <span class="p">[</span><span class="ss">:name</span><span class="p">,</span> <span class="ss">:price</span><span class="p">]</span>
<span class="k">end</span>

<span class="k">defmodule</span> <span class="no">Charge</span> <span class="k">do</span>
  <span class="k">defstruct</span> <span class="p">[</span><span class="ss">:payment</span><span class="p">,</span> <span class="ss">:status</span><span class="p">]</span>
  <span class="n">alias</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Optics</span><span class="o">.</span><span class="no">Prism</span>

  <span class="k">def</span> <span class="n">payment_prism</span> <span class="k">do</span>
    <span class="no">Prism</span><span class="o">.</span><span class="n">path</span><span class="p">([{</span><span class="bp">__MODULE__</span><span class="p">,</span> <span class="ss">:payment</span><span class="p">}])</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="k">defmodule</span> <span class="no">Refund</span> <span class="k">do</span>
  <span class="k">defstruct</span> <span class="p">[</span><span class="ss">:payment</span><span class="p">,</span> <span class="ss">:status</span><span class="p">]</span>
  <span class="n">alias</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Optics</span><span class="o">.</span><span class="no">Prism</span>

  <span class="k">def</span> <span class="n">payment_prism</span> <span class="k">do</span>
    <span class="no">Prism</span><span class="o">.</span><span class="n">path</span><span class="p">([{</span><span class="bp">__MODULE__</span><span class="p">,</span> <span class="ss">:payment</span><span class="p">}])</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="k">defmodule</span> <span class="no">Transaction</span> <span class="k">do</span>
  <span class="k">defstruct</span> <span class="p">[</span><span class="ss">:item</span><span class="p">,</span> <span class="ss">:type</span><span class="p">]</span>
  <span class="n">alias</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Optics</span><span class="o">.</span><span class="no">Prism</span>

  <span class="k">def</span> <span class="n">type_prism</span> <span class="k">do</span>
    <span class="no">Prism</span><span class="o">.</span><span class="n">path</span><span class="p">([{</span><span class="bp">__MODULE__</span><span class="p">,</span> <span class="ss">:type</span><span class="p">}])</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Next, some transactions:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">charge_cc</span> <span class="o">=</span>
  <span class="p">%</span><span class="no">Transaction</span><span class="p">{</span>
    <span class="ss">item:</span> <span class="p">%</span><span class="no">Item</span><span class="p">{</span><span class="ss">name:</span> <span class="s2">"Camera"</span><span class="p">,</span> <span class="ss">price:</span> <span class="mi">500</span><span class="p">},</span>
    <span class="ss">type:</span> <span class="p">%</span><span class="no">Charge</span><span class="p">{</span>
      <span class="ss">payment:</span> <span class="p">%</span><span class="no">CreditCard</span><span class="p">{</span><span class="ss">name:</span> <span class="s2">"Alice"</span><span class="p">,</span> <span class="ss">number:</span> <span class="s2">"4111"</span><span class="p">,</span> <span class="ss">expiry:</span> <span class="s2">"12/26"</span><span class="p">,</span> <span class="ss">amount:</span> <span class="mi">500</span><span class="p">},</span>
      <span class="ss">status:</span> <span class="ss">:pending</span>
    <span class="p">}</span>
  <span class="p">}</span>

<span class="n">invalid_charge_cc</span> <span class="o">=</span>
  <span class="p">%</span><span class="no">Transaction</span><span class="p">{</span>
    <span class="ss">item:</span> <span class="p">%</span><span class="no">Item</span><span class="p">{</span><span class="ss">name:</span> <span class="s2">"Camera"</span><span class="p">,</span> <span class="ss">price:</span> <span class="mi">500</span><span class="p">},</span>
    <span class="ss">type:</span> <span class="p">%</span><span class="no">Charge</span><span class="p">{</span>
      <span class="ss">payment:</span> <span class="p">%</span><span class="no">CreditCard</span><span class="p">{</span><span class="ss">name:</span> <span class="s2">"Alice"</span><span class="p">,</span> <span class="ss">number:</span> <span class="s2">"4111"</span><span class="p">,</span> <span class="ss">expiry:</span> <span class="s2">"12/26"</span><span class="p">,</span> <span class="ss">amount:</span> <span class="mi">400</span><span class="p">},</span>
      <span class="ss">status:</span> <span class="ss">:pending</span>
    <span class="p">}</span>
  <span class="p">}</span>

<span class="n">charge_check</span> <span class="o">=</span>
  <span class="p">%</span><span class="no">Transaction</span><span class="p">{</span>
    <span class="ss">item:</span> <span class="p">%</span><span class="no">Item</span><span class="p">{</span><span class="ss">name:</span> <span class="s2">"Lens"</span><span class="p">,</span> <span class="ss">price:</span> <span class="mi">300</span><span class="p">},</span>
    <span class="ss">type:</span> <span class="p">%</span><span class="no">Charge</span><span class="p">{</span>
      <span class="ss">payment:</span> <span class="p">%</span><span class="no">Check</span><span class="p">{</span><span class="ss">name:</span> <span class="s2">"Bob"</span><span class="p">,</span> <span class="ss">routing_number:</span> <span class="s2">"111000025"</span><span class="p">,</span> <span class="ss">account_number:</span> <span class="s2">"987654"</span><span class="p">,</span> <span class="ss">amount:</span> <span class="mi">300</span><span class="p">},</span>
      <span class="ss">status:</span> <span class="ss">:pending</span>
    <span class="p">}</span>
  <span class="p">}</span>

<span class="n">refund_cc</span> <span class="o">=</span>
  <span class="p">%</span><span class="no">Transaction</span><span class="p">{</span>
    <span class="ss">item:</span> <span class="p">%</span><span class="no">Item</span><span class="p">{</span><span class="ss">name:</span> <span class="s2">"Tripod"</span><span class="p">,</span> <span class="ss">price:</span> <span class="mi">150</span><span class="p">},</span>
    <span class="ss">type:</span> <span class="p">%</span><span class="no">Refund</span><span class="p">{</span>
      <span class="ss">payment:</span> <span class="p">%</span><span class="no">CreditCard</span><span class="p">{</span><span class="ss">name:</span> <span class="s2">"Carol"</span><span class="p">,</span> <span class="ss">number:</span> <span class="s2">"4333"</span><span class="p">,</span> <span class="ss">expiry:</span> <span class="s2">"10/27"</span><span class="p">,</span> <span class="ss">amount:</span> <span class="mi">150</span><span class="p">},</span>
      <span class="ss">status:</span> <span class="ss">:pending</span>
    <span class="p">}</span>
  <span class="p">}</span>

<span class="n">refund_check</span> <span class="o">=</span>
  <span class="p">%</span><span class="no">Transaction</span><span class="p">{</span>
    <span class="ss">item:</span> <span class="p">%</span><span class="no">Item</span><span class="p">{</span><span class="ss">name:</span> <span class="s2">"Flash"</span><span class="p">,</span> <span class="ss">price:</span> <span class="mi">200</span><span class="p">},</span>
    <span class="ss">type:</span> <span class="p">%</span><span class="no">Refund</span><span class="p">{</span>
      <span class="ss">payment:</span> <span class="p">%</span><span class="no">Check</span><span class="p">{</span><span class="ss">name:</span> <span class="s2">"Dave"</span><span class="p">,</span> <span class="ss">routing_number:</span> <span class="s2">"222000025"</span><span class="p">,</span> <span class="ss">account_number:</span> <span class="s2">"123456"</span><span class="p">,</span> <span class="ss">amount:</span> <span class="mi">200</span><span class="p">},</span>
      <span class="ss">status:</span> <span class="ss">:pending</span>
    <span class="p">}</span>
  <span class="p">}</span>

<span class="n">transactions</span> <span class="o">=</span> <span class="p">[</span><span class="n">charge_cc</span><span class="p">,</span> <span class="n">charge_check</span><span class="p">,</span> <span class="n">refund_cc</span><span class="p">,</span> <span class="n">refund_check</span><span class="p">]</span>
</code></pre></div></div>

<p>Our domain requires that a transaction’s item price match its payment amount. We don’t need a single focus: we need both the item and the payment.</p>

<p>This is a boundary problem: we want to prevent an invalid transaction from being processed.</p>

<h2 id="process-a-transaction">Process a Transaction</h2>

<p>In Elixir, the usual way to do that is to protect the boundary in the function head, using pattern matching to ensure the required shape before any work happens.</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">process_basic</span> <span class="o">=</span> <span class="k">fn</span>
  <span class="p">%</span><span class="no">Transaction</span><span class="p">{</span>
    <span class="ss">item:</span> <span class="p">%</span><span class="no">Item</span><span class="p">{</span><span class="ss">price:</span> <span class="n">price</span><span class="p">,</span> <span class="ss">name:</span> <span class="n">name</span><span class="p">},</span>
    <span class="ss">type:</span> <span class="p">%{</span><span class="ss">payment:</span> <span class="p">%{</span><span class="ss">amount:</span> <span class="n">amount</span><span class="p">}}</span> <span class="o">=</span> <span class="n">type</span>
  <span class="p">}</span> <span class="o">=</span> <span class="n">transaction</span>
  <span class="ow">when</span> <span class="n">price</span> <span class="o">==</span> <span class="n">amount</span> <span class="o">-&gt;</span>
    <span class="no">Logger</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="s2">"Processing $</span><span class="si">#{</span><span class="n">amount</span><span class="si">}</span><span class="s2"> for </span><span class="si">#{</span><span class="n">name</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
    <span class="p">%{</span><span class="n">transaction</span> <span class="o">|</span> <span class="ss">type:</span> <span class="p">%{</span><span class="n">type</span> <span class="o">|</span> <span class="ss">status:</span> <span class="ss">:complete</span><span class="p">}}</span>
<span class="k">end</span>
</code></pre></div></div>

<p>This function extracts the price and amount, validates that they match in the guard, and completes the transaction:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">process_basic</span><span class="o">.</span><span class="p">(</span><span class="n">charge_cc</span><span class="p">)</span>

<span class="c1"># [info] Processing $500 for Camera</span>
<span class="c1">#</span>
<span class="c1"># %Transaction{</span>
<span class="c1">#   item: %Item{name: "Camera", price: 500},</span>
<span class="c1">#   type: %Charge{</span>
<span class="c1">#     payment: %CreditCard{name: "Alice", number: "4111", expiry: "12/26", amount: 500},</span>
<span class="c1">#     status: :complete</span>
<span class="c1">#   }</span>
<span class="c1"># }</span>
</code></pre></div></div>

<p>Here, the happy path succeeds.</p>

<p>And the invalid transaction raises:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">process_basic</span><span class="o">.</span><span class="p">(</span><span class="n">invalid_charge_cc</span><span class="p">)</span>

<span class="c1"># ** (FunctionClauseError) no function clause matching in :erl_eval."</span>
</code></pre></div></div>

<p>Putting the domain rule in the function head works, but there is no way to extract this domain logic to test and share.</p>

<h2 id="thinking-functionally">Thinking Functionally</h2>

<p>Let’s use a traversal:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">alias</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Optics</span><span class="o">.</span><span class="p">{</span><span class="no">Lens</span><span class="p">,</span> <span class="no">Traversal</span><span class="p">}</span>

<span class="n">item_and_payment_trav</span> <span class="o">=</span>
  <span class="no">Traversal</span><span class="o">.</span><span class="n">combine</span><span class="p">([</span>
    <span class="no">Lens</span><span class="o">.</span><span class="n">path</span><span class="p">([</span><span class="ss">:item</span><span class="p">]),</span>
    <span class="no">Lens</span><span class="o">.</span><span class="n">path</span><span class="p">([</span><span class="ss">:type</span><span class="p">,</span> <span class="ss">:payment</span><span class="p">])</span>
  <span class="p">])</span>
</code></pre></div></div>

<p>We can use <code class="language-plaintext highlighter-rouge">Traversal.to_list/2</code> to get both values:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">charge_cc</span> <span class="o">|&gt;</span> <span class="no">Traversal</span><span class="o">.</span><span class="n">to_list</span><span class="p">(</span><span class="n">item_and_payment_trav</span><span class="p">)</span>

<span class="c1"># [</span>
<span class="c1">#   %Item{name: "Camera", price: 500},</span>
<span class="c1">#   %CreditCard{name: "Alice", number: "4111", expiry: "12/26", amount: 500}</span>
<span class="c1"># ]</span>
</code></pre></div></div>

<p>Here, we receive a list of foci: the <code class="language-plaintext highlighter-rouge">Item</code> and the <code class="language-plaintext highlighter-rouge">CreditCard</code>.</p>

<p>And we can extend our pipe to check the domain rule:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">charge_cc</span>
<span class="o">|&gt;</span> <span class="no">Traversal</span><span class="o">.</span><span class="n">to_list</span><span class="p">(</span><span class="n">item_and_payment_trav</span><span class="p">)</span>
<span class="o">|&gt;</span> <span class="n">then</span><span class="p">(</span><span class="k">fn</span> <span class="p">[</span><span class="n">item</span><span class="p">,</span> <span class="n">payment</span><span class="p">]</span> <span class="o">-&gt;</span>
  <span class="n">item</span><span class="o">.</span><span class="n">price</span> <span class="o">==</span> <span class="n">payment</span><span class="o">.</span><span class="n">amount</span>
<span class="k">end</span><span class="p">)</span>

<span class="c1"># true</span>
</code></pre></div></div>

<p>Here, the happy path is <code class="language-plaintext highlighter-rouge">true</code>.</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">invalid_charge_cc</span>
<span class="o">|&gt;</span> <span class="no">Traversal</span><span class="o">.</span><span class="n">to_list</span><span class="p">(</span><span class="n">item_and_payment_trav</span><span class="p">)</span>
<span class="o">|&gt;</span> <span class="n">then</span><span class="p">(</span><span class="k">fn</span> <span class="p">[</span><span class="n">item</span><span class="p">,</span> <span class="n">payment</span><span class="p">]</span> <span class="o">-&gt;</span>
  <span class="n">item</span><span class="o">.</span><span class="n">price</span> <span class="o">==</span> <span class="n">payment</span><span class="o">.</span><span class="n">amount</span>
<span class="k">end</span><span class="p">)</span>

<span class="c1"># false</span>
</code></pre></div></div>

<p>And the unhappy path is <code class="language-plaintext highlighter-rouge">false</code>.</p>

<h3 id="maybe-dsl">Maybe DSL</h3>

<p>Let’s implement this logic in the <code class="language-plaintext highlighter-rouge">Maybe</code> DSL:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">process_with_traversal</span> <span class="o">=</span> <span class="k">fn</span> <span class="n">transaction</span> <span class="o">-&gt;</span>
  <span class="n">maybe</span> <span class="n">transaction</span><span class="p">,</span> <span class="ss">as:</span> <span class="ss">:raise</span> <span class="k">do</span>
    <span class="n">bind</span> <span class="no">Traversal</span><span class="o">.</span><span class="n">to_list_maybe</span><span class="p">(</span><span class="n">item_and_payment_trav</span><span class="p">)</span>
    <span class="n">guard</span> <span class="k">fn</span> <span class="p">[</span><span class="n">item</span><span class="p">,</span> <span class="n">payment</span><span class="p">]</span> <span class="o">-&gt;</span> <span class="n">item</span><span class="o">.</span><span class="n">price</span> <span class="o">==</span> <span class="n">payment</span><span class="o">.</span><span class="n">amount</span> <span class="k">end</span>
    <span class="n">tap</span> <span class="k">fn</span> <span class="p">[</span><span class="n">item</span><span class="p">,</span> <span class="n">payment</span><span class="p">]</span> <span class="o">-&gt;</span>
      <span class="no">Logger</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="s2">"Processing $</span><span class="si">#{</span><span class="n">payment</span><span class="o">.</span><span class="n">amount</span><span class="si">}</span><span class="s2"> for </span><span class="si">#{</span><span class="n">item</span><span class="o">.</span><span class="n">name</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
    <span class="k">end</span>
    <span class="n">bind</span> <span class="k">fn</span> <span class="n">_val</span> <span class="o">-&gt;</span> <span class="no">Lens</span><span class="o">.</span><span class="n">set</span><span class="p">(</span><span class="n">transaction</span><span class="p">,</span> <span class="no">Lens</span><span class="o">.</span><span class="n">path</span><span class="p">([</span><span class="ss">:type</span><span class="p">,</span> <span class="ss">:status</span><span class="p">]),</span> <span class="ss">:complete</span><span class="p">)</span> <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Here, we are using <code class="language-plaintext highlighter-rouge">to_list_maybe/2</code> to lift the results of our traversal into the <code class="language-plaintext highlighter-rouge">Maybe</code> context. Next, we narrow the boundary with a guard implementing the domain rule. Then we log what we plan to do, and finally we update our transaction status to <code class="language-plaintext highlighter-rouge">:complete</code>.</p>

<p>Our happy path still works:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">process_with_traversal</span><span class="o">.</span><span class="p">(</span><span class="n">charge_cc</span><span class="p">)</span>

<span class="c1"># [info] Processing $500 for Camera</span>
<span class="c1">#</span>
<span class="c1"># %Transaction{</span>
<span class="c1">#   item: %Item{name: "Camera", price: 500},</span>
<span class="c1">#   type: %Charge{</span>
<span class="c1">#     payment: %CreditCard{name: "Alice", number: "4111", expiry: "12/26", amount: 500},</span>
<span class="c1">#     status: :complete</span>
<span class="c1">#   }</span>
<span class="c1"># }</span>
</code></pre></div></div>

<p>And our unhappy path continues to fail quickly:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">process_with_traversal</span><span class="o">.</span><span class="p">(</span><span class="n">invalid_charge_cc</span><span class="p">)</span>

<span class="c1"># ** (RuntimeError) Nothing value encountered</span>
</code></pre></div></div>

<p>Now our boundary rules are easy to update, test, and share, which becomes more important as complexity grows.</p>

<h2 id="implementing-sum-types">Implementing Sum Types</h2>

<p>Let’s extend our boundary to the payment type. Not just a payment, but the specific payment branch, such as “refund credit card.”</p>

<p>Again, the idiomatic Elixir approach is to encode the boundary logic directly in the function head.</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">defmodule</span> <span class="no">Guarded</span><span class="o">.</span><span class="no">TransactionProcessor</span> <span class="k">do</span>
  <span class="k">def</span> <span class="n">cc_payment</span><span class="p">(</span>
        <span class="p">%</span><span class="no">Transaction</span><span class="p">{</span>
          <span class="ss">item:</span> <span class="p">%</span><span class="no">Item</span><span class="p">{</span><span class="ss">price:</span> <span class="n">item_price</span><span class="p">,</span> <span class="ss">name:</span> <span class="n">name</span><span class="p">},</span>
          <span class="ss">type:</span> <span class="p">%</span><span class="no">Charge</span><span class="p">{</span>
            <span class="ss">payment:</span> <span class="p">%</span><span class="no">CreditCard</span><span class="p">{</span><span class="ss">amount:</span> <span class="n">payment_amount</span><span class="p">}</span>
          <span class="p">}</span> <span class="o">=</span> <span class="n">charge</span>
        <span class="p">}</span> <span class="o">=</span> <span class="n">transaction</span>
      <span class="p">)</span> <span class="ow">when</span> <span class="n">item_price</span> <span class="o">==</span> <span class="n">payment_amount</span> <span class="k">do</span>
    <span class="no">Logger</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="s2">"Charge cc $</span><span class="si">#{</span><span class="n">payment_amount</span><span class="si">}</span><span class="s2"> for </span><span class="si">#{</span><span class="n">name</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>

    <span class="p">%{</span>
      <span class="n">transaction</span>
      <span class="o">|</span> <span class="ss">type:</span> <span class="p">%{</span><span class="n">charge</span> <span class="o">|</span> <span class="ss">status:</span> <span class="ss">:complete</span><span class="p">}</span>
    <span class="p">}</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">check_payment</span><span class="p">(</span>
        <span class="p">%</span><span class="no">Transaction</span><span class="p">{</span>
          <span class="ss">item:</span> <span class="p">%</span><span class="no">Item</span><span class="p">{</span><span class="ss">price:</span> <span class="n">item_price</span><span class="p">,</span> <span class="ss">name:</span> <span class="n">name</span><span class="p">},</span>
          <span class="ss">type:</span> <span class="p">%</span><span class="no">Charge</span><span class="p">{</span>
            <span class="ss">payment:</span> <span class="p">%</span><span class="no">Check</span><span class="p">{</span><span class="ss">amount:</span> <span class="n">payment_amount</span><span class="p">}</span>
          <span class="p">}</span> <span class="o">=</span> <span class="n">charge</span>
        <span class="p">}</span> <span class="o">=</span> <span class="n">transaction</span>
      <span class="p">)</span> <span class="ow">when</span> <span class="n">item_price</span> <span class="o">==</span> <span class="n">payment_amount</span> <span class="k">do</span>
    <span class="no">Logger</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="s2">"Charge check $</span><span class="si">#{</span><span class="n">payment_amount</span><span class="si">}</span><span class="s2"> for </span><span class="si">#{</span><span class="n">name</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>

    <span class="p">%{</span>
      <span class="n">transaction</span>
      <span class="o">|</span> <span class="ss">type:</span> <span class="p">%{</span><span class="n">charge</span> <span class="o">|</span> <span class="ss">status:</span> <span class="ss">:complete</span><span class="p">}</span>
    <span class="p">}</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">cc_refund</span><span class="p">(</span>
        <span class="p">%</span><span class="no">Transaction</span><span class="p">{</span>
          <span class="ss">item:</span> <span class="p">%</span><span class="no">Item</span><span class="p">{</span><span class="ss">price:</span> <span class="n">item_price</span><span class="p">,</span> <span class="ss">name:</span> <span class="n">name</span><span class="p">},</span>
          <span class="ss">type:</span> <span class="p">%</span><span class="no">Refund</span><span class="p">{</span>
            <span class="ss">payment:</span> <span class="p">%</span><span class="no">CreditCard</span><span class="p">{</span><span class="ss">amount:</span> <span class="n">payment_amount</span><span class="p">}</span>
          <span class="p">}</span> <span class="o">=</span> <span class="n">refund</span>
        <span class="p">}</span> <span class="o">=</span> <span class="n">transaction</span>
      <span class="p">)</span> <span class="ow">when</span> <span class="n">item_price</span> <span class="o">==</span> <span class="n">payment_amount</span> <span class="k">do</span>
    <span class="no">Logger</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="s2">"Refund cc $</span><span class="si">#{</span><span class="n">payment_amount</span><span class="si">}</span><span class="s2"> for </span><span class="si">#{</span><span class="n">name</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>

    <span class="p">%{</span>
      <span class="n">transaction</span>
      <span class="o">|</span> <span class="ss">type:</span> <span class="p">%{</span><span class="n">refund</span> <span class="o">|</span> <span class="ss">status:</span> <span class="ss">:complete</span><span class="p">}</span>
    <span class="p">}</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">check_refund</span><span class="p">(</span>
        <span class="p">%</span><span class="no">Transaction</span><span class="p">{</span>
          <span class="ss">item:</span> <span class="p">%</span><span class="no">Item</span><span class="p">{</span><span class="ss">price:</span> <span class="n">item_price</span><span class="p">,</span> <span class="ss">name:</span> <span class="n">name</span><span class="p">},</span>
          <span class="ss">type:</span> <span class="p">%</span><span class="no">Refund</span><span class="p">{</span>
            <span class="ss">payment:</span> <span class="p">%</span><span class="no">Check</span><span class="p">{</span><span class="ss">amount:</span> <span class="n">payment_amount</span><span class="p">}</span>
          <span class="p">}</span> <span class="o">=</span> <span class="n">refund</span>
        <span class="p">}</span> <span class="o">=</span> <span class="n">transaction</span>
      <span class="p">)</span> <span class="ow">when</span> <span class="n">item_price</span> <span class="o">==</span> <span class="n">payment_amount</span> <span class="k">do</span>
    <span class="no">Logger</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="s2">"Refund check $</span><span class="si">#{</span><span class="n">payment_amount</span><span class="si">}</span><span class="s2"> for </span><span class="si">#{</span><span class="n">name</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>

    <span class="p">%{</span>
      <span class="n">transaction</span>
      <span class="o">|</span> <span class="ss">type:</span> <span class="p">%{</span><span class="n">refund</span> <span class="o">|</span> <span class="ss">status:</span> <span class="ss">:complete</span><span class="p">}</span>
    <span class="p">}</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>There is nothing particularly wrong with this logic, but if we need it in other places we will need to copy and paste. In fact, we are copying and pasting our domain rule <code class="language-plaintext highlighter-rouge">item_price == payment_amount</code> four times in this module alone.</p>

<p>Our happy path works:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Guarded</span><span class="o">.</span><span class="no">TransactionProcessor</span><span class="o">.</span><span class="n">cc_payment</span><span class="p">(</span><span class="n">charge_cc</span><span class="p">)</span>

<span class="c1"># [info] Charge cc $500 for Camera</span>
<span class="c1">#</span>
<span class="c1"># %Transaction{</span>
<span class="c1">#   item: %Item{name: "Camera", price: 500},</span>
<span class="c1">#   type: %Charge{</span>
<span class="c1">#     payment: %CreditCard{name: "Alice", number: "4111", expiry: "12/26", amount: 500},</span>
<span class="c1">#     status: :complete</span>
<span class="c1">#   }</span>
<span class="c1"># }</span>
</code></pre></div></div>

<p>The invalid charge still raises:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Guarded</span><span class="o">.</span><span class="no">TransactionProcessor</span><span class="o">.</span><span class="n">cc_payment</span><span class="p">(</span><span class="n">invalid_charge_cc</span><span class="p">)</span>

<span class="c1"># ** (FunctionClauseError) no function clause matching in Guarded.TransactionProcessor.cc_payment/1    </span>
</code></pre></div></div>

<p>And now a payment mismatch also raises:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Guarded</span><span class="o">.</span><span class="no">TransactionProcessor</span><span class="o">.</span><span class="n">cc_payment</span><span class="p">(</span><span class="n">refund_check</span><span class="p">)</span>

<span class="c1"># ** (FunctionClauseError) no function clause matching in Guarded.TransactionProcessor.cc_payment/1    </span>
</code></pre></div></div>

<p>Our function head is doing three things at once:</p>

<ul>
  <li>It selects the shape required by the operation.</li>
  <li>It extracts the foci needed by the operation.</li>
  <li>It enforces the domain rule that relates those foci.</li>
</ul>

<p>Again, it works, but it is not a reusable boundary. If the same applicability rule matters in another workflow, the rule has to be re-expressed in a new function head. Worse, when the domain changes, we need to find and update all the rules in lockstep.</p>

<h2 id="the-functional-way">The Functional Way</h2>

<p>First, let’s compose the traversals we care about:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">defmodule</span> <span class="no">Processor</span> <span class="k">do</span>
  <span class="n">alias</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Optics</span><span class="o">.</span><span class="p">{</span><span class="no">Lens</span><span class="p">,</span> <span class="no">Prism</span><span class="p">,</span> <span class="no">Traversal</span><span class="p">}</span>

  <span class="k">def</span> <span class="n">item_lens</span> <span class="k">do</span>
    <span class="no">Lens</span><span class="o">.</span><span class="n">path</span><span class="p">([</span><span class="ss">:item</span><span class="p">])</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">cc_payment_trav</span> <span class="k">do</span>
    <span class="no">Traversal</span><span class="o">.</span><span class="n">combine</span><span class="p">([</span>
      <span class="no">Processor</span><span class="o">.</span><span class="n">item_lens</span><span class="p">,</span>
      <span class="no">Prism</span><span class="o">.</span><span class="n">compose</span><span class="p">([</span>
        <span class="no">Transaction</span><span class="o">.</span><span class="n">type_prism</span><span class="p">,</span>
        <span class="no">Charge</span><span class="o">.</span><span class="n">payment_prism</span><span class="p">,</span>
        <span class="no">Prism</span><span class="o">.</span><span class="n">struct</span><span class="p">(</span><span class="no">CreditCard</span><span class="p">)</span>
      <span class="p">])</span>
    <span class="p">])</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">check_payment_trav</span> <span class="k">do</span>
    <span class="no">Traversal</span><span class="o">.</span><span class="n">combine</span><span class="p">([</span>
      <span class="no">Processor</span><span class="o">.</span><span class="n">item_lens</span><span class="p">,</span>
      <span class="no">Prism</span><span class="o">.</span><span class="n">compose</span><span class="p">([</span>
        <span class="no">Transaction</span><span class="o">.</span><span class="n">type_prism</span><span class="p">,</span>
        <span class="no">Charge</span><span class="o">.</span><span class="n">payment_prism</span><span class="p">,</span>
        <span class="no">Prism</span><span class="o">.</span><span class="n">struct</span><span class="p">(</span><span class="no">Check</span><span class="p">)</span>
      <span class="p">])</span>
    <span class="p">])</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">cc_refund_trav</span> <span class="k">do</span>
    <span class="no">Traversal</span><span class="o">.</span><span class="n">combine</span><span class="p">([</span>
      <span class="no">Processor</span><span class="o">.</span><span class="n">item_lens</span><span class="p">,</span>
      <span class="no">Prism</span><span class="o">.</span><span class="n">compose</span><span class="p">([</span>
        <span class="no">Transaction</span><span class="o">.</span><span class="n">type_prism</span><span class="p">,</span>
        <span class="no">Refund</span><span class="o">.</span><span class="n">payment_prism</span><span class="p">,</span>
        <span class="no">Prism</span><span class="o">.</span><span class="n">struct</span><span class="p">(</span><span class="no">CreditCard</span><span class="p">)</span>
      <span class="p">])</span>
    <span class="p">])</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">check_refund_trav</span> <span class="k">do</span>
    <span class="no">Traversal</span><span class="o">.</span><span class="n">combine</span><span class="p">([</span>
      <span class="no">Processor</span><span class="o">.</span><span class="n">item_lens</span><span class="p">,</span>
      <span class="no">Prism</span><span class="o">.</span><span class="n">compose</span><span class="p">([</span>
        <span class="no">Transaction</span><span class="o">.</span><span class="n">type_prism</span><span class="p">,</span>
        <span class="no">Refund</span><span class="o">.</span><span class="n">payment_prism</span><span class="p">,</span>
        <span class="no">Prism</span><span class="o">.</span><span class="n">struct</span><span class="p">(</span><span class="no">Check</span><span class="p">)</span>
      <span class="p">])</span>
    <span class="p">])</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">payment_status_lens</span> <span class="k">do</span>
    <span class="no">Lens</span><span class="o">.</span><span class="n">path</span><span class="p">([</span><span class="ss">:type</span><span class="p">,</span> <span class="ss">:status</span><span class="p">])</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Here, we are no longer using a <code class="language-plaintext highlighter-rouge">Lens</code> for our payments. Instead we express the sum type with a <code class="language-plaintext highlighter-rouge">Prism</code>. The item must exist, and the payment branch might exist.</p>

<h3 id="domain-validation">Domain validation</h3>

<p>Let’s extract our guard:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">defmodule</span> <span class="no">PaymentMustMatchPrice</span> <span class="k">do</span>
  <span class="k">def</span> <span class="n">run_maybe</span><span class="p">([</span><span class="n">item</span><span class="p">,</span> <span class="n">payment</span><span class="p">],</span> <span class="n">_opts</span><span class="p">,</span> <span class="n">_env</span><span class="p">)</span> <span class="k">do</span>
    <span class="n">item</span><span class="o">.</span><span class="n">price</span> <span class="o">==</span> <span class="n">payment</span><span class="o">.</span><span class="n">amount</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<h3 id="logging">Logging</h3>

<p>And isolate our log:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">defmodule</span> <span class="no">LogTransaction</span> <span class="k">do</span>
  <span class="k">def</span> <span class="n">run_maybe</span><span class="p">([</span><span class="n">item</span><span class="p">,</span> <span class="n">payment</span><span class="p">],</span> <span class="n">opts</span><span class="p">,</span> <span class="n">_env</span><span class="p">)</span> <span class="k">do</span>
    <span class="n">prefix</span> <span class="o">=</span> <span class="no">Keyword</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">opts</span><span class="p">,</span> <span class="ss">:prefix</span><span class="p">)</span>

    <span class="no">Logger</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="s2">"</span><span class="si">#{</span><span class="n">prefix</span><span class="si">}</span><span class="s2"> $</span><span class="si">#{</span><span class="n">payment</span><span class="o">.</span><span class="n">amount</span><span class="si">}</span><span class="s2"> for </span><span class="si">#{</span><span class="n">item</span><span class="o">.</span><span class="n">name</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
    <span class="ss">:ok</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<h3 id="completing-the-transaction">Completing the transaction</h3>

<p>And our update logic as well:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">defmodule</span> <span class="no">CompleteTransaction</span> <span class="k">do</span>
  <span class="n">alias</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Optics</span><span class="o">.</span><span class="no">Lens</span>

  <span class="k">def</span> <span class="n">run_maybe</span><span class="p">(</span><span class="n">_foci</span><span class="p">,</span> <span class="n">opts</span><span class="p">,</span> <span class="n">_env</span><span class="p">)</span> <span class="k">do</span>
    <span class="n">transaction</span> <span class="o">=</span> <span class="no">Keyword</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">opts</span><span class="p">,</span> <span class="ss">:original</span><span class="p">)</span>

    <span class="no">Lens</span><span class="o">.</span><span class="n">set</span><span class="p">(</span><span class="n">transaction</span><span class="p">,</span> <span class="no">Processor</span><span class="o">.</span><span class="n">payment_status_lens</span><span class="p">,</span> <span class="ss">:complete</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<h2 id="declarative-processor">Declarative Processor</h2>

<p>Now we can make a much more declarative processor:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">defmodule</span> <span class="no">Declarative</span><span class="o">.</span><span class="no">TransactionProcessor</span> <span class="k">do</span>
  <span class="n">alias</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Optics</span><span class="o">.</span><span class="no">Traversal</span>

  <span class="k">def</span> <span class="n">cc_payment</span><span class="p">(</span><span class="n">transaction</span><span class="p">)</span> <span class="k">do</span>
    <span class="n">maybe</span> <span class="n">transaction</span><span class="p">,</span> <span class="ss">as:</span> <span class="ss">:raise</span> <span class="k">do</span>
      <span class="n">bind</span> <span class="no">Traversal</span><span class="o">.</span><span class="n">to_list_maybe</span><span class="p">(</span><span class="no">Processor</span><span class="o">.</span><span class="n">cc_payment_trav</span><span class="p">)</span>
      <span class="n">guard</span> <span class="no">PaymentMustMatchPrice</span>
      <span class="n">tap</span> <span class="p">{</span><span class="no">LogTransaction</span><span class="p">,</span> <span class="ss">prefix:</span> <span class="s2">"Charging cc"</span><span class="p">}</span>
      <span class="n">bind</span> <span class="p">{</span><span class="no">CompleteTransaction</span><span class="p">,</span> <span class="ss">original:</span> <span class="n">transaction</span><span class="p">}</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">check_payment</span><span class="p">(</span><span class="n">transaction</span><span class="p">)</span> <span class="k">do</span>
    <span class="n">maybe</span> <span class="n">transaction</span><span class="p">,</span> <span class="ss">as:</span> <span class="ss">:raise</span> <span class="k">do</span>
      <span class="n">bind</span> <span class="no">Traversal</span><span class="o">.</span><span class="n">to_list_maybe</span><span class="p">(</span><span class="no">Processor</span><span class="o">.</span><span class="n">check_payment_trav</span><span class="p">)</span>
      <span class="n">guard</span> <span class="no">PaymentMustMatchPrice</span>
      <span class="n">tap</span> <span class="p">{</span><span class="no">LogTransaction</span><span class="p">,</span> <span class="ss">prefix:</span> <span class="s2">"Charging check"</span><span class="p">}</span>
      <span class="n">bind</span> <span class="p">{</span><span class="no">CompleteTransaction</span><span class="p">,</span> <span class="ss">original:</span> <span class="n">transaction</span><span class="p">}</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">cc_refund</span><span class="p">(</span><span class="n">transaction</span><span class="p">)</span> <span class="k">do</span>
    <span class="n">maybe</span> <span class="n">transaction</span><span class="p">,</span> <span class="ss">as:</span> <span class="ss">:raise</span> <span class="k">do</span>
      <span class="n">bind</span> <span class="no">Traversal</span><span class="o">.</span><span class="n">to_list_maybe</span><span class="p">(</span><span class="no">Processor</span><span class="o">.</span><span class="n">cc_refund_trav</span><span class="p">)</span>
      <span class="n">guard</span> <span class="no">PaymentMustMatchPrice</span>
      <span class="n">tap</span> <span class="p">{</span><span class="no">LogTransaction</span><span class="p">,</span> <span class="ss">prefix:</span> <span class="s2">"Refunding cc"</span><span class="p">}</span>
      <span class="n">bind</span> <span class="p">{</span><span class="no">CompleteTransaction</span><span class="p">,</span> <span class="ss">original:</span> <span class="n">transaction</span><span class="p">}</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">check_refund</span><span class="p">(</span><span class="n">transaction</span><span class="p">)</span> <span class="k">do</span>
    <span class="n">maybe</span> <span class="n">transaction</span><span class="p">,</span> <span class="ss">as:</span> <span class="ss">:raise</span> <span class="k">do</span>
      <span class="n">bind</span> <span class="no">Traversal</span><span class="o">.</span><span class="n">to_list_maybe</span><span class="p">(</span><span class="no">Processor</span><span class="o">.</span><span class="n">check_refund_trav</span><span class="p">)</span>
      <span class="n">guard</span> <span class="no">PaymentMustMatchPrice</span>
      <span class="n">tap</span> <span class="p">{</span><span class="no">LogTransaction</span><span class="p">,</span> <span class="ss">prefix:</span> <span class="s2">"Refunding check"</span><span class="p">}</span>
      <span class="n">bind</span> <span class="p">{</span><span class="no">CompleteTransaction</span><span class="p">,</span> <span class="ss">original:</span> <span class="n">transaction</span><span class="p">}</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Here:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">Traversal.to_list_maybe/2</code> lifts our traversal into the <code class="language-plaintext highlighter-rouge">Maybe</code> context, and enforces that either all foci exist together (<code class="language-plaintext highlighter-rouge">Just</code>) or one or more prisms are missing (<code class="language-plaintext highlighter-rouge">Nothing</code>).</li>
  <li><code class="language-plaintext highlighter-rouge">guard/2</code> further reduces the boundary with our domain rule.</li>
  <li><code class="language-plaintext highlighter-rouge">tap/2</code> calls the log effect, but does not fail the pipeline.</li>
  <li><code class="language-plaintext highlighter-rouge">CompleteTransaction</code> defines an action that may fail.</li>
</ul>

<p>Again, our happy path succeeds:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Declarative</span><span class="o">.</span><span class="no">TransactionProcessor</span><span class="o">.</span><span class="n">cc_payment</span><span class="p">(</span><span class="n">charge_cc</span><span class="p">)</span>

<span class="c1"># [info] Charge cc $500 for Camera</span>
<span class="c1">#</span>
<span class="c1"># %Transaction{</span>
<span class="c1">#   item: %Item{name: "Camera", price: 500},</span>
<span class="c1">#   type: %Charge{</span>
<span class="c1">#     payment: %CreditCard{name: "Alice", number: "4111", expiry: "12/26", amount: 500},</span>
<span class="c1">#     status: :complete</span>
<span class="c1">#   }</span>
<span class="c1"># }</span>
</code></pre></div></div>

<p>The invalid credit card charge raises:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Declarative</span><span class="o">.</span><span class="no">TransactionProcessor</span><span class="o">.</span><span class="n">cc_payment</span><span class="p">(</span><span class="n">refund_cc</span><span class="p">)</span>

<span class="c1"># ** (RuntimeError) Nothing value encountered</span>
</code></pre></div></div>

<p>And so does the mismatch:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Declarative</span><span class="o">.</span><span class="no">TransactionProcessor</span><span class="o">.</span><span class="n">cc_payment</span><span class="p">(</span><span class="n">invalid_charge_cc</span><span class="p">)</span>

<span class="c1"># ** (RuntimeError) Nothing value encountered</span>
</code></pre></div></div>

<p>This is not about replacing pattern matching. Pattern matching remains a strong tool. The difference is that the requirements for an operation can exist as values, which lets the code stay declarative: named, reusable, testable, and composable.</p>

<h2 id="resources">Resources</h2>

<div style="display: flex; gap: 20px; margin: 20px 0;">
  <div style="flex-shrink: 0;">
    <a href="https://pragprog.com/titles/jkelixir/advanced-functional-programming-with-elixir">
      <img src="/assets/images/jkelixir_small.jpg" alt="Advanced Functional Programming with Elixir book cover" width="150" />
    </a>
  </div>
  <div>
    <h3><a href="https://pragprog.com/titles/jkelixir/advanced-functional-programming-with-elixir">Advanced Functional Programming with Elixir</a></h3>
    <p>Dive deeper into functional programming patterns and advanced Elixir techniques. Learn how to build robust, maintainable applications using functional programming principles.</p>
  </div>
</div>

<div style="display: flex; gap: 20px; margin: 20px 0;">
  <div style="flex-shrink: 0;">
    <a href="https://www.funxlib.com">
      <img src="/assets/images/funx-social.jpg" alt="Funx functional programming library" width="150" />
    </a>
  </div>
  <div>
    <h3><a href="https://www.funxlib.com">Funx - Functional Programming for Elixir</a></h3>
    <p>A library of functional programming abstractions for Elixir, including monads, monoids, Eq, Ord, and more. Built as an ecosystem where learning is the priority from the start.</p>
  </div>
</div>]]></content><author><name>Joseph Koski</name></author><category term="elixir" /><category term="funx" /><summary type="html"><![CDATA[“You’re either in or you’re out.” — Ocean’s Eleven (2001)]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.joekoski.com/assets/images/funx-social.jpg" /><media:content medium="image" url="https://www.joekoski.com/assets/images/funx-social.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Funx: Reorganizing Eq and Ord</title><link href="https://www.joekoski.com/blog/2025/12/31/eq-ord-change.html" rel="alternate" type="text/html" title="Funx: Reorganizing Eq and Ord" /><published>2025-12-31T14:16:06+00:00</published><updated>2025-12-31T14:16:06+00:00</updated><id>https://www.joekoski.com/blog/2025/12/31/eq-ord-change</id><content type="html" xml:base="https://www.joekoski.com/blog/2025/12/31/eq-ord-change.html"><![CDATA[<p>When designing Funx I leveraged protocols, which meant some concessions.</p>

<p>In Haskell, you can state class relationships like “every Monad is a Functor” (via superclass constraints). In Elixir, there isn’t an equivalent way to express that kind of protocol hierarchy. So instead of creating separate protocols and implying a relationship I can’t encode, I grouped the operations into a single protocol. I could split out <code class="language-plaintext highlighter-rouge">Functor</code> and <code class="language-plaintext highlighter-rouge">Applicative</code>, but in practice you are just importing functions and it’s simpler to import <code class="language-plaintext highlighter-rouge">Monad</code> and get <code class="language-plaintext highlighter-rouge">map</code>, <code class="language-plaintext highlighter-rouge">ap</code>, and <code class="language-plaintext highlighter-rouge">bind</code>.</p>

<p>Also, in Elixir, a protocol module can’t serve as the public entry point for functions. That’s not a problem for monads: their contexts provide namespaces: <code class="language-plaintext highlighter-rouge">Either</code>, <code class="language-plaintext highlighter-rouge">Maybe</code>, <code class="language-plaintext highlighter-rouge">Reader</code>. But with <code class="language-plaintext highlighter-rouge">Eq</code> and <code class="language-plaintext highlighter-rouge">Ord</code>, there aren’t obvious namespaces, so I had a choice:</p>

<ol>
  <li>Name the protocol <code class="language-plaintext highlighter-rouge">Eq</code> and put the utility functions in <code class="language-plaintext highlighter-rouge">Eq.Utils</code>.</li>
  <li>Name the protocol something like <code class="language-plaintext highlighter-rouge">Eq.Protocol</code> and use <code class="language-plaintext highlighter-rouge">Eq</code> as the namespace for the grab-bag of equality utilities.</li>
</ol>

<p>I chose consistency with other protocols like <code class="language-plaintext highlighter-rouge">Monad</code>, <code class="language-plaintext highlighter-rouge">Foldable</code>, and <code class="language-plaintext highlighter-rouge">Filterable</code>. Having <code class="language-plaintext highlighter-rouge">Utils</code> in both the <code class="language-plaintext highlighter-rouge">Ord</code> and <code class="language-plaintext highlighter-rouge">Eq</code> namespaces was annoying, but manageable by aliasing them, for example as <code class="language-plaintext highlighter-rouge">EqUtil</code>.</p>

<p>Once I started building a DSL, that decision became harder to justify. The DSL couldn’t live in the <code class="language-plaintext highlighter-rouge">Eq</code> protocol module, which meant either explaining that the DSL lived under <code class="language-plaintext highlighter-rouge">Eq.Utils</code>, or introducing yet another namespace, <code class="language-plaintext highlighter-rouge">Eq.Dsl</code>.</p>

<p>At that point it was clear that in Funx, the <code class="language-plaintext highlighter-rouge">Eq</code> and <code class="language-plaintext highlighter-rouge">Ord</code> protocols mostly live in the background, which means choosing protocol naming consistency makes less sense. So I’m switching to option 2: <code class="language-plaintext highlighter-rouge">Eq</code> and <code class="language-plaintext highlighter-rouge">Ord</code> are the primary public entry points for the utility functions and the DSL, and the protocols move to <code class="language-plaintext highlighter-rouge">Eq.Protocol</code> and <code class="language-plaintext highlighter-rouge">Ord.Protocol</code>.</p>

<p>I considered waiting until 1.0, but once I made the decision it didn’t make sense to delay. The longer I waited, the more code I (and others) would generate that would need to be migrated.</p>

<h2 id="changelog">ChangeLog</h2>

<h3 id="eq-module-changes">Eq module changes</h3>

<ul>
  <li>
    <p><code class="language-plaintext highlighter-rouge">Funx.Eq</code> (protocol) → <code class="language-plaintext highlighter-rouge">Funx.Eq.Protocol</code></p>

    <ul>
      <li>The equality protocol is now <code class="language-plaintext highlighter-rouge">Funx.Eq.Protocol</code></li>
      <li>Protocol implementations must use <code class="language-plaintext highlighter-rouge">defimpl Funx.Eq.Protocol, for: YourType</code></li>
    </ul>
  </li>
  <li>
    <p><code class="language-plaintext highlighter-rouge">Funx.Eq.Utils</code> → <code class="language-plaintext highlighter-rouge">Funx.Eq</code></p>

    <ul>
      <li>Utility functions moved from <code class="language-plaintext highlighter-rouge">Funx.Eq.Utils</code> to <code class="language-plaintext highlighter-rouge">Funx.Eq</code></li>
      <li>DSL merged into <code class="language-plaintext highlighter-rouge">Funx.Eq</code> (no more separate <code class="language-plaintext highlighter-rouge">Funx.Eq.Dsl</code>)</li>
      <li><code class="language-plaintext highlighter-rouge">use Funx.Eq</code> imports only the <code class="language-plaintext highlighter-rouge">eq</code> DSL macro</li>
      <li><code class="language-plaintext highlighter-rouge">alias Funx.Eq</code> for utility functions (optional, or use fully qualified)</li>
    </ul>
  </li>
</ul>

<h3 id="ord-module-changes">Ord module changes</h3>

<ul>
  <li>
    <p><code class="language-plaintext highlighter-rouge">Funx.Ord</code> (protocol) → <code class="language-plaintext highlighter-rouge">Funx.Ord.Protocol</code></p>

    <ul>
      <li>The ordering protocol is now <code class="language-plaintext highlighter-rouge">Funx.Ord.Protocol</code></li>
      <li>Protocol implementations must use <code class="language-plaintext highlighter-rouge">defimpl Funx.Ord.Protocol, for: YourType</code></li>
    </ul>
  </li>
  <li>
    <p><code class="language-plaintext highlighter-rouge">Funx.Ord.Utils</code> → <code class="language-plaintext highlighter-rouge">Funx.Ord</code></p>

    <ul>
      <li>Utility functions moved from <code class="language-plaintext highlighter-rouge">Funx.Ord.Utils</code> to <code class="language-plaintext highlighter-rouge">Funx.Ord</code></li>
      <li>DSL merged into <code class="language-plaintext highlighter-rouge">Funx.Ord</code> (no more separate <code class="language-plaintext highlighter-rouge">Funx.Ord.Dsl</code>)</li>
      <li><code class="language-plaintext highlighter-rouge">use Funx.Ord</code> imports only the <code class="language-plaintext highlighter-rouge">ord</code> DSL macro</li>
      <li><code class="language-plaintext highlighter-rouge">alias Funx.Ord</code> for utility functions (optional, or use fully qualified)</li>
    </ul>
  </li>
</ul>

<h3 id="migration-guide">Migration guide</h3>

<p>Eq changes:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Before</span>
<span class="n">alias</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Eq</span><span class="o">.</span><span class="no">Utils</span>
<span class="kn">use</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Eq</span><span class="o">.</span><span class="no">Dsl</span>          <span class="c1"># DSL macros</span>

<span class="no">Utils</span><span class="o">.</span><span class="n">contramap</span><span class="p">(</span><span class="o">&amp;</span><span class="p">(</span><span class="nv">&amp;1</span><span class="o">.</span><span class="n">age</span><span class="p">))</span>

<span class="k">defimpl</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Eq</span><span class="p">,</span> <span class="ss">for:</span> <span class="no">MyStruct</span> <span class="k">do</span>
  <span class="k">def</span> <span class="n">eq?</span><span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">),</span> <span class="k">do</span><span class="p">:</span> <span class="n">a</span><span class="o">.</span><span class="n">id</span> <span class="o">==</span> <span class="n">b</span><span class="o">.</span><span class="n">id</span>
<span class="k">end</span>

<span class="c1"># After</span>
<span class="kn">use</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Eq</span>              <span class="c1"># Imports eq DSL macro</span>
<span class="n">alias</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Eq</span>            <span class="c1"># For utility functions</span>

<span class="no">Eq</span><span class="o">.</span><span class="n">contramap</span><span class="p">(</span><span class="o">&amp;</span><span class="p">(</span><span class="nv">&amp;1</span><span class="o">.</span><span class="n">age</span><span class="p">))</span>

<span class="k">defimpl</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Eq</span><span class="o">.</span><span class="no">Protocol</span><span class="p">,</span> <span class="ss">for:</span> <span class="no">MyStruct</span> <span class="k">do</span>
  <span class="k">def</span> <span class="n">eq?</span><span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">),</span> <span class="k">do</span><span class="p">:</span> <span class="n">a</span><span class="o">.</span><span class="n">id</span> <span class="o">==</span> <span class="n">b</span><span class="o">.</span><span class="n">id</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Ord changes:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Before</span>
<span class="n">alias</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Ord</span><span class="o">.</span><span class="no">Utils</span>
<span class="kn">use</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Ord</span><span class="o">.</span><span class="no">Dsl</span>         <span class="c1"># DSL macros</span>

<span class="no">Utils</span><span class="o">.</span><span class="n">contramap</span><span class="p">(</span><span class="o">&amp;</span><span class="p">(</span><span class="nv">&amp;1</span><span class="o">.</span><span class="n">score</span><span class="p">))</span>

<span class="k">defimpl</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Ord</span><span class="p">,</span> <span class="ss">for:</span> <span class="no">MyStruct</span> <span class="k">do</span>
  <span class="k">def</span> <span class="n">lt?</span><span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">),</span> <span class="k">do</span><span class="p">:</span> <span class="n">a</span><span class="o">.</span><span class="n">score</span> <span class="o">&lt;</span> <span class="n">b</span><span class="o">.</span><span class="n">score</span>
<span class="k">end</span>

<span class="c1"># After</span>
<span class="kn">use</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Ord</span>             <span class="c1"># Imports ord DSL macro</span>
<span class="n">alias</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Ord</span>           <span class="c1"># For utility functions</span>

<span class="no">Ord</span><span class="o">.</span><span class="n">contramap</span><span class="p">(</span><span class="o">&amp;</span><span class="p">(</span><span class="nv">&amp;1</span><span class="o">.</span><span class="n">score</span><span class="p">))</span>

<span class="k">defimpl</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Ord</span><span class="o">.</span><span class="no">Protocol</span><span class="p">,</span> <span class="ss">for:</span> <span class="no">MyStruct</span> <span class="k">do</span>
  <span class="k">def</span> <span class="n">lt?</span><span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">),</span> <span class="k">do</span><span class="p">:</span> <span class="n">a</span><span class="o">.</span><span class="n">score</span> <span class="o">&lt;</span> <span class="n">b</span><span class="o">.</span><span class="n">score</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Default parameter changes:</p>

<ul>
  <li>Functions with <code class="language-plaintext highlighter-rouge">ord \\ Ord</code> now use <code class="language-plaintext highlighter-rouge">ord \\ Funx.Ord.Protocol</code></li>
  <li>DSL parser defaults to <code class="language-plaintext highlighter-rouge">Funx.Ord.Protocol</code> for comparison checks</li>
</ul>

<h3 id="rationale">Rationale</h3>

<p>This reorganization provides:</p>

<ul>
  <li>Clear separation: Protocols (<code class="language-plaintext highlighter-rouge">*.Protocol</code>) vs utilities (<code class="language-plaintext highlighter-rouge">Funx.Eq</code>, <code class="language-plaintext highlighter-rouge">Funx.Ord</code>)</li>
  <li>Minimal imports: <code class="language-plaintext highlighter-rouge">use</code> imports only the DSL macro, not all functions</li>
  <li>Better discoverability: Main modules contain the utilities users interact with</li>
  <li>User control: Users decide whether to alias or use fully qualified names</li>
</ul>]]></content><author><name>Joseph Koski</name></author><category term="elixir" /><category term="funx" /><summary type="html"><![CDATA[When designing Funx I leveraged protocols, which meant some concessions.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.joekoski.com/assets/images/funx-social.jpg" /><media:content medium="image" url="https://www.joekoski.com/assets/images/funx-social.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Funx: Building Lexicographic Sorts with Optics</title><link href="https://www.joekoski.com/blog/2025/12/29/ord-dsl.html" rel="alternate" type="text/html" title="Funx: Building Lexicographic Sorts with Optics" /><published>2025-12-29T14:16:06+00:00</published><updated>2025-12-29T14:16:06+00:00</updated><id>https://www.joekoski.com/blog/2025/12/29/ord-dsl</id><content type="html" xml:base="https://www.joekoski.com/blog/2025/12/29/ord-dsl.html"><![CDATA[<blockquote>
  <p>“You’re not thinking fourth dimensionally!” — Doc Brown, <em>Back to the Future Part II</em> (1989)</p>
</blockquote>

<p>We’ve seen how <code class="language-plaintext highlighter-rouge">Lens</code> and <code class="language-plaintext highlighter-rouge">Prism</code> let us extract values from nested data. Now we’ll use those same projections to build layered <a href="https://en.wikipedia.org/wiki/Lexicographic_order">lexicographic sorts</a>, one dimension at a time.</p>

<p><a href="https://livebook.dev/run?url=https%3A%2F%2Fwww.joekoski.com%2Fassets%2Flivebooks%2Fblogs%2Ffunx-ord-dsl.livemd"><img src="https://livebook.dev/badge/v1/black.svg" alt="Run in Livebook" /></a></p>

<h2 id="setup">Setup</h2>

<p>Let’s return to our transaction problem:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Transaction
└─ type
   ├─ Charge
   │  └─ payment
   │     ├─ CreditCard
   │     │  └─ amount   ← cc_payment
   │     └─ Check
   │        └─ amount   ← check_payment
   │
   └─ Refund
      └─ payment
         ├─ CreditCard
         │  └─ amount   ← cc_refund
         └─ Check
            └─ amount   ← check_refund
</code></pre></div></div>

<p>But limited to the payments:</p>

<!-- livebook:{"force_markdown":true} -->

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Macros</span><span class="p">,</span> <span class="ss">only:</span> <span class="p">[</span><span class="ss">ord_for:</span> <span class="mi">2</span><span class="p">]</span>
<span class="n">alias</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Optics</span><span class="o">.</span><span class="p">{</span><span class="no">Lens</span><span class="p">,</span> <span class="no">Prism</span><span class="p">}</span>

<span class="k">defmodule</span> <span class="no">Check</span> <span class="k">do</span>
  <span class="n">alias</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Optics</span><span class="o">.</span><span class="no">Prism</span>
  <span class="k">defstruct</span> <span class="p">[</span><span class="ss">:name</span><span class="p">,</span> <span class="ss">:routing_number</span><span class="p">,</span> <span class="ss">:account_number</span><span class="p">,</span> <span class="ss">:amount</span><span class="p">]</span>

  <span class="k">def</span> <span class="n">amount_prism</span> <span class="k">do</span>
    <span class="no">Prism</span><span class="o">.</span><span class="n">path</span><span class="p">([{</span><span class="bp">__MODULE__</span><span class="p">,</span> <span class="ss">:amount</span><span class="p">}])</span>
  <span class="k">end</span>

  <span class="n">ord_for</span><span class="p">(</span><span class="no">Check</span><span class="p">,</span> <span class="no">Lens</span><span class="o">.</span><span class="n">key</span><span class="p">(</span><span class="ss">:name</span><span class="p">))</span>
<span class="k">end</span>

<span class="k">defmodule</span> <span class="no">CreditCard</span> <span class="k">do</span>
  <span class="n">alias</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Optics</span><span class="o">.</span><span class="no">Prism</span>
  <span class="k">defstruct</span> <span class="p">[</span><span class="ss">:name</span><span class="p">,</span> <span class="ss">:number</span><span class="p">,</span> <span class="ss">:expiry</span><span class="p">,</span> <span class="ss">:amount</span><span class="p">]</span>

  <span class="k">def</span> <span class="n">amount_prism</span> <span class="k">do</span>
    <span class="no">Prism</span><span class="o">.</span><span class="n">path</span><span class="p">([{</span><span class="bp">__MODULE__</span><span class="p">,</span> <span class="ss">:amount</span><span class="p">}])</span>
  <span class="k">end</span>

  <span class="n">ord_for</span><span class="p">(</span><span class="no">CreditCard</span><span class="p">,</span> <span class="no">Lens</span><span class="o">.</span><span class="n">key</span><span class="p">(</span><span class="ss">:name</span><span class="p">))</span>
<span class="k">end</span>
</code></pre></div></div>

<p>First, we need some data:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">alias</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Optics</span><span class="o">.</span><span class="p">{</span><span class="no">Lens</span><span class="p">,</span> <span class="no">Prism</span><span class="p">}</span>
<span class="n">alias</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Ord</span>
<span class="n">alias</span> <span class="no">Funx</span><span class="o">.</span><span class="no">List</span>

<span class="n">check_1</span> <span class="o">=</span> <span class="p">%</span><span class="no">Check</span><span class="p">{</span><span class="ss">name:</span> <span class="s2">"Frank"</span><span class="p">,</span> <span class="ss">routing_number:</span> <span class="s2">"111000025"</span><span class="p">,</span> <span class="ss">account_number:</span> <span class="s2">"0001234567"</span><span class="p">,</span> <span class="ss">amount:</span> <span class="mi">100</span><span class="p">}</span>
<span class="n">check_2</span> <span class="o">=</span> <span class="p">%</span><span class="no">Check</span><span class="p">{</span><span class="ss">name:</span> <span class="s2">"Edith"</span><span class="p">,</span>   <span class="ss">routing_number:</span> <span class="s2">"121042882"</span><span class="p">,</span> <span class="ss">account_number:</span> <span class="s2">"0009876543"</span><span class="p">,</span> <span class="ss">amount:</span> <span class="mi">400</span><span class="p">}</span>
<span class="n">check_3</span> <span class="o">=</span> <span class="p">%</span><span class="no">Check</span><span class="p">{</span><span class="ss">name:</span> <span class="s2">"Charles"</span><span class="p">,</span> <span class="ss">routing_number:</span> <span class="s2">"026009593"</span><span class="p">,</span> <span class="ss">account_number:</span> <span class="s2">"0005551122"</span><span class="p">,</span> <span class="ss">amount:</span> <span class="mi">200</span><span class="p">}</span>

<span class="n">cc_1</span> <span class="o">=</span> <span class="p">%</span><span class="no">CreditCard</span><span class="p">{</span><span class="ss">name:</span> <span class="s2">"Dave"</span><span class="p">,</span> <span class="ss">number:</span> <span class="s2">"4111"</span><span class="p">,</span> <span class="ss">expiry:</span> <span class="s2">"12/26"</span><span class="p">,</span> <span class="ss">amount:</span> <span class="mi">400</span><span class="p">}</span>
<span class="n">cc_2</span> <span class="o">=</span> <span class="p">%</span><span class="no">CreditCard</span><span class="p">{</span><span class="ss">name:</span> <span class="s2">"Alice"</span><span class="p">,</span>  <span class="ss">number:</span> <span class="s2">"4242"</span><span class="p">,</span> <span class="ss">expiry:</span> <span class="s2">"01/27"</span><span class="p">,</span> <span class="ss">amount:</span> <span class="mi">300</span><span class="p">}</span>
<span class="n">cc_3</span> <span class="o">=</span> <span class="p">%</span><span class="no">CreditCard</span><span class="p">{</span><span class="ss">name:</span> <span class="s2">"Beth"</span><span class="p">,</span>   <span class="ss">number:</span> <span class="s2">"1324"</span><span class="p">,</span> <span class="ss">expiry:</span> <span class="s2">"06/25"</span><span class="p">,</span> <span class="ss">amount:</span> <span class="mi">100</span><span class="p">}</span>

<span class="n">payment_data</span> <span class="o">=</span> <span class="p">[</span><span class="n">check_1</span><span class="p">,</span> <span class="n">check_2</span><span class="p">,</span> <span class="n">check_3</span><span class="p">,</span> <span class="n">cc_1</span><span class="p">,</span> <span class="n">cc_2</span><span class="p">,</span> <span class="n">cc_3</span><span class="p">]</span>
</code></pre></div></div>

<p>Elixir has <code class="language-plaintext highlighter-rouge">Enum.sort_by/2</code>, which takes a projection function:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">name_projection</span> <span class="o">=</span> <span class="k">fn</span> <span class="p">%{</span><span class="ss">name:</span> <span class="n">name</span><span class="p">}</span> <span class="o">-&gt;</span> <span class="n">name</span> <span class="k">end</span>
<span class="no">Enum</span><span class="o">.</span><span class="n">sort_by</span><span class="p">(</span><span class="n">payment_data</span><span class="p">,</span> <span class="n">name_projection</span><span class="p">)</span>
<span class="c1"># [</span>
<span class="c1">#   %CreditCard{name: "Alice", amount: 300, ...},</span>
<span class="c1">#   %CreditCard{name: "Beth", amount: 100, ...},</span>
<span class="c1">#   %Check{name: "Charles", amount: 200, ...},</span>
<span class="c1">#   %CreditCard{name: "Dave", amount: 400, ...},</span>
<span class="c1">#   %Check{name: "Edith", amount: 400, ...},</span>
<span class="c1">#   %Check{name: "Frank", amount: 100, ...}</span>
<span class="c1"># ]</span>
</code></pre></div></div>

<p>Here, <code class="language-plaintext highlighter-rouge">name_projection</code> gets the <code class="language-plaintext highlighter-rouge">:name</code> key for <code class="language-plaintext highlighter-rouge">sort_by/2</code>.</p>

<p>Funx takes a projection too, but rather than having a separate <code class="language-plaintext highlighter-rouge">sort_by</code> function, it wraps projections in the <code class="language-plaintext highlighter-rouge">contramap/1</code> function:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">name_ord</span> <span class="o">=</span> <span class="no">Ord</span><span class="o">.</span><span class="n">contramap</span><span class="p">(</span><span class="n">name_projection</span><span class="p">)</span>
<span class="no">List</span><span class="o">.</span><span class="n">sort</span><span class="p">(</span><span class="n">payment_data</span><span class="p">,</span> <span class="n">name_ord</span><span class="p">)</span>
<span class="c1"># [</span>
<span class="c1">#   %CreditCard{name: "Alice", amount: 300, ...},</span>
<span class="c1">#   %CreditCard{name: "Beth", amount: 100, ...},</span>
<span class="c1">#   %Check{name: "Charles", amount: 200, ...},</span>
<span class="c1">#   %CreditCard{name: "Dave", amount: 400, ...},</span>
<span class="c1">#   %Check{name: "Edith", amount: 400, ...},</span>
<span class="c1">#   %Check{name: "Frank", amount: 100, ...}</span>
<span class="c1"># ]</span>
</code></pre></div></div>

<p>The nice thing about Funx’s <code class="language-plaintext highlighter-rouge">contramap/1</code> is that it can also take an optic <code class="language-plaintext highlighter-rouge">Lens</code>:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">name_ord</span> <span class="o">=</span> <span class="no">Ord</span><span class="o">.</span><span class="n">contramap</span><span class="p">(</span><span class="no">Lens</span><span class="o">.</span><span class="n">key</span><span class="p">(</span><span class="ss">:name</span><span class="p">))</span>
<span class="no">List</span><span class="o">.</span><span class="n">sort</span><span class="p">(</span><span class="n">payment_data</span><span class="p">,</span> <span class="n">name_ord</span><span class="p">)</span>
<span class="c1"># [</span>
<span class="c1">#   %CreditCard{name: "Alice", amount: 300, ...},</span>
<span class="c1">#   %CreditCard{name: "Beth", amount: 100, ...},</span>
<span class="c1">#   %Check{name: "Charles", amount: 200, ...},</span>
<span class="c1">#   %CreditCard{name: "Dave", amount: 400, ...},</span>
<span class="c1">#   %Check{name: "Edith", amount: 400, ...},</span>
<span class="c1">#   %Check{name: "Frank", amount: 100, ...}</span>
<span class="c1"># ]</span>
</code></pre></div></div>

<p>If we need more complex ord logic we can always return to a projection function, but it’s nice to have the composability and predictable lawfulness of the <code class="language-plaintext highlighter-rouge">Lens</code>.</p>

<p>It’s easy to swap the lens to <code class="language-plaintext highlighter-rouge">:amount</code>:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">amount_ord</span> <span class="o">=</span> <span class="no">Ord</span><span class="o">.</span><span class="n">contramap</span><span class="p">(</span><span class="no">Lens</span><span class="o">.</span><span class="n">key</span><span class="p">(</span><span class="ss">:amount</span><span class="p">))</span>
<span class="no">List</span><span class="o">.</span><span class="n">sort</span><span class="p">(</span><span class="n">payment_data</span><span class="p">,</span> <span class="n">amount_ord</span><span class="p">)</span>
<span class="c1"># [</span>
<span class="c1">#   %Check{name: "Frank", amount: 100, ...},</span>
<span class="c1">#   %CreditCard{name: "Beth", amount: 100, ...},</span>
<span class="c1">#   %Check{name: "Charles", amount: 200, ...},</span>
<span class="c1">#   %CreditCard{name: "Alice", amount: 300, ...},</span>
<span class="c1">#   %Check{name: "Edith", amount: 400, ...},</span>
<span class="c1">#   %CreditCard{name: "Dave", amount: 400, ...}</span>
<span class="c1"># ]</span>
</code></pre></div></div>

<p>But what if we want to sort by <code class="language-plaintext highlighter-rouge">:amount</code> and then <code class="language-plaintext highlighter-rouge">:name</code>?</p>

<p>It can be challenging to compose lexicographic order logic by hand, which is why we often see these types of problems solved with multiple loops. But we can get there using composition, achieving the same result in a single sort.</p>

<p>Funx includes <code class="language-plaintext highlighter-rouge">concat/1</code>, where we can compose a list of <code class="language-plaintext highlighter-rouge">Ord</code>:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">amount_name_ord</span> <span class="o">=</span> <span class="no">Ord</span><span class="o">.</span><span class="n">concat</span><span class="p">([</span><span class="n">amount_ord</span><span class="p">,</span> <span class="n">name_ord</span><span class="p">])</span>
<span class="no">List</span><span class="o">.</span><span class="n">sort</span><span class="p">(</span><span class="n">payment_data</span><span class="p">,</span> <span class="n">amount_name_ord</span><span class="p">)</span>
<span class="c1"># [</span>
<span class="c1">#   %CreditCard{name: "Beth", amount: 100, ...},</span>
<span class="c1">#   %Check{name: "Frank", amount: 100, ...},</span>
<span class="c1">#   %Check{name: "Charles", amount: 200, ...},</span>
<span class="c1">#   %CreditCard{name: "Alice", amount: 300, ...},</span>
<span class="c1">#   %CreditCard{name: "Dave", amount: 400, ...},</span>
<span class="c1">#   %Check{name: "Edith", amount: 400, ...}</span>
<span class="c1"># ]</span>
</code></pre></div></div>

<p>Next, let’s try sorting by <code class="language-plaintext highlighter-rouge">:routing_number</code>:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">routing_ord</span> <span class="o">=</span> <span class="no">Ord</span><span class="o">.</span><span class="n">contramap</span><span class="p">(</span><span class="no">Lens</span><span class="o">.</span><span class="n">key</span><span class="p">(</span><span class="ss">:routing_number</span><span class="p">))</span>
<span class="no">List</span><span class="o">.</span><span class="n">sort</span><span class="p">(</span><span class="n">payment_data</span><span class="p">,</span> <span class="n">routing_ord</span><span class="p">)</span>
<span class="c1"># ** (KeyError) key :routing_number not found in: %CreditCard{...}</span>
</code></pre></div></div>

<p>When we use a <code class="language-plaintext highlighter-rouge">Lens</code>, we are stating, “All our items will have a routing number key.” Its job is to fail fast when we break that domain invariant. In this case, our payment data included a credit card, which does not have a valid routing number focus.</p>

<p>Instead, we need a <code class="language-plaintext highlighter-rouge">Prism</code>:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">routing_ord</span> <span class="o">=</span> <span class="no">Ord</span><span class="o">.</span><span class="n">contramap</span><span class="p">(</span><span class="no">Prism</span><span class="o">.</span><span class="n">key</span><span class="p">(</span><span class="ss">:routing_number</span><span class="p">))</span>
<span class="no">List</span><span class="o">.</span><span class="n">sort</span><span class="p">(</span><span class="n">payment_data</span><span class="p">,</span> <span class="n">routing_ord</span><span class="p">)</span>
<span class="c1"># [</span>
<span class="c1">#   %CreditCard{name: "Dave", amount: 400, ...},</span>
<span class="c1">#   %CreditCard{name: "Alice", amount: 300, ...},</span>
<span class="c1">#   %CreditCard{name: "Beth", amount: 100, ...},</span>
<span class="c1">#   %Check{name: "Charles", amount: 200, routing_number: "026009593", ...},</span>
<span class="c1">#   %Check{name: "Frank", amount: 100, routing_number: "111000025", ...},</span>
<span class="c1">#   %Check{name: "Edith", amount: 400, routing_number: "121042882", ...}</span>
<span class="c1"># ]</span>
</code></pre></div></div>

<p>With <code class="language-plaintext highlighter-rouge">Prism.key(:routing_number)</code>, checks yield <code class="language-plaintext highlighter-rouge">Just(routing_number)</code> and credit cards yield <code class="language-plaintext highlighter-rouge">Nothing</code>. <code class="language-plaintext highlighter-rouge">Nothing</code> sorts before <code class="language-plaintext highlighter-rouge">Just</code>, so credit cards come first.</p>

<p>We can add an <code class="language-plaintext highlighter-rouge">Ord</code> to sort the credit cards by name:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">route_name_ord</span> <span class="o">=</span> <span class="no">Ord</span><span class="o">.</span><span class="n">concat</span><span class="p">([</span><span class="n">routing_ord</span><span class="p">,</span> <span class="n">name_ord</span><span class="p">])</span>
<span class="no">List</span><span class="o">.</span><span class="n">sort</span><span class="p">(</span><span class="n">payment_data</span><span class="p">,</span> <span class="n">route_name_ord</span><span class="p">)</span>
<span class="c1"># [</span>
<span class="c1">#   %CreditCard{name: "Alice", amount: 300, ...},</span>
<span class="c1">#   %CreditCard{name: "Beth", amount: 100, ...},</span>
<span class="c1">#   %CreditCard{name: "Dave", amount: 400, ...},</span>
<span class="c1">#   %Check{name: "Charles", amount: 200, routing_number: "026009593", ...},</span>
<span class="c1">#   %Check{name: "Frank", amount: 100, routing_number: "111000025", ...},</span>
<span class="c1">#   %Check{name: "Edith", amount: 400, routing_number: "121042882", ...}</span>
<span class="c1"># ]</span>
</code></pre></div></div>

<p>The checks can be sorted by <code class="language-plaintext highlighter-rouge">:routing_number</code>, but in this context the credit cards are equal with <code class="language-plaintext highlighter-rouge">Nothing</code>, so the <code class="language-plaintext highlighter-rouge">:name</code> sort is applied. This is the lexicographic sort in action.</p>

<p>Let’s reverse the sort, but only for <code class="language-plaintext highlighter-rouge">routing_number</code>:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">route_name_ord</span> <span class="o">=</span> <span class="no">Ord</span><span class="o">.</span><span class="n">concat</span><span class="p">([</span>
  <span class="no">Ord</span><span class="o">.</span><span class="n">reverse</span><span class="p">(</span><span class="n">routing_ord</span><span class="p">),</span>
  <span class="n">name_ord</span>
<span class="p">])</span>
<span class="no">List</span><span class="o">.</span><span class="n">sort</span><span class="p">(</span><span class="n">payment_data</span><span class="p">,</span> <span class="n">route_name_ord</span><span class="p">)</span>
<span class="c1"># [</span>
<span class="c1">#   %Check{name: "Edith", amount: 400, routing_number: "121042882", ...},</span>
<span class="c1">#   %Check{name: "Frank", amount: 100, routing_number: "111000025", ...},</span>
<span class="c1">#   %Check{name: "Charles", amount: 200, routing_number: "026009593", ...},</span>
<span class="c1">#   %CreditCard{name: "Alice", amount: 300, ...},</span>
<span class="c1">#   %CreditCard{name: "Beth", amount: 100, ...},</span>
<span class="c1">#   %CreditCard{name: "Dave", amount: 400, ...}</span>
<span class="c1"># ]</span>
</code></pre></div></div>

<p>Funx includes a <code class="language-plaintext highlighter-rouge">reverse/1</code>, which flips the order.</p>

<p>Let’s take a look at how we might implement this with <code class="language-plaintext highlighter-rouge">Enum.sort/2</code>:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Enum</span><span class="o">.</span><span class="n">sort</span><span class="p">(</span><span class="n">payment_data</span><span class="p">,</span> <span class="k">fn</span> <span class="n">a</span><span class="p">,</span> <span class="n">b</span> <span class="o">-&gt;</span>
  <span class="n">ra</span> <span class="o">=</span> <span class="no">Map</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="ss">:routing_number</span><span class="p">)</span>
  <span class="n">rb</span> <span class="o">=</span> <span class="no">Map</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">b</span><span class="p">,</span> <span class="ss">:routing_number</span><span class="p">)</span>

  <span class="k">cond</span> <span class="k">do</span>
    <span class="n">ra</span> <span class="o">!=</span> <span class="no">nil</span> <span class="ow">and</span> <span class="n">rb</span> <span class="o">==</span> <span class="no">nil</span> <span class="o">-&gt;</span>
      <span class="no">true</span>

    <span class="n">ra</span> <span class="o">==</span> <span class="no">nil</span> <span class="ow">and</span> <span class="n">rb</span> <span class="o">!=</span> <span class="no">nil</span> <span class="o">-&gt;</span>
      <span class="no">false</span>

    <span class="n">ra</span> <span class="o">!=</span> <span class="no">nil</span> <span class="ow">and</span> <span class="n">rb</span> <span class="o">!=</span> <span class="no">nil</span> <span class="ow">and</span> <span class="n">ra</span> <span class="o">&gt;</span> <span class="n">rb</span> <span class="o">-&gt;</span>
      <span class="no">true</span>

    <span class="n">ra</span> <span class="o">!=</span> <span class="no">nil</span> <span class="ow">and</span> <span class="n">rb</span> <span class="o">!=</span> <span class="no">nil</span> <span class="ow">and</span> <span class="n">ra</span> <span class="o">&lt;</span> <span class="n">rb</span> <span class="o">-&gt;</span>
      <span class="no">false</span>

    <span class="no">true</span> <span class="o">-&gt;</span>
      <span class="n">a</span><span class="o">.</span><span class="n">name</span> <span class="o">&lt;=</span> <span class="n">b</span><span class="o">.</span><span class="n">name</span>
  <span class="k">end</span>
<span class="k">end</span><span class="p">)</span>
<span class="c1"># [</span>
<span class="c1">#   %Check{name: "Edith", amount: 400, routing_number: "121042882", ...},</span>
<span class="c1">#   %Check{name: "Frank", amount: 100, routing_number: "111000025", ...},</span>
<span class="c1">#   %Check{name: "Charles", amount: 200, routing_number: "026009593", ...},</span>
<span class="c1">#   %CreditCard{name: "Alice", amount: 300, ...},</span>
<span class="c1">#   %CreditCard{name: "Beth", amount: 100, ...},</span>
<span class="c1">#   %CreditCard{name: "Dave", amount: 400, ...}</span>
<span class="c1"># ]</span>
</code></pre></div></div>

<p>There are a couple of issues here. First, it’s ad hoc; it is not easily testable or shareable. Also it is a bit more difficult to read than the Funx version. And, as <code class="language-plaintext highlighter-rouge">Ord</code> composition can be arbitrarily large and complex, these problems will grow with size.</p>

<p>Let’s make our life easier by switching to Funx’s more declarative <code class="language-plaintext highlighter-rouge">Ord</code> DSL:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">use</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Ord</span>
<span class="n">route_name_ord</span> <span class="o">=</span>
<span class="n">ord</span> <span class="k">do</span>
   <span class="n">desc</span> <span class="no">Prism</span><span class="o">.</span><span class="n">path</span><span class="p">([{</span><span class="no">Check</span><span class="p">,</span> <span class="ss">:routing_number</span><span class="p">}])</span>
   <span class="n">asc</span> <span class="no">Lens</span><span class="o">.</span><span class="n">key</span><span class="p">(</span><span class="ss">:name</span><span class="p">)</span>
<span class="k">end</span>
<span class="no">List</span><span class="o">.</span><span class="n">sort</span><span class="p">(</span><span class="n">payment_data</span><span class="p">,</span> <span class="n">route_name_ord</span><span class="p">)</span>
<span class="c1"># [</span>
<span class="c1">#   %Check{name: "Edith", amount: 400, routing_number: "121042882", ...},</span>
<span class="c1">#   %Check{name: "Frank", amount: 100, routing_number: "111000025", ...},</span>
<span class="c1">#   %Check{name: "Charles", amount: 200, routing_number: "026009593", ...},</span>
<span class="c1">#   %CreditCard{name: "Alice", amount: 300, ...},</span>
<span class="c1">#   %CreditCard{name: "Beth", amount: 100, ...},</span>
<span class="c1">#   %CreditCard{name: "Dave", amount: 400, ...}</span>
<span class="c1"># ]</span>
</code></pre></div></div>

<p>This solves the same sort problem. Behind the scenes it is just applying our <code class="language-plaintext highlighter-rouge">contramap/1</code>, <code class="language-plaintext highlighter-rouge">concat/1</code> and <code class="language-plaintext highlighter-rouge">reverse/1</code>.</p>

<p>For simple cases we can shorten to a single atom, <code class="language-plaintext highlighter-rouge">:name</code>:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">route_name_ord</span> <span class="o">=</span>
<span class="n">ord</span> <span class="k">do</span>
   <span class="n">desc</span> <span class="ss">:routing_number</span>
   <span class="n">asc</span> <span class="ss">:name</span>
<span class="k">end</span>
<span class="no">List</span><span class="o">.</span><span class="n">sort</span><span class="p">(</span><span class="n">payment_data</span><span class="p">,</span> <span class="n">route_name_ord</span><span class="p">)</span>
<span class="c1"># [</span>
<span class="c1">#   %Check{name: "Edith", amount: 400, routing_number: "121042882", ...},</span>
<span class="c1">#   %Check{name: "Frank", amount: 100, routing_number: "111000025", ...},</span>
<span class="c1">#   %Check{name: "Charles", amount: 200, routing_number: "026009593", ...},</span>
<span class="c1">#   %CreditCard{name: "Alice", amount: 300, ...},</span>
<span class="c1">#   %CreditCard{name: "Beth", amount: 100, ...},</span>
<span class="c1">#   %CreditCard{name: "Dave", amount: 400, ...}</span>
<span class="c1"># ]</span>
</code></pre></div></div>

<p>Often we want to use the default <code class="language-plaintext highlighter-rouge">Ord</code> for the type as a tie-breaker. In Funx, that just means adding <code class="language-plaintext highlighter-rouge">Ord.Protocol</code> to the end of the lexicographic sort:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">route_name_ord</span> <span class="o">=</span>
<span class="n">ord</span> <span class="k">do</span>
  <span class="n">desc</span> <span class="ss">:routing_number</span>
  <span class="n">asc</span> <span class="no">Ord</span><span class="o">.</span><span class="no">Protocol</span>
<span class="k">end</span>
<span class="no">List</span><span class="o">.</span><span class="n">sort</span><span class="p">(</span><span class="n">payment_data</span><span class="p">,</span> <span class="n">route_name_ord</span><span class="p">)</span>
<span class="c1"># [</span>
<span class="c1">#   %Check{name: "Edith", amount: 400, routing_number: "121042882", ...},</span>
<span class="c1">#   %Check{name: "Frank", amount: 100, routing_number: "111000025", ...},</span>
<span class="c1">#   %Check{name: "Charles", amount: 200, routing_number: "026009593", ...},</span>
<span class="c1">#   %CreditCard{name: "Alice", amount: 300, ...},</span>
<span class="c1">#   %CreditCard{name: "Beth", amount: 100, ...},</span>
<span class="c1">#   %CreditCard{name: "Dave", amount: 400, ...}</span>
<span class="c1"># ]</span>
</code></pre></div></div>

<p>For the default <code class="language-plaintext highlighter-rouge">Ord</code> is by <code class="language-plaintext highlighter-rouge">:name</code>, so this orders by <code class="language-plaintext highlighter-rouge">routing_number</code> first, and then falls back to name.</p>

<p>Note that the <code class="language-plaintext highlighter-rouge">:atom</code> context is applying the <code class="language-plaintext highlighter-rouge">Prism</code>.</p>

<p>If the <code class="language-plaintext highlighter-rouge">:routing_number</code> is a domain invariant, we can explicitly choose a <code class="language-plaintext highlighter-rouge">Lens</code>:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">route_name_ord</span> <span class="o">=</span>
<span class="n">ord</span> <span class="k">do</span>
   <span class="n">desc</span> <span class="no">Lens</span><span class="o">.</span><span class="n">key</span><span class="p">(</span><span class="ss">:routing_number</span><span class="p">)</span>
<span class="k">end</span>
<span class="no">List</span><span class="o">.</span><span class="n">sort</span><span class="p">(</span><span class="n">payment_data</span><span class="p">,</span> <span class="n">route_name_ord</span><span class="p">)</span>
<span class="c1"># ** (KeyError) key :routing_number not found in: %CreditCard{...}</span>
</code></pre></div></div>

<p>Which raises a fast failure.</p>

<p>So currently our sort has checks first. But what if we need credit cards first?</p>

<p>We can use <code class="language-plaintext highlighter-rouge">or_else</code> to replace <code class="language-plaintext highlighter-rouge">Nothing</code> with a value that sorts higher than the routing numbers.</p>

<p>In this case, a simple “a” will do the trick:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">route_name_ord</span> <span class="o">=</span>
<span class="n">ord</span> <span class="k">do</span>
   <span class="n">desc</span> <span class="ss">:routing_number</span><span class="p">,</span> <span class="ss">or_else:</span> <span class="s2">"a"</span>
<span class="k">end</span>
<span class="no">List</span><span class="o">.</span><span class="n">sort</span><span class="p">(</span><span class="n">payment_data</span><span class="p">,</span> <span class="n">route_name_ord</span><span class="p">)</span>
<span class="c1"># [</span>
<span class="c1">#   %CreditCard{name: "Alice", amount: 300, ...},</span>
<span class="c1">#   %CreditCard{name: "Beth", amount: 100, ...},</span>
<span class="c1">#   %CreditCard{name: "Dave", amount: 400, ...},</span>
<span class="c1">#   %Check{name: "Edith", amount: 400, routing_number: "121042882", ...},</span>
<span class="c1">#   %Check{name: "Frank", amount: 100, routing_number: "111000025", ...},</span>
<span class="c1">#   %Check{name: "Charles", amount: 200, routing_number: "026009593", ...}</span>
<span class="c1"># ]</span>
</code></pre></div></div>

<p>Here, because this is a descending sort and “a” is larger than any of our routing numbers, the credit cards are sorted before checks. And because the credit cards are all equal with an “a”, they fall back to the <code class="language-plaintext highlighter-rouge">:name</code> order.</p>

<p>But there is a better way to solve our problem:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">route_name_ord</span> <span class="o">=</span>
<span class="n">ord</span> <span class="k">do</span>
   <span class="n">desc</span> <span class="no">CreditCard</span>
   <span class="n">desc</span> <span class="no">Prism</span><span class="o">.</span><span class="n">path</span><span class="p">([{</span><span class="no">Check</span><span class="p">,</span> <span class="ss">:routing_number</span><span class="p">}])</span>
<span class="k">end</span>
<span class="no">List</span><span class="o">.</span><span class="n">sort</span><span class="p">(</span><span class="n">payment_data</span><span class="p">,</span> <span class="n">route_name_ord</span><span class="p">)</span>
<span class="c1"># [</span>
<span class="c1">#   %CreditCard{name: "Alice", amount: 300, ...},</span>
<span class="c1">#   %CreditCard{name: "Beth", amount: 100, ...},</span>
<span class="c1">#   %CreditCard{name: "Dave", amount: 400, ...},</span>
<span class="c1">#   %Check{name: "Edith", amount: 400, routing_number: "121042882", ...},</span>
<span class="c1">#   %Check{name: "Frank", amount: 100, routing_number: "111000025", ...},</span>
<span class="c1">#   %Check{name: "Charles", amount: 200, routing_number: "026009593", ...}</span>
<span class="c1"># ]</span>
</code></pre></div></div>

<p>Here, we have the same result but without the brittle “a” domain logic.</p>

<p>We can also use prisms from the modules, such as <code class="language-plaintext highlighter-rouge">CreditCard.amount_prism/0</code>:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">cc_amount_ord</span> <span class="o">=</span>
<span class="n">ord</span> <span class="k">do</span>
   <span class="n">asc</span> <span class="no">CreditCard</span><span class="o">.</span><span class="n">amount_prism</span>
<span class="k">end</span>
</code></pre></div></div>

<p>With <code class="language-plaintext highlighter-rouge">max!/2</code> we get the highest value:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">List</span><span class="o">.</span><span class="n">max!</span><span class="p">(</span><span class="n">payment_data</span><span class="p">,</span> <span class="n">cc_amount_ord</span><span class="p">)</span>
<span class="c1"># %CreditCard{name: "Dave", amount: 400, ...}</span>
</code></pre></div></div>

<p>And <code class="language-plaintext highlighter-rouge">min!/2</code>:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">List</span><span class="o">.</span><span class="n">min!</span><span class="p">(</span><span class="n">payment_data</span><span class="p">,</span> <span class="n">cc_amount_ord</span><span class="p">)</span>
<span class="c1"># %Check{name: "Charles", amount: 200, ...}</span>
</code></pre></div></div>

<p>Wait, that’s a <code class="language-plaintext highlighter-rouge">Check</code>!</p>

<p>Remember, <code class="language-plaintext highlighter-rouge">cc_amount_ord</code> isn’t a filter, so a check is still in the list.</p>

<p>We need this <code class="language-plaintext highlighter-rouge">Ord</code>:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">cc_amount_min_ord</span> <span class="o">=</span>
<span class="n">ord</span> <span class="k">do</span>
   <span class="n">desc</span> <span class="no">CreditCard</span>
   <span class="n">asc</span> <span class="no">CreditCard</span><span class="o">.</span><span class="n">amount_prism</span>
<span class="k">end</span>

<span class="no">List</span><span class="o">.</span><span class="n">min!</span><span class="p">(</span><span class="n">payment_data</span><span class="p">,</span> <span class="n">cc_amount_min_ord</span><span class="p">)</span>
<span class="c1"># %CreditCard{name: "Beth", amount: 100, ...}</span>
</code></pre></div></div>

<h2 id="nested-data">Nested Data</h2>

<p>So far we’ve worked with flat payment data. Let’s see how this scales to our full nested transaction structure.</p>

<p>For instance, we can set a <code class="language-plaintext highlighter-rouge">Lens</code> for our <code class="language-plaintext highlighter-rouge">Transaction</code> that defaults to sort by payment <code class="language-plaintext highlighter-rouge">:name</code>:</p>

<!-- livebook:{"force_markdown":true} -->

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">defmodule</span> <span class="no">Transaction</span> <span class="k">do</span>
  <span class="k">defstruct</span> <span class="p">[</span><span class="ss">:type</span><span class="p">]</span>

  <span class="k">def</span> <span class="n">type_prism</span> <span class="k">do</span>
    <span class="no">Prism</span><span class="o">.</span><span class="n">path</span><span class="p">([{</span><span class="bp">__MODULE__</span><span class="p">,</span> <span class="ss">:type</span><span class="p">}])</span>
  <span class="k">end</span>

  <span class="n">ord_for</span><span class="p">(</span>
    <span class="no">Transaction</span><span class="p">,</span>
    <span class="no">Lens</span><span class="o">.</span><span class="n">path</span><span class="p">([</span><span class="ss">:type</span><span class="p">,</span> <span class="ss">:payment</span><span class="p">,</span> <span class="ss">:name</span><span class="p">])</span>
  <span class="p">)</span>
<span class="k">end</span>

<span class="k">defmodule</span> <span class="no">Charge</span> <span class="k">do</span>
  <span class="n">alias</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Optics</span><span class="o">.</span><span class="no">Prism</span>
  <span class="k">defstruct</span> <span class="p">[</span><span class="ss">:payment</span><span class="p">]</span>

  <span class="k">def</span> <span class="n">payment_prism</span> <span class="k">do</span>
    <span class="no">Prism</span><span class="o">.</span><span class="n">path</span><span class="p">([{</span><span class="bp">__MODULE__</span><span class="p">,</span> <span class="ss">:payment</span><span class="p">}])</span>
  <span class="k">end</span>

  <span class="n">ord_for</span><span class="p">(</span>
    <span class="no">Charge</span><span class="p">,</span>
    <span class="no">Lens</span><span class="o">.</span><span class="n">path</span><span class="p">([</span><span class="ss">:payment</span><span class="p">,</span> <span class="ss">:name</span><span class="p">])</span>
  <span class="p">)</span>
<span class="k">end</span>

<span class="k">defmodule</span> <span class="no">Refund</span> <span class="k">do</span>
  <span class="n">alias</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Optics</span><span class="o">.</span><span class="no">Prism</span>
  <span class="k">defstruct</span> <span class="p">[</span><span class="ss">:payment</span><span class="p">]</span>

  <span class="k">def</span> <span class="n">payment_prism</span> <span class="k">do</span>
    <span class="no">Prism</span><span class="o">.</span><span class="n">path</span><span class="p">([{</span><span class="bp">__MODULE__</span><span class="p">,</span> <span class="ss">:payment</span><span class="p">}])</span>
  <span class="k">end</span>

  <span class="n">ord_for</span><span class="p">(</span>
    <span class="no">Refund</span><span class="p">,</span>
    <span class="no">Lens</span><span class="o">.</span><span class="n">path</span><span class="p">([</span><span class="ss">:payment</span><span class="p">,</span> <span class="ss">:name</span><span class="p">])</span>
  <span class="p">)</span>
<span class="k">end</span>
</code></pre></div></div>

<p>And here is some transaction data:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">refund_cc_1</span> <span class="o">=</span>
<span class="p">%</span><span class="no">Transaction</span><span class="p">{</span>
  <span class="ss">type:</span> <span class="p">%</span><span class="no">Refund</span><span class="p">{</span>
    <span class="ss">payment:</span> <span class="p">%</span><span class="no">CreditCard</span><span class="p">{</span><span class="ss">name:</span> <span class="s2">"Frank"</span><span class="p">,</span> <span class="ss">number:</span> <span class="s2">"4333"</span><span class="p">,</span> <span class="ss">expiry:</span> <span class="s2">"10/27"</span><span class="p">,</span> <span class="ss">amount:</span> <span class="mi">1200</span><span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="n">refund_cc_2</span> <span class="o">=</span>
<span class="p">%</span><span class="no">Transaction</span><span class="p">{</span>
  <span class="ss">type:</span> <span class="p">%</span><span class="no">Refund</span><span class="p">{</span>
    <span class="ss">payment:</span> <span class="p">%</span><span class="no">CreditCard</span><span class="p">{</span><span class="ss">name:</span> <span class="s2">"Eve"</span><span class="p">,</span> <span class="ss">number:</span> <span class="s2">"4555"</span><span class="p">,</span> <span class="ss">expiry:</span> <span class="s2">"08/28"</span><span class="p">,</span> <span class="ss">amount:</span> <span class="mi">100</span><span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="n">refund_check_1</span> <span class="o">=</span>
  <span class="p">%</span><span class="no">Transaction</span><span class="p">{</span>
    <span class="ss">type:</span> <span class="p">%</span><span class="no">Refund</span><span class="p">{</span>
      <span class="ss">payment:</span> <span class="p">%</span><span class="no">Check</span><span class="p">{</span><span class="ss">name:</span> <span class="s2">"Dave"</span><span class="p">,</span> <span class="ss">routing_number:</span> <span class="s2">"123000025"</span><span class="p">,</span> <span class="ss">account_number:</span> <span class="s2">"0001234567"</span><span class="p">,</span> <span class="ss">amount:</span> <span class="mi">1200</span><span class="p">}</span>
    <span class="p">}</span>
  <span class="p">}</span>

<span class="n">charge_check_1</span> <span class="o">=</span>
  <span class="p">%</span><span class="no">Transaction</span><span class="p">{</span>
    <span class="ss">type:</span> <span class="p">%</span><span class="no">Charge</span><span class="p">{</span>
      <span class="ss">payment:</span> <span class="p">%</span><span class="no">Check</span><span class="p">{</span><span class="ss">name:</span> <span class="s2">"Charles"</span><span class="p">,</span> <span class="ss">routing_number:</span> <span class="s2">"111000025"</span><span class="p">,</span> <span class="ss">account_number:</span> <span class="s2">"0001234567"</span><span class="p">,</span> <span class="ss">amount:</span> <span class="mi">1000</span><span class="p">}</span>
    <span class="p">}</span>
  <span class="p">}</span>

<span class="n">charge_cc_1</span> <span class="o">=</span>
  <span class="p">%</span><span class="no">Transaction</span><span class="p">{</span>
    <span class="ss">type:</span> <span class="p">%</span><span class="no">Charge</span><span class="p">{</span>
      <span class="ss">payment:</span> <span class="p">%</span><span class="no">CreditCard</span><span class="p">{</span><span class="ss">name:</span> <span class="s2">"Bob"</span><span class="p">,</span> <span class="ss">number:</span> <span class="s2">"4444"</span><span class="p">,</span> <span class="ss">expiry:</span> <span class="s2">"09/26"</span><span class="p">,</span> <span class="ss">amount:</span> <span class="mi">400</span><span class="p">}</span>
    <span class="p">}</span>
  <span class="p">}</span>

<span class="n">charge_cc_2</span> <span class="o">=</span>
  <span class="p">%</span><span class="no">Transaction</span><span class="p">{</span>
    <span class="ss">type:</span> <span class="p">%</span><span class="no">Charge</span><span class="p">{</span>
      <span class="ss">payment:</span> <span class="p">%</span><span class="no">CreditCard</span><span class="p">{</span><span class="ss">name:</span> <span class="s2">"Alice"</span><span class="p">,</span> <span class="ss">number:</span> <span class="s2">"4222"</span><span class="p">,</span> <span class="ss">expiry:</span> <span class="s2">"11/25"</span><span class="p">,</span> <span class="ss">amount:</span> <span class="mi">100</span><span class="p">}</span>
    <span class="p">}</span>
  <span class="p">}</span>

<span class="n">transactions</span> <span class="o">=</span> <span class="p">[</span><span class="n">refund_cc_1</span><span class="p">,</span> <span class="n">refund_cc_2</span><span class="p">,</span> <span class="n">refund_check_1</span><span class="p">,</span> <span class="n">charge_check_1</span><span class="p">,</span> <span class="n">charge_cc_1</span><span class="p">,</span> <span class="n">charge_cc_2</span><span class="p">]</span>

<span class="no">List</span><span class="o">.</span><span class="n">sort</span><span class="p">(</span><span class="n">transactions</span><span class="p">)</span>
<span class="c1"># [</span>
<span class="c1">#   %Transaction{type: %Charge{payment: %CreditCard{name: "Alice", amount: 100, ...}}},</span>
<span class="c1">#   %Transaction{type: %Charge{payment: %CreditCard{name: "Bob", amount: 400, ...}}},</span>
<span class="c1">#   %Transaction{type: %Charge{payment: %Check{name: "Charles", amount: 1000, ...}}},</span>
<span class="c1">#   %Transaction{type: %Refund{payment: %Check{name: "Dave", amount: 1200, ...}}},</span>
<span class="c1">#   %Transaction{type: %Refund{payment: %CreditCard{name: "Eve", amount: 100, ...}}},</span>
<span class="c1">#   %Transaction{type: %Refund{payment: %CreditCard{name: "Frank", amount: 1200, ...}}}</span>
<span class="c1"># ]</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">List.sort/1</code> with no ord falls back to the <code class="language-plaintext highlighter-rouge">Transaction</code> default we defined as <code class="language-plaintext highlighter-rouge">ord_for(Transaction, Lens.path([:type, :payment, :name]))</code>.</p>

<p>And we can sort by <code class="language-plaintext highlighter-rouge">:amount</code>:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">payment_amount_ord</span> <span class="o">=</span>
<span class="n">ord</span> <span class="k">do</span>
  <span class="n">asc</span> <span class="no">Lens</span><span class="o">.</span><span class="n">path</span><span class="p">([</span><span class="ss">:type</span><span class="p">,</span> <span class="ss">:payment</span><span class="p">,</span> <span class="ss">:amount</span><span class="p">])</span>
  <span class="n">asc</span> <span class="no">Ord</span><span class="o">.</span><span class="no">Protocol</span>
<span class="k">end</span>

<span class="no">Notice</span> <span class="n">we</span> <span class="n">are</span> <span class="n">adding</span> <span class="n">the</span> <span class="n">tiebreaker</span><span class="p">,</span> <span class="n">the</span> <span class="err">`</span><span class="no">Transaction</span><span class="err">`</span> <span class="n">default</span> <span class="n">payment</span> <span class="n">name</span> <span class="n">ord</span><span class="o">.</span>

<span class="no">List</span><span class="o">.</span><span class="n">sort</span><span class="p">(</span><span class="n">transactions</span><span class="p">,</span> <span class="n">payment_amount_ord</span><span class="p">)</span>
<span class="c1"># [</span>
<span class="c1">#   %Transaction{type: %Charge{payment: %CreditCard{name: "Alice", amount: 100, ...}}},</span>
<span class="c1">#   %Transaction{type: %Refund{payment: %CreditCard{name: "Eve", amount: 100, ...}}},</span>
<span class="c1">#   %Transaction{type: %Charge{payment: %CreditCard{name: "Bob", amount: 400, ...}}},</span>
<span class="c1">#   %Transaction{type: %Charge{payment: %Check{name: "Charles", amount: 1000, ...}}},</span>
<span class="c1">#   %Transaction{type: %Refund{payment: %Check{name: "Dave", amount: 1200, ...}}},</span>
<span class="c1">#   %Transaction{type: %Refund{payment: %CreditCard{name: "Frank", amount: 1200, ...}}}</span>
<span class="c1"># ]</span>
</code></pre></div></div>

<p>Notice we’re adding the tiebreaker, the<code class="language-plaintext highlighter-rouge">Transaction</code>’s default ord.</p>

<p>And we can reuse our <code class="language-plaintext highlighter-rouge">Ord</code> in <code class="language-plaintext highlighter-rouge">max!/2</code>:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">List</span><span class="o">.</span><span class="n">max!</span><span class="p">(</span><span class="n">transactions</span><span class="p">,</span> <span class="n">payment_amount_ord</span><span class="p">)</span>
<span class="c1"># %Transaction{type: %Refund{payment: %CreditCard{name: "Frank", amount: 1200, ...}}}</span>
</code></pre></div></div>

<p>Here, Frank’s credit card refund is our largest transaction.</p>

<p>And we can continue to use our composed prisms:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">defmodule</span> <span class="no">Processor</span> <span class="k">do</span>
  <span class="n">alias</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Optics</span><span class="o">.</span><span class="no">Prism</span>

  <span class="k">def</span> <span class="n">cc_payment_prism</span> <span class="k">do</span>
    <span class="no">Prism</span><span class="o">.</span><span class="n">compose</span><span class="p">([</span>
      <span class="no">Transaction</span><span class="o">.</span><span class="n">type_prism</span><span class="p">(),</span>
      <span class="no">Charge</span><span class="o">.</span><span class="n">payment_prism</span><span class="p">(),</span>
      <span class="no">CreditCard</span><span class="o">.</span><span class="n">amount_prism</span><span class="p">()</span>
    <span class="p">])</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">check_payment_prism</span> <span class="k">do</span>
    <span class="no">Prism</span><span class="o">.</span><span class="n">compose</span><span class="p">([</span>
      <span class="no">Transaction</span><span class="o">.</span><span class="n">type_prism</span><span class="p">(),</span>
      <span class="no">Charge</span><span class="o">.</span><span class="n">payment_prism</span><span class="p">(),</span>
      <span class="no">Check</span><span class="o">.</span><span class="n">amount_prism</span><span class="p">()</span>
    <span class="p">])</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">cc_refund_prism</span> <span class="k">do</span>
    <span class="no">Prism</span><span class="o">.</span><span class="n">compose</span><span class="p">([</span>
      <span class="no">Transaction</span><span class="o">.</span><span class="n">type_prism</span><span class="p">(),</span>
      <span class="no">Refund</span><span class="o">.</span><span class="n">payment_prism</span><span class="p">(),</span>
      <span class="no">CreditCard</span><span class="o">.</span><span class="n">amount_prism</span><span class="p">()</span>
    <span class="p">])</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">check_refund_prism</span> <span class="k">do</span>
    <span class="no">Prism</span><span class="o">.</span><span class="n">compose</span><span class="p">([</span>
      <span class="no">Transaction</span><span class="o">.</span><span class="n">type_prism</span><span class="p">(),</span>
      <span class="no">Refund</span><span class="o">.</span><span class="n">payment_prism</span><span class="p">(),</span>
      <span class="no">Check</span><span class="o">.</span><span class="n">amount_prism</span><span class="p">()</span>
    <span class="p">])</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>To sort by credit card payment, then credit card refund, then check payment, then check refund:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">payment_amount_ord</span> <span class="o">=</span>
<span class="n">ord</span> <span class="k">do</span>
  <span class="n">asc</span> <span class="no">Processor</span><span class="o">.</span><span class="n">cc_payment_prism</span>
  <span class="n">asc</span> <span class="no">Processor</span><span class="o">.</span><span class="n">cc_refund_prism</span>
  <span class="n">asc</span> <span class="no">Processor</span><span class="o">.</span><span class="n">check_payment_prism</span>
  <span class="n">asc</span> <span class="no">Processor</span><span class="o">.</span><span class="n">check_refund_prism</span>
  <span class="n">asc</span> <span class="no">Ord</span><span class="o">.</span><span class="no">Protocol</span>
<span class="k">end</span>

<span class="no">List</span><span class="o">.</span><span class="n">sort</span><span class="p">(</span><span class="n">transactions</span><span class="p">,</span> <span class="n">payment_amount_ord</span><span class="p">)</span>
<span class="c1"># [</span>
<span class="c1">#   %Transaction{type: %Refund{payment: %Check{name: "Dave", amount: 1200, ...}}},</span>
<span class="c1">#   %Transaction{type: %Charge{payment: %Check{name: "Charles", amount: 1000, ...}}},</span>
<span class="c1">#   %Transaction{type: %Refund{payment: %CreditCard{name: "Eve", amount: 100, ...}}},</span>
<span class="c1">#   %Transaction{type: %Refund{payment: %CreditCard{name: "Frank", amount: 1200, ...}}},</span>
<span class="c1">#   %Transaction{type: %Charge{payment: %CreditCard{name: "Alice", amount: 100, ...}}},</span>
<span class="c1">#   %Transaction{type: %Charge{payment: %CreditCard{name: "Bob", amount: 400, ...}}}</span>
<span class="c1"># ]</span>
</code></pre></div></div>

<p>Want to flip the results?</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">payment_amount_rev_ord</span> <span class="o">=</span>
<span class="n">ord</span> <span class="k">do</span>
  <span class="n">desc</span> <span class="n">payment_amount_ord</span>
<span class="k">end</span>

<span class="no">List</span><span class="o">.</span><span class="n">sort</span><span class="p">(</span><span class="n">transactions</span><span class="p">,</span> <span class="n">payment_amount_rev_ord</span><span class="p">)</span>
<span class="c1"># [</span>
<span class="c1">#   %Transaction{type: %Charge{payment: %CreditCard{name: "Bob", amount: 400, ...}}},</span>
<span class="c1">#   %Transaction{type: %Charge{payment: %CreditCard{name: "Alice", amount: 100, ...}}},</span>
<span class="c1">#   %Transaction{type: %Refund{payment: %CreditCard{name: "Frank", amount: 1200, ...}}},</span>
<span class="c1">#   %Transaction{type: %Refund{payment: %CreditCard{name: "Eve", amount: 100, ...}}},</span>
<span class="c1">#   %Transaction{type: %Charge{payment: %Check{name: "Charles", amount: 1000, ...}}},</span>
<span class="c1">#   %Transaction{type: %Refund{payment: %Check{name: "Dave", amount: 1200, ...}}}</span>
<span class="c1"># ]</span>
</code></pre></div></div>

<p>Or flip back with Funx’s <code class="language-plaintext highlighter-rouge">reverse/1</code>:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">List</span><span class="o">.</span><span class="n">sort</span><span class="p">(</span><span class="n">transactions</span><span class="p">,</span> <span class="no">Ord</span><span class="o">.</span><span class="n">reverse</span><span class="p">(</span><span class="n">payment_amount_rev_ord</span><span class="p">))</span>
<span class="c1"># [</span>
<span class="c1">#   %Transaction{type: %Refund{payment: %Check{name: "Dave", amount: 1200, ...}}},</span>
<span class="c1">#   %Transaction{type: %Charge{payment: %Check{name: "Charles", amount: 1000, ...}}},</span>
<span class="c1">#   %Transaction{type: %Refund{payment: %CreditCard{name: "Eve", amount: 100, ...}}},</span>
<span class="c1">#   %Transaction{type: %Refund{payment: %CreditCard{name: "Frank", amount: 1200, ...}}},</span>
<span class="c1">#   %Transaction{type: %Charge{payment: %CreditCard{name: "Alice", amount: 100, ...}}},</span>
<span class="c1">#   %Transaction{type: %Charge{payment: %CreditCard{name: "Bob", amount: 400, ...}}}</span>
<span class="c1"># ]</span>
</code></pre></div></div>

<p>This would be a handful to implement using <code class="language-plaintext highlighter-rouge">Enum.sort/2</code>, but fortunately we have <code class="language-plaintext highlighter-rouge">comparator/1</code>:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Enum</span><span class="o">.</span><span class="n">sort</span><span class="p">(</span><span class="n">transactions</span><span class="p">,</span> <span class="no">Ord</span><span class="o">.</span><span class="n">comparator</span><span class="p">(</span><span class="n">payment_amount_ord</span><span class="p">))</span>
<span class="c1"># [</span>
<span class="c1">#   %Transaction{type: %Refund{payment: %Check{name: "Dave", amount: 1200, ...}}},</span>
<span class="c1">#   %Transaction{type: %Charge{payment: %Check{name: "Charles", amount: 1000, ...}}},</span>
<span class="c1">#   %Transaction{type: %Refund{payment: %CreditCard{name: "Eve", amount: 100, ...}}},</span>
<span class="c1">#   %Transaction{type: %Refund{payment: %CreditCard{name: "Frank", amount: 1200, ...}}},</span>
<span class="c1">#   %Transaction{type: %Charge{payment: %CreditCard{name: "Alice", amount: 100, ...}}},</span>
<span class="c1">#   %Transaction{type: %Charge{payment: %CreditCard{name: "Bob", amount: 400, ...}}}</span>
<span class="c1"># ]</span>
</code></pre></div></div>

<h2 id="resources">Resources</h2>

<div style="display: flex; gap: 20px; margin: 20px 0;">
  <div style="flex-shrink: 0;">
    <a href="https://pragprog.com/titles/jkelixir/advanced-functional-programming-with-elixir">
      <img src="/assets/images/jkelixir_small.jpg" alt="Advanced Functional Programming with Elixir book cover" width="150" />
    </a>
  </div>
  <div>
    <h3><a href="https://pragprog.com/titles/jkelixir/advanced-functional-programming-with-elixir">Advanced Functional Programming with Elixir</a></h3>
    <p>Dive deeper into functional programming patterns and advanced Elixir techniques. Learn how to build robust, maintainable applications using functional programming principles.</p>
  </div>
</div>

<div style="display: flex; gap: 20px; margin: 20px 0;">
  <div style="flex-shrink: 0;">
    <a href="https://www.funxlib.com">
      <img src="/assets/images/funx-social.jpg" alt="Funx functional programming library" width="150" />
    </a>
  </div>
  <div>
    <h3><a href="https://www.funxlib.com">Funx - Functional Programming for Elixir</a></h3>
    <p>A library of functional programming abstractions for Elixir, including monads, monoids, Eq, Ord, and more. Built as an ecosystem where learning is the priority from the start.</p>
  </div>
</div>]]></content><author><name>Joseph Koski</name></author><category term="elixir" /><category term="funx" /><summary type="html"><![CDATA[“You’re not thinking fourth dimensionally!” — Doc Brown, Back to the Future Part II (1989)]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.joekoski.com/assets/images/jkelixir_small.jpg" /><media:content medium="image" url="https://www.joekoski.com/assets/images/jkelixir_small.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Funx: Adding the Optic Prism</title><link href="https://www.joekoski.com/blog/2025/12/21/funx-optics-prism.html" rel="alternate" type="text/html" title="Funx: Adding the Optic Prism" /><published>2025-12-21T14:16:06+00:00</published><updated>2025-12-21T14:16:06+00:00</updated><id>https://www.joekoski.com/blog/2025/12/21/funx-optics-prism</id><content type="html" xml:base="https://www.joekoski.com/blog/2025/12/21/funx-optics-prism.html"><![CDATA[<blockquote>
  <p>“The problem is choice.” — Neo, The Matrix (1999)</p>
</blockquote>

<p><a href="https://livebook.dev/run?url=https%3A%2F%2Fwww.joekoski.com%2Fassets%2Flivebooks%2Fblogs%2Ffunx-optics-prism.livemd"><img src="https://livebook.dev/badge/v1/black.svg" alt="Run in Livebook" /></a></p>

<h2 id="why-prism">Why Prism?</h2>

<p>Like a <code class="language-plaintext highlighter-rouge">Lens</code>, a <code class="language-plaintext highlighter-rouge">Prism</code> is composable. In Elixir, it can also help manage missing keys or expected nils, but that is incidental. A <code class="language-plaintext highlighter-rouge">Prism</code> models conditional existence: a focus that exists only on certain branches of a sum type.</p>

<p>Let’s say we are building a system to process transactions. A transaction can be a purchase or a refund, and it can be paid by check or credit card.</p>

<p>The tricky part is not extracting fields. It is deciding which payment operations apply.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Transaction
└─ type
   ├─ Charge
   │  └─ payment
   │     ├─ CreditCard
   │     │  └─ amount   ← cc_payment
   │     └─ Check
   │        └─ amount   ← check_payment
   │
   └─ Refund
      └─ payment
         ├─ CreditCard
         │  └─ amount   ← cc_refund
         └─ Check
            └─ amount   ← check_refund
</code></pre></div></div>

<p>The path <code class="language-plaintext highlighter-rouge">type.payment.amount</code> does not identify a single thing. It identifies four distinct meanings, depending on which branch of the tree exists.</p>

<p>So <code class="language-plaintext highlighter-rouge">amount</code> is a field in the data, but it is not a single domain meaning. In this model, it can mean one of four things, and the meaning depends on context.</p>

<p>A <code class="language-plaintext highlighter-rouge">Lens</code> is the right tool when you have a product type: a structure where the relevant fields exist together. Here, we have a sum type: one of several possible shapes. Only one branch exists at a time.</p>

<p>In the domain, this is conditional existence. A check refund is a valid transaction. It is not an error case. It simply does not belong to the operation “charge a credit card.”</p>

<p>If we treat that mismatch as “bad data,” we reach for error handling, guards, or defensive code. That treats the symptom, not the problem.</p>

<p>A <code class="language-plaintext highlighter-rouge">Prism</code> states something sharper:</p>

<p>“This value exists, but only in this context.”</p>

<p><a href="https://fsharpforfunandprofit.com/posts/designing-with-types-making-illegal-states-unrepresentable/">Learn more about making illegal states unrepresentable.</a></p>

<h2 id="building-the-transaction-processor">Building the Transaction Processor</h2>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">alias</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Optics</span><span class="o">.</span><span class="no">Prism</span>
<span class="kn">require</span> <span class="no">Logger</span>
<span class="kn">use</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Monad</span><span class="o">.</span><span class="no">Maybe</span>

<span class="k">defmodule</span> <span class="no">CreditCard</span> <span class="k">do</span>
  <span class="k">defstruct</span> <span class="p">[</span><span class="ss">:name</span><span class="p">,</span> <span class="ss">:number</span><span class="p">,</span> <span class="ss">:expiry</span><span class="p">,</span> <span class="ss">:amount</span><span class="p">]</span>
  <span class="n">alias</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Optics</span><span class="o">.</span><span class="no">Prism</span>

  <span class="k">def</span> <span class="n">amount_prism</span> <span class="k">do</span>
    <span class="no">Prism</span><span class="o">.</span><span class="n">path</span><span class="p">([{</span><span class="bp">__MODULE__</span><span class="p">,</span> <span class="ss">:amount</span><span class="p">}])</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="k">defmodule</span> <span class="no">Check</span> <span class="k">do</span>
  <span class="k">defstruct</span> <span class="p">[</span><span class="ss">:name</span><span class="p">,</span> <span class="ss">:routing_number</span><span class="p">,</span> <span class="ss">:account_number</span><span class="p">,</span> <span class="ss">:amount</span><span class="p">]</span>
  <span class="n">alias</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Optics</span><span class="o">.</span><span class="no">Prism</span>

  <span class="k">def</span> <span class="n">amount_prism</span> <span class="k">do</span>
    <span class="no">Prism</span><span class="o">.</span><span class="n">path</span><span class="p">([{</span><span class="bp">__MODULE__</span><span class="p">,</span> <span class="ss">:amount</span><span class="p">}])</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="k">defmodule</span> <span class="no">Charge</span> <span class="k">do</span>
  <span class="k">defstruct</span> <span class="p">[</span><span class="ss">:payment</span><span class="p">]</span>
  <span class="n">alias</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Optics</span><span class="o">.</span><span class="no">Prism</span>

  <span class="k">def</span> <span class="n">payment_prism</span> <span class="k">do</span>
    <span class="no">Prism</span><span class="o">.</span><span class="n">path</span><span class="p">([{</span><span class="bp">__MODULE__</span><span class="p">,</span> <span class="ss">:payment</span><span class="p">}])</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="k">defmodule</span> <span class="no">Refund</span> <span class="k">do</span>
  <span class="k">defstruct</span> <span class="p">[</span><span class="ss">:payment</span><span class="p">]</span>
  <span class="n">alias</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Optics</span><span class="o">.</span><span class="no">Prism</span>

  <span class="k">def</span> <span class="n">payment_prism</span> <span class="k">do</span>
    <span class="no">Prism</span><span class="o">.</span><span class="n">path</span><span class="p">([{</span><span class="bp">__MODULE__</span><span class="p">,</span> <span class="ss">:payment</span><span class="p">}])</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="k">defmodule</span> <span class="no">Transaction</span> <span class="k">do</span>
  <span class="k">defstruct</span> <span class="p">[</span><span class="ss">:type</span><span class="p">]</span>
  <span class="n">alias</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Optics</span><span class="o">.</span><span class="no">Prism</span>

  <span class="k">def</span> <span class="n">type_prism</span> <span class="k">do</span>
    <span class="no">Prism</span><span class="o">.</span><span class="n">path</span><span class="p">([{</span><span class="bp">__MODULE__</span><span class="p">,</span> <span class="ss">:type</span><span class="p">}])</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>And we need some data to work with:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">cc_payment</span> <span class="o">=</span> <span class="p">%</span><span class="no">CreditCard</span><span class="p">{</span><span class="ss">name:</span> <span class="s2">"John"</span><span class="p">,</span> <span class="ss">number:</span> <span class="s2">"1234"</span><span class="p">,</span> <span class="ss">expiry:</span> <span class="s2">"12/26"</span><span class="p">,</span> <span class="ss">amount:</span> <span class="mi">75</span><span class="p">}</span>
<span class="n">check_payment</span> <span class="o">=</span> <span class="p">%</span><span class="no">Check</span><span class="p">{</span><span class="ss">name:</span> <span class="s2">"Dave"</span><span class="p">,</span> <span class="ss">routing_number:</span> <span class="s2">"111000025"</span><span class="p">,</span> <span class="ss">account_number:</span> <span class="s2">"987654"</span><span class="p">,</span> <span class="ss">amount:</span> <span class="mi">125</span><span class="p">}</span>

<span class="n">charge_cc</span> <span class="o">=</span> <span class="p">%</span><span class="no">Transaction</span><span class="p">{</span><span class="ss">type:</span> <span class="p">%</span><span class="no">Charge</span><span class="p">{</span><span class="ss">payment:</span> <span class="n">cc_payment</span><span class="p">}}</span>
<span class="n">charge_check</span> <span class="o">=</span> <span class="p">%</span><span class="no">Transaction</span><span class="p">{</span><span class="ss">type:</span> <span class="p">%</span><span class="no">Charge</span><span class="p">{</span><span class="ss">payment:</span> <span class="n">check_payment</span><span class="p">}}</span>
<span class="n">refund_cc</span> <span class="o">=</span> <span class="p">%</span><span class="no">Transaction</span><span class="p">{</span><span class="ss">type:</span> <span class="p">%</span><span class="no">Refund</span><span class="p">{</span><span class="ss">payment:</span> <span class="n">cc_payment</span><span class="p">}}</span>
<span class="n">refund_check</span> <span class="o">=</span> <span class="p">%</span><span class="no">Transaction</span><span class="p">{</span><span class="ss">type:</span> <span class="p">%</span><span class="no">Refund</span><span class="p">{</span><span class="ss">payment:</span> <span class="n">check_payment</span><span class="p">}}</span>
</code></pre></div></div>

<h2 id="trust-the-caller">Trust the Caller</h2>

<p>A common pattern in dynamic languages is to write the happy path and rely on callers to pass the right thing.</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">defmodule</span> <span class="no">Trust</span><span class="o">.</span><span class="no">TransactionProcessor</span> <span class="k">do</span>
  <span class="k">def</span> <span class="n">cc_payment</span><span class="p">(</span><span class="n">transaction</span><span class="p">)</span> <span class="k">do</span>
    <span class="no">Logger</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="s2">"Charge cc $</span><span class="si">#{</span><span class="n">transaction</span><span class="o">.</span><span class="n">type</span><span class="o">.</span><span class="n">payment</span><span class="o">.</span><span class="n">amount</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
    <span class="n">transaction</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">check_payment</span><span class="p">(</span><span class="n">transaction</span><span class="p">)</span> <span class="k">do</span>
    <span class="no">Logger</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="s2">"Charge check $</span><span class="si">#{</span><span class="n">transaction</span><span class="o">.</span><span class="n">type</span><span class="o">.</span><span class="n">payment</span><span class="o">.</span><span class="n">amount</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
    <span class="n">transaction</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">cc_refund</span><span class="p">(</span><span class="n">transaction</span><span class="p">)</span> <span class="k">do</span>
    <span class="no">Logger</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="s2">"Refund cc $</span><span class="si">#{</span><span class="n">transaction</span><span class="o">.</span><span class="n">type</span><span class="o">.</span><span class="n">payment</span><span class="o">.</span><span class="n">amount</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
    <span class="n">transaction</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">check_refund</span><span class="p">(</span><span class="n">transaction</span><span class="p">)</span> <span class="k">do</span>
    <span class="no">Logger</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="s2">"Refund check $</span><span class="si">#{</span><span class="n">transaction</span><span class="o">.</span><span class="n">type</span><span class="o">.</span><span class="n">payment</span><span class="o">.</span><span class="n">amount</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
    <span class="n">transaction</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>The caller can do the right thing:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Trust</span><span class="o">.</span><span class="no">TransactionProcessor</span><span class="o">.</span><span class="n">cc_payment</span><span class="p">(</span><span class="n">charge_cc</span><span class="p">)</span>

<span class="c1"># [info] Charge cc $75</span>
<span class="c1">#</span>
<span class="c1"># %Transaction{</span>
<span class="c1">#   type: %Charge{payment: %CreditCard{name: "John", number: "1234", expiry: "12/26", amount: 75}}</span>
<span class="c1"># }</span>
</code></pre></div></div>

<p>And the caller can do the wrong thing:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Trust</span><span class="o">.</span><span class="no">TransactionProcessor</span><span class="o">.</span><span class="n">cc_payment</span><span class="p">(</span><span class="n">refund_cc</span><span class="p">)</span>

<span class="c1"># [info] Charge cc $75</span>

<span class="c1"># %Transaction{</span>
<span class="c1">#   type: %Refund{payment: %CreditCard{name: "John", number: "1234", expiry: "12/26", amount: 75}}</span>
<span class="c1"># }</span>
</code></pre></div></div>

<p>Because the shape matches, nothing crashes. We silently applied the wrong operation to a valid transaction.</p>

<p>The bug is not “nil access.” The bug is “we ran a valid operation against the wrong meaning.”</p>

<h2 id="pattern-matching-in-function-heads">Pattern Matching in Function Heads</h2>

<p>The idiomatic Elixir fix is pattern matching in function heads:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">defmodule</span> <span class="no">Guarded</span><span class="o">.</span><span class="no">TransactionProcessor</span> <span class="k">do</span>
  <span class="k">def</span> <span class="n">cc_payment</span><span class="p">(</span>
        <span class="p">%</span><span class="no">Transaction</span><span class="p">{</span>
          <span class="ss">type:</span> <span class="p">%</span><span class="no">Charge</span><span class="p">{</span>
            <span class="ss">payment:</span> <span class="p">%</span><span class="no">CreditCard</span><span class="p">{</span><span class="ss">amount:</span> <span class="n">amount</span><span class="p">}</span>
          <span class="p">}</span>
        <span class="p">}</span> <span class="o">=</span> <span class="n">transaction</span>
      <span class="p">)</span> <span class="k">do</span>
    <span class="no">Logger</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="s2">"Charge cc $</span><span class="si">#{</span><span class="n">amount</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
    <span class="n">transaction</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">check_payment</span><span class="p">(</span>
        <span class="p">%</span><span class="no">Transaction</span><span class="p">{</span>
          <span class="ss">type:</span> <span class="p">%</span><span class="no">Charge</span><span class="p">{</span>
            <span class="ss">payment:</span> <span class="p">%</span><span class="no">Check</span><span class="p">{</span><span class="ss">amount:</span> <span class="n">amount</span><span class="p">}</span>
          <span class="p">}</span>
        <span class="p">}</span> <span class="o">=</span> <span class="n">transaction</span>
      <span class="p">)</span> <span class="k">do</span>
    <span class="no">Logger</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="s2">"Charge check $</span><span class="si">#{</span><span class="n">amount</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
    <span class="n">transaction</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">cc_refund</span><span class="p">(</span>
        <span class="p">%</span><span class="no">Transaction</span><span class="p">{</span>
          <span class="ss">type:</span> <span class="p">%</span><span class="no">Refund</span><span class="p">{</span>
            <span class="ss">payment:</span> <span class="p">%</span><span class="no">CreditCard</span><span class="p">{</span><span class="ss">amount:</span> <span class="n">amount</span><span class="p">}</span>
          <span class="p">}</span>
        <span class="p">}</span> <span class="o">=</span> <span class="n">transaction</span>
      <span class="p">)</span> <span class="k">do</span>
    <span class="no">Logger</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="s2">"Refund cc $</span><span class="si">#{</span><span class="n">amount</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
    <span class="n">transaction</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">check_refund</span><span class="p">(</span>
        <span class="p">%</span><span class="no">Transaction</span><span class="p">{</span>
          <span class="ss">type:</span> <span class="p">%</span><span class="no">Refund</span><span class="p">{</span>
            <span class="ss">payment:</span> <span class="p">%</span><span class="no">Check</span><span class="p">{</span><span class="ss">amount:</span> <span class="n">amount</span><span class="p">}</span>
          <span class="p">}</span>
        <span class="p">}</span> <span class="o">=</span> <span class="n">transaction</span>
      <span class="p">)</span> <span class="k">do</span>
    <span class="no">Logger</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="s2">"Refund check $</span><span class="si">#{</span><span class="n">amount</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
    <span class="n">transaction</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>The boundary is enforced in the function head, and a mismatch raises when no clause matches.</p>

<p>This style has a few challenges:</p>

<ol>
  <li>Intertwining domain and defensive logic makes intent harder to read.</li>
  <li>The logic is not shareable. When the contract changes, every function head that encodes it must be updated in lockstep.</li>
  <li>It behaves like a guard, but the rule is implicit. The constraint exists, but it is not named as a domain concept.</li>
</ol>

<p>If we check the happy path:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Guarded</span><span class="o">.</span><span class="no">TransactionProcessor</span><span class="o">.</span><span class="n">cc_payment</span><span class="p">(</span><span class="n">charge_cc</span><span class="p">)</span>

<span class="c1"># [info] Charge cc $75</span>
<span class="c1">#</span>
<span class="c1"># %Transaction{</span>
<span class="c1">#   type: %Charge{payment: %CreditCard{name: "John", number: "1234", expiry: "12/26", amount: 75}}</span>
<span class="c1"># }</span>
</code></pre></div></div>

<p>It works as before.</p>

<p>And we are treating a mismatch as a broken invariant, so it raises (fails fast):</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Guarded</span><span class="o">.</span><span class="no">TransactionProcessor</span><span class="o">.</span><span class="n">cc_payment</span><span class="p">(</span><span class="n">refund_check</span><span class="p">)</span>

<span class="c1"># ** (FunctionClauseError) no function clause matching in Guarded.TransactionProcessor.cc_payment/1</span>
</code></pre></div></div>

<h2 id="thinking-functionally">Thinking Functionally</h2>

<h3 id="prisms-as-boundaries">Prisms as Boundaries</h3>

<p>A prism is a named boundary. It encodes the contract for when an operation applies.</p>

<p>Previewing through a prism is not “trying to get a field.” It is asking:</p>

<p>Does this transaction exist as a credit card charge amount?</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">cc_payment_prism</span> <span class="o">=</span>
  <span class="no">Prism</span><span class="o">.</span><span class="n">path</span><span class="p">([</span>
    <span class="p">{</span><span class="no">Transaction</span><span class="p">,</span> <span class="ss">:type</span><span class="p">},</span>
    <span class="p">{</span><span class="no">Charge</span><span class="p">,</span> <span class="ss">:payment</span><span class="p">},</span>
    <span class="p">{</span><span class="no">CreditCard</span><span class="p">,</span> <span class="ss">:amount</span><span class="p">}</span>
  <span class="p">])</span>
</code></pre></div></div>

<p>This prism is composable, shareable, and testable.</p>

<p>So, is this transaction a cc payment?</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Prism</span><span class="o">.</span><span class="n">preview</span><span class="p">(</span><span class="n">charge_cc</span><span class="p">,</span> <span class="n">cc_payment_prism</span><span class="p">)</span>

<span class="c1"># %Funx.Monad.Maybe.Just{value: 75}</span>
</code></pre></div></div>

<p>Yes. This transaction exists as a credit card charge amount, and we get the amount.</p>

<p>What about this one?</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Prism</span><span class="o">.</span><span class="n">preview</span><span class="p">(</span><span class="n">refund_check</span><span class="p">,</span> <span class="n">cc_payment_prism</span><span class="p">)</span>

<span class="c1"># %Funx.Monad.Maybe.Nothing{}</span>
</code></pre></div></div>

<p>No.</p>

<p>The <code class="language-plaintext highlighter-rouge">refund_check</code> is not invalid. It simply does not exist in the context defined by <code class="language-plaintext highlighter-rouge">cc_payment_prism</code>.</p>

<p><em>Prisms also support <code class="language-plaintext highlighter-rouge">review/2</code> for constructing values, but this post focuses on selection.</em></p>

<h3 id="organizing-the-boundaries">Organizing the Boundaries</h3>

<p>Let’s compose the boundaries we care about:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">defmodule</span> <span class="no">Processor</span> <span class="k">do</span>
  <span class="k">def</span> <span class="n">cc_payment_prism</span> <span class="k">do</span>
    <span class="no">Prism</span><span class="o">.</span><span class="n">compose</span><span class="p">([</span>
      <span class="no">Transaction</span><span class="o">.</span><span class="n">type_prism</span><span class="p">,</span>
      <span class="no">Charge</span><span class="o">.</span><span class="n">payment_prism</span><span class="p">,</span>
      <span class="no">CreditCard</span><span class="o">.</span><span class="n">amount_prism</span>
    <span class="p">])</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">check_payment_prism</span> <span class="k">do</span>
    <span class="no">Prism</span><span class="o">.</span><span class="n">compose</span><span class="p">([</span>
      <span class="no">Transaction</span><span class="o">.</span><span class="n">type_prism</span><span class="p">,</span>
      <span class="no">Charge</span><span class="o">.</span><span class="n">payment_prism</span><span class="p">,</span>
      <span class="no">Check</span><span class="o">.</span><span class="n">amount_prism</span>
    <span class="p">])</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">cc_refund_prism</span> <span class="k">do</span>
    <span class="no">Prism</span><span class="o">.</span><span class="n">compose</span><span class="p">([</span>
      <span class="no">Transaction</span><span class="o">.</span><span class="n">type_prism</span><span class="p">,</span>
      <span class="no">Refund</span><span class="o">.</span><span class="n">payment_prism</span><span class="p">,</span>
      <span class="no">CreditCard</span><span class="o">.</span><span class="n">amount_prism</span>
    <span class="p">])</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">check_refund_prism</span> <span class="k">do</span>
    <span class="no">Prism</span><span class="o">.</span><span class="n">compose</span><span class="p">([</span>
      <span class="no">Transaction</span><span class="o">.</span><span class="n">type_prism</span><span class="p">,</span>
      <span class="no">Refund</span><span class="o">.</span><span class="n">payment_prism</span><span class="p">,</span>
      <span class="no">Check</span><span class="o">.</span><span class="n">amount_prism</span>
    <span class="p">])</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Now we can express our processors in terms of the prism boundaries:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">defmodule</span> <span class="no">Prism</span><span class="o">.</span><span class="no">TransactionProcessor</span> <span class="k">do</span>
  <span class="k">def</span> <span class="n">cc_payment</span><span class="p">(</span><span class="n">transaction</span><span class="p">)</span> <span class="k">do</span>
    <span class="n">maybe</span> <span class="n">transaction</span><span class="p">,</span> <span class="ss">as:</span> <span class="ss">:raise</span> <span class="k">do</span>
      <span class="n">bind</span> <span class="no">Prism</span><span class="o">.</span><span class="n">preview</span><span class="p">(</span><span class="no">Processor</span><span class="o">.</span><span class="n">cc_payment_prism</span><span class="p">)</span>
      <span class="n">tap</span> <span class="k">fn</span> <span class="n">amount</span> <span class="o">-&gt;</span> <span class="no">Logger</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="s2">"Charge cc $</span><span class="si">#{</span><span class="n">amount</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span> <span class="k">end</span>
      <span class="n">map</span> <span class="k">fn</span> <span class="p">(</span><span class="n">_</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">transaction</span> <span class="k">end</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">check_payment</span><span class="p">(</span><span class="n">transaction</span><span class="p">)</span> <span class="k">do</span>
    <span class="n">maybe</span> <span class="n">transaction</span><span class="p">,</span> <span class="ss">as:</span> <span class="ss">:raise</span> <span class="k">do</span>
      <span class="n">bind</span> <span class="no">Prism</span><span class="o">.</span><span class="n">preview</span><span class="p">(</span><span class="no">Processor</span><span class="o">.</span><span class="n">check_payment_prism</span><span class="p">)</span>
      <span class="n">tap</span> <span class="k">fn</span> <span class="n">amount</span> <span class="o">-&gt;</span> <span class="no">Logger</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="s2">"Charge check $</span><span class="si">#{</span><span class="n">amount</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span> <span class="k">end</span>
      <span class="n">map</span> <span class="k">fn</span> <span class="p">(</span><span class="n">_</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">transaction</span> <span class="k">end</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">cc_refund</span><span class="p">(</span><span class="n">transaction</span><span class="p">)</span> <span class="k">do</span>
    <span class="n">maybe</span> <span class="n">transaction</span><span class="p">,</span> <span class="ss">as:</span> <span class="ss">:raise</span> <span class="k">do</span>
      <span class="n">bind</span> <span class="no">Prism</span><span class="o">.</span><span class="n">preview</span><span class="p">(</span><span class="no">Processor</span><span class="o">.</span><span class="n">cc_refund_prism</span><span class="p">)</span>
      <span class="n">tap</span> <span class="k">fn</span> <span class="n">amount</span> <span class="o">-&gt;</span> <span class="no">Logger</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="s2">"Refund cc $</span><span class="si">#{</span><span class="n">amount</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span> <span class="k">end</span>
      <span class="n">map</span> <span class="k">fn</span> <span class="p">(</span><span class="n">_</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">transaction</span> <span class="k">end</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">check_refund</span><span class="p">(</span><span class="n">transaction</span><span class="p">)</span> <span class="k">do</span>
    <span class="n">maybe</span> <span class="n">transaction</span><span class="p">,</span> <span class="ss">as:</span> <span class="ss">:raise</span> <span class="k">do</span>
      <span class="n">bind</span> <span class="no">Prism</span><span class="o">.</span><span class="n">preview</span><span class="p">(</span><span class="no">Processor</span><span class="o">.</span><span class="n">check_refund_prism</span><span class="p">)</span>
      <span class="n">tap</span> <span class="k">fn</span> <span class="n">amount</span> <span class="o">-&gt;</span> <span class="no">Logger</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="s2">"Refund check $</span><span class="si">#{</span><span class="n">amount</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span> <span class="k">end</span>
      <span class="n">map</span> <span class="k">fn</span> <span class="p">(</span><span class="n">_</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">transaction</span> <span class="k">end</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>The boundary is now a first-class value. It is reusable, composable, and enforced consistently wherever it is applied.</p>

<p>We still get the correct behavior for the happy path:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Prism</span><span class="o">.</span><span class="no">TransactionProcessor</span><span class="o">.</span><span class="n">cc_payment</span><span class="p">(</span><span class="n">charge_cc</span><span class="p">)</span>

<span class="c1"># [info] Charge cc $75</span>
<span class="c1">#</span>
<span class="c1"># %Transaction{</span>
<span class="c1">#   type: %Charge{payment: %CreditCard{name: "John", number: "1234", expiry: "12/26", amount: 75}}</span>
<span class="c1"># }</span>
</code></pre></div></div>

<p>And we fail fast at the edge:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Prism</span><span class="o">.</span><span class="no">TransactionProcessor</span><span class="o">.</span><span class="n">cc_payment</span><span class="p">(</span><span class="n">refund_cc</span><span class="p">)</span>

<span class="c1"># ** (RuntimeError) Nothing value encountered</span>
</code></pre></div></div>

<p>But the meaning is different. We fail at the boundary because “this operation does not apply in this context,” not because “the data is malformed.”</p>

<h2 id="the-context-of-monads">The Context of Monads</h2>

<p><code class="language-plaintext highlighter-rouge">Prism</code>’s <code class="language-plaintext highlighter-rouge">preview/2</code> returns <code class="language-plaintext highlighter-rouge">Maybe</code>, which means we can leverage the monad to manage collections of values.</p>

<p>Let’s start with a list of transactions:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">charge_cc_1</span> <span class="o">=</span>
  <span class="p">%</span><span class="no">Transaction</span><span class="p">{</span>
    <span class="ss">type:</span> <span class="p">%</span><span class="no">Charge</span><span class="p">{</span>
      <span class="ss">payment:</span> <span class="p">%</span><span class="no">CreditCard</span><span class="p">{</span><span class="ss">name:</span> <span class="s2">"Alice"</span><span class="p">,</span> <span class="ss">number:</span> <span class="s2">"4111"</span><span class="p">,</span> <span class="ss">expiry:</span> <span class="s2">"12/26"</span><span class="p">,</span> <span class="ss">amount:</span> <span class="mi">1592</span><span class="p">}</span>
    <span class="p">}</span>
  <span class="p">}</span>

<span class="n">charge_cc_2</span> <span class="o">=</span>
  <span class="p">%</span><span class="no">Transaction</span><span class="p">{</span>
    <span class="ss">type:</span> <span class="p">%</span><span class="no">Charge</span><span class="p">{</span>
      <span class="ss">payment:</span> <span class="p">%</span><span class="no">CreditCard</span><span class="p">{</span><span class="ss">name:</span> <span class="s2">"Bob"</span><span class="p">,</span> <span class="ss">number:</span> <span class="s2">"4222"</span><span class="p">,</span> <span class="ss">expiry:</span> <span class="s2">"11/25"</span><span class="p">,</span> <span class="ss">amount:</span> <span class="mi">823</span><span class="p">}</span>
    <span class="p">}</span>
  <span class="p">}</span>

<span class="n">charge_cc_3</span> <span class="o">=</span>
  <span class="p">%</span><span class="no">Transaction</span><span class="p">{</span>
    <span class="ss">type:</span> <span class="p">%</span><span class="no">Charge</span><span class="p">{</span>
      <span class="ss">payment:</span> <span class="p">%</span><span class="no">CreditCard</span><span class="p">{</span><span class="ss">name:</span> <span class="s2">"Dave"</span><span class="p">,</span> <span class="ss">number:</span> <span class="s2">"4444"</span><span class="p">,</span> <span class="ss">expiry:</span> <span class="s2">"09/26"</span><span class="p">,</span> <span class="ss">amount:</span> <span class="mi">191</span><span class="p">}</span>
    <span class="p">}</span>
  <span class="p">}</span>

<span class="n">refund_cc_1</span> <span class="o">=</span>
  <span class="p">%</span><span class="no">Transaction</span><span class="p">{</span>
    <span class="ss">type:</span> <span class="p">%</span><span class="no">Refund</span><span class="p">{</span>
      <span class="ss">payment:</span> <span class="p">%</span><span class="no">CreditCard</span><span class="p">{</span><span class="ss">name:</span> <span class="s2">"Carol"</span><span class="p">,</span> <span class="ss">number:</span> <span class="s2">"4333"</span><span class="p">,</span> <span class="ss">expiry:</span> <span class="s2">"10/27"</span><span class="p">,</span> <span class="ss">amount:</span> <span class="mi">161</span><span class="p">}</span>
    <span class="p">}</span>
  <span class="p">}</span>

<span class="n">refund_cc_2</span> <span class="o">=</span>
  <span class="p">%</span><span class="no">Transaction</span><span class="p">{</span>
    <span class="ss">type:</span> <span class="p">%</span><span class="no">Refund</span><span class="p">{</span>
      <span class="ss">payment:</span> <span class="p">%</span><span class="no">CreditCard</span><span class="p">{</span><span class="ss">name:</span> <span class="s2">"Eve"</span><span class="p">,</span> <span class="ss">number:</span> <span class="s2">"4555"</span><span class="p">,</span> <span class="ss">expiry:</span> <span class="s2">"08/28"</span><span class="p">,</span> <span class="ss">amount:</span> <span class="mi">110</span><span class="p">}</span>
    <span class="p">}</span>
  <span class="p">}</span>

<span class="n">transactions</span> <span class="o">=</span> <span class="p">[</span><span class="n">charge_cc_1</span><span class="p">,</span> <span class="n">charge_cc_2</span><span class="p">,</span> <span class="n">charge_cc_3</span><span class="p">,</span> <span class="n">refund_cc_1</span><span class="p">,</span> <span class="n">refund_cc_2</span><span class="p">]</span>
</code></pre></div></div>

<p>Once the boundary is a value, we can reuse it across a collection: either to collect matches or to require that every element matches.</p>

<h3 id="collecting-matches">Collecting Matches</h3>

<p><code class="language-plaintext highlighter-rouge">concat_map/2</code> keeps <code class="language-plaintext highlighter-rouge">Just</code> results and drops <code class="language-plaintext highlighter-rouge">Nothing</code>. This lets us collect only the credit card charges:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">alias</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Monad</span><span class="o">.</span><span class="no">Maybe</span>
<span class="n">alias</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Math</span>

<span class="n">cc_payments</span> <span class="o">=</span>
  <span class="n">transactions</span>
  <span class="o">|&gt;</span> <span class="no">Maybe</span><span class="o">.</span><span class="n">concat_map</span><span class="p">(</span><span class="o">&amp;</span><span class="no">Prism</span><span class="o">.</span><span class="n">preview</span><span class="p">(</span><span class="nv">&amp;1</span><span class="p">,</span> <span class="no">Processor</span><span class="o">.</span><span class="n">cc_payment_prism</span><span class="p">))</span>
  <span class="o">|&gt;</span> <span class="no">Math</span><span class="o">.</span><span class="n">sum</span><span class="p">()</span>

<span class="c1"># 2606</span>
</code></pre></div></div>

<p>And only the credit card refunds:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">cc_refunds</span> <span class="o">=</span>
  <span class="n">transactions</span>
  <span class="o">|&gt;</span> <span class="no">Maybe</span><span class="o">.</span><span class="n">concat_map</span><span class="p">(</span><span class="o">&amp;</span><span class="no">Prism</span><span class="o">.</span><span class="n">preview</span><span class="p">(</span><span class="nv">&amp;1</span><span class="p">,</span> <span class="no">Processor</span><span class="o">.</span><span class="n">cc_refund_prism</span><span class="p">))</span>
  <span class="o">|&gt;</span> <span class="no">Math</span><span class="o">.</span><span class="n">sum</span><span class="p">()</span>

<span class="c1"># 271</span>
</code></pre></div></div>

<p>From there, we can calculate our credit card net movement:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">cc_payments</span> <span class="o">-</span> <span class="n">cc_refunds</span>

<span class="c1"># 2335</span>
</code></pre></div></div>

<h3 id="requiring-all-matches">Requiring All Matches</h3>

<p><code class="language-plaintext highlighter-rouge">traverse/2</code> flips the logic. Instead of collecting matches, it asserts that the entire list fits the prism.</p>

<p>With a mixed list, the answer is no:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">transactions</span>
<span class="o">|&gt;</span> <span class="no">Maybe</span><span class="o">.</span><span class="n">traverse</span><span class="p">(</span><span class="o">&amp;</span><span class="no">Prism</span><span class="o">.</span><span class="n">preview</span><span class="p">(</span><span class="nv">&amp;1</span><span class="p">,</span> <span class="no">Processor</span><span class="o">.</span><span class="n">cc_payment_prism</span><span class="p">))</span>

<span class="c1"># %Funx.Monad.Maybe.Nothing{}</span>
</code></pre></div></div>

<p>If we narrow the list to only charges, it succeeds:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">only_charges</span> <span class="o">=</span> <span class="p">[</span><span class="n">charge_cc_1</span><span class="p">,</span> <span class="n">charge_cc_2</span><span class="p">,</span> <span class="n">charge_cc_3</span><span class="p">]</span>

<span class="n">only_charges</span>
<span class="o">|&gt;</span> <span class="no">Maybe</span><span class="o">.</span><span class="n">traverse</span><span class="p">(</span><span class="o">&amp;</span><span class="no">Prism</span><span class="o">.</span><span class="n">preview</span><span class="p">(</span><span class="nv">&amp;1</span><span class="p">,</span> <span class="no">Processor</span><span class="o">.</span><span class="n">cc_payment_prism</span><span class="p">))</span>

<span class="c1"># %Funx.Monad.Maybe.Just{value: [1592, 823, 191]}</span>
</code></pre></div></div>

<p>Now we get <code class="language-plaintext highlighter-rouge">Just</code> a list of credit card charge amounts.</p>

<p>That means we can write batch processors:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">defmodule</span> <span class="no">Batch</span><span class="o">.</span><span class="no">TransactionProcessor</span> <span class="k">do</span>
  <span class="n">alias</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Math</span>
  <span class="kn">require</span> <span class="no">Logger</span>

  <span class="k">def</span> <span class="n">cc_payment</span><span class="p">(</span><span class="n">transactions</span><span class="p">)</span> <span class="k">do</span>
    <span class="n">maybe</span> <span class="n">transactions</span><span class="p">,</span> <span class="ss">as:</span> <span class="ss">:raise</span> <span class="k">do</span>
      <span class="n">bind</span> <span class="no">Maybe</span><span class="o">.</span><span class="n">traverse</span><span class="p">(</span><span class="o">&amp;</span><span class="no">Prism</span><span class="o">.</span><span class="n">preview</span><span class="p">(</span><span class="nv">&amp;1</span><span class="p">,</span> <span class="no">Processor</span><span class="o">.</span><span class="n">cc_payment_prism</span><span class="p">))</span>
      <span class="n">tap</span> <span class="k">fn</span> <span class="n">amounts</span> <span class="o">-&gt;</span>
        <span class="no">Logger</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="s2">"Charge cc total: $</span><span class="si">#{</span><span class="no">Math</span><span class="o">.</span><span class="n">sum</span><span class="p">(</span><span class="n">amounts</span><span class="p">)</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
      <span class="k">end</span>
      <span class="n">map</span> <span class="k">fn</span> <span class="p">(</span><span class="n">_</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">transactions</span> <span class="k">end</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">check_payment</span><span class="p">(</span><span class="n">transactions</span><span class="p">)</span> <span class="k">do</span>
    <span class="n">maybe</span> <span class="n">transactions</span><span class="p">,</span> <span class="ss">as:</span> <span class="ss">:raise</span> <span class="k">do</span>
      <span class="n">bind</span> <span class="no">Maybe</span><span class="o">.</span><span class="n">traverse</span><span class="p">(</span><span class="o">&amp;</span><span class="no">Prism</span><span class="o">.</span><span class="n">preview</span><span class="p">(</span><span class="nv">&amp;1</span><span class="p">,</span> <span class="no">Processor</span><span class="o">.</span><span class="n">check_payment_prism</span><span class="p">))</span>
      <span class="n">tap</span> <span class="k">fn</span> <span class="n">amounts</span> <span class="o">-&gt;</span>
        <span class="no">Logger</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="s2">"Charge check total: $</span><span class="si">#{</span><span class="no">Math</span><span class="o">.</span><span class="n">sum</span><span class="p">(</span><span class="n">amounts</span><span class="p">)</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
      <span class="k">end</span>
      <span class="n">map</span> <span class="k">fn</span> <span class="p">(</span><span class="n">_</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">transactions</span> <span class="k">end</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">cc_refund</span><span class="p">(</span><span class="n">transactions</span><span class="p">)</span> <span class="k">do</span>
    <span class="n">maybe</span> <span class="n">transactions</span><span class="p">,</span> <span class="ss">as:</span> <span class="ss">:raise</span> <span class="k">do</span>
      <span class="n">bind</span> <span class="no">Maybe</span><span class="o">.</span><span class="n">traverse</span><span class="p">(</span><span class="o">&amp;</span><span class="no">Prism</span><span class="o">.</span><span class="n">preview</span><span class="p">(</span><span class="nv">&amp;1</span><span class="p">,</span> <span class="no">Processor</span><span class="o">.</span><span class="n">cc_refund_prism</span><span class="p">))</span>
      <span class="n">tap</span> <span class="k">fn</span> <span class="n">amounts</span> <span class="o">-&gt;</span>
        <span class="no">Logger</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="s2">"Refund cc total: $</span><span class="si">#{</span><span class="no">Math</span><span class="o">.</span><span class="n">sum</span><span class="p">(</span><span class="n">amounts</span><span class="p">)</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
      <span class="k">end</span>
      <span class="n">map</span> <span class="k">fn</span> <span class="p">(</span><span class="n">_</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">transactions</span> <span class="k">end</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="n">check_refund</span><span class="p">(</span><span class="n">transactions</span><span class="p">)</span> <span class="k">do</span>
    <span class="n">maybe</span> <span class="n">transactions</span><span class="p">,</span> <span class="ss">as:</span> <span class="ss">:raise</span> <span class="k">do</span>
      <span class="n">bind</span> <span class="no">Maybe</span><span class="o">.</span><span class="n">traverse</span><span class="p">(</span><span class="o">&amp;</span><span class="no">Prism</span><span class="o">.</span><span class="n">preview</span><span class="p">(</span><span class="nv">&amp;1</span><span class="p">,</span> <span class="no">Processor</span><span class="o">.</span><span class="n">check_refund_prism</span><span class="p">))</span>
      <span class="n">tap</span> <span class="k">fn</span> <span class="n">amounts</span> <span class="o">-&gt;</span>
        <span class="no">Logger</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="s2">"Refund check total: $</span><span class="si">#{</span><span class="no">Math</span><span class="o">.</span><span class="n">sum</span><span class="p">(</span><span class="n">amounts</span><span class="p">)</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
      <span class="k">end</span>
      <span class="n">map</span> <span class="k">fn</span> <span class="p">(</span><span class="n">_</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">transactions</span> <span class="k">end</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>And with a mixed list:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Batch</span><span class="o">.</span><span class="no">TransactionProcessor</span><span class="o">.</span><span class="n">cc_payment</span><span class="p">(</span><span class="n">transactions</span><span class="p">)</span>

<span class="c1"># ** (RuntimeError) Nothing value encountered</span>
</code></pre></div></div>

<p>This fails fast at the boundary.</p>

<p>But, with a homogeneous list of credit card charges:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Batch</span><span class="o">.</span><span class="no">TransactionProcessor</span><span class="o">.</span><span class="n">cc_payment</span><span class="p">(</span><span class="n">only_charges</span><span class="p">)</span>

<span class="c1"># [info] Charge cc total: $2606</span>
<span class="c1">#</span>
<span class="c1"># [</span>
<span class="c1">#   %Transaction{</span>
<span class="c1">#     type: %Charge{</span>
<span class="c1">#       payment: %CreditCard{name: "Alice", number: "4111", expiry: "12/26", amount: 1592}</span>
<span class="c1">#     }</span>
<span class="c1">#   },</span>
<span class="c1">#   %Transaction{</span>
<span class="c1">#     type: %Charge{payment: %CreditCard{name: "Bob", number: "4222", expiry: "11/25", amount: 823}}</span>
<span class="c1">#   },</span>
<span class="c1">#   %Transaction{</span>
<span class="c1">#     type: %Charge{payment: %CreditCard{name: "Dave", number: "4444", expiry: "09/26", amount: 191}}</span>
<span class="c1">#   }</span>
<span class="c1"># ]</span>

</code></pre></div></div>

<p>The batch runs as expected.</p>

<p>Elixir does not have a static way to express “this function accepts only a list of cc charge transactions.” But we can still enforce that domain constraint at the boundary. <code class="language-plaintext highlighter-rouge">Maybe.traverse</code> is all or nothing: either every element matches the prism and we get the extracted amounts, or the entire batch is rejected.</p>

<p>We are no longer treating a list of transactions as a bag of stuff and hoping conventions keep it aligned. We are reusing our existing prisms to assert a batch contract.</p>

<p>Now the defensive checks are isolated, named, and reusable, which keeps the business steps easy to read and maintain.</p>

<h2 id="resources">Resources</h2>

<div style="display: flex; gap: 20px; margin: 20px 0;">
  <div style="flex-shrink: 0;">
    <a href="https://pragprog.com/titles/jkelixir/advanced-functional-programming-with-elixir">
      <img src="/assets/images/jkelixir_small.jpg" alt="Advanced Functional Programming with Elixir book cover" width="150" />
    </a>
  </div>
  <div>
    <h3><a href="https://pragprog.com/titles/jkelixir/advanced-functional-programming-with-elixir">Advanced Functional Programming with Elixir</a></h3>
    <p>Dive deeper into functional programming patterns and advanced Elixir techniques. Learn how to build robust, maintainable applications using functional programming principles.</p>
  </div>
</div>

<div style="display: flex; gap: 20px; margin: 20px 0;">
  <div style="flex-shrink: 0;">
    <a href="https://www.funxlib.com">
      <img src="/assets/images/funx-social.jpg" alt="Funx functional programming library" width="150" />
    </a>
  </div>
  <div>
    <h3><a href="https://www.funxlib.com">Funx - Functional Programming for Elixir</a></h3>
    <p>A library of functional programming abstractions for Elixir, including monads, monoids, Eq, Ord, and more. Built as an ecosystem where learning is the priority from the start.</p>
  </div>
</div>]]></content><author><name>Joseph Koski</name></author><category term="elixir" /><category term="funx" /><summary type="html"><![CDATA[“The problem is choice.” — Neo, The Matrix (1999)]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.joekoski.com/assets/images/jkelixir_small.jpg" /><media:content medium="image" url="https://www.joekoski.com/assets/images/jkelixir_small.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Funx: Adding the Optic Lens</title><link href="https://www.joekoski.com/blog/2025/12/14/funx-optics-lens.html" rel="alternate" type="text/html" title="Funx: Adding the Optic Lens" /><published>2025-12-14T14:16:06+00:00</published><updated>2025-12-14T14:16:06+00:00</updated><id>https://www.joekoski.com/blog/2025/12/14/funx-optics-lens</id><content type="html" xml:base="https://www.joekoski.com/blog/2025/12/14/funx-optics-lens.html"><![CDATA[<blockquote>
  <p>“I didn’t say it would be easy. I just said it would be the truth.” —Morpheus, The Matrix (1999)</p>
</blockquote>

<p>I wanted to cover optics in my book, but length requirements meant I needed to stop at monads.</p>

<p>I finally have time to start adding them to Funx.</p>

<p><a href="https://livebook.dev/run?url=https%3A%2F%2Fwww.joekoski.com%2Fassets%2Flivebooks%2Fblogs%2Ffunx-optics-lens.livemd"><img src="https://livebook.dev/badge/v1/black.svg" alt="Run in Livebook" /></a></p>

<h2 id="why-lenses">Why Lenses?</h2>

<p>If you read technical papers on lensing, they explain why it is “interesting”, but they do not explain why we might want to adopt it.</p>

<p>There are plenty of blog posts that try to make that case.</p>

<h3 id="they-solve-a-tedium-problem">They Solve a Tedium Problem</h3>

<p>A common argument is that manipulating nested data is difficult.</p>

<ul>
  <li><a href="https://richashworth.com/blog/functional-patterns-in-scala-lenses">Manipulating fields can be tedious.</a></li>
  <li><a href="https://rockthejvm.com/articles/lenses-prisms-and-optics-in-scala">Nested data structures are a pain to inspect and change.</a></li>
  <li><a href="https://medium.com/@gcanti/introduction-to-optics-lenses-and-prisms-3230e73bfcfe">They can reduce the amount of code we have to write significantly.</a></li>
</ul>

<p>This is certainly true in Scala. But as one Elixir developer observes, it is not a meaningful challenge in Elixir.</p>

<ul>
  <li><a href="https://elixirforum.com/t/the-complexity-of-haskell-vs-elixirs-simplicity/16366">Lens only exists because the native type system makes that work too hard.</a></li>
</ul>

<p>And even Haskell developers debate whether the tradeoffs are worth it.</p>

<ul>
  <li><a href="https://reasonablypolymorphic.com/blog/code-lenses/">More trouble than they are worth.</a></li>
</ul>

<h3 id="lenses-are-composable">Lenses are Composable</h3>

<p>Another common argument is that lenses compose well.</p>

<ul>
  <li><a href="https://medium.com/@heytherewill/functional-programming-optics-in-net-7e1998bfb47e">What makes lenses useful is the fact that they can be composed.</a></li>
</ul>

<p>But projection functions compose just as well, making it hard to say that composition alone can justify bringing lenses into a codebase.</p>

<h3 id="helpful-for-hard-problems">Helpful for Hard Problems</h3>

<p>Lenses can help in some difficult cases.</p>

<ul>
  <li><a href="https://medium.com/%40wigahluk/another-look-through-optics-ffd253336e9c">Optics as proxies.</a></li>
</ul>

<p>Which suggests lensing is useful when a problem is complicated enough to justify the abstraction.</p>

<p>These are all incidental benefits. They are not the core reason to adopt lenses.</p>

<p>The purpose of lensing is not convenience. It is legality.</p>

<h2 id="elixirs-built-in-accessors">Elixir’s Built-in Accessors</h2>

<p>Elixir provides <code class="language-plaintext highlighter-rouge">get_in/2</code>, <code class="language-plaintext highlighter-rouge">put_in/3</code>, and <code class="language-plaintext highlighter-rouge">update_in/3</code> which operate on paths that describe how to navigate into a structure.</p>

<p>First, we’ll start with some data:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">garfield</span> <span class="o">=</span> <span class="p">%{</span>
  <span class="ss">name:</span> <span class="s2">"Garfield"</span><span class="p">,</span>
  <span class="ss">weight:</span> <span class="mi">20</span><span class="p">,</span>
  <span class="ss">owner:</span> <span class="p">%{</span>
    <span class="ss">name:</span> <span class="s2">"Jon"</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Here is the path into the owner’s name:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">valid_owner_name_path</span> <span class="o">=</span> <span class="p">[</span><span class="ss">:owner</span><span class="p">,</span> <span class="ss">:name</span><span class="p">]</span>
</code></pre></div></div>

<p>We can read through the path:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">get_in</span><span class="p">(</span><span class="n">garfield</span><span class="p">,</span> <span class="n">valid_owner_name_path</span><span class="p">)</span>
<span class="c1"># "Jon"</span>
</code></pre></div></div>

<p>We can write through the path:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">put_in</span><span class="p">(</span><span class="n">garfield</span><span class="p">,</span> <span class="n">valid_owner_name_path</span><span class="p">,</span> <span class="s2">"Jonathon"</span><span class="p">)</span>
<span class="c1"># %{name: "Garfield", owner: %{name: "Jonathon"}, weight: 20}</span>
</code></pre></div></div>

<p>And we can update through the path:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">update_in</span><span class="p">(</span><span class="n">garfield</span><span class="p">,</span> <span class="n">valid_owner_name_path</span><span class="p">,</span> <span class="k">fn</span> <span class="n">name</span> <span class="o">-&gt;</span> <span class="no">String</span><span class="o">.</span><span class="n">upcase</span><span class="p">(</span><span class="n">name</span><span class="p">)</span> <span class="k">end</span><span class="p">)</span>
<span class="c1"># %{name: "Garfield", owner: %{name: "JON"}, weight: 20}</span>
</code></pre></div></div>

<p>Now let’s consider an invalid path:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">invalid_owner_age_path</span> <span class="o">=</span> <span class="p">[</span><span class="ss">:owner</span><span class="p">,</span> <span class="ss">:age</span><span class="p">]</span>
<span class="c1">#[:owner, :age]</span>
</code></pre></div></div>

<p>If we read:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">get_in</span><span class="p">(</span><span class="n">garfield</span><span class="p">,</span> <span class="n">invalid_owner_age_path</span><span class="p">)</span>
<span class="c1"># nil</span>
</code></pre></div></div>

<p>We get an ambiguous <code class="language-plaintext highlighter-rouge">nil</code>. It could mean the <code class="language-plaintext highlighter-rouge">:owner</code> key is missing or the <code class="language-plaintext highlighter-rouge">:age</code> key is missing. It could mean both keys exist and the value is <code class="language-plaintext highlighter-rouge">nil</code>.</p>

<p>When we write:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">put_in</span><span class="p">(</span><span class="n">garfield</span><span class="p">,</span> <span class="n">invalid_owner_age_path</span><span class="p">,</span> <span class="mi">40</span><span class="p">)</span>
<span class="c1"># %{name: "Garfield", owner: %{name: "Jon", age: 40}, weight: 20}</span>
</code></pre></div></div>

<p>This succeeds. Even though the path does not actually point to a valid location in the original structure, Elixir treats the write as a create and silently changes the shape of the data.</p>

<p>And when we update:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">update_in</span><span class="p">(</span><span class="n">garfield</span><span class="p">,</span> <span class="n">invalid_owner_age_path</span><span class="p">,</span> <span class="k">fn</span> <span class="n">curr</span> <span class="o">-&gt;</span> <span class="n">curr</span> <span class="o">+</span> <span class="mi">1</span> <span class="k">end</span><span class="p">)</span>
<span class="c1"># ** (ArithmeticError) bad argument in arithmetic expression: nil + 1</span>
</code></pre></div></div>

<p>Here the fallback breaks down completely. The <code class="language-plaintext highlighter-rouge">curr</code> is <code class="language-plaintext highlighter-rouge">nil</code> and we crash with an <code class="language-plaintext highlighter-rouge">ArithmeticError</code>.</p>

<p>At this point the same path has produced three different behaviors depending on which operation we chose: read returns <code class="language-plaintext highlighter-rouge">nil</code>, write creates new structure, update crashes.</p>

<p>This is convenient, but is not a lawful lens.</p>

<h2 id="lawful-lenses-symmetric-explicit-contracts">Lawful Lenses: Symmetric, Explicit Contracts</h2>

<p>A lens is a contract that guarantees symmetric behavior: if you can read through it, you can write through it, and vice versa. All operations make the same assumptions about the focus. No fallbacks, no auto-creation, no surprises.</p>

<p>Funx provides lawful lenses.</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">alias</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Optics</span><span class="o">.</span><span class="no">Lens</span>
</code></pre></div></div>

<p>Let’s use <code class="language-plaintext highlighter-rouge">path/1</code> to lift our valid path:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">valid_owner_name_lens</span> <span class="o">=</span> <span class="no">Lens</span><span class="o">.</span><span class="n">path</span><span class="p">(</span><span class="n">valid_owner_name_path</span><span class="p">)</span>
</code></pre></div></div>

<p>With Lens, we <em>view</em> to read:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Lens</span><span class="o">.</span><span class="n">view!</span><span class="p">(</span><span class="n">garfield</span><span class="p">,</span> <span class="n">valid_owner_name_lens</span><span class="p">)</span>
<span class="c1"># "Jon"</span>
</code></pre></div></div>

<p><em>Set</em> to write:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Lens</span><span class="o">.</span><span class="n">set!</span><span class="p">(</span><span class="n">garfield</span><span class="p">,</span> <span class="n">valid_owner_name_lens</span><span class="p">,</span> <span class="s2">"Jonathon"</span><span class="p">)</span>
<span class="c1"># %{name: "Garfield", weight: 20, owner: %{name: "Jonathon"}}</span>
</code></pre></div></div>

<p>And <em>over</em> to update:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Lens</span><span class="o">.</span><span class="n">over!</span><span class="p">(</span><span class="n">garfield</span><span class="p">,</span> <span class="n">valid_owner_name_lens</span><span class="p">,</span> <span class="k">fn</span> <span class="n">name</span> <span class="o">-&gt;</span> <span class="no">String</span><span class="o">.</span><span class="n">upcase</span><span class="p">(</span><span class="n">name</span><span class="p">)</span> <span class="k">end</span><span class="p">)</span>
<span class="c1"># %{name: "Garfield", weight: 20, owner: %{name: "JON"}}</span>
</code></pre></div></div>

<p>Elixir doesn’t have a type system to prove whether a lens is valid at compile time, so every operation has two possible outcomes: success or explicit failure.</p>

<p>Let’s look at an invalid path:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">invalid_owner_age_lens</span> <span class="o">=</span> <span class="no">Lens</span><span class="o">.</span><span class="n">path</span><span class="p">(</span><span class="n">invalid_owner_age_path</span><span class="p">)</span>
</code></pre></div></div>

<p>Like Elixir’s <code class="language-plaintext highlighter-rouge">update_in/3</code> accessor, our update raises an error:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Lens</span><span class="o">.</span><span class="n">over!</span><span class="p">(</span><span class="n">garfield</span><span class="p">,</span> <span class="n">invalid_owner_age_lens</span><span class="p">,</span> <span class="k">fn</span> <span class="n">curr</span> <span class="o">-&gt;</span> <span class="n">curr</span> <span class="o">+</span> <span class="mi">1</span> <span class="k">end</span><span class="p">)</span>
<span class="c1"># ** (KeyError) key :age not found in: %{name: "Jon"}</span>
</code></pre></div></div>

<p>But instead of an <code class="language-plaintext highlighter-rouge">ArithmeticError</code>, we get a <code class="language-plaintext highlighter-rouge">KeyError</code>. The lens won’t apply a function to an invalid focus.</p>

<p>Let’s write to an invalid focus:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Lens</span><span class="o">.</span><span class="n">set!</span><span class="p">(</span><span class="n">garfield</span><span class="p">,</span> <span class="n">invalid_owner_age_lens</span><span class="p">,</span> <span class="s2">"Jonathon"</span><span class="p">)</span>
<span class="c1"># ** (KeyError) key :age not found in: %{name: "Jon"}</span>
</code></pre></div></div>

<p>Again, a <code class="language-plaintext highlighter-rouge">KeyError</code>. A lawful lens will not write to an invalid focus.</p>

<p>And if we view:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Lens</span><span class="o">.</span><span class="n">view!</span><span class="p">(</span><span class="n">garfield</span><span class="p">,</span> <span class="n">invalid_owner_age_lens</span><span class="p">)</span>
<span class="c1"># ** (KeyError) key :age not found in: %{name: "Jon"}</span>
</code></pre></div></div>

<p>You guessed it, a <code class="language-plaintext highlighter-rouge">KeyError</code>. A lawful lens will not even allow us to read an invalid focus.</p>

<h3 id="a-lens-is-lawful">A Lens is Lawful</h3>

<p>If the focus is valid, you can read it, write it, and update it. If not valid, all operations fail explicitly. There is no fallback, no auto-creation, and no silent fixing.</p>

<p>This is what makes composition safe and refactoring predictable. Behavior depends only on the lens itself, not the runtime shape of the data. Everything makes the same assumptions, and errors stop exactly where an assumption breaks instead of being “cured” into a hard-to-find downstream bug.</p>

<p>Structs have a fixed schema. The asymmetric behavior of <code class="language-plaintext highlighter-rouge">put_in</code> (creating keys on write) is fundamentally incompatible with that constraint. Lawful lenses enforce symmetric contracts, which means they will work on structs.</p>

<h2 id="yes-we-can-lens-a-struct">Yes, We Can Lens a Struct</h2>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">defmodule</span> <span class="no">Owner</span> <span class="k">do</span>
  <span class="k">defstruct</span> <span class="p">[</span><span class="ss">:name</span><span class="p">]</span>
<span class="k">end</span>

<span class="k">defmodule</span> <span class="no">Cat</span> <span class="k">do</span>
  <span class="k">defstruct</span> <span class="p">[</span><span class="ss">:name</span><span class="p">,</span> <span class="ss">:owner</span><span class="p">,</span> <span class="ss">:weight</span><span class="p">]</span>
<span class="k">end</span>
</code></pre></div></div>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">granny</span> <span class="o">=</span> <span class="p">%</span><span class="no">Owner</span><span class="p">{</span><span class="ss">name:</span> <span class="s2">"Granny"</span><span class="p">}</span>
<span class="n">sylvester</span> <span class="o">=</span> <span class="p">%</span><span class="no">Cat</span><span class="p">{</span><span class="ss">name:</span> <span class="s2">"Sylvester"</span><span class="p">,</span> <span class="ss">owner:</span> <span class="n">granny</span><span class="p">,</span> <span class="ss">weight:</span> <span class="mi">15</span><span class="p">}</span>
<span class="c1"># %Cat{name: "Sylvester", owner: %Owner{name: "Granny"}, weight: 15}</span>
</code></pre></div></div>

<p>Elixir’s accessor cannot lens a struct, even with a valid focus:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">get_in</span><span class="p">(</span><span class="n">sylvester</span><span class="p">,</span> <span class="n">valid_owner_name_path</span><span class="p">)</span>
<span class="c1"># ** (UndefinedFunctionError) function Cat.fetch/2 is undefined </span>
<span class="c1"># (Cat does not implement the Access behaviour)</span>
</code></pre></div></div>

<p>But Funx can:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Lens</span><span class="o">.</span><span class="n">view!</span><span class="p">(</span><span class="n">sylvester</span><span class="p">,</span> <span class="n">valid_owner_name_lens</span><span class="p">)</span>
<span class="c1"># "Granny"</span>
</code></pre></div></div>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Lens</span><span class="o">.</span><span class="n">set!</span><span class="p">(</span><span class="n">sylvester</span><span class="p">,</span> <span class="n">valid_owner_name_lens</span><span class="p">,</span> <span class="s2">"Gramps"</span><span class="p">)</span>
<span class="c1"># %Cat{name: "Sylvester", weight: 15, owner: %Owner{name: "Gramps"}}</span>
</code></pre></div></div>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Lens</span><span class="o">.</span><span class="n">over!</span><span class="p">(</span><span class="n">sylvester</span><span class="p">,</span> <span class="n">valid_owner_name_lens</span><span class="p">,</span> <span class="k">fn</span> <span class="n">name</span> <span class="o">-&gt;</span> <span class="no">String</span><span class="o">.</span><span class="n">upcase</span><span class="p">(</span><span class="n">name</span><span class="p">)</span> <span class="k">end</span><span class="p">)</span>
<span class="c1"># %Cat{name: "Sylvester", weight: 15, owner: %Owner{name: "GRANNY"}}</span>
</code></pre></div></div>

<p>In a lawful Lens, an invalid focus will always fail:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Lens</span><span class="o">.</span><span class="n">view!</span><span class="p">(</span><span class="n">sylvester</span><span class="p">,</span> <span class="n">invalid_owner_age_lens</span><span class="p">)</span>
<span class="c1"># ** (KeyError) key :age not found in: %Owner{name: "Granny"}</span>
</code></pre></div></div>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Lens</span><span class="o">.</span><span class="n">set!</span><span class="p">(</span><span class="n">sylvester</span><span class="p">,</span> <span class="n">invalid_owner_age_lens</span><span class="p">,</span> <span class="s2">"Jonathon"</span><span class="p">)</span>
<span class="c1"># ** (KeyError) key :age not found in: %Owner{name: "Granny"}</span>
</code></pre></div></div>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Lens</span><span class="o">.</span><span class="n">over!</span><span class="p">(</span><span class="n">sylvester</span><span class="p">,</span> <span class="n">invalid_owner_age_lens</span><span class="p">,</span> <span class="k">fn</span> <span class="n">name</span> <span class="o">-&gt;</span> <span class="no">String</span><span class="o">.</span><span class="n">upcase</span><span class="p">(</span><span class="n">name</span><span class="p">)</span> <span class="k">end</span><span class="p">)</span>
<span class="c1"># ** (KeyError) key :age not found in: %Owner{name: "Granny"}</span>
</code></pre></div></div>

<p>But we don’t always have to raise an error. We can use the safe <code class="language-plaintext highlighter-rouge">view/2</code>, <code class="language-plaintext highlighter-rouge">set/3</code>, and <code class="language-plaintext highlighter-rouge">over/3</code>:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Lens</span><span class="o">.</span><span class="n">over</span><span class="p">(</span><span class="n">sylvester</span><span class="p">,</span> <span class="n">invalid_owner_age_lens</span><span class="p">,</span> <span class="k">fn</span> <span class="n">name</span> <span class="o">-&gt;</span> <span class="no">String</span><span class="o">.</span><span class="n">upcase</span><span class="p">(</span><span class="n">name</span><span class="p">)</span> <span class="k">end</span><span class="p">)</span>
<span class="c1"># %Funx.Monad.Either{value: %KeyError{key: :age, term: %Owner{name: "Granny"}}}</span>
</code></pre></div></div>

<p>Here, Funx’s default behavior is its <code class="language-plaintext highlighter-rouge">Either</code>.</p>

<p>But we can elect Elixir’s tuple:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Lens</span><span class="o">.</span><span class="n">view</span><span class="p">(</span><span class="n">sylvester</span><span class="p">,</span> <span class="n">invalid_owner_age_lens</span><span class="p">,</span> <span class="ss">as:</span> <span class="ss">:tuple</span><span class="p">)</span>
<span class="c1"># {:error, %KeyError{key: :age, term: %Owner{name: "Granny"}}}</span>
</code></pre></div></div>

<p>Where we get the typical <code class="language-plaintext highlighter-rouge">{:ok, value}</code> or <code class="language-plaintext highlighter-rouge">{:error, reason}</code> tuple.</p>

<h2 id="so-why-lenses">So, Why Lenses?</h2>

<p>It is not about composition or avoiding tediousness.</p>

<p>We want lenses to solve this problem:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">put_in</span><span class="p">(</span><span class="n">garfield</span><span class="p">,</span> <span class="p">[</span><span class="ss">:owner</span><span class="p">,</span> <span class="ss">:nane</span><span class="p">],</span> <span class="s2">"Dave"</span><span class="p">)</span>
<span class="c1"># %{name: "Garfield", owner: %{name: "Jon", nane: "Dave"}, weight: 20}</span>
</code></pre></div></div>

<p>There’s a downstream bug.</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">struct</span><span class="p">(</span><span class="n">sylvester</span><span class="p">,</span> <span class="p">%{</span><span class="ss">oner:</span> <span class="p">%</span><span class="no">Owner</span><span class="p">{</span><span class="ss">name:</span> <span class="s2">"Dave"</span><span class="p">}})</span>
<span class="c1"># %Cat{name: "Sylvester", owner: %Owner{name: "Granny"}, weight: 15}</span>
</code></pre></div></div>

<p>There’s a different bug.</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Lens</span><span class="o">.</span><span class="n">set!</span><span class="p">(</span><span class="n">garfield</span><span class="p">,</span> <span class="no">Lens</span><span class="o">.</span><span class="n">path</span><span class="p">([</span><span class="ss">:owner</span><span class="p">,</span> <span class="ss">:nane</span><span class="p">]),</span> <span class="s2">"Dave"</span><span class="p">)</span>
<span class="c1"># (KeyError) key :nane not found in: %{name: "Jon"}</span>
</code></pre></div></div>

<p>Under the Lens contract, our code was wrong, so it raised the error.</p>

<h2 id="more-complex-examples">More Complex Examples</h2>

<p>Let’s make a Superhero domain.</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">defmodule</span> <span class="no">Powers</span> <span class="k">do</span>
  <span class="k">defstruct</span> <span class="p">[</span><span class="ss">:strength</span><span class="p">,</span> <span class="ss">:speed</span><span class="p">,</span> <span class="ss">:intelligence</span><span class="p">]</span>

  <span class="k">def</span> <span class="n">total</span><span class="p">(%</span><span class="no">Powers</span><span class="p">{}</span> <span class="o">=</span> <span class="n">p</span><span class="p">),</span> <span class="k">do</span><span class="p">:</span> <span class="n">p</span><span class="o">.</span><span class="n">strength</span> <span class="o">+</span> <span class="n">p</span><span class="o">.</span><span class="n">speed</span> <span class="o">+</span> <span class="n">p</span><span class="o">.</span><span class="n">intelligence</span>
<span class="k">end</span>

<span class="k">defmodule</span> <span class="no">Hero</span> <span class="k">do</span>
  <span class="k">defstruct</span> <span class="p">[</span><span class="ss">:name</span><span class="p">,</span> <span class="ss">:alias</span><span class="p">,</span> <span class="ss">:powers</span><span class="p">]</span>
<span class="k">end</span>

<span class="k">defmodule</span> <span class="no">Headquarters</span> <span class="k">do</span>
  <span class="nv">@cities</span> <span class="p">%{</span>
    <span class="s2">"New York"</span> <span class="o">=&gt;</span> <span class="p">{</span><span class="mf">40.7128</span><span class="p">,</span> <span class="o">-</span><span class="mf">74.0060</span><span class="p">},</span>
    <span class="s2">"Los Angeles"</span> <span class="o">=&gt;</span> <span class="p">{</span><span class="mf">34.0522</span><span class="p">,</span> <span class="o">-</span><span class="mf">118.2437</span><span class="p">},</span>
    <span class="s2">"San Francisco"</span> <span class="o">=&gt;</span> <span class="p">{</span><span class="mf">37.7749</span><span class="p">,</span> <span class="o">-</span><span class="mf">122.4194</span><span class="p">}</span>
  <span class="p">}</span>

  <span class="k">defstruct</span> <span class="p">[</span><span class="ss">:city</span><span class="p">,</span> <span class="ss">:latitude</span><span class="p">,</span> <span class="ss">:longitude</span><span class="p">]</span>

  <span class="k">def</span> <span class="n">relocate</span><span class="p">(</span><span class="n">city</span><span class="p">)</span> <span class="ow">when</span> <span class="n">is_map_key</span><span class="p">(</span><span class="nv">@cities</span><span class="p">,</span> <span class="n">city</span><span class="p">)</span> <span class="k">do</span>
    <span class="p">{</span><span class="n">lat</span><span class="p">,</span> <span class="n">lon</span><span class="p">}</span> <span class="o">=</span> <span class="no">Map</span><span class="o">.</span><span class="n">fetch!</span><span class="p">(</span><span class="nv">@cities</span><span class="p">,</span> <span class="n">city</span><span class="p">)</span>
    <span class="p">%</span><span class="no">Headquarters</span><span class="p">{</span><span class="ss">city:</span> <span class="n">city</span><span class="p">,</span> <span class="ss">latitude:</span> <span class="n">lat</span><span class="p">,</span> <span class="ss">longitude:</span> <span class="n">lon</span><span class="p">}</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="k">defmodule</span> <span class="no">Team</span> <span class="k">do</span>
  <span class="k">defstruct</span> <span class="p">[</span><span class="ss">:name</span><span class="p">,</span> <span class="ss">:leader</span><span class="p">,</span> <span class="ss">:headquarters</span><span class="p">,</span> <span class="ss">:founded</span><span class="p">]</span>
<span class="k">end</span>
</code></pre></div></div>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">iron_man</span> <span class="o">=</span> <span class="p">%</span><span class="no">Hero</span><span class="p">{</span>
  <span class="ss">name:</span> <span class="s2">"Tony Stark"</span><span class="p">,</span>
  <span class="ss">alias:</span> <span class="s2">"Iron Man"</span><span class="p">,</span>
  <span class="ss">powers:</span> <span class="p">%</span><span class="no">Powers</span><span class="p">{</span>
    <span class="ss">strength:</span> <span class="mi">85</span><span class="p">,</span>
    <span class="ss">speed:</span> <span class="mi">70</span><span class="p">,</span>
    <span class="ss">intelligence:</span> <span class="mi">100</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="n">avengers</span> <span class="o">=</span> <span class="p">%</span><span class="no">Team</span><span class="p">{</span>
  <span class="ss">name:</span> <span class="s2">"Avengers"</span><span class="p">,</span>
  <span class="ss">leader:</span> <span class="p">%</span><span class="no">Hero</span><span class="p">{</span>
    <span class="ss">name:</span> <span class="s2">"Steve Rogers"</span><span class="p">,</span>
    <span class="ss">alias:</span> <span class="s2">"Captain America"</span><span class="p">,</span>
    <span class="ss">powers:</span> <span class="p">%</span><span class="no">Powers</span><span class="p">{</span>
      <span class="ss">strength:</span> <span class="mi">90</span><span class="p">,</span>
      <span class="ss">speed:</span> <span class="mi">75</span><span class="p">,</span>
      <span class="ss">intelligence:</span> <span class="mi">80</span>
    <span class="p">}</span>
  <span class="p">},</span>
  <span class="ss">headquarters:</span> <span class="p">%</span><span class="no">Headquarters</span><span class="p">{</span>
    <span class="ss">city:</span> <span class="s2">"New York"</span><span class="p">,</span>
    <span class="ss">latitude:</span> <span class="mf">40.7128</span><span class="p">,</span>
    <span class="ss">longitude:</span> <span class="o">-</span><span class="mf">74.0060</span>
  <span class="p">},</span>
  <span class="ss">founded:</span> <span class="mi">1963</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="constructors">Constructors</h2>

<p>key/1 - single field focus</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">alias_lens</span> <span class="o">=</span> <span class="no">Lens</span><span class="o">.</span><span class="n">key</span><span class="p">(</span><span class="ss">:alias</span><span class="p">)</span>
<span class="no">Lens</span><span class="o">.</span><span class="n">view!</span><span class="p">(</span><span class="n">iron_man</span><span class="p">,</span> <span class="n">alias_lens</span><span class="p">)</span>
<span class="c1"># "Iron Man"</span>
</code></pre></div></div>

<p>path/1 - nested field access</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">leader_name_lens</span> <span class="o">=</span> <span class="no">Lens</span><span class="o">.</span><span class="n">path</span><span class="p">([</span><span class="ss">:leader</span><span class="p">,</span> <span class="ss">:name</span><span class="p">])</span>
<span class="no">Lens</span><span class="o">.</span><span class="n">view!</span><span class="p">(</span><span class="n">avengers</span><span class="p">,</span> <span class="n">leader_name_lens</span><span class="p">)</span>
<span class="c1"># "Steve Rogers"</span>
</code></pre></div></div>

<h2 id="composition">Composition</h2>

<p>Lenses compose to reach arbitrary depth:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">leader_lens</span> <span class="o">=</span> <span class="no">Lens</span><span class="o">.</span><span class="n">key</span><span class="p">(</span><span class="ss">:leader</span><span class="p">)</span>
<span class="n">powers_lens</span> <span class="o">=</span> <span class="no">Lens</span><span class="o">.</span><span class="n">key</span><span class="p">(</span><span class="ss">:powers</span><span class="p">)</span>
<span class="n">intelligence_lens</span> <span class="o">=</span> <span class="no">Lens</span><span class="o">.</span><span class="n">key</span><span class="p">(</span><span class="ss">:intelligence</span><span class="p">)</span>

<span class="n">leader_intelligence</span> <span class="o">=</span>
  <span class="n">leader_lens</span>
  <span class="o">|&gt;</span> <span class="no">Lens</span><span class="o">.</span><span class="n">compose</span><span class="p">(</span><span class="n">powers_lens</span><span class="p">)</span>
  <span class="o">|&gt;</span> <span class="no">Lens</span><span class="o">.</span><span class="n">compose</span><span class="p">(</span><span class="n">intelligence_lens</span><span class="p">)</span>

<span class="no">Lens</span><span class="o">.</span><span class="n">view!</span><span class="p">(</span><span class="n">avengers</span><span class="p">,</span> <span class="n">leader_intelligence</span><span class="p">)</span>
<span class="c1"># 80</span>
</code></pre></div></div>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">avengers</span>
<span class="o">|&gt;</span> <span class="no">Lens</span><span class="o">.</span><span class="n">set!</span><span class="p">(</span><span class="n">leader_intelligence</span><span class="p">,</span> <span class="mi">195</span><span class="p">)</span>
<span class="o">|&gt;</span> <span class="no">Lens</span><span class="o">.</span><span class="n">view!</span><span class="p">(</span><span class="no">Lens</span><span class="o">.</span><span class="n">compose</span><span class="p">(</span><span class="n">leader_lens</span><span class="p">,</span> <span class="n">powers_lens</span><span class="p">))</span>
<span class="c1"># %Powers{strength: 90, speed: 75, intelligence: 195}</span>
</code></pre></div></div>

<p>We can also use a <code class="language-plaintext highlighter-rouge">compose/1</code> with a list:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">leader_intelligence_lens</span> <span class="o">=</span> <span class="no">Lens</span><span class="o">.</span><span class="n">compose</span><span class="p">([</span>
  <span class="no">Lens</span><span class="o">.</span><span class="n">key</span><span class="p">(</span><span class="ss">:leader</span><span class="p">),</span>
  <span class="no">Lens</span><span class="o">.</span><span class="n">key</span><span class="p">(</span><span class="ss">:powers</span><span class="p">),</span>
  <span class="no">Lens</span><span class="o">.</span><span class="n">key</span><span class="p">(</span><span class="ss">:intelligence</span><span class="p">)</span>
<span class="p">])</span>

<span class="no">Lens</span><span class="o">.</span><span class="n">view!</span><span class="p">(</span><span class="n">avengers</span><span class="p">,</span> <span class="n">leader_intelligence_lens</span><span class="p">)</span>
<span class="c1"># 80</span>
</code></pre></div></div>

<p>Or use <code class="language-plaintext highlighter-rouge">path/1</code> as shorthand, which leverages <code class="language-plaintext highlighter-rouge">compose</code> behind the scenes:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Lens</span><span class="o">.</span><span class="n">view!</span><span class="p">(</span><span class="n">avengers</span><span class="p">,</span> <span class="no">Lens</span><span class="o">.</span><span class="n">path</span><span class="p">([</span><span class="ss">:leader</span><span class="p">,</span> <span class="ss">:powers</span><span class="p">,</span> <span class="ss">:intelligence</span><span class="p">]))</span>
<span class="c1"># 80</span>
</code></pre></div></div>

<p>Composition scales to arbitrary depth while preserving lawfulness. Whether you use <code class="language-plaintext highlighter-rouge">compose/2</code>, <code class="language-plaintext highlighter-rouge">compose/1</code>, or <code class="language-plaintext highlighter-rouge">path/1</code>, every lens maintains the same contract: symmetric, total, and type-preserving.</p>

<p>And honestly, if you know this much, you know enough. Feel free to stop here.</p>

<h2 id="the-blue-pill-advanced-lenses">The Blue Pill: Advanced Lenses</h2>

<p>So far we’ve used the <code class="language-plaintext highlighter-rouge">key/1</code> and <code class="language-plaintext highlighter-rouge">path/1</code> constructors. Behind the scenes, both are thin wrappers over <code class="language-plaintext highlighter-rouge">make/2</code> that generate simple structural lenses.</p>

<p>But not every update is a simple structural write.</p>

<p>What happens when a single logical change must update multiple fields together?</p>

<p>Let’s start by defining some basic lenses:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">headquarters_lens</span> <span class="o">=</span> <span class="no">Lens</span><span class="o">.</span><span class="n">key</span><span class="p">(</span><span class="ss">:headquarters</span><span class="p">)</span>
<span class="n">city_lens</span> <span class="o">=</span> <span class="no">Lens</span><span class="o">.</span><span class="n">compose</span><span class="p">(</span><span class="n">headquarters_lens</span><span class="p">,</span> <span class="no">Lens</span><span class="o">.</span><span class="n">key</span><span class="p">(</span><span class="ss">:city</span><span class="p">))</span>
</code></pre></div></div>

<p>With this lens, we can change the city:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">avengers</span>
<span class="o">|&gt;</span> <span class="no">Lens</span><span class="o">.</span><span class="n">set!</span><span class="p">(</span><span class="n">city_lens</span><span class="p">,</span> <span class="s2">"Los Angeles"</span><span class="p">)</span>
<span class="o">|&gt;</span> <span class="no">Lens</span><span class="o">.</span><span class="n">view!</span><span class="p">(</span><span class="n">headquarters_lens</span><span class="p">)</span>
<span class="c1"># %Headquarters{city: "Los Angeles", latitude: 40.7128, longitude: -74.0060}</span>
</code></pre></div></div>

<p>Structurally, this works, but it is semantically wrong. The city is updated, while the latitude and longitude still point to New York. We have violated one of our domain invariants with a lawful structural operation.</p>

<p>We need a lens which <em>focuses</em> the city, but <em>atomically</em> sets the entire headquarters.</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">relocate_lens</span> <span class="o">=</span>
<span class="no">Lens</span><span class="o">.</span><span class="n">make</span><span class="p">(</span>
  <span class="k">fn</span> <span class="n">team</span> <span class="o">-&gt;</span>
    <span class="no">Lens</span><span class="o">.</span><span class="n">view!</span><span class="p">(</span><span class="n">team</span><span class="p">,</span> <span class="n">city_lens</span><span class="p">)</span>
  <span class="k">end</span><span class="p">,</span>
  <span class="k">fn</span> <span class="n">team</span><span class="p">,</span> <span class="n">new_city</span> <span class="o">-&gt;</span>
    <span class="no">Lens</span><span class="o">.</span><span class="n">set!</span><span class="p">(</span><span class="n">team</span><span class="p">,</span> <span class="n">headquarters_lens</span><span class="p">,</span> <span class="no">Headquarters</span><span class="o">.</span><span class="n">relocate</span><span class="p">(</span><span class="n">new_city</span><span class="p">))</span>
  <span class="k">end</span>
<span class="p">)</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">make/2</code> is the core of a lens. It binds two directions:</p>

<ul>
  <li>how the focus is read</li>
  <li>how the structure is rebuilt when the focus changes</li>
</ul>

<p>Both sides must agree on the same logical value. Here, that value is the city.</p>

<p>With this, relocation is a single lawful operation:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">avengers</span>
<span class="o">|&gt;</span> <span class="no">Lens</span><span class="o">.</span><span class="n">set!</span><span class="p">(</span><span class="n">relocate_lens</span><span class="p">,</span> <span class="s2">"Los Angeles"</span><span class="p">)</span>
<span class="c1"># %Team{</span>
<span class="c1">#      name: "Avengers",</span>
<span class="c1">#      leader: %Hero{...},</span>
<span class="c1">#      headquarters: %Headquarters{city: "Los Angeles", latitude: 34.0522, longitude: -118.2437},</span>
<span class="c1">#      founded: 1963</span>
<span class="c1">#    }</span>
</code></pre></div></div>

<p>The headquarters, including city, latitude, and longitude, is updated as one atomic rewrite.</p>

<p>Because this is still a lens, it composes like any other:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">avengers</span>
<span class="o">|&gt;</span> <span class="no">Lens</span><span class="o">.</span><span class="n">set!</span><span class="p">(</span><span class="n">relocate_lens</span><span class="p">,</span> <span class="s2">"San Francisco"</span><span class="p">)</span>
<span class="o">|&gt;</span> <span class="no">Lens</span><span class="o">.</span><span class="n">set!</span><span class="p">(</span><span class="no">Lens</span><span class="o">.</span><span class="n">key</span><span class="p">(</span><span class="ss">:name</span><span class="p">),</span> <span class="s2">"West Coast Avengers"</span><span class="p">)</span>
<span class="c1"># %Team{</span>
<span class="c1">#      name: "West Coast Avengers",</span>
<span class="c1">#      leader: %Hero{...},</span>
<span class="c1">#      headquarters: %Headquarters{city: "San Francisco", latitude: 37.7749, longitude: -122.4194},</span>
<span class="c1">#      founded: 1963</span>
<span class="c1">#    }</span>
</code></pre></div></div>

<p>And we can even use Either’s DSL to create a safe pipe:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">use</span> <span class="no">Funx</span><span class="o">.</span><span class="no">Monad</span><span class="o">.</span><span class="no">Either</span>

<span class="n">either</span> <span class="n">avengers</span><span class="p">,</span> <span class="ss">as:</span> <span class="ss">:tuple</span> <span class="k">do</span>
  <span class="n">bind</span> <span class="no">Lens</span><span class="o">.</span><span class="n">set</span><span class="p">(</span><span class="n">relocate_lens</span><span class="p">,</span> <span class="s2">"San Francisco"</span><span class="p">)</span>
  <span class="n">bind</span> <span class="no">Lens</span><span class="o">.</span><span class="n">set</span><span class="p">(</span><span class="no">Lens</span><span class="o">.</span><span class="n">key</span><span class="p">(</span><span class="ss">:name</span><span class="p">),</span> <span class="s2">"West Coast Avengers"</span><span class="p">)</span>
  <span class="n">bind</span> <span class="no">Lens</span><span class="o">.</span><span class="n">set</span><span class="p">(</span><span class="n">leader_intelligence</span><span class="p">,</span> <span class="mi">195</span><span class="p">)</span>
  <span class="n">bind</span> <span class="no">Lens</span><span class="o">.</span><span class="n">set</span><span class="p">(</span><span class="no">Lens</span><span class="o">.</span><span class="n">path</span><span class="p">([</span><span class="ss">:leader</span><span class="p">,</span> <span class="ss">:alias</span><span class="p">]),</span> <span class="s2">"Cap"</span><span class="p">)</span>
<span class="k">end</span>
<span class="c1"># {:ok,</span>
<span class="c1">#  %Team{</span>
<span class="c1">#    name: "West Coast Avengers",</span>
<span class="c1">#    leader: %Hero{</span>
<span class="c1">#      name: "Steve Rogers",</span>
<span class="c1">#      alias: "Cap",</span>
<span class="c1">#      powers: %Powers{strength: 90, speed: 75, intelligence: 195}</span>
<span class="c1">#    },</span>
<span class="c1">#    headquarters: %Headquarters{city: "San Francisco", latitude: 37.7749, longitude: -122.4194},</span>
<span class="c1">#    founded: 1963</span>
<span class="c1"># }}</span>
</code></pre></div></div>

<p>Which will report its first error:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">either</span> <span class="n">avengers</span><span class="p">,</span> <span class="ss">as:</span> <span class="ss">:tuple</span> <span class="k">do</span>
  <span class="n">bind</span> <span class="no">Lens</span><span class="o">.</span><span class="n">set</span><span class="p">(</span><span class="n">relocate_lens</span><span class="p">,</span> <span class="s2">"San Francisco"</span><span class="p">)</span>
  <span class="n">bind</span> <span class="no">Lens</span><span class="o">.</span><span class="n">set</span><span class="p">(</span><span class="no">Lens</span><span class="o">.</span><span class="n">key</span><span class="p">(</span><span class="ss">:nane</span><span class="p">),</span> <span class="s2">"West Coast Avengers"</span><span class="p">)</span>
  <span class="n">bind</span> <span class="no">Lens</span><span class="o">.</span><span class="n">set</span><span class="p">(</span><span class="n">leader_intelligence</span><span class="p">,</span> <span class="mi">195</span><span class="p">)</span>
  <span class="n">bind</span> <span class="no">Lens</span><span class="o">.</span><span class="n">set</span><span class="p">(</span><span class="no">Lens</span><span class="o">.</span><span class="n">path</span><span class="p">([</span><span class="ss">:leader</span><span class="p">,</span> <span class="ss">:alias</span><span class="p">]),</span> <span class="s2">"Cap"</span><span class="p">)</span>
<span class="k">end</span>
<span class="c1"># {:error, %KeyError{key: :nane, term: %Team{...}}}</span>
</code></pre></div></div>

<h3 id="derived-values-as-fields">Derived Values as Fields</h3>

<p>Not every field we care about is explicitly stored. Some values are <em>derived</em> from multiple fields but still behave like a single domain concept.</p>

<p>Here, a hero’s effective power is derived from three fields: <code class="language-plaintext highlighter-rouge">strength</code>, <code class="language-plaintext highlighter-rouge">speed</code>, and <code class="language-plaintext highlighter-rouge">intelligence</code>. We want to observe that value as a single focus before we decide how it should be rewritten.</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">strength_lens</span> <span class="o">=</span> <span class="no">Lens</span><span class="o">.</span><span class="n">key</span><span class="p">(</span><span class="ss">:strength</span><span class="p">)</span>
<span class="n">speed_lens</span> <span class="o">=</span> <span class="no">Lens</span><span class="o">.</span><span class="n">key</span><span class="p">(</span><span class="ss">:speed</span><span class="p">)</span>
<span class="n">intelligence_lens</span> <span class="o">=</span> <span class="no">Lens</span><span class="o">.</span><span class="n">key</span><span class="p">(</span><span class="ss">:intelligence</span><span class="p">)</span>

<span class="n">power_level</span> <span class="o">=</span> <span class="k">fn</span> <span class="n">powers</span> <span class="o">-&gt;</span>
  <span class="no">Lens</span><span class="o">.</span><span class="n">view!</span><span class="p">(</span><span class="n">powers</span><span class="p">,</span> <span class="n">strength_lens</span><span class="p">)</span> <span class="o">+</span>
  <span class="no">Lens</span><span class="o">.</span><span class="n">view!</span><span class="p">(</span><span class="n">powers</span><span class="p">,</span> <span class="n">speed_lens</span><span class="p">)</span> <span class="o">+</span>
  <span class="no">Lens</span><span class="o">.</span><span class="n">view!</span><span class="p">(</span><span class="n">powers</span><span class="p">,</span> <span class="n">intelligence_lens</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div></div>

<p>This function is read-only. It defines how the derived value is computed from the underlying structure.</p>

<p>Next, we define the inverse operation: how a new derived total is <em>redistributed</em> back by proportionally scaling each field.</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">scale_powers</span> <span class="o">=</span> <span class="k">fn</span> <span class="n">powers</span><span class="p">,</span> <span class="n">new_total</span> <span class="o">-&gt;</span>
  <span class="n">old_total</span> <span class="o">=</span> <span class="n">power_level</span><span class="o">.</span><span class="p">(</span><span class="n">powers</span><span class="p">)</span>
  <span class="n">ratio</span> <span class="o">=</span> <span class="n">new_total</span> <span class="o">/</span> <span class="n">old_total</span>

  <span class="n">strength</span> <span class="o">=</span> <span class="n">round</span><span class="p">(</span><span class="n">powers</span><span class="o">.</span><span class="n">strength</span> <span class="o">*</span> <span class="n">ratio</span><span class="p">)</span>
  <span class="n">speed</span> <span class="o">=</span> <span class="n">round</span><span class="p">(</span><span class="n">powers</span><span class="o">.</span><span class="n">speed</span> <span class="o">*</span> <span class="n">ratio</span><span class="p">)</span>

  <span class="n">intelligence</span> <span class="o">=</span>
    <span class="n">new_total</span> <span class="o">-</span> <span class="n">strength</span> <span class="o">-</span> <span class="n">speed</span>

  <span class="p">%</span><span class="no">Powers</span><span class="p">{}</span>
  <span class="o">|&gt;</span> <span class="no">Lens</span><span class="o">.</span><span class="n">set!</span><span class="p">(</span><span class="no">Lens</span><span class="o">.</span><span class="n">key</span><span class="p">(</span><span class="ss">:strength</span><span class="p">),</span> <span class="n">strength</span><span class="p">)</span>
  <span class="o">|&gt;</span> <span class="no">Lens</span><span class="o">.</span><span class="n">set!</span><span class="p">(</span><span class="no">Lens</span><span class="o">.</span><span class="n">key</span><span class="p">(</span><span class="ss">:speed</span><span class="p">),</span> <span class="n">speed</span><span class="p">)</span>
  <span class="o">|&gt;</span> <span class="no">Lens</span><span class="o">.</span><span class="n">set!</span><span class="p">(</span><span class="no">Lens</span><span class="o">.</span><span class="n">key</span><span class="p">(</span><span class="ss">:intelligence</span><span class="p">),</span> <span class="n">intelligence</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Now we bind those two directions together using <code class="language-plaintext highlighter-rouge">make/2</code>.</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">power_level_lens</span> <span class="o">=</span>
  <span class="no">Lens</span><span class="o">.</span><span class="n">make</span><span class="p">(</span>
    <span class="k">fn</span> <span class="n">hero</span> <span class="o">-&gt;</span>
      <span class="n">hero</span>
      <span class="o">|&gt;</span> <span class="no">Lens</span><span class="o">.</span><span class="n">view!</span><span class="p">(</span><span class="n">powers_lens</span><span class="p">)</span>
      <span class="o">|&gt;</span> <span class="n">power_level</span><span class="o">.</span><span class="p">()</span>
    <span class="k">end</span><span class="p">,</span>
    <span class="k">fn</span> <span class="n">hero</span><span class="p">,</span> <span class="n">new_level</span> <span class="o">-&gt;</span>
      <span class="n">current_powers</span> <span class="o">=</span> <span class="no">Lens</span><span class="o">.</span><span class="n">view!</span><span class="p">(</span><span class="n">hero</span><span class="p">,</span> <span class="n">powers_lens</span><span class="p">)</span>
      <span class="no">Lens</span><span class="o">.</span><span class="n">set!</span><span class="p">(</span>
        <span class="n">hero</span><span class="p">,</span>
        <span class="n">powers_lens</span><span class="p">,</span>
        <span class="n">scale_powers</span><span class="o">.</span><span class="p">(</span><span class="n">current_powers</span><span class="p">,</span> <span class="n">new_level</span><span class="p">)</span>
      <span class="p">)</span>
    <span class="k">end</span>
  <span class="p">)</span>
</code></pre></div></div>

<p>This lens now behaves like a field:</p>

<ul>
  <li>the viewer derives a value</li>
  <li>the setter performs a coordinated multi-field rewrite</li>
  <li>both sides agree on the same logical focus</li>
</ul>

<p>We can read it:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Lens</span><span class="o">.</span><span class="n">view!</span><span class="p">(</span><span class="n">iron_man</span><span class="p">,</span> <span class="n">power_level_lens</span><span class="p">)</span>
<span class="c1"># 255</span>
</code></pre></div></div>

<p>And we can update it:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">iron_man</span>
<span class="o">|&gt;</span> <span class="no">Lens</span><span class="o">.</span><span class="n">set!</span><span class="p">(</span><span class="n">power_level_lens</span><span class="p">,</span> <span class="mi">50</span><span class="p">)</span>
<span class="o">|&gt;</span> <span class="no">Lens</span><span class="o">.</span><span class="n">view!</span><span class="p">(</span><span class="n">power_level_lens</span><span class="p">)</span>
<span class="c1"># 50</span>
</code></pre></div></div>

<p>From the call site, there is no distinction between this derived value and a stored field. The difference is not in how it is used. The difference is that the lens enforces a coordinated rewrite instead of a simple assignment.</p>

<p>And don’t forget, if we mess up and try to set the power level for our Garfield map:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">garfield</span>
<span class="o">|&gt;</span> <span class="no">Lens</span><span class="o">.</span><span class="n">set!</span><span class="p">(</span><span class="n">power_level_lens</span><span class="p">,</span> <span class="mi">50</span><span class="p">)</span>
<span class="c1"># ** (KeyError) key :powers not found in: %{name: "Garfield", weight: 20, owner: %{name: "Jon"}}</span>
</code></pre></div></div>

<p>We get that <code class="language-plaintext highlighter-rouge">KeyError</code>.</p>

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

<p>Elixir makes nested updates convenient. Funx makes them correct. The difference is lawfulness: symmetric contracts that catch bugs at the call site instead of downstream, work uniformly on maps and structs, and compose without surprises. As your codebase grows, that predictability compounds.</p>

<h2 id="resources">Resources</h2>

<div style="display: flex; gap: 20px; margin: 20px 0;">
  <div style="flex-shrink: 0;">
    <a href="https://pragprog.com/titles/jkelixir/advanced-functional-programming-with-elixir">
      <img src="/assets/images/jkelixir_small.jpg" alt="Advanced Functional Programming with Elixir book cover" width="150" />
    </a>
  </div>
  <div>
    <h3><a href="https://pragprog.com/titles/jkelixir/advanced-functional-programming-with-elixir">Advanced Functional Programming with Elixir</a></h3>
    <p>Dive deeper into functional programming patterns and advanced Elixir techniques. Learn how to build robust, maintainable applications using functional programming principles.</p>
  </div>
</div>

<div style="display: flex; gap: 20px; margin: 20px 0;">
  <div style="flex-shrink: 0;">
    <a href="https://www.funxlib.com">
      <img src="/assets/images/funx-social.jpg" alt="Funx functional programming library" width="150" />
    </a>
  </div>
  <div>
    <h3><a href="https://www.funxlib.com">Funx - Functional Programming for Elixir</a></h3>
    <p>A library of functional programming abstractions for Elixir, including monads, monoids, Eq, Ord, and more. Built as an ecosystem where learning is the priority from the start.</p>
  </div>
</div>]]></content><author><name>Joseph Koski</name></author><category term="elixir" /><category term="ash" /><category term="funx" /><summary type="html"><![CDATA[“I didn’t say it would be easy. I just said it would be the truth.” —Morpheus, The Matrix (1999)]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.joekoski.com/assets/images/jkelixir_small.jpg" /><media:content medium="image" url="https://www.joekoski.com/assets/images/jkelixir_small.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>