Scalaの機能でImplicit Conversion(暗黙の型変換)という機能があり、
うまく使うとシンプルなコーディングができます。
よくお世話になる好きなライブラリーに、nscala-timeという時間操作のライブラリーあります。
こちらはImplicit Conversionをうまく使った、シンプルな時間操作をできるようにしてくれるライブラリーです。
nscala-timeの利用例
import com.github.nscala_time.time.Imports._
//現在から2ヶ月後のDateTimeを取得
DateTime.now + 2.months // => org.joda.time.DateTime = 2016-02-17T13:21:36.878+09:00
//DateTimeインスタンスの時間の比較
DateTime.nextMonth < DateTime.now + 2.months // => true
プロジェクトで使う共通クラスでもImplicit Conversionをうまく使っていきたいと思ったので、
nscala-timeの構成やImplicit Conversionの使い方を勉強してみることにしました。
Implicit Conversionとは何か?
イメージでいうと、クラスに定義されていないメソッドをあとから追加して、呼び出せるようになる機能です。
実際のクラスに定義を付け加えたり、継承した新しいクラスを作らず、クラスの機能を拡張できるところが特徴です。
簡単な例として、String型でhelloというメソッドを呼び出せるようにしてみます。
// String型を拡張するImplicit Classとメソッドを定義。
// implicit修飾子と拡張したいクラスの型をコンストラクタにとる。
implicit class RichString(s:String) {
def hello = println(s"Hello, ${s}")
}
//implicit classのメソッドを呼び出す。
"Tom".hello
// => Hello, Tom
//Stringインスタンスがhelloを呼び出すと、もともとStringクラスに定義されていないhelloというメソッド呼び出せる。
拡張したいクラスの型をコンストラクタ引数にとるImplicit Class(暗黙の型変換クラス)を定義します。
処理のスコープからImplicit Classを参照できる場合、
拡張したいクラスがImplicit Classに定義されたメソッドを呼び出すことができます。
実際は呼び出し直前に変換されたり、効率的に呼び出したいメソッドを呼び出せるようにコンパイルされています。
変換関数を使った変換
Inplicit Conversionをする方法は、Implicit Classを使う方法と変換メソッドを使う方法があります。
このデザインを'pimp my library'パターンというそうです。
以下は変換メソッドを使ったサンプルです。
//通常のクラス拡張クラスを定義
class RichString(s:String) {
def hello = println(s"Hello, ${s}")
}
// 拡張クラスへ変換するメソッドを定義
implicit def toRichString(s:String) = new RichString(s)
"Tom".hello
// => Hello, Tom
Implicit Classの構文はScala2.10以降のバージョンから利用できますので、
2.10以降のみをターゲットにする場合、Implicit Classを使った方がシンプルにかけそうです。
nscala-timeをつかってみる
nscala-timeを使うと、RubyのActiveSupportを使った場合と同じように、
シンプルで直感的な日付処理ができます。
nscala-timeはJodaTimeという時間操作ライブラリーのラッパーで、
JodaTimeのクラスをシンプルなコーディングで使えるようにされています。
nscala-timeを使った場合
import com.github.nscala_time.time.Imports._
//現在から2ヶ月後のDateTimeを取得
DateTime.now + 2.months // => org.joda.time.DateTime = 2016-02-17T13:21:36.878+09:00
//DateTimeインスタンスの時間の比較
DateTime.nextMonth < DateTime.now + 2.months // => true
JodaTimeをそのまま使った場合
上の処理と同じことをやるとこんな感じです。
import org.joda.time.DateTime
//現在から2ヶ月後のDateTimeを取得
DateTime.now.plusMonths(2)
//DateTimeインスタンスの時間の比較
DateTime.now.plusMonths(1).isBefore(DateTime.now.plusMonths(2))
nscala-timeを使った場合、直感的にシンプルなコードになっていることが分かります。
その他の公式のサンプル
https://github.com/nscala-time/nscala-time#datetime-operations
ライブラリの構成
どんな構成になっているか見てみます。
Rich***というクラスが拡張先のクラス
import com.github.nscala_time.time.Imports._
DateTime.now + 2.months
こちらの2.months
はInt型がInt型を拡張するクラスに変換され、monthsというメソッドを呼び出しています。
monthsメソッドが定義されているクラスがこちらです。
package com.github.nscala_time.time
import org.joda.time._
import com.github.nscala_time.PimpedType
class RichInt(val underlying: Int) extends Super with PimpedType[Int] {
/** 抜粋してます **/
def days = Period.days(underlying)
def weeks = Period.weeks(underlying)
def months = Period.months(underlying) // ←これ
def years = Period.years(underlying)
・・・・・
}
Int型以外にも型を拡張するクラスがRich***という名前で定義されています。
Implicitsに拡張クラスに変換するメソッドが定義されている
/** 抜粋してます **/
object Implicits extends Implicits
trait Implicits extends BuilderImplicits with IntImplicits with StringImplicits ・・
trait IntImplicits {
implicit def richInt(n: Int): RichInt = new com.github.nscala_time.time.RichInt(n)
implicit def richLong(n: Long): RichLong = new com.github.nscala_time.time.RichLong(n)
}
com.github.nscala_time.time.Implicitsをみてみると、
変換メソッドがImplicits traitに継承され、
最終的にImplicitsというobjectにすべての変換メソッドが継承されています。
Importsが全ての変換メソッドを継承している
/** 抜粋してます **/
object Imports extends Imports
object TypeImports extends TypeImports
object StaticForwarderImports extends StaticForwarderImports
trait Imports extends TypeImports with StaticForwarderImports with Implicits
trait TypeImports {
type Chronology = org.joda.time.Chronology
type DateTime = org.joda.time.DateTime
type DateTimeFormat = org.joda.time.format.DateTimeFormat
type DateTimeZone = org.joda.time.DateTimeZone
type Duration = org.joda.time.Duration
type Interval = org.joda.time.Interval
・・・中略・・・
}
ImportsがImplicitsを継承して全ての変換メソッドをもっているようです。
そのため、nscala-timeを利用するときには、Importsのもっているものをアンダースコア(_)のワイルドーカードでインポートすれば、変換メソッドも拡張クラスも全てインポートされ、Implicit Conversionの機能が利用できるようになっています。
構成として実装をtraitにもち、同名のobjectに継承されています。
目的別にtraitに実装をわけることで、自分の必要なobjectだけをインポートするという使い方もできそうです。
import com.github.nscala_time.time.Imports._
DateTime.now + 2.months
また、typeとしてorg.joda.timeのクラスをImports内で同名で定義しているため、org.joda.timeのクラスを自分でインポートしなくても、利用できるようになっているようです。
拡張クラスがAnyValを継承する意味は何か?
拡張クラスがSuperというクラスを継承しているのが気になったので調べてみました。
実際Superはcom.github.nscala_time.timeというパッケージオブジェクトに定義された
typeであり、AnyValの別名でした。
package com.github.nscala_time
package object time{
private[time] type Super = AnyVal
}
AnyVal
Any
┣ AnyVal => Int, Long, CharなどのJavaのプリミティブ型相当クラスのスーパークラス
┗ AnyRef => ↑以外のクラスのスーパークラス
AnyValはすべてのクラスの親クラスであるAnyのサブクラスで、
Javaのプリミティブ型相当のクラスが継承するスーパークラスです。
自分で新しく作ったクラスが明示的に継承しない場合、AnyRefのサブクラスになります。
公式ドキュメントで調べたところ、
AnyValを継承すると値クラスとして定義され、Implicit Conversionした拡張クラスのメソッド呼び出し時に無駄なインスタンスを作らない
ということのようです。
値クラスとは
通常のクラスと比べていくつか制約がありますが、
… def のみをメンバとして持つことができる。特に、lazy val、var、val をメンバとして持つことができない。
状態を持たないクラスの特性を生かし、値クラスのインスタンスメソッドを呼び出す際に、
毎回インスタンス化せずにインスタンスメソッドをよびだせるように、コンパイラが効率的なコードに変換してくれるように使えるようです。
値クラスと通常のクラスのImplicit Conversionしたときのコンパイル結果の比較
値クラスと、ソースを修正してAnyValの継承を外した通常のクラスのRichIntをコンパイルして、結果をJDのデコンパイラで見てみます。
Implicit Conversionをこのような実行クラスでテストしてみます。
mport com.github.nscala_time.time.Imports._
object Main {
def main(args: Array[String]) {
val oneDayPeriod = 1.day
}
}
AnyValの継承を外したクラスの場合
class RichInt(val underlying: Int) extends PimpedType[Int] {
・・・・
}
逆コンパイルしたクラス
実行クラス
public void main(String[] args) {
Period oneDayPeriod = Imports..MODULE$.richInt(1).day();
}
変換メソッド
public static RichInt richInt(IntImplicits $this, int n) {
return new RichInt(n); // 毎回 new RichIntしている
}
RichIntへの変換メソッドでは毎回 new RichIntでインスタンス化して、RichIntインスタンスのdayメソッドを実行していることが分かります。
それに対して値クラスの場合を見てみます。
値クラスの場合
class RichInt(val underlying: Int) extends Super with PimpedType[Int] {
・・
}
逆コンパイルしたクラス
実行クラス
public void main(String[] args) {
Period oneDayPeriod = RichInt..MODULE$.day$extension(Imports..MODULE$.richInt(1));
}
変換メソッド
public static int richInt(IntImplicits $this, int n) {
return n; // 引数のintをそのままかえしてるだけ
}
実行クラスではRichIntに確保されたのシングルトンインスタンスのdayメソッドが呼ばれるようになっています。
値クラスの場合、変換メソッドのrichIntは引数のIntをそのまま返しているだけです。
RichIntのdayメソッドを呼ぶためだけにインスタンス化することがなくなり、効率的な処理になっているようです。
まとめ
- Implicit Conversionを使うことで、既存のクラスにメソッドを追加できる。
- Implicit Classを値クラスとして定義することで、メソッド呼び出し時に新しいインスタンスをつくらなくてすむ。効率的に拡張したメソッドを実行できる。
- 拡張元のクラスをImportsのtypeとして定義することで、Importsだけをインポートするだけで拡張元のクラスを個別にインポートせずにすむ。
- 変換メソッドは変換先の種別ごとにtraitにもち、objectに継承されることで必要な機能だけをインポートすることもできる。
まだまだ色々ポイントがありそうなので、引き続き勉強していきたいと思います。
参考
値クラスと汎用トレイト
http://docs.scala-lang.org/ja/overviews/core/value-classes.html
Scala 2.10.0 M3の新機能を試してみる(3) - SIP-15 - Value Classes
http://kmizu.hatenablog.com/entry/20120507/1336398449