LoginSignup
21
21

More than 5 years have passed since last update.

<:<を使ってメソッド引数の型を限定する (例: IntもしくはBooleanもしくはString

Last updated at Posted at 2014-12-17

うまく限定出来ないケースが見つかったためそれに対応するための 続<:<を使ってメソッド引数の型を限定する を書きました(2014.12.22)。

はじめに

型はなんでもいいのですが、例えばInt型,Boolean型,String型のいずれかを受け付けるfoo関数を定義したいとする。

案1 親クラス(trait)で型を宣言する

今回の場合、IntとBooleanとStringのみに共通する親クラスやtraitは存在しないのでこの作戦は使えない。

案2 オーバーロード

YES、出来る。
けどScalaさんならもっと違う書き方あるんでしょう?と勘ぐるくらいにはScalaに慣れてきた。

def foo(arg: Int) = println(arg)
def foo(arg: String) = println(arg)
def foo(arg: Boolean) = println(arg)

案3 <:<

見つけた。

def foo[T](arg: T)(implicit ev: Int with Boolean with String <:< T) = println(arg)

動作確認してみる。

foo(1)  // 1
foo(true)  // true
foo("hello")  // hello

foo(2.3) // error: Cannot prove that Int with Boolean with String <:< Double.

Double型渡したらコンパイルエラーになった。成功!

ところで <:< ってなんだよ?

型だよ。
Predef.scalaで宣言されている。

@implicitNotFound(msg = "Cannot prove that ${From} <:< ${To}.")
sealed abstract class <:<[-From, +To] extends (From => To) with Serializable
  • さっきのコンパイル時のエラーメッセージはimplicitで宣言された型に対するインスタンスが見つからないために出ていたメッセージだった
  • <:<は型パラメータを2つ取るクラス。先程のは中置記法で書かれていた。

中置記法ってなんだよ?

型パラメータを2つとる場合、Scalaでは2つの書き方がある。
例えばMap。以下どちらの方法で書いてもコンパイルは通る。

// その1
def foo(m: Map[Int, String]) = ???

// その2
def foo(m: Int Map String) = ???

どういう仕組みだよ?

sealed abstract class <:<[-From, +To] extends (From => To) with Serializable

private[this] final val singleton_<:< = new <:<[Any,Any] { def apply(x: Any): Any = x }

implicit def $conforms[A]: A <:< A = singleton_<:<.asInstanceOf[A <:< A]

これが全貌。

以降、話を簡単にするために、継承関係がイメージしやすいAnimal - Catを使う。
先の例を書き換えて以降foo関数とは以下のこととする。

def foo[T](arg: T)(implicit ev: Cat <:< T) = arg

foo関数にAnimal型のインスタンスを渡したとすると、implicit部は Cat <:< Animal となる。 Cat <:< Animal なインスタンスが見つかればOKだし見つからなければ could not find implicit value ... というおなじみのエラーになる(今回の<:<の場合エラーメッセージを変えているので通常のこれではないが)。

インスタンスが返ってくるのはimplicit defで宣言されている$conforms。
コードを読むと実際にはインスタンス生成はしておらず唯一のインスタンスであるsingleton_<:<をキャストして返しているだけだ。

$conformsは型パラメータを1つしか取っていないので<:<の一つ目の型パラメータが渡ってくる。この例だと ev: Cat <:< T なので1つ目はCat、つまり$conformsからは Cat <:< Cat が返ってくる。
なお、探しているのは Cat <:< Animal なのに Cat <:< Cat が返ってきてもOKな理由は<:<の宣言部にある。
<:<は <:<[-From, +To] と宣言されており、To部分は共変である。共変で馴染み深いのは List[+A] だろう。
以下のようにList[Cat]はList[Animal]に代入可能。

val a: List[Animal] = List(new Cat)

つまり Cat <:< AnimalCat <:< Cat は代入可能。よって Cat <:< Animal は見つかった、ということになる。

追記

あ、もちろん、Tは<:<の1番目の型パラメータに指定しても大丈夫。

def foo[T](arg: T)(implicit ev: T <:< Animal) = arg
21
21
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
21
21