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を返すので注意が必要です。
もうちょっといろいろ書くつもりだったけど日付が変わるのでここまで。
明日はがくぞさんです。
IntelliJ IDEAでScalaのメソッドパラメーターをカッコに合わせて整列しない設定
追記'15/11/15: 専用の設定が追加されました
Scalaプラグインバージョン2.0.181から(?)設定に項目が追加されました。2.0.181は現在ScalaプラグインのUpdate channelをNightlyに設定していればインストール可能。
下記のContinuation Indent
の代わりに、Settings > Editor > Code Style > Scala > Other > Alternate Indentation for constructor args and parameter declarations
をチェックして4spacesに設定したら望む挙動になります。
Continuation Indent
で設定した場合と違って、クラス・メソッド宣言以外のインデントに影響しません。
追記終わり
#rpscalaの懇親会で話題になったものの皆さん知らないようだったので。
case class Hoge(foo: String, bar: Int, baz: Double)
このようなcase classがあるときに各引数の位置で改行すると、標準の設定では以下のようにインデントされる。*1
case class Hoge( foo: String, bar: Int, baz: Double)
これをScala Style Guideに沿って以下のように書きたい。*2
case class Hoge( foo: String, bar: Int, baz: Double)
そのためには、Settings > Editor > Code Style > Scala
で
Tabs and Indents > Continuation Indent
を4
にWrapping and... > Method Declaration parameters > Align when multiline
のチェックを外す
これで改行時にスペース4つの位置にパラメーターが整列するようになる。
また、
Method Declaration parameters
をDo not wrap
からChop down if long
にMethod Declaration parameters > New line after '('
をチェックする
この設定を追加すると、自動整形*3時に行が長い場合*4、Style Guideに沿った形に整えてくれる。
追記 チームで設定を共有したい場合
Code Style
のScheme
をProject
に設定 (既に設定済みのSchemeがある場合はManage...
からCopy to Project
する)- How to manage projects under Version Control Systems – JetBrains Support これを参考に設定ファイルをバージョン管理に追加*5
scalikejdbcでDBMS独自構文を書く
例えばMySQLのon duplicate key update
など
まず、sql"..."
で書けば何でもいけるのはいいとして
def upsertUser1(id: String, email: String)(implicit session: DBSession) = sql""" insert into `user` (`id`, `email`) values ($id, $email) on duplicate key update `email` = values(`email`) """.update().apply()
QueryDSLでinsert文を書いてsqls"..."
をappend
するもよし
def upsertUser2(id: String, email: String)(implicit session: DBSession) = applyUpdate { val c = User.column insert.into(User).namedValues( c.id -> id, c.email -> email ).append( sqls"on duplicate key update ${c.email} = values(${c.email})" ) }
しかしtypoするとメンドクサイし、自分はIDEの補完に頼って生きていきたい。
そういうことでInsertSQLBuilderと、ついでにSQLSyntaxオブジェクトに対してimplicit conversionを定義します。
object MySQLSyntaxSupport { implicit class RichInsertSQLBuilder(val self: InsertSQLBuilder) extends AnyVal { def onDuplicateKeyUpdate(columnsAndValues: (SQLSyntax, Any)*): InsertSQLBuilder = { val cvs = columnsAndValues map { case (c, v) => sqls"$c = $v" } self.append(sqls"on duplicate key update ${sqls.csv(cvs:_*)}") } } implicit class RichSQLSyntax(val self: sqls.type) extends AnyVal { def values(column: SQLSyntax): SQLSyntax = sqls"values($column)" } }
これでこのように書けます。
import MySQLSyntaxSupport._ def upsertUser3(id: String, email: String)(implicit session: DBSession) = applyUpdate { val c = User.column insert.into(User).namedValues( c.id -> id, c.email -> email ).onDuplicateKeyUpdate( c.email -> sqls.values(c.email) ) }
まとめ
implicit conversionによってinsertにon duplicate key updateのサポートを追加しました。
同様にselect/update/deleteやwhereなどの後にも文脈に沿ったメソッドを追加することができます。
version
scalaVersion := "2.10.3" libraryDependencies ++= Seq( "org.scalikejdbc" %% "scalikejdbc" % "1.7.4", "org.scalikejdbc" %% "scalikejdbc-interpolation" % "1.7.4", "mysql" % "mysql-connector-java" % "5.1.28" )
configs 0.2.1
https://github.com/kxbmap/configs/tree/0.2.1
仕事とか提督業が忙しくて大分放置してましたがconfigsの0.2.1をリリースしました。
scalikejdbc/scalikejdbc-asyncのサポート追加とBoneCP 0.8に対応したくらいです。
configsの構成として、コア部分のconfigsと各種ヘルパーを含むconfigs-supportで分けてますが、色んなライブラリの依存をconfigs-supportにoptionalで突っ込むのは筋が悪かったので0.3.0ではライブラリごとに分割する予定です。
色んなSQLをSlickで書くと?
上記の記事のSlick版です。
groupByに関するSlick 1.0.0のバグを回避するため、1.0.1-RC1を使用します。
例によってテーブル名とかは弄りました。 テーブル定義を含む全てのコードは以下のGistを参照してください。
LEFT JOIN + IS NULL
SQL
SELECT * FROM Students s LEFT JOIN Clubs c on (s.club_id = c.id) WHERE c.id IS NULL
Slick
val q1 = for { (s, c) <- Students leftJoin Clubs.map(_.??) on (_.clubId === _._1) if c._1.isNull } yield (s, c) val r1: List[(Student, (Option[Long], Option[String]))] = q1.list()
いきなり苦労した。
??
はid.? ~ name.?
のaliasとしてClubsに定義してあります。
普通にStudents leftJoin Clubs on ...
とするとNULLだよボケみたいな実行時エラーになるので注意が必要です。
難点は元エントリの趣旨に反してSlickが生成するSQLが以下のように意図したものとは違うことでしょうか。
select x2.x3, x2.x4, x2.x5, x2.x6, x7.x8, x7.x9 from ( select x10."NAME" as x3, x10."CLASSROOM" as x4, x10."CLUB_ID" as x5, x10."ID" as x6 from "STUDENTS" x10 ) x2 left outer join ( select x11."ID" as x8, x11."NAME" as x9 from "CLUBS" x11 ) x7 on x2.x5 = x7.x8 where x7.x8 is null
SlickのTabe定義が実際のDBテーブルのカラムを全て含まなくてもいいからこうなる? どうしても必要ならLifted EmbeddingじゃなくてPlain SQLを書かなければいけません。(前述のGist参照)
これ以外の問題では意図した通りのSQLが生成されました。(*
は使われないけど)
相関サブクエリー
SQL
SELECT * FROM Students s WHERE EXISTS ( SELECT c.id FROM Clubs c WHERE s.club_id = c.id )
Slick
val q2 = for { s <- Students if (for { c <- Clubs if s.clubId === c.id } yield c.id).exists } yield s val r2: List[Student] = q2.list()
割と素直な見た目。
複数のカラムでgroup by
SQL
SELECT s.club_id, s.classroom, count(*) FROM Sutudents s GROUP BY s.club_id, u.classroom
Slick
val q3 = for { ((ci, cr), ss) <- Students.groupBy(s => (s.clubId, s.classroom)) } yield (ci, cr, ss.length) val r3: List[(Option[Long], String, Int)] = q3.list()
Slick 1.0.0は複数カラムのgroup byがバグってて実行時エラーになるので1.0.1-RC1を使ってください。
SQLのcountはlengthメソッドで取得することになってます。countメソッドもありますがdeprecatedです。
Typesafe configのScalaラッパーを公開しました
configがJavaのライブラリでちょっと使いづらいのでScala用にラッパーを書きました。
https://github.com/kxbmap/configs
configz というのもあったんだけど、これはScalaz力が足りず思ったように使えなかったです。
使い方
SBTのビルド定義に以下を追加します。
libraryDependencies += "com.github.kxbmap" %% "configs" % "0.1.0"
Sample
// sample.conf int.value = 42 int.values = [1, 2, 3] duration = 10ms bytes = 10k obj { foo = 23 bar = Hello }
scala> import com.typesafe.config._ import com.typesafe.config._ scala> import com.github.kxbmap.configs._ import com.github.kxbmap.configs._ scala> val config = ConfigFactory.load("sample") config: com.typesafe.config.Config = ...
configsパッケージをimportするとPimp my library*1でConfigにget
メソッドが追加されます。
Simple value
scala> val n = config.get[Int]("int.value") n: Int = 42
config.get[Int]("key")
は config.getInt("key")
と同じ動きです。
List
scala> val ns = config.get[List[Int]]("int.values") ns: List[Int] = List(1, 2, 3)
ScalaのListを返します。
Option/Either/Try
型引数に変なものを渡すと例外になります。
scala> config.get[String]("int.values") com.typesafe.config.ConfigException$WrongType: sample.conf: 2: int.values has type LIST rather than STRING at com.typesafe.config.impl.SimpleConfig.findKey(SimpleConfig.java:124) ...
例外を捕捉したい場合はOption/Either/Tryを指定します。
scala> val opt = config.get[Option[String]]("int.values") opt: Option[String] = None scala> val eth = config.get[Either[Throwable, String]]("int.values") eth: Either[Throwable,String] = Left(com.typesafe.config.ConfigException$WrongType: sample.conf: 2: int.values has type LIST rather than STRING) scala> import scala.util.Try import scala.util.Try scala> val tr = config.get[Try[String]]("int.values") tr: scala.util.Try[String] = Failure(com.typesafe.config.ConfigException$WrongType: sample.conf: 2: int.values has type LIST rather than STRING)
Missing
PlayのConfigurationのように値がない場合だけNoneで他は例外にして欲しい場合のために、orMissing
というメソッドを用意しています。
scala> val just = config.orMissing[Int]("int.value") just: Option[Int] = Some(42) scala> val missing = config.orMissing[Int]("missing.value") missing: Option[Int] = None scala> config.orMissing[String]("int.values") com.typesafe.config.ConfigException$WrongType: sample.conf: 2: int.values has type LIST rather than STRING at com.typesafe.config.impl.SimpleConfig.findKey(SimpleConfig.java:124) ...
あるいはEitherを使うこともできます。
scala> val em = config.get[Either[ConfigException.Missing, Int]]("missing.value") em: Either[com.typesafe.config.ConfigException.Missing,Int] = Left(com.typesafe.config.ConfigException$Missing: No configuration setting found for key 'missing') scala> config.get[Either[ConfigException.Missing, Int]]("int.values") com.typesafe.config.ConfigException$WrongType: application.conf: 2: int.values has type LIST rather than NUMBER at com.typesafe.config.impl.SimpleConfig.findKey(SimpleConfig.java:124) ...
Duration
Longではなくscala.concurrent.duration.Duration
で取得します。
scala> import scala.concurrent.duration.Duration import scala.concurrent.duration.Duration scala> val d = config.get[Duration]("duration") d: scala.concurrent.duration.Duration = 10 milliseconds
Bytes
bytesの取得にはそれ用のcase classを使います。
scala> val bs = config.get[Bytes]("bytes") bs: com.github.kxbmap.configs.Bytes = Bytes(10240)
Map
ConfigObjectをMapとして取得することができます。
scala> val m1 = config.get[Map[String, String]]("obj") m1: Map[String,String] = Map(bar -> Hello, foo -> 23) scala> val m2 = config.get[Map[Symbol, String]]("obj") m2: Map[Symbol,String] = Map('bar -> Hello, 'foo -> 23) scala> val m3 = config.get[Map[String, Try[Int]]]("obj") m3: Map[String,scala.util.Try[Int]] = Map(bar -> Failure(com.typesafe.config.ConfigException$WrongType: sample.conf: 7: bar has type STRING rather than NUMBER), foo -> Success(23))
詳しく
get
の型は以下のようになっています。
def get[T: AtPath](path: String): T = ...
また、AtPath
は以下です。
type AtPath[T] = Configs[String => T]
そしてConfigs。
trait Configs[T] { def get(config: Config): T }
Configs[T]
はConfigから型Tの値を得るものです。
つまりAtPath[T]
は、Configから「String(path)を与えると型Tの値を返す関数」を得るものです。
ユーザーはAtPathのインスタンスを定義することで、getを利用してConfigから好きな型の値を取得することができます。
例えば以下のようにInetAddressのためのインスタンスを定義します。(AtPath.applyは (Config, String) => T
を取り AtPath[T]
を返すヘルパーです)
import java.net.InetAddress implicit val inetAddressAtPath: AtPath[InetAddress] = AtPath { (config, path) => InetAddress.getByName(config.getString(path)) } implicit val inetAddressListAtPath: AtPath[List[InetAddress]] = AtPath { (config, path) => config.get[List[String]](path).map(InetAddress.getByName) }
すると以下のようなコードが書けます。
inet = "127.0.0.1" inets = ["127.0.0.1", "::1"]
scala> val inet = config.get[InetAddress]("inet") inet: java.net.InetAddress = /127.0.0.1 scala> val inets = config.get[List[InetAddress]]("inets") inets: List[java.net.InetAddress] = List(/127.0.0.1, /0:0:0:0:0:0:0:1)
また、Configs[T]
をAtPath[T]
及びAtPath[List[T]]
に暗黙的に変換する仕組みが用意されているので、Configsのインスタンスを定義できるなら(Config =>T
という関数を書けるなら)、その方が楽です。
implicit val inetSocketAddressConfigs: Configs[InetSocketAddress] = Configs { config => new InetSocketAddress( config.get[InetAddress]("host"), config.get[Int]("port") ) }
address = { host = "127.0.0.1", port = 8080 } addresses = [ { host = "127.0.0.1", port = 8080 } { host = "::1", port = 9000 } ]
scala> val address = config.get[InetSocketAddress]("address") address: java.net.InetSocketAddress = /127.0.0.1:8080 scala> val addresses = config.get[List[InetSocketAddress]]("addresses") addresses: List[java.net.InetSocketAddress] = List(/127.0.0.1:8080, /0:0:0:0:0:0:0:1:9000)
今後
インスタンスをもっと楽に定義できるようにしておきたい。 自前のcase class用の定義なんかは多分マクロでやるのがいいんだけど、そっち方面にはまだ手を付けていないので勉強するところから。
*1:Enrich my libraryと呼ぼうという話があった気がするけどどうなったんだろう。
Scala 2.10用のSBinaryはどこに?
SBinaryのREADMEに載ってないのでsbtからパクってきた奴をメモ。
resolvers += Resolver.typesafeIvyRepo("releases") libraryDependencies += "org.scala-tools.sbinary" %% "sbinary" % "0.4.1"