$30 off During Our Annual Pro Sale. View Details »

Purely Functional Programming with Cats Effect 3 and Scala 3 [ScalaMatsuri2022]

Purely Functional Programming with Cats Effect 3 and Scala 3 [ScalaMatsuri2022]

Toshiyuki Takahashi

March 19, 2022
Tweet

More Decks by Toshiyuki Takahashi

Other Decks in Programming

Transcript

  1. Purely Functional Programming with Cats Effect 3 and Scala 3

  2. Referential Transparency object ReferentialTransparency1: def main(args: Array[String]): Unit = (println("Hello"),

    println("Hello")) object ReferentialTransparency2: val hello = println("Hello") def main(args: Array[String]): Unit = (hello, hello)
  3. Effect You can't run them twice Useful! I/O (File, DB,

    HTTP) State Clock etc. Effect: 2度実行できないもの
  4. Effect should be controlled Hard to read test refactor Often

    block threads 読みづらい テストしづらい リファクタリングがしづらい スレッドをブロックすることも
  5. Example object Console: def readLine(prompt: String): String = scala.io.StdIn.readLine(prompt) def

    printLine(message: String): Unit = println(message) def main(args: Array[String]): Unit = val name = Console.readLine("Enter your name: ") Console.printLine(s"Hello $name") > run Enter your name: World Hello World
  6. Function0 object Console: def readLine(prompt: String): Function0[String] = () =>

    scala.io.StdIn.readLine(prompt) def printLine(message: String): Function0[Unit] = () => println(message) def program: Function0[Unit] = () => val name = Console.readLine("Enter your name: ")() Console.printLine(s"Hello $name")() def main(args: Array[String]): Unit = program()
  7. Function0 case class Lazy[A](f: () => A): def run(): A

    = f() def flatMap[B](f2: A => Lazy[B]): Lazy[B] = Lazy(() => f2(f()).run()) def map[B](f2: A => B): Lazy[B] = Lazy(() => f2(run())) object Console: def readLine(prompt: String): Lazy[String] = Lazy(() => scala.io.StdIn.readLine(prompt)) def printLine(message: String): Lazy[Unit] = Lazy(() => println(message)) def program: Lazy[Unit] = for name <- Console.readLine("Enter your name: ") _ <- Console.printLine(s"Hello $name") yield () def main(args: Array[String]): Unit = program.run()
  8. Cats

  9. Function0 + Cats import cats.* import cats.implicits.* type Lazy[A] =

    Function0[A] object Console: def readLine(prompt: String): Lazy[String] = () => scala.io.StdIn.readLine(prompt) def printLine(message: String): Lazy[Unit] = () => println(message) def program: Lazy[Unit] = for name <- Console.readLine("Enter your name: ") _ <- Console.printLine(s"Hello $name") yield () def main(args: Array[String]): Unit = program()
  10. Monad Sequential computation def flatMap[A, B](fa: F[A])(f: A => F[B]):

    F[B] Monadで順序や依存関係を表現できる
  11. Program as a description Isolation from runtime environment Manipulation the

    description Concurrency 実行環境からの分離 プログラムに対して加工ができる 並行処理
  12. Problems Stack safety Efficiency, Concurrency def stackUnsafeProgram: Lazy[Unit] = 1.to(100000).foldLeft(Lazy[Unit](()

    => ())) { (res, i) => res.flatMap(_ => Lazy(() => println(i))) } def main(args: Array[String]): Unit = stackUnsafeProgram.run() // => StackOverFlowError Monadは実装によってはスタックセーフではない 単純な実装ではパフォーマンスを上げられない
  13. Cats Effect Stack safe Fast

  14. Cats Effect trait Console: def readLine(prompt: String): IO[String] = IO.blocking(scala.io.StdIn.readLine(prompt))

    def printLine(s: String): IO[Unit] = IO.blocking(println(s)) def program(console: Console) = import console._ for name <- readLine("Enter your name: ") _ <- printLine(s"Hello $name") yield () val run = program(new Console {})
  15. Constructors of IO IO has several constructors apply (delay )

    blocking , interruptible async etc. These give hints to cats-effect runtime about how to handle threads cats-effectのコンストラクタはスレッドの扱いについてランタイ ムにヒントを与える役割がある
  16. Thread Model Thread pools CPU-bound Blocking IO Non-blocking IO polling

    typelevel/cats-effect/.../IORuntime.scala final class IORuntime private[unsafe] ( val compute: ExecutionContext, private[effect] val blocking: ExecutionContext, val scheduler: Scheduler, private[effect] val fiberMonitor: FiberMonitor, val shutdown: () => Unit, val config: IORuntimeConfig )
  17. Fibers Lightweight threads Fiber == 軽量スレッド

  18. Fibers Cancelable Important for resource safety val a = Future

    { while ({ Thread.sleep(100); true }) {}; "a" } val b = Future { Thread.sleep(1000); println("b"); "b" } Future.firstCompletedOf(Seq(a, b)).foreach(println) val a = IO.interruptible { while ({ Thread.sleep(100); true }) {}; "a" } val b = IO.interruptible { Thread.sleep(1000); "b" } IO.race(a, b).map(_.merge).flatMap(IO.println) キャンセルができる
  19. Type Classes

  20. Test

  21. Test case class TestConsoleState(input: String, output: List[String]): def read: String

    = input def write(message: String): TestConsoleState = copy(output = output :+ message) test("Hello World") { var testState = TestConsoleState("World", Nil) val console = new Console: override def readLine(prompt: String): IO[String] = IO({ testState = testState.write(prompt) }) >> IO(testState.read) override def printLine(message: String): IO[Unit] = IO({ testState = testState.write(s"$message\n") }) val result = program(console).unsafeRunSync() val expected = TestConsoleState( "World", List("Enter your name: ", "Hello World\n") ) assert(testState === expected) }
  22. Test It's not easy to test with IO. Can we

    replace IO with a different data structure? IOのままではテストがしづらい IOを別のデータ構造に置き換えられないだろうか
  23. Free Monad Describe computation as data Convert algebras into IO

    using natural transformations 計算をデータとして表現する Natural TransformationでIOに変換する
  24. Free Monad sealed trait ConsoleA[A] case class ReadLine() extends ConsoleA[String]

    case class PrintLine(message: String) extends ConsoleA[Unit] def program: Free[ConsoleA, Unit] = for name <- Free.liftF(ReadLine("Enter your name: ")) message <- Free.liftF(PrintLine(s"Hello $name")) yield ()
  25. Free Monad def interpret: ConsoleA ~> IO = new (ConsoleA

    ~> IO): def apply[A](fa: ConsoleA[A]): IO[A] = fa match case ReadLine(prompt) => IO.print(prompt) >> IO.readLine case PrintLine(message) => IO.println(message) val run = program.foldMap(interpret)
  26. Free Monad case class TestConsoleState(input: String, output: List[String]): def read

    = input def write(message: String) = copy(output = output :+ message) def testInterpret: ConsoleA ~> ([A] =>> State[TestConsoleState, A]) = new (ConsoleA ~> ([A] =>> State[TestConsoleState, A])): def apply[A](fa: ConsoleA[A]): State[TestConsoleState, A] = fa match case ReadLine(prompt) => for s <- State.get _ <- State.set(s.write(prompt)) yield s.read case PrintLine(message) => State.modify(_.write(s"$message\n")) test("Hello World") { val testState = TestConsoleState("World", Nil) val result = program.foldMap(testInterpret).run(testState).value assert(result === (TestConsoleState("World", List("Enter your name: ", "Hello World\n")), ())) }
  27. Tagless final Abstraction of Effects using F[_] Use method definitions

    as algebras. エフェクトを高階型で抽象化する メソッド定義をalgebraとして利用する
  28. Tagless final trait Console[F[_]]: def readLine(prompt: String): F[String] def printLine(message:

    String): F[Unit] object Console: def apply[F[_]](using ev: Console[F]): Console[F] = ev given [F[_]](using F: Sync[F]): Console[F] with def readLine(prompt: String): F[String] = F.blocking(scala.io.StdIn.readLine(prompt)) def printLine(s: String): F[Unit] = F.blocking(scala.Console.println(s)) def program[F[_]: Monad](using C: Console[F]): F[Unit] = for name <- C.readLine("Enter your name: ") _ <- C.printLine(s"Hello $name") yield ()
  29. Tagless final case class TestConsoleState(input: String, output: List[String]): def read:

    String = input def write(message: String): TestConsoleState = copy(output = output :+ message) given Console[[A] =>> State[TestConsoleState, A]] with def readLine(prompt: String): State[TestConsoleState, String] = for s <- State.get _ <- State.set(s.write(prompt)) yield s.read def printLine(message: String): State[TestConsoleState, Unit] = State.modify(_.write(s"$message\n"))
  30. Tagless final val testState = TestConsoleState("World", Nil) val result =

    program[[A] =>> State[TestConsoleState, A]].run(testState).value assert(result == (TestConsoleState("World", List("Enter your name: ", "Hello World\n")), ()))
  31. Summary so far Effects IO Runtime of Cats Effect Testing

  32. Let's practice!

  33. Writing a database library tototoshi/nyanda // Declare a instance of

    "ResultSetRead" Type class given ResultSetRead[IO, String] = ResultSetRead(RS.get[String]("hello")) // Declare a query val q: Query[IO, String] = DB.query[String](sql"select 'Hello, World!' as hello") // Execute the query transactor.readOnly.useKleisli(q) >>= IO.println // => Hello, World!
  34. What are database operations? java.sql.Connection => A

  35. Wrapping Effectful objects trait Connection[F[_]]: def prepareStatement(sql: String): F[PreparedStatement[F]] def

    close(): F[Unit] def commit(): F[Unit] def rollback(): F[Unit] ... trait PreparedStatement[F[_]]: def executeQuery(): F[ResultSet[F]] def executeUpdate(): F[Int] def setInt(parameterIndex: Int, x: Int): F[Unit] def setString(parameterIndex: Int, x: String): F[Unit] ... trait ResultSet[F[_]]: def next(): F[Boolean] def getInt(columnLabel: String): F[Int] def getString(columnLabel: String): F[String] ...
  36. Wrapping Effectful objects object Connection: def apply[F[_]: Sync](conn: java.sql.Connection): Connection[F]

    = new Connection[F]: def prepareStatement(sql: String): F[PreparedStatement[F]] = Sync[F].blocking(conn.prepareStatement(sql)).map(s => PreparedStatement[F](s)) def close(): F[Unit] = Sync[F].blocking(conn.close()) def commit(): F[Unit] = Sync[F].blocking(conn.commit()) def rollback(): F[Unit] = Sync[F].blocking(conn.rollback())
  37. Database operations should be... Connection[F] => F[A]

  38. Database operations should be... Kleisli[F, Connection[F], A] val dbIO: Kleisli[F,

    Connection[F], A] = Kleisli { conn => (???: F[A]) }
  39. Kleisli Kleisli is a wrapper for a monadic function Kleisli

    can be a Monad Kleisli can be a Applicative Kleisliはモナディックな関数ラッパー MonadやApplicativeとして扱える
  40. Handling ResultSets // Equivalent to ResultSet#getX trait ResultSetGet[F[_], A]: def

    get(column: String)(rs: ResultSet[F]): F[A] // Convert a ResultSet to a object trait ResultSetRead[F[_], T]: def read(rs: ResultSet[F]): F[T] // This is like `while (ResultSet#next) {}` trait ResultSetConsume[F[_], T]: def consume(rs: ResultSet[F]): F[T] def get[A](column: String)(using g: ResultSetGet[F, A]): Kleisli[F, ResultSet[F], A]
  41. Handling ResultSets case class Person(id: Int, name: String, nickname: Option[String])

    def personGet[F[_]: Monad]: Kleisli[F, ResultSet[F], Person] = for id <- get[Int]("id") name <- get[String]("name") nickname <- get[Option[Stirng]]("nickname") yield Person(id, name, nickname) def personGet[F[_]: Applicative]: Kleisli[F, ResultSet[F], Person] = (get[String]("id"), get[String]("name"), get("nickname")[Option[String]).mapN(Person.apply)
  42. Database Operation trait DatabaseOps[F[_]]: def query[A](sql: SQL[F])(using g: ResultSetConsume[F, A])

    : Kleisli[F, Connection[F], A] object DatabaseOps: given [F[_]: Monad]: DatabaseOps[F] with def query[A](sql: SQL[F])(using g: ResultSetConsume[F, A]) : Kleisli[F, Connection[F], A] = Kleisli { conn => val rs: F[ResultSet[F]] = for s <- conn.prepareStatement(sql.statement) _ <- bindParams(sql, s) rs <- s.executeQuery() yield rs rs >>= g.consume } private def bindParams(sql: SQL[F], s: PreparedStatement[F]): F[Seq[Unit]] = ...
  43. Composing a transaction def insertAndFind(id: Int): Query[IO, Option[Person]] = for

    _ <- DB.update( sql"insert into person (id, name, nickname, created_at) values ($id...") result <- DB.query[Option[Person]]( sql"select id, name, nickname, created_at from person where id = $id") yield result transactor.transaction.useKelisli(insertAndFind).unsafeRunSync() Kleisliの合成によってトランザクションを表現できる
  44. Conclusion Effects should be controlled IO captures effects Cats Effect

    has a efficient runtime Cats Effectは便利