Help us understand the problem. What is going on with this article?

nscala-timeで学ぶImplicit Conversion

More than 3 years have passed since last update.

Scalaの機能でImplicit Conversion(暗黙の型変換)という機能があり、
うまく使うとシンプルなコーディングができます。

よくお世話になる好きなライブラリーに、nscala-timeという時間操作のライブラリーあります。
こちらはImplicit Conversionをうまく使った、シンプルな時間操作をできるようにしてくれるライブラリーです。

nscala-timeの利用例

com.example.Main
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というメソッドを呼び出せるようにしてみます。

com.example.Main
// 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'パターンというそうです。
以下は変換メソッドを使ったサンプルです。

com.example.RichString
//通常のクラス拡張クラスを定義
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をつかってみる

https://github.com/nscala-time/nscala-time

nscala-timeを使うと、RubyのActiveSupportを使った場合と同じように、
シンプルで直感的な日付処理ができます。
nscala-timeはJodaTimeという時間操作ライブラリーのラッパーで、
JodaTimeのクラスをシンプルなコーディングで使えるようにされています。

nscala-timeを使った場合

com.example.Main
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をそのまま使った場合

上の処理と同じことをやるとこんな感じです。

com.example.Main
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***というクラスが拡張先のクラス

com.example.Main
import com.github.nscala_time.time.Imports._

DateTime.now + 2.months

こちらの2.monthsはInt型がInt型を拡張するクラスに変換され、monthsというメソッドを呼び出しています。
monthsメソッドが定義されているクラスがこちらです。

com.github.nscala_time.time.RichInt
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に拡張クラスに変換するメソッドが定義されている

com.github.nscala_time.time.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が全ての変換メソッドを継承している

com.github.nscala_time.time.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だけをインポートするという使い方もできそうです。

com.example.Main
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のデコンパイラで見てみます。

JD http://jd.benow.ca/

Implicit Conversionをこのような実行クラスでテストしてみます。

com.example.Main
mport com.github.nscala_time.time.Imports._
object Main {
  def main(args: Array[String]) {
    val oneDayPeriod = 1.day
  }
}

AnyValの継承を外したクラスの場合

com.github.nscala_time.time.RichInt
class RichInt(val underlying: Int) extends PimpedType[Int] {
  ・・・・
}

逆コンパイルしたクラス

実行クラス
com.example.Main$
public void main(String[] args) {
  Period oneDayPeriod = Imports..MODULE$.richInt(1).day();
}
変換メソッド
com.github.nscala_time.time.IntImplicits$class
public static RichInt richInt(IntImplicits $this, int n) {
  return new RichInt(n); // 毎回 new RichIntしている
}

RichIntへの変換メソッドでは毎回 new RichIntでインスタンス化して、RichIntインスタンスのdayメソッドを実行していることが分かります。

それに対して値クラスの場合を見てみます。

値クラスの場合

com.github.nscala_time.time.RichInt
class RichInt(val underlying: Int) extends Super with PimpedType[Int] {
  ・・
}

逆コンパイルしたクラス

実行クラス
com.example.Main$
public void main(String[] args) {
  Period oneDayPeriod = RichInt..MODULE$.day$extension(Imports..MODULE$.richInt(1));
}

変換メソッド
com.github.nscala_time.time.IntImplicits$class
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

yhidai
idcf
未来をささえる、Your Innovative Partner
http://www.idcf.jp/cloud/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away