JavaライブラリのScalaラッパーを書くときのTIPS

これは Scala Advent Calendar 2015 - Adventar の18日目の記事です。

Scalaでは特に何もしなくてもJava製のライブラリをそのまま利用することができます。 ただ、そのまま使うよりもScalaでラップしたライブラリを作ることで使いやすさが向上することが多いです。

自分もそのためにライブラリをいくつか開発しています。

この記事では、JavaライブラリをScalaでラップする時に使えるテクニックを見ていきたいと思います。

ラップする方法

最初に、ひとくちにラップするといってもやり方としては複数あると思います。

違う型として定義する

まず、ラップしたい型を内部に持ってメソッド呼び出しを委譲する形が考えられます。

Play FrameworkのConfiguration などがそうですね。 上で紹介したconfigsと同じくTypesafe configのラッパーですが、configを内部に持ったものになっています。(普通にアクセスはできますが)

完全に別の型なので自由にメンバーを定義できる一方、便利に使うためには必要なものも全部定義していかなければいけません。

implicit classを利用する

configsはこちらの方式で、ラップしたい型(com.typesafe.config.Config)に対してimplicit classを用いてメソッドを追加したように見せています。 なのでラップというよりは拡張といったほうが正しいかもしれません。

元々の型のメンバーや周辺のAPIをそのまま利用できますが、 implicit classのメソッドは元の物と名前が被らないように定義したり、 利用者がどのメソッドを使うべきなのかを覚える必要があります。

nullの代わりにOptionを利用する

それでは、基本的なところから。

Javaのライブラリはメソッドの戻り値としてnullを返してくることがままあります。*1 そのまま使うとNullPointerExceptionと戦うことになるので、nullを返す可能性のあるものはOptionでくるみましょう。

public String someNullableMethod(int n);
def someMethod(n: Int): Option[String] = Option(someNullableMethod(n))

Javaのどのメソッドがnullを返すのかはドキュメントや実装を見て判断します(つらい)

Javaコレクション→Scalaコレクションへ変換する

ScalaではJavaコレクションよりもScalaコレクションの方が扱いやすいです。 引数及び返り値にJavaコレクションが出現する場合、Scala標準ライブラリに相互変換する機能が入っているのでそれを使ってScalaコレクションにしましょう。

public List<String> javaCollectionMethod(List<String> xs);
import scala.collection.convert.decorateAll._

def scalaCollectionMethod(xs: List[String]): List[String] =
  javaCollectionMethod(xs.asJava).asScala

importはscala.collection.JavaConverters._でもいいですが、 scala.collection.convertにある物のほうが細かくimplicit conversionを制御できるのでこっちを使っています。

FunctionalInterfaceの扱い

Java8からFunctionalInterface(あるいはSAM)という概念(?)が追加され、FunctionalInterfaceを要求する位置にラムダ式を書けるようになりました。

例えば以下のようなメソッドがあった場合、

public void foo(body: Runnable);

Java8では次のように呼び出すことができます。

foo(() -> System.out.println("Hello, World!"));

このメソッドScalaから普通に呼ぶと、以前のJavaのような見た目になってしまいます。

foo(new Runnable {
  def run(): Unit = {
    println("Hello, World!")
  }
})

これはどうにかしたいです。

単純にラップする

他と同じようにラップしたメソッドを定義します。

def bar(body: => Unit): Unit =
  foo(new Runnable {
    def run(): Unit = {
      body
    }
  })
bar {
  println("Hello, World!")
}

シンプルですが、同じFunctionalInterfaceを使う場所が沢山ある場合、すべてラッパーを書くのは面倒です。

ヘルパーメソッドを用意する

FunctionalInterfaceをインスタンス化するヘルパーを用意します。

def Runnable(body: => Unit): Runnable =
  new Runnable {
    def run(): Unit = body
  }
foo(Runnable {
  println("Hello, World!")
})

利用側のコードが長くなってしまいますが、これひとつでどこでも使えます。

-Xexperimentalを利用してもらう

Scala 2.11ではコンパイラ-Xexperimentalオプションを渡すと、 FunctionalInterfaceを要求するところに関数リテラルを書けるようになります。

foo(() => println("Hello, World!"))

Java8と同じように書けますね。 難点は利用者側がこの設定をする必要があるということですが、次期バージョンのScala 2.12からはこの動作が標準になります。

Boxed typeの型推論の問題

Javaライブラリ内に、以下のようなジェネリックメソッドと、そこに使うための定数があるとします。

public <T> T getValue(Key<T> key);

public <T> void setValue(Key<T> key, T value);

public static final Key<Integer> HOGE_KEY = Key.of("hoge");

これをScalaから素直に使えるでしょうか。

val hoge: Int = getValue(HOGE_KEY)

setValue(HOGE_KEY, 42)  // error

getValueは(nullを返さないとするならば)問題ありません。 getValueの返り値はjava.lang.Integerですが、ここでは変数の型をIntに指定していることで暗黙変換されています。

一方setValueコンパイルエラーです。 型引数のTIntjava.lang.Integerの共通の親であるAnyに推論され、 HOGE_KEYKey[java.lang.Integer]であってKey[Any]でないためエラーとなります。 Int => java.lang.Integerの暗黙変換は起こりません。

複数の引数リストを利用する

これを解決するには、ひとつ目はカリー化したラッパーメソッドを定義することです。

def set[T](key: Key[T])(value: T): Unit =
  setValue(key, value)
set(HOGE_KEY)(42)

最初の引数リストの時点でTの型がjava.lang.Integerに決まるので、次の引数リストに渡した42が暗黙変換され無事コンパイルが通ります。

型クラスを定義して利用する

もうひとつの解決策として、boxingを行う型クラスを定義してみます。(前述の-Xexperimentalを利用)

sealed trait Box[T, U] {
  def box(v: T): U
}

object Box {
  implicit def identity[T]: Box[T, T] = t => t
  implicit val intToInteger: Box[Int, java.lang.Integer] = Int.box
  // Int以外省略
}
def set[T, U](key: Key[U], value: T)(implicit TU: Box[T, U]): Unit =
  setValue(key, TU.box(value))
set(HOGE_KEY, 42)

ラッパーメソッドの定義は長くなりましたが、利用側の見た目はシンプルになっています。

ついでにunboxingの方も定義してみましょう。

sealed trait Unbox[T, U] {
  def unbox(v: T): U
}

sealed trait LowPriorityUnbox {
  implicit def identity[T]: Unbox[T, T] = t => t
}

object Unbox extends LowPriorityUnbox {
  implicit val integerToInt: Unbox[java.lang.Integer, Int] = Int.unbox
  // Int以外省略
}
def get[T, U](key: Key[T])(implicit TU: Unbox[T, U]): U =
  TU.unbox(getValue(key))
val hoge = get(HOGE_KEY)

ここでhogeは型注釈をつけなくてもIntになっています。 ただし、getValueがnullを返す場合、Int.unboxnullに対して0を返すので注意が必要です。


もうちょっといろいろ書くつもりだったけど日付が変わるのでここまで。

明日はがくぞさんです。

*1:Scalaでnullを返すライブラリは死すべし