Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
0
Help us understand the problem. What is going on with this article?
@ippei-takahashi

atnos-effを使ってRead/Writeを合成可能なトランザクションEffを作る

はじめに

DB操作を行う際、ReadのみかWriteを伴うかに応じて処理を分けたい場合があります。これらをコンパイル時に判定できれば、Masterに問い合わせるべきクエリをSlaveに発行してしまうような事故を未然に防ぐことができます。

具体的には、以下の機能が欲しいです。

  • Read/Writeを型で表現する
  • ReadとWriteを合成するとWriteになる

このような合成可能トランザクションモナドとしてはFujitaskが有名ですが、本記事ではatnos-effを使ったトランザクションEffを考案します。

作用の変換

ReadとWriteを区別するため、次のように型を定義します。

// 使っているライブラリのものに置き換える
type DBIO[A] = MyDBIO[A]

case class DBIORead[A](value: DBIO[A])
case class DBIOWrite[A](value: DBIO[A])

type _dbior[R] = DBIORead |= R
type _dbiow[R] = DBIOWrite |= R

Effを利用する側のコードは以下のようになると思います。

def runReadWriteSample: Option[User] = ({
  type R = Fx.fx2[DBIORead, DBIOWrite]
  for {
    _ <- fromDBIOWrite(AddUser(User("hoge", "hoge fuga", 20)))
    userOption <- fromDBIORead(FindUserById("hoge"))
  } yield userOption
}).runDBIO.runTransactionDBIO.run

しかしながら、このようなコードではReadトランザクションとWriteトランザクションをばらばらに実行することになり、期待した挙動にはなりません。

そこで、作用DBIOReadを作用DBIOWriteに変換することを考えます。atnos-effでは、次のように記述することで作用を変換できます。

implicit def fromDBIOReadToWrite: DBIORead ~> DBIOWrite =
  new (DBIORead ~> DBIOWrite) {
    override def apply[X](x: DBIORead[X]): DBIOWrite[X] =
      DBIOWrite(x.value)
  }

implicit def deriveDBIOMember[R](
  implicit member: MemberIn[DBIOWrite, R]
): MemberIn[DBIORead, R] = member.transform[DBIORead]

こうした変換コードを記述することで、ReadとWriteを合成して1つの作用として振る舞います。

def runReadWriteSample: Option[User] = ({
  type R = Fx.fx1[DBIOWrite]
  for {
    _ <- fromDBIOWrite[R, Unit](AddUser(User("hoge", "hoge fuga", 20)))
    userOption <- fromDBIORead[R, Option[User]](FindUserById("hoge")) // 同じfor内でread/writeを合成してwriteになっている
  } yield userOption
}).runTransactionDBIO.run

ここで仮にtype R = Fx.fx1[DBIORead]と書くとコンパイルエラーになります。また、この場合はfromDBIOWrite等の型引数を省略して自動的にDBIOWriteに推論することもできます。ただし、ReadOnlyな場合にtype R = Fx.fx1[DBIOWrite]と書いてしまうと予期せずDBIOWriteに変換されるので注意が必要です。

その他の応用

作用の変換はとても強力な機能で、他にも様々な応用が考えられます。例えば、Aの権限が必要が必要な操作 + Bの権限が必要な操作 = A+Bの権限が必要な操作のようなルールの権限Effは、HListと組み合わせることで次のような変換コードにより実現できます。

implicit def fromAuthZHNil: AuthZ[HNil, *] ~> AuthZ[P, *] =
  new (AuthZ[HNil, *] ~> AuthZ[P, *]) {
    override def apply[X](fa: AuthZ[HNil, X]): AuthZ[P, X] = fa match {
      case Authorize(userId, scope) =>
        Authorize[P](userId, scope)
    }
  }

implicit def fromAuthZHCons[H, T <: HList](
  implicit selector: Selector[P, H],
  tail: AuthZ[T, *] ~> AuthZ[P, *]
): AuthZ[H :: T, *] ~> AuthZ[P, *] = new (AuthZ[H :: T, *] ~> AuthZ[P, *]) {
  override def apply[X](fa: AuthZ[H :: T, X]): AuthZ[P, X] = fa match {
    case a: Authorize[H :: T] =>
      Authorize[P](a.userId, a.scope)
  }
}

implicit def deriveAuthZMember[T[_], R](
  implicit member: MemberIn[AuthZ[P, *], R],
  t: T ~> AuthZ[P, *]
): MemberIn[T, R] = member.transform[T]

サンプルコード

transaction-eff-sample

0
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
0
Help us understand the problem. What is going on with this article?