9. Zmontowanie Aplikacji

Na zakończenie zaaplikujemy zdobytą wiedzę do naszej przykładowej aplikacji i zaimplementujemy klienta oraz serwer HTTP za pomocą czysto funkcyjnej biblioteki http4s.

Kod źródłowy drone-dynamic-agents jest dostępny wraz z kodem źródłowym tej książki na https://github.com/fommil/fpmortals w folderze examples. Obecność przy komputerze w trakcie lektury tego rozdziału nie jest co prawda obowiązkowa, ale wielu czytelników może zechcieć śledzić kod źródłowy wraz z tekstem tego rozdziału.

Niektóre części aplikacji pozostały niezaimplementowane i pozostawione jako ćwiczenie dla czytelnika. Więcej instrukcji znajdziesz w README.

9.1 Przegląd

Nasza główna aplikacja wymaga jedynie implementacji algebry DynAgents.

  trait DynAgents[F[_]] {
    def initial: F[WorldView]
    def update(old: WorldView): F[WorldView]
    def act(world: WorldView): F[WorldView]
  }

Mamy już taką implementację w postaci DynAgentsModule, ale wymaga ona implementacji algebr Drone i Machines, które z kolei wymagają algebr JsonClient, LocalClock i Oauth2, itd., itd., itd.

Przydatnym bywa spojrzenie z lotu ptaka na wszystkie algebry, moduły i interpretery naszej aplikacji. Oto jak ułożony jest nasz kod źródłowy:

  ├── dda
  │   ├── algebra.scala
  │   ├── DynAgents.scala
  │   ├── main.scala
  │   └── interpreters
  │       ├── DroneModule.scala
  │       └── GoogleMachinesModule.scala
  ├── http
  │   ├── JsonClient.scala
  │   ├── OAuth2JsonClient.scala
  │   ├── encoding
  │   │   ├── UrlEncoded.scala
  │   │   ├── UrlEncodedWriter.scala
  │   │   ├── UrlQuery.scala
  │   │   └── UrlQueryWriter.scala
  │   ├── oauth2
  │   │   ├── Access.scala
  │   │   ├── Auth.scala
  │   │   ├── Refresh.scala
  │   │   └── interpreters
  │   │       └── BlazeUserInteraction.scala
  │   └── interpreters
  │       └── BlazeJsonClient.scala
  ├── os
  │   └── Browser.scala
  └── time
      ├── Epoch.scala
      ├── LocalClock.scala
      └── Sleep.scala

Sygnatury wszystkich algebr możemy podsumować jako

  trait Sleep[F[_]] {
    def sleep(time: FiniteDuration): F[Unit]
  }
  
  trait LocalClock[F[_]] {
    def now: F[Epoch]
  }
  
  trait JsonClient[F[_]] {
    def get[A: JsDecoder](
      uri: String Refined Url,
      headers: IList[(String, String)]
    ): F[A]
  
    def post[P: UrlEncodedWriter, A: JsDecoder](
      uri: String Refined Url,
      payload: P,
      headers: IList[(String, String)]
    ): F[A]
  }
  
  trait Auth[F[_]] {
    def authenticate: F[CodeToken]
  }
  trait Access[F[_]] {
    def access(code: CodeToken): F[(RefreshToken, BearerToken)]
  }
  trait Refresh[F[_]] {
    def bearer(refresh: RefreshToken): F[BearerToken]
  }
  trait OAuth2JsonClient[F[_]] {
    // same methods as JsonClient, but doing OAuth2 transparently
  }
  
  trait UserInteraction[F[_]] {
    def start: F[String Refined Url]
    def open(uri: String Refined Url): F[Unit]
    def stop: F[CodeToken]
  }
  
  trait Drone[F[_]] {
    def getBacklog: F[Int]
    def getAgents: F[Int]
  }
  
  trait Machines[F[_]] {
    def getTime: F[Epoch]
    def getManaged: F[NonEmptyList[MachineNode]]
    def getAlive: F[MachineNode ==>> Epoch]
    def start(node: MachineNode): F[Unit]
    def stop(node: MachineNode): F[Unit]
  }

Zauważ, że niektóre sygnatury z poprzednich rozdziałów zostały przerefaktorowane tak, aby używały typów danych ze Scalaz, skoro już wiemy, że są lepsze od tych z biblioteki standardowej.

Definiowane typy danych to:

  @xderiving(Order, Arbitrary)
  final case class Epoch(millis: Long) extends AnyVal
  
  @deriving(Order, Show)
  final case class MachineNode(id: String)
  
  @deriving(Equal, Show)
  final case class CodeToken(token: String, redirect_uri: String Refined Url)
  
  @xderiving(Equal, Show, ConfigReader)
  final case class RefreshToken(token: String) extends AnyVal
  
  @deriving(Equal, Show, ConfigReader)
  final case class BearerToken(token: String, expires: Epoch)
  
  @deriving(ConfigReader)
  final case class OAuth2Config(token: RefreshToken, server: ServerConfig)
  
  @deriving(ConfigReader)
  final case class AppConfig(drone: BearerToken, machines: OAuth2Config)
  
  @xderiving(UrlEncodedWriter)
  final case class UrlQuery(params: IList[(String, String)]) extends AnyVal

Oraz typeklasy:

  @typeclass trait UrlEncodedWriter[A] {
    def toUrlEncoded(a: A): String Refined UrlEncoded
  }
  @typeclass trait UrlQueryWriter[A] {
    def toUrlQuery(a: A): UrlQuery
  }

Derywujemy przydatne typeklasy używając scalaz-deriving oraz Magnolii. ConfigReader pochodzi z biblioteki pureconfig i służy do odczytywania konfiguracji z plików HOCON.

Przeanalizujmy też, bez zaglądania do implementacji, jak kształtuje się graf zależności w DynAgentsModule.

  final class DynAgentsModule[F[_]: Applicative](
    D: Drone[F],
    M: Machines[F]
  ) extends DynAgents[F] { ... }
  
  final class DroneModule[F[_]](
    H: OAuth2JsonClient[F]
  ) extends Drone[F] { ... }
  
  final class GoogleMachinesModule[F[_]](
    H: OAuth2JsonClient[F]
  ) extends Machines[F] { ... }

Dwa moduły implementują OAuth2JsonClient, jeden używa algebry Refresh dla usług Google’a, a drugi niewygasającego BearerToken dla `Drone’a.

  final class OAuth2JsonClientModule[F[_]](
    token: RefreshToken
  )(
    H: JsonClient[F],
    T: LocalClock[F],
    A: Refresh[F]
  )(
    implicit F: MonadState[F, BearerToken]
  ) extends OAuth2JsonClient[F] { ... }
  
  final class BearerJsonClientModule[F[_]: Monad](
    bearer: BearerToken
  )(
    H: JsonClient[F]
  ) extends OAuth2JsonClient[F] { ... }

Do tej pory widzieliśmy wymagania względem F mówiące, że musimy dostarczyć Applicative[F], Monad[F] oraz MonadState[F, BearerToken]. Wszystkie te wymagania spełnia StateT[Task, BearerToken, ?] co pozwala nam uczynić ten typ kontekstem naszej aplikacji.

Jednak niektóre algebry mają interpretery używające bezpośrednio typu Task

  final class LocalClockTask extends LocalClock[Task] { ... }
  final class SleepTask extends Sleep[Task] { ... }

Przypomnijmy, że nasze algebry mogą dostarczać liftM w swoich obiektach towarzyszących (patrz rozdział 7.4 na temat Biblioteki Transformatorów Monad), co pozwala nam wynieść LocalClock[Task] do pożądanego StateT[Task, BearerToken, ?] czyniąc wszystko idealnie spójnym.

Niestety to nie koniec. Sprawy komplikują się na następnej warstwie, gdyż JsonClient posiada interpreter używający innego kontekstu

  final class BlazeJsonClient[F[_]](H: Client[Task])(
    implicit
    F: MonadError[F, JsonClient.Error],
    I: MonadIO[F, Throwable]
  ) extends JsonClient[F] { ... }
  object BlazeJsonClient {
    def apply[F[_]](
      implicit
      F: MonadError[F, JsonClient.Error],
      I: MonadIO[F, Throwable]
    ): Task[JsonClient[F]] = ...
  }

Zauważ, że konstruktor BlazeJsonClient zwraca Task[JsonClient[F]], a nie JsonClient[F]. Dzieje się tak, ponieważ stworzenie tego klient powoduje efekt w postaci utworzenia mutowalnej puli połączeń zarządzanej wewnętrznie przez http4s.

Nie możemy zapomnieć o dostarczeniu RefreshToken dla GoogleMachinesModule. Moglibyśmy zrzucić to zadanie na użytkownika, ale jesteśmy mili i dostarczamy osobną aplikację, która używając algebr Auth i Access rozwiązuje ten problem. Implementacje AuthModule i AccessModule niosą ze sobą kolejne wymagania, ale na szczęście żadnych zmian co do kontekstu F[_].

  final class AuthModule[F[_]: Monad](
    config: ServerConfig
  )(
    I: UserInteraction[F]
  ) extends Auth[F] { ... }
  
  final class AccessModule[F[_]: Monad](
    config: ServerConfig
  )(
    H: JsonClient[F],
    T: LocalClock[F]
  ) extends Access[F] { ... }
  
  final class BlazeUserInteraction private (
    pserver: Promise[Void, Server[Task]],
    ptoken: Promise[Void, String]
  ) extends UserInteraction[Task] { ... }
  object BlazeUserInteraction {
    def apply(): Task[BlazeUserInteraction] = ...
  }

Interpreter algebry UserInteraction jest najbardziej skomplikowanym elementem naszego kodu. Startuje on serwer HTTP, prosi użytkownika o otworzenie strony w przeglądarce, odbiera wywołanie zwrotne w serwerze i zwraca wynik jednocześnie zakańczając pracę serwera w bezpieczny sposób.

Zamiast używać StateT do zarządzania tym stanem użyliśmy typu Promise (pochodzącego z ioeffect). Powinniśmy zawsze używać Promise (lub IORef) zamiast StateT, gdy piszemy interpreter oparty o IO, gdyż pozwala nam to opanować abstrakcje. Gdybyśmy użyli StateT, to nie tylko miałoby to wpływ na całą aplikacje, ale również powodowałoby wyciek lokalnego stanu do głównej aplikacji, która musiałaby przejąc odpowiedzialność za dostarczenie inicjalnej wartości. W tym wypadku nie moglibyśmy użyć StateT również dlatego, że potrzebujemy możliwości “czekania”, którą daje nam jedynie Promise.

9.2 Main

Najbrzydsza część FP pojawia się, gdy musimy sprawić by wszystkie monady się zgadzały. Najczęściej ma to miejsce w punkcie wejściowym naszej aplikacji, czyli klasie Main.

Nasza główna pętla wyglądała tak

  state = initial()
  while True:
    state = update(state)
    state = act(state)

Dobra wiadomość jest taka, że teraz ten kod będzie wyglądał tak:

  for {
    old     <- F.get
    updated <- A.update(old)
    changed <- A.act(updated)
    _       <- F.put(changed)
    _       <- S.sleep(10.seconds)
  } yield ()

gdzie F przechowuje stan świata w MonadState[F, WoldView]. Możemy zamknąć ten fragment w metodzie .step i powtarzać ją w nieskończoność wywołując .step[F].forever[Unit].

W tym momencie mamy do wyboru dwa podejścia i oba omówimy. Pierwszym i jednocześnie najprostszym jest skonstruowanie stosu monad kompatybilnego ze wszystkimi algebrami, a każda z nich musi definiować liftM, aby wynieść ją do większego stosu.

Kod, który chcemy napisać dla trybu jednorazowego uwierzytelnienia to

  def auth(name: String): Task[Unit] = {
    for {
      config    <- readConfig[ServerConfig](name + ".server")
      ui        <- BlazeUserInteraction()
      auth      = new AuthModule(config)(ui)
      codetoken <- auth.authenticate
      client    <- BlazeJsonClient
      clock     = new LocalClockTask
      access    = new AccessModule(config)(client, clock)
      token     <- access.access(codetoken)
      _         <- putStrLn(s"got token: $token")
    } yield ()
  }.run

gdzie .readConfig i .putStrLn to wywołania funkcji z bibliotek. Możemy potraktować je jako interpretery oparte o Task dla algebr odczytujących konfigurację i wypisująca ciąg znaków.

Ten kod jednak się nie kompiluje z dwóch powodów. Po pierwsze, musimy zdecydować jak będzie wyglądał nasz stos monad. Konstruktor BlazeJsonClient zwraca Task, ale JsonClientwymaga MonadError[..., JsonClient.Error], co można rozwiązać za pomocą EitherT. Możemy więc skonstruować nasz stos dla całej konstrukcji for jako

  type H[a] = EitherT[Task, JsonClient.Error, a]

Niestety, oznacza to, że musimy wywołać .liftM dla wszystkiego co zwraca Task, co dodaje dość dużo boilerplate’u. Niestety metoda liftM nie przyjmuje typów o kształcie H[_] tylko H[_[_]. _], więc musimy stworzyć alias, który pomoże kompilatorowi:

  type HT[f[_], a] = EitherT[f, JsonClient.Error, a]
  type H[a]        = HT[Task, a]

możemy teraz wywołać .liftM[HT] kiedy dostajemy Task

  for {
    config    <- readConfig[ServerConfig](name + ".server").liftM[HT]
    ui        <- BlazeUserInteraction().liftM[HT]
    auth      = new AuthModule(config)(ui)
    codetoken <- auth.authenticate.liftM[HT]
    client    <- BlazeJsonClient[H].liftM[HT]
    clock     = new LocalClockTask
    access    = new AccessModule(config)(client, clock)
    token     <- access.access(codetoken)
    _         <- putStrLn(s"got token: $token").liftM[HT]
  } yield ()

Ale nasz kod nadal się nie kompiluje. Tym razem dlatego, że clock jest typu LocalClock[Task] a AccessModule wymaga LocalClock[H]. Dodajmy więc potrzebny boilerplate .liftM do obiektu towarzyszącego LocalClock i wynieśmy całą algebrę

  clock     = LocalClock.liftM[Task, HT](new LocalClockTask)

Wreszcie wszystko się kompiluje!

Drugie podejście do zmontowywania aplikacji jest bardziej złożone, ale niezbędne, gdy pojawiają się konflikty w stosie monad, tak jak w naszej głównej pętli. Jeśli przeanalizujemy wymagania, zobaczymy, że potrzebujemy poniższych instancji:

  • MonadError[F, JsonClient.Error] w JsonClient
  • MonadState[F, BearerToken] w OAuth2JsonClient
  • MonadState[F, WorldView] w głównej pętli

Niestety, dwa wymagania na MonadState są ze sobą sprzeczne. Moglibyśmy skonstruować typ danych, który przechowuje cały stan aplikacji, ale byłaby to cieknąca abstrakcja. Zamiast tego zagnieździmy konstrukcję for i dostarczymy stan tam gdzie jest potrzebny

Musimy teraz przemyśleć trzy warstwy, które nazwiemy F, G i H

  type HT[f[_], a] = EitherT[f, JsonClient.Error, a]
  type GT[f[_], a] = StateT[f, BearerToken, a]
  type FT[f[_], a] = StateT[f, WorldView, a]
  
  type H[a]        = HT[Task, a]
  type G[a]        = GT[H, a]
  type F[a]        = FT[G, a]

Teraz złe wieści: liftM obsługuje tylko jedną warstwę na raz. Jeśli mamy Task[A], a chcemy uzyskać F[A] to musimy przejść przez wszystkie kroki i wywołać ta.liftM[HT].liftM[GT].liftM[FT]. Podobnie, gdy wynosimy algebry, musimy zawołać liftM wielokrotnie. Aby uzyskać Sleep[F], musimy napisać

  val S: Sleep[F] = {
    import Sleep.liftM
    liftM(liftM(liftM(new SleepTask)))
  }

a żeby dostać LocalClock[G] robimy dwa wyniesienia

  val T: LocalClock[G] = {
    import LocalClock.liftM
    liftM(liftM(new LocalClockTask))
  }

Główna aplikacja wygląda więc tak:

  def agents(bearer: BearerToken): Task[Unit] = {
    ...
    for {
      config <- readConfig[AppConfig]
      blaze  <- BlazeJsonClient[G]
      _ <- {
        val bearerClient = new BearerJsonClientModule(bearer)(blaze)
        val drone        = new DroneModule(bearerClient)
        val refresh      = new RefreshModule(config.machines.server)(blaze, T)
        val oauthClient =
          new OAuth2JsonClientModule(config.machines.token)(blaze, T, refresh)
        val machines = new GoogleMachinesModule(oauthClient)
        val agents   = new DynAgentsModule(drone, machines)
        for {
          start <- agents.initial
          _ <- {
            val fagents = DynAgents.liftM[G, FT](agents)
            step(fagents, S).forever[Unit]
          }.run(start)
        } yield ()
      }.eval(bearer).run
    } yield ()
  }

gdzie zewnętrzna pętla używa Task, środkowa G a wewnętrzna F.

Wywołania .run(start) oraz .eval(bearer) dostarczają inicjalny stan dla części bazujących na StateT. .run z kolei pokazuje błędy zgromadzone w EitherT.

Na koniec wołamy te dwie aplikacji z naszej instancji SafeApp

  object Main extends SafeApp {
    def run(args: List[String]): IO[Void, ExitStatus] = {
      if (args.contains("--machines")) auth("machines")
      else agents(BearerToken("<invalid>", Epoch(0)))
    }.attempt[Void].map {
      case \/-(_)   => ExitStatus.ExitNow(0)
      case -\/(err) => ExitStatus.ExitNow(1)
    }
  }

i uruchamiamy ją!

  > runMain fommil.dda.Main --machines
  [info] Running (fork) fommil.dda.Main --machines
  ...
  [info] Service bound to address /127.0.0.1:46687
  ...
  [info] Created new window in existing browser session.
  ...
  [info] Headers(Host: localhost:46687, Connection: keep-alive, User-Agent: Mozilla/5.0 ...)
  ...
  [info] POST https://www.googleapis.com/oauth2/v4/token
  ...
  [info] got token: "<elided>"

Hurra!

9.3 Blaze

Server i klienta HTTP zaimplementujemy z użyciem zewnętrznej biblioteki http4s. Interpretery dla odpowiednich algebr dostaną w związku z tym prefiks Blaze, gdyż tak też nazywa się właściwy komponent tej biblioteki.

Dodajemy poniższe zależności

  val http4sVersion = "0.18.16"
  libraryDependencies ++= Seq(
    "org.http4s"            %% "http4s-dsl"          % http4sVersion,
    "org.http4s"            %% "http4s-blaze-server" % http4sVersion,
    "org.http4s"            %% "http4s-blaze-client" % http4sVersion
  )

9.3.1 BlazeJsonClient

Będziemy potrzebować kilku dodatkowych importów

  import org.http4s
  import org.http4s.{ EntityEncoder, MediaType }
  import org.http4s.headers.`Content-Type`
  import org.http4s.client.Client
  import org.http4s.client.blaze.{ BlazeClientConfig, Http1Client }

Moduł Client może być podsumowany jako

  final class Client[F[_]](
    val shutdown: F[Unit]
  )(implicit F: MonadError[F, Throwable]) {
    def fetch[A](req: Request[F])(f: Response[F] => F[A]): F[A] = ...
    ...
  }

gdzie Request i Response to typy danych:

  final case class Request[F[_]](
    method: Method
    uri: Uri,
    headers: Headers,
    body: EntityBody[F]
  ) {
    def withBody[A](a: A)
                   (implicit F: Monad[F], A: EntityEncoder[F, A]): F[Request[F]] = ...
    ...
  }
  
  final case class Response[F[_]](
    status: Status,
    headers: Headers,
    body: EntityBody[F]
  )

składające się z

  final case class Headers(headers: List[Header])
  final case class Header(name: String, value: String)
  
  final case class Uri( ... )
  object Uri {
    // not total, only use if `s` is guaranteed to be a URL
    def unsafeFromString(s: String): Uri = ...
    ...
  }
  
  final case class Status(code: Int) {
    def isSuccess: Boolean = ...
    ...
  }
  
  type EntityBody[F[_]] = fs2.Stream[F, Byte]

EntityBody jest aliasem na typ Stream z biblioteki fs2. Możemy rozumieć go jako leniwy strumień danych wykonujący efekty, bazujący na wyciąganiu danych (pull-based). Zaimplementowany jest jako monada Free z dodatkowym łapaniem wyjątków i obsługą przerwań. Stream przyjmuje dwa parametry typu: typ efektów i typ zawartości. Dodatkowo posiada wewnątrz wydajną reprezentację pozwalającą na łączenie danych (batching), więc przykładowo, używając Stream[F, Byte] tak naprawdę mamy do czynienia z opakowaną tablicą Array[Byte], która przybywa do nas za pośrednictwem sieci.

Musimy przekonwertować nasze reprezentacje nagłówków i URLi na wersje wymagane przez http4s:

  def convert(headers: IList[(String, String)]): http4s.Headers =
    http4s.Headers(
      headers.foldRight(List[http4s.Header]()) {
        case ((key, value), acc) => http4s.Header(key, value) :: acc
      }
    )
  
  def convert(uri: String Refined Url): http4s.Uri =
    http4s.Uri.unsafeFromString(uri.value) // we already validated our String

Obie nasze metody .get i .post muszą przekonwertować instancję Response pochodząca z http4s na typ A. Możemy wydzielić tę logikę do pojedynczej funkcji .handler

  import JsonClient.Error
  
  final class BlazeJsonClient[F[_]] private (H: Client[Task])(
    implicit
    F: MonadError[F, Error],
    I: MonadIO[F, Throwable]
  ) extends JsonClient[F] {
    ...
    def handler[A: JsDecoder](resp: http4s.Response[Task]): Task[Error \/ A] = {
      if (!resp.status.isSuccess)
        Task.now(JsonClient.ServerError(resp.status.code).left)
      else
        for {
          text <- resp.body.through(fs2.text.utf8Decode).compile.foldMonoid
          res = JsParser(text)
            .flatMap(_.as[A])
            .leftMap(JsonClient.DecodingError(_))
        } yield res
    }
  }

through(fs2.text.utf8Decode) pozwala przekonwertować Stream[Task, Byte] na Stream[Task, String]. compile.foldMonoid interpretuje strumień z użyciem naszego Taska i łączy wyniki przy pomocy Monoid[String], zwracając Task[String].

Następnie parsujemy string do JSONa, a JsDecoder[A]dostarcza potrzebny rezultat.

Oto nasza implementacja .get

  def get[A: JsDecoder](
    uri: String Refined Url,
    headers: IList[(String, String)]
  ): F[A] =
    I.liftIO(
        H.fetch(
          http4s.Request[Task](
            uri = convert(uri),
            headers = convert(headers)
          )
        )(handler[A])
      )
      .emap(identity)

Trzeba przyznać, że jest to w 100% łączenie istniejących kawałków. Konwertujemy nasze typy wejściowe do http4s.Request, wołamy .fetch na kliencie przekazując nasz handler, w odpowiedzi dostajemy Task[Error \/ A]. Musimy jednak zwrócić F[A], więc używamy MonadIO.liftIO do stworzenia F[Error \/ ], na którym z kolei wywołujemy emap umieszczając błąd wewnątrz F.

Niestety, próba skompilowania tego kodu zakończy się porażką, a błąd będzie wyglądał mniej więcej tak:

  [error] BlazeJsonClient.scala:95:64: could not find implicit value for parameter
  [error]  F: cats.effect.Sync[scalaz.ioeffect.Task]

Coś na temat zaginionego kota?

Dzieje się tak, gdyż http4s używa innej biblioteki wspomagającej FP niż Scalaz. Na szczęście scalaz-ioeffect dostarcza warstwę dodającą kompatybilność z tą biblioteką, a projekt shims definiuje niezauważalne (zazwyczaj) niejawne konwersje. Tak więc możemy sprawić, że nasz kod zacznie się kompilować dodając zależności

  libraryDependencies ++= Seq(
    "com.codecommit" %% "shims"                % "1.4.0",
    "org.scalaz"     %% "scalaz-ioeffect-cats" % "2.10.1"
  )

i importy

  import shims._
  import scalaz.ioeffect.catz._

Implementacja .post jest podobna, ale musimy jeszcze dostarczyć instancję

  EntityEncoder[Task, String Refined UrlEncoded]

Na szczęście typeklasa EntityEncoder pozwala nam łatwo ją wyderywować z istniejącego enkodera dla typu String

  implicit val encoder: EntityEncoder[Task, String Refined UrlEncoded] =
    EntityEncoder[Task, String]
      .contramap[String Refined UrlEncoded](_.value)
      .withContentType(
        `Content-Type`(MediaType.`application/x-www-form-urlencoded`)
      )

Jedyną różnicą między .get i .post jest sposób w jaki konstruujemy http4s.Request

  http4s.Request[Task](
    method = http4s.Method.POST,
    uri = convert(uri),
    headers = convert(headers)
  )
  .withBody(payload.toUrlEncoded)

Ostatnim fragmentem układanki jest konstruktor, w którym wywołujemy Http1Client przekazując obiekt konfiguracyjny

  object BlazeJsonClient {
    def apply[F[_]](
      implicit
      F: MonadError[F, JsonClient.Error],
      I: MonadIO[F, Throwable]
    ): Task[JsonClient[F]] =
      Http1Client(BlazeClientConfig.defaultConfig).map(new BlazeJsonClient(_))
  }

9.3.2 BlazeUserInteraction

Musimy uruchomić serwer HTTP, co jest dużo łatwiejsze niż może się wydawać. Po pierwsze, importy

  import org.http4s._
  import org.http4s.dsl._
  import org.http4s.server.Server
  import org.http4s.server.blaze._

Następnie musimy utworzyć dsl dla naszego typu efektów, z którego zaimportujemy zawartość

  private val dsl = new Http4sDsl[Task] {}
  import dsl._

Teraz możemy używać dsla http4s do obsługi żądań HTTP. Zamiast opisywać wszystko co jest możliwe zaimplementujemy po prostu pojedynczą końcówkę (endpoint), która przypomina każdy inny DSL HTTP

  private object Code extends QueryParamDecoderMatcher[String]("code")
  private val service: HttpService[Task] = HttpService[Task] {
    case GET -> Root :? Code(code) => ...
  }

Każde dopasowanie musi zwrócić Task[Response[Task]]. W naszym przypadku chcemy wziąć code i ukończyć obietnicę ptoken:

  final class BlazeUserInteraction private (
    pserver: Promise[Throwable, Server[Task]],
    ptoken: Promise[Throwable, String]
  ) extends UserInteraction[Task] {
    ...
    private val service: HttpService[Task] = HttpService[Task] {
      case GET -> Root :? Code(code) =>
        ptoken.complete(code) >> Ok(
          "That seems to have worked, go back to the console."
        )
    }
    ...
  }

ale zdefiniowanie logiki nie wystarczy, musimy jeszcze uruchomić nasz serwer, co też zrobimy używając BlazeBuilder

  private val launch: Task[Server[Task]] =
    BlazeBuilder[Task].bindHttp(0, "localhost").mountService(service, "/").start

Przypisanie do portu 0 sprawia, że system operacyjny użyje tymczasowego portu, który możemy odczytać z pola server.address.

Nasza implementacja .start i .stop jest więc bardzo prosta

  def start: Task[String Refined Url] =
    for {
      server  <- launch
      updated <- pserver.complete(server)
      _ <- if (updated) Task.unit
           else server.shutdown *> fail("server was already running")
    } yield mkUrl(server)
  
  def stop: Task[CodeToken] =
    for {
      server <- pserver.get
      token  <- ptoken.get
      _      <- IO.sleep(1.second) *> server.shutdown
    } yield CodeToken(token, mkUrl(server))
  
  private def mkUrl(s: Server[Task]): String Refined Url = {
    val port = s.address.getPort
    Refined.unsafeApply(s"http://localhost:${port}/")
  }
  private def fail[A](s: String): String =
    Task.fail(new IOException(s) with NoStackTrace)

Uśpienie wątku na 1.second jest niezbędne, aby uniknąć wyłączenia serwera zanim odpowiedź trafi z powrotem do przeglądarki. Z wydajnością współbieżności IO nie ma żartów!

W końcu, aby utworzyć BlazeUserInteraction potrzebuje jedynie dwóch niezainicjalizowanych obietnic

  object BlazeUserInteraction {
    def apply(): Task[BlazeUserInteraction] = {
      for {
        p1 <- Promise.make[Void, Server[Task]].widenError[Throwable]
        p2 <- Promise.make[Void, String].widenError[Throwable]
      } yield new BlazeUserInteraction(p1, p2)
    }
  }

Mogliśmy użyć IO[Void, ?], ale skoro reszta naszej aplikacji używa Task (czyli IO[Throwable, ?]), wywołujemy .widenError, aby nie wprowadzać zbędnego boilerplate’u.

9.4 Podziękowania

I to tyle! Gratulujemy dotarcia do końca podróży.

Jeśli w trakcie jej trwania nauczyłeś się czegoś, to proszę, powiedz o tym swoim znajomym. Ta książka nie ma działu marketingu, więc jest to jedyny sposób w jaki potencjalni czytelnicy mogą się o niej dowiedzieć.

Aby zaangażować się w rozwój Scalaz wystarczy dołączyć do pokoju na gitterze. Stamtąd możesz zadawać pytania, pomagać innym (teraz jesteś ekspertem!) i pomagać w tworzeniu kolejnych wersji biblioteki.