コンパイルが通らない
以下のコードのコンパイルが通らないので、どうしたものかと質問した。
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 も同様
(多分もっと良い設計がありそうなので、コメント大歓迎です。)
その他
コメントで書いてもらったコードを見れば内容は分かるんだけど、自分であういうコードがパッと出てこないところが実力不足だと思った。
人のコードを沢山読んだり、定石(いわゆるデザインパターン)をもっと勉強しないといけないなと思った。