2015/07/24 関数型Scalaの集い
- 中村 学
- @gakuzzzz
- 株式会社 Tech to Value
- Scala関西 Summit 2015 スポンサーしてます
- Scala Matsuri 2016 よろしくおねがいします!
- JDBC を Scala から使いやすくするためのライブラリ
- なるべくDRYかつミスを少なくSQLを書けるように
val (p, c) = (Programmer.syntax("p"), Company.syntax("c"))
val programmers: Seq[Programmer] = DB.readOnly { implicit session =>
withSQL {
select
.from(Programmer as p)
.leftJoin(Company as c).on(p.companyId, c.id)
.where.eq(p.isDeleted, false)
.orderBy(p.createdAt)
.limit(10)
.offset(0)
}.map(Programmer(p, c)).list.apply()
}
- RDBをコレクションのように見なして操作できるライブラリ
val a: DBIOAction[Unit] = for {
ns <- coffees.filter(_.name.startsWith("ESPRESSO")).map(_.name).result
_ <- ns.traverse(n => coffees.filter(_.name === n).delete.result)
} yield ()
val f: Future[Unit] = db.transactionally.run(a)
モナド!!!
DBIOAction
https://github.com/tpolecat/doobie
- doobie is a pure functional JDBC layer for Scala. It is not an ORM, nor is it a relational algebra
- it just provides a principled way to construct programs (and higher-level libraries) that use JDBC
case class CountryCode(code: String)
def main(args: Array[String]): Unit =
tmain.trans[IO].unsafePerformIO
val tmain: DM.DriverManagerIO[Unit] =
for {
_ <- DM.delay(Class.forName("org.h2.Driver"))
c <- DM.getConnection("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1", "sa", "")
a <- DM.liftConnection(c, examples ensuring C.close).except(t => t.toString.point[DM.DriverManagerIO])
_ <- DM.delay(Console.println(a))
} yield ()
def examples: C.ConnectionIO[String] =
for {
_ <- C.delay(println("Loading database..."))
_ <- loadDatabase(new File("example/world.sql"))
s <- speakerQuery("English", 10)
_ <- s.traverseU(a => C.delay(println(a)))
} yield "Ok"
def loadDatabase(f: File): C.ConnectionIO[Unit] =
for {
ps <- C.prepareStatement("RUNSCRIPT FROM ? CHARSET 'UTF-8'")
_ <- C.liftPreparedStatement(ps, (PS.setString(1, f.getName) >> PS.execute) ensuring PS.close)
} yield ()
def speakerQuery(s: String, p: Double): C.ConnectionIO[List[CountryCode]] =
for {
ps <- C.prepareStatement("SELECT COUNTRYCODE FROM COUNTRYLANGUAGE WHERE LANGUAGE = ? AND PERCENTAGE > ?")
l <- C.liftPreparedStatement(ps, speakerPS(s, p) ensuring PS.close)
} yield l
def speakerPS(s: String, p: Double): PS.PreparedStatementIO[List[CountryCode]] =
for {
_ <- PS.setString(1, s)
_ <- PS.setDouble(2, p)
rs <- PS.executeQuery
l <- PS.liftResultSet(rs, unroll(RS.getString(1).map(CountryCode(_))) ensuring RS.close)
} yield l
モナド!!!!
Do you guys have something like a DB monad that I can use?
モナド!!!!!
基本的に副作用があり状態も管理する必要がある
- Connection
- Transaction
手続き的トランザクションでは合成できない
// 擬似コードなので動きません
def logicA(entity: EntityA): Unit = {
val tx = Transaction.begin()
try {
insert(entity)
tx.commit()
} catch {
e: Throwable => tx.rollback()
}
}
def logicB(entity: EntityB): Unit = {
val tx = Transaction.begin()
try {
insert(entity)
tx.commit()
} catch {
e: Throwable => tx.rollback()
}
}
def logicC(): Unit = {
logicA と logicB を同一トランザクションで呼びたい
}
// 擬似コードなので動きません
def logicA(entity: EntityA)(implicit session: DBSession = AutoSession): Unit = {
insert(entity)
}
def logicB(entity: EntityB)(implicit session: DBSession = AutoSession): Unit = {
insert(entity)
}
def logicC(): Unit = {
DB.localTx { implicit s =>
logicA(a)
logicB(b)
}
}
実用上、これで困ることはないです。
けどなんか悔しいので ScalikeJDBC でもモナモナしたい!!!
というわけで作りました。
https://github.com/gakuzzzz/free-scalikejdbc
def createProgrammer[F[_]](name: Name, skillIds: List[SkillId])(implicit S: ScalikeJDBC[F], M: Applicative[FreeC[F, ?]]) = {
import S._
for {
id <- generateKey(insert.into(Programmer).namedValues(pc.name -> name))
skills <- list(select.from(Skill as s).where.in(s.id, skillIds))(Skill(s))
_ <- skills.traverse[FreeC[F, ?], Boolean](s => execute(insert.into(ProgrammerSkill).namedValues(sc.programmerId -> id, sc.skillId -> s.id)))
} yield Programmer(id, name, skills)
}
val newProgrammer = DB.localTx {
Interpreter.transaction.run(createProgrammer("Alice", List(2, 3)))
}
- Free モナドとは、Functor を ベースに Monad を作れる構造のこと
- 合成可能なDSLを作りたい時に使われる
日本語でおk
たぶん、文章で説明してもすでに理解できてる人にしか理解できない怪文章になってしまうので、具体的にコードで
Functor とか Monad とかは知ってるものとして話を進めるので、怪しい人は去年のScalaz勉強会の資料 主要な型クラスの紹介 もご参照ください
sealed abstract class Query
sealed abstract class Query[A]
sealed abstract class Query[A]
object Query {
case class GetList[A] extends Query[List[A]]
case class GetOption[A] extends Query[Option[A]]
case object Execute extends Query[Boolean]
}
ここでは Scalaz7.1.x を使います。
sealed class ScalikeJDBC[F[_]] {
def list[A](sql: SQLBuilder[_])(f: WrappedResultSet => A): FreeC[F, List[A]] = ???
def first[A](sql: SQLBuilder[_])(f: WrappedResultSet => A): FreeC[F, Option[A]] = ???
def execute(sql: SQLBuilder[UpdateOperation]): FreeC[F, Boolean] = ???
}
object ScalikeJDBC {
implicit def instance[F[_]]: ScalikeJDBC[F] = new ScalikeJDBC[F]
}
Free は Functor を使って Monad にする構造です。しかし今定義した Query[A]
は特に Functor を定義していません。
そこで、Coyoneda を使って、ただの * -> * kind の型である Query[A]
を Functor にします。
FreeC
はこの Coyoneda
を使ってラップした Free
のことです。
type FreeC[S[_], A] = Free[Coyoneda[S, ?], A]
Coyoneda
を使って Free
を作るアイデアは非常に便利で Operational Monad
という名前でも知られています。
sealed abstract class Query[A](private[free] val statement: String, private[free] val parameters: Seq[Any])
object Query {
case class GetList[A](sql: SQLToList[A, HasExtractor]) extends Query[List[A]](sql.statement, sql.parameters)
case class GetOption[A](sql: SQLToOption[A, HasExtractor]) extends Query[Option[A]](sql.statement, sql.parameters)
case class Execute(sql: SQLExecution) extends Query[Boolean](sql.statement, sql.parameters)
}
sealed class ScalikeJDBC[F[_]](implicit I: Inject[Query, F]) {
private def lift[A](v: Query[A]): FreeC[F, A] = Free.liftFC(I.inj(v))
def list[A](sql: SQLBuilder[_])(f: WrappedResultSet => A): FreeC[F, List[A]] = {
val q = withSQL(sql).map(f).list()
lift(GetList[A](q.statement, q.parameters))
}
def first[A](sql: SQLBuilder[_])(f: WrappedResultSet => A): FreeC[F, Option[A]] = {
val q = withSQL(sql).map(f).first()
lift(GetOption[A](q.statement, q.parameters))
}
def execute(sql: SQLBuilder[UpdateOperation]): FreeC[F, Boolean] = {
val q = withSQL(sql).execute()
lift(Execute(q.statement, q.parameters))
}
}
object ScalikeJDBC {
implicit def instance[F[_]](implicit I: Inject[Query, F]): ScalikeJDBC[F] = new ScalikeJDBC[F]
}
突然の Inject[Query, F]
!!!!
実は Free で DSL を作る、というだけの範囲では Inject
を使う必要はありません。
Free-ScalikeJDBC では Coproduct
を使って、他の Free モナドを利用した DSL と合成ができるようにするため、ここで Inject
を使用しています。
Coproduct
を利用した Free モナドの合成については吉田さんの記事 が日本語でわかりやすいので参考まで。
実は以上でDSLとしては完成です。
以下のようにfor式で各クエリを合成して Query[A]
を取得するコードを書くことができます。
private lazy val a = Account.syntax("a")
private lazy val ac = Account.column
def create[F[_]](name: String)(implicit S: ScalikeJDBC[F]) = {
import S._
for {
account <- first(select.from(Account as a).where.eq(a.id, id))(Account(a))
_ <- S.execute(insert.into(Account).namedValues(ac.name -> account.name + " 2nd"))
} yield account
}
でも Query[A]
インスタンスがあるだけでは何もできないので困ってしまいます。
Interpreter ってかっこよく言ってますが、実際のところ、Query[A]
から何らかの M[A]
の値を取り出す只の関数のことです。
つまり Query ~> M
の事ですね。
Query ~> M
は NaturalTransformation[Query, M]
のことです。
abstract class Interpreter[M[_]](implicit M: Monad[M]) extends (Query ~> M) {
protected def exec[A](f: DBSession => A): M[A]
def apply[A](c: Query[A]): M[A] = c match {
case GetList(sql) => exec(implicit s => sql.apply())
case GetOption(sql) => exec(implicit s => sql.apply())
case Execute(sql) => exec(implicit s => sql.apply())
}
def run[A](q: FreeC[Query, A]): M[A] = Free.runFC(q)(this)
}
object Interpreter {
lazy val auto = new Interpreter[Id] {
protected def exec[A](f: DBSession => A) = f(AutoSession)
}
type SQLEither[A] = SQLException \/ A
object SQLEither {
implicit def TxBoundary[A] = new TxBoundary[SQLEither[A]] {
def finishTx(result: SQLEither[A], tx: Tx) = {
result match {
case \/-(_) => tx.commit()
case -\/(_) => tx.rollback()
}
result
}
}
}
lazy val safe = new Interpreter[SQLEither] {
protected def exec[A](f: DBSession => A) = \/.fromTryCatchThrowable[A, SQLException](f(AutoSession))
}
type TxExecutor[A] = Reader[DBSession, A]
lazy val transaction = new Interpreter[TxExecutor] {
protected def exec[A](f: DBSession => A) = Reader.apply(f)
}
type SafeExecutor[A] = ReaderT[SQLEither, DBSession, A]
lazy val safeTransaction = new Interpreter[SafeExecutor] {
protected def exec[A](f: DBSession => A) = {
Kleisli.kleisliU { s: DBSession => \/.fromTryCatchThrowable[A, SQLException](f(s)) }
}
}
}
ここではいろんな M を返す Interpreter を作れるように、abstract class で共通処理をまとめています。
* -> *
kind の型をつくる- 1.で作った型をFreeでラップした値を返すメソッドをつくる
- Interpreter つくる
簡単ですね!!!
Slick3 が reactive-streams API に対応してるぜ! ってドヤ顔してるので、Free-ScalikeJDBC も scalaz-stream 使用したAPIを提供して、streamz 経由で reactive-streams API に対応してるぜ!ってドヤ顔しかえしたい(PR募集中)
あ、その通りです。ご指摘ありがとうございます。 gist の Markdown の解釈が変わった影響で意図しない形にrenderingされたようですね。修正しておきます。