Hammock による Free Monad な HTTPクライント
Hammock という純粋関数型HTTPクライアントライブラリがある。HTTP アクセスを Free Monad で表現していて、effect のパラメータ化技法の応用例として面白いので、今回はこれを試してみる。
概要
- Hammock は、型安全かつ純粋な関数型 HTTP クライアントライブラリ
-
effect をパラメータ化する技法としてFree Monad が活用されている。
- HttpF: HTTP の8つの動詞を Free Monad のいわゆる Algebra で 表現
-
MarshallF: レスポンス中の Entity から他の型
A
へのアンマーシャルも Free Monad で表現
-
effect は
Sync[F]
を持つ任意のF[_]
が使える(典型的にはIO
やTask
など)。 - 既存の HTTP クライアントをベースで利用していて、それぞれの Free Monad インタープリターが付属モジュールとして提供される。JVM 用だと、Apach HttpComponent Client, HttpAsyncClient, Akka HTTP Client API から選択可能。
サンプル実装
公式サイトのサンプル をベースにして、Cats Effect、Monix、Iota などの要素を取り入れて実装してみる。
使ったバージョンなど
- hammock: "0.9.0"
- circe: "0.10.0"
- monix: "3.0.0-RC2"
- iota: "0.3.10"
一番簡単なサンプル
一般に Free Monad を使った Scala プログラムは、下のような部分からなる。
- 代数: いわゆる Algebra の定義
- リフト/スマートコンストラクタ: Algebra から Free Monad へのリフトとスマートコンストラクタ
- インタプリタ: Free Monad から他のモナドへの変換
- 記述: Algebraを使ったプログラムの記述
- 実行: プログラムにインタプリタを与えて具体的に実行する部分
このうち、Hammock では、(1) の代数(HttpF, MarshallF)、(2) リフト/スマートコンストラクタ、(3)ベース HTTPクライアントごとの各インタプリタが、モジュールとして提供される。
(4)のプログラムの記述は、Hammock のユーティリティメソッドを使って、動詞、URL、ヘッダーなどから Free[HttpF, HttpResponse]
を得た上で、必要に応じてレスポンスをデコード→アンマーシャルしたり、map
、flatMap
して変換したりといった操作を追加して組み立てる。
もっとも簡単な例は、以下のようなものになる。
val program: Free[HttpF, HttpResponse] = Hammock
.request(Method.GET, uri"http://httpbin.org/get", Map())
この program
にインタープリタを与えて実行する部分(5)は、以下のようになる。ベースの HTTPクライアントには Apache Http Client、effect としては Cats Effect の IO
を使ってみた。
object SimpleMain extends IOApp {
...
implicit val interpreter: InterpTrans[IO] = ApacheInterpreter[IO]
def run(args: List[String]): IO[ExitCode] = for {
res <- program.exec[IO] // ここで Free から IO に変換
_ <- IO(println(res.entity))
} yield ExitCode.Success
}
run
メソッド内の program.exec[IO]
の行で、Free[HttpF, HttpResponse]
が IO[HttpResponse]
に変換される。このときに implicit な ApacheInterpreter
インタープリタが参照される。
実行すると httpbin からリクエストの内容をそのまま表すレスポンスが返ってくる。
複数の Free Monad の合成
次に、公式の Algebras のサンプルをベースにしつつ、Iota を使って複数 Free Monad の合成をシンプルに書いてみる(Iota の過去記事)。
まずコンソール入出力のための自前の Free Monad 代数 ConsoleF
を、次のように定める。
object Console {
sealed trait ConsoleF[A]
case object Read extends ConsoleF[String]
case class Write(msg: String) extends ConsoleF[Unit]
...
def trans[F[_]](implicit F: Sync[F]): ConsoleF ~> F = new (ConsoleF ~> F) {
def apply[A](ca: ConsoleF[A]): F[A] = ca match {
case Read => F.delay(scala.io.StdIn.readLine())
case Write(msg) => F.delay(println(msg))
}
}
}
この ConsoleF
と Hammock の HttpF
、MarshallF
を全て合成した型 App
と、App
から F[_]
への変換 trans
を次のように定めておく。
type App[A] = CopK[MarshallF ::: HttpF ::: ConsoleF ::: TNilK, A]
implicit def trans[F[_]](implicit S: Sync[F]): App ~> F = CopK.FunctionK.of(
marshallNT[F], ApacheInterpreter[F].trans, Console.trans(S))
HttpF
、MarshallF
のスマートコンストラクタは Iota の CopK.Inject
をサポートしていないため、下記のように暗黙の InjectK
1 を用意しておく。
implicit def httpI: InjectK[HttpF, App] = CopK.Inject[HttpF, App]
implicit def marshallI: InjectK[MarshallF, App] = CopK.Inject[MarshallF, App]
これらを使うと、以下のようにプログラムが記述できる。
def program(implicit
Console: ConsoleC[App],
Marshall: MarshallC[App],
Hammock: HttpRequestC[App]
): Free[App, String] = for {
_ <- Console.write("What's the ID?")
id <- Console.read
response <- Hammock.get(uri"https://jsonplaceholder.typicode.com/users?id=${id.toString}", Map())
parsed <- Marshall.unmarshall[String](response.entity)
} yield parsed
実行するコードは先述の SimpleApp
と同様だが、ここは monix の Task
を使ってみた。
override def run(args: List[String]): Task[ExitCode] = for {
entity <- program foldMap trans[Task]
_ <- Task { println(entity) }
} yield ExitCode.Success
コンソールで "What's the ID?" と聞かれるので、"4"などを入力するとダミーのユーザ情報2が返ってくる。
補足と所感
上の Free Monad のサンプルでは、やや多めに Free Monad を使うコードを書いてみたが、
Hammock.request(...)
等でFree[F, HttpResponse]
を得た直後に.exec[IO]
や.exec[Task]
をつなげてIO
やTask
に変換すると、逆に Free Monad の使用をあえて見せないようなコーディングもできる。Scala 界隈での、Free Monad の全盛期といえば、多分『Functional and Reactive Domain Modeling』が出版された 2016 年ごろかなと個人的には思っていて、今思えばその頃はファッション的に濫用されすぎていた気がしないでもない。その後 Tagless Final の台頭とともに、Free Monad が徐々にオワコン化してきた一方で、むしろ濫用が収まって代数化のメリットがデメリット(特にボイラープレートなど)を普通に上回るような、正しい使い方が残ってきたのかなと思う3。この Hammock も、そうした良い Free Monad の活用という感触はあった。
-
ちなみにここで言う
Inject[A, B]
、InjectK[A, B]
とは、「Open Union 型B[_]
にA[_]
が含まれる」といった事実を示すエビデンス的な意味で DI 的な意味での Inject とは関係ない。 ↩ -
JSONPlaceholder というテスト用のフェイク REST API サイトを使用。 ↩
-
この辺りの記事では、そうした良い Free Monad の例として Slick の DBIOAction や doobie の ConnectionIO などが挙げられている。 ↩
Author And Source
この問題について(Hammock による Free Monad な HTTPクライント), 我々は、より多くの情報をここで見つけました https://qiita.com/yasuabe2613/items/f4485b1a2ff1e0c643fd著者帰属:元の著者の情報は、元のURLに含まれています。著作権は原作者に属する。
Content is automatically searched and collected through network algorithms . If there is a violation . Please contact us . We will adjust (correct author information ,or delete content ) as soon as possible .