4.18.2015

Scala: sbt-jmh Fails when Using Doubly Nested Inner-class

Scala: 二重にネストされた内部クラスが存在すると、sbt-jmh によるコード生成が失敗する

 

環境

  • Scala: 2.11.2, 2.11.6
  • sbt-jmh: 0.1.10, 0.1.14

 

事象

コードベース内に二重にネストされた内部クラス/オブジェクトが存在している時に sbt-jmh を使うと
以下のようなエラーが発生する。

Annotation generator had thrown the exception.
java.lang.InternalError: Malformed class name
        at java.lang.Class.getSimpleName(Class.java:1195)
        at java.lang.Class.getCanonicalName(Class.java:1238)
        at org.openjdk.jmh.generators.reflection.RFClassInfo.getQualifiedName(RFClassInfo.java:67)
        at org.openjdk.jmh.generators.core.BenchmarkGenerator.buildAnnotatedSet(BenchmarkGenerator.java:244)
        at org.openjdk.jmh.generators.core.BenchmarkGenerator.generate(BenchmarkGenerator.java:110)
        at org.openjdk.jmh.generators.bytecode.JmhBytecodeGenerator.main(JmhBytecodeGenerator.java:100)
        at pl.project13.scala.sbt.SbtJmh$.generateBenchmarkJavaSources(SbtJmh.scala:85)
        at pl.project13.scala.sbt.SbtJmh$$anonfun$jmhSettings$6.apply(SbtJmh.scala:28)
        at pl.project13.scala.sbt.SbtJmh$$anonfun$jmhSettings$6.apply(SbtJmh.scala:28)
        at scala.Function1$$anonfun$compose$1.apply(Function1.scala:47)
        at sbt.$tilde$greater$$anonfun$$u2219$1.apply(TypeFunctions.scala:40)
        at sbt.std.Transform$$anon$4.work(System.scala:63)
        at sbt.Execute$$anonfun$submit$1$$anonfun$apply$1.apply(Execute.scala:226)
        at sbt.Execute$$anonfun$submit$1$$anonfun$apply$1.apply(Execute.scala:226)
        at sbt.ErrorHandling$.wideConvert(ErrorHandling.scala:17)
        at sbt.Execute.work(Execute.scala:235)
        at sbt.Execute$$anonfun$submit$1.apply(Execute.scala:226)
        at sbt.Execute$$anonfun$submit$1.apply(Execute.scala:226)
        at sbt.ConcurrentRestrictions$$anon$4$$anonfun$1.apply(ConcurrentRestrictions.scala:159)
        at sbt.CompletionService$$anon$2.call(CompletionService.scala:28)
        at java.util.concurrent.FutureTask.run(FutureTask.java:262)
        at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:471)
        at java.util.concurrent.FutureTask.run(FutureTask.java:262)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
        at java.lang.Thread.run(Thread.java:745)

ベンチマークに入る前の sbt run の初期化処理で sbt 自体が落ちてしまうため、ヘルプ (-h) やベンチマークの一覧 (-l) を見ることもできない。

 

原因

Scala コンパイラ本体の問題 [SI-2034] に起因する。

sbt-jmh は、Java のライブラリである jmh を利用するため、sbt run のタイミングでリフレクションを使って Scala コードから Java のコードを生成している。

org.openjdk.jmh.generators.bytecode.JmhBytecodeGenerator の処理の中で、全てのクラス名が java.lang.Class.getCanonicalName によって調べ上げられるのだが、これが上記のバグにより二重ネストされた内部クラスに対して正しく動作せず、java.lang.InternalError: Malformed class name が引き起こされてしまう。

 

回避策

この現象は Javaライブラリである jmh の処理で起こるので、sbt-jmh 側の修正で対応するのはかなり難しそうだ。

1. 内部クラスの二重ネストを避ける

これから作るものに関しては、極力ネストは避けたほうがいいのだろう。

2. 無名クラスに置き換える

以下のような変換を行えば、とりあえずは対応前とほぼ同じ挙動となるはずだ。

  • 対応前
    object A {
      object B {
        object C {
          val x = 123
        }
      }
    }
  • 対応後
    object A {
      object B {
        val C = new {
          val x = 123
        }
      }
    }

しかし、例えばこの場合に A.B.C.x と呼び出すと、以下のようなコンパイル時警告が出てしまう。
(scalac に -feature オプションを付けると警告の内容を見られる)

[warn] /path/to/example-sbt-jmh/src/main/scala/com/github/mogproject/example.scala:9:
 reflective access of structural type member value x should be enabled
[warn] by making the implicit value scala.language.reflectiveCalls visible.
[warn] This can be achieved by adding the import clause 'import scala.language.reflectiveCalls'
[warn] or by setting the compiler option -language:reflectiveCalls.
[warn] See the Scala docs for value scala.language.reflectiveCalls for a discussion
[warn] why the feature should be explicitly enabled.
[warn]   val x = A.B.C.x
[warn]                 ^

この警告を回避するには、以下いずれかの対応が必要となる。

  • A.B.C.x を呼び出す箇所全てに import scala.language.reflectiveCalls を追加する
  • scalac のオプションに -language:reflectiveCalls を追加する

 

 

Related Posts

4.12.2015

Building CentOS 5.8 Desktop VM with Packer

Packer で CentOS 5.8 デスクトップ環境の仮想マシンを作る

 

必要に迫られて、CentOS 5.8 のレガシーなデスクトップ環境を VirtualBox VM で作った。

s 

kickstart + Packer + Vagrant でサクッと構築。

 

改めて感じる、HashiCorp の偉大さ。

4.10.2015

Scala: Property-Based Testing with DateTime

Scala: DateTime型のプロパティテスト

Specs2 + ScalaCheck の例。

 

依存ライブラリ

libraryDependencies ++= Seq(
  "com.github.nscala-time" %% "nscala-time" % "1.8.0",
  "org.specs2" %% "specs2-core" % "3.4" % "test",
  "org.scalacheck" %% "scalacheck" % "1.12.2" % "test"
)

 

実装例

Arbitrary ベース
import com.github.nscala_time.time.Imports.DateTime
import org.scalacheck.{Gen, Arbitrary}
import org.specs2.ScalaCheck
import org.specs2.mutable.Specification

class DateTimeSpec extends Specification with ScalaCheck {

  implicit val arbDateTime: Arbitrary[DateTime] =
    Arbitrary(Gen.choose(0L, Long.MaxValue).map(new DateTime(_)))

  "DateTime" should {
    "do something" in prop { t: DateTime =>
      t must ???  // write tests here
    }
  }
}

 

Gen ベース
import com.github.nscala_time.time.Imports.DateTime
import org.scalacheck.{Gen, Prop}
import org.specs2.ScalaCheck
import org.specs2.mutable.Specification

class DateTimeSpec extends Specification with ScalaCheck {

  private val genDateTime = Gen.choose(0L, Long.MaxValue).map(new DateTime(_))

  "DateTime" should {
    "do something" in Prop.forAll(genDateTime) { t: DateTime =>
      t must ???  // write tests here
    }
  }
}

 

Related Posts

4.06.2015

Scala: Dependency Injection with DynamicVariable

Scala: DynamicVariable を利用した DI

Dependency Injection (DI/依存性の注入) とは

依存性の注入 - Wikipedia

DI の種類 (例)

  • コンストラクタ DI
  • インタフェース DI
  • setter DI

 

Scala における DI

いくつかの手法があり、多くの議論を生み出してきた。

 

参考

 

モチベーション

  • ユニットテストの際に、外部サービスをモックに切り替えたい
    • そんなに複雑な依存関係はない
  • 「インジェクトしたいコンポーネントの利用者」の利用者のコードは変えたくない
  • 実行時効率も考える
    • 無駄な初期化が何度も行われるのは避ける
  • 学習コストは小さいほうがいい

 

DynamicVariable とは

あまり脚光を浴びることはない(?)が、標準で Scala に組み込まれているクラス。

 

DynamicVariable を利用した DI の例

 

サンプル・アプリケーション

あるキャッシュサービスに対して、外部サービスとモックの切り替えを DI で行うケースを考える。

  • インジェクト対象: CacheService
  • CacheService の利用者: ImageCache
  • ImageCache を他のロジックが使用

といった構成とする。

アイデア
  • implicit parameter を使った コンストラクタ DI + Factory パターンがベース => ほぼ こちら の写経
  • Factory の状態を グローバルな DynamicVariable として保持し、目的に応じて Factory を切り替えて使う
    • 各コンポーネントの初期化処理は、利用する Factory の個数分だけで済むように

 

コード例

 

インジェクト対象

CacheService は適当に実装されているものとする。

trait CacheService {
  def get(key: String): String
}
object RealCache extends CacheService {
  def get(key: String): String = ???
}
object MockCache extends CacheService {
  def get(key: String): String = ???
}

 

コンポーネント利用者

implicit なコンストラクタ・パラメータを受け取るクラスを作る。

class ImageCache(implicit cacheService: CacheService) {
  def read(key: String): String = cacheService.get(key)
}

 

コンポーネントの管理

環境に応じたコンポーネントのセットを Repositories として定義する。

sealed trait Repositories {
  implicit def cacheService: CacheService
}

case object DefaultRepositories extends Repositories {
  lazy val cacheService = RealCache
}

case object MockRepositories extends Repositories {
  lazy val cacheService = MockCache
}

 

ファクトリの作成
  • Factory.currentVar に Factory クラスを継承したオブジェクトを DynamicVariable として保持する
  • withMock という関数を作り、一時的に環境を MockFactory に切り替えて処理を実行できるようにする
  • Factory.imageCache は毎回評価が行われるように、関数として定義する。(val や lazy val では NG)
sealed abstract class Factory(repos: Repositories) {
  import repos._

  lazy val imageCache = new ImageCache
}

// concrete factories
case object DefaultFactory extends Factory(DefaultRepositories)
case object MockFactory extends Factory(MockRepositories)

// global accessor
object Factory {
  private[this] val currentVar = new DynamicVariable[Factory](DefaultFactory)

  def withMock[T](thunk: => T) = currentVar.withValue(MockFactory)(thunk)

  def imageCache = currentVar.value.imageCache
}

 

コンポーネント利用クラスを使う

インスタンス生成は、必ずファクトリ経由で行うようにする。

trait Image {
  def readCache = Factory.imageCache.read("abc")
}

 

ユニットテストを行う

明示的に Factory.withMock を指定するので、処理の流れがわかりやすいと思う。
「コンポーネント利用クラス」を利用する処理も同じ書式で書けるのが嬉しい。

"ImageCache#read" should {
  "be xxx" in {
    Factory.withMock {
      Factory.imageCache.read("abc") must_== "xxx"
    }
  }
}

 

いずれもまだアイデア段階のものである。考慮漏れ and/or もっと良い方法があるかもしれない。