JavaライブラリのScalaラッパーを書くときのTIPS
これは Scala Advent Calendar 2015 - Adventar の18日目の記事です。
Scalaでは特に何もしなくてもJava製のライブラリをそのまま利用することができます。 ただ、そのまま使うよりもScalaでラップしたライブラリを作ることで使いやすさが向上することが多いです。
自分もそのためにライブラリをいくつか開発しています。
- kxbmap/configs · GitHub
- Typesafe configのラッパー
- kxbmap/jooqs · GitHub
- jOOQのラッパー
- 未リリース
この記事では、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
はコンパイルエラーです。
型引数のT
がInt
とjava.lang.Integer
の共通の親であるAny
に推論され、
HOGE_KEY
はKey[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.unbox
はnullに対して0を返すので注意が必要です。
もうちょっといろいろ書くつもりだったけど日付が変わるのでここまで。
明日はがくぞさんです。