<?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.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>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 pre-alpha. If these ideas resonate, 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 pre-alpha. If these ideas resonate, 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 pre-alpha. What I can show right now is the thinking behind it — and a place to engage with the design before it solidifies. If the ideas in this article resonates, 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>
