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. 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
10. 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.
14. 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])
}
}
16. Play Framework with Google Guice
Google Guice is a runtime DI library
- 実行時に依存性を解決する
- Play で主に使われるのは Constructor Injection
Constructor Injection is mostly used in Play
Framework.
18. 全て Google Guice でやる必要はない
そもそもプログラムとインタープリタの分離がしたい。
インターフェースと実装を分けたい、後で入れ替えたい。
そして以下のような性質を持っていると嬉しい
- Compile Time DI
- Composability
- Testability
We want to separate program & execution. Compile
time DI, composability and testability are desired.
19. 全て Google Guice でやる必要はない
純粋関数型プログラミング言語由来のツールを使っておけば全て
解決するんでしょ?
- Reader Monad
- Free Monad
- etc.
I know! Purely functional programming solves all the
things.
20. 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
23. 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
}
24. 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)
25. Separation of program & execution - Reader Monad
- Reader Monad では依存が複数になると扱いにくい。
型が冗長になる
- 引数に一つしか取れないので、Context 組み立てと分解の手
間が発生する
It’s hard to handle with reader monad when it has
multiple dependencies.
26. 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))
}
28. 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(...)
}
}
}
29. Separation of program & execution - Free Monad
val dbSession = new DBSession()
UserRepository.resolveByName("John Doe")
.foldMap(futureInterp(dbSession))
30. Separation of program & execution - Free Monad
Free Monad は
- ボイラープレートが多い
でもコード自動生成に頼るのは微妙
- 実装時に何を処理の最小単位 (≒ 1つの ADT)とするか見極
めが難しい
- 異なる ADT の合成には一苦労
Free Monad requires too many boilerplates,
and composing multiple ADTs is difficult.
32. 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
33. What’s Tagless Final style
言語内 DSL (Embedded DSL) を作成する手法の一つ
Free Monad と比較して少ない記述量でプログラムと実装の分
離、DSL の合成が実現できる。
One method of creating EDSL. Tagless final has less
boilerplate.
34. Introduction to Tagless Final
trait UserRepository[F[_]] {
def store(user: User): F[Unit]
def resolveByName(name: String): F[Option[User]]
}
35. 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]] = ???
}
}
36. 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)
38. 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
40. Problem 1:
各メソッドが Context を暗黙の値として受け取る
- Context を一つだけとることを前提としている
- 暗黙的な値が複数必要だった場合にはそれらをまとめるコン
テナを用意する必要がある
- メソッド内では受け取った値から自分に必要なものを取り出す
作業が発生する
Each method takes a context as an implicit
parameter.
41. 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
}
}
42. 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
})
}
54. 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
65. 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
}
}
}
66. 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
}
}
}
68. 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
}
}
69. 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
70. 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
}
}
}
71. 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]] = ???
}
}
72. 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] ^
73. F[_] が異なる場合は for 式で合成できない
いくつかの解決策がある
- Monad を積み上げる
- F[_] を具体的な型にしてから controller 合成する
- 自然変換を使って型を揃える
There are some solutions. Stacking Monads,
composing concrete types, Natural Transformation.
78. 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) }
}
}
79. 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)
}
80. 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
}
}
81. 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
83. Conclusion
Tagless Final 便利
- 手で書いても苦にならない程度のコード量
- 複数の DSL を比較的簡単に合成できる
- F[_] が異なる場合に一工夫必要。銀の弾丸ではない。
- Play Framework に限らずいろんな場面で使える!
Tagless final is useful because it has less boilerplate.