Scalaのcase classに副作用のある振る舞いを持たせる時のパターン

不変オブジェクトを使ったプログラムを書く時に便利なcase classですが、case classのメリットを生かしつつ副作用を扱うためにはどのような書き方をするのが良いでしょうか。具体的には外部APIやデータベースへのアクセスをcase classを使ってどう実装するのが良いか考えます。

例として次のような簡単なUserクラスを用意します。

case class User(id: Int, name: String)

Userの名前を変更するメソッドを追加してみます。これはcopyメソッドを使えば簡単です。ここまではサンプルコードでよく目にするような例です。

case class User(id: Int, name: String) {
  def rename(name: String): User = copy(name = name)
}

さて、このユーザー情報をデータベースに保存することにしましょう。保存処理自体はUserRepositoryというクラスを利用します。次の例はあまり良くない例です。

case class User(id: Int, name: String) {
  def rename(name: String): User = copy(name = name)
  def save(): Unit = UserRepository.save(this)
}

object UserRepository {
  def save(user: User): Unit = ...
}

UserRepositoryはシングルトンとして実装しました。簡単ですがUser#saveは副作用を伴うメソッドで、テストをするのにデータベースの管理までが必要になって面倒です。

こういった副作用を分離するために関数型っぽいアプローチを取ることもできますが、ここではJavaっぽくインタフェースで副作用のある処理との結合を切り離し、副作用を制御できるようにします。

trait UserRepository {
  def save(user: User): Unit
}

class UserRepositoryImpl extends UserRepository {
  def save(user: User): Unit = ...
}

class UserRepositoryMock extends UserRepository {
  var users: Seq[User] = Seq.empty
  def save(user: User): Unit = users :+ user
}

UserRepositoryをインタフェースとして実装し、Userクラスにはこのインタフェースに対しての実装を書くことにしました。これでテストの時にMockを使えばDBの管理まで考える必要がなくなります。

さて、このUserRepositoryをcase classに組み込むとどうなるでしょうか。

case class User(
    id: Int,
    name: String,
    userRepository: UserRepository) {
  def rename(name: String): User = copy(name = name)
  def save(): Unit = userRepository.save(this)
}

コンストラクタでUserRepositoryを渡してみました。しかしこれはNGです。case classには自動でequalsメソッドが付いてきますが、そのメソッドは比較対象である2つのcase classのフィールドが同じかを見ています。ということはUserRepositoryが何を持って同値とするかを決めておかないとまずいのです。つまりUserRepositoryのequalsを実装する、という作業が必要ですが正直面倒。そもそもUserRepositoryに同値性を持たせることは必要だろうか?

コンストラクタでは無くメソッドの引数で渡すというのはどうでしょうか。

case class User(
    id: Int,
    name: String) {
  def rename(name: String): User = copy(name = name)
  def save(userRepository: UserRepository): Unit = userRepository.save(this)
}

これはcase classの使い方として間違ってはいないですが、どうせそのUserインスタンスではいつも同じUserRepositoryを使うはずなのに、メソッド引数で毎回指定するというのは正直面倒です。そこでこの引数をimplicitにして引数を省略…とかやると破滅するのでやめましょう。

で、私の結論ですが、case classに副作用を混ぜようとすることを諦めて、補助的なクラスに切り出せば良いと思います。Userの操作なのでUserOpsとかでいいかな。

case class User(
    id: Int,
    name: String) {
  def rename(name: String): User = copy(name = name)
}

class UserOps(userRepository: userRepository) {
  def save(user: User): Unit = userRepository.save(user)
}

これならcase classのequalsを壊すこともないし、テストもしやすい。主観だけどコンパニオンオブジェクトみたいなものと捉えれば違和感もあまりない。

ついでに、DIコンテナの恩恵を受けるのが簡単というメリットもあります。

case class User(
    id: Int,
    name: String) {
  def rename(name: String): User = copy(name = name)
}

class UserOps @Inject() (userRepository: userRepository) {
  def save(user: User): Unit = userRepository.save(user)
}

ドメイン駆動設計ではよくデータと振る舞いが一緒にあるべきと言われますが、1つのクラスに押し込める必要はないかと思います。この場合はUser+UserOpsで1つのUserモデルと考えることになります。汎用的で使いやすいパターンではないかと思います。

というわけでタイトルは嘘で「Scalaのcase classに副作用のある振る舞いを持たせないパターン」でした。

typesafe configの設定パスをscalaのコードで表現する

ScalaMatsuriの感想ブログです

ScalaMatsuriでscala.metaの話を2つ聞いて面白そうと思ったので私もやってみました。 typesafe configをscala.metaとscalameta/paradiseのmacro annotationで設定パスを文字列ではなくscalaのコードで表現できるようにしたやつです。

github.com

こんな感じで使います。

// src/main/resources/application.conf
akka {
  actor {
    serializers {
      akka-containers = "akka.remote.serialization.MessageContainerSerializer"
    }
  }
}
import com.typesafe.config.ConfigFactory
import com.github.tototoshi.configpath.compile

@compile("src/main/resources/application.conf")
object path

object Example {

  def main(args: Array[String]): Unit = {
    val config = ConfigFactory.load()
    val serializer1 = config.getString(
      path.akka.actor.serializers.`akka-containers`.full)
    val serializer2 = config.getString(
      "akka.actor.serializers.akka-containers")
    assert(serializer1 == serializer2)
  }

}

objectかclassにcompileアノテーションをつけて、typesafe configの設定ファイルパスを渡すとmacroでコードを生成します。 上記のような設定ファイルがあった時、次のコードは

@compile("src/main/resources/application.conf")
object path

次のように展開されています。

object path {
  abstract class ConfigTree(val name: String, val full: String)
  object `akka` extends ConfigTree("akka", "akka") {
    object `actor` extends ConfigTree("actor", "akka.actor") {
      object `serializers` extends ConfigTree("serializers", "akka.actor.serializers") {
        object `akka-containers` extends ConfigTree("akka-containers", "akka.actor.serializers.akka-containers")
      }
    }
  }
}

これで設定の名前を間違えることがなくなりました。すごい。IntelliJがマクロに弱くて赤くなるけどね。IntelliJが追いついて補完が効くようになれば意外と便利かもしれない。

Scalaで#map系メソッドで副作用を起こすとバグるやつ

遅延評価と副作用は相性悪いよねって話です。

Scalaでforeachではなくmapの中で副作用を起こすとたまに評価タイミングによるわかりづらいバグに遭遇することがあります。

次のコードはMap#mapValuesの中でscalikejdbcで書き込みを行おうとするコードです。 一見うまくいくようで、エラーになります。手元では java.sql.SQLException: Connection is null. というエラーが発生しています。

import scalikejdbc._

val contents = DB.localTx { implicit session =>
  data.mapValues { s =>
    val text = s * 2
    sql"insert into test_table(text) values ($text)".update().apply()
    text
  }
}

contents.foreach(println)

mapValuesに渡している関数の処理はcontentsが評価されるまで行われません。つまり contents.foreach(println) の処理が行われるタイミングでDBへの書き込みを実行するのでその時にはトランザクションがcommitされてしまっているのです。

ちなみにこれはあくまでmapValuesの実装がそうなっているからで、mapでは起きませんでした。そんな実装依存でいいのかって話ですが、そんなこと言うと副作用はダメだ、参照透過なら何も問題ないだろ!と怒られます。怖いですね。

これは .view.force とやってその場で評価させるとうまくいっちゃいます。

import scalikejdbc._

val contents = DB.localTx { implicit session =>
  data.mapValues { s =>
    val text = s * 2
    sql"insert into test_table(text) values ($text)".update().apply()
    text
  }.view.force
}

contents.foreach(println)

いや、なんかひどいですねこれ。.view.force ?意味ないじゃん、と思って消すとバグります。 やっぱり普通に副作用は分けて、mapじゃなくてforeachにしましょう。

val texts = data.mapValues { s => s * 2 }
DB.localTx { implicit session =>
  texts.foreach { case (k, v) =>
    sql"insert into test_table(text) values ($v)".update().apply()
  }
}

for式の中のEitherでifを使いたい

scala 2.12ではJava8対応の変更が多く、派手な変更は多くはありませんでした。そんな中でEitherがright-biasedになったのは割とキャッチーなのではないでしょうか。Eitherには今までflatMapなどのメソッドがなかったので、for式の中で使うときなど、いったんRightProjectionに変換する必要があったのですが、それが必要なくなりました。

for {
  x <- Right(3).right
} yield x

これが

for {
  x <- Right(3)
} yield x

こう書けるようになりました。

これは便利ですね。まあrightが取れるだけなんですが、私もよく .right つけろよとコンパイラに怒られていたので嬉しいです。

さて、これでscalaのEitherが実用的になったという声も聞こえるのですが、実際 .right が不要になったから実用的かというと疑問で、どちらかというとscalaのEitherで困るのはfor式の中でifが使えないことだと思います。私はwithFilter問題と勝手に呼んでいてRightProjectionにwithFilterがないせいなんですが、この問題は2.12.xでもまだ健在です。

scala> for {
     |   x <- Right(3)
     |   if x % 2 == 1
     |   y = x + 1
     | } yield y
<console>:13: error: value withFilter is not a member of scala.util.Right[Nothing,Int]
         x <- Right(3)
                   ^

ほら、if使えないでしょ。これは困りますね。そこでEitherに限った方法ではないんですが、ちょっとヘルパーを定義します。

def when[E](p: Boolean)(e: => E): Either[E, Unit] =
  if (p) Right(()) else Left(e)

このwhenはpの条件を満たせばRightを返すのでそれ以下のfor式も実行されます。満たさなければLeftを返してエラーにします。

scala> val result = for {
     |   x <- Right(3)
     |   _ <- when(x % 2 == 1)("error")
     |   y = x + 1
     | } yield y
result: scala.util.Either[String,Int] = Right(4)

scala> val result = for {
     |   x <- Right(3)
     |   _ <- when(x % 2 == 0)("error")
     |   y = x + 1
     | } yield y
result: scala.util.Either[String,Int] = Left(error)

これでEitherでもifっぽいことをできるようになり、めでたしです。

追記

@gakuzzzzさんにEither.condの存在を教えられた。

def when[E](p: Boolean)(e: => E): Either[E, Unit] =
  Either.cond(p, (), e)

Scala関西 Summit 2016に参加しました

f:id:tototoshi:20161009234716p:plain

sbt再入門ということで、sbtの主にKeyとScopeについてデモを中心に発表させていただきました。 デモを中心にするのは自分としては挑戦のつもりだったけれど、やっぱり難しくて心残りなところもありました。 とはいえ発表中に実際にsbtを触りつつ話を聞いてくれてる方が多かったのは嬉しかったです。 スライドの内容は全てsbtのドキュメントにあるので特に公開する予定はありません。かわりにドキュメントを読んでもらえればと思います。

イベントはとても楽しかったのですが、全体の空気は去年よりトーンが落ちたというか、淘汰されてしまった印象がありました。切ない。関西勢の発表が少なかったからでしょうか。そう考えるとScala盛り上がってる感出すにはとりあえずCFP出すことって大事ですね。自分も次のScalaMatsuriはどうしようか迷っていたんですがとりあえず出してみようと思います。ElixirとFregeどっちが良いでしょうか。

きの子さんをはじめ、スタッフの方々、ありがとうございました。きの子さんのテンションと声の通りを前にするとやや自信を失くします。私は東で生きていきます。

画像は偶然見つけたパチンコ屋さんです。派手ですね。

giter8 & sbt new

最近はactivator newを使っている人が多いんだと思いますが、5年くらい前?はgiter8というツールが使われていました。 giter8は一時期Typesafe Stackにも含まれるほどだったんですが、なぜか見捨てられてしまいます。 その後皆さんご存知の通りactivator newに取って代わられ、開発も下火になり、知る人ぞ知るみたいな存在になってしまいましたが、少し前にpamflet、conscriptとともにfoundweekendsに拾われ、メンテナンスが継続されていくことになりました。

github.com

foundweekendsは@eed3si9nさんが作ったorganizationですが、@eed3si9nさんはsbtの開発者でもあります。そんなこんなでgiter8がsbtで使えるようになりました。sbt 0.13.13-RC1から new コマンドが追加されたのですが、 new コマンドはgiter8をサポートしています。

sbt 0.13.13-RC1のlauncherを使うとgiter8テンプレートからプロジェクトを作成することができます。

$ mkdir hello
$ cd hello
$ sbt new tototoshi/hello.g8 # https://github.com/tototoshi/hello.g8

さて、giter8を触ったことがない人も最近は多いと思うのでgiter8テンプレートの作り方も紹介しておこうと思います。 昔はレイアウトを作る少し面倒だったんですが、今はプロジェクト名の置換などの気の利いたことをしないのであればごく普通のリポジトリを作り、名前の最後に.g8とつけて終了です。

http://www.foundweekends.org/giter8/template.html#root+layout

activatorとの使い分けという疑問がわくかもしれませんが、activatorのアンインストールをすることで解決できます。最初からいらなかったんです。

$ bew uninstall --force typesafe-activator

7つのクラスローダー 7つの世界

Playを触っていると謎のClassNotFoundExceptionが発生することがあります。 もしかしてそれはDEVモードだけで起きてはいないでしょうか?

先日、開発中のPlayアプリケーションで、ある依存ライブラリからClassNotFoundExceptionが発生するという現象にあたりました。 そのClassNotFoundExceptionはObjectInputStream.readObjectを呼び出した時に起きるのですが、なぜこのようなことが起きるのでしょう。

それではここでPlayのソースコードにある深イイコメントを見てみましょう。

/*
 * We need to do a bit of classloader magic to run the Play application.
 *
 * There are seven classloaders:
 *
 * 1. buildLoader, the classloader of sbt and the Play sbt plugin.
 * 2. commonLoader, a classloader that persists across calls to run.
 *    This classloader is stored inside the
 *    PlayInternalKeys.playCommonClassloader task. This classloader will
 *    load the classes for the H2 database if it finds them in the user's
 *    classpath. This allows H2's in-memory database state to survive across
 *    calls to run.
 * 3. delegatingLoader, a special classloader that overrides class loading
 *    to delegate shared classes for build link to the buildLoader, and accesses
 *    the reloader.currentApplicationClassLoader for resource loading to
 *    make user resources available to dependency classes.
 *    Has the commonLoader as its parent.
 * 4. applicationLoader, contains the application dependencies. Has the
 *    delegatingLoader as its parent. Classes from the commonLoader and
 *    the delegatingLoader are checked for loading first.
 * 5. docsLoader, the classloader for the special play-docs application
 *    that is used to serve documentation when running in development mode.
 *    Has the applicationLoader as its parent for Play dependencies and
 *    delegation to the shared sbt doc link classes.
 * 6. playAssetsClassLoader, serves assets from all projects, prefixed as
 *    configured.  It does no caching, and doesn't need to be reloaded each
 *    time the assets are rebuilt.
 * 7. reloader.currentApplicationClassLoader, contains the user classes
 *    and resources. Has applicationLoader as its parent, where the
 *    application dependencies are found, and which will delegate through
 *    to the buildLoader via the delegatingLoader for the shared link.
 *    Resources are actually loaded by the delegatingLoader, where they
 *    are available to both the reloader and the applicationLoader.
 *    This classloader is recreated on reload. See PlayReloader.
 *
 * Someone working on this code in the future might want to tidy things up
 * by splitting some of the custom logic out of the URLClassLoaders and into
 * their own simpler ClassLoader implementations. The curious cycle between
 * applicationLoader and reloader.currentApplicationClassLoader could also
 * use some attention.
 */

Playはいわゆるソースコードのホットリロード機能などを実現するために、DEVモードではなんと7つのクラスローダーが駆使されているんですね。わー、すごい。

さて、この中で重要なのは 4.applicationLoader と 7.reloader.currentApplicationClassLoader です。 applicationLoaderはライブラリをロードされているクラスローダー、一方reloader.currentApplicationClassLoaderはapplicationLoaderを親とするクラスローダーで、アプリケーションコードを読み込んでいます。

と、いうことは、Playアプリケーションのコードってreloader.currentApplicationClassLoaderにしか見えていないことになります。依存ライブラリ内のObjectInputStream.readObjectはapplicationLoaderを使っているからまあそりゃClassNotFoundExceptionが起きますね。

解決方法は、依存ライブラリがreloader.currentApplicationClassLoaderを使うようにすることです。依存ライブラリに修正入れなきゃいけないのがイケてないですがまあ仕方ない。この場合はただのObjectInputStreamの替わりに

class ObjectInputStreamWithCustomClassLoader(
  stream: InputStream,
  customClassloader: ClassLoader
) extends ObjectInputStream(stream) {
  override protected def resolveClass(cls: ObjectStreamClass) = {
    Class.forName(cls.getName, false, customClassloader)
  }
}

のように利用するクラスローダーを選べるObjectInputStreamを作って、reloader.currentApplicationClassLoaderにあたるplay.api.Environment#classLoaderを利用します。これでClassNotFoundExceptionは出なくなります。めでたし、めでたし。

まとめ

  • We need to do a bit of classloader magic to run the Play application.
  • play.api.Environment#classLoader を使おう