<?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: Maple</title>
    <description>The latest articles on DEV Community by Maple (@maple).</description>
    <link>https://dev.to/maple</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%2F456950%2Ff15c6126-448c-4b4f-96ce-7e1383e5707d.jpg</url>
      <title>DEV Community: Maple</title>
      <link>https://dev.to/maple</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/maple"/>
    <language>en</language>
    <item>
      <title>The Mention File Myth in Code Agents</title>
      <dc:creator>Maple</dc:creator>
      <pubDate>Sun, 19 Jul 2026 12:55:25 +0000</pubDate>
      <link>https://dev.to/maple/the-mention-file-myth-in-code-agents-5567</link>
      <guid>https://dev.to/maple/the-mention-file-myth-in-code-agents-5567</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Disclaimer: This post was written by me, but I used GLM 5.6 to restructure and polish the text. Parts of the investigation behind it were also done with GLM 5.6.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  How it started
&lt;/h2&gt;

&lt;p&gt;When Claude Code first came out, I picked up two habits from the official blog post&lt;sup id="fnref1"&gt;1&lt;/sup&gt; that immediately felt like upgrades: using &lt;code&gt;!&lt;/code&gt; to run shell commands inline, and using &lt;code&gt;@&lt;/code&gt; to mention a file.&lt;/p&gt;

&lt;p&gt;Both felt handy. &lt;code&gt;!&lt;/code&gt; let me run commands I was comfortable with myself and feed the output into Claude's context, or simply take over the follow-up work by hand while Claude kept the full context of what happened. &lt;code&gt;@&lt;/code&gt; felt just as good: it let me tell Claude exactly where a file was. If I only gave it a file name, it would have to generate content to call tools and go look for the file, the location might not be accurate, and it could take several rounds of operations before it found the right one.&lt;/p&gt;

&lt;p&gt;For a while, this was simply how I worked.&lt;/p&gt;

&lt;h2&gt;
  
  
  The "wasted tokens" take
&lt;/h2&gt;

&lt;p&gt;A few months later, I started seeing people on X arguing against &lt;code&gt;@&lt;/code&gt;. The argument was simple and, at the time, felt obviously correct: &lt;code&gt;@&lt;/code&gt; is not magic. It is a deterministic piece of client-side code that reads the file from disk, expands its contents into the user's prompt, and sends the whole thing to the LLM.&lt;sup id="fnref2"&gt;2&lt;/sup&gt; So if you &lt;code&gt;@&lt;/code&gt; a very large file, you are wasting an enormous amount of tokens.&lt;/p&gt;

&lt;p&gt;That clicked for me. Of course that is how mention file had to be implemented: like &lt;code&gt;!&lt;/code&gt; invoking the shell, it is at heart a deterministic program, so this kind of problem is hard to avoid.&lt;/p&gt;

&lt;h2&gt;
  
  
  The vibe coding shift
&lt;/h2&gt;

&lt;p&gt;Then vibe coding became a thing, models got noticeably stronger, and I found myself no longer using &lt;code&gt;@&lt;/code&gt; most of the time. Every time I watched the model work, it checked the directory structure whenever it needed to, and when I asked it to read some file, it usually already knew where the file was. Context windows had also grown across the board, so letting the model spend a few extra tool calls and round trips was not much of a cost; in exchange, I got the efficiency of describing files vaguely instead of typing exact paths. (Yes: I had discovered that the bottleneck was now me, and my typing speed.)&lt;/p&gt;

&lt;p&gt;I didn't think about how &lt;code&gt;@&lt;/code&gt; was implemented again for a long time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building my own agent
&lt;/h2&gt;

&lt;p&gt;That changed when I started building &lt;a href="https://github.com/aisk/paimon" rel="noopener noreferrer"&gt;paimon&lt;/a&gt;, my own code agent. When it came time to implement mention file, the old question came back to me. I thought it through carefully, and suddenly it seemed the old conclusion wasn't necessarily right.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What if &lt;code&gt;@&lt;/code&gt; is just a local convenience for the user at input time (an autocomplete for file paths), and the program never reads any content into the prompt at all?&lt;/li&gt;
&lt;li&gt;Or, thinking about it more: mention file really is a deterministic behavior. If the code agent has actually read a file once, could it send the full content along with more information, like a sha256 of the contents? Then the next time the file is read, if it finds it was sent before and the sha256 hasn't changed, could it send only the metadata, letting the model know it has already read this file and the hash is unchanged?&lt;/li&gt;
&lt;li&gt;And if so, when the content does need to be expanded, how should compaction be handled? Would this need some complicated machinery to manage?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After thinking it over, I came up with a design based on passing metadata. Each mentioned file would be wrapped in an XML envelope that tells the model the file path, a sha256 of the contents, whether the enclosed content is the full file or only part of it, which lines are included, and how many lines the file actually has. Roughly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;file&lt;/span&gt; &lt;span class="na"&gt;path=&lt;/span&gt;&lt;span class="s"&gt;"src/parser.py"&lt;/span&gt; &lt;span class="na"&gt;sha256=&lt;/span&gt;&lt;span class="s"&gt;"3f6a…"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"partial"&lt;/span&gt; &lt;span class="na"&gt;lines=&lt;/span&gt;&lt;span class="s"&gt;"1-200"&lt;/span&gt; &lt;span class="na"&gt;total-lines=&lt;/span&gt;&lt;span class="s"&gt;"1240"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
...
&lt;span class="nt"&gt;&amp;lt;/file&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the full content had already been sent in the current session and the hash was unchanged, only the reference metadata would go out, letting the model know it had already read this file and nothing had changed; files over a size threshold would only get their head sent, with the real length in the metadata. Since no model has ever seen this convention, the rules would be spelled out in the system prompt. Underneath, the agent would keep an in-memory table of every file sent so far (the hash, and what exactly had been transmitted), and reset it after compaction, when "already sent in full" no longer holds. It added up to a rather complex state transition diagram.&lt;/p&gt;

&lt;p&gt;It sounds like a bit of a hassle, but with the help of ChatGPT 5.6 sol, the states and flows got sorted out quickly. It even thoughtfully added one more design on top: when the user pins line numbers with the &lt;code&gt;@filename:10-20&lt;/code&gt; syntax, the fully transmitted ranges are recorded too; when a later mention of another range overlaps with what was sent before and is fully covered, the ranges can be merged. Computing those overlaps (which ranges are already covered, which partially intersect, which should be merged into one) was the genuinely fiddly part of the whole design.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wait, didn't Cursor try this?
&lt;/h2&gt;

&lt;p&gt;That was the moment it suddenly hit me: isn't this mechanism getting way too complex?&lt;/p&gt;

&lt;p&gt;It reminded me of when Cursor first launched, and people were analyzing and guessing at how it must work under the hood. The story went: LLM context windows were small, so the code was retrieved with RAG and only the relevant snippets were sent&lt;sup id="fnref3"&gt;3&lt;/sup&gt;; some analyses went further, speculating that after a tool call finished operating on files or commands, the context around the call was dropped entirely, with just the result stitched back in. Whether or not that second part was ever true, the design people believed in looked extremely clever. But in my own use at the time, the results on longer agent-style tasks were underwhelming. Then Claude Code came out and did almost the opposite: it hunted for code bluntly with &lt;code&gt;rg&lt;/code&gt; and other plain commands, never rolled context back, and just kept pushing everything forward until the window filled up and compaction kicked in. For me it worked dramatically better.&lt;sup id="fnref4"&gt;4&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;I don't claim to fully understand how LLMs work, but seeing Claude Code's approach made me realize what that rumored design was essentially doing: treating the context window as a kind of database, assuming that anything sent to the LLM would be reliably remembered. But the attention-based LLMs popular today make no such guarantee. Content that has already appeared in the context is not reliably used, especially once it sits deep in a long history&lt;sup id="fnref5"&gt;5&lt;/sup&gt;, which is why the model will try to read things again.&lt;/p&gt;

&lt;p&gt;And now I was designing exactly that kind of clever mechanism. The overlap merging, the hash-based dedup, the "just send metadata if the hash matches" trick. All of it was treating the context window as a database of record, and maybe I was making the same mistake.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking the facts
&lt;/h2&gt;

&lt;p&gt;Fortunately, open-source code agents are everywhere now, and quite a few of them are well known. So instead of theorizing, I decided to actually investigate whether the conclusions I had reached last year were true. Using opencode with GLM 5.6, I went through five well-known open-source agents: &lt;code&gt;pi&lt;/code&gt;, &lt;code&gt;opencode&lt;/code&gt;, &lt;code&gt;gemini-cli&lt;/code&gt;, &lt;code&gt;grok-build&lt;/code&gt;, and &lt;code&gt;codex&lt;/code&gt;.&lt;sup id="fnref6"&gt;6&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;Even the very first question, whether &lt;code&gt;@&lt;/code&gt; sends file contents at all, splits the field. Four of the five read the file and inline it into the user message: pi wraps it in &lt;code&gt;&amp;lt;file name="..."&amp;gt;&lt;/code&gt; (though only for files passed as CLI startup arguments; in its interactive TUI, &lt;code&gt;@&lt;/code&gt; is just path autocomplete and the literal path text is sent), grok-build in &lt;code&gt;&amp;lt;file_contents path="..."&amp;gt;&lt;/code&gt; with line numbers, gemini-cli between &lt;code&gt;--- Content from referenced files ---&lt;/code&gt; markers, and opencode fakes a tool call — the model sees &lt;code&gt;Called the Read tool with the following input: {...}&lt;/code&gt; followed by standard Read output, as if it had called the tool itself. And then there is codex, which does not read the file at all: &lt;code&gt;@&lt;/code&gt; is a fuzzy filename search, and picking a result just inserts the path as plain text. The model is expected to &lt;code&gt;cat&lt;/code&gt; or &lt;code&gt;rg&lt;/code&gt; it when it cares. That is exactly the "maybe &lt;code&gt;@&lt;/code&gt; is just autocomplete" possibility from my first bullet point.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Agent&lt;/th&gt;
&lt;th&gt;What &lt;code&gt;@&lt;/code&gt; sends&lt;/th&gt;
&lt;th&gt;Metadata beyond the path&lt;/th&gt;
&lt;th&gt;Line-range syntax&lt;/th&gt;
&lt;th&gt;Tracks what was sent&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;pi&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Full content, no truncation (CLI args only)&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;opencode&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Content through its own Read tool (2000 lines / 50 KB cap), faked as a tool call&lt;/td&gt;
&lt;td&gt;Truncation notes: "Use offset=N to continue"&lt;/td&gt;
&lt;td&gt;✅ &lt;code&gt;@file#12-18&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;gemini-cli&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Full content (2000-line cap, 20 MB hard reject)&lt;/td&gt;
&lt;td&gt;A truncation warning pointing at &lt;code&gt;read_file&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;grok-build&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Full content up to ~5,000 est. tokens, then a metadata-only stub&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;skipped="true"&lt;/code&gt; + reason on oversized files&lt;/td&gt;
&lt;td&gt;✅ &lt;code&gt;@foo.rs:10-20&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;codex&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Nothing, just the path as plain text&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;Then I held my paimon design up against these implementations, point by point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nobody sends a hash, and a file's true size surfaces only in truncation hints.&lt;/strong&gt; The envelopes carry a path and the content, and that is essentially it (opencode's Read-style output does close with a total line count). The closest thing to my metadata design is grok-build's handling of oversized files: past ~5,000 estimated tokens it drops the body entirely and sends a stub like &lt;code&gt;&amp;lt;file_contents path="..." skipped="true" reason="file too large (~5800 estimated tokens, limit 5000). Use read_file tool to read specific sections."/&amp;gt;&lt;/code&gt;. The truncating agents all do some version of this: tell the model it got cut off and point it back at its own read tool. Metadata as a "go read it yourself" hint, never as a dedup key.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nobody tracks what was already sent.&lt;/strong&gt; No hash, no mtime, no in-memory table, no "you already have this file" path. Every mention re-reads the disk and re-sends in full; the only dedup anywhere is a &lt;code&gt;Set&lt;/code&gt; that collapses duplicate mentions within a single message. The one sha256 I found in a mention path (grok-build's) is used to name spill files on disk and never reaches the model. The one genuine counterexample is buried in gemini-cli: a &lt;code&gt;ContextCompressionService&lt;/code&gt; that hashes file contents and asks a small model to route each old file to FULL / PARTIAL / SUMMARY / EXCLUDED, startlingly close to a chunk of my design. Except it sits behind a default-off experimental flag, nothing in the runtime actually instantiates it at the commit I read, and even if it ran, it only processes read-tool responses, not &lt;code&gt;@&lt;/code&gt;-mention inlines. Someone had the same idea, and it has not shipped.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Line-range syntax exists; overlap bookkeeping does not.&lt;/strong&gt; opencode supports &lt;code&gt;@file#12-18&lt;/code&gt; (translated into a Read call with offset and limit) and grok-build supports &lt;code&gt;@foo.rs:10-20&lt;/code&gt;. Neither records which ranges were sent, and neither merges overlapping ones; every mention is an independent read. The overlap-merging machinery ChatGPT and I sketched exists nowhere.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nobody explains the convention in the system prompt.&lt;/strong&gt; All five system prompts are silent about what a mention looks like. pi and grok-build rely on the XML being self-describing; a comment in grok-build's source calls its format "the training format we have been using". opencode's fake Read call is the cleverest dodge: nothing needs to be documented, because the model already knows what Read output looks like. And codex has nothing to explain, because it sends nothing but a path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compaction does nothing special with mentions.&lt;/strong&gt; In all five, when history gets summarized, inlined file content is treated as ordinary user text and fed to the summarizer wholesale: no hash routing, no placeholder substitution. My worry about "how does the state survive compaction" dissolves completely: there is no state to survive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nobody watches for staleness.&lt;/strong&gt; If a mentioned file changes on disk afterwards, nothing marks the old copy in the context as expired. The model finds out whenever it next happens to read the file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And the most telling discovery: codex used to do it the other way.&lt;/strong&gt; In its TypeScript CLI era, codex's &lt;code&gt;@&lt;/code&gt; worked exactly like the others: "file contents automatically expanded into XML blocks before being sent to the LLM", in the words of the pull request that introduced it, with &lt;code&gt;@path[50:80]&lt;/code&gt; line selection listed as a next step. The Rust rewrite replaced all of that with the path-only fuzzy search, without a word in the commit history about why content expansion was dropped, and the TypeScript implementation was later deleted outright. The only agent that demonstrably walked the "expand and enrich" road turned around and walked all the way back to the most minimal design possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing thought
&lt;/h2&gt;

&lt;p&gt;So my old understanding was wrong on just about every count. In one direction, today's code agents are smarter than I imagined: most of them will not naively shovel an entire file at the LLM in one go; past a certain size they truncate, refuse, or swap in a stub that tells the model to go read the file itself. In the other direction, none of them needed the elaborate system I had been so pleased with designing. However precise the state diagram looked, the LLM does not want any of it. The model does not fail because we re-sent a file; it fails when it cannot find the file, or when we sent less than it needed. The complexity was for my own satisfaction, not the model's.&lt;/p&gt;

&lt;p&gt;As for where this is heading: models keep getting smarter, and codex's design is a straight bet on that. Send nothing but the filename, and let the model fetch whatever it cares about. Given how well agentic search already works, I suspect this is the better way, and I would not be surprised to see more agents drift toward it.&lt;/p&gt;

&lt;p&gt;And there is a bigger difficulty hiding behind this small one, which I only appreciated by building an agent myself. Questions like "expand the file or just send the path" cannot be settled by reading code or by taste: you have to evaluate them against real tasks, and evaluation burns tokens at a scale my previous side projects never did. A CLI tool can be verified locally in seconds, for free; an agent design decision costs real money per data point.&lt;/p&gt;

&lt;p&gt;Worse, the answer may not transfer between models. Reinforcement learning is doing a lot of the work in making models better, and it also stamps each model with its own working style, so each vendor's CLI will naturally ship whatever mechanism its own model was trained toward and scores best on its own evals. grok-build's source says this out loud: its XML format exists because it is "the training format we have been using". For anyone building a general-purpose agent on top of other people's models, that is a quiet, permanent source of trouble: the best mechanism is not universal, and you cannot afford to measure everything.&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;"Claude Code: Best practices for agentic coding", originally published on Anthropic's engineering blog, now maintained as part of the &lt;a href="https://code.claude.com/docs/en/best-practices" rel="noopener noreferrer"&gt;official docs&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn2"&gt;
&lt;p&gt;The official docs still describe &lt;code&gt;@&lt;/code&gt; this way: "Reference files with &lt;code&gt;@&lt;/code&gt; instead of describing where code lives. Claude reads the file before responding."&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn3"&gt;
&lt;p&gt;This part of the guess was roughly right, except for the "local": Cursor chunks files locally, but computes embeddings on its servers and stores them in a remote vector database, while the code itself stays on your machine. See &lt;a href="https://read.engineerscodex.com/p/how-cursor-indexes-codebases-fast" rel="noopener noreferrer"&gt;How Cursor Indexes Codebases Fast&lt;/a&gt;. The "drop the tool-call context" part, as far as I can tell, was never confirmed; early Cursor didn't even have agent-style tool calls.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn4"&gt;
&lt;p&gt;A detail I only learned while fact-checking this post: early Claude Code had also tried RAG with a local vector database, and &lt;a href="https://vadim.blog/claude-code-no-indexing/" rel="noopener noreferrer"&gt;dropped it in favor of plain agentic search&lt;/a&gt;. Boris Cherny, its creator: "Early versions of Claude Code used RAG + a local vector db, but we found pretty quickly that agentic search generally works better." Another Anthropic engineer in the same thread: "In our testing we found that agentic search outperformed [it] by a lot, and this was surprising."&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn5"&gt;
&lt;p&gt;This is a measured phenomenon, not just a vibe: models use information in the middle of a long context significantly worse than information at the beginning or end. See &lt;a href="https://arxiv.org/abs/2307.03172" rel="noopener noreferrer"&gt;Lost in the Middle: How Language Models Use Long Contexts&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn6"&gt;
&lt;p&gt;All five checked in mid-July 2026, at commits: pi &lt;code&gt;87ad8243&lt;/code&gt;, opencode &lt;code&gt;efb6cc2d4&lt;/code&gt;, gemini-cli &lt;code&gt;3ff5ba2&lt;/code&gt;, grok-build &lt;code&gt;98c3b24&lt;/code&gt;, codex &lt;code&gt;315195492c&lt;/code&gt;. These are moving targets; details below may have changed by the time you read this.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>claude</category>
      <category>chatgpt</category>
    </item>
    <item>
      <title>Building a Python-requests-style HTTP Client in Haskell</title>
      <dc:creator>Maple</dc:creator>
      <pubDate>Sat, 14 Mar 2026 11:16:08 +0000</pubDate>
      <link>https://dev.to/maple/building-a-python-requests-style-http-client-in-haskell-3a0k</link>
      <guid>https://dev.to/maple/building-a-python-requests-style-http-client-in-haskell-3a0k</guid>
      <description>&lt;h2&gt;
  
  
  The Problem with Learning Haskell
&lt;/h2&gt;

&lt;p&gt;When I started learning Haskell, I always struggled to find a suitable scenario to apply it. Without a concrete problem to solve, it is difficult to make real progress.&lt;/p&gt;

&lt;p&gt;At some point I had some practical work to do: fetch data from HTTP APIs, process the results, and post them to other endpoints. My instinct was to reach for Python and the &lt;code&gt;requests&lt;/code&gt; library, because it is genuinely handy for this kind of task. You grab a URL, get back some JSON, transform it, and send it somewhere else. The whole thing can be done in a few lines.&lt;/p&gt;

&lt;p&gt;But I thought: this is exactly the kind of real problem I need. Why not try Haskell?&lt;/p&gt;

&lt;h2&gt;
  
  
  Exploring the Existing Ecosystem
&lt;/h2&gt;

&lt;p&gt;The first thing I reached for was &lt;code&gt;http-client&lt;/code&gt;. It is powerful and the foundation for most other Haskell HTTP libraries. But using it directly requires quite a bit of setup for simple tasks. You need to create a manager, parse a request, and work with a set of low-level types. It is a great library, but it is designed for cases where you need precise control.&lt;/p&gt;

&lt;p&gt;There are some lighter alternatives. &lt;code&gt;wreq&lt;/code&gt; is a popular one, built on a lens API. Lenses are a fascinating part of the Haskell ecosystem and many people find them worth learning. &lt;code&gt;req&lt;/code&gt; is another option with a clean, type-safe API. I used &lt;code&gt;req&lt;/code&gt; to finish the work I was doing, and it worked well. But I still missed the simplicity of Python's &lt;code&gt;requests&lt;/code&gt;, and I kept thinking about whether something like it was possible in Haskell.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding http-dispatch and Deciding to Build Something
&lt;/h2&gt;

&lt;p&gt;Some time later, I came across &lt;code&gt;http-dispatch&lt;/code&gt;. It was much closer to what I had in mind. The API was straightforward and the concepts were familiar. Unfortunately the library had not been maintained for a long time. There were some bugs, and some fixes in the source code had never made it to Hackage.&lt;/p&gt;

&lt;p&gt;That is when I decided to build my own.&lt;/p&gt;

&lt;p&gt;The goal was simple: a library for scenarios where you do not need fine-grained control over the HTTP lifecycle. For those cases, &lt;code&gt;http-client&lt;/code&gt; is the right tool. But for the common case of making a request and getting back a response, I wanted something that felt closer to the Python experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Core Abstraction
&lt;/h2&gt;

&lt;p&gt;I started from the most basic mental model of an HTTP interaction: you have a request, you send it to a server, and you get back a response.&lt;/p&gt;

&lt;p&gt;This became the core API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight haskell"&gt;&lt;code&gt;&lt;span class="kr"&gt;data&lt;/span&gt; &lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Request&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt;  &lt;span class="o"&gt;::&lt;/span&gt; &lt;span class="kt"&gt;Method&lt;/span&gt;
  &lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;     &lt;span class="o"&gt;::&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;
  &lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;::&lt;/span&gt; &lt;span class="kt"&gt;Headers&lt;/span&gt;
  &lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;    &lt;span class="o"&gt;::&lt;/span&gt; &lt;span class="kt"&gt;Maybe&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kr"&gt;data&lt;/span&gt; &lt;span class="kt"&gt;Response&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Response&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;  &lt;span class="o"&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;headers&lt;/span&gt; &lt;span class="o"&gt;::&lt;/span&gt; &lt;span class="kt"&gt;Headers&lt;/span&gt;
  &lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;    &lt;span class="o"&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;send&lt;/span&gt; &lt;span class="o"&gt;::&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;ToRequestBody&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;FromResponseBody&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;IO&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Response&lt;/span&gt; &lt;span class="n"&gt;b&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;send&lt;/code&gt; function takes a &lt;code&gt;Request&lt;/code&gt; and produces a &lt;code&gt;Response&lt;/code&gt; in &lt;code&gt;IO&lt;/code&gt;. The type parameters carry the body types, and the typeclass constraints handle serialization and deserialization automatically.&lt;/p&gt;

&lt;p&gt;For common HTTP methods, there are simple shortcuts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight haskell"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- A simple GET request&lt;/span&gt;
&lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="s"&gt;"https://example.com"&lt;/span&gt; &lt;span class="o"&gt;::&lt;/span&gt; &lt;span class="kt"&gt;IO&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Response&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;print&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;  &lt;span class="c1"&gt;-- 200&lt;/span&gt;
&lt;span class="n"&gt;print&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;    &lt;span class="c1"&gt;-- HTML content as String&lt;/span&gt;

&lt;span class="c1"&gt;-- POST with a plain text body&lt;/span&gt;
&lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="s"&gt;"https://httpbin.org/post"&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"hello"&lt;/span&gt; &lt;span class="o"&gt;::&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;::&lt;/span&gt; &lt;span class="kt"&gt;IO&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Response&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;print&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;  &lt;span class="c1"&gt;-- 200&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also construct a &lt;code&gt;Request&lt;/code&gt; manually when you need custom headers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight haskell"&gt;&lt;code&gt;&lt;span class="kr"&gt;let&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Request&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;GET&lt;/span&gt;
      &lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"https://api.example.com/data"&lt;/span&gt;
      &lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="s"&gt;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Bearer my-token"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
      &lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Nothing&lt;/span&gt; &lt;span class="o"&gt;::&lt;/span&gt; &lt;span class="kt"&gt;Maybe&lt;/span&gt; &lt;span class="kt"&gt;BS&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;ByteString&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;send&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;::&lt;/span&gt; &lt;span class="kt"&gt;IO&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Response&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The library supports modern Haskell record dot syntax (&lt;code&gt;resp.status&lt;/code&gt;, &lt;code&gt;resp.body&lt;/code&gt;) as well as traditional accessor functions (&lt;code&gt;responseStatus resp&lt;/code&gt;, &lt;code&gt;responseBody resp&lt;/code&gt;) for those who prefer not to enable language extensions.&lt;/p&gt;

&lt;h2&gt;
  
  
  JSON Integration
&lt;/h2&gt;

&lt;p&gt;The feature I most wanted to replicate from Python's &lt;code&gt;requests&lt;/code&gt; is its JSON handling. In Python, you pass &lt;code&gt;json=&lt;/code&gt; to your request and call &lt;code&gt;.json()&lt;/code&gt; on the response. It handles the content type header, the serialization, and the deserialization for you. Most API work is exactly this: send JSON, receive JSON.&lt;/p&gt;

&lt;p&gt;Haskell's type system makes this possible in a way that is, I think, even nicer than Python. Because we declare our data types upfront, we get static guarantees about the shape of the data.&lt;/p&gt;

&lt;p&gt;Here is an example of parsing a JSON response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight haskell"&gt;&lt;code&gt;&lt;span class="cp"&gt;{-# LANGUAGE DeriveGeneric #-}&lt;/span&gt;

&lt;span class="kr"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;Data.Aeson&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;FromJSON&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kr"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;GHC.Generics&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Generic&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kr"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;Network.HTTP.Request&lt;/span&gt;

&lt;span class="kr"&gt;data&lt;/span&gt; &lt;span class="kt"&gt;Date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Date&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;__type&lt;/span&gt; &lt;span class="o"&gt;::&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;
  &lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;iso&lt;/span&gt;     &lt;span class="o"&gt;::&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="kr"&gt;deriving&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Show&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Generic&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kr"&gt;instance&lt;/span&gt; &lt;span class="kt"&gt;FromJSON&lt;/span&gt; &lt;span class="kt"&gt;Date&lt;/span&gt;

&lt;span class="n"&gt;main&lt;/span&gt; &lt;span class="o"&gt;::&lt;/span&gt; &lt;span class="kt"&gt;IO&lt;/span&gt; &lt;span class="nb"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;main&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kr"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="s"&gt;"https://api.leancloud.cn/1.1/date"&lt;/span&gt; &lt;span class="o"&gt;::&lt;/span&gt; &lt;span class="kt"&gt;IO&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Response&lt;/span&gt; &lt;span class="kt"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;print&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;  &lt;span class="c1"&gt;-- 200&lt;/span&gt;
  &lt;span class="n"&gt;print&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;    &lt;span class="c1"&gt;-- Date { __type = "Date", iso = "2026-03-14T..." }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is no explicit parsing step. The type annotation &lt;code&gt;:: IO (Response Date)&lt;/code&gt; is enough to tell the library to decode the JSON body into a &lt;code&gt;Date&lt;/code&gt; value. If decoding fails, an &lt;code&gt;AesonException&lt;/code&gt; is thrown.&lt;/p&gt;

&lt;p&gt;Sending JSON works the same way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight haskell"&gt;&lt;code&gt;&lt;span class="kr"&gt;data&lt;/span&gt; &lt;span class="kt"&gt;Message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Message&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;::&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="kr"&gt;deriving&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Generic&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kr"&gt;instance&lt;/span&gt; &lt;span class="kt"&gt;ToJSON&lt;/span&gt; &lt;span class="kt"&gt;Message&lt;/span&gt;

&lt;span class="n"&gt;main&lt;/span&gt; &lt;span class="o"&gt;::&lt;/span&gt; &lt;span class="kt"&gt;IO&lt;/span&gt; &lt;span class="nb"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;main&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kr"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="s"&gt;"https://api.example.com/messages"&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Message&lt;/span&gt; &lt;span class="s"&gt;"Hello"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;::&lt;/span&gt; &lt;span class="kt"&gt;IO&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Response&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;print&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any type with a &lt;code&gt;ToJSON&lt;/code&gt; instance is automatically serialized, and &lt;code&gt;Content-Type: application/json&lt;/code&gt; is set on the request. You do not need to think about it.&lt;/p&gt;

&lt;p&gt;This gives you the simplicity of Python's &lt;code&gt;requests&lt;/code&gt; combined with the compile-time guarantees of a static type system. You define your API shapes as types, and the compiler helps you use them correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using It for an LLM Agent
&lt;/h2&gt;

&lt;p&gt;With the library in a working state, I wanted to build something more substantial with it. I decided to implement a small LLM agent. That became &lt;code&gt;hasuke&lt;/code&gt;, a CLI tool for interacting with Claude.&lt;/p&gt;

&lt;p&gt;Calling an Anthropic-style API is exactly the use case this library was built for. You construct a JSON request body, send it to the endpoint, and get back a JSON response. The library handled all of this without any friction.&lt;/p&gt;

&lt;p&gt;But after building the first version, I noticed that it would sit silently until the full response was generated, then display everything at once. Modern LLM providers support streaming responses to address this: they send partial results incrementally using Server-Sent Events, so you start seeing output right away.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding Streaming and SSE Support
&lt;/h2&gt;

&lt;p&gt;I extended the library to support streaming. The key was to express this within the existing type system without changing the core API. I introduced a &lt;code&gt;StreamBody&lt;/code&gt; type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight haskell"&gt;&lt;code&gt;&lt;span class="kr"&gt;data&lt;/span&gt; &lt;span class="kt"&gt;StreamBody&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;StreamBody&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;readNext&lt;/span&gt;    &lt;span class="o"&gt;::&lt;/span&gt; &lt;span class="kt"&gt;IO&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Maybe&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;closeStream&lt;/span&gt; &lt;span class="o"&gt;::&lt;/span&gt; &lt;span class="kt"&gt;IO&lt;/span&gt; &lt;span class="nb"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To receive a streaming response, you just change the type annotation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight haskell"&gt;&lt;code&gt;&lt;span class="kr"&gt;let&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="kt"&gt;GET&lt;/span&gt; &lt;span class="s"&gt;"https://example.com/stream"&lt;/span&gt; &lt;span class="kt"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Nothing&lt;/span&gt; &lt;span class="o"&gt;::&lt;/span&gt; &lt;span class="kt"&gt;Maybe&lt;/span&gt; &lt;span class="kt"&gt;BS&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;ByteString&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;send&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;::&lt;/span&gt; &lt;span class="kt"&gt;IO&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Response&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;StreamBody&lt;/span&gt; &lt;span class="kt"&gt;BS&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;ByteString&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="kr"&gt;let&lt;/span&gt; &lt;span class="n"&gt;loop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kr"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;mChunk&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;readNext&lt;/span&gt;
      &lt;span class="kr"&gt;case&lt;/span&gt; &lt;span class="n"&gt;mChunk&lt;/span&gt; &lt;span class="kr"&gt;of&lt;/span&gt;
        &lt;span class="kt"&gt;Nothing&lt;/span&gt;    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;()&lt;/span&gt;
        &lt;span class="kt"&gt;Just&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;BS&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;putStr&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;loop&lt;/span&gt;
&lt;span class="n"&gt;loop&lt;/span&gt;
&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;closeStream&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For SSE, the library parses the event stream protocol automatically. Each &lt;code&gt;SseEvent&lt;/code&gt; has fields for the data, event type, and event id:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight haskell"&gt;&lt;code&gt;&lt;span class="kr"&gt;data&lt;/span&gt; &lt;span class="kt"&gt;SseEvent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;SseEvent&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;sseData&lt;/span&gt; &lt;span class="o"&gt;::&lt;/span&gt; &lt;span class="kt"&gt;T&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;Text&lt;/span&gt;
  &lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sseType&lt;/span&gt; &lt;span class="o"&gt;::&lt;/span&gt; &lt;span class="kt"&gt;Maybe&lt;/span&gt; &lt;span class="kt"&gt;T&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;Text&lt;/span&gt;
  &lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sseId&lt;/span&gt;   &lt;span class="o"&gt;::&lt;/span&gt; &lt;span class="kt"&gt;Maybe&lt;/span&gt; &lt;span class="kt"&gt;T&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;Text&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using it looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight haskell"&gt;&lt;code&gt;&lt;span class="kr"&gt;let&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="kt"&gt;POST&lt;/span&gt; &lt;span class="s"&gt;"https://api.anthropic.com/v1/messages"&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Just&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;send&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;::&lt;/span&gt; &lt;span class="kt"&gt;IO&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Response&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;StreamBody&lt;/span&gt; &lt;span class="kt"&gt;SseEvent&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="kr"&gt;let&lt;/span&gt; &lt;span class="n"&gt;loop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kr"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;mEvent&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;readNext&lt;/span&gt;
      &lt;span class="kr"&gt;case&lt;/span&gt; &lt;span class="n"&gt;mEvent&lt;/span&gt; &lt;span class="kr"&gt;of&lt;/span&gt;
        &lt;span class="kt"&gt;Nothing&lt;/span&gt;    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;()&lt;/span&gt;
        &lt;span class="kt"&gt;Just&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;T&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;putStr&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sseData&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;loop&lt;/span&gt;
&lt;span class="n"&gt;loop&lt;/span&gt;
&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;closeStream&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;send&lt;/code&gt; function signature did not change at all. When the target type is &lt;code&gt;StreamBody SseEvent&lt;/code&gt;, the library keeps the connection open and streams events through an internal buffer. From the caller's side, you are just getting a different kind of response body.&lt;/p&gt;

&lt;p&gt;This is where Haskell's type system earns its keep. Adding a completely different data transfer mode required almost no changes to the existing API. The same &lt;code&gt;send&lt;/code&gt; function, the same &lt;code&gt;Request&lt;/code&gt; type, the same conventions. It is now working well in &lt;code&gt;hasuke&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Things Stand
&lt;/h2&gt;

&lt;p&gt;The library is published on Hackage under the name &lt;code&gt;request&lt;/code&gt; and can be installed with cabal or stack in the usual way. The source code is on GitHub at &lt;a href="https://github.com/aisk/request" rel="noopener noreferrer"&gt;https://github.com/aisk/request&lt;/a&gt;. It powers the streaming output in &lt;code&gt;hasuke&lt;/code&gt;, which you can find at &lt;a href="https://github.com/aisk/hasuke" rel="noopener noreferrer"&gt;https://github.com/aisk/hasuke&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;There are features still missing, such as support for HTML form encoding and some other less common use cases. I plan to add these gradually over time. If you run into something you need, feel free to open an issue on GitHub. Feedback and contributions are very welcome.&lt;/p&gt;

&lt;p&gt;For the common case of calling JSON APIs, whether in a single response or as a stream, the library does what it was built to do. If you are learning Haskell and looking for a practical project, or you just need a lightweight HTTP client, it might be worth a try.&lt;/p&gt;

</description>
      <category>haskell</category>
      <category>http</category>
    </item>
    <item>
      <title>Making Python Modules Callable: Introducing Cadule</title>
      <dc:creator>Maple</dc:creator>
      <pubDate>Fri, 26 Dec 2025 15:02:37 +0000</pubDate>
      <link>https://dev.to/maple/making-python-modules-callable-introducing-cadule-mba</link>
      <guid>https://dev.to/maple/making-python-modules-callable-introducing-cadule-mba</guid>
      <description>&lt;p&gt;When writing Python code, I often find myself missing a feature from Node.js: the ability to directly &lt;code&gt;require&lt;/code&gt; a module and have it become callable. In Node.js, you can export a function from a module and call it immediately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;messUpThings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./mess-up-things&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;messUpThings&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Works!&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I know some developers aren't fond of this pattern, but there are legitimate use cases for it.&lt;/p&gt;

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

&lt;p&gt;Consider a scenario where you need to write a helper function with a self-descriptive name, like &lt;code&gt;mess_up_things&lt;/code&gt;. The function name itself tells you what it does. You have a few options:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Create a util or helper module&lt;/strong&gt;: Put it in a &lt;code&gt;util.py&lt;/code&gt; or &lt;code&gt;helper.py&lt;/code&gt; module. But many developers dislike these generic names, especially Go developers who prefer more descriptive package names.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Create a dedicated file&lt;/strong&gt;: Put it in &lt;code&gt;mess_up_things.py&lt;/code&gt;. But then you need to write verbose import statements like:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;mess_up_things&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;mess_up_things&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This repetition feels unnecessary. Wouldn't it be nice if you could just do:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;mess_up_things&lt;/span&gt;
&lt;span class="nf"&gt;mess_up_things&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Exploring Solutions
&lt;/h2&gt;

&lt;p&gt;If you're familiar with Python's magic methods, you might know that any object with a &lt;code&gt;__call__&lt;/code&gt; method becomes callable. Since modules are instances of &lt;code&gt;types.ModuleType&lt;/code&gt;, and they support magic methods like &lt;code&gt;__getattr__&lt;/code&gt;, couldn't we just define a &lt;code&gt;__call__&lt;/code&gt; method directly on a module?&lt;/p&gt;

&lt;p&gt;Unfortunately, Python doesn't support this out of the box. There was actually &lt;a href="https://peps.python.org/pep-0713/" rel="noopener noreferrer"&gt;PEP 713&lt;/a&gt; that proposed adding module-level &lt;code&gt;__call__&lt;/code&gt; support, but it was rejected.&lt;/p&gt;

&lt;p&gt;However, if you're familiar with Python's runtime, you'll find there's a trick to achieve this behavior:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyModule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;modules&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="n"&gt;__class__&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__call__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Messing up things...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;modules&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="n"&gt;__class__&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MyModule&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works by dynamically replacing the module's &lt;code&gt;__class__&lt;/code&gt; at runtime. But who wants to copy-paste this boilerplate every time?&lt;/p&gt;

&lt;h2&gt;
  
  
  Enter Cadule
&lt;/h2&gt;

&lt;p&gt;To avoid repeating this boilerplate, I've encapsulated this simple mechanism into a package called &lt;strong&gt;Cadule&lt;/strong&gt; (short for &lt;strong&gt;Ca&lt;/strong&gt;llable &lt;strong&gt;[Mo]dule&lt;/strong&gt; &lt;strong&gt;Le&lt;/strong&gt;ss).&lt;/p&gt;

&lt;h3&gt;
  
  
  Installation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;cadule
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Usage
&lt;/h3&gt;

&lt;p&gt;Create a file called &lt;code&gt;mess_up_things.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;cadule&lt;/span&gt;

&lt;span class="nd"&gt;@cadule&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__call__&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Messing up things...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then in your Python REPL or another script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;mess_up_things&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;mess_up_things&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;Messing&lt;/span&gt; &lt;span class="n"&gt;up&lt;/span&gt; &lt;span class="n"&gt;things&lt;/span&gt;&lt;span class="bp"&gt;...&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;callable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mess_up_things&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="bp"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it! The entire &lt;code&gt;mess_up_things&lt;/code&gt; module is now a callable object. When you call it, it executes the decorated &lt;code&gt;__call__&lt;/code&gt; function.&lt;/p&gt;

&lt;p&gt;You can also pass arguments and return values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;cadule&lt;/span&gt;

&lt;span class="nd"&gt;@cadule&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__call__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Messing up &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;mess_up_things&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;mess_up_things&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;the database&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Messing up the database!&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  When to Use It
&lt;/h2&gt;

&lt;p&gt;Cadule is particularly useful for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Single-purpose modules&lt;/strong&gt;: When a module's main purpose is to expose one function&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DSL and fluent interfaces&lt;/strong&gt;: Creating more natural-looking APIs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scripts and utilities&lt;/strong&gt;: Making command-line tools more intuitive&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Github: &lt;a href="https://github.com/aisk/cadule" rel="noopener noreferrer"&gt;https://github.com/aisk/cadule&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
    </item>
    <item>
      <title>Bringing timeout to macOS Without Installing All of GNU Coreutils</title>
      <dc:creator>Maple</dc:creator>
      <pubDate>Sun, 21 Sep 2025 07:21:11 +0000</pubDate>
      <link>https://dev.to/maple/bringing-timeout-to-macos-without-installing-all-of-gnu-coreutils-22lj</link>
      <guid>https://dev.to/maple/bringing-timeout-to-macos-without-installing-all-of-gnu-coreutils-22lj</guid>
      <description>&lt;p&gt;Not long ago, I was writing a shell script on macOS and ran into a small but surprising issue: there was no built-in &lt;code&gt;timeout&lt;/code&gt; command. If you’ve spent time on Linux, you’ve probably taken &lt;code&gt;timeout&lt;/code&gt; for granted, it’s part of GNU coreutils and is incredibly handy when you want to limit how long a command can run.&lt;/p&gt;

&lt;p&gt;On macOS, though, the situation is different. macOS ships with a BSD-flavored set of core utilities, and GNU’s version of &lt;code&gt;timeout&lt;/code&gt; simply isn’t there. My first instinct was to install GNU coreutils through Homebrew:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;coreutils
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This does work—but it comes with a catch. To avoid conflicts with the BSD commands that ship with macOS (like &lt;code&gt;ls&lt;/code&gt; and &lt;code&gt;cat&lt;/code&gt;), Homebrew installs GNU coreutils under names prefixed with a &lt;code&gt;g&lt;/code&gt;. So instead of &lt;code&gt;ls&lt;/code&gt;, you get &lt;code&gt;gls&lt;/code&gt;; instead of &lt;code&gt;timeout&lt;/code&gt;, you get &lt;code&gt;gtimeout&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That’s fine if you’re working in a personal environment, but it creates headaches for writing portable shell scripts. If I write a script that calls &lt;code&gt;gtimeout&lt;/code&gt; on macOS, it won’t run out-of-the-box on Linux, where the command is just &lt;code&gt;timeout&lt;/code&gt;. Homebrew does provide a way to remove the &lt;code&gt;g&lt;/code&gt; prefix, but that’s a pretty heavy-handed solution—suddenly your system’s default &lt;code&gt;ls&lt;/code&gt;, &lt;code&gt;cat&lt;/code&gt;, and other tools are replaced by GNU versions, which can break expectations in subtle ways.&lt;/p&gt;

&lt;p&gt;So, instead of going down that path, I decided to build a standalone &lt;code&gt;timeout&lt;/code&gt; command that behaves like GNU’s version but doesn’t bring along the entire coreutils package. This way, you can install just what you need without worrying about side effects.&lt;/p&gt;

&lt;p&gt;The project is open source and lives here:&lt;br&gt;
👉 &lt;a href="https://github.com/aisk/timeout" rel="noopener noreferrer"&gt;https://github.com/aisk/timeout&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And to make installation easier, I also set up a Homebrew tap. You can install it directly with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;aisk/homebrew-tap/timeout
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s it—you’ll get a drop-in replacement for &lt;code&gt;timeout&lt;/code&gt;, without the &lt;code&gt;g&lt;/code&gt; prefix, and without swapping out your other system tools.&lt;/p&gt;

&lt;p&gt;I built this mainly to solve my own problem, but if it helps you too, I’d really appreciate a star on the GitHub repo. Every bit of support means a lot and helps me keep the project alive.&lt;/p&gt;

</description>
      <category>homebrew</category>
      <category>cli</category>
      <category>linux</category>
      <category>bash</category>
    </item>
    <item>
      <title>Build and Modify Linux System Image for C-sky based Gx6605s Board</title>
      <dc:creator>Maple</dc:creator>
      <pubDate>Sun, 11 Aug 2024 12:02:03 +0000</pubDate>
      <link>https://dev.to/maple/build-and-modify-linux-system-image-for-c-sky-based-gx6605s-board-3i7j</link>
      <guid>https://dev.to/maple/build-and-modify-linux-system-image-for-c-sky-based-gx6605s-board-3i7j</guid>
      <description>&lt;p&gt;The Gx6605s is a very cheap development board (39 Chinese Yuan with free shipping), which has a C-sky instruction set based CPU. The support for Linux kernel, GCC, and even Greenlet, is upstreamed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp0woalkicrtw0z6q0fbi.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp0woalkicrtw0z6q0fbi.jpg" alt="Gx6605s development board" width="800" height="597"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I have a Gx6605s board which I bought years ago, and I only tried to run it with C-sky's official system image. It's a Buildroot-based Linux system, and there is no package manager for it. I can't install more packages / software on it, thus it is not useful for me, so I quickly lost interest in it.&lt;/p&gt;

&lt;p&gt;When I found it in a box after moving to another city, I thought maybe I could do something with it, so I decided to give it a try.&lt;/p&gt;

&lt;p&gt;But sadly, the company behind the development of the dev board and instruction set has abandoned the development for the dev board and C-sky instructions, and has continued their work on RiscV.&lt;/p&gt;

&lt;p&gt;They have a Buildroot fork on GitLab which can produce the root file system for the image, but the support for this board and C-sky instruction set was dropped some time ago. Now this repo can't produce a system image for Gx6605s.&lt;/p&gt;

&lt;p&gt;I made a dig into the repo and found that there is a branch called &lt;a href="https://gitlab.com/c-sky/buildroot/-/tree/master_bakup" rel="noopener noreferrer"&gt;&lt;code&gt;master_backup&lt;/code&gt;&lt;/a&gt;, which was once their development branch capable of building C-sky CPU based system images. The build processes were run on GitLab Pipeline. So, you can fork this repo, and then run the GitLab Pipeline on the web page to get the generated system image.&lt;/p&gt;

&lt;p&gt;But unfortunately, there is a file that Gx6605s Buildroot depended on, hosted at &lt;a href="https://github.com/c-sky/tools" rel="noopener noreferrer"&gt;https://github.com/c-sky/tools&lt;/a&gt;, which has been deleted and changed to a new unrelated repo. Luckily, I found a mirror site that has this repo, so you can just apply this patch to get the file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gh"&gt;diff --git a/package/csky-debug/csky-debug.mk b/package/csky-debug/csky-debug.mk
index 241755019e..a318247ebf 100644
&lt;/span&gt;&lt;span class="gd"&gt;--- a/package/csky-debug/csky-debug.mk
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/package/csky-debug/csky-debug.mk
&lt;/span&gt;&lt;span class="p"&gt;@@ -6,7 +6,7 @@&lt;/span&gt;
&lt;span class="err"&gt;
&lt;/span&gt; CSKY_DEBUG_VERSION = V4.2.0-tmp-20170411
 CSKY_DEBUG_SOURCE = DebugServerConsole-linux-x86_64-$(CSKY_DEBUG_VERSION).tar.gz
&lt;span class="gd"&gt;-CSKY_DEBUG_SITE = https://github.com/c-sky/tools/raw/master
&lt;/span&gt;&lt;span class="gi"&gt;+CSKY_DEBUG_SITE = https://isrc.iscas.ac.cn/gitlab/mirrors/github.com/c-sky_tools/-/raw/master
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt; define HOST_CSKY_DEBUG_INSTALL_CMDS
        mkdir -p $(HOST_DIR)/csky-debug
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this, you can build a system image for the Gx6605s board, and it's a little newer than the one provided on the C-sky official site. However, I want to modify the system image by adding more packages, such as MicroPython and even a GCC compiler. Therefore, I need to build it locally.&lt;/p&gt;

&lt;p&gt;Take a look at the &lt;a href="https://gitlab.com/c-sky/buildroot/-/blob/master_bakup/.gitlab-ci.yml" rel="noopener noreferrer"&gt;&lt;code&gt;.gitlab-ci.yml&lt;/code&gt;&lt;/a&gt;. I believe it's using a Docker image called &lt;code&gt;maohan001/ubuntu-buildroot&lt;/code&gt; as the building environment. Please note that since they dropped support for the C-sky instruction set, if you want to build the image in the future, please pull and backup this image. It's not Dockerfile-based, so we don't know how to reproduce it.&lt;/p&gt;

&lt;p&gt;I've never used Buildroot before, but from the GitLab Pipeline's log and the &lt;code&gt;.gitlab-ci.yml&lt;/code&gt;, I assume one should run &lt;code&gt;make {config-name}&lt;/code&gt; inside the Docker image with the &lt;code&gt;buildroot&lt;/code&gt; root path to create a configuration for the specified development board. Then, run &lt;code&gt;make&lt;/code&gt; to download all necessary files and build the compiler, kernel, libc, and userland.&lt;/p&gt;

&lt;p&gt;So, for the Gx6605s, we should use &lt;code&gt;$ make csky_610_gx6605s_4.9_uclibc_br_defconfig&lt;/code&gt;, and then &lt;code&gt;$ make&lt;/code&gt;. You will get a &lt;code&gt;usb.img.xz&lt;/code&gt; in the &lt;code&gt;output/images&lt;/code&gt; folder. This should be exactly what you got in the GitLab Pipeline.&lt;/p&gt;

&lt;p&gt;And as the &lt;code&gt;Buildroot&lt;/code&gt;'s official website says, now we can run &lt;code&gt;$ make menuconfig&lt;/code&gt; to customize the build process, such as adding MicroPython to the system.&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
