Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

purely_functional_play_framework_application

3,882 views

Published on

ScalaMatsuri 2018 登壇資料

Published in: Software
  • Nice !! Download 100 % Free Ebooks, PPts, Study Notes, Novels, etc @ https://www.thesisscientist.com/top-30-sites-for-download-free-books-2018
       Reply 
    Are you sure you want to  Yes  No
    Your message goes here

purely_functional_play_framework_application

  1. 1. Purely Functional Play Framework Application 2018/03/17 ScalaMatsuri 2018 Naoki Aoyama
  2. 2. whoami Naoki Aoyama Twitter: @AoiroAoino GitHub: @aoiroaoino Working at Septeni Original,Inc.
  3. 3. 膨大なデータ収集から社内ワークフロー効率化までを行い、 広告主様の広告効果を高める広告運用最適化ツール 様々なメディアに対する広告運用やレポート、 クリエイティブ最適化を支援 Scalaを採用し、ドメイン駆動設計(DDD)で開発
  4. 4. グループ会社のコミックスマート社が手掛ける 連載型新作マンガ配信サービス アプリは900万 DL を超え、オリジナルマンガも100作以上配信。 2017年12月にはアプリ内にアニメ視聴機能を実装し、 オリジナルアニメの配信を開始。 Scala + DDD ほか、 Android 版も Scala で開発。※ iOS 版は Swift
  5. 5. セプテーニ・オリジナルでは いつでも Scala をやってみたい方を募集しております! Scala や DDD 未経験でも問題ありません! 入社の研修カリキュラムを通して Scala と DDD のトレーニングを積むことが出来ます。
  6. 6. </head> <body> Let’s get started!
  7. 7. Agenda Introduction - Play Framework with Google Guice - Purely functional separation of program & execution What’s Tagless Final style? - Overview - It’s your neighbor Tagless Final style practice in Play Framework - Purely, so useful! - But, No Silver Bullet Conclusion
  8. 8. Agenda Introduction - Play Framework with Google Guice - Purely functional separation of program & execution What’s Tagless Final style? - Overview - It’s your neighbor Tagless Final style practice in Play Framework - Purely, so useful! - But, No Silver Bullet Conclusion
  9. 9. Play Framework with Google Guice - Play v2.4.0 より Google Guice を用いた DI の仕組みが取り 入れられた - 大規模なアプリケーションには DI は欠かせない - 実際、慣れれば便利。※慣れれば Google Guice is introduce to Play Framework from v2.4.0. It’s useful once you get used.
  10. 10. Sample Application on Play Framework - Play Framework v2.6.12 - Scala 2.12.4
  11. 11. Sample Application on Play Framework package controllers import javax.inject._ import play.api.mvc._ import models.Greeting @Singleton class HomeController @Inject()(cc: ControllerComponents, @Named("en") greeting: Greeting) extends AbstractController(cc) { def index() = Action { implicit request: Request[AnyContent] => Ok(views.html.index()) } def message(name: String) = Action { Ok(greeting.message(name)) } }
  12. 12. Sample Application on Play Framework package models trait Greeting { def message(name: String): String } class EnglishGreeting extends Greeting { override def message(name: String) = s"Hello, $name" } class JapaneseGreeting extends Greeting { override def message(name: String) = s"こんにちは、$name" }
  13. 13. Sample Application on Play Framework import com.google.inject.AbstractModule import com.google.inject.name.Names import models._ class Module extends AbstractModule { override def configure(): Unit = { bind(classOf[Greeting]) .annotatedWith(Names.named("en")) .to(classOf[EnglishGreeting]) bind(classOf[Greeting]) .annotatedWith(Names.named("jp")) .to(classOf[JapaneseGreeting]) } }
  14. 14. Play Framework with Google Guice Google Guice is a runtime DI library - 実行時に依存性を解決する - Play で主に使われるのは Constructor Injection Constructor Injection is mostly used in Play Framework.
  15. 15. 全て Google Guice でやる必要はない そもそもプログラムとインタープリタの分離がしたい。 インターフェースと実装を分けたい、後で入れ替えたい。 そして以下のような性質を持っていると嬉しい - Compile Time DI - Composability - Testability We want to separate program & execution. Compile time DI, composability and testability are desired.
  16. 16. 全て Google Guice でやる必要はない 純粋関数型プログラミング言語由来のツールを使っておけば全て 解決するんでしょ? - Reader Monad - Free Monad - etc. I know! Purely functional programming solves all the things.
  17. 17. Agenda Introduction - Play Framework with Google Guice - Purely functional separation of program & execution What’s Tagless Final style? - Overview - It’s your neighbor Tagless Final style practice in Play Framework - Purely, so useful! - But, No Silver Bullet Conclusion
  18. 18. Separation of program & execution - Reader Monad trait UserRepository { def store(user: User)(implicit session: DBSession): Try[Unit] def resolveByName(name: String)(implicit session: DBSession): Try[Option[User]] def resolveAllByNames(name: String*) (implicit session: DBSession): Try[List[User]] } class UserRepositoryOnJDBC extends UserRepository { def store(user: User)(implicit dbSession: DBSession) = ??? def resolveByName(name: String)(implicit dbSession: DBSession) = ??? def resolveAllByNames(name: String*)(implicit dbSession: DBSession) = ??? }
  19. 19. Separation of program & execution - Reader Monad trait UserRepositoryModule { def userRepository: UserRepository } trait MessageRepositoryModule { def messageRepository: MessageRepository }
  20. 20. Separation of program & execution - Reader Monad type Prog[A] = Reader[UserRepositoryModule with MessageRepositoryModule, A] def getReplyTargetUsers(messageId: Long): Prog[Try[List[User]]] = Reader { module => for { messageOpt <- module.messageRepository.resolveBy(messageId) names = messageOpt.fold(List.empty[String])(_.replyTargets) users <- module.userRepository.resolveAllByNames(names: _*) } yield users }
  21. 21. Separation of program & execution - Reader Monad val module = new UserRepositoryModule with MessageRepositoryModule { override def userRepository = new UserRepositoryOnJDBC() override def messageRepository = new MessageRepositoryOnJDBC() } MessageAPI.getReplyTargetUsers(1).run(module)
  22. 22. Separation of program & execution - Reader Monad - Reader Monad では依存が複数になると扱いにくい。 型が冗長になる - 引数に一つしか取れないので、Context 組み立てと分解の手 間が発生する It’s hard to handle with reader monad when it has multiple dependencies.
  23. 23. Separation of program & execution - Free Monad type UserRepositoryFree[A] = Free[UserRepositoryOp, A] sealed trait UserRepositoryOp[A] object UserRepositoryOp { case class Store(user: User) extends UserRepositoryOp[Unit] case class ResolveByName(name: String) extends UserRepositoryOp[Option[User]] def store(user: User): UserRepositoryFree[Unit] = Free.liftF[UserRepositoryOp, Unit](Store(user)) def resolveByName(name: String): UserRepositoryFree[Option[User]] = Free.liftF[UserRepositoryOp, Option[User]](ResolveByName(name)) }
  24. 24. Separation of program & execution - Free Monad object UserRepository { def store(user: User): UserRepositoryFree[Unit] = UserRepositoryOp.store(user) def resolveByName(name: String): UserRepositoryFree[Option[User]] = UserRepositoryOp.resolveByName(name) }
  25. 25. Separation of program & execution - Free Monad object UserRepositoryOnJDBCInterp { def futureInterp(session: DBSession) (implicit ec: ExecutionContext): UserRepositoryOp ~> Future = new (UserRepositoryOp ~> Future) { def apply[A](op: UserRepositoryOp[A]): Future[A] = op match { case UserRepositoryOp.Store(user) => Future(...) case UserRepositoryOp.ResolveByName(name) => Future(...) } } }
  26. 26. Separation of program & execution - Free Monad val dbSession = new DBSession() UserRepository.resolveByName("John Doe") .foldMap(futureInterp(dbSession))
  27. 27. Separation of program & execution - Free Monad Free Monad は - ボイラープレートが多い でもコード自動生成に頼るのは微妙 - 実装時に何を処理の最小単位 (≒ 1つの ADT)とするか見極 めが難しい - 異なる ADT の合成には一苦労 Free Monad requires too many boilerplates, and composing multiple ADTs is difficult.
  28. 28. _人人人人人人人人人人_ > Tagless final style <  ̄Y^Y^Y^Y^Y^Y^Y^Y^Y ̄
  29. 29. Agenda Introduction - Play Framework with Google Guice - Purely functional separation of program & execution What’s Tagless Final style? - Overview - It’s your neighbor Tagless Final style practice in Play Framework - Purely, so useful! - But, No Silver Bullet Conclusion
  30. 30. What’s Tagless Final style 言語内 DSL (Embedded DSL) を作成する手法の一つ Free Monad と比較して少ない記述量でプログラムと実装の分 離、DSL の合成が実現できる。 One method of creating EDSL. Tagless final has less boilerplate.
  31. 31. Introduction to Tagless Final trait UserRepository[F[_]] { def store(user: User): F[Unit] def resolveByName(name: String): F[Option[User]] }
  32. 32. Introduction to Tagless Final object UserRepository { implicit val tryUserRepository: UserRepository[Try] = new UserRepository[Try] { def store(user: User): Try[Unit] = ??? def resolveByName(name: String): Try[Option[User]] = ??? } }
  33. 33. Introduction to Tagless Final def prog[F[_]](userName: String) (implicit userRepo: UserRepository[F]): F[Option[User]] = userRepo.resolveByName(userName) prog[Try]("John") // scala.util.Try[Option[User]] = Success(None)
  34. 34. また宇宙からやってきた難しい概念なんでしょう...? Where are you from? Isn’t it purely functional Alien?
  35. 35. Agenda Introduction - Play Framework with Google Guice - Purely functional separation of program & execution What’s Tagless Final style? - Overview - It’s your neighbor Tagless Final style practice in Play Framework - Purely, so useful! - But, No Silver Bullet Conclusion
  36. 36. よくある UserRepository インターフェースと実装例 trait UserRepository[Ctx] { def store(user: User)(implicit ctx: Ctx): Future[Unit] def resolveByName(name: String)(implicit ctx: Ctx): Future[Option[User]] } // impl class UserRepositoryOnJDBC()(implicit ec: ExecutionContext) extends UserRepository[DBSession] { def store(user: User)(implicit ctx: DBSession): Future[Unit] = ??? def resolveByName(name: String)(implicit ctx: DBSession): Future[Option[User]] = ??? }
  37. 37. Problem 1: 各メソッドが Context を暗黙の値として受け取る - Context を一つだけとることを前提としている - 暗黙的な値が複数必要だった場合にはそれらをまとめるコン テナを用意する必要がある - メソッド内では受け取った値から自分に必要なものを取り出す 作業が発生する Each method takes a context as an implicit parameter.
  38. 38. Problem 1: 各メソッドが Context を暗黙の値として受け取る class UserRepositoryCtx(ec: ExecutionContext, dbSession: DBSession) // impl on database class UserRepositoryOnJDBC() extends UserRepository[UserRepositoryCtx] { def store(user: User)(implicit ctx: UserRepositoryCtx): Future[Unit] = { implicit val (ec, s) = (ctx.ec, ctx.dbSession) ??? // update } def resolveByName(name: String) (implicit ctx: UserRepositoryCtx): Future[Option[User]] = { implicit val (ec, s) = (ctx.ec, ctx.dbSession) ??? // select } }
  39. 39. Problem 1: 各メソッドが Context を暗黙の値として受け取る case class DummyCtx() // impl by memory object UserRepositoryOnMemory extends UserRepository[DummyCtx]{ private val db = mutable.Map.empty[UserId, User] def store(user: User)(implicit ctx: DummyCtx): Future[Unit] = Future.successful(db.update(user.id, user)) def resolveByName(name: String)(implicit ctx: DummyCtx): Future[Option[User]] = Future.successful(db.collectFirst { case (_, u@User(_, n, _)) if n == name => u }) }
  40. 40. Problem 2: メソッドの実行結果をどう表現するか いくつかのパターンが考えられる - 呼び出し側に責任を押し付け失敗を全て例外で throw - 例外を型で表現するために Try に包む - 非同期処理もありうるから Future にしておく - etc. How to express the return value as a type.
  41. 41. Problem 2: メソッドの実行結果をどう表現するか // It’s simply! but throwable... trait UserRepository[Ctx] { def store(user: User)(implicit ctx: Ctx): Unit def resolveByName(name: String)(implicit ctx: Ctx): Option[User] } // It’s not throwable! but blocking only... trait UserRepository[Ctx] { def store(user: User)(implicit ctx: Ctx): Try[Unit] def resolveByName(name: String)(implicit ctx: Ctx): Try[Option[User]] } // It’s non blocking! but needs ExecutionContext in Ctx... trait UserRepository[Ctx] { def store(user: User)(implicit ctx: Ctx): Future[Unit] def resolveByName(name: String)(implicit ctx: Ctx): Future[Option[User]] }
  42. 42. つまり、どうすればいいのか UserRepository インターフェース内に実装側の都合を一切持ち 込まないというのが重要 - Context という概念を UserRepository 内に持ち込まない - Context を暗黙の値にとるような前提の実装をやめる - メソッドの実行方法や失敗の表現など、まるっと含めて返り値 の型を抽象的に表現する Do not bring implementation related things to UserRepository interface.
  43. 43. Remove implicit parameter trait UserRepository[Ctx] { def store(user: User)(implicit ctx: Ctx): Future[Unit] def resolveByName(name: String)(implicit ctx: Ctx): Future[Option[User]] }
  44. 44. Remove implicit parameter trait UserRepository[Ctx] { def store(user: User)(implicit ctx: Ctx): Future[Unit] def resolveByName(name: String)(implicit ctx: Ctx): Future[Option[User]] } // 1. remove implicit trait UserRepository[Ctx] { def store(user: User)(ctx: Ctx): Future[Unit] def resolveByName(name: String)(ctx: Ctx): Future[Option[User]] }
  45. 45. Remove implicit parameter trait UserRepository[Ctx] { def store(user: User)(implicit ctx: Ctx): Future[Unit] def resolveByName(name: String)(implicit ctx: Ctx): Future[Option[User]] } // 1. remove implicit trait UserRepository[Ctx] { def store(user: User)(ctx: Ctx): Future[Unit] def resolveByName(name: String)(ctx: Ctx): Future[Option[User]] } // 2. modify return function trait UserRepository[Ctx] { def store(user: User): Ctx => Future[Unit] def resolveByName(name: String): Ctx => Future[Option[User]] }
  46. 46. Remove implicit parameter trait UserRepository[Ctx] { def store(user: User): Ctx => Future[Unit] def resolveByName(name: String): Ctx => Future[Option[User]] } // easy to use implicit parameter within impl. class UserRepositoryOnJDBC(implicit ec: ExecutionContext) extends UserRepository[DBSession] { def store(user: User): DBSession => Future[Unit] = { implicit dbSession => ??? } def resolveByName(name: String): DBSession => Future[Option[User]] = { implicit dbSession => ??? } }
  47. 47. Really need Context? trait UserRepository[Ctx] { def store(user: User): Ctx => Future[Unit] def resolveByName(name: String): Ctx => Future[Option[User]] }
  48. 48. Really need Context? trait UserRepository[Ctx] { def store(user: User): Ctx => Future[Unit] def resolveByName(name: String): Ctx => Future[Option[User]] } // 1. create type alias named `Exec` and replace return type trait UserRepository[Ctx] { type Exec[R] = Ctx => Future[R] def store(user: User): Exec[Unit] def resolveByName(name: String): Exec[Option[User]] }
  49. 49. Really need Context? trait UserRepository[Ctx] { def store(user: User): Ctx => Future[Unit] def resolveByName(name: String): Ctx => Future[Option[User]] } // 1. create type alias named `Exec` and replace return type trait UserRepository[Ctx] { type Exec[R] = Ctx => Future[R] def store(user: User): Exec[Unit] def resolveByName(name: String): Exec[Option[User]] } // 2. add `F[_]` type parameter and remove `Exec` trait UserRepository[F[_]] { def store(user: User): F[Unit] def resolveByName(name: String): F[Option[User]] }
  50. 50. It’s more abstract interface trait UserRepository[F[_]] { def store(user: User): F[Unit] def resolveByName(name: String): F[Option[User]] }
  51. 51. Agenda Introduction - Play Framework with Google Guice - Purely functional separation of program & execution What’s Tagless Final style? - Overview - It’s your neighbor Tagless Final style practice in Play Framework - Purely, so useful! - But, No Silver Bullet Conclusion
  52. 52. UserRepository implementation trait UserRepository[F[_]] { def store(user: User): F[Unit] def resolveByName(name: String): F[Option[User]] }
  53. 53. UserRepository implementation trait UserRepository[F[_]] { def store(user: User): F[Unit] def resolveByName(name: String): F[Option[User]] } type Query[A] = Reader[DBSession, A] object UserRepositoryOnJDBC { implicit def queryUserRepository: UserRepository[Query] = new UserRepository[Query] { def store(user: User): Query[Unit] = Reader { implicit dbSession => ??? } def resolveByName(name: String): Query[Option[User]] = Reader { implicit dbSession => ??? } } }
  54. 54. UserRepository implementation trait UserRepository[F[_]] { def store(user: User): F[Unit] def resolveByName(name: String): F[Option[User]] } type AsyncQuery[A] = ReaderT[Task, DBSession, A] object UserRepositoryOnJDBC { implicit def asyncQueryUserRepository: UserRepository[AsyncQuery] = new UserRepository[AsyncQuery] { def store(user: User): AsyncQuery[Unit] = ReaderT { implicit dbSession => Task(???) } def resolveByName(name: String): AsyncQuery[Option[User]] = ReaderT { implicit dbSession => Task(???) } } }
  55. 55. UserRepository implementation trait UserRepository[F[_]] { def store(user: User): F[Unit] def resolveByName(name: String): F[Option[User]] } type UserRepositoryFree[A] = Free[UserRepositoryOp, A] object UserRepositoryOnJDBC { implicit def freeUserRepository: UserRepository[UserRepositoryFree] = new UserRepository[UserRepositoryFree] { def store(user: User): UserRepositoryFree[Unit] = UserRepositoryOp.store(user) def resolveByName(name: String): UserRepositoryFree[Option[User]] = UserRepositoryOp.resolveByName(name) } }
  56. 56. UserRepository implementation trait UserRepository[F[_]] { def store(user: User): F[Unit] def resolveByName(name: String): F[Option[User]] } type ConnectionIO[A] = Free[ConnectionOp, A] object UserRepositoryOnJDBC { implicit def doobieUserRepository: UserRepository[ConnectionIO] = new UserRepository[ConnectionIO] { def store(user: User): ConnectionIO[Unit] = sql"...".update.run.map(_ => ()) def resolveByName(name: String): ConnectionIO[Option[User]] = sql"...".query[User].option } }
  57. 57. UserRepository implementation object UserRepository { def apply[F[_]](implicit F: UserRepository[F]): UserRepository[F] = F } val dbSession = new DBSession() type Query[A] = Reader[DBSession, A] UserRepository[Query].resolveByName("John Doe") .run(dbSession) type AsyncQuery[A] = ReaderT[Task, DBSession, A] UserRepository[AsyncQuery].resolveByName("John Doe") .run(dbSession).runAsync type UserRepositoryFree[A] = Free[UserRepositoryOp, A] UserRepository[UserRepositoryFree].resolveByName("John Doe") .foldMap(futureInterp(dbSession))
  58. 58. UserRepository implementation object UserRepository { def apply[F[_]](implicit F: UserRepository[F]): UserRepository[F] = F } val xa = Transactor.fromDriverManager[Task](...) // for doobie type ConnectionIO[A] = Free[ConnectionOp, A] UserRepository[ConnectionIO].resolveByName("John Doe") .transact(xa).runAsync
  59. 59. UserController implementation object UserController { def getUser[F[_]: Monad: UserRepository](id: Long): F[Result] = UserRepository[F].resolveBy(id).map { case Some(user) => Results.Ok(GetUserResponse.from(user).toJson) case None => Results.NotFound } }
  60. 60. UserController implementation class UserController @Inject()(cc: ControllerComponents, ec: ExecutionContext) extends AbstractController(cc) { // ... import UserRepositoryOnJDBC._ // impls def getUser(id: Long) = Action { UserController.getUser[Query](id).run(dbSession) } def getUserAsync(id: Long) = Action.async { UserController.getUser[AsyncQuery](id).run(dbSession).runAsync } }
  61. 61. UserController testing class UserRepositoryMock extends UserRepository[Id] { def store(user: User): Id[Unit] = ??? def resolveBy(id: Long): Id[Option[User]] = ??? def resolveByName(name: String): Id[Option[User]] = ??? }
  62. 62. UserController testing class UserControllerSpec extends PlaySpec { "UserController GET" should { "get user from static controller" in { implicit val mock = new UserRepositoryMock { override def resolveBy(id: Long): Option[User] = if (id == 100) Some(User(100, "John")) else None } UserController.getUser[Id](100).header.status mustBe OK UserController.getUser[Id](999).header.status mustBe NOT_FOUND } } }
  63. 63. UserController testing object UserController { // ... def storeAndGet[F[_]: Monad: UserRepository](name: String): F[Result] = for { _ <- UserRepository[F].store(User(Random.nextLong(), name)) user <- UserRepository[F].resolveByName(name) } yield { user match { case Some(u) => Results.Ok(GetUserResponse.from(u).toJson) case None => Results.InternalServerError } } }
  64. 64. UserController testing type DB = List[User] class UserRepositoryStateMock extends UserRepository[State[DB, ?]] { def store(user: User): State[DB, Unit] = ??? def resolveBy(id: Long): State[DB, Option[User]] = ??? def resolveByName(name: String): State[DB, Option[User]] = ??? }
  65. 65. UserController testing class UserControllerSpec extends PlaySpec { "UserController GET" should { "store and get user from static controller" in { implicit val mock = new UserRepositoryStateMock { override def store(user: User): State[DB, Unit] = State(db => (user :: db, ())) override def resolveByName(name: String): State[DB, Option[User]] = State.inspect(_.find(_.name == name)) } UserController.storeAndGet[State[DB, ?]]("John") .runEmptyA.value.header.status mustBe OK } }
  66. 66. Agenda Introduction - Play Framework with Google Guice - Purely functional separation of program & execution What’s Tagless Final style? - Overview - It’s your neighbor Tagless Final style practice in Play Framework - Purely, so useful! - But, No Silver Bullet Conclusion
  67. 67. F[_] が異なる場合は for 式で合成できない object MessageController { def getUserMessages[F[_]: Monad: MessageRepository: UserRepository]( userId: Long ): F[Result] = for { user <- UserRepository[F].resolveBy(userId) messages <- MessageRepository[F].resolveAllByUserId(userId) } yield { user match { case Some(u) => Results.Ok(GetMessagesResponse.from(u, messages).toJson) case None => Results.NotFound } } }
  68. 68. F[_] が異なる場合は for 式で合成できない type AsyncQuery[A] = ReaderT[Task, DBSession, A] type AsyncCommand[A] = ReaderT[Task, RedisConnection, A] object MessageRepositoryOnJDBC { // MessageRepository[AsyncQuery] is not implemented implicit def redisConnectionMessageRepository: MessageRepository[AsyncCommand] = new MessageRepository[AsyncCommand] { def resolveBy(id: Long): AsyncCommand[Option[Message]] = ??? def store(message: Message): AsyncCommand[Unit] = ??? def resolveAllByUserId(userId: Long): AsyncCommand[List[Message]] = ??? } }
  69. 69. F[_] が異なる場合は for 式で合成できない [pfpfap] $ compile [info] Compiling 1 Scala source to /Users/aoiroaoino/git/pfpf/bbs/target/scala-2.12/classes … ... [error] /Users/aoiroaoino/git/pfpf/bbs/app/controllers/MessageController.scala:26:50: could not find implicit value for evidence parameter of type models.repository.MessageRepository[models.AsyncQuery] [error] MessageController.getUserMessages[AsyncQuery](userId).run(dbSession).runAsync [error] ^
  70. 70. F[_] が異なる場合は for 式で合成できない いくつかの解決策がある - Monad を積み上げる - F[_] を具体的な型にしてから controller 合成する - 自然変換を使って型を揃える There are some solutions. Stacking Monads, composing concrete types, Natural Transformation.
  71. 71. Stacking Monads class MessageController @Inject()(cc: ControllerComponents, ec: ExecutionContext) extends AbstractController(cc) { // ... type SpecialAction[A] = ReaderT[ReaderT[Task, DBSession, ?], RedisConnection, A] def getUserMessagesSM(userId: Long) = Action.async { MessageController.getUserMessages[SpecialAction](userId) .run(redisConnection) // RedisConnection => ReaderT[Task, DBSession, A] .run(dbSession) // DBSession => Task[A] .runAsync // CancelableFuture[A] } }
  72. 72. Composing concrete types class MessageController @Inject()(cc: ControllerComponents, ec: ExecutionContext) extends AbstractController(cc) { // ... def getUserMessages(userId: Long) = Action.async { (for { user <- UserRepository[AsyncQuery].resolveBy(userId).run(dbSession) messages <- MessageRepository[AsyncCommand].resolveAllByUserId(userId) .run(redisConnection) } yield { user match { case Some(u) => Results.Ok(GetMessagesResponse.from(u, messages).toJson) case None => Results.NotFound } }).runAsync } }
  73. 73. Use natural transformation and mapK() class MessageController @Inject()(cc: ControllerComponents, ec: ExecutionContext) extends AbstractController(cc) { // ... type AsyncQuery[A] = ReaderT[Task, DBSession, A] type AsyncCommand[A] = ReaderT[Task, RedisConnection, A] // common type of AsyncQuery and AsyncCommand type Exec[A] = ReaderT[Task, (DBSession, RedisConnection), A] def getUserMessages(userId: Long) = Action.async { MessageController.getUserMessages[Exec](userId) .run((dbSession, redisConnection)).runAsync } }
  74. 74. Use natural transformation and mapK() trait MessageRepository[F[_]] { def store(message: Message): F[Unit] def resolveBy(id: Long): F[Option[Message]] def resolveAllByUserId(userId: Long): F[List[Message]] def mapK[G[_]](fk: F ~> G): MessageRepository[G] = new MessageRepository[G] { def store(message: Message): G[Unit] = fk(self.store(message)) def resolveBy(id: Long): G[Option[Message]] = fk(self.resolveBy(id)) def resolveAllByUserId(userId: Long): G[List[Message]] = fk(self.resolveAllByUserId(userId)) } }
  75. 75. Use natural transformation and mapK() class MessageController @Inject()(cc: ControllerComponents, ec: ExecutionContext) extends AbstractController(cc) { // ... type Exec[A] = ReaderT[Task, (DBSession, RedisConnection), A] val asyncQueryToExec: AsyncQuery ~> Exec = new (AsyncQuery ~> Exec) { def apply[A](aq: AsyncQuery[A]): Exec[A] = ReaderT { case (dbSession, _) => aq.run(dbSessioin) } } val asyncCommandToExec: AsyncCommand ~> Exec = new (AsyncCommand ~> Exec) { def apply[A](ac: AsyncCommand[A]): Exec[A] = ReaderT { case (_, redisConn) => ac.run(redisConn) } } }
  76. 76. Use natural transformation and mapK() class MessageController @Inject()(cc: ControllerComponents, ec: ExecutionContext) extends AbstractController(cc) { // ... type Exec[A] = ReaderT[Task, (DBSession, RedisConnection), A] val asyncQueryToExec: AsyncQuery ~> Exec = ... val asyncCommandToExec: AsyncCommand ~> Exec = ... implicit val execUserRepository: UserRepository[Exec] = UserRepository[AsyncQuery].mapK(asyncQueryToExec) implicit val execMessageRepository: MessageRepository[Exec] = MessageRepository[AsyncCommand].mapK(asyncCommandToExec) }
  77. 77. Use natural transformation and mapK() class MessageController @Inject()(cc: ControllerComponents, ec: ExecutionContext) extends AbstractController(cc) { // ... type Exec[A] = ReaderT[Task, (DBSession, RedisConnection), A] val asyncQueryToExec: AsyncQuery ~> Exec = ... val asyncCommandToExec: AsyncCommand ~> Exec = ... implicit val execUserRepository: UserRepository[Exec] = ... implicit val execMessageRepository: MessageRepository[Exec] = ... def getUserMessages(userId: Long) = Action.async { MessageController.getUserMessages[Exec](userId) .run((dbSession, redisConnection)).runAsync } }
  78. 78. Agenda Introduction - Play Framework with Google Guice - Purely functional separation of program & execution What’s Tagless Final style? - Overview - It’s your neighbor Tagless Final style practice in Play Framework - Purely, so useful! - But, No Silver Bullet Conclusion
  79. 79. Conclusion Google Guice でアプリを全て組み立てる必要はない - プログラムとインタープリタの分離をする方法はいくつもあ る。 - Reader や Free もいいけど直接使うと不便さもある。 There are many ways to separate program & interpreter.
  80. 80. Conclusion Tagless Final 便利 - 手で書いても苦にならない程度のコード量 - 複数の DSL を比較的簡単に合成できる - F[_] が異なる場合に一工夫必要。銀の弾丸ではない。 - Play Framework に限らずいろんな場面で使える! Tagless final is useful because it has less boilerplate.

×