$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

    View Slide

  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)

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

  8. Cats

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  13. Cats Effect
    Stack safe
    Fast

    View Slide

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

    View Slide

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

    View Slide

  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
    )

    View Slide

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

    View Slide

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

    View Slide

  19. Type Classes

    View Slide

  20. Test

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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)

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  31. Summary so far
    Effects
    IO
    Runtime of Cats Effect
    Testing

    View Slide

  32. Let's practice!

    View Slide

  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!

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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]

    View Slide

  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)

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide