A batch job, in The Elm Architecture

linggen1 pts0 comments

A batch job, in The Elm Architecture · cekrem.github.ioAt the end of my native Elm post I said I hoped my next elm-run project would be a REST API, or maybe replacing some small existing app. I lied a little. I went the other direction and grabbed something much bigger: a real batch job, inside a real (and fairly large) application I&rsquo;m actually building. The fetch PoC was 80 lines of the most ordinary Elm imaginable. This one isn&rsquo;t a toy, and that&rsquo;s the whole point – Damir (who&rsquo;s building elm-run) needs someone leaning on it at scale and reporting back what creaks, so that&rsquo;s what I&rsquo;ve been doing.<br>Quick confession before anything else: This batch works, it&rsquo;s correct, the types are lovely, but &ldquo;production-grade throughput&rdquo; it is not (but closer than one might think!). I&rsquo;ll get there (or elm-run will, rather). What I want to talk about isn&rsquo;t the speed. It&rsquo;s the shape.<br>I&rsquo;ve done the CLI thing before

Link to heading<br>This isn&rsquo;t my first batch-ish rodeo in Elm. Back in February I wrote blog-bot, a little Bluesky poster that runs daily on GitHub Actions, using elm-pages Script mode. That was genuinely great. BackendTask chained five steps together, the compiler caught my mistakes, and I deployed the whole thing in a handful of commits.<br>But elm-pages Script is a pipeline shape. You compose tasks, you andThen your way to the end, Node runs underneath. Perfect for &ldquo;read RSS, transform, post, done.&rdquo; What it is not is The Elm Architecture. There&rsquo;s no Model, no update, no long-lived loop with effects flowing in and out over time. For a fire-and-forget pipeline you don&rsquo;t miss that. For a job that has to babysit a few hundred things, each one drifting through several states at its own pace, some of them failing halfway and needing a nudge – you miss it a lot.<br>That&rsquo;s the difference native Elm gave me. Not &ldquo;Elm can run a script&rdquo; (elm-pages already does that, and quite beautifully at that). It&rsquo;s &ldquo;Elm can run the loop – init, update, subscriptions – against the OS instead of the browser.&rdquo; Same loop I&rsquo;ve written a hundred times for widgets. Now it&rsquo;s doing back-office work.<br>Each item is a tiny state machine

Link to heading<br>A batch over a list of things is really a list of little lifecycles, and a lifecycle is exactly what a union type is for. So instead of a record with a status : String and a pile of nullable fields (the shape every untyped worker eventually rots into), each item is this:<br>type Item<br>= Queued Input<br>| Submitted Ref<br>| Processing Ref Partial<br>| Finished Ref Result<br>| Failed Item Http.Error

That&rsquo;s not like railway-oriented programming. It is railway-oriented programming, just with more stations on the track. Scott Wlaschin&rsquo;s Result – the two-state success/failure railway everyone draws – is the degenerate case of this. Add intermediate stops and you&rsquo;ve got the same track, longer. (I&rsquo;ve written about parse, don&rsquo;t validate before; this is the same instinct pointed at a process instead of a value.)<br>And the transitions are functions whose types won&rsquo;t let you cheat. The step that turns a Submitted into a Processing simply cannot be handed a Queued. You can&rsquo;t fetch a result for something you never submitted, because the function that fetches results asks for a Ref, and a Queued item hasn&rsquo;t got one yet. I didn&rsquo;t write a single guard for any of that. It&rsquo;s a product of the shape. The illegal orderings became unrepresentable, not just the illegal values.<br>A failure that remembers where it was standing

Link to heading<br>Look at that last branch again:<br>| Failed Item Http.Error

Failed carries the whole previous Item. Not an error code floating in the void – the actual last-good state the item was in when the HTTP call gave up. Which means rewinding a failure is comically boring:<br>rewind : Item -> Item<br>rewind item =<br>case item of<br>Failed previous _ -><br>previous

_ -><br>item

A thrown exception is amnesiac. It knows that it failed, never where you were standing when it did. So you restart from the depot and re-run the three expensive calls that already succeeded. Here the failure is a checkpoint with a return address. Retry isn&rsquo;t &ldquo;start over and pray it&rsquo;s idempotent&rdquo; – it&rsquo;s &ldquo;resume from the siding you parked on.&rdquo; The type literally hands you back the state from just before things went sideways.<br>In a request/response handler you can almost get away without this. One request dies, the user hits refresh, nobody notices. In a batch over a few hundred items, item 287 will fail, and &ldquo;the job crashed at 64%&rdquo; is a disaster while &ldquo;283 finished, 4 parked, here they are&rdquo; is a Tuesday. Batch is precisely where the difference between an exception and a checkpoint stops being academic.<br>Kicking the whole list off is the same TEA reflex too – map your inputs into pending...

rsquo item batch ldquo rdquo failed

Related Articles