Slide 1

Slide 1 text

Purely Functional Programming with Cats Effect 3 and Scala 3

Slide 2

Slide 2 text

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)

Slide 3

Slide 3 text

Effect You can't run them twice Useful! I/O (File, DB, HTTP) State Clock etc. Effect: 2度実行できないもの

Slide 4

Slide 4 text

Effect should be controlled Hard to read test refactor Often block threads 読みづらい テストしづらい リファクタリングがしづらい スレッドをブロックすることも

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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()

Slide 7

Slide 7 text

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()

Slide 8

Slide 8 text

Cats

Slide 9

Slide 9 text

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()

Slide 10

Slide 10 text

Monad Sequential computation def flatMap[A, B](fa: F[A])(f: A => F[B]): F[B] Monadで順序や依存関係を表現できる

Slide 11

Slide 11 text

Program as a description Isolation from runtime environment Manipulation the description Concurrency 実行環境からの分離 プログラムに対して加工ができる 並行処理

Slide 12

Slide 12 text

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は実装によってはスタックセーフではない 単純な実装ではパフォーマンスを上げられない

Slide 13

Slide 13 text

Cats Effect Stack safe Fast

Slide 14

Slide 14 text

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 {})

Slide 15

Slide 15 text

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のコンストラクタはスレッドの扱いについてランタイ ムにヒントを与える役割がある

Slide 16

Slide 16 text

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 )

Slide 17

Slide 17 text

Fibers Lightweight threads Fiber == 軽量スレッド

Slide 18

Slide 18 text

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) キャンセルができる

Slide 19

Slide 19 text

Type Classes

Slide 20

Slide 20 text

Test

Slide 21

Slide 21 text

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) }

Slide 22

Slide 22 text

Test It's not easy to test with IO. Can we replace IO with a different data structure? IOのままではテストがしづらい IOを別のデータ構造に置き換えられないだろうか

Slide 23

Slide 23 text

Free Monad Describe computation as data Convert algebras into IO using natural transformations 計算をデータとして表現する Natural TransformationでIOに変換する

Slide 24

Slide 24 text

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 ()

Slide 25

Slide 25 text

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)

Slide 26

Slide 26 text

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")), ())) }

Slide 27

Slide 27 text

Tagless final Abstraction of Effects using F[_] Use method definitions as algebras. エフェクトを高階型で抽象化する メソッド定義をalgebraとして利用する

Slide 28

Slide 28 text

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 ()

Slide 29

Slide 29 text

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"))

Slide 30

Slide 30 text

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")), ()))

Slide 31

Slide 31 text

Summary so far Effects IO Runtime of Cats Effect Testing

Slide 32

Slide 32 text

Let's practice!

Slide 33

Slide 33 text

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!

Slide 34

Slide 34 text

What are database operations? java.sql.Connection => A

Slide 35

Slide 35 text

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] ...

Slide 36

Slide 36 text

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())

Slide 37

Slide 37 text

Database operations should be... Connection[F] => F[A]

Slide 38

Slide 38 text

Database operations should be... Kleisli[F, Connection[F], A] val dbIO: Kleisli[F, Connection[F], A] = Kleisli { conn => (???: F[A]) }

Slide 39

Slide 39 text

Kleisli Kleisli is a wrapper for a monadic function Kleisli can be a Monad Kleisli can be a Applicative Kleisliはモナディックな関数ラッパー MonadやApplicativeとして扱える

Slide 40

Slide 40 text

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]

Slide 41

Slide 41 text

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)

Slide 42

Slide 42 text

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]] = ...

Slide 43

Slide 43 text

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の合成によってトランザクションを表現できる

Slide 44

Slide 44 text

Conclusion Effects should be controlled IO captures effects Cats Effect has a efficient runtime Cats Effectは便利