<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Mat Weiss</title>
    <description>The latest articles on DEV Community by Mat Weiss (@matweiss).</description>
    <link>https://dev.to/matweiss</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F994946%2F2107b19e-bba0-4076-94bd-dbd1e6e7f2a9.png</url>
      <title>DEV Community: Mat Weiss</title>
      <link>https://dev.to/matweiss</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/matweiss"/>
    <language>en</language>
    <item>
      <title>When Step Five Fails, Undo Steps One Through Four</title>
      <dc:creator>Mat Weiss</dc:creator>
      <pubDate>Tue, 16 Jun 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/matweiss/when-step-five-fails-undo-steps-one-through-four-48g6</link>
      <guid>https://dev.to/matweiss/when-step-five-fails-undo-steps-one-through-four-48g6</guid>
      <description>&lt;p&gt;The previous articles built up a picture of what a compiler can verify about individual operations: their outcomes, what data they can see, what state they require. The operations article introduced N-track pipelines where every outcome gets handled and errors can accumulate along the way. If every step is a pure transformation, that model is sufficient — errors collect, the caller handles them, nothing external has changed.&lt;/p&gt;

&lt;p&gt;But real procedures aren't pure transformations. An away mission involves assembling the team, briefing them, beaming them down, establishing contact, collecting samples, and beaming them back. Each step commits real effects — crew assignments change, transporter logs update, communication channels open. When step four fails, steps one through three have already happened. Accumulated errors tell you what went wrong. They don't undo what already happened.&lt;/p&gt;

&lt;p&gt;I explored error recovery for effectful pipelines — steps that commit external state, not just transform data. The answer was compensation: each forward step paired, when possible, with the action that reverses or settles it. Ruuk's &lt;code&gt;saga&lt;/code&gt; keyword makes that pairing a first-class part of the declaration.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: Distributed Commitment
&lt;/h2&gt;

&lt;p&gt;The challenge isn't failure itself. The challenge is &lt;em&gt;partial&lt;/em&gt; failure after &lt;em&gt;partial&lt;/em&gt; commitment.&lt;/p&gt;

&lt;p&gt;A pipeline handles failure cleanly when no step has committed external state. If step three returns a failure outcome, steps one and two produced data that's still local — nothing external has changed. The failure is just an outcome, not a cleanup problem.&lt;/p&gt;

&lt;p&gt;But starship operations commit to external systems. Beaming a team down changes their location in the ship's records. Opening a communication channel allocates subspace bandwidth. Logging samples in the science database creates records other systems depend on. These aren't local transformations — they're effects that persist whether the procedure succeeds or not.&lt;/p&gt;

&lt;p&gt;When &lt;code&gt;establishContact&lt;/code&gt; fails after &lt;code&gt;beamDownTeam&lt;/code&gt; succeeded, the team is on the planet surface with no confirmed communication link. The correct response is to beam them back up — but that compensation logic lives in a catch block somewhere, and whether it matches the current beam-down procedure depends on whether someone updated both when the procedure changed.&lt;/p&gt;

&lt;p&gt;The standard approach: execute each step, check the result, compensate on failure.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;conductAwayMission&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mission&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;team&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;planet&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;assigned&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;assembleTeam&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;team&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mission&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;assigned&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;missionFailed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;assigned&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;briefed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;briefTeam&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;assigned&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;team&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mission&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;briefed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;releaseTeam&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;assigned&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;team&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;missionFailed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;briefed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;landed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;beamDown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;briefed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;team&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;planet&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;landed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;debriefTeam&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;briefed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;team&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;releaseTeam&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;assigned&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;team&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;missionFailed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;landed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;contact&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;establishContact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;landed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;team&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;planet&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;contact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;beamUp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;landed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;team&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;debriefTeam&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;briefed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;team&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;releaseTeam&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;assigned&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;team&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;missionFailed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;contact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// ... remaining steps&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is competent, readable code. It handles each failure and compensates correctly. But the compensation list grows with every step — each failure handler must repeat every previous compensation in reverse order. Add a step between &lt;code&gt;briefTeam&lt;/code&gt; and &lt;code&gt;beamDown&lt;/code&gt; and every subsequent handler needs updating. The relationship between a forward step and its compensation is implicit: &lt;code&gt;releaseTeam&lt;/code&gt; undoes &lt;code&gt;assembleTeam&lt;/code&gt;, but you learn that by reading the error handler, not the step declaration. And nothing verifies that every effectful step has a corresponding undo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sagas as Declarations
&lt;/h2&gt;

&lt;p&gt;A saga declares steps in order; each step that commits external state declares its compensating operation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pub saga ConductAwayMission =
    subject mission: Mission&amp;lt;Approved&amp;gt;
    payload team: List&amp;lt;CrewMember&amp;gt;
    payload planet: Planet

    step assembleTeam
        compensate releaseTeam

    step briefTeam
        compensate debriefTeam

    step launchMission
        compensate abortMission
        performs Mission.Approved -&amp;gt; Mission.InProgress

    step beamDown
        compensate beamUp

    step establishContact

    step collectSamples

    step beamUp

    step completeMission
        performs Mission.InProgress -&amp;gt; Mission.Completed

    outcomes =
        | MissionComplete of Mission&amp;lt;Completed&amp;gt;
        | ContactFailed of reason: String
        | TransporterFailure of TransporterError
        | TeamUnavailable of missing: List&amp;lt;String&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read it as a mission briefing: assemble the team (if it fails later, release them), brief them (debrief if needed), launch the mission, beam them down (beam them back up if something goes wrong), establish contact, collect samples, beam up, and complete the mission with a state transition from &lt;code&gt;InProgress&lt;/code&gt; to &lt;code&gt;Completed&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;establishContact&lt;/code&gt; and &lt;code&gt;collectSamples&lt;/code&gt; have no &lt;code&gt;compensate&lt;/code&gt; clause. &lt;code&gt;establishContact&lt;/code&gt; is a read/verify step — if it fails, there's nothing to undo beyond the earlier compensations. &lt;code&gt;collectSamples&lt;/code&gt; is the forward payload of the mission — if sample collection fails, the earlier steps still need unwinding, but "uncollecting" samples isn't a meaningful operation.&lt;/p&gt;

&lt;p&gt;The final &lt;code&gt;beamUp&lt;/code&gt; step (the planned return, not the compensation) and &lt;code&gt;completeMission&lt;/code&gt; also lack compensation — by the time you reach them, the mission has substantively succeeded. &lt;code&gt;completeMission&lt;/code&gt; performs the typestate transition from article 5, moving the mission from &lt;code&gt;InProgress&lt;/code&gt; to &lt;code&gt;Completed&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The saga's &lt;code&gt;outcomes&lt;/code&gt; block declares the saga-level results — the outcomes a caller must handle. When a step fails, the saga unwinds completed steps that declared compensation and surfaces the failure as the appropriate saga outcome.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automatic Compensation on Failure
&lt;/h2&gt;

&lt;p&gt;If &lt;code&gt;establishContact&lt;/code&gt; fails after &lt;code&gt;beamDown&lt;/code&gt;, &lt;code&gt;launchMission&lt;/code&gt;, &lt;code&gt;briefTeam&lt;/code&gt;, and &lt;code&gt;assembleTeam&lt;/code&gt; have all succeeded, the saga unwinds automatically in reverse order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;beamUp&lt;/code&gt; — compensates &lt;code&gt;beamDown&lt;/code&gt; (team is returned to the ship)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;abortMission&lt;/code&gt; — compensates &lt;code&gt;launchMission&lt;/code&gt; (mission is settled as aborted)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;debriefTeam&lt;/code&gt; — compensates &lt;code&gt;briefTeam&lt;/code&gt; (team status is reset)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;releaseTeam&lt;/code&gt; — compensates &lt;code&gt;assembleTeam&lt;/code&gt; (crew assignments are freed)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Last completed, first compensated. The compensation order is the reverse of the execution order — the same principle as unwinding a call stack, but applied to domain operations with real-world effects.&lt;/p&gt;

&lt;p&gt;The developer doesn't write this unwind logic. The saga declaration defines it. The compensation for &lt;code&gt;beamDown&lt;/code&gt; is &lt;code&gt;beamUp&lt;/code&gt;, declared on the same line. The relationship between forward step and undo is visible, explicit, and maintained in one place.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Compiler Verifies
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Compensation completeness.&lt;/strong&gt; Every step that calls a mutating operation should declare a compensating action. The compiler warns on steps that modify external state without a &lt;code&gt;compensate&lt;/code&gt; clause — not an error, because some mutations genuinely can't be undone (you can't un-send a transmission), but a signal that the developer should make that judgment explicitly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Operation existence.&lt;/strong&gt; Each operation named in &lt;code&gt;step&lt;/code&gt; and &lt;code&gt;compensate&lt;/code&gt; must exist in scope. You can't reference a compensation operation that hasn't been defined.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Performs consistency.&lt;/strong&gt; If a saga step uses &lt;code&gt;performs&lt;/code&gt;, the same validation rules from the typestate article apply: the subject parameter must match the source state, the success outcome must match the target state, no mismatched transitions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Order guarantees.&lt;/strong&gt; Compensation executes in reverse execution order by definition. The saga declaration makes this explicit — reading the steps top to bottom tells you the compensation order bottom to top. That leaves less runtime coordination logic to get wrong.&lt;/p&gt;

&lt;p&gt;An agent generating a saga gets the same guardrails. The compiler warns on uncompensated mutations whether a human or an agent wrote the declaration — the structural check doesn't depend on who authored the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Richer Example: Ship Repair Workflow
&lt;/h2&gt;

&lt;p&gt;Away missions are dramatic but linear. Ship repairs show how sagas handle more complex workflows with multiple external system interactions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pub saga RepairCriticalSystem =
    payload system: ShipSystem
    payload damage: DamageReport
    by chief: CrewMember

    step assessDamage

    step allocateRepairTeam
        compensate releaseRepairTeam

    step requisitionParts from supplyStore
        compensate returnParts to supplyStore

    step takeSectionOffline
        compensate bringBackOnline

    step performRepair

    step runDiagnostic

    step certifyRepair
        performs RepairOrder.InProgress -&amp;gt; RepairOrder.Completed

    outcomes =
        | Repaired of RepairOrder&amp;lt;Completed&amp;gt;
        | PartsUnavailable of needed: List&amp;lt;PartId&amp;gt;
        | DiagnosticFailed of failures: List&amp;lt;String&amp;gt;
        | SectionCritical of reason: String
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each forward step reads naturally with its compensation: allocate a team / release the team, requisition parts / return the parts, take the section offline / bring it back online. The parameter roles from article 3 appear in the steps — &lt;code&gt;requisitionParts from supplyStore&lt;/code&gt; — making the saga declaration read like a procedure manual.&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;runDiagnostic&lt;/code&gt; fails after the repair is done, the saga unwinds: bring the section back online, return the parts, release the team. The repair itself may need manual intervention — there's no &lt;code&gt;compensate&lt;/code&gt; on &lt;code&gt;performRepair&lt;/code&gt; because "un-repairing" isn't a meaningful action. That's a deliberate design choice, visible in the declaration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sagas vs. Pipelines
&lt;/h2&gt;

&lt;p&gt;Both sagas and pipelines compose sequential operations. The distinction matters:&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;pipeline&lt;/strong&gt; passes data forward. Each step transforms the previous step's result. N-track pipelines can accumulate multiple errors along the way, but the model assumes no step has committed external state — failures are outcomes to report, not effects to undo.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;saga&lt;/strong&gt; coordinates effects. Each step may commit external state — a transport, a database write, a resource allocation. If a later step fails, committed state must be undone. The saga manages the compensation stack.&lt;/p&gt;

&lt;p&gt;Use a pipeline when steps are pure transformations or when a single system handles rollback automatically (a database transaction). Use a saga when you're coordinating across multiple systems that don't share a transaction boundary — a transporter system, a personnel database, a supply chain, a communication array.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Sagas Complete
&lt;/h2&gt;

&lt;p&gt;The previous article ended with "What Compounds" — each feature was designed for practical relief and arrived at structural correctness. Sagas extend the pattern one more time. I was exploring error recovery in effectful pipelines, and what started as a way to declare compensation alongside forward steps turned into compiler-verified workflow integrity.&lt;/p&gt;

&lt;p&gt;With scattered compensation logic, the answer to "what happens if the transporter fails mid-mission?" is: "our error handlers call the right cleanup functions." That answer depends on every handler being correct, complete, and synchronized with the forward path. With a saga declaration, the answer is the declaration itself: &lt;code&gt;beamDown&lt;/code&gt; has &lt;code&gt;compensate beamUp&lt;/code&gt;. When someone adds a step, the syntax prompts the question "does this step need compensation?" — and the compiler warns if a mutating step omits one. The forward path and the compensation path live together, so they evolve together.&lt;/p&gt;

&lt;h2&gt;
  
  
  Series Conclusion
&lt;/h2&gt;

&lt;p&gt;This series has built up a picture of what a compiler could verify about the code agents write and humans review.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Operations and outcomes&lt;/strong&gt; (article 3) give the compiler visibility into what an operation means — not just its return type, but its domain-specific results. The compiler holds every caller to every outcome.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Projections&lt;/strong&gt; (article 4) give the compiler control over what each operation can see. Data access boundaries are structural properties of the type, not runtime filters maintained separately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Typestate&lt;/strong&gt; (article 5) gives the compiler control over when operations can run. State preconditions are in the type system, not in runtime guards. Invalid transitions are compile errors.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sagas&lt;/strong&gt; (this article) give the compiler visibility into multi-step workflows and their compensation. The forward path and the undo path are declared together, maintained together, and verified together.&lt;/p&gt;

&lt;p&gt;Each feature stands on its own, but the compounding is the point. An operation with typed outcomes, scoped to a projected data view, guarded by typestate, coordinated within a saga — that's a level of structural verification that testing and code review alone don't reliably achieve. The compiler holds invariants that humans struggle to keep in working memory. In an era where agents write code at volume and humans review it under time pressure, that starts to look less like a nice-to-have and more like part of the architecture of trust.&lt;/p&gt;

&lt;p&gt;Ruuk is in &lt;a href="https://github.com/ruuk-lang/ruuk/releases" rel="noopener noreferrer"&gt;alpha&lt;/a&gt;. The syntax will continue to evolve and the implementation has a long road ahead. But the design criteria — compiler-visible domain semantics, structural enforcement over behavioral convention, progressive development that doesn't sacrifice rigor — are the properties I think the agentic era makes newly important. If these ideas resonate, give ruuk a spin; follow along on &lt;a href="https://github.com/ruuk-lang/ruuk" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; and weigh in on the discussions. The best languages get shaped by the people who care about the problems they solve.&lt;/p&gt;

&lt;p&gt;This article was created with the help of AI&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>architecture</category>
      <category>software</category>
    </item>
    <item>
      <title>You Can't Launch What Hasn't Been Approved</title>
      <dc:creator>Mat Weiss</dc:creator>
      <pubDate>Tue, 09 Jun 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/matweiss/you-cant-launch-what-hasnt-been-approved-17g2</link>
      <guid>https://dev.to/matweiss/you-cant-launch-what-hasnt-been-approved-17g2</guid>
      <description>&lt;p&gt;I designed resources because I was tired of reasoning about async I/O.&lt;/p&gt;

&lt;p&gt;Every developer knows the ritual: open a connection, do work, close the connection. Handle the case where the open failed. Handle the case where you're writing to something that isn't ready yet. Handle the case where you close something twice. Add async to the mix and the state space multiplies — is the request in flight? Has the response arrived? Am I holding a promise or a result?&lt;/p&gt;

&lt;p&gt;The logic is a state machine, and most developers could draw it on a whiteboard in thirty seconds. But the compiler can't see the whiteboard. The states live in a boolean field or an enum, the transitions live in runtime guards, and a caller who uses a resource in the wrong state gets an exception instead of a compile error.&lt;/p&gt;

&lt;p&gt;I wanted the compiler to track that state in the program's model: whether a connection value represents an open channel, whether a request is still in flight, whether a response has arrived. This doesn't make the outside world infallible — networks still fail, files still disappear, other systems still change state — but it does mean your own code can't call an operation with a value in the wrong modeled state.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Subspace Channel
&lt;/h2&gt;

&lt;p&gt;Ruuk's &lt;code&gt;resource&lt;/code&gt; keyword declares an entity with named states that the compiler tracks. Here's a subspace channel — the Enterprise's long-range communication link:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pub resource SubspaceChannel =
    state Idle
    state Open
    state Transmitting
    state Terminal
        state Closed
        state Failed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An idle channel has type &lt;code&gt;SubspaceChannel&amp;lt;Idle&amp;gt;&lt;/code&gt;. An open channel has type &lt;code&gt;SubspaceChannel&amp;lt;Open&amp;gt;&lt;/code&gt;. These are different types — operations that expect one reject the other.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;Terminal&lt;/code&gt; grouping declares &lt;code&gt;Closed&lt;/code&gt; and &lt;code&gt;Failed&lt;/code&gt; as intentional endpoints: states the channel doesn't leave. The compiler won't warn about missing outgoing transitions from terminal states.&lt;/p&gt;

&lt;p&gt;Operations declare which state they require using the &lt;code&gt;subject&lt;/code&gt; role — a parameter role from the &lt;code&gt;op&lt;/code&gt; vocabulary introduced earlier in the series, which marks the resource being acted on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pub op openChannel =
    subject channel: SubspaceChannel&amp;lt;Idle&amp;gt;
    payload frequency: SubspaceFrequency
    outcomes =
        | Connected of SubspaceChannel&amp;lt;Open&amp;gt;
        | FrequencyBusy
        | OutOfRange

pub op send =
    subject channel: SubspaceChannel&amp;lt;Open&amp;gt;
    payload message: Message
    outcomes =
        | Transmitting of SubspaceChannel&amp;lt;Transmitting&amp;gt;
        | SendFailed of SubspaceChannel&amp;lt;Failed&amp;gt;

pub op awaitResponse =
    subject channel: SubspaceChannel&amp;lt;Transmitting&amp;gt;
    outcomes =
        | Received of response: Message, channel: SubspaceChannel&amp;lt;Open&amp;gt;
        | Timeout of SubspaceChannel&amp;lt;Open&amp;gt;
        | ConnectionLost of SubspaceChannel&amp;lt;Failed&amp;gt;

pub op closeChannel =
    subject channel: SubspaceChannel&amp;lt;Open&amp;gt;
    outcomes =
        | Closed of SubspaceChannel&amp;lt;Closed&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;send&lt;/code&gt; only accepts a &lt;code&gt;SubspaceChannel&amp;lt;Open&amp;gt;&lt;/code&gt; and transitions it to &lt;code&gt;Transmitting&lt;/code&gt;. While transmitting, you can't send another message — &lt;code&gt;send&lt;/code&gt; requires &lt;code&gt;Open&lt;/code&gt;, and you're holding &lt;code&gt;Transmitting&lt;/code&gt;. You call &lt;code&gt;awaitResponse&lt;/code&gt;, which returns the channel to &lt;code&gt;Open&lt;/code&gt; on success or transitions to &lt;code&gt;Failed&lt;/code&gt; on connection loss. The types enforce the async state flow: the compiler won't let you send on a channel that's still waiting for a reply, and it won't let you close a channel that's mid-transmission.&lt;/p&gt;

&lt;p&gt;This is what I wanted: I/O where the compiler tracks the resource state your program is holding, and using that value in the wrong state is a type error rather than a runtime exception. The state machine is in the declaration, not in a developer's head — which means an agent working with the channel gets the same guardrails as a human.&lt;/p&gt;

&lt;h2&gt;
  
  
  From I/O to Workflows
&lt;/h2&gt;

&lt;p&gt;I designed resources for I/O. What I didn't expect was how naturally the same mechanism models domain workflows.&lt;/p&gt;

&lt;p&gt;Every significant entity on a starship has a lifecycle. An away mission is proposed, reviewed, approved, launched, and completed — or aborted. A repair order is filed, triaged, assigned, and resolved. A course change is plotted, verified, authorized, and executed. These aren't I/O resources, but they have the same structure: named states, valid transitions, operations that only make sense in certain states.&lt;/p&gt;

&lt;p&gt;Most codebases model this with a status field and a runtime guard:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight fsharp"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="nc"&gt;MissionStatus&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;Proposed&lt;/span&gt;
    &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;Approved&lt;/span&gt;
    &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;InProgress&lt;/span&gt;
    &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;Completed&lt;/span&gt;
    &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;Aborted&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="nc"&gt;Mission&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Guid&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;MissionStatus&lt;/span&gt;
    &lt;span class="n"&gt;team&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;CrewMember&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="n"&gt;objective&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;launchMission&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mission&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Mission&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;mission&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Approved&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
        &lt;span class="n"&gt;failwith&lt;/span&gt; &lt;span class="s2"&gt;"Can only launch approved missions"&lt;/span&gt;
    &lt;span class="c1"&gt;// ... proceed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The guard works. But the compiler doesn't know that &lt;code&gt;launchMission&lt;/code&gt; requires an approved mission — it accepts any &lt;code&gt;Mission&lt;/code&gt;. A developer who calls &lt;code&gt;launchMission&lt;/code&gt; with a proposed mission gets a runtime failure, not a compile error. And there's nothing in the type that tells a reader — or an agent modifying the code — what the valid states and transitions are.&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;resource&lt;/code&gt;, the mission lifecycle works like the subspace channel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pub resource Mission =
    state Proposed
    state Approved
    state InProgress
    state Terminal
        state Completed
        state Aborted
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each state produces a different type. &lt;code&gt;Mission&amp;lt;Proposed&amp;gt;&lt;/code&gt; and &lt;code&gt;Mission&amp;lt;Approved&amp;gt;&lt;/code&gt; are as distinct to the compiler as &lt;code&gt;String&lt;/code&gt; and &lt;code&gt;Int&lt;/code&gt;. An operation that approves a mission only accepts the proposed state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pub op approveMission =
    subject mission: Mission&amp;lt;Proposed&amp;gt;
    by captain: CrewMember
    outcomes =
        | Approved of Mission&amp;lt;Approved&amp;gt;
        | InsufficientIntelligence
        | CrewNotAvailable of missing: List&amp;lt;String&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now try launching a mission that hasn't been approved:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;launchMission proposedMission by riker
-- Compile error: launchMission expects Mission&amp;lt;Approved&amp;gt;, got Mission&amp;lt;Proposed&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Riker can't skip Picard's sign-off. Not because of a runtime check that someone might forget to add, but because the types don't permit it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The &lt;code&gt;performs&lt;/code&gt; Clause
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;approveMission&lt;/code&gt; operation above constrains which state it accepts — only &lt;code&gt;Mission&amp;lt;Proposed&amp;gt;&lt;/code&gt;. But it doesn't yet declare which state it produces. The &lt;code&gt;performs&lt;/code&gt; clause makes the transition explicit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pub op approveMission =
    subject mission: Mission&amp;lt;Proposed&amp;gt;
    by captain: CrewMember
    performs Mission.Proposed -&amp;gt; Mission.Approved
    outcomes =
        | Approved of Mission&amp;lt;Approved&amp;gt;
        | InsufficientIntelligence
        | CrewNotAvailable of missing: List&amp;lt;String&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;performs&lt;/code&gt; clause tells the compiler two things: this operation starts with the resource in &lt;code&gt;Proposed&lt;/code&gt; state, and on success, transitions it to &lt;code&gt;Approved&lt;/code&gt;. The compiler validates both directions. If &lt;code&gt;performs&lt;/code&gt; says &lt;code&gt;Proposed -&amp;gt; Approved&lt;/code&gt; but the success outcome carries &lt;code&gt;Mission&amp;lt;InProgress&amp;gt;&lt;/code&gt;, that's a compile error. If the &lt;code&gt;subject&lt;/code&gt; is typed as &lt;code&gt;Mission&amp;lt;InProgress&amp;gt;&lt;/code&gt; but &lt;code&gt;performs&lt;/code&gt; says &lt;code&gt;Proposed -&amp;gt; Approved&lt;/code&gt;, that's also a compile error. Declaration and type must agree.&lt;/p&gt;

&lt;p&gt;Not every operation performs a state transition. Read-only operations omit &lt;code&gt;performs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pub op getMissionBriefing =
    subject mission: Mission&amp;lt;Approved&amp;gt;
    outcomes =
        | Briefing of MissionBriefing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This requires a specific state — you can only get the briefing for an approved mission — without transitioning to a new state. The &lt;code&gt;subject&lt;/code&gt; type still enforces the precondition.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Full Mission Lifecycle
&lt;/h2&gt;

&lt;p&gt;Here's the complete state machine with all operations declared:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pub resource Mission =
    state Proposed
    state Approved
    state InProgress
    state Terminal
        state Completed
        state Aborted

pub op proposeMission =
    payload objective: MissionObjective
    payload team: List&amp;lt;CrewMember&amp;gt;
    by officer: CrewMember
    outcomes =
        | Proposed of Mission&amp;lt;Proposed&amp;gt;
        | InvalidObjective of reason: String

pub op approveMission =
    subject mission: Mission&amp;lt;Proposed&amp;gt;
    by captain: CrewMember
    performs Mission.Proposed -&amp;gt; Mission.Approved
    outcomes =
        | Approved of Mission&amp;lt;Approved&amp;gt;
        | InsufficientIntelligence
        | CrewNotAvailable of missing: List&amp;lt;String&amp;gt;

pub op launchMission =
    subject mission: Mission&amp;lt;Approved&amp;gt;
    by firstOfficer: CrewMember
    performs Mission.Approved -&amp;gt; Mission.InProgress
    outcomes =
        | Launched of Mission&amp;lt;InProgress&amp;gt;
        | TransporterUnavailable
        | EnvironmentalHazard of hazard: String

pub op completeMission =
    subject mission: Mission&amp;lt;InProgress&amp;gt;
    performs Mission.InProgress -&amp;gt; Mission.Completed
    outcomes =
        | Completed of Mission&amp;lt;Completed&amp;gt;
        | ObjectivesIncomplete of remaining: List&amp;lt;String&amp;gt;

pub op abortMission =
    subject mission: Mission&amp;lt;InProgress&amp;gt;
    by officer: CrewMember
    performs Mission.InProgress -&amp;gt; Mission.Aborted
    outcomes =
        | Aborted of Mission&amp;lt;Aborted&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A pipeline that processes a mission through its lifecycle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;proposeMission objective team by riker
|&amp;gt; on Proposed mission -&amp;gt;
    approveMission mission by picard
    |&amp;gt; on Approved approved -&amp;gt;
        launchMission approved by riker
        |&amp;gt; on Launched active -&amp;gt;
            completeMission active
            |&amp;gt; on Completed done -&amp;gt;
                log $"Mission {done.name} completed successfully."
            |&amp;gt; on ObjectivesIncomplete remaining -&amp;gt;
                log $"Objectives remaining: {remaining}. Continuing mission."
        |&amp;gt; on TransporterUnavailable -&amp;gt;
            log "Transporter offline. Scheduling shuttle departure."
        |&amp;gt; on EnvironmentalHazard hazard -&amp;gt;
            log $"Launch aborted: {hazard}. Revising mission parameters."
    |&amp;gt; on InsufficientIntelligence -&amp;gt;
        log "Insufficient intelligence for mission approval. Requesting sensor sweep."
    |&amp;gt; on CrewNotAvailable missing -&amp;gt;
        log $"Required crew unavailable: {missing}."
|&amp;gt; on InvalidObjective reason -&amp;gt;
    log $"Mission objective rejected: {reason}."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The nesting mirrors the state progression. You can only enter the &lt;code&gt;launchMission&lt;/code&gt; block with a &lt;code&gt;Mission&amp;lt;Approved&amp;gt;&lt;/code&gt; because &lt;code&gt;approveMission&lt;/code&gt; only produces one on its &lt;code&gt;Approved&lt;/code&gt; arm. The types flow through the pipeline, and the compiler verifies every step. Each level handles its own outcomes completely — exhaustive handling from article 3 applies at every level.&lt;/p&gt;

&lt;h2&gt;
  
  
  Whole-Program State Machine Validation
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;performs&lt;/code&gt; clauses across all operations define the state machine's transition graph. The compiler builds this graph and validates it for structural correctness.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Orphan state detection.&lt;/strong&gt; A state is orphan if no operation transitions into it. If you declare a &lt;code&gt;Suspended&lt;/code&gt; state on &lt;code&gt;Mission&lt;/code&gt; but no &lt;code&gt;performs&lt;/code&gt; clause has &lt;code&gt;-&amp;gt; Mission.Suspended&lt;/code&gt;, the compiler warns: that state is unreachable. Either a &lt;code&gt;performs&lt;/code&gt; clause is missing, the state is mislabeled, or it should be removed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dead-end state detection.&lt;/strong&gt; A state is a dead-end if it has no outgoing transitions and isn't declared terminal. If &lt;code&gt;Mission&amp;lt;Approved&amp;gt;&lt;/code&gt; had no operations with &lt;code&gt;performs Mission.Approved -&amp;gt; ...&lt;/code&gt;, missions would get stuck — once approved, they could never progress. The compiler flags this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Performs/subject consistency.&lt;/strong&gt; Every &lt;code&gt;performs From -&amp;gt; To&lt;/code&gt; clause must match its &lt;code&gt;subject&lt;/code&gt; parameter type. If the &lt;code&gt;subject&lt;/code&gt; is &lt;code&gt;Mission&amp;lt;InProgress&amp;gt;&lt;/code&gt; but &lt;code&gt;performs&lt;/code&gt; says &lt;code&gt;Proposed -&amp;gt; Approved&lt;/code&gt;, the compiler rejects the mismatch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Performs/outcome consistency.&lt;/strong&gt; Every success outcome of an operation with &lt;code&gt;performs From -&amp;gt; To&lt;/code&gt; must carry the resource typed as the target state. If &lt;code&gt;performs&lt;/code&gt; says &lt;code&gt;Proposed -&amp;gt; Approved&lt;/code&gt; but the outcome carries &lt;code&gt;Mission&amp;lt;InProgress&amp;gt;&lt;/code&gt;, that's a compile error.&lt;/p&gt;

&lt;p&gt;Together, these four checks make the state machine self-consistent and verifiable without running any code. The compiler catches the structural errors — orphan states, dead ends, mismatched transitions — that would otherwise surface as unreachable code paths, stuck entities, or confused runtime behavior in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Projections with State
&lt;/h2&gt;

&lt;p&gt;Projections from the previous article interact naturally with typestate. A projection can require a specific state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;type MissionBriefing = Mission&amp;lt;Approved&amp;gt; only {
    name; objective; team; approvedBy; approvedAt
}

type MissionReport = Mission&amp;lt;Completed&amp;gt; only {
    name; objective; team; completedAt; findings
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;MissionBriefing&lt;/code&gt; can only be constructed from a &lt;code&gt;Mission&amp;lt;Approved&amp;gt;&lt;/code&gt;. Passing a &lt;code&gt;Mission&amp;lt;Proposed&amp;gt;&lt;/code&gt; is a type mismatch. &lt;code&gt;MissionReport&lt;/code&gt; requires &lt;code&gt;Mission&amp;lt;Completed&amp;gt;&lt;/code&gt;. The state requirement is encoded in the projection, not in a runtime check.&lt;/p&gt;

&lt;p&gt;This addresses a subtle problem: some fields only have meaningful values in certain states. The &lt;code&gt;approvedBy&lt;/code&gt; field on a mission is empty before approval. A projection that requires &lt;code&gt;Mission&amp;lt;Approved&amp;gt;&lt;/code&gt; says, at the type level, that approval data is part of the value being projected. The &lt;code&gt;findings&lt;/code&gt; field is empty before completion. A projection that requires &lt;code&gt;Mission&amp;lt;Completed&amp;gt;&lt;/code&gt; says the same thing about mission findings.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Compounds
&lt;/h2&gt;

&lt;p&gt;A pattern has emerged across these articles. &lt;code&gt;op&lt;/code&gt; started as a way to enforce error handling under schedule pressure; it turned out to produce exhaustive compiler verification. Projections started as DTO shorthand; they turned out to produce provable access boundaries. Resources started as a way to reason about I/O; they turned out to model enterprise workflows with compiler-verified state machines. Each feature was designed for practical relief and arrived at structural correctness.&lt;/p&gt;

&lt;p&gt;What I didn't anticipate is how they compound. An &lt;code&gt;op&lt;/code&gt; whose &lt;code&gt;subject&lt;/code&gt; is a &lt;code&gt;resource&lt;/code&gt; in a specific state, whose &lt;code&gt;payload&lt;/code&gt; is a projection of that resource — that single declaration encodes what data the operation can see, what state the resource must be in, what transitions are valid, and what outcomes the caller must handle. The compiler verifies all of it. Runtime guards, permission filters, tests, and code review still matter, but they usually operate as separate practices. These language features are designed to compose inside one declaration.&lt;/p&gt;

&lt;p&gt;The first article argued that agentic coding needs a three-party model: humans define intent, agents write implementations, compilers verify structure. This is where that model becomes concrete. An agent modifying the mission workflow doesn't need to discover the state machine by reading runtime guards scattered across the codebase. The &lt;code&gt;resource&lt;/code&gt; declaration and &lt;code&gt;performs&lt;/code&gt; clauses are the state machine — declarative, complete, and compiler-verified. An agent that tries to skip from &lt;code&gt;Proposed&lt;/code&gt; directly to &lt;code&gt;InProgress&lt;/code&gt; gets a compile error. The compiler is the third party — verifying structure that neither the human nor the agent needs to hold in working memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;Operations declare what can happen and what comes back. Projections control what each operation can see. Typestate controls when each operation can run. But real procedures chain multiple operations together, and when step five fails, steps one through four may need to be undone.&lt;/p&gt;

&lt;p&gt;The next article introduces &lt;strong&gt;sagas&lt;/strong&gt; — multi-step workflows where each forward step is declared alongside its compensation. If beaming an away team down succeeds but establishing contact fails, the team needs to be beamed back up. The saga declaration reads like a mission briefing: each step, what undoes it, and what happens when the sequence breaks.&lt;/p&gt;

&lt;p&gt;Ruuk is in &lt;a href="https://github.com/ruuk-lang/ruuk/releases" rel="noopener noreferrer"&gt;alpha&lt;/a&gt;. If these ideas resonate, give ruuk a spin; follow along on &lt;a href="https://github.com/ruuk-lang/ruuk" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; and weigh in on the discussions.&lt;/p&gt;

&lt;p&gt;This article was created with the help of AI&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>architecture</category>
      <category>software</category>
    </item>
    <item>
      <title>"It Didn't Happen" vs. "It Couldn't Happen"</title>
      <dc:creator>Mat Weiss</dc:creator>
      <pubDate>Thu, 04 Jun 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/matweiss/it-didnt-happen-vs-it-couldnt-happen-4ibn</link>
      <guid>https://dev.to/matweiss/it-didnt-happen-vs-it-couldnt-happen-4ibn</guid>
      <description>&lt;p&gt;Ever notice this pattern: a core type exists — &lt;code&gt;User&lt;/code&gt;, &lt;code&gt;Order&lt;/code&gt;, &lt;code&gt;ShipSystems&lt;/code&gt; — and then derived types fan out from it? Response DTOs, dashboard views, API contracts, audit records. Each is a subset of the source, maintained separately, with no compiler-visible relationship to the original.&lt;/p&gt;

&lt;p&gt;I started designing projections because I was tired of writing DTOs. What I ended up with was a correctness feature I hadn't anticipated — and one that generation alone can't provide.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Subset Problem
&lt;/h2&gt;

&lt;p&gt;Consider a starship's operational systems. Engineering, Tactical, Medical, and Science all need access to ship status, but each department should see only what it needs. Two approaches are standard.&lt;/p&gt;

&lt;p&gt;The first: write separate types for each view. &lt;code&gt;EngineeringData&lt;/code&gt; with five fields, &lt;code&gt;TacticalData&lt;/code&gt; with four, &lt;code&gt;MedicalData&lt;/code&gt; with two. Each is a manual subset of &lt;code&gt;ShipSystems&lt;/code&gt;, maintained independently. When &lt;code&gt;ShipSystems&lt;/code&gt; gains a field, each subset type is unaffected — which is either correct or a silent omission, depending on whether the new field belonged in that view. GraphQL takes a different angle on the same problem — let the client declare which fields it wants — but the boundary usually lives in schema and resolver tooling, not as a compiler-tracked derivation from the source type.&lt;/p&gt;

&lt;p&gt;The second: write a filter function that strips fields at runtime.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getShipData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DepartmentRole&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;ShipData&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;systems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getCurrentStatus&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;engineering&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;filterToEngineeringFields&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;systems&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tactical&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;filterToTacticalFields&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;systems&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;medical&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;filterToMedicalFields&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;systems&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;science&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;filterToScienceFields&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;systems&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both work. The manual types are safe but tedious — and they drift from the source type over time, with nothing connecting the two. The filter function is flexible but operates at runtime, where misconfiguration can be silent: a filter that returns weapons targeting data to Medical may produce no error, no warning, no signal that anything went wrong.&lt;/p&gt;

&lt;p&gt;Some type systems get partway there. TypeScript's &lt;code&gt;Pick&amp;lt;ShipSystems, 'hullIntegrity' | 'warpCorePower'&amp;gt;&lt;/code&gt; gives you a structural subset — an operation typed against it cannot access fields outside it. That's real, and it's better than a runtime filter. But &lt;code&gt;Pick&lt;/code&gt; is passive: add a field to &lt;code&gt;ShipSystems&lt;/code&gt; and every &lt;code&gt;Pick&lt;/code&gt; that excludes it continues to compile silently. No decision was forced.&lt;/p&gt;

&lt;p&gt;Every developer who has written a DTO has said it in their head: "&lt;code&gt;ShipSystems&lt;/code&gt;, but only these five fields." The missing piece is &lt;strong&gt;derivation tracking&lt;/strong&gt; — a compiler that knows which views were derived from which source types, and surfaces the effect of schema changes across all of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Projections
&lt;/h2&gt;

&lt;p&gt;This is what I wanted — a way to say "this type, but only these fields" that the compiler tracks. Ruuk calls them &lt;strong&gt;projections&lt;/strong&gt;: typed views of a record type that maintain a declared relationship to their source.&lt;/p&gt;

&lt;p&gt;Start with the full ship systems record:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pub type ShipSystems = {
    hullIntegrity: Float
    shieldStrength: Float
    weaponsStatus: WeaponsStatus
    targetingData: TargetingData
    warpCorePower: Float
    auxiliaryPower: Float
    lifeSupportLevel: Float
    crewBiosigns: List&amp;lt;Biosign&amp;gt;
    sensorReadings: SensorData
    missionParameters: MissionBriefing
    navigationCourse: CourseData
    communicationsLog: List&amp;lt;CommEntry&amp;gt;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now declare what each department can see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;type EngineeringView = ShipSystems only {
    hullIntegrity; warpCorePower; auxiliaryPower; lifeSupportLevel; shieldStrength
}

type TacticalView = ShipSystems only {
    shieldStrength; weaponsStatus; targetingData; sensorReadings
}

type MedicalView = ShipSystems only {
    crewBiosigns; lifeSupportLevel
}

type ScienceView = ShipSystems only {
    sensorReadings; navigationCourse
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each projection is a type. &lt;code&gt;EngineeringView&lt;/code&gt; has five fields. &lt;code&gt;targetingData&lt;/code&gt; is not one of them — not because a filter removes it at runtime, but because it was never part of &lt;code&gt;EngineeringView&lt;/code&gt;. The type doesn't have the field. There's nothing to misconfigure.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;only&lt;/code&gt; operator names the fields to include; everything else is excluded. Ruuk also has &lt;code&gt;without&lt;/code&gt;, which works the other direction — name the fields to exclude, include everything else:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;type PublicShipStatus = ShipSystems without {
    targetingData; missionParameters; communicationsLog
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The choice between &lt;code&gt;only&lt;/code&gt; and &lt;code&gt;without&lt;/code&gt; is about intent and maintenance. &lt;code&gt;only&lt;/code&gt; is explicit: you get exactly these fields, nothing more. When &lt;code&gt;ShipSystems&lt;/code&gt; gains a new field, an &lt;code&gt;only&lt;/code&gt; projection doesn't include it — you've declared what you want. &lt;code&gt;without&lt;/code&gt; is inclusive by default: you get everything except the named fields. When &lt;code&gt;ShipSystems&lt;/code&gt; gains a field, a &lt;code&gt;without&lt;/code&gt; projection includes it automatically. Both are valid; the right choice depends on whether the safer default is "exclude new fields until reviewed" or "include new fields unless sensitive."&lt;/p&gt;

&lt;h2&gt;
  
  
  Projections Meet Operations
&lt;/h2&gt;

&lt;p&gt;This connects directly to &lt;code&gt;op&lt;/code&gt; from the previous article. Projections work with any function, but combined with &lt;code&gt;op&lt;/code&gt;, the access boundary becomes part of the operation's contract. The parameter type on an operation controls what that operation can see. An engineering diagnostic that takes an &lt;code&gt;EngineeringView&lt;/code&gt; cannot access weapons targeting data — not by policy, by type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pub op runDiagnostic =
    payload systems: EngineeringView
    by engineer: CrewMember
    outcomes =
        | DiagnosticComplete of DiagnosticReport
        | SystemDegraded of system: String
        | InsufficientPower
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Writing &lt;code&gt;systems.targetingData&lt;/code&gt; anywhere in this operation's implementation is a compile error. The field does not exist on &lt;code&gt;EngineeringView&lt;/code&gt;. There is no filter to misconfigure and no log to reconstruct.&lt;/p&gt;

&lt;p&gt;Medical operations see only medical data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pub op assessCrewReadiness =
    payload systems: MedicalView
    by medic: CrewMember
    outcomes =
        | AllClear
        | CrewMembersUnfit of count: Int
        | LifeSupportCritical of level: Float
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;assessCrewReadiness&lt;/code&gt; takes a &lt;code&gt;MedicalView&lt;/code&gt;. It can read &lt;code&gt;crewBiosigns&lt;/code&gt; and &lt;code&gt;lifeSupportLevel&lt;/code&gt;. It cannot read &lt;code&gt;weaponsStatus&lt;/code&gt;, &lt;code&gt;targetingData&lt;/code&gt;, or &lt;code&gt;missionParameters&lt;/code&gt;. The constraint is in the type signature, enforced at every call site, visible to any reader. This matters for the three-party model: an agent generating the implementation body of &lt;code&gt;assessCrewReadiness&lt;/code&gt; cannot access fields outside the projection, even accidentally. The compiler blocks it the same way it blocks a human who reaches for the wrong field.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Happens When the Data Model Changes
&lt;/h2&gt;

&lt;p&gt;This is where derivation tracking earns its keep.&lt;/p&gt;

&lt;p&gt;Suppose the Enterprise gets a sensor upgrade, and &lt;code&gt;ShipSystems&lt;/code&gt; gains a new field:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pub type ShipSystems = {
    -- ... existing fields ...
    subspaceFieldReadings: SubspaceData   -- new field
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What happens to the projections?&lt;/p&gt;

&lt;p&gt;For &lt;code&gt;without&lt;/code&gt;-based projections like &lt;code&gt;PublicShipStatus&lt;/code&gt;, the new field is included automatically — it wasn't in the exclusion list. If &lt;code&gt;subspaceFieldReadings&lt;/code&gt; is sensitive, every view that uses &lt;code&gt;without&lt;/code&gt; now needs review. But the compiler knows which projections were affected by the schema change, so the developer can inspect each one and decide whether the new field belongs in the exclusion list.&lt;/p&gt;

&lt;p&gt;For &lt;code&gt;only&lt;/code&gt;-based projections like &lt;code&gt;EngineeringView&lt;/code&gt;, the new field is excluded automatically — it wasn't in the inclusion list. No accidental exposure. If Engineering should see subspace readings, someone adds it explicitly.&lt;/p&gt;

&lt;p&gt;Either way, the question "which views can see this new field?" has a definitive, machine-readable answer. The compiler traces the derivation relationship from source type to every projection. No manual audit of filter functions. No hoping someone remembered to update the access layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Field Accounting
&lt;/h2&gt;

&lt;p&gt;Projections handle narrowing — taking a type and producing something smaller. Going the other direction is where &lt;strong&gt;field accounting&lt;/strong&gt; kicks in.&lt;/p&gt;

&lt;p&gt;If you receive a &lt;code&gt;CrewMember&lt;/code&gt; and need to produce a &lt;code&gt;CrewRecord&lt;/code&gt; for the ship's database, the relationship between the two types is explicit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;type CrewRecord = CrewMember extending {
    assignedQuarters: String
    dutyShift: DutyShift
    boardingDate: StarDate
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;CrewRecord&lt;/code&gt; has all the fields of &lt;code&gt;CrewMember&lt;/code&gt; plus three more. The &lt;code&gt;extending&lt;/code&gt; keyword declares the derivation — same idea as &lt;code&gt;only&lt;/code&gt; and &lt;code&gt;without&lt;/code&gt;, but in the widening direction. Now the compiler requires you to account for every field difference when converting between them.&lt;/p&gt;

&lt;p&gt;Ruuk's &lt;code&gt;transform&lt;/code&gt; keyword defines a conversion between related types — a mapping function with compiler-verified field coverage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pub transform toCrewRecord (member: CrewMember) (quarters: String)
                           (shift: DutyShift) (date: StarDate) : CrewRecord =
    { member with
        assignedQuarters = quarters
        dutyShift = shift
        boardingDate = date }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The compiler checks this. The &lt;code&gt;transform&lt;/code&gt; body must supply those three — and it does. If you forgot &lt;code&gt;boardingDate&lt;/code&gt;, the compiler tells you exactly which field is missing and that it came from the gap between &lt;code&gt;CrewMember&lt;/code&gt; and &lt;code&gt;CrewRecord&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is the balance equation: fields from the source plus fields added in the body must equal the fields required in the output. The compiler verifies the equation. You verify the domain logic. The division of labor is clean.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Stronger Answer
&lt;/h2&gt;

&lt;p&gt;There's a consequence of all this that I didn't anticipate when I started designing projections.&lt;/p&gt;

&lt;p&gt;At some point, someone asks: "can Engineering access classified mission parameters?" With manual subset types or runtime filters, answering that question takes work. You trace the type definition or check the filter function, verify the configuration was correct for the period in question, and conclude that access didn't happen. That's a real answer — but it's a statement about observed behavior, and it depends on every link in the verification chain being intact.&lt;/p&gt;

&lt;p&gt;With projections, the answer is the source code itself. &lt;code&gt;runDiagnostic&lt;/code&gt; takes an &lt;code&gt;EngineeringView&lt;/code&gt;, declared as &lt;code&gt;ShipSystems only { hullIntegrity; warpCorePower; auxiliaryPower; lifeSupportLevel; shieldStrength }&lt;/code&gt;. The field &lt;code&gt;missionParameters&lt;/code&gt; is not in that list. Access is structurally impossible — not unobserved, but inexpressible.&lt;/p&gt;

&lt;p&gt;That's a meaningfully different kind of answer. It holds for every execution of the code, past and future, regardless of who or what wrote the implementation — until someone changes the projection declaration, at which point the change appears in version control, goes through review, and is auditable in its own right.&lt;/p&gt;

&lt;p&gt;This is the same arc as &lt;code&gt;op&lt;/code&gt;. I designed &lt;code&gt;op&lt;/code&gt; to enforce error handling commitments under schedule pressure; it turned out to produce exhaustive compiler verification. I designed projections to avoid writing DTOs; they turned out to produce provable access boundaries. In both cases, the practical motivation led somewhere I hadn't planned — the feature I built for convenience became the feature I kept for correctness.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;Projections control what an operation can see. The next article addresses when an operation can run.&lt;/p&gt;

&lt;p&gt;A starship mission moves through stages: proposed, approved, in progress, completed. You shouldn't be able to launch a mission that hasn't been approved, and you shouldn't be able to approve a mission that's already in progress. Today, those constraints live in runtime guards — &lt;code&gt;if mission.status != "approved" then throw&lt;/code&gt;. The compiler doesn't know about them, doesn't verify them, and doesn't prevent someone from calling the wrong operation at the wrong time.&lt;/p&gt;

&lt;p&gt;Ruuk's &lt;code&gt;resource&lt;/code&gt; keyword and &lt;strong&gt;typestate&lt;/strong&gt; move the state machine into the type system. A &lt;code&gt;Mission&amp;lt;Proposed&amp;gt;&lt;/code&gt; is a different type from a &lt;code&gt;Mission&amp;lt;Approved&amp;gt;&lt;/code&gt;, and the operation that launches a mission only accepts the latter. The compiler catches the violation before the code runs.&lt;/p&gt;

&lt;p&gt;Ruuk is in &lt;a href="https://github.com/ruuk-lang/ruuk/releases" rel="noopener noreferrer"&gt;alpha&lt;/a&gt;. If these ideas resonate, give ruuk a spin; follow along on &lt;a href="https://github.com/ruuk-lang/ruuk" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; and weigh in on the discussions.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>architecture</category>
      <category>software</category>
    </item>
    <item>
      <title>Five Ways to Fail a Transport</title>
      <dc:creator>Mat Weiss</dc:creator>
      <pubDate>Tue, 26 May 2026 12:36:00 +0000</pubDate>
      <link>https://dev.to/matweiss/five-ways-to-fail-a-transport-509b</link>
      <guid>https://dev.to/matweiss/five-ways-to-fail-a-transport-509b</guid>
      <description>&lt;p&gt;&lt;em&gt;(Ruuk shares syntax with F#. A brief introduction to F# is provided &lt;a href="https://dev.to/matweiss/from-braces-to-pipes-gp3"&gt;here&lt;/a&gt; if you need it.)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;As a tech lead, I'd set minimum standards for API error handling — every call covers 400, 401, 403, 500. In ticket reviews I'd still find three failure scenarios in the notes and one in the code. Developers had spotted the cases; the language gave them nowhere permanent to put that knowledge.&lt;/p&gt;

&lt;p&gt;The knowledge of which errors to handle was always there. It lived in tickets, in team standards, in code comments — and it degraded, because none of those places were connected to the compiler.&lt;/p&gt;

&lt;p&gt;I started designing &lt;code&gt;op&lt;/code&gt; because I wanted that knowledge to have a durable home in the code itself — a declaration that names every outcome an operation can produce, enforced at every call site. The first article in this series argued that this intent already exists in every codebase — in Javadoc, in JSDoc comments, in OpenAPI response schemas — but lives where the compiler can't see it. &lt;code&gt;op&lt;/code&gt; moves it into the declaration.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Binary Split
&lt;/h2&gt;

&lt;p&gt;Every mainstream approach to error handling asks the same question first: did it work, or didn't it? Try/catch, &lt;code&gt;Result&lt;/code&gt;, sealed exception hierarchies — they all partition outcomes into two buckets. If your background is Java or C#, you've reached for this with custom exception hierarchies or result wrapper classes — the intent is always the same: name the failure modes, make them visible to callers. But the binary framing works only when an operation genuinely has one way to succeed and one way to fail. This isn't always the best way to model many domain operations.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://dev.to/matweiss/from-braces-to-pipes-gp3"&gt;previous article&lt;/a&gt; ended with a transporter that uses F#'s &lt;code&gt;Result&lt;/code&gt; type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight fsharp"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="nc"&gt;TransporterFailure&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;SignalLost&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="n"&gt;lastCoords&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;PatternDegradation&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="n"&gt;integrity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt;
    &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;TargetShielded&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;beamUp&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SignalStrength&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
        &lt;span class="nc"&gt;Ok&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Name&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SignalStrength&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
        &lt;span class="nc"&gt;Error&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;PatternDegradation&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SignalStrength&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
        &lt;span class="nc"&gt;Error&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SignalLost&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LastKnownCoords&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The discriminated union inside &lt;code&gt;Error&lt;/code&gt; means the failure modes are typed and named. Pattern matching means you can handle each one. This is genuinely good — I prefer it to exception hierarchies, or status codes. But three properties of the binary model compound at scale, regardless of which language you use.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;New outcomes slip through the cracks.&lt;/strong&gt; Add &lt;code&gt;InsufficientPower&lt;/code&gt; to the transporter. In Java, callers that catch the base &lt;code&gt;TransporterException&lt;/code&gt; still compile. In C#, a &lt;code&gt;switch&lt;/code&gt; with a &lt;code&gt;default&lt;/code&gt; arm still compiles. In F#, callers that forward the &lt;code&gt;Error&lt;/code&gt; side with &lt;code&gt;Result.map&lt;/code&gt; still compile. The call sites that explicitly enumerate cases will break — as they should — but the ones that handle errors generically don't. That's the pattern I saw as a tech lead. Developers would identify an error case during implementation, document it, and move on — and nothing in the toolchain pushed back. What if the compiler could make this impossible?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Success is more than one thing.&lt;/strong&gt; Every binary error model — try/catch, &lt;code&gt;Result&amp;lt;T, E&amp;gt;&lt;/code&gt;, Go's &lt;code&gt;(value, error)&lt;/code&gt; — gives you exactly one success channel. But many domain operations have more than one meaningful success path. An upsert either created a new record or updated an existing one. A negotiation reached a full agreement or a provisional ceasefire. Both outcomes are successes, but they require different handling. Model two success paths and you're either misclassifying one as a failure or nesting another dispatch layer inside the success channel, where callers can process the value without ever distinguishing between the two paths.&lt;/p&gt;

&lt;p&gt;This is the constraint we rarely notice: working in languages where success is modeled as singular can train us not to see cases where it isn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Outcomes are peers, not subcategories.&lt;/strong&gt; Even in the best case — F#'s pattern matching, where the compiler verifies every arm — the binary structure leaks into every call site:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight fsharp"&gt;&lt;code&gt;&lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;beamUp&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt;
&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;Ok&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="s2"&gt;"Transport complete. {name} is aboard."&lt;/span&gt;
&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SignalLost&lt;/span&gt; &lt;span class="n"&gt;coords&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="s2"&gt;"Signal lost at {coords}."&lt;/span&gt;
&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;PatternDegradation&lt;/span&gt; &lt;span class="n"&gt;integrity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="s2"&gt;"Pattern at {integrity}."&lt;/span&gt;
&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt; &lt;span class="nc"&gt;TargetShielded&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"Cannot beam through shields."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The domain has four outcomes; the type has two. Every failure arm spells out &lt;code&gt;Error (...)&lt;/code&gt; as a preamble before reaching the actual case. The same ceremony appears in Java sealed hierarchies, C# result wrappers, and Go error returns. The wrapper changes; the two-bucket structure doesn't — and it shows everywhere the code touches the result.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Parameter intent is implicit.&lt;/strong&gt; A real transporter operation has a target, a source location, a destination pad, and an operator. Most languages handle this with positional parameters or named arguments — &lt;code&gt;beamUp(target, surface, pad, operator)&lt;/code&gt; or &lt;code&gt;beamUp(target: riker, from: surface)&lt;/code&gt;. Named arguments are a real improvement over positional-only. But the names are ad hoc: one operation uses &lt;code&gt;from&lt;/code&gt;, another uses &lt;code&gt;source&lt;/code&gt;, a third uses &lt;code&gt;origin&lt;/code&gt;. Nothing enforces consistency across the codebase, and the compiler doesn't verify that a parameter labeled &lt;code&gt;from&lt;/code&gt; actually plays the role of a source.&lt;/p&gt;

&lt;p&gt;These properties are fine for small codebases where a team can hold the conventions in working memory. They compound as the number of operations and call sites grows — particularly in agentic workflows where the code is written by one system and reviewed by another.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a Better Approach Could Look Like
&lt;/h2&gt;

&lt;p&gt;These properties point toward three design criteria:&lt;/p&gt;

&lt;p&gt;All outcomes must be handled at every call site. F#'s &lt;code&gt;match&lt;/code&gt; and Java's switch expressions on sealed types already enforce exhaustive coverage — that's strong, and it's the right foundation. The remaining gap is call sites that forward, transform, or ignore the result without matching. When someone adds a sixth outcome, every call site should break until it's handled — not just the ones that happen to use &lt;code&gt;match&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Outcomes should be peers, not wrapped.&lt;/strong&gt; An operation that can produce five results should declare five outcomes at the same level. &lt;code&gt;SignalLost&lt;/code&gt; is not a sub-category of "error" — it's a domain result as legitimate as &lt;code&gt;Transported&lt;/code&gt;. The caller should handle each one directly, without unwrapping a success or error container first.&lt;/p&gt;

&lt;p&gt;Parameters should carry semantic roles. Named parameters are good. A small, fixed vocabulary of parameter roles is better. If every operation in the codebase uses &lt;code&gt;from&lt;/code&gt; to mean "source" and &lt;code&gt;to&lt;/code&gt; to mean "destination" — not because of a naming convention, but because the language defines those roles and the compiler verifies them — then call sites are consistent by construction. A developer who learns the role vocabulary once can read any operation's call site without checking its signature.&lt;/p&gt;

&lt;p&gt;This matters for agentic coding too. Language models trained on natural language have already internalized &lt;code&gt;from&lt;/code&gt; and &lt;code&gt;to&lt;/code&gt; as directional roles. A fixed vocabulary gives a model the same reference point as the human reviewer — generated call sites are consistent not by training luck, but by structural constraint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introducing &lt;code&gt;op&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Ruuk's &lt;code&gt;op&lt;/code&gt; keyword declares a domain operation as a first-class language construct. A domain operation is any action with outcomes that callers need to handle differently — a database write, an API call, a state transition, a validation check. Pure transformations and internal helpers stay as regular &lt;code&gt;let&lt;/code&gt; functions.&lt;/p&gt;

&lt;p&gt;Here's the transporter. &lt;code&gt;pub&lt;/code&gt; marks the operation as visible to other modules — the same access modifier you'd find in Rust:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pub op beamUp =
    payload target: CrewMember
    from surface: PlanetarySurface
    to pad: TransporterPad
    by operator: CrewMember
    outcomes =
        | Transported of CrewMember
        | SignalLost of lastCoords: String
        | PatternDegradation of integrity: Float
        | TargetShielded
        | InsufficientPower
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Five outcomes, declared as peers. Each carries its own data — &lt;code&gt;SignalLost&lt;/code&gt; carries coordinates, &lt;code&gt;PatternDegradation&lt;/code&gt; carries an integrity reading, &lt;code&gt;TargetShielded&lt;/code&gt; and &lt;code&gt;InsufficientPower&lt;/code&gt; carry nothing. No outcome is privileged as "success" or "error" — each is a peer-level domain result.&lt;/p&gt;

&lt;p&gt;The parameters have &lt;strong&gt;roles&lt;/strong&gt;. &lt;code&gt;payload&lt;/code&gt; marks the main data argument. &lt;code&gt;from&lt;/code&gt;, &lt;code&gt;to&lt;/code&gt;, and &lt;code&gt;by&lt;/code&gt; are prepositions that describe each parameter's relationship to the operation. These aren't arbitrary labels chosen by the developer — they're drawn from a small, fixed vocabulary that Ruuk defines: &lt;code&gt;payload&lt;/code&gt;, &lt;code&gt;subject&lt;/code&gt;, &lt;code&gt;from&lt;/code&gt;, &lt;code&gt;to&lt;/code&gt;, &lt;code&gt;by&lt;/code&gt;, &lt;code&gt;via&lt;/code&gt;, &lt;code&gt;in&lt;/code&gt;, &lt;code&gt;at&lt;/code&gt;, &lt;code&gt;for&lt;/code&gt;. Every operation in every codebase uses the same words for the same meanings.&lt;/p&gt;

&lt;p&gt;The roles appear at the call site:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;beamUp riker from planetSurface to padOne by laForge
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read it out loud: "beam up Riker from the planet surface to pad one by La Forge." Many languages have named parameters. The fixed role vocabulary adds consistency across the entire codebase — you don't learn &lt;code&gt;source&lt;/code&gt; on one operation and &lt;code&gt;origin&lt;/code&gt; on another. Every operation that takes something from somewhere uses &lt;code&gt;from&lt;/code&gt;. Swap &lt;code&gt;from&lt;/code&gt; and &lt;code&gt;to&lt;/code&gt; and the compiler rejects it — &lt;code&gt;TransporterPad&lt;/code&gt; is not a &lt;code&gt;PlanetarySurface&lt;/code&gt;. The vocabulary is small enough to memorize in minutes, and once you know it, every call site reads the same way.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;op&lt;/code&gt; declaration is the contract. It tells every other module what &lt;code&gt;beamUp&lt;/code&gt; needs and what it can produce. The implementation is a separate &lt;code&gt;let&lt;/code&gt; binding:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;let beamUp (target: CrewMember) (surface: PlanetarySurface)
           (pad: TransporterPad) (operator: CrewMember) =
    if pad.powerLevel &amp;lt; minimumPower then
        InsufficientPower
    elif surface.shielded then
        TargetShielded
    elif target.signalStrength &amp;gt; 0.8 then
        Transported target
    elif target.signalStrength &amp;gt; 0.4 then
        PatternDegradation target.signalStrength
    else
        SignalLost target.lastKnownCoords
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The separation is deliberate. From another module, you read the &lt;code&gt;op&lt;/code&gt; declaration — the contract — which tells you what to provide, what role each argument plays, and what can come back. The implementation is an internal detail. This is the three-party model from the first article in practice: the human defines the declaration, the agent generates the implementation body, and the compiler verifies that the body satisfies the contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling Outcomes with &lt;code&gt;|&amp;gt; on&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Outcomes are handled with &lt;code&gt;|&amp;gt; on&lt;/code&gt;, which integrates directly into Ruuk's pipeline syntax:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;beamUp riker from planetSurface to padOne by laForge
|&amp;gt; on Transported crew   -&amp;gt; printfn $"Transport complete. {crew.name} is aboard."
|&amp;gt; on SignalLost coords  -&amp;gt; printfn $"Signal lost at {coords}. Dispatching shuttle."
|&amp;gt; on PatternDegradation integrity -&amp;gt;
    printfn $"Pattern integrity at {integrity}. Boosting signal."
|&amp;gt; on TargetShielded     -&amp;gt; printfn "Cannot beam through shields. Hailing target."
|&amp;gt; on InsufficientPower  -&amp;gt; printfn "Rerouting power to transporter systems."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each &lt;code&gt;|&amp;gt; on&lt;/code&gt; arm matches one outcome and binds its data. &lt;code&gt;Transported crew&lt;/code&gt; binds the returned &lt;code&gt;CrewMember&lt;/code&gt; to &lt;code&gt;crew&lt;/code&gt;. &lt;code&gt;SignalLost coords&lt;/code&gt; binds the coordinates. &lt;code&gt;TargetShielded&lt;/code&gt; carries no data, so there's no binding.&lt;/p&gt;

&lt;p&gt;Compare this to the &lt;code&gt;Result&lt;/code&gt; matching from earlier. The outcomes are flat — one level, five arms, each handled directly. There's no &lt;code&gt;Ok&lt;/code&gt;/&lt;code&gt;Error&lt;/code&gt; wrapper to spell out on every arm. The pipeline reads top to bottom: the operation produces a result, and each rail leads to its own destination.&lt;/p&gt;

&lt;p&gt;If you've encountered Scott Wlaschin's &lt;a href="https://fsharpforfunandprofit.com/rop/" rel="noopener noreferrer"&gt;railway-oriented programming&lt;/a&gt;, this is the same mental model extended from two rails to N — reflecting the actual structure of domain operations rather than forcing them through a binary split.&lt;/p&gt;

&lt;h2&gt;
  
  
  Exhaustive Handling
&lt;/h2&gt;

&lt;p&gt;F# &lt;code&gt;match&lt;/code&gt; already fails to compile when an arm is missing — that's the foundation &lt;code&gt;op&lt;/code&gt; builds on. The difference is what happens when a developer reaches for a wildcard:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight fsharp"&gt;&lt;code&gt;&lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;beamUp&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt;
&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;Ok&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="s2"&gt;"Transport complete. {name} is aboard."&lt;/span&gt;
&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SignalLost&lt;/span&gt; &lt;span class="n"&gt;coords&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="s2"&gt;"Signal lost at {coords}."&lt;/span&gt;
&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;PatternDegradation&lt;/span&gt; &lt;span class="n"&gt;integrity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="s2"&gt;"Pattern at {integrity}."&lt;/span&gt;
&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="p"&gt;_&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"Something went wrong."&lt;/span&gt;  &lt;span class="c1"&gt;// compiles; TargetShielded silently swallowed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This compiles. The wildcard arm satisfies the compiler while silently discarding any outcome it matches. &lt;code&gt;|&amp;gt; on&lt;/code&gt; has no equivalent. Remove an arm and there's nowhere for that outcome to go:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;beamUp riker from planetSurface to padOne by laForge
|&amp;gt; on Transported crew   -&amp;gt; printfn $"Transport complete. {crew.name} is aboard."
|&amp;gt; on SignalLost coords  -&amp;gt; printfn $"Signal lost at {coords}."
|&amp;gt; on PatternDegradation integrity -&amp;gt; printfn $"Pattern at {integrity}."
|&amp;gt; on TargetShielded     -&amp;gt; printfn "Cannot beam through shields."
-- InsufficientPower is missing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This does not compile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Unhandled outcome: InsufficientPower
  in beamUp call at TransporterRoom.rk:14
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every declared outcome must be handled or explicitly marked as deferred. There's no catch-all to hide behind.&lt;/p&gt;

&lt;p&gt;The practical consequence: if you add a new outcome to &lt;code&gt;beamUp&lt;/code&gt; — say &lt;code&gt;WarpFieldInterference&lt;/code&gt; because the ship is about to jump to warp — the compiler produces an error at every call site that doesn't handle it or explicitly mark it as deferred. That error list is your work queue. The decision can't be deferred silently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Progressive Development with &lt;code&gt;todo&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;If you've worked under schedule pressure — and you have — you might see a tension with exhaustive handling. The compiler now requires every outcome to be accounted for before the code compiles. But the whole motivation for &lt;code&gt;op&lt;/code&gt; was that schedules don't leave room to handle everything at once. Demanding completeness on day one would just trade one problem for another.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;todo&lt;/code&gt; resolves this. It's the developer's explicit acknowledgment to the compiler: I know this outcome needs handling. Not today.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;beamUp riker from planetSurface to padOne by laForge
|&amp;gt; on Transported crew   -&amp;gt; printfn $"Transport complete. {crew.name} is aboard."
|&amp;gt; on SignalLost coords  -&amp;gt; printfn $"Signal lost at {coords}."
|&amp;gt; on PatternDegradation integrity -&amp;gt; printfn $"Pattern at {integrity}."
|&amp;gt; on TargetShielded     -&amp;gt; todo
|&amp;gt; on InsufficientPower  -&amp;gt; todo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This compiles. The compiler emits warnings — "todo: TargetShielded outcome unhandled at line 5" — but not errors. If &lt;code&gt;TargetShielded&lt;/code&gt; fires at runtime, &lt;code&gt;todo&lt;/code&gt; panics with a clear message pointing to the unhandled outcome and its location.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;todo&lt;/code&gt; works anywhere an expression is expected. In a function body you haven't implemented yet. In a match arm you'll fill in later. In a pipeline handler you're deferring. Every &lt;code&gt;todo&lt;/code&gt; in the codebase is a compiler-tracked work item. The warnings list is your checklist; the compiler maintains it automatically.&lt;/p&gt;

&lt;p&gt;This is the difference that mattered to me: without a declaration like &lt;code&gt;op&lt;/code&gt;, unhandled error paths can become invisible — they compile silently, and under schedule pressure, the team moves on. With &lt;code&gt;op&lt;/code&gt; and &lt;code&gt;todo&lt;/code&gt;, the sprint can end with incomplete handling, but every gap is tracked, visible, and named. That visibility is just as critical in agentic workflows — an agent that generates an implementation can't silently skip an outcome, and a reviewer scanning &lt;code&gt;todo&lt;/code&gt; warnings sees exactly which decisions remain open. A language should be a feedback system during development, not just a gatekeeper at the end.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Richer Example: Routing Emergency Power
&lt;/h2&gt;

&lt;p&gt;The transporter demonstrates outcomes and parameter roles. A more complex operation shows how they compose.&lt;/p&gt;

&lt;p&gt;During a Red Alert, Engineering needs to reroute power from non-essential systems to critical ones — shields, weapons, life support. But a reroute isn't all-or-nothing: the holodeck might spare 300 units without shutting down completely, even when 500 were requested. The operation has two distinct success paths and several failure paths:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pub op routePower =
    payload amount: PowerUnits
    from source: ShipSystem
    to target: ShipSystem
    by officer: CrewMember
    outcomes =
        | FullyRouted of PowerAllocation
        | PartiallyRouted of PowerAllocation
        | InsufficientPower of available: PowerUnits
        | SystemOffline of system: ShipSystem
        | OverloadRisk of currentLoad: Float
        | UnauthorizedAccess
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At the call site, the roles make the operation self-documenting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;routePower 500 from holodecks to shields by laForge
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;"Route 500 power units from holodecks to shields by La Forge." A developer cold-reading this code doesn't need the signature to tell which system gives power and which receives it.&lt;/p&gt;

&lt;p&gt;Chaining this with the alert status makes a realistic pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pub op setAlertStatus =
    payload status: AlertStatus
    by officer: CrewMember
    outcomes =
        | AlertSet of AlertStatus
        | AlreadyAtStatus
        | InsufficientRank

setAlertStatus Red by picard
|&amp;gt; on AlertSet _ -&amp;gt;            -- discard the AlertStatus value
    routePower 500 from holodecks to shields by laForge
    |&amp;gt; on FullyRouted alloc -&amp;gt;
        printfn $"Power rerouted: {alloc.amount} units to shields."
    |&amp;gt; on PartiallyRouted alloc -&amp;gt;
        printfn $"Partial reroute: {alloc.amount} units to shields. Requesting auxiliary power."
    |&amp;gt; on InsufficientPower available -&amp;gt;
        printfn $"Only {available} units available. Requesting auxiliary power."
    |&amp;gt; on SystemOffline system -&amp;gt;
        printfn $"Cannot draw from {system.name}: system offline."
    |&amp;gt; on OverloadRisk load -&amp;gt;
        printfn $"Shield grid at {load}% capacity. Reroute would overload."
    |&amp;gt; on UnauthorizedAccess -&amp;gt;
        printfn "Officer lacks authorization for power routing."
|&amp;gt; on AlreadyAtStatus -&amp;gt;
    printfn "Already at Red Alert."
|&amp;gt; on InsufficientRank -&amp;gt;
    printfn "Only senior officers can set alert status."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The nesting mirrors the domain logic. You can only reroute power after setting the alert succeeds — and each level handles its own outcomes completely. The compiler verifies coverage at both levels independently.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;FullyRouted&lt;/code&gt; and &lt;code&gt;PartiallyRouted&lt;/code&gt; are both successes — the power moved — but they require different responses. With &lt;code&gt;Result&lt;/code&gt;, expressing two success paths means either misclassifying one as an error or wrapping both in another union inside &lt;code&gt;Ok&lt;/code&gt;, where callers can ignore the distinction. With &lt;code&gt;op&lt;/code&gt;, both sit at the top level as peers, and the compiler holds every call site to both.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Comparison
&lt;/h2&gt;

&lt;p&gt;The argument for &lt;code&gt;op&lt;/code&gt; is not that functions-returning-unions are wrong. They're a good pattern — better than exceptions, better than status codes, better than unchecked error returns. &lt;code&gt;op&lt;/code&gt; builds on that foundation by making the properties structural rather than advisory:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concern&lt;/th&gt;
&lt;th&gt;Function + DU&lt;/th&gt;
&lt;th&gt;&lt;code&gt;op&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Parameter roles&lt;/td&gt;
&lt;td&gt;Named arguments (ad hoc)&lt;/td&gt;
&lt;td&gt;Fixed role vocabulary (compiler-verified)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exhaustive handling&lt;/td&gt;
&lt;td&gt;Compiler-checked in &lt;code&gt;match&lt;/code&gt; expressions&lt;/td&gt;
&lt;td&gt;Compiler-checked at every call site, no wildcards&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Adding an outcome&lt;/td&gt;
&lt;td&gt;Breaks &lt;code&gt;match&lt;/code&gt; arms; forwarding sites compile unchanged&lt;/td&gt;
&lt;td&gt;Breaks call sites until handled or marked &lt;code&gt;todo&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Outcome contract location&lt;/td&gt;
&lt;td&gt;Separate return type&lt;/td&gt;
&lt;td&gt;Declared with the operation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Readable without implementation&lt;/td&gt;
&lt;td&gt;Often split between function and result type&lt;/td&gt;
&lt;td&gt;Declaration is the contract&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Binary success/failure bias&lt;/td&gt;
&lt;td&gt;Baked into &lt;code&gt;Result&amp;lt;T, E&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;All outcomes are peers&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;F# provides the foundation &lt;code&gt;op&lt;/code&gt; builds on — exhaustive matching, typed unions, immutable data. The table highlights the specific gaps that remain when &lt;code&gt;Result&lt;/code&gt; is the primary abstraction for domain operations: call sites that forward or ignore results aren't covered by exhaustive checking, the binary split can obscure domain structure, and parameter semantics depend on naming conventions. &lt;code&gt;op&lt;/code&gt; is Ruuk's attempt to close those gaps.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to Use &lt;code&gt;op&lt;/code&gt; vs &lt;code&gt;let&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Not everything is a domain operation. &lt;code&gt;op&lt;/code&gt; is for actions with multiple outcomes that callers handle differently — service calls, state transitions, validations. Pure transformations, utility functions, and internal helpers stay as regular &lt;code&gt;let&lt;/code&gt; functions. If a function has one return path with no alternative outcomes, &lt;code&gt;let&lt;/code&gt; is the right tool. The parameter roles offer a practical test: if &lt;code&gt;payload&lt;/code&gt;, &lt;code&gt;from&lt;/code&gt;, &lt;code&gt;to&lt;/code&gt;, and the rest don't fit naturally, the operation probably isn't a domain action — it's a transformation.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;op&lt;/code&gt; builds on F#'s strengths — exhaustive matching, typed unions, immutable data — and addresses the gaps this series cares about. Parameter roles replace ad hoc naming conventions. The binary &lt;code&gt;Ok&lt;/code&gt;/&lt;code&gt;Error&lt;/code&gt; split gives way to peer-level outcomes. And exhaustive handling extends from match expressions to every call site. The declaration carries it all, and the compiler enforces it everywhere. That's the foundation the rest of the series builds on.&lt;/p&gt;

&lt;p&gt;But the declaration doesn't yet control what each operation can &lt;em&gt;see&lt;/em&gt; or &lt;em&gt;when&lt;/em&gt; it can run.&lt;/p&gt;

&lt;p&gt;The next article introduces projections — type-level views that control which fields an operation can access. If the transporter's &lt;code&gt;beamUp&lt;/code&gt; operation shouldn't see a crew member's medical records, that restriction should be structural, not a runtime filter somebody remembers to apply. Projections make information boundaries part of the type system.&lt;/p&gt;

&lt;p&gt;After that: typestate, where the compiler tracks what state an entity is in and prevents operations that don't make sense in that state. You shouldn't be able to beam someone up who hasn't been cleared for transport — and the type system should enforce that, not a runtime guard.&lt;/p&gt;

&lt;p&gt;The transporter is a single operation. Real starship procedures chain dozens of them, and when step five fails, steps one through four may need to be undone. That's the saga article. But first, we need to control what operations can see and when they can act.&lt;/p&gt;

&lt;p&gt;Ruuk is in &lt;a href="https://github.com/ruuk-lang/ruuk/releases" rel="noopener noreferrer"&gt;alpha&lt;/a&gt;. If these ideas resonate, give ruuk a spin; follow along on &lt;a href="https://github.com/ruuk-lang/ruuk" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; and weigh in on the discussions.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>architecture</category>
      <category>software</category>
    </item>
    <item>
      <title>From Braces to Pipes</title>
      <dc:creator>Mat Weiss</dc:creator>
      <pubDate>Tue, 26 May 2026 12:30:00 +0000</pubDate>
      <link>https://dev.to/matweiss/from-braces-to-pipes-gp3</link>
      <guid>https://dev.to/matweiss/from-braces-to-pipes-gp3</guid>
      <description>&lt;p&gt;The &lt;a href="https://dev.to/matweiss/your-compiler-is-missing-from-the-party-4bf1"&gt;previous article&lt;/a&gt; argued that compilers could check more than they currently do — and that the agentic coding era makes this urgent. The articles that follow demonstrate specific features in Ruuk, a language designed around that idea. But Ruuk's syntax is based on F#, and if you've never seen an ML-family language, the examples will be harder to follow than they need to be.&lt;/p&gt;

&lt;p&gt;This article is a warp-speed tour of F# syntax — just enough to read the Ruuk code in the rest of the series. If you've written C#, Java, or TypeScript, nothing here is conceptually alien. The ideas have direct parallels; the notation is just different. And to keep things interesting, we'll model our examples around the operational systems of a certain starship.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Fsharp
&lt;/h2&gt;

&lt;p&gt;The choice of F# as Ruuk's syntactic foundation wasn't personal preference. Before I'd decided on paradigm or syntax style, I studied how programmers actually read code — drawing on cognitive linguistics research. Two findings shaped the design. First, people tend to read code the same way they read prose: linearly, left to right, top to bottom. Nested function calls like &lt;code&gt;toUpper(trim(getName(user)))&lt;/code&gt; force inside-out reading — you start at the innermost call and work outward. Fluent method chaining — &lt;code&gt;user.getName().trim().toUpper()&lt;/code&gt; — solves the reading-order problem, but it ties each step to a method defined on the preceding type. A pipeline like &lt;code&gt;user |&amp;gt; getName |&amp;gt; trim |&amp;gt; toUpper&lt;/code&gt; reads in the same order things happen, and the functions are standalone — they compose freely without needing to live on a class. ML-family languages build around this style, and that made them a natural place to start.&lt;/p&gt;

&lt;p&gt;Second, natural language encodes relationships through prepositions — &lt;code&gt;from&lt;/code&gt;, &lt;code&gt;to&lt;/code&gt;, &lt;code&gt;by&lt;/code&gt;, &lt;code&gt;in&lt;/code&gt; — and humans process these almost reflexively. That observation led directly to Ruuk's parameter roles, which you'll see in the next article: &lt;code&gt;beamUp riker from planetSurface to padOne by laForge&lt;/code&gt; reads the way you'd describe the operation out loud. The syntax isn't ML-flavored because ML is elegant (though it is). It's ML-flavored because that family gave Ruuk a strong foundation for readable, linear code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Values and Functions
&lt;/h2&gt;

&lt;p&gt;F# uses &lt;code&gt;let&lt;/code&gt; to bind a name to a value. If you're coming from a C-family language, think &lt;code&gt;const&lt;/code&gt; — bindings are immutable by default.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight fsharp"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;shipName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Enterprise"&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;registry&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"NCC-1701-D"&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;maxWarpFactor&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No type annotations. F# infers types from usage — &lt;code&gt;shipName&lt;/code&gt; is a &lt;code&gt;string&lt;/code&gt;, &lt;code&gt;maxWarpFactor&lt;/code&gt; is a &lt;code&gt;float&lt;/code&gt;. You &lt;em&gt;can&lt;/em&gt; annotate explicitly, but idiomatic F# lets the compiler do the work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight fsharp"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;shipName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Enterprise"&lt;/span&gt;   &lt;span class="c1"&gt;// valid, but unnecessary here&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Functions use the same &lt;code&gt;let&lt;/code&gt; keyword. There's no &lt;code&gt;function&lt;/code&gt; or &lt;code&gt;func&lt;/code&gt; or &lt;code&gt;fn&lt;/code&gt; — a binding that takes parameters is a function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight fsharp"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;warpSpeed&lt;/span&gt; &lt;span class="n"&gt;factor&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="n"&gt;factor&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;299792&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;458&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;greeting&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="n"&gt;rank&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="s2"&gt;"{rank} {name}, reporting for duty."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;warpSpeed&lt;/code&gt; takes one argument and returns a &lt;code&gt;float&lt;/code&gt;. &lt;code&gt;greeting&lt;/code&gt; takes two and returns a &lt;code&gt;string&lt;/code&gt;. The compiler infers all of it. No return keyword either — the last expression in a function is its return value.&lt;/p&gt;

&lt;p&gt;And if you call &lt;code&gt;warpSpeed&lt;/code&gt; with the wrong type, the compiler catches it immediately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight fsharp"&gt;&lt;code&gt;&lt;span class="n"&gt;warpSpeed&lt;/span&gt; &lt;span class="s2"&gt;"fast"&lt;/span&gt;
&lt;span class="c1"&gt;// Error: This expression was expected to have type 'float'&lt;/span&gt;
&lt;span class="c1"&gt;//        but here has type 'string'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No runtime surprise. The compiler saw that &lt;code&gt;warpSpeed&lt;/code&gt; multiplies its argument by a &lt;code&gt;float&lt;/code&gt;, inferred the parameter must be a &lt;code&gt;float&lt;/code&gt;, and rejected &lt;code&gt;"fast"&lt;/code&gt; at compile time.&lt;/p&gt;

&lt;p&gt;If you're used to braces and semicolons: whitespace is significant in F#. Indentation defines scope, similar to Python. The body of &lt;code&gt;warpSpeed&lt;/code&gt; is indented under its declaration — that's what makes it the body, not a pair of curly braces.&lt;/p&gt;

&lt;p&gt;These three properties — immutable bindings, type inference, and functions defined with &lt;code&gt;let&lt;/code&gt; — are the foundation everything else in this tour builds on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Records
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;type&lt;/code&gt; keyword defines new types. A &lt;strong&gt;record&lt;/strong&gt; is a named collection of fields — the closest analog is a C# &lt;code&gt;record&lt;/code&gt; or a TypeScript object literal with a fixed shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight fsharp"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="nc"&gt;CrewMember&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="nc"&gt;Rank&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="nc"&gt;Department&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="nc"&gt;ClearanceLevel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Creating a record instance looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight fsharp"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;picard&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Jean-Luc Picard"&lt;/span&gt;
    &lt;span class="nc"&gt;Rank&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Captain"&lt;/span&gt;
    &lt;span class="nc"&gt;Department&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Command"&lt;/span&gt;
    &lt;span class="nc"&gt;ClearanceLevel&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice there's no &lt;code&gt;new CrewMember(...)&lt;/code&gt;. F# infers that &lt;code&gt;picard&lt;/code&gt; is a &lt;code&gt;CrewMember&lt;/code&gt; because the field names match — only one record type in scope has that exact set of fields. Type inference extends beyond simple values.&lt;/p&gt;

&lt;p&gt;Field access uses dot notation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight fsharp"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;picard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Name&lt;/span&gt;   &lt;span class="c1"&gt;// "Jean-Luc Picard"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Records are immutable. You don't modify a record — you create a copy with specific fields changed using the &lt;code&gt;with&lt;/code&gt; keyword:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight fsharp"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;promoted&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;picard&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;Rank&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Admiral"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;picard&lt;/code&gt; still has &lt;code&gt;Rank = "Captain"&lt;/code&gt;. &lt;code&gt;promoted&lt;/code&gt; is a separate value with &lt;code&gt;Rank = "Admiral"&lt;/code&gt; and everything else copied. If you've used spread syntax in JavaScript (&lt;code&gt;{ ...picard, rank: "Admiral" }&lt;/code&gt;), it's the same idea with a compile-time guarantee that the field names and types are correct.&lt;/p&gt;

&lt;p&gt;Immutable records with copy-on-write are the default data model in F# — and in Ruuk. You define the shape, the compiler tracks it, and transformations produce new values instead of mutations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lists and Pipelines
&lt;/h2&gt;

&lt;p&gt;F# lists use square brackets. Items are separated by semicolons, or by newlines if each item is on its own line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight fsharp"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;bridge&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Picard"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nc"&gt;Rank&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Captain"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nc"&gt;Department&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Command"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nc"&gt;ClearanceLevel&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Riker"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nc"&gt;Rank&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Commander"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nc"&gt;Department&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Command"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nc"&gt;ClearanceLevel&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"La Forge"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nc"&gt;Rank&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Lt. Commander"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nc"&gt;Department&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Engineering"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nc"&gt;ClearanceLevel&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Crusher"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nc"&gt;Rank&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Commander"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nc"&gt;Department&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Medical"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nc"&gt;ClearanceLevel&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Worf"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nc"&gt;Rank&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Lieutenant"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nc"&gt;Department&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Security"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nc"&gt;ClearanceLevel&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Data"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nc"&gt;Rank&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Lt. Commander"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nc"&gt;Department&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Operations"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nc"&gt;ClearanceLevel&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;F# lists are immutable. Adding an element produces a new list; the original doesn't change.&lt;/p&gt;

&lt;p&gt;The real power of lists in F# is how you process them. The &lt;strong&gt;pipe operator&lt;/strong&gt; &lt;code&gt;|&amp;gt;&lt;/code&gt; takes the result of the left side and passes it as the last argument to the function on the right. This lets you write data transformations as a top-to-bottom pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight fsharp"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;seniorStaff&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="n"&gt;bridge&lt;/span&gt;
    &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;List&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;filter&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ClearanceLevel&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;List&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sortByDescending&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ClearanceLevel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;List&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// ["Picard"; "Riker"; "La Forge"; "Crusher"; "Data"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read it top to bottom: start with the bridge crew, keep only those with clearance 8 or above, sort by clearance descending, extract their names. Each step feeds into the next.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;fun m -&amp;gt; m.ClearanceLevel &amp;gt;= 8&lt;/code&gt; is a lambda — same idea as &lt;code&gt;m =&amp;gt; m.ClearanceLevel &amp;gt;= 8&lt;/code&gt; in C# or JavaScript, different arrow. &lt;code&gt;List.filter&lt;/code&gt;, &lt;code&gt;List.map&lt;/code&gt;, and &lt;code&gt;List.sortByDescending&lt;/code&gt; are direct counterparts to LINQ's &lt;code&gt;Where&lt;/code&gt;, &lt;code&gt;Select&lt;/code&gt;, and &lt;code&gt;OrderByDescending&lt;/code&gt;, or Java's &lt;code&gt;filter&lt;/code&gt;, &lt;code&gt;map&lt;/code&gt;, and &lt;code&gt;sorted&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you've chained LINQ methods or Java streams, pipelines will feel natural. The difference is that pipes compose standalone functions rather than calling methods on an object. Data flows through; functions don't need to know about each other.&lt;/p&gt;

&lt;h2&gt;
  
  
  Discriminated Unions
&lt;/h2&gt;

&lt;p&gt;We're at warp now — this is the feature that matters most for the rest of the series.&lt;/p&gt;

&lt;p&gt;Records describe things that have multiple fields. &lt;strong&gt;Discriminated unions&lt;/strong&gt; describe things that can be one of several cases. The &lt;code&gt;type&lt;/code&gt; keyword does both jobs.&lt;/p&gt;

&lt;p&gt;A simple union with no associated data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight fsharp"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="nc"&gt;Department&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;Command&lt;/span&gt;
    &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;Engineering&lt;/span&gt;
    &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;Medical&lt;/span&gt;
    &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;Science&lt;/span&gt;
    &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;Security&lt;/span&gt;
    &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;Operations&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each case is a distinct value. This replaces the &lt;code&gt;Department: string&lt;/code&gt; we used earlier in &lt;code&gt;CrewMember&lt;/code&gt; — instead of hoping someone types &lt;code&gt;"Engineering"&lt;/code&gt; correctly, the compiler knows exactly which values are valid:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight fsharp"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="nc"&gt;CrewMember&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="nc"&gt;Rank&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="nc"&gt;Department&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Department&lt;/span&gt;
    &lt;span class="nc"&gt;ClearanceLevel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;worf&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Worf"&lt;/span&gt;
    &lt;span class="nc"&gt;Rank&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Lieutenant"&lt;/span&gt;
    &lt;span class="nc"&gt;Department&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Security&lt;/span&gt;
    &lt;span class="nc"&gt;ClearanceLevel&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No quotes around &lt;code&gt;Security&lt;/code&gt; — it's a value of type &lt;code&gt;Department&lt;/code&gt;, not a string. Try assigning &lt;code&gt;Department = "Security"&lt;/code&gt; and the compiler rejects it. Try assigning &lt;code&gt;Department = Tactical&lt;/code&gt; and the compiler rejects that too — &lt;code&gt;Tactical&lt;/code&gt; isn't one of the cases.&lt;/p&gt;

&lt;p&gt;If you're coming from Java or C#, this looks like an enum, and for simple cases it works the same way. The difference is that &lt;strong&gt;each case can carry its own data&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight fsharp"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="nc"&gt;AlertStatus&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;Green&lt;/span&gt;
    &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;Yellow&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;Red&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="n"&gt;threat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;shieldsUp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Green&lt;/code&gt; carries nothing. &lt;code&gt;Yellow&lt;/code&gt; carries a reason. &lt;code&gt;Red&lt;/code&gt; carries a threat description and whether shields are raised. These aren't three variations of the same data shape — each case has its own structure. Try modeling this with a C# enum and you'll end up with a class hierarchy or nullable fields. The union makes it one type.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight fsharp"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;currentAlert&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Yellow&lt;/span&gt; &lt;span class="s2"&gt;"Unidentified vessel on long-range sensors"&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;battleStations&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Red&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Romulan warbird decloaking"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ruuk's &lt;code&gt;outcomes&lt;/code&gt; — which we'll see in the next article — are discriminated unions. When an operation can succeed, fail, or partially fail in domain-specific ways, each outcome is a case with its own data. The compiler knows all of them and can verify you've handled every one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option and Result
&lt;/h2&gt;

&lt;p&gt;Two discriminated unions show up so often in F# that they're built into the standard library. Rust has the same pair (&lt;code&gt;Option&lt;/code&gt; and &lt;code&gt;Result&lt;/code&gt;); if you've used those, this is identical in intent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option&lt;/strong&gt; represents a value that might not exist. It has two cases: &lt;code&gt;Some&lt;/code&gt; with a value inside, or &lt;code&gt;None&lt;/code&gt;. This replaces null — and unlike null, you can't use the value without first deciding what absence means.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight fsharp"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;findCrewMember&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="n"&gt;bridge&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;List&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tryFind&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;found&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;findCrewMember&lt;/span&gt; &lt;span class="s2"&gt;"Worf"&lt;/span&gt;          &lt;span class="c1"&gt;// Some { Name = "Worf"; ... }&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;missing&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;findCrewMember&lt;/span&gt; &lt;span class="s2"&gt;"Kirk"&lt;/span&gt;        &lt;span class="c1"&gt;// None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;List.tryFind&lt;/code&gt; returns an &lt;code&gt;Option&amp;lt;CrewMember&amp;gt;&lt;/code&gt;, not a &lt;code&gt;CrewMember&lt;/code&gt;. You can't call &lt;code&gt;.Name&lt;/code&gt; on the result directly — the compiler knows it might be &lt;code&gt;None&lt;/code&gt;. You have to unwrap it first — which means deciding what happens when nothing is found. No &lt;code&gt;NullReferenceException&lt;/code&gt; three layers away from the actual problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Result&lt;/strong&gt; represents an operation that can succeed or fail, with data in both cases: &lt;code&gt;Ok&lt;/code&gt; carries the success value, &lt;code&gt;Error&lt;/code&gt; carries the failure value.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight fsharp"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="nc"&gt;TransporterFailure&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;SignalLost&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="n"&gt;lastCoords&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;PatternDegradation&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="n"&gt;integrity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt;
    &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;TargetShielded&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="nc"&gt;TransporterTarget&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="nc"&gt;SignalStrength&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt;
    &lt;span class="nc"&gt;LastKnownCoords&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;beamUp&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SignalStrength&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
        &lt;span class="nc"&gt;Ok&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Name&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SignalStrength&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
        &lt;span class="nc"&gt;Error&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;PatternDegradation&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SignalStrength&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
        &lt;span class="nc"&gt;Error&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SignalLost&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LastKnownCoords&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;beamUp&lt;/code&gt; returns &lt;code&gt;Result&amp;lt;string, TransporterFailure&amp;gt;&lt;/code&gt; — either a name on success or a typed failure explaining what went wrong. If you're coming from Java or C#, your instinct might be to throw a &lt;code&gt;SignalLostException&lt;/code&gt;, a &lt;code&gt;PatternDegradationException&lt;/code&gt;, and so on — one exception class per failure mode. That encodes the failures, but it doesn't make them visible in the function's signature. Callers compile fine whether they catch anything or not. Java's checked exceptions tried to fix this, but the syntactic overhead led to so much catch-and-swallow boilerplate that C#, Kotlin, and most modern Java frameworks chose not to adopt the pattern. With &lt;code&gt;Result&lt;/code&gt;, the failure cases live in the return type — they're part of the contract, not a side channel — and the compiler won't let you ignore them.&lt;/p&gt;

&lt;p&gt;Ruuk inherits &lt;code&gt;Option&lt;/code&gt; and &lt;code&gt;Result&lt;/code&gt; from this lineage and extends the idea further. Where F# gives you two slots — &lt;code&gt;Ok&lt;/code&gt; or &lt;code&gt;Error&lt;/code&gt; — Ruuk's operations declare arbitrary domain-specific outcomes as first-class members of the signature. The next article shows what that looks like.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern Matching
&lt;/h2&gt;

&lt;p&gt;Once you inspect an &lt;code&gt;Option&lt;/code&gt; or &lt;code&gt;Result&lt;/code&gt;, F# gives you a precise tool for handling its cases: &lt;code&gt;match&lt;/code&gt;/&lt;code&gt;with&lt;/code&gt;, which branches on the shape of data.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight fsharp"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;transportReport&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt;
    &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;Ok&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
        &lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="s2"&gt;"Transport complete. {name} is aboard."&lt;/span&gt;
    &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SignalLost&lt;/span&gt; &lt;span class="n"&gt;coords&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
        &lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="s2"&gt;"Signal lost at {coords}. Dispatching shuttle."&lt;/span&gt;
    &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;PatternDegradation&lt;/span&gt; &lt;span class="n"&gt;integrity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
        &lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="s2"&gt;"Pattern integrity at {integrity}. Boosting signal."&lt;/span&gt;
    &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt; &lt;span class="nc"&gt;TargetShielded&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
        &lt;span class="s2"&gt;"Cannot beam through shields. Hailing target."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each branch matches a specific shape and destructures its data. &lt;code&gt;Error (SignalLost coords)&lt;/code&gt; doesn't just check that the result is an error — it checks that it's specifically a &lt;code&gt;SignalLost&lt;/code&gt; error and pulls out the coordinates in one step. No casting, no &lt;code&gt;instanceof&lt;/code&gt;, no nested &lt;code&gt;if&lt;/code&gt; chains.&lt;/p&gt;

&lt;p&gt;Here's the important part: &lt;strong&gt;the compiler checks that every case is handled.&lt;/strong&gt; Remove the &lt;code&gt;TargetShielded&lt;/code&gt; branch and the compiler warns you:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Incomplete pattern matches on this expression.
For example, the value 'Error TargetShielded' may indicate
a case not covered by the pattern(s).
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is &lt;strong&gt;exhaustive checking&lt;/strong&gt;. The compiler knows every case in the union and verifies you've accounted for all of them. Add a new case to &lt;code&gt;TransporterFailure&lt;/code&gt; — say &lt;code&gt;WarpFieldInterference&lt;/code&gt; — and every &lt;code&gt;match&lt;/code&gt; expression that doesn't handle it gets flagged. F# isn't alone here — Java's switch expressions on sealed types and Rust's &lt;code&gt;match&lt;/code&gt; enforce the same guarantee. But F# has had it from the start, and it's one of the language's strongest properties: a closed, compiler-verified contract between the type definition and every piece of code that consumes it. It's the property the rest of this series depends on. When Ruuk declares that an operation has four outcomes, this is the mechanism that ensures every caller handles all four.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;That's enough F# to read Ruuk. Six concepts: &lt;code&gt;let&lt;/code&gt; bindings, records, lists and pipelines, discriminated unions, &lt;code&gt;Option&lt;/code&gt;/&lt;code&gt;Result&lt;/code&gt;, and pattern matching with exhaustive checking. If you followed the transporter example — a function that returns a typed result, where pattern matching lets the compiler check every handled failure mode — you have the mental model for what comes next.&lt;/p&gt;

&lt;p&gt;The next article introduces Ruuk's &lt;code&gt;op&lt;/code&gt; keyword and its &lt;code&gt;outcomes&lt;/code&gt; block — where these F# foundations meet domain-specific compiler enforcement. The compiler takes it from there.&lt;/p&gt;

&lt;p&gt;For deeper F# coverage, Scott Wlaschin's &lt;a href="https://fsharpforfunandprofit.com/" rel="noopener noreferrer"&gt;F# for Fun and Profit&lt;/a&gt; and Isaac Abraham's &lt;a href="https://www.manning.com/books/get-programming-with-f-sharp" rel="noopener noreferrer"&gt;Get Programming with F#&lt;/a&gt; are both excellent starting points for developers coming from OOP languages.&lt;/p&gt;

&lt;p&gt;Ruuk is in &lt;a href="https://github.com/ruuk-lang/ruuk/releases" rel="noopener noreferrer"&gt;alpha&lt;/a&gt;. If these ideas resonate, give ruuk a spin; follow along on &lt;a href="https://github.com/ruuk-lang/ruuk" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; and weigh in on the discussions.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>architecture</category>
      <category>software</category>
    </item>
    <item>
      <title>Your Compiler Is Missing from the Party</title>
      <dc:creator>Mat Weiss</dc:creator>
      <pubDate>Thu, 30 Apr 2026 12:00:00 +0000</pubDate>
      <link>https://dev.to/matweiss/your-compiler-is-missing-from-the-party-4bf1</link>
      <guid>https://dev.to/matweiss/your-compiler-is-missing-from-the-party-4bf1</guid>
      <description>&lt;p&gt;Handwriting code is the new cursive. AI agents write code competently, and they're improving fast. My recent agentic work spans refactoring C++ machine learning libraries, writing CLIs in Rust, building web apps in ASP.NET, and shipping mobile apps in Flutter. For the mechanical parts — scaffolding, boilerplate, repetitive transformations — agents handle it well. I've used them to write entire features on their own as well.&lt;/p&gt;

&lt;p&gt;It makes you wonder, if the agent writes the code, is the language is an implementation detail — and the intuition makes sense: why care about syntax you'll never type? But the language isn't just what the agent writes in. It's what the compiler checks, what the human reviews, and what determines how fast the feedback loop closes. Agentic coding raises the stakes for language design, and points toward specific properties a language should have.&lt;/p&gt;

&lt;h2&gt;
  
  
  Better Compiler Feedback Multiplies Agent Productivity
&lt;/h2&gt;

&lt;p&gt;My agentic coding experience has varied by language. Part of that is training data — AI models have seen more code in some languages than others. But the larger factor is whether mistakes are caught at compile time or runtime, which determines how quickly the loop closes.&lt;/p&gt;

&lt;p&gt;In Rust, when an agent generates something wrong, the compiler identifies the exact location, names the violated constraint, and usually suggests a fix. The agent iterates on that feedback directly — no execution required. When checks happen at runtime instead, there's an extra round-trip: generate code, execute tests, parse results, feed output back to the agent. More wall time per iteration, more tokens spent on test output instead of code. Same agent, longer loop, higher cost per correction. And those runtime checks are only as good as the inputs that exercise them. Communities built around runtime-checked languages know this well — it's why they invest heavily in defensive testing, property-based tools like &lt;a href="https://hypothesis.readthedocs.io" rel="noopener noreferrer"&gt;Hypothesis&lt;/a&gt;, and comprehensive test suites. But even thorough tests depend on the paths you think to exercise. A compile-time check flags the error unconditionally.&lt;/p&gt;

&lt;p&gt;This isn't a judgment on any particular language — it's a property of where in the cycle errors surface. The industry has started redesigning CLI tools for agents — Trevin Chow's &lt;a href="https://trevinsays.com/p/7-principles-for-agent-friendly-clis" rel="noopener noreferrer"&gt;seven principles for agent-friendly CLIs&lt;/a&gt; captures the pattern: structured output, unambiguous interfaces, clear error signals. The same thinking applies to compilers. The compiler is the agent's primary feedback surface — and most compilers were designed before agentic coding existed.&lt;/p&gt;

&lt;p&gt;Better compiler output is the starting point. The deeper question is what classes of mistakes the compiler can catch. The best current compilers handle memory errors (Rust), type mismatches (TypeScript), and null dereferences (Kotlin). Entire categories of domain mistakes remain invisible: unhandled operation outcomes, invalid state transitions, missing field mappings when converting between data representations. These aren't obscure edge cases — they slip past review and surface in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Development Model Needs a Third Party
&lt;/h2&gt;

&lt;p&gt;Our current approach to AI coding is a two-party arrangement: humans describe intent, agents write code. Somebody is missing the party.&lt;/p&gt;

&lt;p&gt;The danger isn't that AI writes bad code. It's that humans lose the ability to evaluate what AI writes — and the two-party model accelerates this. The volume of AI-generated code is already outpacing review capacity; the trend is toward reviewing less, not more. That makes the compiler more load-bearing, not less. A smarter AI doesn't close this gap on its own — even the best engineer benefits from code review, because independent verification catches what self-consistency misses. The same principle applies to generated code, except the stakes are higher: the reviewer understands less of the codebase with each generation cycle.&lt;/p&gt;

&lt;p&gt;The Rust model points at the answer: compiler-enforced properties rather than runtime hopes. The question is whether we can extend that from memory safety to operational semantics. Expanded compiler checking gives us a basis for trusting AI-generated code — not because the AI earned that trust, but because an independent third party verified the structural claims. Checks and balances: human + compiler + AI, each with a distinct responsibility.&lt;/p&gt;

&lt;p&gt;The human defines the operation's shape: its outcomes, state transitions, data projections. The AI generates the implementation body. The compiler stands between them, rejecting anything that violates the declared structure. This changes what a developer needs to review. The programmer's job is to get the declaration right — to ensure the outcome variants are exhaustive, the state transitions are valid, the field projections are accurate. That's the work only a human can do: verifying that the declaration honestly represents the domain. Once that's done, the compiler owns enforcement everywhere.&lt;/p&gt;

&lt;p&gt;The languages most of us work in weren't designed for the assumption that you'd be reading tens of thousands of lines generated by someone else, at volume, under time pressure. That's the new reality. The closer a language's constructs map to domain concepts, the less translation the reader's brain performs — and the faster a developer can audit generated code for correctness.&lt;/p&gt;

&lt;h2&gt;
  
  
  Domain Knowledge Belongs in the Declaration
&lt;/h2&gt;

&lt;p&gt;The common thread between better compiler feedback, the three-party model, and cheaper review is semantic content — how much meaning the language lets you encode, and how much of that meaning the compiler verifies.&lt;/p&gt;

&lt;p&gt;Current compilers have limited capability to check domain rules. Your service calls a payment gateway — the response can mean charged, declined, fraud-held, or gateway failure. Most languages give you a status code and a body; whether you distinguish "declined" from "fraud hold" depends on your discipline, not the compiler. A &lt;code&gt;User&lt;/code&gt; has twenty fields; an API response should expose five of them. Derive the response type by hand, add a field to &lt;code&gt;User&lt;/code&gt; next quarter, and now you're trusting every downstream DTO to have been updated.&lt;/p&gt;

&lt;p&gt;The convention of documenting this is everywhere — Javadoc's &lt;code&gt;@throws&lt;/code&gt;, Python's &lt;code&gt;Raises&lt;/code&gt;, OpenAPI's response schemas. The problem is that none of it is compiler-visible. You can document four outcomes and handle three. The gap stays invisible until production. Here's a typical pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/**
 * @throws DeclinedError
 * @throws FraudHoldError
 * @throws GatewayError
 */&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;chargeCard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ChargeRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;gateway&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PaymentGateway&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Receipt&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The return type promises a &lt;code&gt;Receipt&lt;/code&gt; — that's the happy path. The three failure modes live in a JSDoc comment the compiler will never read. A caller that ignores all three error cases compiles without a warning. The information is there, but it's decorative.&lt;/p&gt;

&lt;p&gt;Here is the same operation in Ruuk, a language designed to include this information where the compiler can verify it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight fsharp"&gt;&lt;code&gt;&lt;span class="n"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;op&lt;/span&gt; &lt;span class="n"&gt;chargeCard&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ChargeRequest&lt;/span&gt;
    &lt;span class="n"&gt;via&lt;/span&gt; &lt;span class="n"&gt;gateway&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;PaymentGateway&lt;/span&gt;
    &lt;span class="n"&gt;outcomes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
        &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;Charged&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nc"&gt;Receipt&lt;/span&gt;
        &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;Declined&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nc"&gt;DeclineReason&lt;/span&gt;
        &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;FraudHold&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nc"&gt;ReviewId&lt;/span&gt;
        &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;GatewayError&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nc"&gt;ErrorDetail&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This declares not just inputs but what role each plays (&lt;code&gt;payload&lt;/code&gt; is the data being acted on, &lt;code&gt;via&lt;/code&gt; is the external system being called) and what outcomes the caller must handle. If you call &lt;code&gt;chargeCard&lt;/code&gt; and don't handle the &lt;code&gt;FraudHold&lt;/code&gt; outcome, it doesn't compile. The meaning you would have put in a comment is now visible to the compiler — and enforced by it.&lt;/p&gt;

&lt;p&gt;The same declaration that gives the compiler more to verify also makes the code faster to cold-read. A developer scanning AI-generated code can evaluate &lt;code&gt;chargeCard&lt;/code&gt;'s shape in seconds: what it takes, where the data goes, what can go wrong. No implementation diving required.&lt;/p&gt;

&lt;p&gt;This design direction asks developers to formalize what they already know — it's on the whiteboard, in the docs, in comments scattered through the codebase. You already know your payment operation has four possible outcomes. The language asks you to type that in. The compiler takes it from there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Agentic coding hasn't reduced the importance of language design — it's exposed where it needs to grow. The properties that improve the agentic loop are the same ones that improve human review: more meaning in the syntax, more verification in the compiler, less translation between what the code says and what the domain demands.&lt;/p&gt;

&lt;p&gt;That points toward a class of language, not a single answer. Ruuk is my attempt to build one — designed from the start for the world where agents write the code and humans verify the shape. Hopefully it won't be the only attempt, and competition here is genuinely good. The industry needs more people thinking about this problem.&lt;/p&gt;

&lt;p&gt;The articles that follow make the design concrete. The next piece is a fast tour of the OCaml/F# syntax Ruuk builds on — enough to read the examples without getting lost. After that: how operations and outcomes give the compiler visibility into failure modes, and how projections enforce structural rules when data crosses boundaries. The chargeCard declaration above is the destination. The series shows how you get there.&lt;/p&gt;

&lt;p&gt;Ruuk is in &lt;a href="https://github.com/ruuk-lang/ruuk/releases" rel="noopener noreferrer"&gt;alpha&lt;/a&gt; — the language is available to try, and the design is still evolving. If the ideas in this article resonate, give ruuk a spin; follow along on &lt;a href="https://github.com/ruuk-lang/ruuk" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; and weigh in on the discussions. The best languages get shaped by the people who care about the problems they solve.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>software</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Code Intelligence Is Being Retrofitted. Ruuk Builds It In.</title>
      <dc:creator>Mat Weiss</dc:creator>
      <pubDate>Fri, 17 Apr 2026 12:01:48 +0000</pubDate>
      <link>https://dev.to/matweiss/code-intelligence-is-being-retrofitted-ruuk-builds-it-in-1nb</link>
      <guid>https://dev.to/matweiss/code-intelligence-is-being-retrofitted-ruuk-builds-it-in-1nb</guid>
      <description>&lt;p&gt;&lt;em&gt;A response to Thoughtworks Technology Radar Vol. 34, Blip 18: Code Intelligence as Agentic Tooling&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;a href="https://www.thoughtworks.com/radar/techniques/summary/code-intelligence-as-agentic-tooling" rel="noopener noreferrer"&gt;Blip 18&lt;/a&gt; of the April 2026 Thoughtworks Radar names a real problem: AI coding agents are effectively blind to the meaning of the code they operate on. The Radar's answer is richer tooling — LSP integrations, OpenRewrite's Lossless Semantic Tree, JetBrains MCP servers.&lt;/p&gt;

&lt;p&gt;That's the practical path for mainstream languages. But it's still using the AST to &lt;em&gt;infer&lt;/em&gt; intent. What would it look like to use the AST to &lt;em&gt;see&lt;/em&gt; it?&lt;/p&gt;

&lt;p&gt;Ruuk — a language I'm designing — takes a different position: the constraints worth enforcing should be in the language itself.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the AST Cannot Tell You
&lt;/h2&gt;

&lt;p&gt;Take a typical enterprise operation: approving an order. In Java, an agent with full LSP access sees something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;ApprovalResult&lt;/span&gt; &lt;span class="nf"&gt;approveOrder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OrderId&lt;/span&gt; &lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="no"&gt;CSR&lt;/span&gt; &lt;span class="n"&gt;csr&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Order&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;orderStore&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// ... validation logic&lt;/span&gt;
    &lt;span class="c1"&gt;// ... state transition&lt;/span&gt;
    &lt;span class="c1"&gt;// ... outcome handling&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent knows the function name, its signature, its call sites, and the types it touches. What it doesn't know is more consequential:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;order&lt;/code&gt; must be in &lt;code&gt;Created&lt;/code&gt; state for this call to be valid. Calling it on a &lt;code&gt;Cancelled&lt;/code&gt; order is a logic error, not a type error — the compiler won't catch it, and neither will the agent.&lt;/li&gt;
&lt;li&gt;There are exactly two outcomes — &lt;code&gt;Approved&lt;/code&gt; and &lt;code&gt;Rejected&lt;/code&gt; — and callers must handle both. The agent has to read the implementation to find out.&lt;/li&gt;
&lt;li&gt;This is an instantaneous state transition, not a long-running process. That distinction matters for error handling and compensation strategy. The function signature doesn't say.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;orderStore&lt;/code&gt; is being mutated — it's an entity undergoing state change, not a read-only data source. The agent can't tell that from the call.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Modern Java closes some of these gaps. Sealed classes and pattern matching (Java 21+) give you exhaustive outcome handling. Some languages go further: TypeScript's discriminated unions and Rust's typestate patterns encode more intent into the type system. But even in those languages, the precondition that the order must be in &lt;code&gt;Created&lt;/code&gt; state remains a convention, not a compiler-checked constraint. The resource mutation role is still invisible. The state machine is still implicit. They moved the needle on &lt;em&gt;what can happen&lt;/em&gt;; they didn't touch &lt;em&gt;what must be true before&lt;/em&gt; or &lt;em&gt;what kind of change is occurring&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;An agent can reconstruct those properties for any well-written function by reading its implementation and tests. The problem is scale. Across a codebase of thousands of operations, inference compounds uncertainty. It fails hardest on the code that needs agents most: legacy systems, inconsistent patterns, missing tests. Structural declarations don't degrade. The thousandth operation is as machine-readable as the first.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Ruuk Exposes at Declaration Time
&lt;/h2&gt;

&lt;p&gt;In Ruuk, the same operation looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight fsharp"&gt;&lt;code&gt;&lt;span class="n"&gt;resource&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Created&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Approved&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Shipped&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Delivered&lt;/span&gt;

&lt;span class="n"&gt;op&lt;/span&gt; &lt;span class="n"&gt;approveOrder&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Created&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;by&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;goal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;OrderStore&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="n"&gt;performs&lt;/span&gt; &lt;span class="nn"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Created&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Approved&lt;/span&gt;
    &lt;span class="n"&gt;outcomes&lt;/span&gt;
        &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;Approved&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Approved&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;Rejected&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything the Java version hid is in the declaration. The precondition is in the type: &lt;code&gt;Order&amp;lt;Created&amp;gt;&lt;/code&gt;. Passing an &lt;code&gt;Order&amp;lt;Cancelled&amp;gt;&lt;/code&gt; is a compile error. The state transition is explicit in the &lt;code&gt;performs&lt;/code&gt; clause. The outcomes are enumerated, and the compiler verifies that every call site handles both. The &lt;code&gt;subject&lt;/code&gt; role marks the order as the entity being changed; the &lt;code&gt;goal&lt;/code&gt; role marks the store as the mutation target, distinct from a read-only data source.&lt;/p&gt;

&lt;p&gt;The compiler checks these declarations against the implementation. If the function body transitions to a state that doesn't match the &lt;code&gt;performs&lt;/code&gt; clause, it won't compile. If you add an outcome variant without updating call sites, they won't compile. Declarations can't drift from reality because they're verified at compile time, then erased. Zero runtime cost.&lt;/p&gt;

&lt;p&gt;That's a meaningful difference from design-by-contract predecessors like Eiffel and JML, where contracts were primarily runtime-checked or relied on external verification tools. In Ruuk, the declaration &lt;em&gt;is&lt;/em&gt; the constraint, and the compiler enforces it statically.&lt;/p&gt;

&lt;p&gt;I designed it this way because the information always existed. Every team I've worked with on enterprise applications knew their preconditions, their state machines, their failure modes. They just had no place to put that knowledge where the compiler could use it.&lt;/p&gt;

&lt;p&gt;From this declaration alone, an agent can answer — without reading one line of implementation — every question the Java version left open. The Radar points to the AST and the richer Lossless Semantic Tree as the right representations for agent tooling. Those capture structure: how code is organized, what the syntactic relationships are. Ruuk's declarations go further — they capture &lt;em&gt;meaning&lt;/em&gt;: what must be true, what can happen, and what kind of change is occurring.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Could an Agent Do With This?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Impact analysis.&lt;/strong&gt; When &lt;code&gt;Order&lt;/code&gt; gains a new field, an agent can ask: which projections include this field? A Ruuk projection is a typed, compiler-checked subset of a resource's fields. &lt;code&gt;CustomerOrderView = Order only { id; customer; total }&lt;/code&gt; declares exactly which fields downstream consumers can see. Finding every projection affected by a schema change is a structural query, not a grep through the codebase.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Outcome coverage.&lt;/strong&gt; Which operations have outcomes stubbed with &lt;code&gt;todo&lt;/code&gt;? In Ruuk, &lt;code&gt;todo&lt;/code&gt; is a compiler-tracked placeholder, not a comment. Which call sites are missing a handler for a specific variant? The answers are compiler-verified facts, not heuristic inference.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;State machine queries.&lt;/strong&gt; A resource like &lt;code&gt;Order&lt;/code&gt; carries its current state in its type: &lt;code&gt;Order&amp;lt;Approved&amp;gt;&lt;/code&gt;, &lt;code&gt;Order&amp;lt;Shipped&amp;gt;&lt;/code&gt;. The compiler knows which operations are valid for which states. What transitions are reachable from &lt;code&gt;Order&amp;lt;Created&amp;gt;&lt;/code&gt;? What operations apply when the order is in transit? The answers come from the declared lifecycle, queryable and independent of implementation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Role-based refactoring.&lt;/strong&gt; An agent told to "add audit logging to every operation that mutates order state" doesn't need to read method bodies. It queries the declared &lt;code&gt;subject&lt;/code&gt; role across every operation, identifies the affected call sites, and makes the change. The semantic structure tells it exactly where to edit and what contract to preserve.&lt;/p&gt;

&lt;p&gt;In each case, the agent spends fewer tokens reconstructing context and gets closer to a correct edit on the first pass. That's a ceiling that better tooling can't raise if the language never captured the information.&lt;/p&gt;




&lt;p&gt;Blip 18 asks how to make agents smarter about existing code. That's the right question for existing codebases, and the Radar's answers are practical.&lt;/p&gt;

&lt;p&gt;Ruuk is exploring a different one: what happens when the language itself captures enough domain semantics that agents — and humans — can reason from declarations alone?&lt;/p&gt;

&lt;p&gt;That's a bet on language design, not tooling. Ruuk is in early development, and none of this has been tested at scale with real agents. But I believe the overhead pays for itself: what Ruuk asks you to declare is what you already know. The cost isn't in knowing your preconditions and state machines. It's in not having a compiler that checks them.&lt;/p&gt;




&lt;p&gt;The language design is documented on &lt;a href="https://github.com/ruuk-lang/ruuk" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;, where I'm using Discussions to think through design decisions in the open. I also write about Ruuk's design rationale here on &lt;a href="https://dev.to/matweiss"&gt;dev.to&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>ai</category>
      <category>software</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
