Skip to content

Instantly share code, notes, and snippets.

@gakuzzzz
Last active August 10, 2021 08:50
Show Gist options
  • Select an option

  • Save gakuzzzz/147c520e32177fea75f0 to your computer and use it in GitHub Desktop.

Select an option

Save gakuzzzz/147c520e32177fea75f0 to your computer and use it in GitHub Desktop.

Revisions

  1. gakuzzzz revised this gist Aug 10, 2021. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion slide.md
    Original file line number Diff line number Diff line change
    @@ -407,7 +407,7 @@ object Interpreter {

    ## FreeでのDSLの作り方まとめ

    1. * -> * kind の型をつくる
    1. `* -> *` kind の型をつくる
    1. 1.で作った型をFreeでラップした値を返すメソッドをつくる
    1. Interpreter つくる

  2. gakuzzzz revised this gist Jul 25, 2015. 1 changed file with 12 additions and 3 deletions.
    15 changes: 12 additions & 3 deletions slide.md
    Original 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]] = 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())
    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]
  3. gakuzzzz revised this gist Jul 25, 2015. 1 changed file with 4 additions and 0 deletions.
    4 changes: 4 additions & 0 deletions slide.md
    Original 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

  4. gakuzzzz revised this gist Jul 25, 2015. 1 changed file with 4 additions and 0 deletions.
    4 changes: 4 additions & 0 deletions slide.md
    Original file line number Diff line number Diff line change
    @@ -12,6 +12,10 @@

    ## ScalikeJDBCとは

    http://scalikejdbc.org/

    ![scalikejdbc](http://scalikejdbc.org/images/logo.png)

    * JDBC を Scala から使いやすくするためのライブラリ
    * なるべくDRYかつミスを少なくSQLを書けるように

  5. gakuzzzz revised this gist Jul 25, 2015. 1 changed file with 1 addition and 0 deletions.
    1 change: 1 addition & 0 deletions slide.md
    Original file line number Diff line number Diff line change
    @@ -394,6 +394,7 @@ object Interpreter {
    1. 1.で作った型をFreeでラップした値を返すメソッドをつくる
    1. Interpreter つくる

    簡単ですね!!!

    ## Free-ScalikeJDBC の今後の野望

  6. gakuzzzz revised this gist Jul 25, 2015. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion slide.md
    Original 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]` にパラメータを足します
    ### 5. 現状だと、構文のメソッドが `SQLBuilder` を受け取っているけど `Query[A]` がそれを使えないので、`Query[A]` にパラメータを足します

    ```scala
    sealed abstract class Query[A](private[free] val statement: String, private[free] val parameters: Seq[Any])
  7. gakuzzzz revised this gist Jul 25, 2015. 1 changed file with 8 additions and 8 deletions.
    16 changes: 8 additions & 8 deletions slide.md
    Original file line number Diff line number Diff line change
    @@ -218,19 +218,19 @@ Functor とか Monad とかは知ってるものとして話を進めるので

    ## Free モナドを使った DSL の作り方

    ### まず合成したい単位を表す型を定義します
    ### 1. まず合成したい単位を表す型を定義します

    ```scala
    sealed abstract class Query
    ```

    ### これだけでは値を返せないしFreeにもできないので、型引数を足します
    ### 2. これだけでは値を返せないしFreeにもできないので、型引数を足します

    ```scala
    sealed abstract class Query[A]
    ```

    ### 作成した型の実装としてDSLの構文木を表すクラスorオブジェクトを定義します
    ### 3. 作成した型の実装としてDSLの構文木を表すクラスorオブジェクトを定義します

    ```scala
    sealed abstract class Query[A]
    @@ -241,7 +241,7 @@ object Query {
    }
    ```

    ### DSLとしての構文をFreeを返すメソッドとして定義します
    ### 4. DSLの構文としてFreeを返すメソッドを定義します

    ここでは Scalaz7.1.x を使います。

    @@ -270,7 +270,7 @@ type FreeC[S[_], A] = Free[Coyoneda[S, ?], A]

    `Coyoneda` を使って `Free` を作るアイデアは非常に便利で `Operational Monad` という名前でも知られています。

    1. 現状だと、構文のメソッドが SQLBuilder` を受け取っているけど `Query[A]` がそれを使えないので、`Query[A]` にパラメータを足します
    ### 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 {
    }
    ```

    ### 改良した `Query[A]` を使って、構文のメソッドを実装していきます
    ### 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) が日本語でわかりやすいので参考まで。


    ### DSL完成!
    ### 7. DSL完成!

    実は以上でDSLとしては完成です。

    @@ -326,7 +326,7 @@ Free-ScalikeJDBC では `Coproduct` を使って、他の Free モナドを利
    でも `Query[A]` インスタンスがあるだけでは何もできないので困ってしまいます。


    ### Interpreter を実装します
    ### 8. Interpreter を実装します

    Interpreter ってかっこよく言ってますが、実際のところ、`Query[A]` から何らかの `M[A]` の値を取り出す只の関数のことです。

  8. gakuzzzz created this gist Jul 25, 2015.
    402 changes: 402 additions & 0 deletions slide.md
    Original 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募集中)