Scalaのimplicit parameterでC#のdefault(T)を実現する
このエントリはScala Advent Calendar jp 2010の11日目です。また、このエントリはTwitterでの@xuwei_kさんの疑問に答えようと考えて作成したコードが元になっています。どうもありがとうございます。
皆さん、C#のdefaultみたいなことScalaでしたくなったことありませんか。C#のdefaultというのはdefault(T)のようにして使い、Tが参照型の時はnullを、数値の型の時は0を、構造体の場合、0またはnullでメンバが初期化された構造体を返します。たとえば、
using System; namespace example { class DefaultValue { static void Main(string[] args) { Console.WriteLine(default(string)); Console.WriteLine(default(int)); Console.WriteLine(default(long)); Console.WriteLine(default(float)); Console.WriteLine(default(double)); Console.WriteLine(default(bool)); } } }
のような感じで書くことができます。このdefault(T)という式、Tが型パラメータのときも使うことができて、たとえば次のようにして使うこともできます。
using System; namespace example { class DefaultValueGeneric { static void print<T>() { Console.WriteLine(default(T)); } static void Main(string[] args) { print<string>(); print<int>(); print<long>(); print<float>(); print<double>(); print<bool>(); } } }
さて、このような事ができるdefaultキーワードですが、これをScalaで実現する方法はあるのでしょうか?まず思いつくのが、
var x: Int = _
のようにして、初期化で_を使うことですが、これはxがフィールドの時しか使えず、ローカル変数やvalなフィールドには使えないので汎用性は全くありません。別の方法としては、
scala> def foo[T]: T = null.asInstanceOf[T] foo: [T]T scala> foo[Int] res0: Int = 0 scala> foo[Double] res1: Double = 0.0 scala> foo[Boolean] res2: Boolean = false scala> foo[String] res3: String = null
のようにして、nullをasInstanceOf[T]でT型にキャストする方法がありますが、この方法は現在の実装というかバグに依存した方法で、nullをIntにasInstanceOf[T]でキャストした場合、本当はNullPointerExceptionがthrowされるのが言語仕様的には正しいらしいので、あまり使うべきではないでしょう。
では、実装に依存しない方法でこのような挙動を実現する方法は無いのでしょうか?いや、そうではない、というのがこの記事の趣旨です。まずはコードをご覧ください。
trait DefaultValue[T] { def value: T } object DefaultValue { implicit object DefaultByteValue extends DefaultValue[Byte] { def value: Byte = 0 } implicit object DefaultShortValue extends DefaultValue[Short] { def value: Short = 0 } implicit object DefaultCharValue extends DefaultValue[Char] { def value: Char = 0 } implicit object DefaultIntValue extends DefaultValue[Int] { def value: Int = 0 } implicit object DefaultLongValue extends DefaultValue[Long] { def value: Long = 0L } implicit object DefaultFloatValue extends DefaultValue[Float] { def value: Float = 0.0f } implicit object DefaultDoubleValue extends DefaultValue[Double] { def value: Double = 0.0 } implicit object DefaultBooleanValue extends DefaultValue[Boolean] { def value: Boolean = false } implicit object DefaultUnitValue extends DefaultValue[Unit] { def value: Unit = () } private val defaultValueCache = new DefaultValue[AnyRef] { def value: AnyRef = null } //To avoid creation of DefaultValue[AnyRef] instance. implicit def DefaultAnyRef[T >: Null <: AnyRef]: DefaultValue[T] = { defaultValueCache.asInstanceOf[DefaultValue[T]] } def default[T:DefaultValue]: T = implicitly[DefaultValue[T]].value }
あとは、REPLなりなんなりで、
scala> import DefaultValue._ import DefaultValue._
とでもしてやれば、
scala> default[Int] res0: Int = 0 scala> default[Long] res1: Long = 0 scala> default[Float] res2: Float = 0.0 scala> default[Double] res3: Double = 0.0 scala> default[Boolean] res4: Boolean = false scala> default[String] res5: String = null scala> default[Nothing] <console>:9: error: could not find implicit value for evidence parameter of type DefaultValue[Nothin g] default[Nothing]
のように、見事にC#のdefaultの振る舞いを再現できています。型パラメータTに対してdefaultを使いたい場合は、
scala> def printValue[T:DefaultValue] { println(default[T]) } printValue: [T](implicit evidence$1: DefaultValue[T])Unit scala> printValue[Int] 0 scala> printValue[Double] 0.0 scala> printValue[String] null
のようにして、context boundsでDefaultValueを指定してやる(あるいは明示的にDefaultValue[T]型のimplicit parameterを指定してやる)必要があります。
さて、この挙動はどのようにして実現されているのでしょうか?鍵になるのは、ジェネリックなトレイトDefaultValue[T]と、そのコンパニオンオブジェクトDefaultValueです。
まず、DefaultValue[T]は、T型のデフォルト値を返すためのメソッドvalue: Tのみを持ちます。これと、DefaultValue[T]の型ごとの実装さえあれば、ユーザがDefaultValue[T]型のオブジェクトを明示的に与えてやればそこからT型の値が取得できるようになります。しかし、それではあまりに面倒くさ過ぎるので、implicit parameterを使ってDefaultValue[T]型のオブジェクトを明示的に渡さなくても勝手に渡してくれるようにします。
implicit parameterの探索対象としてもらうためには、通常は該当する型のimplicit val/objectが、呼び出しのスコープから見えている必要があるので、通常はimportが必須です。しかし、ここで、implicit parameterに関する特別な規則を利用することでそのようなimportを必須でなくする事ができます。それは、ある型G[T]のimplicit parameterを探索するときには、そのコンパニオンオブジェクトGのメンバのimplicit val/objectも探索対象になるというものです。今回の場合、DefaultValue[T]型のimplicitな値は、DefaultValue[T]型のコンパニオンオブジェクトDefaultValueのメンバとして、
object DefaultValue { implicit object DefaultByteValue extends DefaultValue[Byte] { def value: Byte = 0 } ... }
のように定義されていますから、DefaultValue[T]型のimplicit parameterを用意してあげるだけで、後は勝手に処理系がDefaultValueコンパニオンオブジェクトの中から適切な値を選択して処理してくれます。
定義の最後の方にある
private val defaultValueCache = new DefaultValue[AnyRef] { def value: AnyRef = null } //To avoid creation of DefaultValue[AnyRef] instance. implicit def DefaultAnyRef[T >: Null <: AnyRef]: DefaultValue[T] = { defaultValueCache.asInstanceOf[DefaultValue[T]] }
の部分は一体何をしているのでしょうか。これは、AnyRefのサブタイプで、かつNullのスーパータイプである任意のTのデフォルト値はnullであるという事を実現するためにある処理ですが、何故
implicit object DefaultAnyRef extends DefaultValue[AnyRef] = { def value: AnyRef = null }
では駄目なのかというと、これでは、default[String]のようにしたときに、返り値の静的な型がStringではなくAnyRefになってしまうため、使う側でいちいちダウンキャスト(value[String].asInstanceOf[String])が必要になってしまい、めんどうくさいのです。というわけで、AnyRefに対するimplicitの定義は
implicit def DefaultAnyRef[T >: Null <: AnyRef]: DefaultValue[T] = ...
のようにする必要があります。ここで、DefaultAnyRefはvalではなくdefであるため、定義の本体にそのままnew DefaultValue[T] { ... } のようにして書くと、DefaultValue[T]のインスタンスが呼び出しのたびに生成されてしまい、大変非効率的です。そのため、DefaultValue[AnyRef]のインスタンスをあらかじめprivateなフィールドとして保持しておき、
private val defaultValueCache = new DefaultValue[AnyRef] { def value: AnyRef = null }
これをダウンキャストして返すようにする事でインスタンス生成のオーバーヘッドを避けているわけです。
ちなみに、このコードはgistに置いてありますので、好きなようにいじっていただいて構いません。