Last active
August 10, 2021 08:50
-
-
Save gakuzzzz/147c520e32177fea75f0 to your computer and use it in GitHub Desktop.
Revisions
-
gakuzzzz revised this gist
Aug 10, 2021 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -407,7 +407,7 @@ object Interpreter { ## FreeでのDSLの作り方まとめ 1. `* -> *` kind の型をつくる 1. 1.で作った型をFreeでラップした値を返すメソッドをつくる 1. Interpreter つくる -
gakuzzzz revised this gist
Jul 25, 2015 . 1 changed file with 12 additions and 3 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -294,9 +294,18 @@ object Query { ```scala 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] -
gakuzzzz revised this gist
Jul 25, 2015 . 1 changed file with 4 additions and 0 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -40,6 +40,8 @@ val programmers: Seq[Programmer] = DB.readOnly { implicit session => ### Slick3 http://slick.typesafe.com/ * RDBをコレクションのように見なして操作できるライブラリ ```scala @@ -58,6 +60,8 @@ DBIOAction ### doobie 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 -
gakuzzzz revised this gist
Jul 25, 2015 . 1 changed file with 4 additions and 0 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -12,6 +12,10 @@ ## ScalikeJDBCとは http://scalikejdbc.org/  * JDBC を Scala から使いやすくするためのライブラリ * なるべくDRYかつミスを少なくSQLを書けるように -
gakuzzzz revised this gist
Jul 25, 2015 . 1 changed file with 1 addition and 0 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -394,6 +394,7 @@ object Interpreter { 1. 1.で作った型をFreeでラップした値を返すメソッドをつくる 1. Interpreter つくる 簡単ですね!!! ## Free-ScalikeJDBC の今後の野望 -
gakuzzzz revised this gist
Jul 25, 2015 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -270,7 +270,7 @@ type FreeC[S[_], A] = Free[Coyoneda[S, ?], A] `Coyoneda` を使って `Free` を作るアイデアは非常に便利で `Operational Monad` という名前でも知られています。 ### 5. 現状だと、構文のメソッドが `SQLBuilder` を受け取っているけど `Query[A]` がそれを使えないので、`Query[A]` にパラメータを足します ```scala sealed abstract class Query[A](private[free] val statement: String, private[free] val parameters: Seq[Any]) -
gakuzzzz revised this gist
Jul 25, 2015 . 1 changed file with 8 additions and 8 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -218,19 +218,19 @@ Functor とか Monad とかは知ってるものとして話を進めるので ## Free モナドを使った DSL の作り方 ### 1. まず合成したい単位を表す型を定義します ```scala sealed abstract class Query ``` ### 2. これだけでは値を返せないしFreeにもできないので、型引数を足します ```scala sealed abstract class Query[A] ``` ### 3. 作成した型の実装としてDSLの構文木を表すクラスorオブジェクトを定義します ```scala sealed abstract class Query[A] @@ -241,7 +241,7 @@ object Query { } ``` ### 4. DSLの構文としてFreeを返すメソッドを定義します ここでは Scalaz7.1.x を使います。 @@ -270,7 +270,7 @@ type FreeC[S[_], A] = Free[Coyoneda[S, ?], A] `Coyoneda` を使って `Free` を作るアイデアは非常に便利で `Operational Monad` という名前でも知られています。 ### 5. 現状だと、構文のメソッドが SQLBuilder` を受け取っているけど `Query[A]` がそれを使えないので、`Query[A]` にパラメータを足します ```scala sealed abstract class Query[A](private[free] val statement: String, private[free] val parameters: Seq[Any]) @@ -281,7 +281,7 @@ object Query { } ``` ### 6. 改良した `Query[A]` を使って、構文のメソッドを実装していきます ```scala sealed class ScalikeJDBC[F[_]](implicit I: Inject[Query, F]) { @@ -304,7 +304,7 @@ Free-ScalikeJDBC では `Coproduct` を使って、他の Free モナドを利 `Coproduct` を利用した Free モナドの合成については[吉田さんの記事](http://d.hatena.ne.jp/xuwei/20140618/1403054751) が日本語でわかりやすいので参考まで。 ### 7. DSL完成! 実は以上でDSLとしては完成です。 @@ -326,7 +326,7 @@ Free-ScalikeJDBC では `Coproduct` を使って、他の Free モナドを利 でも `Query[A]` インスタンスがあるだけでは何もできないので困ってしまいます。 ### 8. Interpreter を実装します Interpreter ってかっこよく言ってますが、実際のところ、`Query[A]` から何らかの `M[A]` の値を取り出す只の関数のことです。 -
gakuzzzz created this gist
Jul 25, 2015 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,402 @@ # Free-ScalikeJDBC から見る合成可能なDSLの作り方 2015/07/24 関数型Scalaの集い ## 自己紹介 * 中村 学 * [@gakuzzzz](https://twitter.com/gakuzzzz) * 株式会社 Tech to Value * [Scala関西 Summit 2015](http://summit.scala-kansai.org/) スポンサーしてます * [Scala Matsuri 2016](http://scalamatsuri.org/) よろしくおねがいします! ## ScalikeJDBCとは * JDBC を Scala から使いやすくするためのライブラリ * なるべくDRYかつミスを少なくSQLを書けるように ```scala 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() } ``` ## その他のDBライブラリ ### Slick3 * RDBをコレクションのように見なして操作できるライブラリ ```scala 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 ### 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 ```scala 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 ``` モナド!!!! ## ある日の ScalikeJDBC の Gitter > Do you guys have something like a DB monad that I can use? モナド!!!!! ## DBライブラリはなぜモナドにしたがるのか ### 状態管理 基本的に副作用があり状態も管理する必要がある * Connection * Transaction ### 合成可能性 手続き的トランザクションでは合成できない ```scala // 擬似コードなので動きません 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 を同一トランザクションで呼びたい } ``` ### ScalikeJDBC は合成については implicit parameter で実現しています ```scala // 擬似コードなので動きません 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 でもモナモナしたい!!! ## free-scalikejdbc というわけで作りました。 https://github.com/gakuzzzz/free-scalikejdbc ```scala 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 モナド * Free モナドとは、Functor を ベースに Monad を作れる構造のこと * 合成可能なDSLを作りたい時に使われる 日本語でおk たぶん、文章で説明してもすでに理解できてる人にしか理解できない怪文章になってしまうので、具体的にコードで Functor とか Monad とかは知ってるものとして話を進めるので、怪しい人は去年のScalaz勉強会の資料 [主要な型クラスの紹介](https://gist.github.com/gakuzzzz/8d497609012863b3ea50) もご参照ください ## Free モナドを使った DSL の作り方 ### まず合成したい単位を表す型を定義します ```scala sealed abstract class Query ``` ### これだけでは値を返せないしFreeにもできないので、型引数を足します ```scala sealed abstract class Query[A] ``` ### 作成した型の実装としてDSLの構文木を表すクラスorオブジェクトを定義します ```scala 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] } ``` ### DSLとしての構文をFreeを返すメソッドとして定義します ここでは Scalaz7.1.x を使います。 ```scala 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](https://github.com/scalaz/scalaz/blob/7bbe2669267e992dc96d8e0e7e9e5d7c54a70033/core/src/main/scala/scalaz/Coyoneda.scala) を使って、ただの * -> * kind の型である `Query[A]` を Functor にします。 `FreeC` はこの `Coyoneda` を使ってラップした `Free` のことです。 ```scala type FreeC[S[_], A] = Free[Coyoneda[S, ?], A] ``` `Coyoneda` を使って `Free` を作るアイデアは非常に便利で `Operational Monad` という名前でも知られています。 1. 現状だと、構文のメソッドが SQLBuilder` を受け取っているけど `Query[A]` がそれを使えないので、`Query[A]` にパラメータを足します ```scala 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) } ``` ### 改良した `Query[A]` を使って、構文のメソッドを実装していきます ```scala 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]] = lift(GetList[A](withSQL(sql).map(f).list())) def first[A](sql: SQLBuilder[_])(f: WrappedResultSet => A): FreeC[F, Option[A]] = lift(GetOption[A](withSQL(sql).map(f).first())) def execute(sql: SQLBuilder[UpdateOperation]): FreeC[F, Boolean] = lift(withSQL(sql).execute()) } 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 モナドの合成については[吉田さんの記事](http://d.hatena.ne.jp/xuwei/20140618/1403054751) が日本語でわかりやすいので参考まで。 ### DSL完成! 実は以上でDSLとしては完成です。 以下のようにfor式で各クエリを合成して `Query[A]` を取得するコードを書くことができます。 ```scala 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 を実装します Interpreter ってかっこよく言ってますが、実際のところ、`Query[A]` から何らかの `M[A]` の値を取り出す只の関数のことです。 つまり `Query ~> M` の事ですね。 `Query ~> M` は [`NaturalTransformation[Query, M]`](https://github.com/scalaz/scalaz/blob/7bbe2669267e992dc96d8e0e7e9e5d7c54a70033/core/src/main/scala/scalaz/NaturalTransformation.scala) のことです。 ```scala 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 で共通処理をまとめています。 ## FreeでのDSLの作り方まとめ 1. * -> * kind の型をつくる 1. 1.で作った型をFreeでラップした値を返すメソッドをつくる 1. Interpreter つくる ## Free-ScalikeJDBC の今後の野望 Slick3 が reactive-streams API に対応してるぜ! ってドヤ顔してるので、Free-ScalikeJDBC も scalaz-stream 使用したAPIを提供して、streamz 経由で reactive-streams API に対応してるぜ!ってドヤ顔しかえしたい(PR募集中)