Scalaの型引数とかサブ型とか
コンパイルが通らない
以下のコードのコンパイルが通らないので、どうしたものかと質問した。
abstract class C case class C1() extends C case class C2() extends C trait Service[T <: C] { def getFoo(): Option[T] } object Service { // found : List[Option[Any]] // required: List[Option[T]] def getAllFoos(services: List[Service[_]]): List[Option[C]] = { services.map(_.getFoo()) } }
(答えだけ知りたい場合は、@gakuzzzz さんのコメントが簡潔かつ完全な内容なのでそちらを参照。)
エラーの原因
さて、エラーの原因は @gakuzzzz さんの解説にある通り。
ポイントは trait Service[T <: C] にした場合、 Service[C] と Service[C1] と Service[C2] が全然関係ない型になってしまうので、統一的に扱えないって所ですね。
(なぜ全然関係ない型になってしまうのだろう。調べないと。)
試行錯誤
@xuwei-k さんに教えてもらった方法
@xuwei-k さんに教えてもらった方法だとコンパイルは通った。
object Service { def getAllFoos[A <: C](services: List[Service[A]]): List[Option[C]] = { services.map(_.getFoo()) } }
ただ、元のgistで端折ってしまったんだけど、@gakuzzzz さんが書いたように、実際にはServiceは以下のように継承して使うことを前提としてる。
object C1Service extends Service[C1] { def getFoo(): Option[C1] = None } object C2Service extends Service[C2] { def getFoo(): Option[C2] = None }
servicesにC1ServiceとC2Serviceが混ざったものを渡すとエラーになった。
scala> Service.getAllFoos(List(C1Service)) res4: List[Option[C]] = List(None) scala> Service.getAllFoos(List(C2Service)) res5: List[Option[C]] = List(None) scala> Service.getAllFoos(List(C1Service, C2Service)) <console>:17: error: no type parameters for method getAllFoos: (services: List[Service[A]])List[Option[C]] exist so that it can be applied to arguments (List[Service[_ >: C2 with C1 <: Product with Serializable with C]]) --- because --- argument expression's type is not compatible with formal parameter type; found : List[Service[_ >: C2 with C1 <: Product with Serializable with C]] required: List[Service[?A]] Service.getAllFoos(List(C1Service, C2Service)) ^ <console>:17: error: type mismatch; found : List[Service[_ >: C2 with C1 <: Product with Serializable with C]] required: List[Service[A]] Service.getAllFoos(List(C1Service, C2Service))
エラーの意味がよく分からないが、「Service[C] と Service[C1] と Service[C2] が全然関係ない型になってしま」ってるという事なのかな。
@gakuzzzzさんに書いてもらった方法1
こちら。
abstract class C case class C1() extends C case class C2() extends C trait Service[+T <: C] { // ここがポイント def getFoo(): Option[T] } object Service { def getAllFoos(services: List[Service[C]]): List[Option[C]] = { services.map(_.getFoo()) } } object C1Service extends Service[C1] { def getFoo(): Option[C1] = None } object C2Service extends Service[C2] { def getFoo(): Option[C2] = None } Service.getAllFoos(List(C1Service, C2Service))
うまく行った。
これを
trait Service[+T <: C]
とすれば、Service[C1]
とService[C2]
がService[C]
のサブ型と見做せるようになるのでService[C]
として統一的に扱えるようになります。
とのこと。
@gakuzzzzさんに書いてもらった方法2
gistはこちら。
abstract class C case class C1() extends C case class C2() extends C trait Service { type T <: C def getFoo(): Option[T] } object Service { def getAllFoos(services: List[Service]): List[Option[C]] = { services.map(_.getFoo()) } } object C1Service extends Service { type T = C1; def getFoo(): Option[C1] = None } object C2Service extends Service { type T = C2; def getFoo(): Option[C2] = None } Service.getAllFoos(List(C1Service, C2Service))
こちらに関しても先生のコメント。
型引数を止めて、抽象メンバ型でやれば、
Service
という型は一つになるので、こちらもService
で統一的に扱えるようになります。type T = C1
が必要なのはこの場合ですね。
使い分け
これも先生のコメントそのまま書いとく。
共変を使うか、抽象メンバ型を使うか、の判断基準としては、「C1に特化したServiceの型を扱いたいかどうか」になるかと思います。
なるほど。
考えたこと
これだけだと、@xuwei-k さんと @gakuzzzz さんのコメントを切り貼りしただけなので、もう少し書いておく。
そもそもの動機
実際のコードだと、C1とかC2はユーザーの外部アカウントとの連携情報で、例えば以下の様なもの(簡略化してるけど)。どれとも連携していない人もいれば、全てと連携している人もいる。
- Facebook連携の情報: UserFacebookInfo
- Twitter連携の情報: UserTwitterInfo
- GitHub連携の情報: UserGitHubInfo
それぞれが抽象クラスUserExternalAccountInfoを継承している。
で、Serviceに当たるのはいくつかあるけど、例えばDAOで、それぞれ違うテーブルにデータが入っているので(それぞれ user_facebook_info, user_twitter_info, user_github_info テーブル)、別々のDAOを用意した。以下の感じ。
trait ExternalAccountInfoDao[T <: UserExternalAccountInfo] { def getInfo(userId: Int): Option[T] } object ExternalAccountInfoDao { def getAllAccounts(userId: Int): List[Option[UserExternalAccountInfo]] = { val daos = List(UserFacebookInfoDao, UserTwitterInfoDao, UserGithubInfoDao) daos.map(_.getInfo(userId)) } } object UserFacebookInfoDao extends ExternalAccountInfoDao[UserFacebookInfo] { def getInfo(userId: Int): Option[UserFacebookInfo] = { /* code */ } } // UserTwitterInfoDao, UserGithubInfoDao も同様
(多分もっと良い設計がありそうなので、コメント大歓迎です。)
その他
コメントで書いてもらったコードを見れば内容は分かるんだけど、自分であういうコードがパッと出てこないところが実力不足だと思った。
人のコードを沢山読んだり、定石(いわゆるデザインパターン)をもっと勉強しないといけないなと思った。