circeについて

混迷極まるScalaJsonライブラリ事情ですが、最近はcirceというライブラリもメジャーになってきました。ただ個人的にはあまり仕事では採用する気にはならないライブラリです。

catsに依存している

circeの売りとして、cats以外のライブラリに依存していないというものがあるんですが、catsに依存してるという時点ですでに判断が別れるところだと思います。私はscalazやcatsは仕事ではほぼ使ったことがないので依存ライブラリがcatsに依存しているのは結構嫌です。またscalaz派から見ても同じようなライブラリが加わるのは嫌でしょう。

shapelessに依存している

なぜcirceの人気が出てきたかといえば、おそらくgenericの存在ではないでしょうか。 play-jsonでは毎回 implicit val hogeFormat = Json.Format[Hoge] を書かされるのが辛いですが、circeのgenericを使えばその煩わしさから解放されます。

ところがcirceのgenericってつまりshapelessのgenericなんですよね。従ってcirceのgenericモジュールを使うとshapelessがくっついてきます。shapelessはscalazやcats以上に難易度が高いライブラリだと思います。少なくとも implicit val hogeFormat = Json.Format[Hoge] と書きたくないばかりにshapelessを使う、というのはやり過ぎではないかと思います。

scalazに依存している

circeにはopticsというモジュールがあります。Jsonの構造をグリグリといじるのに便利なモジュールなんですが、これはMonocleというライブラリを使っています。Monocleはshapelessに加えてscalazを使っています。なんか強い奴らばっかり出てきますね。

まあopticsの機能はよくあるようなアプリケーションでは使わないでしょう。 jsonの構造を変更するとしてもそれは一旦jsonドメインオブジェクトに変更したのちに、そのドメインオブジェクトを変更し、それをjsonに戻すという手順を踏むでしょう。jsonライブラリ自信がjsonの構造をいじるというのはjson自体が重要な関心事でない限りはあまり必要にならなそうです。

以上難しいライブラリ使いたくないマンのぼやきでした。

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
Remove all ads