読者です 読者をやめる 読者になる 読者になる

倭マン's BLOG

くだらない日々の日記書いてます。 たまにプログラミング関連の記事書いてます。 書いてます。

implicit な「<:<」や「=:=」とはなんぞや? (Scala)

Scala

次のような汎用的な(何に使うか分からない)コンテナクラス Container があるとしましょう:

class Container[E](es:E*){
  def apply(i:Int):E = es(i)
}

今の段階では要素を取得することができるだけです:

val stringC = new Container("abc", "def", "ghi")
println(stringC(0))  // 「abc」と表示

val intC = new Container(1, 2, 4, 8, 16)
println(intC(1))  // 「2」と表示

基本コンストラクタに可変な配列を渡してますが、単に簡略化のためです。

Numeric 型

さて、ここで型パラメータが数値の場合のみ、要素の合計を返すメソッド sum を定義したいとき、implicitNumeric 型の引数を定義して

class Container[E](es:E*){

  def apply(i:Int):E = es(i)

  def sum(implicit num: Numeric[E]):E = es.sum
}

この sum メソッドは以下のように使えます:

val intC = new Container(1, 2, 4, 8, 16)  
println(intC.sum)  // 「31」と表示

val stringC = new Container("abc", "def", "ghi")
stringC.sum  // コンパイル・エラー

要素が数値の場合のみ合計を計算するようにできました。 IntelliJ IDEA だと、最後の行のコンパイル・エラーがリアルタイムに表示されないのがちょっと悲しいですが。

<:< 型と =:= 型

さて、<:< とは Predef に定義されている型なんですが、やることは上記の Numeric 型と同じようなことです。 違いは制限が数値ではなくて、指定した型のサブ型もしくはスーパー型であることです。 例えば要素の型が String (のサブ型)の場合のみ、引数で指定したセパレータで要素の文字列を連結した文字列を返す join メソッドを定義してみましょう。 このとき、Container クラスの型パラメータ E は String のサブ型であって欲しいので、Numeric の代わりに「<:<[E, String]」型、もしくはもう少し見やすく「E <:< String」型の implicit な引数を指定します:

class Container[E](es:E*){
  ...

  def join(sep:String)(implicit ev: E <:< String):String = es.mkString(sep)
}

引数の名前として「ev」がよく使われるようですが、これは「evidence」が由来なようです。 このとき、

val stringC = new Container("abc", "def", "ghi")
println(stringC.join(":"))  // 「abc:def:ghi」と表示

val intC = new Container(1, 2, 4, 8, 16)
println(intC.join("-"))  // コンパイル・エラー

となって、確かに要素が String の場合だけ join メソッドが使えます。 ちなみに「>:>」という型はないので「E >:> String」とは書けませんが、これは単に「String <:< E」と書けば解決します。

=:= 型は文法的な使い方は <:< 型と同じで、サブ型スーパー型の代わりに完全に同一の型であるという条件を課します。

class Person(val name:String, val age:Int, val sex:String)
class Man(override val name:String, override val age:Int) extends Person(name, age, "male")

class Container[E](es:E*){
  ...
  def ageSum(implicit ev: E <:< Person) = es.map(_.age).sum
  def strictAgeSum(implicit ev: E =:= Person) = es.map(_.age).sum
}

val me = new Man("waman", 100)
val you = new Man("vaman", 99)

val personC = new Container[Person](me, you)
println(personC.ageSum)
println(personC.strictAgeSum)

val manC = new Container[Man](me, you)
println(manC.ageSum)
println(manC.strictAgeSum)  // これだけコンパイル・エラー!

strinctAgeSum メソッドでは要素の型は Person でないとダメだよって言ってるので、Container[Man] 型のオブジェクトで呼び出すとコンパイル・エラーにされます。

ちょっと詰まった事項

コードを書いててちょっと詰まった事項。 まず、メソッド本体で暗黙の型変換が無効になってるっぽい。 要素の型が文字列の場合のみ、要素の文字列を反転させて要素自体の順番も反転させるようなメソッドを書こうとすると

  def reverse(implicit ev: E <:< String) = es.map(_.reverse).reverse

という風に書けそうだけど、実際には map の引数の部分で reverse メソッドがないと怒られてしまいます。 まぁ、java.lang.String には確かにそんなメソッドはないんですが、通常、Scala のコード内だと WrappedString に暗黙的に変換して reverse メソッドを呼び出してくれます。 ここでは明示的に WrappedString に変換してやると確かにうまくいきます:

class Container[E](es:E*){
  ...

  def reverse(implicit ev: E <:< String) = es.map(new WrappedString(_).reverse).reverse
}

val stringC = new Container("abc", "def", "ghi")
println(stringC.reverse.mkString("-"))  // 「ihg-fed-cba」と表示

だけど面倒ぅ。 Scala のバージョン上げたらできるようになってるかもしれないけど(試したのは scala 2.11.6)。

もうひとつの詰まった事項はオーバーロードが微妙なところ。 例えば、Container[String] に対して引数無しの join メソッド

  def join(implicit ev: E <:< String):String = es.mkString(",")

また、Container[Int] にも引数無しの join メソッド

  def join(implicit ev: Numeric[E]):E = es.sum

を定義してやると、呼び出し側でコンパイル・エラーがでます(参照が曖昧だという旨のエラー)

  println(stringC.join)  // コンパイル・エラー
  println(intC.join)  // コンパイル・エラー

まぁ、型消去したら区別付かんわなぁ。 他にも implicit による制限が型パラメータが違うだけの場合なども NG。 この辺りが Scala に型クラスを移植しようとしたときの限界みたいなもんなのかな?

ScalaTest の API 見てて「implicit T <:< String」なんてのがメソッドの引数になってて何じゃこりゃ?と思って、コード書いたり Scaladoc 見たりしてたんだけど、まぁ大体何をやろうとしてるものなのかは分かってきた感じです。 むしろ暗黙のパラメータでよくやる implicit な object などに比べて、やってることは大したことないですな。

P.S. メソッドの型パラメータにも同様の方法で制限をかけることができますが、いまいち有り難みが感じられません。

Scala関数型デザイン&プログラミング―Scalazコントリビューターによる関数型徹底ガイド

Scala関数型デザイン&プログラミング―Scalazコントリビューターによる関数型徹底ガイド

Scalaスケーラブルプログラミング第2版

Scalaスケーラブルプログラミング第2版

  • 作者: Martin Odersky,Lex Spoon,Bill Venners,羽生田栄一,水島宏太,長尾高弘
  • 出版社/メーカー: インプレスジャパン
  • 発売日: 2011/09/27
  • メディア: 単行本(ソフトカバー)
  • 購入: 12人 クリック: 235回
  • この商品を含むブログ (46件) を見る