From ebc549f0b8b2cd09a471a2b88eb1bb1d7a4158a1 Mon Sep 17 00:00:00 2001 From: YAMAMOTO Yuji Date: Sun, 9 Feb 2025 18:07:58 +0900 Subject: [PATCH 01/39] (WIP) Add article of wai-sample --- preprocessed-site/posts/2024/wai-sample.md | 232 +++++++++++++++++++++ 1 file changed, 232 insertions(+) create mode 100644 preprocessed-site/posts/2024/wai-sample.md diff --git a/preprocessed-site/posts/2024/wai-sample.md b/preprocessed-site/posts/2024/wai-sample.md new file mode 100644 index 0000000..00bee9c --- /dev/null +++ b/preprocessed-site/posts/2024/wai-sample.md @@ -0,0 +1,232 @@ +--- +title: 単純なHaskellのみでServant並に高機能なライブラリーを作ろうとした振り返り +subHeading: +headingBackgroundImage: ../img/background.png +headingDivClass: post-heading +author: YAMAMOTO Yuji +postedBy: YAMAMOTO Yuji(@igrep) +date: November 27, 2024 +tags: +... +--- + +この記事では、「[Haskell製ウェブアプリケーションフレームワークを作る配信](https://www.youtube.com/playlist?list=PLRVf2pXOpAzJMFN810EWwGrH_qii7DKyn)」で配信していた、Haskell製ウェブアプリケーションフレームワークを作るプロジェクトについて振り返ります。Servantのような型安全なAPI定義を、(Servantのような)高度な型レベルプログラミングも、(Yesodのような)TemplateHaskellもなしに可能にするライブラリーを目指していましたが、開発を途中で止めることにしました。その振り返り --- とりわけ、そのゴールに基づいて実装するのが原理上不可能だと分かった機能などを中心にまとめます。 + +# 動機 + +そもそも、Haskellには既にServantやYesod、Scottyといった人気のフレームワークがあるにもかかわらず、なぜ新しいフレームワークを作ろうと思ったのでしょうか。第一に、かつて私が[「Haskellの歩き方」という記事の「Webアプリケーション」の節](https://wiki.haskell.jp/Hikers%20Guide%20to%20Haskell.html#web%E3%82%A2%E3%83%97%E3%83%AA%E3%82%B1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3)で述べた、次の問題を解決したかったから、という理由があります: + +> ただしServant, Yesod, 共通した困った特徴があります。 +> それぞれがHaskellの高度な機能を利用した独特なDSLを提供しているため、仕組みがわかりづらい、という点です。 +> Servantは、「型レベルプログラミング」と呼ばれる、GHCの言語拡張を使った仕組みを駆使して、型宣言だけでREST APIの仕様を記述できるようにしています。 +> YesodもGHCの言語拡張をたくさん使っているのに加え、特に変わった特徴として、TemplateHaskellやQuasiQuoteという仕組みを利用して、独自のDSLを提供しています。 +> それぞれ、見慣れたHaskellと多かれ少なかれ異なる構文で書かなければいけない部分があるのです。 +> つまり、これらのうちどちらかを使う以上、どちらかの魔法を覚えなければならないのです。 + +この「どちらかの魔法を覚えなければならない」という問題は、初心者がHaskellでウェブアプリケーションを作る上で大きな壁になりえます。入門書に書いてあるHaskellの機能だけでは、ServantやYesodなどのフレームワークで書くコードを理解できず、サンプルコードから雰囲気で書かなければならないのです。これが、新しいフレームワークを作ろうとした一番の動機です。 + +その他、このフレームワークを開発し始めるより更に前から開発・執筆している、[「失敗しながら学ぶHaskell入門」](https://github.com/haskell-jp/makeMistakesToLearnHaskell/)をウェブアプリケーションとして公開する際のフレームワークとしても使おうという考えもありました。「失敗しながら学ぶHaskell入門」はタイトルの通りHaskell入門者のためのコンテンツです。そのため、Haskellを学習したばかりの人でも簡単に修正できるフレームワークにしたかったのです。 + +# できたもの + +ソースコードはこちらにあります: + +[igrep/wai-sample: Prototype of a new web application framework based on WAI.](https://github.com/igrep/wai-sample) + +YouTubeで配信する前から行っていた(私の前職である)IIJの社内勉強会中の開発と、全128回のYouTubeでのライブコーディングを経て(一部配信終了後に手を入れたこともありましたが)、次のような構文でウェブアプリケーションを記述できるようにしました: + +```haskell +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TypeApplications #-} + +import WaiSample + +sampleRoutes :: [Handler] +sampleRoutes = + [ -- ... 中略 ... + + -- (1) 最も単純な例 + , get @(PlainText, T.Text) "aboutUs" (path "about/us") (\_ -> return "About IIJ") + + -- (2) ステータスコードを指定した例 + , get @(WithStatus Status503 PlainText, T.Text) "maintenance" (path "maintenance") + (\_ -> return "Sorry, we are under maintenance") + + -- ... 中略 ... + + -- (3) パスをパースして含まれる整数を取得する例 + , get @(PlainText, T.Text) + "customerTransaction" + ( (,) <$> (path "customer/" *> decimalPiece) + <*> (path "/transaction/" *> paramPiece) + ) + (\(cId, transactionName) -> + return $ "Customer " <> T.pack (show cId) <> " Transaction " <> transactionName + ) + + -- ... 中略 ... + ] +``` + +※完全なサンプルコードは[WaiSample/Sample.hs](https://github.com/igrep/wai-sample/blob/b4ddb75a28b927b76ac7c4c182bad6812769ed01/src/WaiSample/Sample.hs)をご覧ください。上記はその一部に説明用のコメントを加えています。 + +上記のサンプルコードにおける`sampleRoutes`が、Web APIの仕様を定めている部分です: + +```haskell +sampleRoutes :: [Handler] +``` + +`Handler`という型の値のリストで、それぞれの`Handler`には、Web APIのエンドポイントを表すのに必要な情報が全て含まれています。wai-sampleでは、この`Handler`のリストを解釈してWAIベースのサーバーアプリケーションを実行したり、Template Haskellを通じてクライアントコードを生成したり、はたまたサーバーアプリケーションのドキュメントを生成したりすることができるようになっています。 + +## (1) 最も単純な例 + +```haskell +get @(PlainText, T.Text) "aboutUs" (path "about/us") (\_ -> return "About IIJ") +``` + +先程のサンプルコードから抜粋した最も単純な例↑では、`get`関数を使ってエンドポイントを定義しています。`get`関数は名前のとおりHTTPのGETメソッドに対応するエンドポイントを定義します。`TypeApplications`言語拡張を使って指定している`(PlainText, T.Text)`という型が、このエンドポイントが返すレスポンスの型を表しています。ここでは、`get`に渡す最後の引数に当たる関数([`Responder`](https://github.com/igrep/wai-sample/blob/b4ddb75a28b927b76ac7c4c182bad6812769ed01/src/WaiSample/Types.hs#L104)と呼びます。詳細は後ほど)がレスポンスボディーとして返す型をお馴染みの`Text`型として指定しつつ、サーバーやクライアントが処理する際はMIMEタイプを`text/plain`として扱うように指定しています。 + +`get`関数の(値の)第1引数では、エンドポイントの名前を指定しています。この名前は、後述するクライアントコードを生成する機能において、関数名の一部として使われます。 + +`get`関数の第2引数は、エンドポイントのパスの仕様を表す[`Route`型](https://github.com/igrep/wai-sample/blob/b4ddb75a28b927b76ac7c4c182bad6812769ed01/src/WaiSample/Types.hs#L56-L65)の値です。この例では、`path`関数を使って`"about/us"`という単純な文字列を指定しています。結果、このエンドポイントのパスは`/about/us`となります[^leading-slash])。 + +[^leading-slash]: 先頭のスラッシュにご注意ください。wai-sampleが`Route`型の値を処理する際は、先頭のスラッシュは付けない前提としています。 + +`get`関数の最後の引数が、このエンドポイントがHTTPリクエストを受け取った際に実行する関数、[`Responder`](https://github.com/igrep/wai-sample/blob/b4ddb75a28b927b76ac7c4c182bad6812769ed01/src/WaiSample/Types.hs#L104)です。ここでは、単純にレスポンスボディーとして文字列を返すだけの関数を指定しています。 + +## (2) ステータスコードを指定した例 + +```haskell +get @(WithStatus Status503 PlainText, T.Text) "maintenance" (path "maintenance") + (\_ -> return "Sorry, we are under maintenance") +``` + +デフォルトでは、`get`関数で定義したエンドポイントはやっぱりステータスコード200(OK)を返します。この挙動を変えるには、先程指定したレスポンスの型のうち、MIMEタイプを指定していた箇所を`WithStatus`型でラップしましょう。型引数で指定しているタプルの1つ目の要素は、このようにHTTPのレスポンスに関する仕様をHaskellの型で指定するパラメーターとなっています。 + +この例では、`Status503`という型を指定しているため、HTTPステータスコード503(Service Unavailable)を返すエンドポイントを定義しています。 + +## (3) パスの中に含まれる整数を処理する例 + +よくあるWebアプリケーションフレームワークでは、パスの一部に含まれる整数など文字列以外の型の値を取得するための仕組みが用意されています。 + +Haskellにおいて、文字列から特定の型の値を取り出す...といえばそう、パーサーコンビネーターですね。wai-sampleでは、サーバーが受け取ったパスをパーサーコンビネーターでパースするようになっています。従って下記の例では、`/customer/123/transaction/abc`というパスを受け取った場合、`123`と`"abc"`をタプルに詰め込んで`Responder`に渡すパスのパーサーを定義しています: + +```haskell +get @(PlainText, T.Text) + "customerTransaction" + ( (,) <$> (path "customer/" *> decimalPiece) + <*> (path "/transaction/" *> paramPiece) + ) + (\(cId, transactionName) -> + return $ "Customer " <> T.pack (show cId) <> " Transaction " <> transactionName + ) +``` + +実際のところここまでの話は`Route`型の値をサーバーアプリケーションが解釈した場合の挙動です。`Route`型はパスの仕様を定義する`Applicative`な内部DSLとなっています。これによって、サーバーアプリケーションだけでなくクライアントのコード生成機能やドキュメントの生成など、様々な応用ができるようになっています。詳しくは後述しますが、例えばクライアントのコード生成機能が`Route`型の値を解釈すると、`decimalPiece`や`paramPiece`などの値は生成した関数の引数を一つずつ追加します。 + +## Content-Typeを複数指定する + +Ruby on Railsの`respond_to`メソッドなどで実現できるように、一つのエンドポイントで一つの種類のレスポンスボディーを、複数のContent-Typeで返す、といった機能は昨今のWebアプリケーションフレームワークではごく一般的な機能でしょう。wai-sampleの場合、例えば次のようにして、`Customer`という型の値をJSONや`application/x-www-form-urlencoded`な文字列として返すエンドポイントを定義できます: + +```haskell +sampleRoutes = + [ -- ... 中略 ... + , get @(ContentTypes '[Json, FormUrlEncoded], Customer) + -- ... 中略 ... + ] +``` + +これまでの例では`get`の型引数においてMIMEタイプを表す箇所に一つの型のみ(`PlainText`型)を指定していましたが、ここでは代わりに`ContentTypes`という型を使用しています。`ContentTypes`型コンストラクターに、MIMEタイプを表す型の型レベルリストを渡せば、レスポンスボディーを表す一つの型に対して、複数のMIMEタイプを指定できるようになります。 + +なお、`Json`や`FormUrlEncoded`と一緒に指定した`Customer`型は、当然[`ToJSON`](https://hackage.haskell.org/package/aeson/docs/Data-Aeson-Types.html#t:ToJSON)・[`FromJSON`](https://hackage.haskell.org/package/aeson/docs/Data-Aeson-Types.html#t:FromJSON)や[`ToForm`](https://hackage.haskell.org/package/http-api-data/docs/Web-FormUrlEncoded.html#t:ToForm)・[`FromForm`](https://hackage.haskell.org/package/http-api-data/docs/Web-FormUrlEncoded.html#t:FromForm)といった型クラスのインスタンスである必要があります[^http-api-data]。レスポンスボディーとして指定した型が、同時に指定したMIMEタイプに対応する形式に変換できることを、保証できるようになっているのです。 + +[^http-api-data]: 諸般の事情で、wai-sampleでは[`http-api-data`パッケージをフォーク](https://github.com/igrep/http-api-data/tree/151de32409960354de3a3f786f20bc4a496d2b65)して使っています。そのため、`ToForm`型クラスなどの仕様がHackageにあるものと異なっています。最終的にwai-sampleを公開する際、フォークしたhttp-api-dataを新しいパッケージとして同時に公開する予定でした。 + +## サーバーアプリケーションとしての使い方 + +ここまでで定義した`Handler`型の値、すなわちWeb APIのエンドポイントの仕様に基づいてサーバーアプリケーションを実行するには、次のように書きます: + +```haskell +import Network.Wai (Application) +import Network.Wai.Handler.Warp (runEnv) + +import WaiSample.Sample (sampleRoutes) +import WaiSample.Server (handles) + + +sampleApp :: Application +sampleApp = handles sampleRoutes + + +runSampleApp :: IO () +runSampleApp = runEnv 8020 sampleApp +``` + +ℹ️[こちら](https://github.com/igrep/wai-sample/blob/b4ddb75a28b927b76ac7c4c182bad6812769ed01/src/WaiSample/Server/Sample.hs)にあるコードと同じ内容です。 + +`get`関数などで作った`Handler`型のリストを`handles`関数に渡すと、WAIの[`Application`](https://hackage.haskell.org/package/wai-3.2.4/docs/Network-Wai.html#t:Application)型の値が出来上がります。`Application`型はWAIにおけるサーバーアプリケーションを表す型で、ServantやYesodなど他の多くのHaskell製フレームワークでも、最終的にこの`Application`型の値を作るよう設計されています。上記の例は`Application`型の値をWarpというウェブサーバーで動かす場合のコードです。`Application`型の値をWarpの`runEnv`関数に渡すことで、指定したポート番号でアプリケーションを起動できます。 + +ここで起動したサーバーアプリケーションが、実際にエンドポイントへのリクエストを受け取った際実行する関数は、`get`関数などの最後の引数にあたる関数です。その関数は`SimpleResponder`という型シノニム[^simple]が設定されており、次のような定義となっています: + +[^simple]: 名前から察せられるとおり`Simple`じゃない普通の`Responder`型もありますが、ここでは割愛します。`Responder`型はクエリーパラメーターやリクエストヘッダーなど、パスに含めるパラメーター以外の情報を受け取るためのものです。`SimpleResponder`型のすぐ近くで定義されているので、興味があったらご覧ください。 + +```haskell +type SimpleResponder p resObj = p -> IO resObj +``` + +ℹ️[こちら](https://github.com/igrep/wai-sample/blob/b4ddb75a28b927b76ac7c4c182bad6812769ed01/src/WaiSample/Types.hs#L106)より + +型パラメーター`p`は、エンドポイントのパスに含まれるパラメーターを表す型です。 + +hoge + +## Template Haskellによる、クライアントの生成 + +https://github.com/igrep/wai-sample/blob/b4ddb75a28b927b76ac7c4c182bad6812769ed01/src/WaiSample/Client/Sample.hs + +```haskell +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeApplications #-} + +import WaiSample.Client +import WaiSample.Sample + + +$(declareClient "sample" sampleRoutes) +``` + +## ドキュメントの生成 + +詳しくは割愛 + +# 何故開発を止めるのか + +# 想定通りにできなかったもの + +## レスポンスの型 + +「できたもの」の節では割愛しましたが、 + +返しうるレスポンスに複数の種類があるとき + +間違い探しみたいな型 + +https://github.com/igrep/wai-sample/blob/b4ddb75a28b927b76ac7c4c182bad6812769ed01/src/WaiSample/Types/Response.hs#L51-L57 + +https://github.com/igrep/wai-sample/blob/b4ddb75a28b927b76ac7c4c182bad6812769ed01/src/WaiSample/Sample.hs#L175-L182 + +# 実装し切れなかったもの + +## よりよいドキュメント生成機能 + +## よりよいリクエストヘッダー・クエリーパラメーター + +# 類似のライブラリー・解決策 + + + +開発している途中で見つけた類似のライブラリー + +# 終わりに From 425abd418387722e41afff607dc95ac9a50b5100 Mon Sep 17 00:00:00 2001 From: YAMAMOTO Yuji Date: Sun, 16 Feb 2025 16:12:19 +0900 Subject: [PATCH 02/39] Explain each type variables of `SimpleResponder` --- preprocessed-site/posts/2024/wai-sample.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/preprocessed-site/posts/2024/wai-sample.md b/preprocessed-site/posts/2024/wai-sample.md index 00bee9c..483090a 100644 --- a/preprocessed-site/posts/2024/wai-sample.md +++ b/preprocessed-site/posts/2024/wai-sample.md @@ -177,7 +177,11 @@ type SimpleResponder p resObj = p -> IO resObj ℹ️[こちら](https://github.com/igrep/wai-sample/blob/b4ddb75a28b927b76ac7c4c182bad6812769ed01/src/WaiSample/Types.hs#L106)より -型パラメーター`p`は、エンドポイントのパスに含まれるパラメーターを表す型です。 +型パラメーター`p`は、エンドポイントのパスに含まれるパラメーターを表す型です。これまでの例で`get`関数に渡した`(path "about/us")`や`((,) <$> (path "customer/" *> decimalPiece) <*> (path "/transaction/" *> paramPiece))`という式で作られる、`Route`型の値を解釈した結果の型`p`です。 + +そして`resObj`は、エンドポイントが返すレスポンスボディーの型です。これまでの例でいうと、`get`関数の型引数で指定した`(PlainText, T.Text)`における`T.Text`型、`(ContentTypes '[Json, FormUrlEncoded], Customer)`における`Customer`型が該当します。 + +`runSampleApp`は各`Handler`型の値を解釈することで、 hoge From 0f075d25df3d971834bf59510e2ea46532a4ff81 Mon Sep 17 00:00:00 2001 From: YAMAMOTO Yuji Date: Sun, 23 Feb 2025 18:33:40 +0900 Subject: [PATCH 03/39] Complete the section of server apps --- preprocessed-site/posts/2024/wai-sample.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/preprocessed-site/posts/2024/wai-sample.md b/preprocessed-site/posts/2024/wai-sample.md index 483090a..b3cff29 100644 --- a/preprocessed-site/posts/2024/wai-sample.md +++ b/preprocessed-site/posts/2024/wai-sample.md @@ -181,12 +181,14 @@ type SimpleResponder p resObj = p -> IO resObj そして`resObj`は、エンドポイントが返すレスポンスボディーの型です。これまでの例でいうと、`get`関数の型引数で指定した`(PlainText, T.Text)`における`T.Text`型、`(ContentTypes '[Json, FormUrlEncoded], Customer)`における`Customer`型が該当します。 -`runSampleApp`は各`Handler`型の値を解釈することで、 +`runSampleApp`は各`Handler`型の値を解釈し、サーバーアプリケーションとして実行します。エンドポイントのパスの仕様(`(path "about/us")`など)をパーサーコンビネーターとして解釈し[^parser]、パースが成功した`Handler`が持つ`SimpleResponder`(`p -> IO resObj`)を呼び出します。そして`SimpleResponder`が返した`resObj`を、クライアントが要求したMIMEタイプに応じたレスポンスボディーに変換し、クライアントに返す、という流れで動くようになっています。 -hoge +[^parser]: パーサーコンビネーター以外のアプローチ、例えば基数木を使ってより多くのエンドポイントを高速に処理できるようにするのも可能でしょう。 ## Template Haskellによる、クライアントの生成 +hoge + https://github.com/igrep/wai-sample/blob/b4ddb75a28b927b76ac7c4c182bad6812769ed01/src/WaiSample/Client/Sample.hs ```haskell From 0f241700e75a2d766d2d40b795a8a8d9a931c318 Mon Sep 17 00:00:00 2001 From: YAMAMOTO Yuji Date: Sun, 2 Mar 2025 19:18:34 +0900 Subject: [PATCH 04/39] Introduction of client code generation in wai-sample --- preprocessed-site/posts/2024/wai-sample.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/preprocessed-site/posts/2024/wai-sample.md b/preprocessed-site/posts/2024/wai-sample.md index b3cff29..ba73a93 100644 --- a/preprocessed-site/posts/2024/wai-sample.md +++ b/preprocessed-site/posts/2024/wai-sample.md @@ -187,9 +187,7 @@ type SimpleResponder p resObj = p -> IO resObj ## Template Haskellによる、クライアントの生成 -hoge - -https://github.com/igrep/wai-sample/blob/b4ddb75a28b927b76ac7c4c182bad6812769ed01/src/WaiSample/Client/Sample.hs +サーバーアプリケーションの定義だけであれば、Haskell以外のものも含め、従来の多くのウェブアプリケーションフレームワークでも可能でしょう。しかしServantを始め、昨今におけるREST APIの開発を想定したWebアプリケーションフレームワークは、クライアントコードを生成する機能まで備えていることが多いです。wai-sampleはそうしたフレームワークを目指しているため、当然クライアントコードの生成もできるようになっています: ```haskell {-# LANGUAGE DataKinds #-} @@ -203,6 +201,10 @@ import WaiSample.Sample $(declareClient "sample" sampleRoutes) ``` +ℹ️[こちら](https://github.com/igrep/wai-sample/blob/b4ddb75a28b927b76ac7c4c182bad6812769ed01/src/WaiSample/Client/Sample.hs)からほぼそのままコピペしたコードです。 + +hoge + ## ドキュメントの生成 詳しくは割愛 From a5c188d4f3e3e320a43640e4b7b2de47464b0e5e Mon Sep 17 00:00:00 2001 From: YAMAMOTO Yuji Date: Sun, 9 Mar 2025 19:22:12 +0900 Subject: [PATCH 05/39] Begin describing how to generate client code --- preprocessed-site/posts/2024/wai-sample.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/preprocessed-site/posts/2024/wai-sample.md b/preprocessed-site/posts/2024/wai-sample.md index ba73a93..5d372e5 100644 --- a/preprocessed-site/posts/2024/wai-sample.md +++ b/preprocessed-site/posts/2024/wai-sample.md @@ -203,6 +203,8 @@ $(declareClient "sample" sampleRoutes) ℹ️[こちら](https://github.com/igrep/wai-sample/blob/b4ddb75a28b927b76ac7c4c182bad6812769ed01/src/WaiSample/Client/Sample.hs)からほぼそのままコピペしたコードです。 +上記の通り、クライアントコードの生成は`TemplateHaskell`を使って行います。`declareClient`という関数に、生成する関数の名前の接頭辞(prefix)とこれまで定義した`Handler`型のリスト(`sampleRoutes`)を渡すと、次のような型の関数の定義を生成します: + hoge ## ドキュメントの生成 From e16f446ba3023bf78d9def1c7a62adf3e95a5aa5 Mon Sep 17 00:00:00 2001 From: YAMAMOTO Yuji Date: Sun, 16 Mar 2025 18:09:03 +0900 Subject: [PATCH 06/39] (WIP) Describe functions generated by `declareClient` --- preprocessed-site/posts/2024/wai-sample.md | 40 +++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/preprocessed-site/posts/2024/wai-sample.md b/preprocessed-site/posts/2024/wai-sample.md index 5d372e5..16f8544 100644 --- a/preprocessed-site/posts/2024/wai-sample.md +++ b/preprocessed-site/posts/2024/wai-sample.md @@ -203,7 +203,45 @@ $(declareClient "sample" sampleRoutes) ℹ️[こちら](https://github.com/igrep/wai-sample/blob/b4ddb75a28b927b76ac7c4c182bad6812769ed01/src/WaiSample/Client/Sample.hs)からほぼそのままコピペしたコードです。 -上記の通り、クライアントコードの生成は`TemplateHaskell`を使って行います。`declareClient`という関数に、生成する関数の名前の接頭辞(prefix)とこれまで定義した`Handler`型のリスト(`sampleRoutes`)を渡すと、次のような型の関数の定義を生成します: +上記の通り、クライアントコードの生成は`TemplateHaskell`を使って行います。`declareClient`という関数に、生成する関数の名前の接頭辞(prefix)とこれまで定義した`Handler`型のリスト(`sampleRoutes`)を渡すと、次のような型の関数の定義を生成します[^ddump-splices]: + +```haskell +sampleAboutUs :: Backend -> IO Text +sampleMaintenance :: Backend -> IO Text +sampleCustomerTransaction :: Backend -> Integer -> Text -> IO Text +``` + +[^ddump-splices]: `ghc`コマンドの`-ddump-splices`オプションを使って、`declareClient`関数が生成したコードを貼り付けました。みなさんの手元で試す場合は`stack build --ghc-options=-ddump-splices`などと実行するのが簡単でしょう。 + +生成された関数は、`get`関数などの第1引数として渡した関数の名前に、`declareClient`の第1引数として渡した接頭辞が付いた名前で定義されます。 + +生成された関数の第1引数、`Backend`型は、クライアントがサーバーアプリケーションに実際にHTTPリクエストを送るための関数です。次のように定義されています: + +```haskell +import qualified Data.ByteString.Lazy.Char8 as BL +import qualified Network.HTTP.Client as HC + +type Backend = Method -> Url -> RequestHeaders -> IO (HC.Response BL.ByteString) +``` + +このバックエンドを、例えば`http-client`パッケージの関数を使って実装することで、生成された関数がサーバーアプリケーションにリクエストを送ることができます。以下は実際に`http-client`パッケージを使って実装したバックエンドの例です: + +```haskell +import qualified Network.HTTP.Client as HC +import qualified Data.ByteString.UTF8 as BS + +httpClientBackend :: String -> Manager -> Backend +httpClientBackend rootUrl manager method pathPieces rawReqHds = do + req0 <- parseUrlThrow . BS.toString $ method <> B.pack " " <> BS.fromString rootUrl <> pathPieces + let req = req0 { HC.requestHeaders = rawReqHds } + httpLbs (setRequestIgnoreStatus req) manager +``` + +ℹ️[こちら](https://github.com/igrep/wai-sample/blob/b4ddb75a28b927b76ac7c4c182bad6812769ed01/src/WaiSample/Client.hs#L225-L230)からほぼそのままコピペしたコードです。 + +他の引数 + +レスポンス hoge From 93dd30a30a01ba886ae1069feaacab98fc4e5b45 Mon Sep 17 00:00:00 2001 From: YAMAMOTO Yuji Date: Sun, 23 Mar 2025 18:01:46 +0900 Subject: [PATCH 07/39] Perhaps finish describing the client code --- preprocessed-site/posts/2024/wai-sample.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/preprocessed-site/posts/2024/wai-sample.md b/preprocessed-site/posts/2024/wai-sample.md index 16f8544..599fc60 100644 --- a/preprocessed-site/posts/2024/wai-sample.md +++ b/preprocessed-site/posts/2024/wai-sample.md @@ -239,9 +239,9 @@ httpClientBackend rootUrl manager method pathPieces rawReqHds = do ℹ️[こちら](https://github.com/igrep/wai-sample/blob/b4ddb75a28b927b76ac7c4c182bad6812769ed01/src/WaiSample/Client.hs#L225-L230)からほぼそのままコピペしたコードです。 -他の引数 +`Backend`型以外の引数は、パスパラメーターを始めとする、HTTPリクエストを組み立てるのに必要な情報です。`get`関数などで`Handler`型の値を定義する際に指定した`decimalPiece`や`paramPiece`を`declareClient`関数が回収して、生成した関数の引数に追加します。実際に生成した関数が受け取った引数は、もちろんパスの一部として当てはめるのに用います。 -レスポンス +生成した関数の戻り値は、サーバーからのレスポンスを表す型です。`get`関数の型引数として渡した`(PlainText, T.Text)`や`(ContentTypes '[Json, FormUrlEncoded], Customer)`などにおける`T.Text`や`Customer`がそれに当たります。クライアントの関数はサーバーからのレスポンスを、MIMEタイプを表す型などに従って、この型に変換してから返すよう実装されています。 hoge From 7475b7dcf37a9f6b5b87e8e35ec33dc7d72033f7 Mon Sep 17 00:00:00 2001 From: YAMAMOTO Yuji Date: Sun, 30 Mar 2025 15:03:15 +0900 Subject: [PATCH 08/39] Begin to explain the documentation feature --- preprocessed-site/posts/2024/wai-sample.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/preprocessed-site/posts/2024/wai-sample.md b/preprocessed-site/posts/2024/wai-sample.md index 599fc60..b645944 100644 --- a/preprocessed-site/posts/2024/wai-sample.md +++ b/preprocessed-site/posts/2024/wai-sample.md @@ -241,12 +241,12 @@ httpClientBackend rootUrl manager method pathPieces rawReqHds = do `Backend`型以外の引数は、パスパラメーターを始めとする、HTTPリクエストを組み立てるのに必要な情報です。`get`関数などで`Handler`型の値を定義する際に指定した`decimalPiece`や`paramPiece`を`declareClient`関数が回収して、生成した関数の引数に追加します。実際に生成した関数が受け取った引数は、もちろんパスの一部として当てはめるのに用います。 -生成した関数の戻り値は、サーバーからのレスポンスを表す型です。`get`関数の型引数として渡した`(PlainText, T.Text)`や`(ContentTypes '[Json, FormUrlEncoded], Customer)`などにおける`T.Text`や`Customer`がそれに当たります。クライアントの関数はサーバーからのレスポンスを、MIMEタイプを表す型などに従って、この型に変換してから返すよう実装されています。 - -hoge +生成した関数の戻り値は、サーバーからのレスポンスを表す型です。`get`関数の型引数として渡した`(PlainText, T.Text)`や`(ContentTypes '[Json, FormUrlEncoded], Customer)`などにおける`T.Text`や`Customer`がそれに当たります。クライアントの関数はサーバーからのレスポンスを、MIMEタイプを表す型などに従って、この型に変換してから返すよう実装されているのです。 ## ドキュメントの生成 +[ServantではOpenAPIに則ったドキュメントを生成するパッケージがある](https://hackage.haskell.org/package/servant-openapi3)ように、Haskellの構文で定義したREST APIの仕様から、APIのドキュメントを生成する機能があると便利でしょう。wai-sampleでも、`Handler`型のリストからAPIのドキュメントを生成する機能を + 詳しくは割愛 # 何故開発を止めるのか From 8ee72a25c991254f144f54a9be8f70ac78226802 Mon Sep 17 00:00:00 2001 From: YAMAMOTO Yuji Date: Sun, 6 Apr 2025 19:44:39 +0900 Subject: [PATCH 09/39] Add example of document generation by wai-sample --- preprocessed-site/posts/2024/wai-sample.md | 110 ++++++++++++++++++++- 1 file changed, 109 insertions(+), 1 deletion(-) diff --git a/preprocessed-site/posts/2024/wai-sample.md b/preprocessed-site/posts/2024/wai-sample.md index b645944..d362e85 100644 --- a/preprocessed-site/posts/2024/wai-sample.md +++ b/preprocessed-site/posts/2024/wai-sample.md @@ -245,7 +245,115 @@ httpClientBackend rootUrl manager method pathPieces rawReqHds = do ## ドキュメントの生成 -[ServantではOpenAPIに則ったドキュメントを生成するパッケージがある](https://hackage.haskell.org/package/servant-openapi3)ように、Haskellの構文で定義したREST APIの仕様から、APIのドキュメントを生成する機能があると便利でしょう。wai-sampleでも、`Handler`型のリストからAPIのドキュメントを生成する機能を +[ServantではOpenAPIに則ったドキュメントを生成するパッケージがある](https://hackage.haskell.org/package/servant-openapi3)ように、Haskellの構文で定義したREST APIの仕様から、APIのドキュメントを生成する機能があると便利でしょう。wai-sampleでも、`Handler`型のリストからAPIのドキュメントを生成する機能を実装しました --- 完成度が低く、とても実用に耐えるものではありませんが。 + +ともあれ、試しに使ってみましょう。これまで例として紹介した[`sampleRoutes`](https://github.com/igrep/wai-sample/blob/b4ddb75a28b927b76ac7c4c182bad6812769ed01/src/WaiSample/Sample.hs#L147)の各`Handler`に[`showHandlerSpec`](https://github.com/igrep/wai-sample/blob/b4ddb75a28b927b76ac7c4c182bad6812769ed01/src/WaiSample.hs#L47)という関数を適用すると、次のようにhoge + +```haskell +> mapM_ (TIO.putStrLn . showHandlerSpec) sampleRoutes +index "GET" / + Request: + Query Params: (none) + Headers: (none) + Response: (PlainText,Text) + +maintenance "GET" /maintenance + Request: + Query Params: (none) + Headers: (none) + Response: ((WithStatus Status503 PlainText),Text) + +aboutUs "GET" /about/us + Request: + Query Params: (none) + Headers: (none) + Response: (PlainText,Text) + +aboutUsFinance "GET" /about/us/finance + Request: + Query Params: (none) + Headers: (none) + Response: (PlainText,Text) + +aboutFinance "GET" /about/finance + Request: + Query Params: (none) + Headers: (none) + Response: (PlainText,Text) + +aboutFinanceImpossible "GET" //about/finance/impossible + Request: + Query Params: (none) + Headers: (none) + Response: (PlainText,Text) + +customerId "GET" /customer/:param + Request: + Query Params: (none) + Headers: (none) + Response: ((ContentTypes (': * Json (': * FormUrlEncoded ('[] *)))),Customer) + +customerIdJson "GET" /customer/:param.json + Request: + Query Params: (none) + Headers: (optional (X-API-VERSION: Integer | X-API-REVISION: Integer)) + Response: Sum (': * (Json,Customer) (': * ((WithStatus Status503 Json),SampleError) ('[] *))) + +customerIdTxt "GET" /customer/:param.txt + Request: + Query Params: (none) + Headers: (none) + Response: Sum (': * (PlainText,Text) (': * (Response (WithStatus Status503 PlainText) Text) ('[] *))) + +customerTransaction "GET" /customer/:param/transaction/:param + Request: + Query Params: (none) + Headers: (none) + Response: (PlainText,Text) + +createProduct "POST" /products + Request: + Query Params: (none) + Headers: (none) + Response: (PlainText,Text) + +customerHeadered "GET" /customerHeadered + Request: + Query Params: (none) + Headers: (none) + Response: (Json,(Headered (': * (Header "X-RateLimit-Limit" Int) (': * (Header "X-RateLimit-Reset" UTCTime) ('[] *))) Customer)) + +customerIdTxtHeadered "GET" /customer/:param.txt-or-json + Request: + Query Params: (none) + Headers: (none) + Response: Sum (': * ((ContentTypes (': * PlainText (': * Json ('[] *)))),(Headered (': * (Header "X-RateLimit-Limit" Int) (': * (Header "X-RateLimit-Reset" UTCTime) ('[] *))) Text)) (': * (Response (Wit +hStatus Status503 (ContentTypes (': * Json (': * PlainText ('[] *))))) (Headered (': * (Header "X-ErrorId" Text) ('[] *)) Text)) ('[] *))) + +echoApiVersion "POST" /echoApiVersion/ + Request: + Query Params: (none) + Headers: (X-API-VERSION: Integer | X-API-REVISION: Integer) + Response: (Json,ApiVersion) + +getExampleRequestHeaders "GET" /exampleRequestHeaders/ + Request: + Query Params: (none) + Headers: (X-API-VERSION: Integer | X-API-REVISION: Integer) & X-API-KEY: Text + Response: (Json,ExampleRequestHeaders) + +echoApiVersionQ "POST" /echoApiVersionQ/ + Request: + Query Params: (apiVersion: Integer | apiRevision: Integer) + Headers: (none) + Response: (Json,QueryParamsApiVersion) + +getExampleQueryParams "GET" /exampleQueryParams/ + Request: + Query Params: (apiVersion: Integer | apiRevision: Integer) & apiKey: Text + Headers: (none) + Response: (Json,ExampleQueryParams) +``` 詳しくは割愛 From ef24c564710cceb988fea7b6af753f46fdba2ab4 Mon Sep 17 00:00:00 2001 From: YAMAMOTO Yuji Date: Sun, 13 Apr 2025 18:23:35 +0900 Subject: [PATCH 10/39] Omit the verbose part of example document generated by wai-sample --- preprocessed-site/posts/2024/wai-sample.md | 77 ++-------------------- 1 file changed, 4 insertions(+), 73 deletions(-) diff --git a/preprocessed-site/posts/2024/wai-sample.md b/preprocessed-site/posts/2024/wai-sample.md index d362e85..409e20c 100644 --- a/preprocessed-site/posts/2024/wai-sample.md +++ b/preprocessed-site/posts/2024/wai-sample.md @@ -247,10 +247,10 @@ httpClientBackend rootUrl manager method pathPieces rawReqHds = do [ServantではOpenAPIに則ったドキュメントを生成するパッケージがある](https://hackage.haskell.org/package/servant-openapi3)ように、Haskellの構文で定義したREST APIの仕様から、APIのドキュメントを生成する機能があると便利でしょう。wai-sampleでも、`Handler`型のリストからAPIのドキュメントを生成する機能を実装しました --- 完成度が低く、とても実用に耐えるものではありませんが。 -ともあれ、試しに使ってみましょう。これまで例として紹介した[`sampleRoutes`](https://github.com/igrep/wai-sample/blob/b4ddb75a28b927b76ac7c4c182bad6812769ed01/src/WaiSample/Sample.hs#L147)の各`Handler`に[`showHandlerSpec`](https://github.com/igrep/wai-sample/blob/b4ddb75a28b927b76ac7c4c182bad6812769ed01/src/WaiSample.hs#L47)という関数を適用すると、次のようにhoge +ともあれ、試しに使ってみましょう。これまで例として紹介した[`sampleRoutes`](https://github.com/igrep/wai-sample/blob/b4ddb75a28b927b76ac7c4c182bad6812769ed01/src/WaiSample/Sample.hs#L147)の各`Handler`に[`showHandlerSpec`](https://github.com/igrep/wai-sample/blob/b4ddb75a28b927b76ac7c4c182bad6812769ed01/src/WaiSample.hs#L47)という関数を適用すると、次のように各エンドポイントへのパスやリクエスト・レスポンスの情報を取得することが出来ます: ```haskell -> mapM_ (TIO.putStrLn . showHandlerSpec) sampleRoutes +> mapM_ (TIO.putStrLn . showHandlerSpec) sampleRoutes index "GET" / Request: Query Params: (none) @@ -269,41 +269,7 @@ aboutUs "GET" /about/us Headers: (none) Response: (PlainText,Text) -aboutUsFinance "GET" /about/us/finance - Request: - Query Params: (none) - Headers: (none) - Response: (PlainText,Text) - -aboutFinance "GET" /about/finance - Request: - Query Params: (none) - Headers: (none) - Response: (PlainText,Text) - -aboutFinanceImpossible "GET" //about/finance/impossible - Request: - Query Params: (none) - Headers: (none) - Response: (PlainText,Text) - -customerId "GET" /customer/:param - Request: - Query Params: (none) - Headers: (none) - Response: ((ContentTypes (': * Json (': * FormUrlEncoded ('[] *)))),Customer) - -customerIdJson "GET" /customer/:param.json - Request: - Query Params: (none) - Headers: (optional (X-API-VERSION: Integer | X-API-REVISION: Integer)) - Response: Sum (': * (Json,Customer) (': * ((WithStatus Status503 Json),SampleError) ('[] *))) - -customerIdTxt "GET" /customer/:param.txt - Request: - Query Params: (none) - Headers: (none) - Response: Sum (': * (PlainText,Text) (': * (Response (WithStatus Status503 PlainText) Text) ('[] *))) +-- ... 中略 ... customerTransaction "GET" /customer/:param/transaction/:param Request: @@ -317,42 +283,7 @@ createProduct "POST" /products Headers: (none) Response: (PlainText,Text) -customerHeadered "GET" /customerHeadered - Request: - Query Params: (none) - Headers: (none) - Response: (Json,(Headered (': * (Header "X-RateLimit-Limit" Int) (': * (Header "X-RateLimit-Reset" UTCTime) ('[] *))) Customer)) - -customerIdTxtHeadered "GET" /customer/:param.txt-or-json - Request: - Query Params: (none) - Headers: (none) - Response: Sum (': * ((ContentTypes (': * PlainText (': * Json ('[] *)))),(Headered (': * (Header "X-RateLimit-Limit" Int) (': * (Header "X-RateLimit-Reset" UTCTime) ('[] *))) Text)) (': * (Response (Wit -hStatus Status503 (ContentTypes (': * Json (': * PlainText ('[] *))))) (Headered (': * (Header "X-ErrorId" Text) ('[] *)) Text)) ('[] *))) - -echoApiVersion "POST" /echoApiVersion/ - Request: - Query Params: (none) - Headers: (X-API-VERSION: Integer | X-API-REVISION: Integer) - Response: (Json,ApiVersion) - -getExampleRequestHeaders "GET" /exampleRequestHeaders/ - Request: - Query Params: (none) - Headers: (X-API-VERSION: Integer | X-API-REVISION: Integer) & X-API-KEY: Text - Response: (Json,ExampleRequestHeaders) - -echoApiVersionQ "POST" /echoApiVersionQ/ - Request: - Query Params: (apiVersion: Integer | apiRevision: Integer) - Headers: (none) - Response: (Json,QueryParamsApiVersion) - -getExampleQueryParams "GET" /exampleQueryParams/ - Request: - Query Params: (apiVersion: Integer | apiRevision: Integer) & apiKey: Text - Headers: (none) - Response: (Json,ExampleQueryParams) +-- ... 以下略 ... ``` 詳しくは割愛 From c1bc0c5ea758126b4cce2a7227a52ac18d01311a Mon Sep 17 00:00:00 2001 From: YAMAMOTO Yuji Date: Sun, 20 Apr 2025 18:28:02 +0900 Subject: [PATCH 11/39] Complete description of wai-sample's documentation feature --- preprocessed-site/posts/2024/wai-sample.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/preprocessed-site/posts/2024/wai-sample.md b/preprocessed-site/posts/2024/wai-sample.md index 409e20c..e52763b 100644 --- a/preprocessed-site/posts/2024/wai-sample.md +++ b/preprocessed-site/posts/2024/wai-sample.md @@ -286,7 +286,7 @@ createProduct "POST" /products -- ... 以下略 ... ``` -詳しくは割愛 +...が、あまりに完成度が低いので、詳しくは解説しません。実際に上記のコード実行すると、`Response`の型などがとても人間に読めるような出力になっていないことが分かります。今どきのWeb APIフレームワークであればOpenAPIに則ったドキュメントを生成する機能が欲しいでしょうが、それもありません。この方向で拡張すれば実装できるとは思いますが、次の節で述べるとおり開発を止めることにしたので、ここまでとしておきます。 # 何故開発を止めるのか From 127e490ae0c960036a2ca46455c3277b08cd0dd3 Mon Sep 17 00:00:00 2001 From: YAMAMOTO Yuji Date: Mon, 5 May 2025 22:23:12 +0900 Subject: [PATCH 12/39] Make progress in wai-sample.md --- preprocessed-site/posts/{2024 => 2025}/wai-sample.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) rename preprocessed-site/posts/{2024 => 2025}/wai-sample.md (96%) diff --git a/preprocessed-site/posts/2024/wai-sample.md b/preprocessed-site/posts/2025/wai-sample.md similarity index 96% rename from preprocessed-site/posts/2024/wai-sample.md rename to preprocessed-site/posts/2025/wai-sample.md index e52763b..b3b71b7 100644 --- a/preprocessed-site/posts/2024/wai-sample.md +++ b/preprocessed-site/posts/2025/wai-sample.md @@ -5,12 +5,12 @@ headingBackgroundImage: ../img/background.png headingDivClass: post-heading author: YAMAMOTO Yuji postedBy: YAMAMOTO Yuji(@igrep) -date: November 27, 2024 +date: July 27, 2025 tags: ... --- -この記事では、「[Haskell製ウェブアプリケーションフレームワークを作る配信](https://www.youtube.com/playlist?list=PLRVf2pXOpAzJMFN810EWwGrH_qii7DKyn)」で配信していた、Haskell製ウェブアプリケーションフレームワークを作るプロジェクトについて振り返ります。Servantのような型安全なAPI定義を、(Servantのような)高度な型レベルプログラミングも、(Yesodのような)TemplateHaskellもなしに可能にするライブラリーを目指していましたが、開発を途中で止めることにしました。その振り返り --- とりわけ、そのゴールに基づいて実装するのが原理上不可能だと分かった機能などを中心にまとめます。 +この記事では、「[Haskell製ウェブアプリケーションフレームワークを作る配信](https://www.youtube.com/playlist?list=PLRVf2pXOpAzJMFN810EWwGrH_qii7DKyn)」で配信していた、Haskell製ウェブアプリケーションフレームワークを作るプロジェクトについて振り返ります。Servantのような型安全なAPI定義を、(Servantのような)高度な型レベルプログラミングも、(Yesodのような)TemplateHaskellもなしに可能にするライブラリーを目指していましたが、開発を途中で止めることにしました。その振り返り --- とりわけ、そのゴールに基づいて実装するのが困難だと分かった機能などを中心にまとめます。 # 動機 @@ -29,7 +29,7 @@ tags: # できたもの -ソースコードはこちらにあります: +ソースコードはこちら👇️にあります。名前は仮に「wai-sample」としました。 [igrep/wai-sample: Prototype of a new web application framework based on WAI.](https://github.com/igrep/wai-sample) @@ -290,6 +290,10 @@ createProduct "POST" /products # 何故開発を止めるのか +開発をやめる最も大きな理由は、冒頭でも触れたとおり、当初考えていたゴールを達成するのが難しいと判断したからです[^motive]。wai-sampleのゴールは、「Servantのような型安全なAPI定義を、(Servantのような)高度な型レベルプログラミングも、(Yesodのような)TemplateHaskellもなしに可能にするライブラリー」にすることでした。hoge + +[^motive]: もう1つは、大変申し訳ないですが、私自身のHaskellに対する情熱が落ち込んでしまった、という理由もあります😞。 + # 想定通りにできなかったもの ## レスポンスの型 @@ -304,6 +308,8 @@ https://github.com/igrep/wai-sample/blob/b4ddb75a28b927b76ac7c4c182bad6812769ed0 https://github.com/igrep/wai-sample/blob/b4ddb75a28b927b76ac7c4c182bad6812769ed01/src/WaiSample/Sample.hs#L175-L182 +## パスのパーサー: 実は`<$>`がすでに危ない + # 実装し切れなかったもの ## よりよいドキュメント生成機能 From 4dfd3c5f859897aa70fb138912d3b08e09b3e9b8 Mon Sep 17 00:00:00 2001 From: YAMAMOTO Yuji Date: Sun, 11 May 2025 19:16:28 +0900 Subject: [PATCH 13/39] Complete introduction of why I stop development --- preprocessed-site/posts/2025/wai-sample.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/preprocessed-site/posts/2025/wai-sample.md b/preprocessed-site/posts/2025/wai-sample.md index b3b71b7..56962c3 100644 --- a/preprocessed-site/posts/2025/wai-sample.md +++ b/preprocessed-site/posts/2025/wai-sample.md @@ -290,13 +290,13 @@ createProduct "POST" /products # 何故開発を止めるのか -開発をやめる最も大きな理由は、冒頭でも触れたとおり、当初考えていたゴールを達成するのが難しいと判断したからです[^motive]。wai-sampleのゴールは、「Servantのような型安全なAPI定義を、(Servantのような)高度な型レベルプログラミングも、(Yesodのような)TemplateHaskellもなしに可能にするライブラリー」にすることでした。hoge +開発をやめる最も大きな理由は、冒頭でも触れたとおり、当初考えていたゴールを達成するのが難しいと判断したからです[^motive]。wai-sampleのゴールは、「Servantのような型安全なAPI定義を、(Servantのような)高度な型レベルプログラミングも、(Yesodのような)TemplateHaskellもなしに可能にするライブラリー」にすることでした。ところが、後述の通りいくつかの機能においてそれが無理ではないか(少なくとも難しい)ということが発覚したのです。 [^motive]: もう1つは、大変申し訳ないですが、私自身のHaskellに対する情熱が落ち込んでしまった、という理由もあります😞。 -# 想定通りにできなかったもの +## 想定通りにできなかったもの: レスポンスの型 -## レスポンスの型 +hoge 「できたもの」の節では割愛しましたが、 From 1b55ee79e2d2e04a48d73a72368facc34a897a62 Mon Sep 17 00:00:00 2001 From: YAMAMOTO Yuji Date: Sun, 22 Jun 2025 18:21:14 +0900 Subject: [PATCH 14/39] Begin to explain the case when the server returns several kinds of responses --- preprocessed-site/posts/2025/wai-sample.md | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/preprocessed-site/posts/2025/wai-sample.md b/preprocessed-site/posts/2025/wai-sample.md index 56962c3..3e497cc 100644 --- a/preprocessed-site/posts/2025/wai-sample.md +++ b/preprocessed-site/posts/2025/wai-sample.md @@ -294,11 +294,24 @@ createProduct "POST" /products [^motive]: もう1つは、大変申し訳ないですが、私自身のHaskellに対する情熱が落ち込んでしまった、という理由もあります😞。 -## 想定通りにできなかったもの: レスポンスの型 +## 想定通りにできなかったもの: レスポンスに複数のパターンがあるとき + +「できたもの」の節では割愛しましたが、wai-sampleでは、サーバーが返すレスポンスに複数のケースがあるとき --- 例えば、一方ではステータスコード200 OKと共に取得できたリソースの情報を返しつつ、一方では403 Forbiddenと共にエラーメッセージを返す --- といった場合ですね。 hoge -「できたもの」の節では割愛しましたが、 +```haskell +get @(Sum '[(PlainText, T.Text), Response (WithStatus Status503 PlainText) T.Text]) + "customerIdTxt" + -- /customer/:id.txt + (path "customer/" *> decimalPiece <* path ".txt") + (\i -> + if i == 503 + then return . sumLift $ Response @(WithStatus Status503 PlainText) ("error" :: T.Text) + else return . sumLift $ "Customer " <> T.pack (show i)) +``` + +ℹ️[こちら](https://github.com/igrep/wai-sample/blob/b4ddb75a28b927b76ac7c4c182bad6812769ed01/src/WaiSample/Sample.hs#L175-L182)からほぼそのままコピペしたコードです。 返しうるレスポンスに複数の種類があるとき From e8f9bab56a235cbd3a74e41d8e9fce63165e427b Mon Sep 17 00:00:00 2001 From: YAMAMOTO Yuji Date: Sun, 29 Jun 2025 18:11:45 +0900 Subject: [PATCH 15/39] Describe the example response's cases --- preprocessed-site/posts/2025/wai-sample.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/preprocessed-site/posts/2025/wai-sample.md b/preprocessed-site/posts/2025/wai-sample.md index 3e497cc..ae2d481 100644 --- a/preprocessed-site/posts/2025/wai-sample.md +++ b/preprocessed-site/posts/2025/wai-sample.md @@ -296,9 +296,10 @@ createProduct "POST" /products ## 想定通りにできなかったもの: レスポンスに複数のパターンがあるとき -「できたもの」の節では割愛しましたが、wai-sampleでは、サーバーが返すレスポンスに複数のケースがあるとき --- 例えば、一方ではステータスコード200 OKと共に取得できたリソースの情報を返しつつ、一方では403 Forbiddenと共にエラーメッセージを返す --- といった場合ですね。 +「できたもの」の節では割愛しましたが、wai-sampleでは、サーバーが返すレスポンスに複数のケースがあるエンドポイント --- 例えば、一方ではステータスコード200 OKと共に取得できたリソースの情報を返しつつ、一方では403 Forbiddenと共にエラーメッセージを返す --- の実装もサポートしています。例えば次のように書けば、`/customer/:id.txt`というパスで、以下のいずれかのレスポンスを返すエンドポイントを定義することが出来ます: -hoge +- ステータスコードが(デフォルトの)`200 OK`で、`text/plain`なレスポンスボディー +- ステータスコードが`503 Service Unavailable`で、`text/plain`なレスポンスボディー ```haskell get @(Sum '[(PlainText, T.Text), Response (WithStatus Status503 PlainText) T.Text]) From cc20bdf91bbf420ce1139ce349351a8aeef62bb8 Mon Sep 17 00:00:00 2001 From: YAMAMOTO Yuji Date: Sun, 13 Jul 2025 18:11:40 +0900 Subject: [PATCH 16/39] Describe the parameters of the `Sum` type --- preprocessed-site/posts/2025/wai-sample.md | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/preprocessed-site/posts/2025/wai-sample.md b/preprocessed-site/posts/2025/wai-sample.md index ae2d481..83d2267 100644 --- a/preprocessed-site/posts/2025/wai-sample.md +++ b/preprocessed-site/posts/2025/wai-sample.md @@ -296,10 +296,7 @@ createProduct "POST" /products ## 想定通りにできなかったもの: レスポンスに複数のパターンがあるとき -「できたもの」の節では割愛しましたが、wai-sampleでは、サーバーが返すレスポンスに複数のケースがあるエンドポイント --- 例えば、一方ではステータスコード200 OKと共に取得できたリソースの情報を返しつつ、一方では403 Forbiddenと共にエラーメッセージを返す --- の実装もサポートしています。例えば次のように書けば、`/customer/:id.txt`というパスで、以下のいずれかのレスポンスを返すエンドポイントを定義することが出来ます: - -- ステータスコードが(デフォルトの)`200 OK`で、`text/plain`なレスポンスボディー -- ステータスコードが`503 Service Unavailable`で、`text/plain`なレスポンスボディー +「できたもの」の節では割愛しましたが、wai-sampleでは、サーバーが返すレスポンスに複数のケースがあるエンドポイント --- 例えば、一方ではステータスコード200 OKと共に取得できたリソースの情報を返しつつ、一方では403 Forbiddenと共にエラーメッセージを返す --- の実装もサポートしています。例えば次のように書けば、`/customer/:id.txt`というパスで複数の種類のレスポンスを返すエンドポイントを定義することが出来ます: ```haskell get @(Sum '[(PlainText, T.Text), Response (WithStatus Status503 PlainText) T.Text]) @@ -314,7 +311,18 @@ get @(Sum '[(PlainText, T.Text), Response (WithStatus Status503 PlainText) T.Tex ℹ️[こちら](https://github.com/igrep/wai-sample/blob/b4ddb75a28b927b76ac7c4c182bad6812769ed01/src/WaiSample/Sample.hs#L175-L182)からほぼそのままコピペしたコードです。 -返しうるレスポンスに複数の種類があるとき +`get`関数の型引数に、随分仰々しい型が現れました。`Sum`という型は、名前のとおり和型を作ります。型レベルリストの要素としてContent-Typeやステータスコードを表す型と、実際のレスポンスボディーの型を組み合わせたタプル(あるいは後述する`Response`型)を渡すことで、複数のケースを持つレスポンスの型を定義しています。上記の例における`Sum '[(PlainText, T.Text), Response (WithStatus Status503 PlainText) T.Text]`は、次の2つのケースを持つレスポンスの型を表しています: + +- ステータスコードが(デフォルトの)`200 OK`で、Content-Typeが`text/plain`、レスポンスボディーを表す型が`Text`型 +- ステータスコードが`503 Service Unavailable`で、Content-Typeが`text/plain`、レスポンスボディーを表す型が`Text`型 + +以上のように書くことで実装できるようにはしたのですが、これによって当初の目的である「高度な型レベルプログラミングなしに実装する」という目標から外れてしまいました。型レベルリストは「高度な型レベルプログラミング」に該当すると言って差し支えないでしょう。 + +なぜこのようなAPIになったのかというと、hoge + + +それから、型レベルリストを使ったこと以外においても、複雑で分かりづらい要因があります。 +タプル以外にも`Response`という型を 間違い探しみたいな型 From cac48a442591bce62e815189ee857cecf8d44973 Mon Sep 17 00:00:00 2001 From: YAMAMOTO Yuji Date: Sun, 20 Jul 2025 19:02:09 +0900 Subject: [PATCH 17/39] Explain why I had to adopt a type-level DSL --- preprocessed-site/posts/2025/wai-sample.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/preprocessed-site/posts/2025/wai-sample.md b/preprocessed-site/posts/2025/wai-sample.md index 83d2267..df3b2ea 100644 --- a/preprocessed-site/posts/2025/wai-sample.md +++ b/preprocessed-site/posts/2025/wai-sample.md @@ -318,7 +318,11 @@ get @(Sum '[(PlainText, T.Text), Response (WithStatus Status503 PlainText) T.Tex 以上のように書くことで実装できるようにはしたのですが、これによって当初の目的である「高度な型レベルプログラミングなしに実装する」という目標から外れてしまいました。型レベルリストは「高度な型レベルプログラミング」に該当すると言って差し支えないでしょう。 -なぜこのようなAPIになったのかというと、hoge +なぜこのようなAPIになったのかというと、Web APIに対する「入力」に当たる、パスのパース(や、今回は実装しませんでしたがリクエストボディーなどの処理も)などと、Web APIからの「出力」に当たるレスポンスの処理では、実行時に使える情報が大きく異なっていたからです。「入力」は値レベルでも(高度な型レベルプログラミングなしで)Free Applicativeを応用したDSLを使えば[^free-applicative]、サーバーアプリケーション・クライアントコード・ドキュメント、いずれにも実行時に解釈できるフレームワークにできた一方、「出力」の型は値レベルのDSLを書いても、サーバーアプリケーションを実行しない限りそれに整合しているかどうかが分からない、という原理的な問題が判明したからです。 + +[^free-applicative]: 今回は詳細を省きましたが`Free Applicative`を使ったDSLの実装は、[WaiSample.Typesモジュール](https://github.com/igrep/wai-sample/blob/b4ddb75a28b927b76ac7c4c182bad6812769ed01/src/WaiSample/Types.hs)をご覧ください。 + +従って、例えば、hoge それから、型レベルリストを使ったこと以外においても、複雑で分かりづらい要因があります。 From 6a33c94824829e76cdc5bed1746d0f0519d09455 Mon Sep 17 00:00:00 2001 From: YAMAMOTO Yuji Date: Sun, 27 Jul 2025 18:49:05 +0900 Subject: [PATCH 18/39] Example why value-level DSL doesn't work --- preprocessed-site/posts/2025/wai-sample.md | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/preprocessed-site/posts/2025/wai-sample.md b/preprocessed-site/posts/2025/wai-sample.md index df3b2ea..c87d66c 100644 --- a/preprocessed-site/posts/2025/wai-sample.md +++ b/preprocessed-site/posts/2025/wai-sample.md @@ -318,11 +318,22 @@ get @(Sum '[(PlainText, T.Text), Response (WithStatus Status503 PlainText) T.Tex 以上のように書くことで実装できるようにはしたのですが、これによって当初の目的である「高度な型レベルプログラミングなしに実装する」という目標から外れてしまいました。型レベルリストは「高度な型レベルプログラミング」に該当すると言って差し支えないでしょう。 -なぜこのようなAPIになったのかというと、Web APIに対する「入力」に当たる、パスのパース(や、今回は実装しませんでしたがリクエストボディーなどの処理も)などと、Web APIからの「出力」に当たるレスポンスの処理では、実行時に使える情報が大きく異なっていたからです。「入力」は値レベルでも(高度な型レベルプログラミングなしで)Free Applicativeを応用したDSLを使えば[^free-applicative]、サーバーアプリケーション・クライアントコード・ドキュメント、いずれにも実行時に解釈できるフレームワークにできた一方、「出力」の型は値レベルのDSLを書いても、サーバーアプリケーションを実行しない限りそれに整合しているかどうかが分からない、という原理的な問題が判明したからです。 +なぜこのようなAPIになったのかというと、Web APIに対する「入力」に当たる、パスのパース(や、今回は実装しませんでしたがリクエストボディーなどの処理も)などと、Web APIからの「出力」に当たるレスポンスの処理では、実行時に使える情報が大きく異なっていたからです。「入力」は値レベルでも(高度な型レベルプログラミングなしで)Free Applicativeを応用したDSLを使えば[^free-applicative]、サーバーアプリケーション・クライアントコード・ドキュメント、いずれにも実行時に解釈できるフレームワークにできた一方、レスポンスボディーなど「出力」の型は値レベルのDSLを書いても、サーバーアプリケーションを実行しない限りそれに整合しているかどうかが分からない、という原理的な問題が判明したからです。 [^free-applicative]: 今回は詳細を省きましたが`Free Applicative`を使ったDSLの実装は、[WaiSample.Typesモジュール](https://github.com/igrep/wai-sample/blob/b4ddb75a28b927b76ac7c4c182bad6812769ed01/src/WaiSample/Types.hs)をご覧ください。 -従って、例えば、hoge +例えば、レスポンスボディーの仕様を次のような内部DSLで定義できるようにしたとします: + +```haskell +get [(plainText, text), ((withStatus status503 plainText), text)] + -- ... +``` + +型レベルプログラミングバージョンでは型引数に渡していた情報を、ほぼそのまま値レベルに落とし込んだものです。しかしこのように書いたとしても、サーバーアプリケーションを起動して、実際にクライアントからリクエストを受け取り、それに対して`get`に渡した関数(`Responder`)がレスポンスの元となる値を返すまで、レスポンスボディーの型が正しいかどうか、検証できないのです。「レスポンスの元となる値」の型はライブラリーのユーザー自身が`Responder`で返す値の型ですし、実行時以前にコンパイル時に保証できていて欲しいものです[^launch-time]。これが、値レベルのDSLを採用した場合の限界です。 + +[^launch-time]: と、ここまで書いて気付いたのですが、hoge起動時 + +hoge それから、型レベルリストを使ったこと以外においても、複雑で分かりづらい要因があります。 From c5eeb34c396283fbc19746d534d1fc3febc93255 Mon Sep 17 00:00:00 2001 From: YAMAMOTO Yuji Date: Sun, 3 Aug 2025 18:36:08 +0900 Subject: [PATCH 19/39] Complete the note about the runtime type checking --- preprocessed-site/posts/2025/wai-sample.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/preprocessed-site/posts/2025/wai-sample.md b/preprocessed-site/posts/2025/wai-sample.md index c87d66c..d83ab83 100644 --- a/preprocessed-site/posts/2025/wai-sample.md +++ b/preprocessed-site/posts/2025/wai-sample.md @@ -331,9 +331,7 @@ get [(plainText, text), ((withStatus status503 plainText), text)] 型レベルプログラミングバージョンでは型引数に渡していた情報を、ほぼそのまま値レベルに落とし込んだものです。しかしこのように書いたとしても、サーバーアプリケーションを起動して、実際にクライアントからリクエストを受け取り、それに対して`get`に渡した関数(`Responder`)がレスポンスの元となる値を返すまで、レスポンスボディーの型が正しいかどうか、検証できないのです。「レスポンスの元となる値」の型はライブラリーのユーザー自身が`Responder`で返す値の型ですし、実行時以前にコンパイル時に保証できていて欲しいものです[^launch-time]。これが、値レベルのDSLを採用した場合の限界です。 -[^launch-time]: と、ここまで書いて気付いたのですが、hoge起動時 - -hoge +[^launch-time]: と、ここまで書いて気付いたのですが、`Typeable`型クラスを利用して、サーバーアプリケーション起動時に各`Responder`が返す型が正しいかチェックする、という手法もありだったかもしれません(あるいはそれだけのためにTemplate Haskellをどこかに挟む?)。まあ、型チェックである以上コンパイル時にチェックする方が望ましいでしょうし、私自身のやる気ももう失われてしまったので特に取り組みはしませんが...。 それから、型レベルリストを使ったこと以外においても、複雑で分かりづらい要因があります。 From 426c652dfde8a41e071bfe6b955d14257028ff2b Mon Sep 17 00:00:00 2001 From: YAMAMOTO Yuji Date: Sun, 10 Aug 2025 17:38:04 +0900 Subject: [PATCH 20/39] Complete the section about when the server returns multiple patterns of responses --- preprocessed-site/posts/2025/wai-sample.md | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/preprocessed-site/posts/2025/wai-sample.md b/preprocessed-site/posts/2025/wai-sample.md index d83ab83..24af000 100644 --- a/preprocessed-site/posts/2025/wai-sample.md +++ b/preprocessed-site/posts/2025/wai-sample.md @@ -333,15 +333,23 @@ get [(plainText, text), ((withStatus status503 plainText), text)] [^launch-time]: と、ここまで書いて気付いたのですが、`Typeable`型クラスを利用して、サーバーアプリケーション起動時に各`Responder`が返す型が正しいかチェックする、という手法もありだったかもしれません(あるいはそれだけのためにTemplate Haskellをどこかに挟む?)。まあ、型チェックである以上コンパイル時にチェックする方が望ましいでしょうし、私自身のやる気ももう失われてしまったので特に取り組みはしませんが...。 +それから、型レベルリストを使ったこと以外においても、複雑で分かりづらい要因があります。先程から少し触れているとおり、Content-Typeやステータスコードとレスポンスボディーの型を組み合わせを表すのに、タプル以外にも`Response`という型を用いています。`Response`型とタプル型はいずれも[`ResponseSpec`](https://github.com/igrep/wai-sample/blob/b4ddb75a28b927b76ac7c4c182bad6812769ed01/src/WaiSample/Types/Response.hs#L47-L49)(下記に転載)という型クラスのインスタンスとなることで、「Content-Typeやステータスコード」を表す型(`ResponseType`)と`Responder`がレスポンスボディーとして返す型(`ResponseObject`)を宣言することが出来ます: -それから、型レベルリストを使ったこと以外においても、複雑で分かりづらい要因があります。 -タプル以外にも`Response`という型を +```hs +class ResponseSpec resSpec where + type ResponseType resSpec + type ResponseObject resSpec -間違い探しみたいな型 +instance ResponseSpec (resTyp, resObj) where + type ResponseType (resTyp, resObj) = resTyp + type ResponseObject (resTyp, resObj) = resObj -https://github.com/igrep/wai-sample/blob/b4ddb75a28b927b76ac7c4c182bad6812769ed01/src/WaiSample/Types/Response.hs#L51-L57 +instance ResponseSpec (Response resTyp resObj) where + type ResponseType (Response resTyp resObj) = resTyp + type ResponseObject (Response resTyp resObj) = Response resTyp resObj +``` -https://github.com/igrep/wai-sample/blob/b4ddb75a28b927b76ac7c4c182bad6812769ed01/src/WaiSample/Sample.hs#L175-L182 +`ResponseSpec (resTyp, resObj)`と`ResponseSpec (Response resTyp resObj)`の2つのインスタンスの違い、分かるでしょうか?まるで間違い探しですよね...😥。タプル型も`Response`型も`get`などに渡す型レベルリストでの役割はほぼ同じで、最初はタプルだけをとることにしていたのですが、やむを得ない理由があって`Response`を別途設けることにしました(詳しい理由は面倒なので解説しません!これまでに出てきたコードだけで推測できるはずですし考えてみてください!)。こうした分かりづらい部分が出来てしまったのも、失敗の1つです。 ## パスのパーサー: 実は`<$>`がすでに危ない From a1e149178d6befbd1363caa0369629329539e305 Mon Sep 17 00:00:00 2001 From: YAMAMOTO Yuji Date: Sun, 17 Aug 2025 18:44:27 +0900 Subject: [PATCH 21/39] Begin to explain the type-unsafety of `<$>` --- preprocessed-site/posts/2025/wai-sample.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/preprocessed-site/posts/2025/wai-sample.md b/preprocessed-site/posts/2025/wai-sample.md index 24af000..47c4059 100644 --- a/preprocessed-site/posts/2025/wai-sample.md +++ b/preprocessed-site/posts/2025/wai-sample.md @@ -349,10 +349,22 @@ instance ResponseSpec (Response resTyp resObj) where type ResponseObject (Response resTyp resObj) = Response resTyp resObj ``` -`ResponseSpec (resTyp, resObj)`と`ResponseSpec (Response resTyp resObj)`の2つのインスタンスの違い、分かるでしょうか?まるで間違い探しですよね...😥。タプル型も`Response`型も`get`などに渡す型レベルリストでの役割はほぼ同じで、最初はタプルだけをとることにしていたのですが、やむを得ない理由があって`Response`を別途設けることにしました(詳しい理由は面倒なので解説しません!これまでに出てきたコードだけで推測できるはずですし考えてみてください!)。こうした分かりづらい部分が出来てしまったのも、失敗の1つです。 +`ResponseSpec (resTyp, resObj)`と`ResponseSpec (Response resTyp resObj)`の2つのインスタンスの違い、分かるでしょうか?まるで間違い探しですよね...😥。タプル型も`Response`型も`get`などに渡す型レベルリストでの役割はほぼ同じで、最初はタプルだけをとることにしていたのですが、やむを得ない理由があって`Response`を別途設けることにしました[^reason]。こうした分かりづらい部分が出来てしまったのも、失敗の1つです。 + +[^reason]: 詳しい理由は面倒なので解説しません!これまでに出てきたコードだけで推測できるはずですし考えてみてください! ## パスのパーサー: 実は`<$>`がすでに危ない +パスのパーサーを値レベルの、`Applicative`な内部DSLとして実装した結果、Servantと比べて型安全性を損なってしまうという問題があることも、作ってから気付きました。例えば、次のように`<$>`に渡す関数としてコンストラクターでない、普通の関数を渡した場合です: + +```haskell +hoge +``` + +hoge + +まあ、実は同じ問題が同じように`Applicative`ベースの内部DSLを使った他のライブラリーにもあるでしょうから、敢えて気にしない、という手もあるのかも知れませんが。ちなみに、似たような問題を解決するため[relational-record](https://hackage.haskell.org/package/relational-record)というパッケージでは`Functor`や`Applicative`は使わず、[product-isomorphic](https://hackage.haskell.org/package/product-isomorphic)というパッケージにおいて、言わば「コンストラクターだけが適用できる`Functor`・`Applicative`」とも言うべき専用の型クラスを作ることで解決していました。wai-sampleもこれを使えないかと企みましたが、どうもうまく適用できなかったため諦めました。 + # 実装し切れなかったもの ## よりよいドキュメント生成機能 From ae96bb2442cf38a548300223ddab87c457147038 Mon Sep 17 00:00:00 2001 From: YAMAMOTO Yuji Date: Sun, 24 Aug 2025 17:57:05 +0900 Subject: [PATCH 22/39] Finish explaining the type-unsafety of `<$>` --- preprocessed-site/posts/2025/wai-sample.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/preprocessed-site/posts/2025/wai-sample.md b/preprocessed-site/posts/2025/wai-sample.md index 47c4059..3a6356a 100644 --- a/preprocessed-site/posts/2025/wai-sample.md +++ b/preprocessed-site/posts/2025/wai-sample.md @@ -358,10 +358,10 @@ instance ResponseSpec (Response resTyp resObj) where パスのパーサーを値レベルの、`Applicative`な内部DSLとして実装した結果、Servantと比べて型安全性を損なってしまうという問題があることも、作ってから気付きました。例えば、次のように`<$>`に渡す関数としてコンストラクターでない、普通の関数を渡した場合です: ```haskell -hoge +path "integers/" *> (show <$> decimalPiece) ``` -hoge +[`decimalPiece`](https://github.com/igrep/wai-sample/blob/b4ddb75a28b927b76ac7c4c182bad6812769ed01/src/WaiSample/Routes.hs#L22)は`Route Integer`という型で、それに`show <$>`を適用した結果は`Route String`となります。`Route String`は、パスの一部として文字列を受け取ることを表す型ですから、上記の式は`integers/<任意の文字列>`というパスを表すことになります。ところが!実際にサーバーアプリケーションがパスをパースするのに使っているのは`decimalPiece`なので、整数でなければなりません。このように`<$>`を使うだけで、`Route String`という型が表すパスのパーサーと、実際にパースできるパスの仕様が食い違ってしまうことがあります。`Applicative`(厳密に言えば`Functor`の機能ですが)を使ったDSLである以上、こうしたことが防げないのです。 まあ、実は同じ問題が同じように`Applicative`ベースの内部DSLを使った他のライブラリーにもあるでしょうから、敢えて気にしない、という手もあるのかも知れませんが。ちなみに、似たような問題を解決するため[relational-record](https://hackage.haskell.org/package/relational-record)というパッケージでは`Functor`や`Applicative`は使わず、[product-isomorphic](https://hackage.haskell.org/package/product-isomorphic)というパッケージにおいて、言わば「コンストラクターだけが適用できる`Functor`・`Applicative`」とも言うべき専用の型クラスを作ることで解決していました。wai-sampleもこれを使えないかと企みましたが、どうもうまく適用できなかったため諦めました。 From 25da5c61babcf7a9e53e89d50bf3e8e464061d7a Mon Sep 17 00:00:00 2001 From: YAMAMOTO Yuji Date: Sun, 31 Aug 2025 19:08:06 +0900 Subject: [PATCH 23/39] Complete the section about what I left unimplemented --- preprocessed-site/posts/2025/wai-sample.md | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/preprocessed-site/posts/2025/wai-sample.md b/preprocessed-site/posts/2025/wai-sample.md index 3a6356a..b1ea10a 100644 --- a/preprocessed-site/posts/2025/wai-sample.md +++ b/preprocessed-site/posts/2025/wai-sample.md @@ -367,9 +367,21 @@ path "integers/" *> (show <$> decimalPiece) # 実装し切れなかったもの -## よりよいドキュメント生成機能 - -## よりよいリクエストヘッダー・クエリーパラメーター +ウェブアプリケーションフレームワークとして実装すべき機能のうち、実装し切れなかったものは当然たくさんあります。例えば以下のような機能でしょう: + +- HTTPリクエストに関わるもの: + - リクエストヘッダーの処理 + - クエリーパラメーターの処理 + - この辺りは、リクエストボディーの処理と似たような要領で実装できるはず +- HTTPレスポンスに関わるもの: + - 動的なHTMLの配信 + - ファイルシステムにあるファイルの配信 + - REST APIに特化したフレームワークであればこれは不要でしょうが +- 両方に関わるもの: + - Cookieの読み書き +- ドキュメント生成に関わるもの: + - OpenAPIに準拠したドキュメント生成 +- などなど! # 類似のライブラリー・解決策 From 75c363ecd7cfb112f3333dd2bac0a863f0408d34 Mon Sep 17 00:00:00 2001 From: YAMAMOTO Yuji Date: Sun, 7 Sep 2025 21:32:50 +0900 Subject: [PATCH 24/39] Add parentheses --- preprocessed-site/posts/2025/wai-sample.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/preprocessed-site/posts/2025/wai-sample.md b/preprocessed-site/posts/2025/wai-sample.md index b1ea10a..07ceffb 100644 --- a/preprocessed-site/posts/2025/wai-sample.md +++ b/preprocessed-site/posts/2025/wai-sample.md @@ -372,11 +372,11 @@ path "integers/" *> (show <$> decimalPiece) - HTTPリクエストに関わるもの: - リクエストヘッダーの処理 - クエリーパラメーターの処理 - - この辺りは、リクエストボディーの処理と似たような要領で実装できるはず + - (この辺りは、リクエストボディーの処理と似たような要領で実装できるはず) - HTTPレスポンスに関わるもの: - 動的なHTMLの配信 - ファイルシステムにあるファイルの配信 - - REST APIに特化したフレームワークであればこれは不要でしょうが + - (REST APIに特化したフレームワークであればこれは不要でしょうが) - 両方に関わるもの: - Cookieの読み書き - ドキュメント生成に関わるもの: From 0bfcf698523c97053150683f79d485eb7022e821 Mon Sep 17 00:00:00 2001 From: YAMAMOTO Yuji Date: Sun, 14 Sep 2025 18:01:46 +0900 Subject: [PATCH 25/39] Introduce Okapi --- preprocessed-site/posts/2025/wai-sample.md | 24 +++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/preprocessed-site/posts/2025/wai-sample.md b/preprocessed-site/posts/2025/wai-sample.md index 07ceffb..af00b7f 100644 --- a/preprocessed-site/posts/2025/wai-sample.md +++ b/preprocessed-site/posts/2025/wai-sample.md @@ -385,8 +385,30 @@ path "integers/" *> (show <$> decimalPiece) # 類似のライブラリー・解決策 +手が遅いもので、私が最初にwai-sampleのリポジトリーに対して行った[最初のコミット](https://github.com/igrep/wai-sample/commit/37f49dfe86af7482b09ab82b2282c5b9bf1cd73d)から、既に約5年の歳月が過ぎました[^last-commit]。当時は私の前職、IIJにおける社内勉強会のネタとして始めたのが懐かしいです。私が知る限り、当時はwai-sampleのように「値レベルのプログラミングで」「Servantのように1つの定義からクライアントやドキュメントの生成も出来る」ことを目指したライブラリーはなかったように思います。しかし実際のところ、執筆時点で次のライブラリーが類似の機能を実装しています。これらのライブラリーがいつ開発を始めたのかは分かりませんが、やはり私がwai-sampleを作り始めた時点で同じような問題意識を持った人はいたのでしょう。 +[^last-commit]: [実装に対する最後の修正](https://github.com/igrep/wai-sample/commit/b2647de2a1a4c7ec8c799ec07972c3d9df6fcb55)からも既に約1年が過ぎました。記録を作るのも遅い...😥 + +## [Okapi](https://okapi.wiki/) + +[Okapi](https://okapi.wiki/)にある[Endpoint](https://okapi.wiki/#endpoint)という機能は「An Endpoint is an executable specification representing a single Operation that can be taken against your API.」と謳っているとおり、APIの仕様を表現する内部DSLを提供します。下記の`Endpoint`という型 --- wai-sampleでいう`Handler`に相当するようです --- に、「Script」と呼ばれる値レベルDSLを設定して使うようです: + +```haskell +data Endpoint p q h b r = Endpoint + { method :: StdMethod + , path :: Path.Script p + , query :: Query.Script q + , body :: Body.Script b + , headers :: Headers.Script h + , responder :: Responder.Script r + } +``` + +詳細は公式ドキュメントに委ねます。wai-sampleがうまく実装できなかった、レスポンスに複数のパターンがある場合の処理を、case analysisを表す型で実装しているのが興味深いですね。前述した「原理的な問題」に対する解決策なのでしょう。 + +## [IHP](https://ihp.digitallyinduced.com/) + +https://ihp.digitallyinduced.com/Guide/routing.html -開発している途中で見つけた類似のライブラリー # 終わりに From 034febb1427e2cb44d9788ea3e582812b35421da Mon Sep 17 00:00:00 2001 From: YAMAMOTO Yuji Date: Sun, 28 Sep 2025 18:33:39 +0900 Subject: [PATCH 26/39] Introduce IHP --- preprocessed-site/posts/2025/wai-sample.md | 41 ++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/preprocessed-site/posts/2025/wai-sample.md b/preprocessed-site/posts/2025/wai-sample.md index af00b7f..ed3f823 100644 --- a/preprocessed-site/posts/2025/wai-sample.md +++ b/preprocessed-site/posts/2025/wai-sample.md @@ -385,7 +385,7 @@ path "integers/" *> (show <$> decimalPiece) # 類似のライブラリー・解決策 -手が遅いもので、私が最初にwai-sampleのリポジトリーに対して行った[最初のコミット](https://github.com/igrep/wai-sample/commit/37f49dfe86af7482b09ab82b2282c5b9bf1cd73d)から、既に約5年の歳月が過ぎました[^last-commit]。当時は私の前職、IIJにおける社内勉強会のネタとして始めたのが懐かしいです。私が知る限り、当時はwai-sampleのように「値レベルのプログラミングで」「Servantのように1つの定義からクライアントやドキュメントの生成も出来る」ことを目指したライブラリーはなかったように思います。しかし実際のところ、執筆時点で次のライブラリーが類似の機能を実装しています。これらのライブラリーがいつ開発を始めたのかは分かりませんが、やはり私がwai-sampleを作り始めた時点で同じような問題意識を持った人はいたのでしょう。 +手が遅いもので、私が最初にwai-sampleのリポジトリーに対して行った[最初のコミット](https://github.com/igrep/wai-sample/commit/37f49dfe86af7482b09ab82b2282c5b9bf1cd73d)から、既に約5年の歳月が過ぎました[^last-commit]。当時は私の前職、IIJにおける社内勉強会のネタとして始めたのが懐かしいです。私が知る限り、当時はwai-sampleのように「値レベルのプログラミングで」「Servantのように1つの定義からクライアントやドキュメントの生成も出来る」ことを目指したライブラリーはなかったように思います。しかし実際のところ、執筆時点で次のライブラリーが類似の機能を実装しているようです。これらのライブラリーがいつ開発を始めたのかは分かりませんが、やはり私がwai-sampleを作り始めた時点で同じような問題意識を持った人はいたのでしょう。 [^last-commit]: [実装に対する最後の修正](https://github.com/igrep/wai-sample/commit/b2647de2a1a4c7ec8c799ec07972c3d9df6fcb55)からも既に約1年が過ぎました。記録を作るのも遅い...😥 @@ -408,7 +408,44 @@ data Endpoint p q h b r = Endpoint ## [IHP](https://ihp.digitallyinduced.com/) -https://ihp.digitallyinduced.com/Guide/routing.html +IHP (Integrated Haskell Platform)は、Haskellで書かれたフルスタックなWebアプリケーションフレームワークです。wai-sampleのような、与えられたパスに基づいて対応する関数を呼び出す機能(ルーティング機能)はもちろんのこと、PostgreSQLと接続するORMやメールの送信、バックグラウンド処理にGUIから管理する機能など、様々な機能を備えています。[Architecture](https://ihp.digitallyinduced.com/Guide/architecture.html)を読むと察せられるとおり、古き良きRuby on Railsのようなスタイルのフレームワークのようです。 +そんな[IHPのルーティング機能](https://ihp.digitallyinduced.com/Guide/routing.html)、とりわけ古き良きREST APIの慣習では表現しきれず、[カスタマイズしたパスを定義する際の機能](https://ihp.digitallyinduced.com/Guide/routing.html#custom-routing)は、まさにパスのパーサーコンビネーターを書くことで実装できるようになっています。以下はドキュメントにあった例をそのまま貼り付けています: + +```haskell +-- /posts/an-example-blog-post というような記事の名前(slug)や +-- /posts/f85dc0bc-fc11-4341-a4e3-e047074a7982 というような記事のIDから +-- 記事を表示するアクションを呼び出すルーティング + +-- パスにあるパラメーターを表す型 +data PostsController + = ShowPostAction { postId :: !(Maybe (Id Post)), slug :: !(Maybe Text) } + + +-- CanRoute 型クラスのインスタンスで、 +-- パスのパーサーコンビネーターを定義する +instance CanRoute PostsController where + parseRoute' = do + string "/posts/" + let postById = do id <- parseId; endOfInput; pure ShowPostAction { postId = Just id, slug = Nothing } + let postBySlug = do slug <- remainingText; pure ShowPostAction { postId = Nothing, slug = Just slug } + postById <|> postBySlug + + +-- HasPath 型クラスのインスタンスで、 +-- パスに含めるパラメーターからパスを生成する関数を定義する +instance HasPath PostsController where + pathTo ShowPostAction { postId = Just id, slug = Nothing } = "/posts/" <> tshow id + pathTo ShowPostAction { postId = Nothing, slug = Just slug } = "/posts/" <> slug + + +action ShowPostAction { postId, slug } = do + post <- case slug of + Just slug -> query @Post |> filterWhere (#slug, slug) |> fetchOne + Nothing -> fetchOne postId + -- ... +``` + +wai-sampleやOkapiのようにパスの定義を1箇所で済ませられるわけではない(`CanRoute`と`HasPath`の2つの型クラスのインスタンスを定義する必要がある)ようですが、パーサーコンビネーターを使って自由にパスを定義できるところは似ていますね。 # 終わりに From f143a5e9e7794f4a230dbab2c4925dc67f4838b5 Mon Sep 17 00:00:00 2001 From: YAMAMOTO Yuji Date: Sun, 5 Oct 2025 18:09:15 +0900 Subject: [PATCH 27/39] Complete the initial draft of wai-sample.md --- preprocessed-site/posts/2025/wai-sample.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/preprocessed-site/posts/2025/wai-sample.md b/preprocessed-site/posts/2025/wai-sample.md index ed3f823..d028ded 100644 --- a/preprocessed-site/posts/2025/wai-sample.md +++ b/preprocessed-site/posts/2025/wai-sample.md @@ -410,7 +410,7 @@ data Endpoint p q h b r = Endpoint IHP (Integrated Haskell Platform)は、Haskellで書かれたフルスタックなWebアプリケーションフレームワークです。wai-sampleのような、与えられたパスに基づいて対応する関数を呼び出す機能(ルーティング機能)はもちろんのこと、PostgreSQLと接続するORMやメールの送信、バックグラウンド処理にGUIから管理する機能など、様々な機能を備えています。[Architecture](https://ihp.digitallyinduced.com/Guide/architecture.html)を読むと察せられるとおり、古き良きRuby on Railsのようなスタイルのフレームワークのようです。 -そんな[IHPのルーティング機能](https://ihp.digitallyinduced.com/Guide/routing.html)、とりわけ古き良きREST APIの慣習では表現しきれず、[カスタマイズしたパスを定義する際の機能](https://ihp.digitallyinduced.com/Guide/routing.html#custom-routing)は、まさにパスのパーサーコンビネーターを書くことで実装できるようになっています。以下はドキュメントにあった例をそのまま貼り付けています: +そんな[IHPのルーティング機能](https://ihp.digitallyinduced.com/Guide/routing.html)、とりわけREST APIの慣習では表現しきれず、[カスタマイズしたパスを定義する際の機能](https://ihp.digitallyinduced.com/Guide/routing.html#custom-routing)は、まさにパスのパーサーコンビネーターを書くことで実装できるようになっています。以下はドキュメントにあった例をそのまま貼り付けています: ```haskell -- /posts/an-example-blog-post というような記事の名前(slug)や @@ -449,3 +449,5 @@ action ShowPostAction { postId, slug } = do wai-sampleやOkapiのようにパスの定義を1箇所で済ませられるわけではない(`CanRoute`と`HasPath`の2つの型クラスのインスタンスを定義する必要がある)ようですが、パーサーコンビネーターを使って自由にパスを定義できるところは似ていますね。 # 終わりに + +wai-sampleは、HaskellでWeb APIを実装するためのフレームワークとして、ServantやYesodのような既存のフレームワークとは異なるアプローチを試みました。残念ながら目標の達成が技術的に困難であることが分かり、開発を止めることにしましたが、HaskellでWeb APIを実装するため新しいアプローチとして、何かしら参考になれば幸いです。 From 5c375afe52e1c339d03096dd8a0c3c82d961226c Mon Sep 17 00:00:00 2001 From: YAMAMOTO Yuji Date: Sun, 12 Oct 2025 20:52:41 +0900 Subject: [PATCH 28/39] Modify wai-sample.md --- preprocessed-site/posts/2025/wai-sample.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/preprocessed-site/posts/2025/wai-sample.md b/preprocessed-site/posts/2025/wai-sample.md index d028ded..5bca62e 100644 --- a/preprocessed-site/posts/2025/wai-sample.md +++ b/preprocessed-site/posts/2025/wai-sample.md @@ -14,7 +14,7 @@ tags: # 動機 -そもそも、Haskellには既にServantやYesod、Scottyといった人気のフレームワークがあるにもかかわらず、なぜ新しいフレームワークを作ろうと思ったのでしょうか。第一に、かつて私が[「Haskellの歩き方」という記事の「Webアプリケーション」の節](https://wiki.haskell.jp/Hikers%20Guide%20to%20Haskell.html#web%E3%82%A2%E3%83%97%E3%83%AA%E3%82%B1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3)で述べた、次の問題を解決したかったから、という理由があります: +そもそも、Haskellには既にServantやYesod、Scottyといった人気のフレームワークがあるにもかかわらず、なぜ新しいフレームワークを作ろうと思ったのでしょうか。第1に、かつて私が[「Haskellの歩き方」という記事の「Webアプリケーション」の節](https://wiki.haskell.jp/Hikers%20Guide%20to%20Haskell.html#web%E3%82%A2%E3%83%97%E3%83%AA%E3%82%B1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3)で述べた、次の問題を解決したかったから、という理由があります: > ただしServant, Yesod, 共通した困った特徴があります。 > それぞれがHaskellの高度な機能を利用した独特なDSLを提供しているため、仕組みがわかりづらい、という点です。 @@ -77,7 +77,7 @@ sampleRoutes = sampleRoutes :: [Handler] ``` -`Handler`という型の値のリストで、それぞれの`Handler`には、Web APIのエンドポイントを表すのに必要な情報が全て含まれています。wai-sampleでは、この`Handler`のリストを解釈してWAIベースのサーバーアプリケーションを実行したり、Template Haskellを通じてクライアントコードを生成したり、はたまたサーバーアプリケーションのドキュメントを生成したりすることができるようになっています。 +`Handler`という型のリストで、それぞれの`Handler`には、Web APIのエンドポイントを表すのに必要な情報が全て含まれています。wai-sampleでは、この`Handler`のリストを解釈してWAIベースのサーバーアプリケーションを実行したり、Template Haskellを通じてクライアントコードを生成したり、はたまたサーバーアプリケーションのドキュメントを生成したりすることができるようになっています。 ## (1) 最も単純な例 @@ -108,7 +108,7 @@ get @(WithStatus Status503 PlainText, T.Text) "maintenance" (path "maintenance") ## (3) パスの中に含まれる整数を処理する例 -よくあるWebアプリケーションフレームワークでは、パスの一部に含まれる整数など文字列以外の型の値を取得するための仕組みが用意されています。 +よくあるWebアプリケーションフレームワークでは、パスの一部に含まれる整数など、文字列型以外の値を取得するための仕組みが用意されています。 Haskellにおいて、文字列から特定の型の値を取り出す...といえばそう、パーサーコンビネーターですね。wai-sampleでは、サーバーが受け取ったパスをパーサーコンビネーターでパースするようになっています。従って下記の例では、`/customer/123/transaction/abc`というパスを受け取った場合、`123`と`"abc"`をタプルに詰め込んで`Responder`に渡すパスのパーサーを定義しています: @@ -245,7 +245,7 @@ httpClientBackend rootUrl manager method pathPieces rawReqHds = do ## ドキュメントの生成 -[ServantではOpenAPIに則ったドキュメントを生成するパッケージがある](https://hackage.haskell.org/package/servant-openapi3)ように、Haskellの構文で定義したREST APIの仕様から、APIのドキュメントを生成する機能があると便利でしょう。wai-sampleでも、`Handler`型のリストからAPIのドキュメントを生成する機能を実装しました --- 完成度が低く、とても実用に耐えるものではありませんが。 +[ServantではOpenAPIに則ったドキュメントを生成するパッケージがある](https://hackage.haskell.org/package/servant-openapi3)ように、Haskellの構文で定義したREST APIの仕様から、APIのドキュメントを生成する機能があると便利でしょう。wai-sampleでも、`Handler`型のリストからAPIのドキュメントを生成する機能を実装しました --- 残念ながら完成度が低く、とても実用に耐えるものではありませんが。 ともあれ、試しに使ってみましょう。これまで例として紹介した[`sampleRoutes`](https://github.com/igrep/wai-sample/blob/b4ddb75a28b927b76ac7c4c182bad6812769ed01/src/WaiSample/Sample.hs#L147)の各`Handler`に[`showHandlerSpec`](https://github.com/igrep/wai-sample/blob/b4ddb75a28b927b76ac7c4c182bad6812769ed01/src/WaiSample.hs#L47)という関数を適用すると、次のように各エンドポイントへのパスやリクエスト・レスポンスの情報を取得することが出来ます: @@ -286,7 +286,7 @@ createProduct "POST" /products -- ... 以下略 ... ``` -...が、あまりに完成度が低いので、詳しくは解説しません。実際に上記のコード実行すると、`Response`の型などがとても人間に読めるような出力になっていないことが分かります。今どきのWeb APIフレームワークであればOpenAPIに則ったドキュメントを生成する機能が欲しいでしょうが、それもありません。この方向で拡張すれば実装できるとは思いますが、次の節で述べるとおり開発を止めることにしたので、ここまでとしておきます。 +...が、前述の通りあまりに完成度が低いので、詳しくは解説しません。実際に上記のコード実行すると、`Response`の型などがとても人間に読めるような出力になっていないことが分かります。今どきのWeb APIフレームワークであればOpenAPIに則ったドキュメントを生成する機能が欲しいでしょうが、それもありません。この方向で拡張すれば実装できるとは思いますが、次の節で述べるとおり開発を止めることにしたので、ここまでとしておきます。 # 何故開発を止めるのか @@ -329,9 +329,7 @@ get [(plainText, text), ((withStatus status503 plainText), text)] -- ... ``` -型レベルプログラミングバージョンでは型引数に渡していた情報を、ほぼそのまま値レベルに落とし込んだものです。しかしこのように書いたとしても、サーバーアプリケーションを起動して、実際にクライアントからリクエストを受け取り、それに対して`get`に渡した関数(`Responder`)がレスポンスの元となる値を返すまで、レスポンスボディーの型が正しいかどうか、検証できないのです。「レスポンスの元となる値」の型はライブラリーのユーザー自身が`Responder`で返す値の型ですし、実行時以前にコンパイル時に保証できていて欲しいものです[^launch-time]。これが、値レベルのDSLを採用した場合の限界です。 - -[^launch-time]: と、ここまで書いて気付いたのですが、`Typeable`型クラスを利用して、サーバーアプリケーション起動時に各`Responder`が返す型が正しいかチェックする、という手法もありだったかもしれません(あるいはそれだけのためにTemplate Haskellをどこかに挟む?)。まあ、型チェックである以上コンパイル時にチェックする方が望ましいでしょうし、私自身のやる気ももう失われてしまったので特に取り組みはしませんが...。 +型レベルプログラミングバージョンでは型引数に渡していた情報を、ほぼそのまま値レベルに落とし込んだものです。しかしこのように書いたとしても、サーバーアプリケーションを起動して、実際にクライアントからリクエストを受け取り、それに対して`get`に渡した関数(`Responder`)がレスポンスの元となる値を返すまで、レスポンスボディーの型が正しいかどうか、検証できないのです。「レスポンスの元となる値」の型はライブラリーのユーザー自身が`Responder`で返す値の型ですし、実行時以前にコンパイル時に保証できていて欲しいものです。これが、値レベルのDSLを採用した場合の限界です。 それから、型レベルリストを使ったこと以外においても、複雑で分かりづらい要因があります。先程から少し触れているとおり、Content-Typeやステータスコードとレスポンスボディーの型を組み合わせを表すのに、タプル以外にも`Response`という型を用いています。`Response`型とタプル型はいずれも[`ResponseSpec`](https://github.com/igrep/wai-sample/blob/b4ddb75a28b927b76ac7c4c182bad6812769ed01/src/WaiSample/Types/Response.hs#L47-L49)(下記に転載)という型クラスのインスタンスとなることで、「Content-Typeやステータスコード」を表す型(`ResponseType`)と`Responder`がレスポンスボディーとして返す型(`ResponseObject`)を宣言することが出来ます: @@ -376,7 +374,7 @@ path "integers/" *> (show <$> decimalPiece) - HTTPレスポンスに関わるもの: - 動的なHTMLの配信 - ファイルシステムにあるファイルの配信 - - (REST APIに特化したフレームワークであればこれは不要でしょうが) + - (REST APIに特化したフレームワークであればこれらは不要でしょうが、拡張として簡単に追加できるようにはしたいですね) - 両方に関わるもの: - Cookieの読み書き - ドキュメント生成に関わるもの: @@ -404,11 +402,13 @@ data Endpoint p q h b r = Endpoint } ``` +TODO: もう少しサンプルを + 詳細は公式ドキュメントに委ねます。wai-sampleがうまく実装できなかった、レスポンスに複数のパターンがある場合の処理を、case analysisを表す型で実装しているのが興味深いですね。前述した「原理的な問題」に対する解決策なのでしょう。 ## [IHP](https://ihp.digitallyinduced.com/) -IHP (Integrated Haskell Platform)は、Haskellで書かれたフルスタックなWebアプリケーションフレームワークです。wai-sampleのような、与えられたパスに基づいて対応する関数を呼び出す機能(ルーティング機能)はもちろんのこと、PostgreSQLと接続するORMやメールの送信、バックグラウンド処理にGUIから管理する機能など、様々な機能を備えています。[Architecture](https://ihp.digitallyinduced.com/Guide/architecture.html)を読むと察せられるとおり、古き良きRuby on Railsのようなスタイルのフレームワークのようです。 +IHP (Integrated Haskell Platform)は、Haskellで書かれたフルスタックなWebアプリケーションフレームワークです。wai-sampleのような、与えられたパスに基づいて対応する関数を呼び出す機能(ルーティング機能)はもちろんのこと、PostgreSQLと接続するORMやメールの送信、バックグラウンド処理に加えてGUIから管理する機能など、様々な機能を備えています。[Architecture](https://ihp.digitallyinduced.com/Guide/architecture.html)を読むと察せられるとおり、古き良きRuby on Railsのようなスタイルのフレームワークのようです。 そんな[IHPのルーティング機能](https://ihp.digitallyinduced.com/Guide/routing.html)、とりわけREST APIの慣習では表現しきれず、[カスタマイズしたパスを定義する際の機能](https://ihp.digitallyinduced.com/Guide/routing.html#custom-routing)は、まさにパスのパーサーコンビネーターを書くことで実装できるようになっています。以下はドキュメントにあった例をそのまま貼り付けています: From 3e58bf7d4cd1bb13608110ec8739f3a163e5a663 Mon Sep 17 00:00:00 2001 From: YAMAMOTO Yuji Date: Sun, 26 Oct 2025 20:38:52 +0900 Subject: [PATCH 29/39] Begin to explain Okapi more in detail --- preprocessed-site/posts/2025/wai-sample.md | 43 ++++++++++++++++++++-- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/preprocessed-site/posts/2025/wai-sample.md b/preprocessed-site/posts/2025/wai-sample.md index 5bca62e..9e53e8e 100644 --- a/preprocessed-site/posts/2025/wai-sample.md +++ b/preprocessed-site/posts/2025/wai-sample.md @@ -389,7 +389,9 @@ path "integers/" *> (show <$> decimalPiece) ## [Okapi](https://okapi.wiki/) -[Okapi](https://okapi.wiki/)にある[Endpoint](https://okapi.wiki/#endpoint)という機能は「An Endpoint is an executable specification representing a single Operation that can be taken against your API.」と謳っているとおり、APIの仕様を表現する内部DSLを提供します。下記の`Endpoint`という型 --- wai-sampleでいう`Handler`に相当するようです --- に、「Script」と呼ばれる値レベルDSLを設定して使うようです: +[Okapi](https://okapi.wiki/)にある[Endpoint](https://okapi.wiki/#endpoint)という機能は「An Endpoint is an executable specification representing a single Operation that can be taken against your API.」と謳っているとおり、APIの仕様を表現する内部DSLを提供します。しかもこれから紹介するとおり、wai-sampleより幾分洗練されているように見えます。 + +Okapiでは下記の`Endpoint`という型 --- wai-sampleでいう`Handler`に相当するようです --- に、「Script」と呼ばれる値レベルDSLを設定して使うようです: ```haskell data Endpoint p q h b r = Endpoint @@ -402,9 +404,44 @@ data Endpoint p q h b r = Endpoint } ``` -TODO: もう少しサンプルを +詳細はもちろん公式ドキュメントにも書かれていますが、読んでわかる範囲でこちらでも解説しましょう。`Endpoint`型の各フィールドは、HTTPリクエスト・レスポンスに関わる各要素の仕様を表しています。`method`フィールドを除くすべてのフィールドは、それぞれのフィールドのために作られた`Script`という型の`Applicative`[^alternaive]なDSLを使って仕様を表現します。`Path.Script p`はパスの仕様、`Query.Script q`はクエリーパラメーターの仕様、`Body.Script b`はリクエストボディーの仕様、`Headers.Script h`はリクエストヘッダーの仕様、`Responder.Script r`はレスポンスの仕様、といったところです。 + +[^alternaive]: 個人的には、なぜ`Alternative`にしなかったのかが気になります。`Body.optional`や`Headers.optional`などは文字通り[`Alternative`の`optional`](https://hackage.haskell.org/package/base-4.21.0.0/docs/Control-Applicative.html#v:optional)で実現できそうに見えるからです。 + +各`Script`型のうち、`Path.Script`型はwai-sampleの`Route`型とよく似てますし、と`Query.Script`型はよくある、key・valueのリストからレコード型を組み立てる際のDSL(例えば、[`FromJSON`](https://hackage.haskell.org/package/aeson-2.2.3.0/docs/Data-Aeson-Types.html#t:FromJSON)のインスタンスを定義する際のDSL)と似たようなもの、と説明すれば概ね通じそうなので割愛します。 + +各`Script`型のうち、特筆すべきは`Responder.Script`でしょう。`Responder.Script`では、hoge + +```haskell +data SecretHeaders = SecretHeaders + { firstSecret :: Int -> Response -> Response + , secondSecret :: Int -> Response -> Response + } + +data MyResponders = MyResponders + { allGood :: (SecretHeaders %1 -> Response -> Response) -> Text -> Response + , notGood :: (() %1 -> Response -> Response) -> Text -> Response + } + +myResponderScript = do + allGood <- Responder.json @Text status200 do + addSecret <- AddHeader.using @Int "IntSecret" + addAnotherSecret <- AddHeader.using @Int "X-Another-Secret" + pure SecretHeaders {..} + notGood <- Responder.json @Text status501 $ pure () + pure MyResponders {..} + +myHandler someNumber _ _ _ _ (MyResponders allGood notGood) = do + if someNumber < 100 + then return $ allGood + (\(SecretHeaders firstSecret secondSecret) response -> secondSecret 0 $ firstSecret 7 response) + "All Good!" + else return $ notGood + (\() response -> response) + "Not Good!" +``` -詳細は公式ドキュメントに委ねます。wai-sampleがうまく実装できなかった、レスポンスに複数のパターンがある場合の処理を、case analysisを表す型で実装しているのが興味深いですね。前述した「原理的な問題」に対する解決策なのでしょう。 +wai-sampleがうまく実装できなかった、レスポンスに複数のパターンがある場合の処理を、case analysisを表す型で実装しているのが興味深いですね。前述した「原理的な問題」に対する解決策なのでしょう。 ## [IHP](https://ihp.digitallyinduced.com/) From 2c2802d6804959b10064ca34567c56ba595bf98a Mon Sep 17 00:00:00 2001 From: YAMAMOTO Yuji Date: Sun, 26 Oct 2025 20:39:58 +0900 Subject: [PATCH 30/39] Remove the verbose paragraph --- preprocessed-site/posts/2025/wai-sample.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/preprocessed-site/posts/2025/wai-sample.md b/preprocessed-site/posts/2025/wai-sample.md index 9e53e8e..46eb9ad 100644 --- a/preprocessed-site/posts/2025/wai-sample.md +++ b/preprocessed-site/posts/2025/wai-sample.md @@ -408,8 +408,6 @@ data Endpoint p q h b r = Endpoint [^alternaive]: 個人的には、なぜ`Alternative`にしなかったのかが気になります。`Body.optional`や`Headers.optional`などは文字通り[`Alternative`の`optional`](https://hackage.haskell.org/package/base-4.21.0.0/docs/Control-Applicative.html#v:optional)で実現できそうに見えるからです。 -各`Script`型のうち、`Path.Script`型はwai-sampleの`Route`型とよく似てますし、と`Query.Script`型はよくある、key・valueのリストからレコード型を組み立てる際のDSL(例えば、[`FromJSON`](https://hackage.haskell.org/package/aeson-2.2.3.0/docs/Data-Aeson-Types.html#t:FromJSON)のインスタンスを定義する際のDSL)と似たようなもの、と説明すれば概ね通じそうなので割愛します。 - 各`Script`型のうち、特筆すべきは`Responder.Script`でしょう。`Responder.Script`では、hoge ```haskell From 58a4a7a887a5792f85e2a1a3a19e64c8649e4e09 Mon Sep 17 00:00:00 2001 From: YAMAMOTO Yuji Date: Sun, 2 Nov 2025 18:28:25 +0900 Subject: [PATCH 31/39] Complete the second draft of wai-sample.md --- preprocessed-site/posts/2025/wai-sample.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/preprocessed-site/posts/2025/wai-sample.md b/preprocessed-site/posts/2025/wai-sample.md index 46eb9ad..8080e25 100644 --- a/preprocessed-site/posts/2025/wai-sample.md +++ b/preprocessed-site/posts/2025/wai-sample.md @@ -408,27 +408,43 @@ data Endpoint p q h b r = Endpoint [^alternaive]: 個人的には、なぜ`Alternative`にしなかったのかが気になります。`Body.optional`や`Headers.optional`などは文字通り[`Alternative`の`optional`](https://hackage.haskell.org/package/base-4.21.0.0/docs/Control-Applicative.html#v:optional)で実現できそうに見えるからです。 -各`Script`型のうち、特筆すべきは`Responder.Script`でしょう。`Responder.Script`では、hoge +各`Script`型のうち、特筆すべきは`Responder.Script`でしょう。`Responder.Script`では、レスポンスの種類毎にレスポンスボディーの型やステータスコード、レスポンスヘッダーの型を、case analysisを表す型として定義できるようになっています。そして、`Handler`型は`Endpoint`型が各種`Script`を使って設定した値を使って、実際に`Response`型の値を組み立てます: + +(⚠️以下のコードは、[Okapiのドキュメントにあったサンプルコード](https://okapi.wiki/#cb10)を元に、私が推測してコメントを追加したものです。間違っていたらごめんなさい hask(\_ \_)eller) ```haskell +-- | Responseにヘッダーを設定する関数群 data SecretHeaders = SecretHeaders { firstSecret :: Int -> Response -> Response , secondSecret :: Int -> Response -> Response } +-- | Responseにヘッダーとボディーを設定する関数群 +-- レスポンスの種類毎にフィールドラベルを1つ備えた、case analysisを表す型 data MyResponders = MyResponders { allGood :: (SecretHeaders %1 -> Response -> Response) -> Text -> Response , notGood :: (() %1 -> Response -> Response) -> Text -> Response } +-- | `Responder.Script`として定義する、レスポンスの仕様 myResponderScript = do + -- allGood の場合はレスポンスボディーは`Text`型で、ステータスコードは200。 + -- レスポンスヘッダーとしては、`IntSecret`と`X-Another-Secret`という + -- `Int`型の2つのヘッダーを追加する。 allGood <- Responder.json @Text status200 do addSecret <- AddHeader.using @Int "IntSecret" addAnotherSecret <- AddHeader.using @Int "X-Another-Secret" pure SecretHeaders {..} + + -- notGood の場合はレスポンスボディーは`Text`型で、ステータスコードは501。 + -- レスポンスヘッダーはなし。 notGood <- Responder.json @Text status501 $ pure () pure MyResponders {..} +-- | Responder.Scriptで定義したcase analysisを表す型、`MyResponders`を使って、 +-- レスポンスを組み立てる関数。 +-- `someNumber`が100未満なら`allGood`を、そうでなければ`notGood`を使う。 +-- この関数が利用していない引数は、`Endpoint`型の他のフィールドに対応するもの。 myHandler someNumber _ _ _ _ (MyResponders allGood notGood) = do if someNumber < 100 then return $ allGood From 39e24f70d3505143b94b5d4da242308145db6827 Mon Sep 17 00:00:00 2001 From: YAMAMOTO Yuji Date: Sun, 9 Nov 2025 17:47:43 +0900 Subject: [PATCH 32/39] Update LTS Haskell --- .circleci/config.yml | 8 ++++---- stack.yaml | 2 +- stack.yaml.lock | 10 +++++----- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 544f4a5..b77ef10 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -3,7 +3,7 @@ jobs: build: # Ref: https://mmhaskell.com/blog/2018/4/25/dockerizing-our-haskell-app docker: - - image: haskell:9.0.2-slim + - image: haskell:24.19-slim steps: - run: apt update - run: apt install -y zip jq curl @@ -16,7 +16,7 @@ jobs: keys: - 'dependencies-{{ checksum "stack.yaml" }}-{{ checksum "haskell-jp-blog.cabal" }}' - 'dependencies-' - - run: stack build --compiler=ghc-9.0.2 --no-terminal --only-dependencies + - run: stack build --compiler=ghc-24.19 --no-terminal --only-dependencies - save_cache: key: 'dependencies-{{ checksum "stack.yaml" }}-{{ checksum "haskell-jp-blog.cabal" }}' paths: @@ -27,7 +27,7 @@ jobs: keys: - 'executable-{{ checksum "src/site.hs" }}' - 'executable-' - - run: stack --compiler=ghc-9.0.2 --local-bin-path='.' --no-terminal install --pedantic + - run: stack --compiler=ghc-24.19 --local-bin-path='.' --no-terminal install --pedantic - save_cache: key: 'executable-{{ checksum "src/site.hs" }}' paths: @@ -55,7 +55,7 @@ jobs: deploy: docker: - - image: haskell:9.0.2-slim + - image: haskell:24.19-slim steps: - checkout: path: ~/project diff --git a/stack.yaml b/stack.yaml index 31fd4a7..13aafc1 100644 --- a/stack.yaml +++ b/stack.yaml @@ -3,4 +3,4 @@ packages: - '.' extra-deps: [] -resolver: lts-19.22 +resolver: lts-24.19 diff --git a/stack.yaml.lock b/stack.yaml.lock index 6ab576e..5346b53 100644 --- a/stack.yaml.lock +++ b/stack.yaml.lock @@ -1,12 +1,12 @@ # This file was autogenerated by Stack. # You should not edit this file by hand. # For more information, please see the documentation at: -# https://docs.haskellstack.org/en/stable/lock_files +# https://docs.haskellstack.org/en/stable/topics/lock_files packages: [] snapshots: - completed: - size: 619399 - url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/19/22.yaml - sha256: 5098594e71bdefe0c13e9e6236f12e3414ef91a2b89b029fd30e8fc8087f3a07 - original: lts-19.22 + sha256: 5524530ac8c0fd4c9d7488442ff14edb0fcbf4989b928b86398f94f367e01ee3 + size: 726110 + url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/24/19.yaml + original: lts-24.19 From 9383b2ee7ce1ff8d14ae93d4f1e1ad7d9a4ee0d7 Mon Sep 17 00:00:00 2001 From: YAMAMOTO Yuji Date: Sun, 9 Nov 2025 17:49:33 +0900 Subject: [PATCH 33/39] Correct the GHC version --- .circleci/config.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index b77ef10..bacfcc7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -3,7 +3,7 @@ jobs: build: # Ref: https://mmhaskell.com/blog/2018/4/25/dockerizing-our-haskell-app docker: - - image: haskell:24.19-slim + - image: haskell:9.10.3-slim steps: - run: apt update - run: apt install -y zip jq curl @@ -16,7 +16,7 @@ jobs: keys: - 'dependencies-{{ checksum "stack.yaml" }}-{{ checksum "haskell-jp-blog.cabal" }}' - 'dependencies-' - - run: stack build --compiler=ghc-24.19 --no-terminal --only-dependencies + - run: stack build --compiler=ghc-9.10.3 --no-terminal --only-dependencies - save_cache: key: 'dependencies-{{ checksum "stack.yaml" }}-{{ checksum "haskell-jp-blog.cabal" }}' paths: @@ -27,7 +27,7 @@ jobs: keys: - 'executable-{{ checksum "src/site.hs" }}' - 'executable-' - - run: stack --compiler=ghc-24.19 --local-bin-path='.' --no-terminal install --pedantic + - run: stack --compiler=ghc-9.10.3 --local-bin-path='.' --no-terminal install --pedantic - save_cache: key: 'executable-{{ checksum "src/site.hs" }}' paths: @@ -55,7 +55,7 @@ jobs: deploy: docker: - - image: haskell:24.19-slim + - image: haskell:9.10.3-slim steps: - checkout: path: ~/project From fec1e9830785b3890911c46836c58faa135d0469 Mon Sep 17 00:00:00 2001 From: YAMAMOTO Yuji Date: Sun, 9 Nov 2025 17:52:48 +0900 Subject: [PATCH 34/39] Correct the Stackage version --- .circleci/config.yml | 8 ++++---- stack.yaml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index bacfcc7..d88d896 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -3,7 +3,7 @@ jobs: build: # Ref: https://mmhaskell.com/blog/2018/4/25/dockerizing-our-haskell-app docker: - - image: haskell:9.10.3-slim + - image: haskell:9.10.2-slim steps: - run: apt update - run: apt install -y zip jq curl @@ -16,7 +16,7 @@ jobs: keys: - 'dependencies-{{ checksum "stack.yaml" }}-{{ checksum "haskell-jp-blog.cabal" }}' - 'dependencies-' - - run: stack build --compiler=ghc-9.10.3 --no-terminal --only-dependencies + - run: stack build --compiler=ghc-9.10.2 --no-terminal --only-dependencies - save_cache: key: 'dependencies-{{ checksum "stack.yaml" }}-{{ checksum "haskell-jp-blog.cabal" }}' paths: @@ -27,7 +27,7 @@ jobs: keys: - 'executable-{{ checksum "src/site.hs" }}' - 'executable-' - - run: stack --compiler=ghc-9.10.3 --local-bin-path='.' --no-terminal install --pedantic + - run: stack --compiler=ghc-9.10.2 --local-bin-path='.' --no-terminal install --pedantic - save_cache: key: 'executable-{{ checksum "src/site.hs" }}' paths: @@ -55,7 +55,7 @@ jobs: deploy: docker: - - image: haskell:9.10.3-slim + - image: haskell:9.10.2-slim steps: - checkout: path: ~/project diff --git a/stack.yaml b/stack.yaml index 13aafc1..8825774 100644 --- a/stack.yaml +++ b/stack.yaml @@ -3,4 +3,4 @@ packages: - '.' extra-deps: [] -resolver: lts-24.19 +resolver: lts-24.11 From e22d192f589e20610c5b67fac4ed3641336fc760 Mon Sep 17 00:00:00 2001 From: YAMAMOTO Yuji Date: Sun, 9 Nov 2025 17:54:37 +0900 Subject: [PATCH 35/39] Correct the Docker image tag --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index d88d896..e0b0d86 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -3,7 +3,7 @@ jobs: build: # Ref: https://mmhaskell.com/blog/2018/4/25/dockerizing-our-haskell-app docker: - - image: haskell:9.10.2-slim + - image: haskell:9.10.2-slim-bullseye steps: - run: apt update - run: apt install -y zip jq curl From 483116b91cca19db3b86505475c9dfc2864d82c6 Mon Sep 17 00:00:00 2001 From: YAMAMOTO Yuji Date: Sun, 9 Nov 2025 17:59:24 +0900 Subject: [PATCH 36/39] Install Git and OpenSSH in CircleCI --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index e0b0d86..9d614b3 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -6,7 +6,7 @@ jobs: - image: haskell:9.10.2-slim-bullseye steps: - run: apt update - - run: apt install -y zip jq curl + - run: apt install -y zip jq curl git openssh-client - run: stack upgrade - run: "echo 'tcp 6 TCP' > /etc/protocols" - run: "stack config --system-ghc set system-ghc --global true" From 48979b417cfc8a5186899182d875d0bf50e9c121 Mon Sep 17 00:00:00 2001 From: YAMAMOTO Yuji Date: Sun, 9 Nov 2025 18:28:51 +0900 Subject: [PATCH 37/39] Install pkgconfig on CircleCI --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 9d614b3..9874e3e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -6,7 +6,7 @@ jobs: - image: haskell:9.10.2-slim-bullseye steps: - run: apt update - - run: apt install -y zip jq curl git openssh-client + - run: apt install -y zip jq curl git openssh-client pkg-config - run: stack upgrade - run: "echo 'tcp 6 TCP' > /etc/protocols" - run: "stack config --system-ghc set system-ghc --global true" From 32d0d42de4dc58602b44ded55028d4a0a9af55ab Mon Sep 17 00:00:00 2001 From: YAMAMOTO Yuji Date: Sun, 9 Nov 2025 18:29:26 +0900 Subject: [PATCH 38/39] Forgot to update stack.yaml.lock --- stack.yaml.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/stack.yaml.lock b/stack.yaml.lock index 5346b53..9dc6b15 100644 --- a/stack.yaml.lock +++ b/stack.yaml.lock @@ -6,7 +6,7 @@ packages: [] snapshots: - completed: - sha256: 5524530ac8c0fd4c9d7488442ff14edb0fcbf4989b928b86398f94f367e01ee3 - size: 726110 - url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/24/19.yaml - original: lts-24.19 + sha256: 468e1afa06cd069e57554f10e84fdf1ac5e8893e3eefc503ef837e2449f7e60c + size: 726310 + url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/24/11.yaml + original: lts-24.11 From b6a4f7845c72d202ac6af4cb9381e5be80f12baa Mon Sep 17 00:00:00 2001 From: YAMAMOTO Yuji Date: Sun, 9 Nov 2025 19:30:32 +0900 Subject: [PATCH 39/39] Avoid warning in CircleCI --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 9874e3e..5dd8645 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -27,7 +27,7 @@ jobs: keys: - 'executable-{{ checksum "src/site.hs" }}' - 'executable-' - - run: stack --compiler=ghc-9.10.2 --local-bin-path='.' --no-terminal install --pedantic + - run: stack --compiler=ghc-9.10.2 --local-bin-path='.' --no-terminal install - save_cache: key: 'executable-{{ checksum "src/site.hs" }}' paths: