Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Haskell/Servantを通してWebミドルウェアを捉え直す

Sponsored · Ship Features Fearlessly Turn features on and off without deploys. Used by thousands of Ruby developers.

 Haskell/Servantを通してWebミドルウェアを捉え直す

関数型まつり2026の登壇資料です。

Avatar for pizzacat83

pizzacat83

July 10, 2026

More Decks by pizzacat83

Other Decks in Programming

Transcript

  1. Express の Request 型に user フィールドを生やす // express.d.ts declare global

    { namespace Express { interface Request { user?: User | null; } } }
  2. Request.user フィールドが常に存在するということにするか declare global { namespace Express { interface Request

    { user: User; } } } (でも認証ミドルウェアを通過する前は Request.user は無いから unsound...) // middleware.ts const payload = jwt.verify(token, JWT_SECRET) as JWTPayload; // この時点で req.user はまだ undefined なのに、 // 型推論上は non-null になってしまう req.user = { ... };
  3. 多くのフレームワークにおけるミドルウェアの型 type Handler = (req: Req, resp: RespWriter) => Promise<void>

    type Middleware = ( req: Req, resp: RespWriter, next: (Req, RespWriter) => Promise<void> ) => Promise<void> // ミドルウェアの例 const authMiddleware = async (req: Req, resp: Resp, next: Handler) => { const authResult = await auth(req); if (!authResult.ok) { resp.send(403); return; } await next( { ...req, user: authResult.user }, resp, ) }
  4. // type Middleware = ( req: Req, resp: RespWriter, next:

    (Req, RespWriter) => Promise<void>, ) => Promise<void> ↓引数順を入れ替えると ( next: (Req, RespWriter) => Promise<void>, req: Req, resp: RespWriter, ) => Promise<void> ↓カリー化 (一部だけ) (next: (Req, RespWriter) => Promise<void>) => (req: Req, resp: RespWriter) => Promise<void>
  5. 認証のあるAPIの実装例 GET /orders が「自分の注文一覧」を返す API main = run 8080 (serveWithContext

    api ctx server) -- (1) API の仕様 ( 型レベルDSL で記述) type API = BasicAuth User :> "orders" :> Get '[JSON] [Post] api :: Proxy API api = Proxy -- (2) サーバーの実装 server :: Server API -- サーバー実装の型は `API` から自動導出 server user = do orders <- listOrders (userId user) pure orders -- (3) 認証の中身の実装 ctx = checkBasicAuth :. ... where checkBasicAuth :: BasicAuthCheck User checkBasicAuth db = BasicAuthCheck $ (username, password) ->
  6. まず API 仕様を型レベル DSL で記述 type API = BasicAuth User

    :> "orders" :> Get '[JSON] [Order] GET /orders のレスポンスは Order のリスト、 application/json Basic 認証がかかっていて、ログイン成功時のユーザー情報の型が User type API 自体はリクエストハンドラの型ではない 型レベルの項で記述された DSL 構文木のようなもの
  7. まず API 仕様を型レベル DSL で記述 もちろん、複数エンドポイントのある API も記述できる type API

    = "books" :> Get '[JSON] [Book] :<|> BasicAuth User :> ( "orders" :> Get '[JSON] [Order] :<|> "settings" :> Get '[JSON] Settings ) GET /books (認証不要) GET /orders (要認証) GET /settings (要認証) :<|> で枝を作れる
  8. ハンドラの型は、記述した「API 仕様」から自動導出 server :: Server API server user = do

    orders <- listOrders (userId user) pure orders Server _ は「API 仕様を受け取ってハンドラの型を返す」型レベルの関数っぽいもの 計算すると…… Server API = User -> IO (Either ServerError [Order]) ユーザーを引数にとり、 Order リスト or ServerError を返す IO
  9. 引数に User 型が生える原理 type instance Server (BasicAuth user :> api)

    = user -> Server api つまり BasicAuth User :> api に対応するサーバーの型は、 api に対応するハンドラの型に引数 User -> を足したもの 「認証を通ったハンドラは User を得られる」が、型にちゃんと反映されている!
  10. 「型を保つミドルウェア」と Servant の設計は一見違 うもの? Express: ミドルウェアを重ねてアプリケーションを構築 ミドルウェアはハンドラ型を変えない Handler -> Handler

    Servant: コンビネータを組み合わせて API 仕様を記述 コンビネータはハンドラ型を変形できる User -> Server api 実は共通の哲学がある: 「合成可能な部品を組み合わ せてアプリケーションを構築」
  11. 「型シグネチャを変えうるミドルウェア」を合成可能 にするのは非自明 認証を「User付きハンドラ」→「ふつうのハンドラ」でモデル化してみる const authMiddleware: (handlerAfterAuth: (User, Req, Resp) =>

    Promise<void>) => (Req, Resp) => Promise<void> 別のミドルウェアもこんな感じでモデル化してみる const corsMiddleware: (handler: (Req, Resp) => Promise<void>) => (Req, Resp) => Promise<void> Q. authMiddleware と corsMiddleware は自由に組み合わせられるか?
  12. 「型シグネチャを変えうるミドルウェア」を合成可能 にするのは非自明 const authMiddleware: (handlerAfterAuth: (User, Req, Resp) => Promise<void>)

    => (Req, Resp) => Promise<void> const corsMiddleware: (handler: (Req, Resp) => Promise<void>) => (Req, Resp) => Promise<void> Q. authMiddleware と corsMiddleware は自由に組み合わせられるか? corsMiddleware . authMiddleware $ handler authMiddleware . corsMiddleware $ handler → 型エラー! handler は User 引数をもつが、 corsMiddleware は (Req, Res) だけ渡す
  13. Servant コンビネータの合成可能性 (:>) :: forall k. k -> Type ->

    Type (BasicAuth User :>) は「API 仕様を引数にとって API 仕様を返す」 コンビネータは「API仕様 → API仕様」という "形" (kind) を保っている (ハンドラの型を保っているわけではない)
  14. ちなみに:パスパラメータも同じ「コンビネータ」 よくあるフレームワークだと、ルーティング定義から id は確実に存在するのに、型 推論上は string | null id を

    typo しても気づけない router.get('/orders/:id', (req, res) => { const orderId: string | null = req.params.id // ... }) Servant で /orders/:id はこう↓ type API = "orders" :> Capture "id" Int :> Get '[JSON] Order Server API = Int -> IO (Either ServerError Order)