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を返すライブラリは死すべし

IntelliJ IDEAでScalaのメソッドパラメーターをカッコに合わせて整列しない設定

追記'15/11/15: 専用の設定が追加されました

Scalaプラグインバージョン2.0.181から(?)設定に項目が追加されました。2.0.181は現在ScalaプラグインのUpdate channelをNightlyに設定していればインストール可能。

f:id:kxbmap:20151115153041p:plain

下記の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 Indent4
  • Wrapping and... > Method Declaration parameters > Align when multilineのチェックを外す

f:id:kxbmap:20150226030749p:plain

これで改行時にスペース4つの位置にパラメーターが整列するようになる。

また、

  • Method Declaration parametersDo not wrapからChop down if long
  • Method Declaration parameters > New line after '('をチェックする

この設定を追加すると、自動整形*3時に行が長い場合*4、Style Guideに沿った形に整えてくれる。

追記 チームで設定を共有したい場合

*1:この設定の場合、普通1つ目の改行は入れないと思うけども

*2:このクラスは短いので1行で書くべきだが、実際はもっと長いとして

*3:WindowsではCtrl+Alt+L

*4:Code StyleのRight margin (columns)を超える場合

*5:Code Styleは.idea/codeStyleSettings.xmlに保存される

scalikejdbcでDBMS独自構文を書く

例えばMySQLon 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"