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 を使おう

インターフェースで副作用を分離する

techlife.cookpad.com

この記事、わざわざgemを作ったりしていて、まあそのgem自体は便利そうだからいいと思うんですが、時刻以外の場合はどういうアプローチをしてるんでしょうか。そのたびにgemを作るんでしょうか。 そんなに悩まなくとも、オブジェクト指向の引き出しがあれば一瞬で片付く問題です。

package com.example

import java.time.LocalDateTime

// Clockインターフェース
trait Clock {
  def now(): LocalDateTime
}

// デフォルトのClock実装
trait DefaultClock extends Clock {
  def now(): LocalDateTime = LocalDateTime.now()
}

// 時刻を取得する時にはClockインターフェースを通して取得する
class SpecialContent(clock: Clock) {
  def enabled: Boolean = clock.now()
    .isAfter(LocalDateTime.of(2016, 7, 1, 0, 0, 0))
}
package com.example

import java.time.LocalDateTime

import org.scalatest.FunSuite

// 固定された時間を返すClockインターフェースの実装
class FixedClock(fixedDateTime: LocalDateTime) extends Clock {
  override def now(): LocalDateTime = fixedDateTime
}

class SpecialContentTest extends FunSuite {

  test("disabled at 2016-06-30 23:59:59") {
    val content = new SpecialContent(
      new FixedClock(LocalDateTime.of(2016, 6, 30, 23, 59, 59)))
    assert(!content.enabled)
  }

  test("enabled at 2016-07-01 00:00:01") {
    val content = new SpecialContent(
      new FixedClock(LocalDateTime.of(2016, 7, 1, 0, 0, 1))))
    assert(content.enabled)
  }

}

java.time.Clock というものが存在するので自分でClockを定義する必要はないんですが、そういうのがあるないの問題じゃないので。

Java/Scalaの世界とLLな世界を行ったり来たりして文化の違いに戸惑ったことがあるのですが、 こういう外部入力とかが絡んだ場合、Java/ScalaおじさんはOOPっぽくインターフェースを使って分離したりしようとするのに対して、 LLの世界だとクックパッドの記事中にも触れられているようにtimecopみたいにメソッドごと置き換えてしまうとか、個別のアプローチを取ることが多かったです。 オブジェクト指向は無駄だと主張する人が多いんですよねえ。

またScalaな世界でもOOPではなくモナドがどうという話になることがあります。 確かに参照透過であることは良いんですが別にファンクショナルなアプローチに頼らなくてもテストは書けるし、参照透過でもテストが辛いこともあるのでまあみんな仲良くしましょう。