1. IO データ型

IO データ型 

Launchbury と SPJ が State Thread を用いたように、Cats Effect はライトウェイトなスレッド的概念であるファイバーと呼ばれるものを使ってエフェクトをモデル化する。

sealed abstract class IO[+A] private () extends IOPlatform[A] {

  def flatMap[B](f: A => IO[B]): IO[B] = IO.FlatMap(this, f)

  ....


  // from IOPlatform
  final def unsafeRunSync()(implicit runtime: unsafe.IORuntime): A
}

object IO extends IOCompanionPlatform with IOLowPriorityImplicits {
  /**
   * Suspends a synchronous side effect in `IO`.
   *
   * Alias for `IO.delay(body)`.
   */
  def apply[A](thunk: => A): IO[A] = Delay(() => thunk)

  def delay[A](thunk: => A): IO[A] = apply(thunk)

  def async[A](k: (Either[Throwable, A] => Unit) => IO[Option[IO[Unit]]]): IO[A] =
    asyncForIO.async(k)

  def async_[A](k: (Either[Throwable, A] => Unit) => Unit): IO[A] =
    asyncForIO.async_(k)

  def canceled: IO[Unit] = Canceled

  def cede: IO[Unit] = Cede

  def sleep(delay: FiniteDuration): IO[Unit] =
    Sleep(delay)

  def race[A, B](left: IO[A], right: IO[B]): IO[Either[A, B]] =
    asyncForIO.race(left, right)

  def readLine: IO[String] =
    Console[IO].readLine

  def print[A](a: A)(implicit S: Show[A] = Show.fromToString[A]): IO[Unit] =
    Console[IO].print(a)

  def println[A](a: A)(implicit S: Show[A] = Show.fromToString[A]): IO[Unit] =
    Console[IO].println(a)

  def blocking[A](thunk: => A): IO[A] =
    Blocking(TypeBlocking, () => thunk)

  def interruptible[A](many: Boolean)(thunk: => A): IO[A] =
    Blocking(if (many) TypeInterruptibleMany else TypeInterruptibleOnce, () => thunk)

  def suspend[A](hint: Sync.Type)(thunk: => A): IO[A] =
    if (hint eq TypeDelay)
      apply(thunk)
    else
      Blocking(hint, () => thunk)

  ....
}

Hello world 

以下は Cats Effect IO を用いた hello world のプログラムだ。

package example

import cats._, cats.syntax.all._
import cats.effect.IO

object Hello extends App {
  val program = for {
    _ <- IO.print("What's your name? ")
    x <- IO.readLine
    _ <- IO.println(s"Hello, $x")
  } yield ()
}

実行するとこのようになる:

> run
[info] running example.Hello
[success] Total time: 1 s, completed Apr 11, 2021 12:51:44 PM

何も起こらなかったはずだ。標準ライブラリの scala.concurrent.Future + 普通の ExecutionContext と違って、IO データ型は停止状態のエフェクトを表し、明示的に実行するまで実行されない。

以下のように走らせることができる:

package example

import cats._, cats.syntax.all._
import cats.effect.IO

object Hello extends App {
  val program = for {
    _ <- IO.print("What's your name? ")
    x <- IO.readLine
    _ <- IO.println(s"Hello, $x")
  } yield ()

  import cats.effect.unsafe.implicits.global
  program.unsafeRunSync()
}

これで副作用が見えるようになった:

sbt:herding-cats> run
[info] running example.Hello
What's your name? eugene
Hello, eugene
[success] Total time: 4 s, completed Apr 11, 2021 1:00:19 PM

実際のプログラムを書くときは IOApp というより良いプログラムハーネスがあるので、それを使う:

import cats._, cats.syntax.all._
import cats.effect.{ ExitCode, IO, IOApp }

object Hello extends IOApp {
  override def run(args: List[String]): IO[ExitCode] =
    program.as(ExitCode.Success)

  lazy val program = for {
    _ <- IO.print("What's your name? ")
    x <- IO.readLine
    _ <- IO.println(s"Hello, $x")
  } yield ()
}

これらの例は IO データ型がモナディックに合成可能であることを例示するが、実行は逐次的だ。

Pizza app 

もう少し IO の何が嬉しいのかを示すために、http4s client を使ったピザアプリを考える。

import cats._, cats.syntax.all._
import cats.effect.IO
import org.http4s.client.Client

def withHttpClient[A](f: Client[IO] => IO[A]): IO[A] = {
  import java.util.concurrent.Executors
  import scala.concurrent.ExecutionContext
  import org.http4s.client.blaze.BlazeClientBuilder
  val threadPool = Executors.newFixedThreadPool(5)
  val httpEc = ExecutionContext.fromExecutor(threadPool)
  BlazeClientBuilder[IO](httpEc).resource.use(f)
}

def search(httpClient: Client[IO], q: String): IO[String] = {
  import io.circe.Json
  import org.http4s.Uri
  import org.http4s.circe._
  val baseUri = Uri.unsafeFromString("https://api.duckduckgo.com/")
  val target = baseUri
    .withQueryParam("q", q + " pizza")
    .withQueryParam("format", "json")
  httpClient.expect[Json](target) map { json =>
    json.findAllByKey("Abstract").headOption.flatMap(_.asString).getOrElse("")
  }
}

{
  import cats.effect.unsafe.implicits.global
  val program = withHttpClient { httpClient =>
    search(httpClient, "New York")
  }
  program.unsafeRunSync()
}
// res0: String = "New York\u2013style pizza is pizza made with a characteristically large hand-tossed thin crust, often sold in wide slices to go. The crust is thick and crisp only along its edge, yet soft, thin, and pliable enough beneath its toppings to be folded in half to eat. Traditional toppings are simply tomato sauce and shredded mozzarella cheese. This style evolved in the U.S. from the pizza that originated in New York City in the early 1900s, itself derived from the Neapolitan-style pizza made in Italy. Today it is the dominant style eaten in the New York Metropolitan Area states of New York, and New Jersey and variously popular throughout the United States. Regional variations exist throughout the Northeast and elsewhere in the U.S."

これは Duck Duck Go API に New York スタイルのピザのクエリをする。ネットワーク IO によるレイテンシーを低下させるために、並列呼び出しをしたい:

{
  import cats.effect.unsafe.implicits.global
  val program = withHttpClient { httpClient =>
    val xs = List("New York", "Neapolitan", "Sicilian", "Chicago", "Detroit", "London")
    xs.parTraverse(search(httpClient, _))
  }
  program.unsafeRunSync()
}
// res1: List[String] = List(
//   "New York\u2013style pizza is pizza made with a characteristically large hand-tossed thin crust, often sold in wide slices to go. The crust is thick and crisp only along its edge, yet soft, thin, and pliable enough beneath its toppings to be folded in half to eat. Traditional toppings are simply tomato sauce and shredded mozzarella cheese. This style evolved in the U.S. from the pizza that originated in New York City in the early 1900s, itself derived from the Neapolitan-style pizza made in Italy. Today it is the dominant style eaten in the New York Metropolitan Area states of New York, and New Jersey and variously popular throughout the United States. Regional variations exist throughout the Northeast and elsewhere in the U.S.",
//   "Neapolitan pizza also known as Naples-style pizza, is a style of pizza made with tomatoes and mozzarella cheese. It must be made with either San Marzano tomatoes or Pomodorino del Piennolo del Vesuvio, which grow on the volcanic plains to the south of Mount Vesuvius, and Mozzarella di Bufala Campana, a protected designation of origin cheese made with the milk from water buffalo raised in the marshlands of Campania and Lazio in a semi-wild state, or \"Mozzarella STG\", a cow's milk mozzarella. Neapolitan pizza is a Traditional Speciality Guaranteed product in Europe, and the art of its making is included on UNESCO's list of intangible cultural heritage. This style of pizza gave rise to the New York-style pizza that was first made by Italian immigrants to the United States in the early 20th century.",
//   "Sicilian pizza is pizza prepared in a manner that originated in Sicily, Italy. Sicilian pizza is also known as sfincione or focaccia with toppings. This type of pizza became a popular dish in western Sicily by the mid-19th century and was the type of pizza usually consumed in Sicily until the 1860s. The version with tomatoes was not available prior to the 17th century. It eventually reached North America in a slightly altered form, with thicker crust and a rectangular shape. Traditional Sicilian pizza is often thick crusted and rectangular, but can also be round and similar to the Neapolitan pizza. It is often topped with onions, anchovies, tomatoes, herbs and strong cheese such as caciocavallo and toma. Other versions do not include cheese. The Sicilian methods of making pizza are linked to local culture and country traditions, so there are differences in preparing pizza among the Sicilian regions of Palermo, Catania, Siracusa and Messina.",
//   "Chicago-style pizza is pizza prepared according to several different styles developed in Chicago. The most famous is the deep-dish pizza. The pan in which it is baked gives the pizza its characteristically high edge which provides ample space for large amounts of cheese and a chunky tomato sauce. Chicago-style pizza may be prepared in deep-dish style and as a stuffed pizza.",
//   "Detroit-style pizza is a rectangular pizza with a thick crust that is crispy and chewy. It is traditionally topped with tomato sauce and Wisconsin brick cheese that goes all the way to the edges. This style of pizza is often baked in rectangular steel trays designed for use as automotive drip pans or to hold small industrial parts in factories. The style was developed during the mid-twentieth century in Detroit before spreading to other parts of the United States in the 2010s. The dish is one of Detroit's iconic local foods.",
//   ""
// )

.parTraverse(...) は内部でファイバーを作成して IO のアクションを並列実行する。並列な IO ができたところで、Ref を使ってみてスレッド安全性を試してみよう。

import cats.effect.Ref

def appendCharCount(httpClient: Client[IO], q: String, ref: Ref[IO, List[(String, Int)]]): IO[Unit] =
  for {
    s <- search(httpClient, q)
    _ <- ref.update(((q, s.size)) :: _)
  } yield ()

{
  import cats.effect.unsafe.implicits.global
  val program = withHttpClient { httpClient =>
    val xs = List("New York", "Neapolitan", "Sicilian", "Chicago", "Detroit", "London")

    for {
      r <- Ref[IO].of(Nil: List[(String, Int)])
      _ <- xs.parTraverse(appendCharCount(httpClient, _, r))
      x <- r.get
    } yield x
  }
  program.unsafeRunSync().reverse
}
// res2: List[(String, Int)] = List(
//   ("Sicilian", 954),
//   ("London", 0),
//   ("Neapolitan", 806),
//   ("Chicago", 376),
//   ("New York", 731),
//   ("Detroit", 530)
// )

ここでは、IO エフェクトの逐次合成と並列合成の両方を組み合わせている。