<?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: stepinto.dev</title>
    <description>The latest articles on DEV Community by stepinto.dev (@stepinto).</description>
    <link>https://dev.to/stepinto</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4186945%2F27bce636-dcde-4827-83aa-0f2f1f2a14e9.png</url>
      <title>DEV Community: stepinto.dev</title>
      <link>https://dev.to/stepinto</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/stepinto"/>
    <language>en</language>
    <item>
      <title>Learning the Heater From Its Own Logs</title>
      <dc:creator>stepinto.dev</dc:creator>
      <pubDate>Fri, 16 Oct 2026 19:44:40 +0000</pubDate>
      <link>https://dev.to/stepinto/learning-the-heater-from-its-own-logs-1g9o</link>
      <guid>https://dev.to/stepinto/learning-the-heater-from-its-own-logs-1g9o</guid>
      <description>&lt;p&gt;The &lt;a href="https://stepinto.dev/posts/world-models-101/" rel="noopener noreferrer"&gt;first code post&lt;/a&gt; in this series had a &lt;code&gt;predict&lt;/code&gt; function whose body I typed by hand. Heat leaks towards outside at a tenth of the difference per step, the heater adds a degree and a half, and I made both of those numbers up. That was fine for showing the shape of a world model. It is useless for a real room, because a real room does not tell you its constants. What it gives you instead is a log.&lt;/p&gt;

&lt;p&gt;This post throws the hand-written body away and fits a new one from logged state and action pairs. The model is four numbers. The training loop is twenty lines. At the end it predicts a room it has never seen more closely than my guess did, and you can &lt;a href="https://stepinto.dev/heater/" rel="noopener noreferrer"&gt;run the whole thing in your browser&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  A room that keeps its constants to itself
&lt;/h2&gt;

&lt;p&gt;I don't have a thermostat wired up to this blog, so a simulator stands in for the hardware. It has the same shape as the 101 &lt;code&gt;predict&lt;/code&gt;, with two changes: the constants are different from the ones I guessed, and there is a little noise, because someone keeps opening the door.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;REAL_LEAK&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.07&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// better insulated than I guessed&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;REAL_HEAT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;    &lt;span class="c1"&gt;// and a weaker heater&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;NOISE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;        &lt;span class="c1"&gt;// draughts, in °C per step&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;realRoom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;State&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;State&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;heaterOn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;turnHeaterOn&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;turnHeaterOff&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;heaterOn&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;leak&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outsideTemp&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;roomTemp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;REAL_LEAK&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;heat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;heaterOn&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;REAL_HEAT&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;draught&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;gauss&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;NOISE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;roomTemp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;roomTemp&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;leak&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;heat&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;draught&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;outsideTemp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outsideTemp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;heaterOn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rule for the rest of the post is that nothing below this line gets to look inside &lt;code&gt;realRoom&lt;/code&gt;. The model only sees what a logger would see.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recording the logs
&lt;/h2&gt;

&lt;p&gt;The log comes from running a thermostat against the room. Each episode picks an outside temperature between 2 and 12 degrees, a starting room temperature between 10 and 22, a setpoint between 18 and 21, and runs sixty steps. One step in five the thermostat does something random instead, because a log with only sensible decisions in it never shows the model what a daft decision does. Eighty episodes gives 4,800 transitions, each one the &lt;code&gt;Transition&lt;/code&gt; type from the 101 post.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;episodes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;Transition&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Transition&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;episodes&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;State&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;roomTemp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;rand&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;outsideTemp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;rand&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;heaterOn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;rand&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;setpoint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;rand&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;thermostat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setpoint&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nextState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;realRoom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;nextState&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;nextState&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;log&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here are the first two rows, which is all of the data the model ever gets, four thousand eight hundred times over.&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="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;roomTemp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;16.51&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;outsideTemp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;7.62&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;heaterOn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;turnHeaterOn&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;nextState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;roomTemp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;17.09&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;outsideTemp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;7.62&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;heaterOn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&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="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;roomTemp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;17.09&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;outsideTemp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;7.62&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;heaterOn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;wait&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;nextState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;roomTemp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;17.75&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;outsideTemp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;7.62&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;heaterOn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&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;If you run energy hardware, this is your telemetry table with the commands joined on, which was the whole point of the &lt;a href="https://stepinto.dev/posts/world-models-001/" rel="noopener noreferrer"&gt;first post&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The WHERE clause runs out
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://stepinto.dev/posts/world-models-102/" rel="noopener noreferrer"&gt;last post&lt;/a&gt; computed a world model with a &lt;code&gt;filter&lt;/code&gt; and a count: keep the rows where the state and action matched, tally what happened next. It worked because temperatures were bucketed into &lt;code&gt;cold&lt;/code&gt;, &lt;code&gt;mild&lt;/code&gt; and &lt;code&gt;warm&lt;/code&gt;. Try it on the real log and it falls over immediately.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;probe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1234&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;matches&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;roomTemp&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;probe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;roomTemp&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
  &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outsideTemp&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;probe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outsideTemp&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
  &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;heaterOn&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;probe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;heaterOn&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
  &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;probe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// 1, and it's row 1234 itself&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Loosening the match to within a twentieth of a degree finds two rows. A continuous state almost never recurs, so the condition on the right of the bar almost never matches, which is the same wall the LLM equation hit in that post. The fix there was a very large neural network. The fix here is a very small one, because the room is a very small world.&lt;/p&gt;

&lt;h2&gt;
  
  
  The model is four numbers
&lt;/h2&gt;

&lt;p&gt;What I want is a function with the &lt;code&gt;predict&lt;/code&gt; signature whose body I did not write. The simplest body that could work is a weighted sum. Take the readings the logger can see, multiply each by a weight, add them up, and call the result the change in room temperature for this step.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Weights&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;room&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;outside&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;heater&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;bias&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;features&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;State&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;heaterOn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;turnHeaterOn&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;turnHeaterOff&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;heaterOn&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;room&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;roomTemp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;outside&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outsideTemp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;heater&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;heaterOn&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;bias&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;heaterOn&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;predictWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Weights&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;State&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;State&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;features&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;delta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;room&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;room&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outside&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outside&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;heater&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;heater&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bias&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bias&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;roomTemp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;roomTemp&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;outsideTemp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outsideTemp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;heaterOn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;heaterOn&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;Predicting the &lt;em&gt;change&lt;/em&gt; rather than the next temperature outright is the one design decision in here, and it matters more than it looks. The next temperature is dominated by the current temperature, so a model asked to predict it spends its capacity learning "next is roughly the same as now" and has little left for the interesting part. The change is the interesting part on its own.&lt;/p&gt;

&lt;p&gt;Notice what the model is not given. It does not know that the leak depends on the difference between inside and outside. It gets the two temperatures as separate inputs and has to discover the relationship, if there is one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The training loop
&lt;/h2&gt;

&lt;p&gt;Training means finding the four weights that make &lt;code&gt;predictWith&lt;/code&gt; agree with the log. Gradient descent does that the slow and honest way: start at zero, measure how wrong the model is across every row, nudge each weight in the direction that reduces the error, repeat.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;train&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Transition&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nx"&gt;epochs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Weights&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;room&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;outside&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;heater&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;bias&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;losses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;epoch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;epoch&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;epochs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;epoch&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Weights&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;room&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;outside&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;heater&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;bias&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;loss&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;features&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nextState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;roomTemp&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;roomTemp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;guess&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;room&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;room&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outside&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outside&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;heater&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;heater&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bias&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bias&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;guess&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;loss&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;room&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;room&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outside&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outside&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;heater&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;heater&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bias&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bias&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;room&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="nx"&gt;lr&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;room&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outside&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="nx"&gt;lr&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outside&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;heater&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="nx"&gt;lr&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;heater&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bias&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="nx"&gt;lr&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bias&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;losses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;loss&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;losses&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;losses&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;train&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.003&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;err * f.room&lt;/code&gt; lines are the entire derivative. For a weighted sum with a squared error, the gradient of the loss with respect to each weight is the error times that weight's input, which is why no library is needed. Ten thousand passes over 4,800 rows takes about 300 milliseconds in Node 23.&lt;/p&gt;

&lt;p&gt;{{&amp;lt; chart name="heater-loss" csv="heater-loss" caption="Mean squared error per epoch. It flattens at 0.01, which is the variance of the draughts: the model has learned everything in the log that is not noise." &amp;gt;}}&lt;/p&gt;

&lt;p&gt;Ten thousand epochs is a lot for four weights, and the reason is worth knowing. Room temperature and outside temperature move together in the log (warm days make warm rooms), so the loss surface is a long thin valley and plain gradient descent zigzags down it. Scaling the inputs or using a smarter optimiser would fix that in a few hundred epochs. I left it slow because the loop is easier to read this way, and because the playground is more fun when you can watch it converge.&lt;/p&gt;

&lt;p&gt;Here are the weights it found, next to the constants &lt;code&gt;realRoom&lt;/code&gt; was hiding.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;weight&lt;/th&gt;
&lt;th&gt;learned&lt;/th&gt;
&lt;th&gt;truth&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;room&lt;/td&gt;
&lt;td&gt;-0.074&lt;/td&gt;
&lt;td&gt;-0.07&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;outside&lt;/td&gt;
&lt;td&gt;0.069&lt;/td&gt;
&lt;td&gt;0.07&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;heater&lt;/td&gt;
&lt;td&gt;1.19&lt;/td&gt;
&lt;td&gt;1.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bias&lt;/td&gt;
&lt;td&gt;0.077&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The room and outside weights came out equal and opposite. Nobody told the model that heat flow depends on the difference between the two temperatures. It found that from the logs, which is the small version of the thing world models are for. The bias absorbed a little of the room weight's share, because the two inputs are correlated and the noise lets them trade off. That is what a fit looks like when the data is real rather than typed in.&lt;/p&gt;

&lt;p&gt;And &lt;code&gt;predict&lt;/code&gt; now has a learned body with the same signature as before.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;predict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;State&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;predictWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A room it has never seen
&lt;/h2&gt;

&lt;p&gt;The test that matters is a room outside the training range. Every episode in the log had an outside temperature between 2 and 12 degrees. The test room is at minus four, starts at 22, and follows a script the thermostat never produced: heater off for ten steps, on for ten, off for ten. The same script runs through three functions: the real room, the learned model, and the hand-written guess from 101.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;unseen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;State&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;roomTemp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;outsideTemp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;heaterOn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// rollout from 101, with the step function passed in instead of baked in&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;truth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;rollout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;unseen&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;realRoom&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;learned&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;rollout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;unseen&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;predict&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;guessed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;rollout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;unseen&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;predictGuess&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;{{&amp;lt; chart name="heater-rollout" csv="heater-rollout" caption="Thirty steps at minus four outside. The learned model tracks the real room to within a few tenths of a degree the whole way. The hand-written guess leaks too fast and heats too hard, and is three degrees adrift by step thirty." &amp;gt;}}&lt;/p&gt;

&lt;p&gt;The numbers at the three turning points:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;step&lt;/th&gt;
&lt;th&gt;real room&lt;/th&gt;
&lt;th&gt;learned&lt;/th&gt;
&lt;th&gt;hand-written&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;10, heater goes on&lt;/td&gt;
&lt;td&gt;8.78&lt;/td&gt;
&lt;td&gt;8.79&lt;/td&gt;
&lt;td&gt;5.07&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20, heater goes off&lt;/td&gt;
&lt;td&gt;11.02&lt;/td&gt;
&lt;td&gt;11.28&lt;/td&gt;
&lt;td&gt;8.93&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;3.69&lt;/td&gt;
&lt;td&gt;3.80&lt;/td&gt;
&lt;td&gt;0.51&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Root mean square error over the thirty steps is 0.20 degrees for the learned model and 2.77 for my guess. The model extrapolated to an outside temperature six degrees colder than anything in its logs, because what it learned was the physics rather than the trajectories. A lookup table could not have done that, and neither could I, since I had the constants wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  What four weights cannot hold
&lt;/h2&gt;

&lt;p&gt;The playground at &lt;a href="https://stepinto.dev/heater/" rel="noopener noreferrer"&gt;/heater/&lt;/a&gt; runs all of the above. Change the room's constants, record fresh logs, press train, and watch the weights settle while the prediction on the right pulls into line with the real room. Turn the draughts up and the loss floor rises to match. Set the leak rate to 0.10 and the heater to 1.5 and the hand-written guess becomes correct, which is a nice reminder that my guess was only ever a model with its weights typed in.&lt;/p&gt;

&lt;p&gt;It also shows where this model stops. The weighted sum can only learn relationships that are a straight line through the inputs it can see. The room I built is linear and fully observed, so four weights are enough. Give the real room a door that opens and closes on a schedule the logger never records, and the best a linear model can do is average over the door, predicting a room that is always half open. The field it needs is one that is not in the log. The &lt;a href="https://stepinto.dev/posts/world-models-102/" rel="noopener noreferrer"&gt;102 post&lt;/a&gt; called that a private field, and the next post gives the model one and watches it work out the door on its own.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>worldmodels</category>
      <category>typescript</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>A Partial JSON Parser You Can npm install</title>
      <dc:creator>stepinto.dev</dc:creator>
      <pubDate>Fri, 16 Oct 2026 14:56:44 +0000</pubDate>
      <link>https://dev.to/stepinto/a-partial-json-parser-you-can-npm-install-39no</link>
      <guid>https://dev.to/stepinto/a-partial-json-parser-you-can-npm-install-39no</guid>
      <description>&lt;p&gt;Two years ago I wrote &lt;a href="https://stepinto.dev/posts/streaming-json-partial-parser/" rel="noopener noreferrer"&gt;a partial JSON parser&lt;/a&gt; for one job: pulling the &lt;code&gt;response&lt;/code&gt; field out of a half-received LLM reply so the text could appear on screen while the rest of the JSON was still streaming. The post ended with a ninety-line function in a code block and a gist to copy. The streaming post that followed it leaned on the same function, and each app I used it in got its own copy with the field name changed, which is the sign that a code block should have been a package.&lt;/p&gt;

&lt;p&gt;So it is one now.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;fenced-json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It has no dependencies, runs in Node and the browser, ships with tests, and there is a &lt;a href="https://stepinto.dev/partial-json/" rel="noopener noreferrer"&gt;playground&lt;/a&gt; where you can paste a response, replay it a few characters at a time, and watch the fields appear. This post is about what had to change when the function stopped being specific to my app.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the old one did
&lt;/h2&gt;

&lt;p&gt;The 2024 version made four assumptions, and said so:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I only care about one field (&lt;code&gt;response&lt;/code&gt;). The structure is known and flat. The value I need is always a string. I'm already handling the stream myself.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Given those, the implementation was &lt;code&gt;indexOf('"response"')&lt;/code&gt;, skip to the opening quote, then walk characters handling escapes until the closing quote or the end of the text. The post described it as "scrappy, specific to my use case" and it was. It also listed what it did not handle: nested objects, arrays as the streaming target, unknown schemas, non-string values.&lt;/p&gt;

&lt;p&gt;A general version has to drop every one of those assumptions, and each one turned out to hide a design decision rather than just more code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where does the JSON start
&lt;/h2&gt;

&lt;p&gt;The first problem is not JSON at all. Models wrap structured output in a markdown fence because the system prompt told them to, and some models add a line of prose first ("Sure, here is the structured answer:") no matter what the prompt says. The old code searched for&lt;br&gt;
&lt;br&gt;
 &lt;code&gt;```json&lt;/code&gt;&lt;br&gt;
&lt;br&gt;
 and chopped. That works until the fence has not fully arrived.&lt;/p&gt;

&lt;p&gt;Consider the text so far being &lt;code&gt;Sure!\n&lt;/code&gt;`&lt;code&gt;json&lt;/code&gt;. Is the JSON about to start? Probably. But the parser cannot tell the language tag from the start of the content until the newline arrives, so the honest answer is "nothing yet". The same goes for &lt;code&gt;Sure!\n&lt;/code&gt; on its own: a fence may be coming, or bare JSON may be coming, or the model may be about to ramble. The package returns a state of &lt;code&gt;empty&lt;/code&gt; for all of these and parses nothing.&lt;/p&gt;

&lt;p&gt;Once there is a fence followed by a newline, parsing starts after it. When there is no fence, parsing starts at the first &lt;code&gt;{&lt;/code&gt; or &lt;code&gt;[&lt;/code&gt;. That second rule is a compromise: a brace inside the preamble would trip it. In practice preamble before bare JSON is rare, because models that add preamble tend to also add fences, and you can turn the rule off with &lt;code&gt;fences: false&lt;/code&gt; if you know the text is bare JSON and want a preamble to be an error.&lt;/p&gt;

&lt;p&gt;After the closing brace, whatever follows (the closing fence, a "Let me know if you need anything else") goes into &lt;code&gt;rest&lt;/code&gt;, so you can show it or drop it.&lt;/p&gt;
&lt;h2&gt;
  
  
  A half string is a string, a half number is a lie
&lt;/h2&gt;

&lt;p&gt;The old parser only streamed strings, and a prefix of a string is useful: "Here's what I fou" is readable and the user sees it grow. Generalising to every JSON type forced the question of what a prefix of each type means.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cut inside a&lt;/th&gt;
&lt;th&gt;What you get&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;the prefix so far&lt;/td&gt;
&lt;td&gt;a partial sentence is still readable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;escape sequence&lt;/td&gt;
&lt;td&gt;the string without the half escape&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;\u00&lt;/code&gt; is not a character yet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;number&lt;/td&gt;
&lt;td&gt;nothing, until a delimiter arrives&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;12&lt;/code&gt; may be about to become &lt;code&gt;125&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;true&lt;/code&gt;, &lt;code&gt;false&lt;/code&gt;, &lt;code&gt;null&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;the completed literal&lt;/td&gt;
&lt;td&gt;only one literal starts with each letter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;object key&lt;/td&gt;
&lt;td&gt;nothing, the key is left out&lt;/td&gt;
&lt;td&gt;a key with no value is not a field&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;array or object&lt;/td&gt;
&lt;td&gt;the container, closed&lt;/td&gt;
&lt;td&gt;with whatever children have arrived&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The number rule is the one I went back and forth on. Every other partial-JSON library I tried returns &lt;code&gt;12&lt;/code&gt; for &lt;code&gt;{"n": 12&lt;/code&gt; at the end of the text, and for a price field that is wrong by a factor of ten for the duration of one chunk. A string prefix is a true statement about the final string. A number prefix is not a true statement about the final number. So the default drops it, and &lt;code&gt;partialNumbers: 'keep'&lt;/code&gt; brings it back for the cases where a flickering digit is acceptable.&lt;/p&gt;

&lt;p&gt;Strings get the opposite treatment with the same reasoning in reverse. The default keeps the prefix because that is the whole point for display text. But if the string is an enum or an identifier you will switch on, a prefix is dangerous: &lt;code&gt;"lang": "e"&lt;/code&gt; is not &lt;code&gt;"en"&lt;/code&gt;. For those, &lt;code&gt;partialStrings: 'drop'&lt;/code&gt; holds every string back until its closing quote. It is a per-call option rather than per-field because the parser does not know your schema; you can call it twice with different options if you need both behaviours, and at the sizes involved that costs nothing.&lt;/p&gt;
&lt;h2&gt;
  
  
  Which field is streaming
&lt;/h2&gt;

&lt;p&gt;With one field there was no question of which field was still arriving. With a whole tree there is, and it matters more than I expected. The suggestions list in my own app should render once it is complete, not item by item with half a name on the last one. The response text should render as it grows. Both come from the same parse result, so the result has to say where the cut is.&lt;/p&gt;

&lt;p&gt;The parser tracks its path as it descends and records it the first time it runs out of text. For &lt;code&gt;{"a": "x", "b": [1, {"c": "d&lt;/code&gt; the streaming path is &lt;code&gt;["b", 1, "c"]&lt;/code&gt;. For &lt;code&gt;{"a": "x", "b": [1, {"c&lt;/code&gt; it is &lt;code&gt;["b", 1]&lt;/code&gt;, because the inner object is open and its next key is incomplete. When the document is complete the path is &lt;code&gt;null&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;From that path, &lt;code&gt;isComplete(result, ["b", 0])&lt;/code&gt; is a one-line check: the value at the path exists, and the streaming path does not pass through it. A field that has not arrived yet is not complete either, which is the case the obvious implementation gets wrong.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;`ts&lt;br&gt;
const r = parsePartial('{"response": "done", "items": [{"id": 1}, {"id": 2, "name": "Lu');&lt;br&gt;
r.streaming                      // ['items', 1, 'name']&lt;br&gt;
isComplete(r, ['response'])      // true&lt;br&gt;
isComplete(r, ['items', 0])      // true&lt;br&gt;
isComplete(r, ['items', 1])      // false&lt;br&gt;
isComplete(r, ['language'])      // false, not arrived&lt;br&gt;
`&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Incomplete is not invalid
&lt;/h2&gt;

&lt;p&gt;The old function returned &lt;code&gt;null&lt;/code&gt; for "nothing to show yet" and there was no way to say "this will never parse". Models do emit broken JSON occasionally: a trailing comma, a single-quoted key, an unescaped quote. A general parser has to distinguish "not yet" from "never", because the UI response differs. For "not yet" you wait. For "never" you stop waiting and fall back to showing the raw text.&lt;/p&gt;

&lt;p&gt;The result's &lt;code&gt;state&lt;/code&gt; is one of &lt;code&gt;empty&lt;/code&gt;, &lt;code&gt;partial&lt;/code&gt;, &lt;code&gt;complete&lt;/code&gt; or &lt;code&gt;invalid&lt;/code&gt;, and the rule the tests enforce is that every prefix of a valid document is one of the first three. The test suite takes a sample document and parses all of its prefixes, asserting that none is invalid and that every value returned is a sub-tree of the final value (every string a prefix of the final string, every array no longer than the final array, every key present in the final object). That one property test is also what settled the partial-number rule. I had written the obvious expectation, that &lt;code&gt;[1, 2&lt;/code&gt; parses to &lt;code&gt;[1, 2]&lt;/code&gt;, and the test failed because the trailing &lt;code&gt;2&lt;/code&gt; had been dropped. Writing out why the drop was right is where the table above came from. The escape-sequence cases, &lt;code&gt;\u&lt;/code&gt; split across two chunks among them, are the ones the 2024 version would have got wrong, so they each have their own test.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;invalid&lt;/code&gt; carries the offset where parsing failed, which turns out to be useful for prompt debugging. If the model keeps producing bad JSON at the same offset, you learn something about your schema.&lt;/p&gt;
&lt;h2&gt;
  
  
  Re-parse, do not be clever
&lt;/h2&gt;

&lt;p&gt;The obvious architecture for a streaming parser is incremental: keep the state machine between chunks and only consume new characters. The Vercel AI SDK's &lt;code&gt;fixJson&lt;/code&gt; is a single-pass scanner of that sort. I started down that road and stopped, because the incremental version needs the partially built tree to be mutable in place, and then the value you handed the caller last chunk changes under them. Immutable results mean a fresh tree each time, and a fresh tree means you may as well re-parse.&lt;/p&gt;

&lt;p&gt;Re-parsing from the start on every chunk is quadratic in the response length. For LLM responses that does not matter, and I measured rather than asserted it. The benchmark feeds a pretty-printed fenced document in twenty-character chunks and times the whole stream, taking the best of several runs.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Document&lt;/th&gt;
&lt;th&gt;Chunks&lt;/th&gt;
&lt;th&gt;Total&lt;/th&gt;
&lt;th&gt;Per chunk&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1 KB&lt;/td&gt;
&lt;td&gt;61&lt;/td&gt;
&lt;td&gt;0.2 ms&lt;/td&gt;
&lt;td&gt;3 µs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4 KB&lt;/td&gt;
&lt;td&gt;215&lt;/td&gt;
&lt;td&gt;2.3 ms&lt;/td&gt;
&lt;td&gt;11 µs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16 KB&lt;/td&gt;
&lt;td&gt;826&lt;/td&gt;
&lt;td&gt;33 ms&lt;/td&gt;
&lt;td&gt;40 µs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;64 KB&lt;/td&gt;
&lt;td&gt;3,286&lt;/td&gt;
&lt;td&gt;523 ms&lt;/td&gt;
&lt;td&gt;159 µs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Node 23.11 on an M-series laptop. A typical chat response with a few suggestions attached is under 4 KB. Even at 64 KB the per-chunk cost is well under a frame. If your structured output runs to hundreds of kilobytes, you want an incremental parser and this is not it, and the README says so.&lt;/p&gt;
&lt;h2&gt;
  
  
  The API
&lt;/h2&gt;

&lt;p&gt;Three exports do the work.&lt;/p&gt;

&lt;p&gt;`&lt;code&gt;&lt;/code&gt;ts&lt;br&gt;
import { parsePartial, PartialJson, isComplete } from 'fenced-json';&lt;/p&gt;

&lt;p&gt;type Reply = {&lt;br&gt;
  response: string;&lt;br&gt;
  language: string;&lt;br&gt;
  items: { id: string; name: string }[];&lt;br&gt;
};&lt;/p&gt;

&lt;p&gt;const json = new PartialJson();&lt;/p&gt;

&lt;p&gt;for await (const chunk of stream) {&lt;br&gt;
  const r = json.push(chunk);&lt;br&gt;
  showText(r.value?.response ?? '');&lt;br&gt;
  if (isComplete(r, ['items'])) showItems(r.value!.items!);&lt;br&gt;
}&lt;br&gt;
&lt;code&gt;&lt;/code&gt;`&lt;/p&gt;

&lt;p&gt;&lt;code&gt;value&lt;/code&gt; is typed as &lt;code&gt;DeepPartial&amp;lt;Reply&amp;gt;&lt;/code&gt;: every field optional, all the way down, because that is what it is until the stream finishes. The compiler then makes you handle the undefined case at every access, which is tedious for about ten minutes and then saves you from the bug where the suggestions render with no names.&lt;/p&gt;

&lt;p&gt;The 2024 usage pattern maps onto this almost line for line. &lt;code&gt;extractResponseFromPartialJson(accumulated)&lt;/code&gt; becomes &lt;code&gt;json.push(chunk).value?.response&lt;/code&gt;, and &lt;code&gt;tryParseCompleteJson&lt;/code&gt; becomes checking &lt;code&gt;state === 'complete'&lt;/code&gt;. The streaming post's React Native app calls the old function from its chunk handler, one layer above the transport, and swapping it for the new one is a two-line change there too. That app's copy had also grown two more suggestion fields and a renamed one, which is the drift that a package stops.&lt;/p&gt;
&lt;h2&gt;
  
  
  What the other packages do
&lt;/h2&gt;

&lt;p&gt;There are several partial JSON parsers on npm and I tried four of them against the same inputs before deciding to publish another. The fenced case is the one that separates them.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Input&lt;/th&gt;
&lt;th&gt;partial-json&lt;/th&gt;
&lt;th&gt;best-effort-json-parser&lt;/th&gt;
&lt;th&gt;incomplete-json-parser&lt;/th&gt;
&lt;th&gt;jsonrepair&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;&lt;/code&gt; `&lt;code&gt;&lt;/code&gt;json\n{"response": "Hello th `&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
&lt;code&gt;| throws | `{response: "Hello th"}` | throws | `{response: "Hello th"}` |&lt;br&gt;
| `Sure! ...\n&lt;/code&gt;&lt;br&gt;
&lt;br&gt;
json\n{"response": "Hi", "items": [{"id": 1, "na&lt;code&gt;| throws |&lt;/code&gt;{response, items: [{id: 1}]}&lt;code&gt;| throws | wraps the preamble and the object in an array |&lt;br&gt;
|&lt;/code&gt;{"n": 12.&lt;code&gt;|&lt;/code&gt;{}&lt;code&gt;|&lt;/code&gt;{n: 12}&lt;code&gt;|&lt;/code&gt;{n: 12}&lt;code&gt;|&lt;/code&gt;{n: 12}&lt;code&gt;|&lt;br&gt;
|&lt;/code&gt;{"s": "a\u00&lt;code&gt;|&lt;/code&gt;{s: "a"}&lt;code&gt;| throws |&lt;/code&gt;{s: "a"}&lt;code&gt;|&lt;/code&gt;{s: "a"}&lt;code&gt;|&lt;br&gt;
|&lt;/code&gt;{"response": "done", "lang&lt;code&gt;|&lt;/code&gt;{response: "done"}&lt;code&gt;|&lt;/code&gt;{response: "done"}&lt;code&gt;| adds&lt;/code&gt;lang: null&lt;code&gt;| adds&lt;/code&gt;lang: null` |&lt;/p&gt;

&lt;p&gt;Versions as of October 2026: partial-json 0.1.7, best-effort-json-parser 1.5.1, incomplete-json-parser 1.1.5, jsonrepair 3.15.0. The Vercel AI SDK's &lt;code&gt;parsePartialJson&lt;/code&gt; is not in the table because it lives inside the &lt;code&gt;ai&lt;/code&gt; package and expects bare JSON; it tries &lt;code&gt;JSON.parse&lt;/code&gt;, then repairs with a scanner and tries again, and reports one of &lt;code&gt;successful-parse&lt;/code&gt;, &lt;code&gt;repaired-parse&lt;/code&gt; or &lt;code&gt;failed-parse&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;None of them tells you which field is still streaming, and none of them distinguishes a number that is finished from one that is not. Two of them handle fences. If you already depend on jsonrepair for its actual purpose, which is fixing broken JSON rather than completing truncated JSON, it does a reasonable job on this too, though the preamble handling will surprise you.&lt;/p&gt;
&lt;h2&gt;
  
  
  The playground
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://stepinto.dev/partial-json/" rel="noopener noreferrer"&gt;playground&lt;/a&gt; loads the same &lt;code&gt;dist/index.js&lt;/code&gt; the package ships, so what you see there is what you get from npm. Paste a response or use the sample, press play, and the right-hand pane shows the received text, the state, the streaming path and the parsed value with the live field highlighted. The slider lets you scrub to the exact character where a field appears, which is how I checked most of the rules in the table above. The options panel toggles the fence stripping and the two partial-value rules so you can see what each one does to the same text.&lt;/p&gt;
&lt;h2&gt;
  
  
  What it does not do
&lt;/h2&gt;

&lt;p&gt;It parses bare JSON when no fence is present, where the app copies of the old function refused to parse anything outside a `&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
json 

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
&lt;code&gt;block. If you port from one of those and your preamble can contain a brace, pass&lt;/code&gt;fences: false` or keep the fence in the prompt.&lt;/p&gt;

&lt;p&gt;It does not repair invalid JSON. If the model emits a trailing comma the result is &lt;code&gt;invalid&lt;/code&gt;, not a best guess, because guessing is a different job and jsonrepair already does it well. It does not validate against a schema; pass the complete value to whatever you use for that. It is not incremental, for the reasons above. And it tolerates one thing &lt;code&gt;JSON.parse&lt;/code&gt; does not, which is raw newlines inside strings, because models produce them and rejecting a whole response over one is not helpful.&lt;/p&gt;

&lt;p&gt;The source is a single file, shipped inside the package alongside the compiled output, and the tests are in the same style as the examples in this post. MIT licence, as before. If you built one of those variants from the gist, this should replace it without you having to think about escape sequences ever again.&lt;/p&gt;

</description>
      <category>llm</category>
      <category>ai</category>
      <category>typescript</category>
      <category>streaming</category>
    </item>
    <item>
      <title>Everything I Automated on This Blog</title>
      <dc:creator>stepinto.dev</dc:creator>
      <pubDate>Fri, 16 Oct 2026 14:56:40 +0000</pubDate>
      <link>https://dev.to/stepinto/everything-i-automated-on-this-blog-1iph</link>
      <guid>https://dev.to/stepinto/everything-i-automated-on-this-blog-1iph</guid>
      <description>&lt;p&gt;This site is a Hugo build pushed to Azure Static Web Apps from a GitHub Actions workflow. Over the past year the workflow and the templates have grown a handful of jobs that do the chores I used to do by hand after publishing a post. None of it is clever, and most of it is under a hundred lines, but each piece came with one gotcha that cost me an afternoon. Here they are, in the order a post meets them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Publishing on a date I am not at the keyboard
&lt;/h2&gt;

&lt;p&gt;Hugo skips any post whose date is in the future unless you build with &lt;code&gt;--buildFuture&lt;/code&gt;. That is the whole scheduling system. I write a post, give it a Tuesday, and push it with everything else. The build ignores it until the date passes.&lt;/p&gt;

&lt;p&gt;The gotcha is that the deploy has no schedule trigger. It runs on push, and that is all. A post dated Tuesday appears on the first push after Tuesday, which might be Tuesday if I remember and Thursday if I do not. I have decided this is fine. A cron trigger that rebuilds every morning would fix it in three lines, and I have left it out on purpose, because a rebuild with nothing new in it is the kind of thing that fires for years after you have forgotten it exists.&lt;/p&gt;

&lt;p&gt;The dev server runs with &lt;code&gt;--buildFuture&lt;/code&gt; so I can read queued posts, and the plan for the quarter lives in a Markdown table that says which file carries which date.&lt;/p&gt;

&lt;h2&gt;
  
  
  Share images drawn at build time
&lt;/h2&gt;

&lt;p&gt;Every page gets a 1200 by 630 share image rendered by Hugo itself, with no Puppeteer, no Node, and no image service. The partial takes a base PNG from &lt;code&gt;assets/&lt;/code&gt;, draws the series name in blue above the title in white, and publishes the result next to the page as &lt;code&gt;og.png&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{{ $filters := slice
     (images.Text ($kicker | upper) (dict "font" $font "size" 26 "color" "#60a5fa" "x" 80 "y" 120))
     (images.Text $title (dict "font" $font "size" $size "color" "#ffffff" "x" 80 "y" 180 "linespacing" 10)) }}
{{ $img := $base.Filter $filters }}
{{ $img = resources.Copy (path.Join (path.Dir .RelPermalink) "og.png") $img }}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;images.Text&lt;/code&gt; wraps long lines at the image edge, which I did not expect and was pleased to find. What it does not do is shrink the text to fit, so the partial picks a font size from the title length: 64 pixels up to 45 characters, 56 up to 70, and 48 beyond that. The font has to be a TrueType resource, so JetBrains Mono Bold lives in &lt;code&gt;assets/fonts/&lt;/code&gt;. The head partial prefers a hand-made cover image if the front matter has one, falls back to the generated one, and only then to the site hero.&lt;/p&gt;

&lt;p&gt;A full build produces about 190 of these. They are regenerated only when the title or the base image changes, because Hugo caches image filters by their inputs, so the build cost is a one-off.&lt;/p&gt;

&lt;h2&gt;
  
  
  Structured data, and the double escaping
&lt;/h2&gt;

&lt;p&gt;Each post carries a &lt;code&gt;BlogPosting&lt;/code&gt; block in JSON-LD so search engines get the headline, dates, author and word count without guessing. My first version wrote the JSON by hand in the template and piped each value through &lt;code&gt;jsonify&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;script type="application/ld+json"&amp;gt;
{
  "headline": {{ .Title | jsonify }},
  ...
}
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That looks right and is wrong. Hugo templates are Go's &lt;code&gt;html/template&lt;/code&gt;, which knows it is inside a &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; element and escapes anything you print there as a JavaScript string. &lt;code&gt;jsonify&lt;/code&gt; had already produced a quoted JSON string, so the escaper quoted it again. A title containing a quotation mark came out as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"headline"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Why &lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;Barely&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt; Is the Right Word&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Valid JSON, and every validator accepted it. The headline was just a string that happened to begin and end with quote marks, and because the page source still parses as JSON it is easy to miss until you read a value back. The fix is to build the whole object as a Hugo dict, serialise it once, and mark the result as safe so the script context leaves it alone:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{{ $ld := dict
  "@context" "https://schema.org"
  "@type" "BlogPosting"
  "headline" .Title
  "datePublished" (.Date.Format "2006-01-02")
  "wordCount" .WordCount }}
&amp;lt;script type="application/ld+json"&amp;gt;{{ $ld | jsonify | safeJS }}&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rule I took from it: in a Hugo template, never emit JSON field by field. Make the object, serialise it in one go.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cross-posting to dev.to
&lt;/h2&gt;

&lt;p&gt;The deploy workflow has a job that runs after the build and calls a small Python script against the Forem API. The script reads the Hugo post, converts it, and creates or updates the matching article on dev.to. It carries no state. Before writing anything it lists every article on the account and matches by &lt;code&gt;canonical_url&lt;/code&gt;, so there is no mapping file to keep in sync and nothing to break if I rename a post locally.&lt;/p&gt;

&lt;p&gt;The conversions are small:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The canonical URL points back here, so search credit stays on this domain.&lt;/li&gt;
&lt;li&gt;Site-relative links and image paths are rewritten to absolute ones. A &lt;code&gt;/posts/foo/&lt;/code&gt; link that works on this site is a dead link on dev.to.&lt;/li&gt;
&lt;li&gt;Tags are lowercased, stripped to alphanumerics, deduplicated, and cut to four. Dev.to allows four tags made of letters and digits only, so &lt;code&gt;react-native&lt;/code&gt; becomes &lt;code&gt;reactnative&lt;/code&gt; and a post with six tags loses its last two.&lt;/li&gt;
&lt;li&gt;The series slug is mapped to the series title from the section's index file.&lt;/li&gt;
&lt;li&gt;Posts with no description get one from the first paragraph, trimmed to 150 characters.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The job distinguishes between added and modified files in the push. New posts are created. Edited posts are only pushed if an article already exists for them, so fixing a typo in a 2024 post never creates a 2024 post on dev.to with today's date. The script waits three seconds between writes and backs off on a 429, because the API throttles article writes.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;--dry-run&lt;/code&gt; flag prints the converted payload and calls nothing. I run it on every new post before pushing, mostly to see what the tag mangling has done.&lt;/p&gt;

&lt;h2&gt;
  
  
  Newsletter drafts
&lt;/h2&gt;

&lt;p&gt;A second job watches for added posts and creates a Buttondown email for each, with the title and a link. It creates them with &lt;code&gt;status: draft&lt;/code&gt;, not &lt;code&gt;about_to_send&lt;/code&gt;, so I open Buttondown, add a sentence or two, and send it myself. Fully automatic sending from a CI job felt like the kind of thing that emails every subscriber a half-finished post at 2am.&lt;/p&gt;

&lt;p&gt;This job has a bug I know about and have left alone. It finds new posts by diffing the last commit against its parent. A push that carries three commits only sees the last one, so a post added two commits back gets no draft. The dev.to job does this properly, by diffing from the commit the push started at, which GitHub puts in the event payload. One day I will make the newsletter job do the same. Until then I push posts one commit at a time, which I was doing anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  Markdown and plain text alongside the HTML
&lt;/h2&gt;

&lt;p&gt;Every post is also published as &lt;code&gt;index.md&lt;/code&gt; next to its &lt;code&gt;index.html&lt;/code&gt;, from a four-line template that emits the title, description, date and the raw Markdown source. The "Copy as Markdown" button at the top of each post fetches that file and puts it on the clipboard, and prefetches it on hover so the click feels instant. The site root also serves &lt;code&gt;llms.txt&lt;/code&gt;, a plain list of every post with a one-line summary and a link to its Markdown, for whatever reads those.&lt;/p&gt;

&lt;p&gt;The one thing to know is that Azure Static Web Apps rewrites unknown paths to &lt;code&gt;index.html&lt;/code&gt; for single-page apps, so the config has to exclude &lt;code&gt;.md&lt;/code&gt;, &lt;code&gt;.txt&lt;/code&gt;, &lt;code&gt;.xml&lt;/code&gt; and &lt;code&gt;.json&lt;/code&gt; from the fallback and declare their MIME types, or your Markdown comes back as your home page.&lt;/p&gt;

&lt;h2&gt;
  
  
  A hand crank
&lt;/h2&gt;

&lt;p&gt;The workflow also runs on &lt;code&gt;workflow_dispatch&lt;/code&gt;. I added that after a deploy failed on the hosting side, with nothing wrong in the commit, and the only way to retry was an empty commit. One button in the Actions tab is better than that.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I have not automated
&lt;/h2&gt;

&lt;p&gt;Posting to Hacker News, Reddit, Bluesky and LinkedIn is still me, by hand, on the morning of. Each of those wants a different sentence and a different time of day, and the ones I have seen automated read like it. The analytics check is also manual: two counters, three numbers, Monday morning. That is probably the right amount of automation for a blog, which is a thing I write rather than a thing I operate.&lt;/p&gt;

</description>
      <category>hugo</category>
      <category>githubactions</category>
      <category>automation</category>
      <category>writing</category>
    </item>
    <item>
      <title>Range-Over-Func Iterators for People Who Miss LINQ</title>
      <dc:creator>stepinto.dev</dc:creator>
      <pubDate>Fri, 16 Oct 2026 13:34:41 +0000</pubDate>
      <link>https://dev.to/stepinto/range-over-func-iterators-for-people-who-miss-linq-5gk1</link>
      <guid>https://dev.to/stepinto/range-over-func-iterators-for-people-who-miss-linq-5gk1</guid>
      <description>&lt;p&gt;The most common complaint in the comments on this series, and the first item in &lt;a href="https://stepinto.dev/posts/dotnet-to-go-what-i-miss/" rel="noopener noreferrer"&gt;what I miss from C#&lt;/a&gt;, is LINQ. That post shows this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;activeUsers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsActive&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OrderBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;u&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToList&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and then the for loop and the &lt;code&gt;sort.Slice&lt;/code&gt; call you wrote instead. Go has had range-over-func iterators since 1.23, the standard library has been growing functions that return them in every release since, and Go 1.27 removed the one language restriction that made a fluent chain impossible. This post is the honest side by side. Every snippet runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What an iterator is in Go
&lt;/h2&gt;

&lt;p&gt;In C#, &lt;code&gt;IEnumerable&amp;lt;T&amp;gt;&lt;/code&gt; is a pull model. The consumer holds an enumerator and calls &lt;code&gt;MoveNext()&lt;/code&gt; until it says no. A &lt;code&gt;yield return&lt;/code&gt; method is compiled into a state machine that implements that interface for you.&lt;/p&gt;

&lt;p&gt;Go went the other way. An iterator is a function that takes a callback:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Seq&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;V&lt;/span&gt; &lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;yield&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;V&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The iterator pushes values into &lt;code&gt;yield&lt;/code&gt;, and &lt;code&gt;yield&lt;/code&gt; returns false when the consumer wants to stop. The &lt;code&gt;for range&lt;/code&gt; statement knows how to drive one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;Naturals&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;iter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Seq&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="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;yield&lt;/span&gt; &lt;span class="k"&gt;func&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="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;yield&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;return&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="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;Naturals&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;   &lt;span class="c"&gt;// yield returns false, the iterator returns, nothing leaks&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&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;That is the whole mechanism. The body of your &lt;code&gt;for&lt;/code&gt; loop becomes the &lt;code&gt;yield&lt;/code&gt; function. &lt;code&gt;break&lt;/code&gt; becomes &lt;code&gt;yield&lt;/code&gt; returning false. There is no state machine and no interface, which is why the compiler can inline simple cases and why the design went through so many rounds before it shipped.&lt;/p&gt;

&lt;p&gt;There is also &lt;code&gt;iter.Seq2[K, V]&lt;/code&gt; for pairs, which is what &lt;code&gt;maps.All&lt;/code&gt; and &lt;code&gt;slices.All&lt;/code&gt; return, and &lt;code&gt;iter.Pull&lt;/code&gt; for the rare case where you really do need &lt;code&gt;MoveNext()&lt;/code&gt; semantics, at the cost of a goroutine-like coroutine behind the scenes. You will almost never want it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the standard library gives you
&lt;/h2&gt;

&lt;p&gt;Sources and sinks, mostly. No adapters.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;slices&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;        &lt;span class="c"&gt;// iter.Seq[E]&lt;/span&gt;
&lt;span class="n"&gt;slices&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;All&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;           &lt;span class="c"&gt;// iter.Seq2[int, E]&lt;/span&gt;
&lt;span class="n"&gt;slices&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Backward&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;slices&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Chunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;maps&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;maps&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;maps&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;All&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Lines&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;        &lt;span class="c"&gt;// Go 1.24&lt;/span&gt;
&lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SplitSeq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sep&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FieldsSeq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;slices&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Collect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seq&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="c"&gt;// []E&lt;/span&gt;
&lt;span class="n"&gt;slices&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seq&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="c"&gt;// []E, sorted&lt;/span&gt;
&lt;span class="n"&gt;slices&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SortedFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seq&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cmp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;maps&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Collect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seq2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="c"&gt;// map[K]V&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the first thing LINQ people reach for already works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;keys&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;slices&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;maps&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That line replaced a nine line function in half the Go codebases I have worked in.&lt;/p&gt;

&lt;p&gt;What is missing is &lt;code&gt;Where&lt;/code&gt;, &lt;code&gt;Select&lt;/code&gt;, &lt;code&gt;Take&lt;/code&gt;, &lt;code&gt;First&lt;/code&gt; and friends. The proposal to add them as &lt;code&gt;x/exp/xiter&lt;/code&gt; was &lt;a href="https://github.com/golang/go/issues/61898" rel="noopener noreferrer"&gt;withdrawn&lt;/a&gt; after a long argument about whether adapters encourage over-abstraction. There is a decent chance they come back once the community has settled on idioms. For now you write them, and they are short.&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing Where and Select
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="n"&gt;seq&lt;/span&gt; &lt;span class="n"&gt;iter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Seq&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;keep&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;iter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Seq&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;yield&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;seq&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;keep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;yield&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;return&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="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;U&lt;/span&gt; &lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="n"&gt;seq&lt;/span&gt; &lt;span class="n"&gt;iter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Seq&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;U&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;iter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Seq&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;U&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;yield&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;U&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;seq&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;yield&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;return&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each one is a function that returns a function. The &lt;code&gt;!yield(v)&lt;/code&gt; check is the only line that matters: it is what lets a &lt;code&gt;break&lt;/code&gt; three layers up stop an infinite source at the bottom.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;First&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="n"&gt;seq&lt;/span&gt; &lt;span class="n"&gt;iter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Seq&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;seq&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;zero&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;zero&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;First&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Naturals&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;
&lt;span class="c"&gt;// 101, and Naturals stopped at 101&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lazy, composable, early-terminating. All three properties the "what I miss" post said Go did not have.&lt;/p&gt;

&lt;h2&gt;
  
  
  The example, rewritten
&lt;/h2&gt;

&lt;p&gt;With those two helpers and &lt;code&gt;slices.SortedFunc&lt;/code&gt;, the LINQ query from the top becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;active&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;slices&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SortedFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;slices&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsActive&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
        &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;UserSummary&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;UserSummary&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&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="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="n"&gt;UserSummary&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;return&lt;/span&gt; &lt;span class="n"&gt;cmp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Compare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;.&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;b&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It does the same thing. It is lazy until the sort. And it reads inside-out, which is the complaint everyone has about it within five minutes. In C# the pipeline reads top to bottom because &lt;code&gt;Where&lt;/code&gt; and &lt;code&gt;Select&lt;/code&gt; are extension methods on the sequence. In Go, up to 1.26, there were two reasons you could not do that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You cannot define methods on a type from another package, so nothing can hang off &lt;code&gt;iter.Seq&lt;/code&gt; directly. You need your own named type.&lt;/li&gt;
&lt;li&gt;Methods could not have their own type parameters, so even on your own &lt;code&gt;Query[T]&lt;/code&gt; type, a &lt;code&gt;Select&lt;/code&gt; that returns &lt;code&gt;Query[U]&lt;/code&gt; was impossible to declare. That is the one that killed every attempt at a fluent LINQ in Go.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The first restriction is permanent and the workaround is one line. The second one is gone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Go 1.27 and generic methods
&lt;/h2&gt;

&lt;p&gt;As of August 2026 a method can declare type parameters of its own. So this is now legal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;iter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Seq&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;From&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="n"&gt;slices&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="n"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keep&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;yield&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;q&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;keep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;yield&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;return&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="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="n"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;U&lt;/span&gt; &lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;U&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;U&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;yield&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;U&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;yield&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;return&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="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="n"&gt;OrderBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cmp&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="n"&gt;T&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="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;slices&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SortedFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;iter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Seq&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;cmp&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="n"&gt;ToSlice&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;slices&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Collect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;iter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Seq&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="n"&gt;q&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;And the query reads the way your C# brain wants it to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;active&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
    &lt;span class="n"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsActive&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
    &lt;span class="n"&gt;OrderBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="n"&gt;User&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;return&lt;/span&gt; &lt;span class="n"&gt;cmp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Compare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;.&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;b&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
    &lt;span class="n"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;UserSummary&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;UserSummary&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
    &lt;span class="n"&gt;ToSlice&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Type inference fills in &lt;code&gt;U&lt;/code&gt; from the function you pass, so you never write &lt;code&gt;Select[UserSummary]&lt;/code&gt;. The two caveats from the release notes: an interface still cannot declare a generic method, and a generic method cannot satisfy an interface. For a query type that is irrelevant. For anyone hoping to write &lt;code&gt;IQueryable&lt;/code&gt;, it is the end of the road, and honestly good.&lt;/p&gt;

&lt;p&gt;Whether you should build this is a separate question. The Go answer is still that a pipeline of three steps is a for loop with three lines in it, and the loop is easier to debug. Where &lt;code&gt;Query&lt;/code&gt; earns its keep is the same place LINQ does: when the pipeline is the point, when it is reused, or when the source is lazy and you want &lt;code&gt;First&lt;/code&gt; to stop it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs
&lt;/h2&gt;

&lt;p&gt;Each stage of the pipeline is an indirect function call per element. The compiler inlines the simple cases but not a chain of closures. Filtering and projecting a hundred thousand structs on Go 1.25, three runs each:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;ns/op&lt;/th&gt;
&lt;th&gt;allocs/op&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;for loop with append&lt;/td&gt;
&lt;td&gt;500,000&lt;/td&gt;
&lt;td&gt;23&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Collect(Select(Where(Values(...))))&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;920,000&lt;/td&gt;
&lt;td&gt;33&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Roughly double on a benchmark that does nothing but iterate. In a real handler the database round trip dwarfs it. It is the same trade LINQ makes against a &lt;code&gt;foreach&lt;/code&gt;, and the same answer: write the pipeline, profile before you unroll it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;LINQ&lt;/th&gt;
&lt;th&gt;Go now&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Where&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;write it, 8 lines&lt;/td&gt;
&lt;td&gt;or &lt;code&gt;Query.Where&lt;/code&gt; on 1.27&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Select&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;write it, 8 lines&lt;/td&gt;
&lt;td&gt;generic method needs 1.27&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;OrderBy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;slices.SortedFunc&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;materialises, same as LINQ&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;First&lt;/code&gt; / &lt;code&gt;FirstOrDefault&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;write it, 6 lines&lt;/td&gt;
&lt;td&gt;returns &lt;code&gt;(T, bool)&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Take&lt;/code&gt; / &lt;code&gt;Skip&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;write it&lt;/td&gt;
&lt;td&gt;easy, counter in the closure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Any&lt;/code&gt; / &lt;code&gt;All&lt;/code&gt; / &lt;code&gt;Count&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;loop or write it&lt;/td&gt;
&lt;td&gt;three lines each&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ToList&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;slices.Collect&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ToDictionary&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;maps.Collect&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;from a &lt;code&gt;Seq2&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Distinct&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;slices.Sorted&lt;/code&gt; + &lt;code&gt;slices.Compact&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;or a map, if order matters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GroupBy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;write it, returns &lt;code&gt;map[K][]T&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;no lazy grouping&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Zip&lt;/code&gt; / &lt;code&gt;Join&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;iter.Pull&lt;/code&gt; on one side&lt;/td&gt;
&lt;td&gt;ugly, write the loop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Aggregate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;loop&lt;/td&gt;
&lt;td&gt;a fold is a loop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SelectMany&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;nested &lt;code&gt;range&lt;/code&gt; in a closure&lt;/td&gt;
&lt;td&gt;fine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;IQueryable&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;nothing&lt;/td&gt;
&lt;td&gt;and nothing coming&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Everything in the "write it" column is under ten lines and lives in one file in your module. There are third-party packages that have already written them, and the &lt;code&gt;lo&lt;/code&gt; library the old post mentioned now has an &lt;code&gt;it&lt;/code&gt; subpackage built on &lt;code&gt;iter.Seq&lt;/code&gt;, but a dependency for forty lines of code is a bad Go instinct and the standard library may yet absorb them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would say now
&lt;/h2&gt;

&lt;p&gt;The "what I miss" post said Go's approach "lacks the expressiveness" of LINQ. In 2024 that was fair. In 2026 the laziness and composition are there, the standard library sources and sinks are there, and generic methods mean the fluent form is a type definition away. What is still missing is the adapters in the box and the convention that everyone uses the same ones, which is the thing that makes LINQ readable across codebases rather than just within one.&lt;/p&gt;

&lt;p&gt;So: less missing than I said. Not gone. Write the eight lines, keep the loop for the simple cases, and do not import a query language.&lt;/p&gt;

</description>
      <category>go</category>
      <category>dotnet</category>
      <category>linq</category>
      <category>iterators</category>
    </item>
    <item>
      <title>Forty-Eight Posts on Moving From C# to Go, in the Right Order</title>
      <dc:creator>stepinto.dev</dc:creator>
      <pubDate>Fri, 16 Oct 2026 13:34:37 +0000</pubDate>
      <link>https://dev.to/stepinto/forty-eight-posts-on-moving-from-c-to-go-in-the-right-order-22dg</link>
      <guid>https://dev.to/stepinto/forty-eight-posts-on-moving-from-c-to-go-in-the-right-order-22dg</guid>
      <description>&lt;p&gt;The C# to Go series is forty-eight posts long. It covers the move from twenty years of .NET to writing Go in production, from the first &lt;code&gt;go build&lt;/code&gt; through to a six month retrospective, and it is the most-read thing on this site by a distance.&lt;/p&gt;

&lt;p&gt;It was written as a sequence, but most readers do not arrive at the start of it. They arrive from a search for "Go equivalent of IOptions" and land on the configuration post, with no signpost to the twenty posts that explain the vocabulary it uses. The series page lists everything by date, which is correct and unhelpful.&lt;/p&gt;

&lt;p&gt;So there is now a landing page at &lt;a href="https://stepinto.dev/go-for-csharp-developers/" rel="noopener noreferrer"&gt;/go-for-csharp-developers/&lt;/a&gt; that orders the whole thing as chapters. This post explains what is on it, who it is for, and what has changed in Go since.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who it is for
&lt;/h2&gt;

&lt;p&gt;You know C# well. You have shipped ASP.NET Core services, you have opinions about Entity Framework, and you can explain &lt;code&gt;ConfigureAwait(false)&lt;/code&gt; to a junior without looking it up. You are either curious about Go or you have just been handed a Go codebase and need to be productive in it by the end of the month.&lt;/p&gt;

&lt;p&gt;The series assumes all of that. It does not explain what a slice is from first principles. It explains what a slice is to someone who thinks in &lt;code&gt;List&amp;lt;T&amp;gt;&lt;/code&gt;, including the shared backing array behaviour that catches every single one of us. Each post takes a thing you already know in .NET and shows the Go shape of it, where the shape is different, and where your muscle memory will hurt you.&lt;/p&gt;

&lt;p&gt;If you do not know C#, the posts still work, but half the jokes will be lost on you.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to read it
&lt;/h2&gt;

&lt;p&gt;Start with &lt;a href="https://stepinto.dev/posts/dotnet-to-go-phrasebook/" rel="noopener noreferrer"&gt;the phrasebook&lt;/a&gt;. It maps C# vocabulary to Go vocabulary in one table and takes ten minutes. Bookmark it. Every other post uses the Go words without stopping to translate.&lt;/p&gt;

&lt;p&gt;After that, read by chapter. The landing page groups the posts like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Getting started.&lt;/strong&gt; The toolchain, modules instead of csproj, and the phrasebook.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Types.&lt;/strong&gt; Structs instead of classes, visibility by capitalisation, the enum situation, generics, and nil.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Control flow and errors.&lt;/strong&gt; The switch statement, &lt;code&gt;if err != nil&lt;/code&gt;, and why panic is an emergency exit rather than an exception.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory.&lt;/strong&gt; Pointers, slices, and escape analysis for people who have never thought about the stack.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Concurrency.&lt;/strong&gt; Goroutines against async/await, channels, select, context, and the sync package for when channels are the wrong tool.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interfaces and composition.&lt;/strong&gt; Implicit interfaces, embedding instead of inheritance, and the empty interface.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Testing and project layout.&lt;/strong&gt; The built-in testing package, mocking without Moq, benchmarks, organising a real project, and multi-module repos.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Building services.&lt;/strong&gt; net/http, JSON, database/sql, GORM, configuration, and logging.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tooling.&lt;/strong&gt; Linting, debugging with Delve, and code generation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deployment.&lt;/strong&gt; Single binaries, build tags, Docker, Kubernetes, health checks, Lambda, observability, GitHub Actions, and versioning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Looking back.&lt;/strong&gt; What I miss, what I will never go back for, and the six month retrospective.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you are in a hurry, chapters one, two, three and five are the ones that change how you think. The rest you can read when the relevant problem turns up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three things I would write differently now
&lt;/h2&gt;

&lt;p&gt;The series was written against Go 1.22 and 1.23. Go is now at 1.27 and .NET is at 10, and three of the posts read differently against the current toolchain. The smaller version drift (image pins in Dockerfiles, the golangci-lint config format, &lt;code&gt;b.N&lt;/code&gt; loops where &lt;code&gt;b.Loop()&lt;/code&gt; now goes) gets a short post of its own at Christmas. These are the three that change the argument.&lt;/p&gt;

&lt;h3&gt;
  
  
  LINQ has a real answer now
&lt;/h3&gt;

&lt;p&gt;The phrasebook's row for LINQ says "nothing built-in", and the &lt;a href="https://stepinto.dev/posts/dotnet-to-go-what-i-miss/" rel="noopener noreferrer"&gt;"what I miss"&lt;/a&gt; post opens with "God, I miss LINQ" and shows the for loop you write instead. That was the honest position for production Go at the time. Range-over-func iterators had only just landed in 1.23 and the standard library had barely started using them.&lt;/p&gt;

&lt;p&gt;It has caught up. The &lt;code&gt;slices&lt;/code&gt; and &lt;code&gt;maps&lt;/code&gt; packages have grown iterator-returning functions in every release since, &lt;code&gt;strings.Lines&lt;/code&gt; and &lt;code&gt;SplitSeq&lt;/code&gt; arrived in 1.24, and a lazy pipeline is now a thing you can write without a third-party package. Whether it reads as well as &lt;code&gt;Where().Select().First()&lt;/code&gt; is a different question, and that one gets a full post in December with an honest side by side.&lt;/p&gt;

&lt;p&gt;Meanwhile C# 14 shipped extension members, so the extension methods gap got wider from the other direction. Fair is fair.&lt;/p&gt;

&lt;h3&gt;
  
  
  Container-aware GOMAXPROCS is real now, and the Kubernetes post needs a date fixed
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://stepinto.dev/posts/dotnet-to-go-kubernetes/" rel="noopener noreferrer"&gt;Kubernetes post&lt;/a&gt; says Go has respected container CPU limits since 1.19. The 1.19 change was &lt;code&gt;GOMEMLIMIT&lt;/code&gt;, which covers memory. CPU awareness arrived in Go 1.25 in August 2025. Before that, a Go process in a container with a two core limit on a sixty-four core node would set GOMAXPROCS to sixty-four and spend its life being throttled, and the fix was Uber's &lt;code&gt;automaxprocs&lt;/code&gt; library.&lt;/p&gt;

&lt;p&gt;On any toolchain from the last year the post's advice holds as written. If you are running something older in Kubernetes, that is the first thing to check. It is the kind of mistake that costs money quietly.&lt;/p&gt;

&lt;h3&gt;
  
  
  JSON got a new engine and the tag everyone wanted
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://stepinto.dev/posts/dotnet-to-go-json/" rel="noopener noreferrer"&gt;JSON post&lt;/a&gt; spends a whole section on the &lt;code&gt;omitempty&lt;/code&gt; gotcha, where a zero is treated as empty and silently dropped from the output, and reaches for a pointer to work around it. Go 1.24 added &lt;code&gt;omitzero&lt;/code&gt;, which does what everyone assumed &lt;code&gt;omitempty&lt;/code&gt; did. The pointer trick still works. It is no longer the right answer.&lt;/p&gt;

&lt;p&gt;Then Go 1.27 switched &lt;code&gt;encoding/json&lt;/code&gt; over to the v2 implementation underneath. Decoding is a good deal faster and the API you call is the same, but the defaults are stricter: duplicate keys and invalid UTF-8 in strings are now errors rather than things that quietly passed. If you are moving a service that consumes JSON from systems you do not control, test that first after you bump the toolchain. Coming from System.Text.Json, which is also strict by default, you will probably find the new behaviour more familiar than the old.&lt;/p&gt;

&lt;h2&gt;
  
  
  The whole thing as a PDF
&lt;/h2&gt;

&lt;p&gt;The forty-eight posts come to a little over two hundred pages when laid out as a book, with the chapters above as sections and the phrasebook as an appendix. It is the version I would have wanted on the train in week one.&lt;/p&gt;

&lt;p&gt;It goes to newsletter subscribers. The signup form is at the bottom of this page, and the download link arrives in the welcome email. The newsletter is one email a week, on a Tuesday, when a post goes out, and nothing else.&lt;/p&gt;

&lt;p&gt;If you have made the same move and want a post that is not here, say so. The Christmas post is the corrections and additions, and the comments on the series have written half of it already.&lt;/p&gt;

</description>
      <category>go</category>
      <category>dotnet</category>
      <category>csharp</category>
      <category>series</category>
    </item>
    <item>
      <title>A World Model in Forty Lines of TypeScript</title>
      <dc:creator>stepinto.dev</dc:creator>
      <pubDate>Thu, 15 Oct 2026 13:56:58 +0000</pubDate>
      <link>https://dev.to/stepinto/a-world-model-in-forty-lines-of-typescript-3co0</link>
      <guid>https://dev.to/stepinto/a-world-model-in-forty-lines-of-typescript-3co0</guid>
      <description>&lt;p&gt;Every explanation of world models I've read starts the same way. A conditional probability, a subscript, a latent variable, and half the audience gone by paragraph three. Which is a shame, because the core idea fits in a few lines of code and most working programmers already have the intuition. They've just never had it pointed at.&lt;/p&gt;

&lt;p&gt;So this series starts with TypeScript. We'll get to the equations eventually, but only once they're describing something you've already watched run. (If you'd rather have today's ideas with no code at all, the &lt;a href="https://stepinto.dev/posts/world-models-001/" rel="noopener noreferrer"&gt;previous post&lt;/a&gt; covers the same ground in prose.)&lt;/p&gt;

&lt;h2&gt;
  
  
  The whole idea, in code
&lt;/h2&gt;

&lt;p&gt;A world model answers one question: &lt;strong&gt;if the world looks like this, and I do that, what happens next?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;State in, action in, predicted next state out. Here's the smallest honest version I can write, using a domain I spend a lot of my life in: a room with a heater.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;State&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;roomTemp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;     &lt;span class="c1"&gt;// °C&lt;/span&gt;
  &lt;span class="nl"&gt;outsideTemp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// °C&lt;/span&gt;
  &lt;span class="nl"&gt;heaterOn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Action&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;turnHeaterOn&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;turnHeaterOff&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;wait&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;predict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;State&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;State&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;heaterOn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;turnHeaterOn&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;turnHeaterOff&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;heaterOn&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Heat leaks towards outside; the heater pushes back.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;leak&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outsideTemp&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;roomTemp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;heat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;heaterOn&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mf"&gt;1.5&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;roomTemp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;roomTemp&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;leak&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;heat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;outsideTemp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outsideTemp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;heaterOn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;predict&lt;/code&gt; is a world model. A hand-written, embarrassingly simple one, but structurally the real thing: it takes a state and an action and returns the next state. The fancy research systems (DreamerV3, LeCun's JEPA work, NVIDIA's Cosmos line) are this same function signature with the body learned from data instead of typed by me, and with &lt;code&gt;State&lt;/code&gt; replaced by something the model invents for itself. We'll unpack both of those moves later in the series. The shape doesn't change.&lt;/p&gt;

&lt;p&gt;And once you have that function, you get the properly useful trick for free: you can run the future without touching the present.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;rollout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;State&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;[]):&lt;/span&gt; &lt;span class="nx"&gt;State&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;trajectory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;trajectory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;predict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trajectory&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;trajectory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;trajectory&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;State&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;roomTemp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;outsideTemp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;heaterOn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// What happens if I turn the heater on and leave it?&lt;/span&gt;
&lt;span class="nf"&gt;rollout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cold&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;turnHeaterOn&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;wait&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;wait&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;wait&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;roomTemp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="c1"&gt;// [ '14.0', '14.6', '15.1', '15.6', '16.1' ]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No heater was harmed. We asked "what if?" four steps deep and got an answer in microseconds. Planning, in the world-model sense, is exactly this: propose a few candidate action sequences, roll each one forward inside the model, pick the future you like best, then execute only the first action for real.&lt;/p&gt;

&lt;h2&gt;
  
  
  Action-result pairs are the raw material
&lt;/h2&gt;

&lt;p&gt;My &lt;code&gt;predict&lt;/code&gt; function encodes physics I made up. A real world model learns its &lt;code&gt;predict&lt;/code&gt; from logged experience, and the unit of experience is the &lt;strong&gt;action-result pair&lt;/strong&gt;: I was in state &lt;code&gt;s&lt;/code&gt;, I did action &lt;code&gt;a&lt;/code&gt;, I ended up in state &lt;code&gt;s'&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Transition&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;State&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;nextState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;State&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;experience&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Transition&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;roomTemp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;14.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;outsideTemp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;heaterOn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;turnHeaterOn&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;nextState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;roomTemp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;14.6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;outsideTemp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;heaterOn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&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="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;roomTemp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;14.6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;outsideTemp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;heaterOn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;wait&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;nextState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;roomTemp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;15.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;outsideTemp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;heaterOn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="c1"&gt;// ...thousands more&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Training a world model means fitting a function to this table: given the first two columns, predict the third. Which is just supervised learning with free labels, because the world hands you the answer one timestep later. Nobody annotates anything.&lt;/p&gt;

&lt;p&gt;If you run devices in the field, look at that &lt;code&gt;Transition&lt;/code&gt; type again. It's a telemetry log with the actuator commands joined on. Every thermostat, pump controller and battery system that's been logging sensor readings alongside its control decisions has been accumulating world-model training data for years. This is a big part of why I care: at &lt;a href="https://getunimo.com" rel="noopener noreferrer"&gt;Unimo&lt;/a&gt; we live in exactly this stream of readings and commands from energy hardware, which is the realisation the &lt;a href="https://stepinto.dev/posts/world-models-001/" rel="noopener noreferrer"&gt;previous post&lt;/a&gt; was built around. The boring operational log is also the training set.&lt;/p&gt;

&lt;h2&gt;
  
  
  So how is this different from an LLM?
&lt;/h2&gt;

&lt;p&gt;An LLM is also a next-thing predictor, so it's fair to ask what's actually new here. Two things, and the first is the one I think matters most.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Statefulness.&lt;/strong&gt; An LLM has no memory of its own. Every API call, you hand it the entire conversation and it recomputes everything from scratch, predicts some tokens, and forgets you existed. The "state" of your chat lives in your database, not in the model. It's a pure function over a context window, and when the conversation outgrows the window, the model's grip on what's true starts to slip. Anyone who's built an agent that gradually forgets what it did twenty steps ago has felt this.&lt;/p&gt;

&lt;p&gt;A world model is stateful by construction. It carries a compact internal state forward from step to step, updating it as actions happen and observations arrive. Ask it about step 500 and it doesn't need to rummage through 500 steps of history, because everything that still matters is already folded into the current state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Actions are first-class.&lt;/strong&gt; For an LLM, an action is just more text. A tool call is tokens out, a tool result is tokens in, and the model must infer from prose what its action did. A world model takes the action as an explicit, typed input (the &lt;code&gt;a&lt;/code&gt; in &lt;code&gt;predict(s, a)&lt;/code&gt;) and is trained specifically on what actions &lt;em&gt;do&lt;/em&gt;. Cause and effect is the whole curriculum rather than something absorbed second-hand from prose about other people's causes and effects.&lt;/p&gt;

&lt;p&gt;Side by side:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;LLM&lt;/th&gt;
&lt;th&gt;World model&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Predicts&lt;/td&gt;
&lt;td&gt;next token&lt;/td&gt;
&lt;td&gt;next state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Given&lt;/td&gt;
&lt;td&gt;a context window of text&lt;/td&gt;
&lt;td&gt;current state + an action&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;State between calls&lt;/td&gt;
&lt;td&gt;none, resent every time&lt;/td&gt;
&lt;td&gt;persistent, carried forward&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"What if?"&lt;/td&gt;
&lt;td&gt;narrates a plausible story&lt;/td&gt;
&lt;td&gt;rolls the trajectory forward&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Learns from&lt;/td&gt;
&lt;td&gt;mostly static text&lt;/td&gt;
&lt;td&gt;logged action-result pairs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Ask an LLM what happens if you turn the heater on and it will tell you a &lt;em&gt;story&lt;/em&gt; about the room warming up, and it will be a good story, statistically resembling the millions of similar stories in its training data. Ask a world model and it &lt;em&gt;runs&lt;/em&gt; it, the same way our &lt;code&gt;rollout&lt;/code&gt; did. For chatting, the story is fine. For deciding whether to actually flip a relay on hardware you're responsible for, I want the simulation.&lt;/p&gt;

&lt;p&gt;To be fair to LLMs, they do pick up something world-model-ish internally. Interpretability work has found neurons in Llama-family models tracking space and time surprisingly well. But those representations are passive and rebuilt from scratch on every call. Having read a lot about rooms is not the same as tracking the temperature of this one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this series goes
&lt;/h2&gt;

&lt;p&gt;This post is deliberately the shallow end. My &lt;code&gt;predict&lt;/code&gt; cheats in ways that stop mattering only in toy problems: the state is three fields I chose by hand, the dynamics are made up, and nothing is learned. Real systems have to &lt;em&gt;invent&lt;/em&gt; their own state representation from messy high-dimensional observations, learn the transition function from logs, and avoid some entertaining failure modes while doing it (there's one called representation collapse, where the model discovers it can score perfectly by declaring every state identical, which is the ML equivalent of a student answering every question with "no change").&lt;/p&gt;

&lt;p&gt;Roughly where we're headed:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The &lt;a href="https://stepinto.dev/posts/world-models-001/" rel="noopener noreferrer"&gt;code-free introduction&lt;/a&gt; and this post.&lt;/strong&gt; State, actions, rollouts. The function signature.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://stepinto.dev/posts/world-models-103/" rel="noopener noreferrer"&gt;Learning &lt;code&gt;predict&lt;/code&gt; from data&lt;/a&gt;.&lt;/strong&gt; Replacing my hand-written physics with a fitted model, still in TypeScript, still no notation. &lt;em&gt;Published December 2026, with a &lt;a href="https://stepinto.dev/heater/" rel="noopener noreferrer"&gt;playground&lt;/a&gt; that fits the model in your browser.&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latent states.&lt;/strong&gt; What happens when the state is a sensor array or camera frame and the model has to compress it itself. This is where the field gets interesting and where the maths finally earns its keep.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The open-source landscape.&lt;/strong&gt; DreamerV3, JEPA and friends, what you can run on your own hardware, and why that matters more than it first appears.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;World models and physical devices.&lt;/strong&gt; Where the action-result firehose from real hardware meets all of the above.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you take one thing from today, take the function signature. An LLM completes your text. A world model, given where you are and what you're about to do, tells you where you'll be. Everything else in this series is elaboration on that difference.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>worldmodels</category>
      <category>typescript</category>
      <category>iot</category>
    </item>
  </channel>
</rss>
