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 も同様

(多分もっと良い設計がありそうなので、コメント大歓迎です。)

その他

コメントで書いてもらったコードを見れば内容は分かるんだけど、自分であういうコードがパッと出てこないところが実力不足だと思った。

人のコードを沢山読んだり、定石(いわゆるデザインパターン)をもっと勉強しないといけないなと思った。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です