Back to list
dev_to 2026年3月14日

Haskell に Python-requests 風の HTTP クライアントを構築する

Building a Python-requests-style HTTP Client in Haskell

Translated: 2026/3/14 11:17:50
haskellhttp-clientfunctional-programminghttp-requesttype-safe

Japanese Translation

Haskell を学ぶ際の課題 私が高askellを学び始めた頃、それを適用するための適切なシナリオを見つけるのに苦労していました。具体的な問題を解くことなしに、本格的な進捗を上げるのは困難です。ある時点で私は実務的なタスクを抱えることになりました: HTTP API からデータを取得し、結果を処理し、他のエンドポイントに投稿する。私の本能は、この種のタスクには Python と requests ライブラリを選ぶことでした。それは本当に便利なもので、これを数行で済ませられます。しかし私は: これがまさに私が解決すべき実問題だと思いました。なぜなら Haskell を試すのか? 最初に手がけたのは http-client でした。これは強力であり、他の大半の Haskell HTTP ライブラリの基盤です。しかし、それを直接使うには単純なタスクのためにかなり多くの設定が必要です。マネージャーを作成し、リクエストを解析し、低レベルのタイプセットで作業する必要があります。それは素晴らしいライブラリですが、正確な制御が必要なケースのために設計されています。いくつかの軽量な代替案があります。wreq は人気のあるものです。lens API に基づいています。レンズは Haskell エコシステムの魅力的な一部分であり、多くの人々がそれを学ぶ価値を見つけます。req はもう一つの選択肢であり、清潔でタイプ安全的な API です。私は req を使って私の行っていた作業を終え、それはよく機能しましたが。それでも Python の requests のシンプルさを欠き、何か同様のものが Haskell で可能かという考えをずっと持ち続けます。ある時、http-dispatch が見つかりました。それは私が考え込んでいたものとかなり近かったです。API は単純明快で、概念も馴染んでいました。残念ながら、このライブラリは長期間メンテナンスされておらず。いくつかのバグがあり、ソースコード上のいくつかの修正は Hackage には決して到達しませんでした。それが私を決して自らのものを作ることを決意させたものです。目標は単純で: HTTP ライフサイクルに対して細かい制御が不要なシナリオのためにライブラリです。对于那些情況、http-client が適切なツールです。しかし、一般的なリクエストを送り、応答を受け取るケースについては、Python の経験に近いものを感じたいと思いました。私は最も基本的な HTTP インタラクションの精神モデルから始めました: あなたはリクエストを持って、それをサーバーに送ります、そして応答を受け取ります。これは核心の API になりました:データリクエスト a = リクエスト { メソッド :: メソッド , url :: スtring , ヘッダー :: ヘッダー , ボディ :: ?a } デスパンス a = レスポンンス { ステータス :: 整数 , ヘッダー :: ヘッダー , ボディ :: a } 送信 :: (トRequestBody a, フロムResponseBody b) => リクエスト a -> IO (レスポンンス b) 送信関数はリクエストを受け取り、IO でレスポンンスを生み出します。タイプパラメータはボディタイプを持ち、タイプクラス制約はシリアライゼーションとデシリアライゼーションを自動的に処理します。一般的な HTTP メソッドについては、単純なショートカットがあります:-- 単なる GET リクエストレスポンス <- グッ "https://example.com" :: IO (レスポンス スリング) プリンット レスポンンスステータス -- 200 プリンット レスポンンスボディ -- HTML コンテンツ スリング -- ボディに素文字で POST レスポンンス <- ポスト "https://httpbin.org/post" ("こんにちは" :: スリング) :: IO (レスポンス スリング) プリンット レスポンンスステータス -- 200 あなたはカスタムヘッダーが必要な場合に手動でリクエストを構築することもできます: let リクエスト = リクエスト { メソッド = グッ , url = "https://api.example.com/data" , ヘッダー = [("認証", "私のトークン"] , ボディ = ?BS.ByteString } レスポンンス <- 送信 リクエスト :: IO (レスポンス スリング) ライブラリは現代的な Haskell レコード点記法 (レスポンスステータス、レスポンスボディ) だけでなく、伝統的なアクセサ関数 (レスポンンスステータス レスポンンス、レスポンスボディ レスポンンス) をサポートし、ラングエイジ拡張を有効にしない人々向けです。私が最も真似したかった Python の requests の機能はその JSON 処理です。Python では、あなたのリクエストに json= を渡し、応答に .json () を呼び出し、それはコンテンツタイプヘッダー、シリアライゼーション、そしてデシリアライゼーションをあなたのために処理します。ほとんどの API ワークはまさにこれがそうです:JSON を送信し、JSON を受け取る。Haskell のタイプシステムは、これは、私は思います、Python よりも nicerな形でこれを可能にします。なぜなら、私たちのデータタイプを前もって宣言するから、私たちは静的な保証を得るからです、ab ...

Original Content

The Problem with Learning Haskell 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. 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 requests 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. But I thought: this is exactly the kind of real problem I need. Why not try Haskell? The first thing I reached for was http-client. 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. There are some lighter alternatives. wreq 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. req is another option with a clean, type-safe API. I used req to finish the work I was doing, and it worked well. But I still missed the simplicity of Python's requests, and I kept thinking about whether something like it was possible in Haskell. Some time later, I came across http-dispatch. 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. That is when I decided to build my own. The goal was simple: a library for scenarios where you do not need fine-grained control over the HTTP lifecycle. For those cases, http-client 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. 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. This became the core API: data Request a = Request { method :: Method , url :: String , headers :: Headers , body :: Maybe a } data Response a = Response { status :: Int , headers :: Headers , body :: a } send :: (ToRequestBody a, FromResponseBody b) => Request a -> IO (Response b) The send function takes a Request and produces a Response in IO. The type parameters carry the body types, and the typeclass constraints handle serialization and deserialization automatically. For common HTTP methods, there are simple shortcuts: -- A simple GET request resp <- get "https://example.com" :: IO (Response String) print resp.status -- 200 print resp.body -- HTML content as String -- POST with a plain text body resp <- post "https://httpbin.org/post" ("hello" :: String) :: IO (Response String) print resp.status -- 200 You can also construct a Request manually when you need custom headers: let req = Request { method = GET , url = "https://api.example.com/data" , headers = [("Authorization", "Bearer my-token")] , body = Nothing :: Maybe BS.ByteString } resp <- send req :: IO (Response String) The library supports modern Haskell record dot syntax (resp.status, resp.body) as well as traditional accessor functions (responseStatus resp, responseBody resp) for those who prefer not to enable language extensions. The feature I most wanted to replicate from Python's requests is its JSON handling. In Python, you pass json= to your request and call .json() 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. 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. Here is an example of parsing a JSON response: {-# LANGUAGE DeriveGeneric #-} import Data.Aeson (FromJSON) import GHC.Generics (Generic) import Network.HTTP.Request data Date = Date { __type :: String , iso :: String } deriving (Show, Generic) instance FromJSON Date main :: IO () main = do response <- get "https://api.leancloud.cn/1.1/date" :: IO (Response Date) print response.status -- 200 print response.body -- Date { __type = "Date", iso = "2026-03-14T..." } There is no explicit parsing step. The type annotation :: IO (Response Date) is enough to tell the library to decode the JSON body into a Date value. If decoding fails, an AesonException is thrown. Sending JSON works the same way: data Message = Message { content :: String } deriving (Generic) instance ToJSON Message main :: IO () main = do resp <- post "https://api.example.com/messages" (Message "Hello") :: IO (Response String) print resp.status Any type with a ToJSON instance is automatically serialized, and Content-Type: application/json is set on the request. You do not need to think about it. This gives you the simplicity of Python's requests 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. 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 hasuke, a CLI tool for interacting with Claude. 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. 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. 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 StreamBody type: data StreamBody a = StreamBody { readNext :: IO (Maybe a) , closeStream :: IO () } To receive a streaming response, you just change the type annotation: let req = Request GET "https://example.com/stream" [] (Nothing :: Maybe BS.ByteString) resp <- send req :: IO (Response (StreamBody BS.ByteString)) let loop = do mChunk <- resp.body.readNext case mChunk of Nothing -> return () Just chunk -> BS.putStr chunk >> loop loop resp.body.closeStream For SSE, the library parses the event stream protocol automatically. Each SseEvent has fields for the data, event type, and event id: data SseEvent = SseEvent { sseData :: T.Text , sseType :: Maybe T.Text , sseId :: Maybe T.Text } Using it looks like this: let req = Request POST "https://api.anthropic.com/v1/messages" headers (Just body) resp <- send req :: IO (Response (StreamBody SseEvent)) let loop = do mEvent <- resp.body.readNext case mEvent of Nothing -> return () Just event -> T.putStr event.sseData >> loop loop resp.body.closeStream The send function signature did not change at all. When the target type is StreamBody SseEvent, 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. 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 send function, the same Request type, the same conventions. It is now working well in hasuke. The library is published on Hackage under the name request and can be installed with cabal or stack in the usual way. The source code is on GitHub at https://github.com/aisk/request. It powers the streaming output in hasuke, which you can find at https://github.com/aisk/hasuke. 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. 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.