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 で表現
  • effectSync[F] を持つ任意の F[_] が使える(典型的には IOTask など)。
  • 既存の HTTP クライアントをベースで利用していて、それぞれの Free Monad インタープリターが付属モジュールとして提供される。JVM 用だと、Apach HttpComponent Client, HttpAsyncClient, Akka HTTP Client API から選択可能。

サンプル実装

公式サイトのサンプル をベースにして、Cats EffectMonixIota などの要素を取り入れて実装してみる。

使ったバージョンなど

  • hammock: "0.9.0"
  • circe: "0.10.0"
  • monix: "3.0.0-RC2"
  • iota: "0.3.10"

一番簡単なサンプル

一般に Free Monad を使った Scala プログラムは、下のような部分からなる。

  1. 代数: いわゆる Algebra の定義
  2. リフト/スマートコンストラクタ: Algebra から Free Monad へのリフトとスマートコンストラクタ
  3. インタプリタ: Free Monad から他のモナドへの変換
  4. 記述: Algebraを使ったプログラムの記述
  5. 実行: プログラムにインタプリタを与えて具体的に実行する部分

このうち、Hammock では、(1) の代数(HttpF, MarshallF)、(2) リフト/スマートコンストラクタ、(3)ベース HTTPクライアントごとの各インタプリタが、モジュールとして提供される。

(4)のプログラムの記述は、Hammock のユーティリティメソッドを使って、動詞、URL、ヘッダーなどから Free[HttpF, HttpResponse] を得た上で、必要に応じてレスポンスをデコード→アンマーシャルしたり、mapflatMap して変換したりといった操作を追加して組み立てる。

もっとも簡単な例は、以下のようなものになる。

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 の HttpFMarshallF を全て合成した型 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))

HttpFMarshallF のスマートコンストラクタは 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が返ってくる。

ソース

補足と所感

  • ここでは触れなかったが、circemonocle を使ったリクエスト/レスポンスの操作・DSLも提供されている。

  • 上の Free Monad のサンプルでは、やや多めに Free Monad を使うコードを書いてみたが、Hammock.request(...) 等で Free[F, HttpResponse] を得た直後に .exec[IO].exec[Task]をつなげて IOTask に変換すると、逆に Free Monad の使用をあえて見せないようなコーディングもできる。

  • Scala 界隈での、Free Monad の全盛期といえば、多分『Functional and Reactive Domain Modeling』が出版された 2016 年ごろかなと個人的には思っていて、今思えばその頃はファッション的に濫用されすぎていた気がしないでもない。その後 Tagless Final の台頭とともに、Free Monad が徐々にオワコン化してきた一方で、むしろ濫用が収まって代数化のメリットがデメリット(特にボイラープレートなど)を普通に上回るような、正しい使い方が残ってきたのかなと思う3。この Hammock も、そうした良い Free Monad の活用という感触はあった。


  1. ちなみにここで言う Inject[A, B]InjectK[A, B]とは、「Open Union 型 B[_]A[_] が含まれる」といった事実を示すエビデンス的な意味で DI 的な意味での Inject とは関係ない。 

  2. JSONPlaceholder というテスト用のフェイク REST API サイトを使用。 

  3. この辺りの記事では、そうした良い Free Monad の例として Slick の DBIOAction や doobie の ConnectionIO などが挙げられている。