前提
[wartremover:JavaSerializable] Inferred type containing Serializable: Product with Animal with java.io.Serializable
というコンパイルエラーに遭遇したので、その原因と対処方法について紹介します。
使用Version: Scala2.13.8
はじめに
型推論について簡単に説明します。
※すでにご存知の方は読み飛ばしていただいて構いません。
Scalaのような静的型付け言語では、変数や関数の引数や返り値などに型を定義する必要があります。
次のコードはScalaで文字列を扱う例です。
val kata: String = "'Kata' is called 'type' in English."
: String
によって、 kata という変数はString型ですよー とコンパイラに教えています。
ただ実際は、Scala コンパイラにはこの型を自動で推論してくれる機能が備わっているため、: String
を省略することができます。
val kata = "'Kata' is called 'type' in English."
公式ドキュメントにも次のように記載されています。
"The Scala compiler can often infer the type of an expression so you don’t have to declare it explicitly."
https://docs.scala-lang.org/tour/type-inference.html を参照
この型推論機能ですが、いちいち型を書かなくて済むので開発がとても楽になります。
もし型推論がなかったら、「""で囲っているんだから文字列だって分かってくれよ」などとぼやいていると思います。
本題
[wartremover:JavaSerializable] Inferred type containing Serializable: Product with Animal with java.io.Serializable
に遭遇しました。
原因
端的にいうと、case classの暗黙の継承と型推論が組み合わさることで想定外の挙動になってしまったのが原因でした。
以下、具体例を使って説明します。
まず適当な trait と それを継承する case class を考えます。
trait Animal {
abstract def getSound(): String
}
case class Dog() extends Animal {
override def getSound() = "Wan wan"
}
case class Cat() extends Animal {
override def getSound() = "Nyah Nyah"
}
さて、次のようにインスタンスを生成し、鳴き声のリストを作るコードを書いてみます。
val pochi = Dog()
val tama = Cat()
val pets = Seq(pochi, tama)
val sounds = pets.map(_.getSound)
すると、次のようなアウトプットを期待しますよね。
val sounds = Seq("Wan wan", "Nyah Nyah")
ところが、実際には次のようにコンパイラに怒られてしまいます。
[wartremover:JavaSerializable] Inferred type containing Serializable: Product with Animal with java.io.Serializable
メッセージを読むと型が間違っていそうだと分かるので、型を明記してみてます。
val pochi: Dog = Dog()
val tama: Cat = Cat()
val pets: Seq[Animal] = Seq(pochi, tama)
val sounds: Seq[String] = pets.map(_.getSound)
一見よさそうですが、実は3行目が間違っています。
冒頭にちらっと述べたcase classの暗黙の継承
が関係しています。
case classはパターンマッチをしてくれたり、applyメソッドを自動で作ってくれたりととても便利なクラスです。その便利さの裏では、ProductトレイトとSerializableトレイトを継承しています。
https://qiita.com/chimeiwang/items/b776c032bc4d4b687c32#6-product%E3%83%88%E3%83%AC%E3%82%A4%E3%83%88%E3%81%8C%E3%83%9F%E3%83%83%E3%82%AF%E3%82%B9%E3%82%A4%E3%83%B3%E3%81%95%E3%82%8C%E3%82%8B を参照
恥ずかしながら、私も今回ハマるまで知らなかったですし、気にしたこともありませんでした。
では、暗黙の継承を明記してみるとどうでしょうか。
trait Animal {
abstract def getSound(): String
}
case class Dog() extends Animal with Product with Serializable {
override def getSound() = "Wan wan"
}
case class Cat() extends Animal with Product with Serializable {
override def getSound() = "Nyah Nyah"
}
val pochi: Dog = Dog()
val tama: Cat = Cat()
val pets: Seq[Animal with Product with Serializable] = Seq(pochi, tama)
val sounds: Seq[?] = pets.map(_.getSound)
3行目を見ると、型はAnimal
ではないことが分かりました。
val pets: Seq[Animal with Product with Serializable]
Animal with Product with Serializable クラスにはgetSoundというメソッドは存在しないから怒られていたんですね!
解決策
前節で、未定義のメソッドを使用したから怒られたということが分かりました。
ここでは、「じゃあどうしたらいいの?」という疑問に答えようと思います。
解決策1
ProductとSerializableは無視するようにコンパイラにお願いする。
case class Dog() extends Animal {
@SuppressWarnings(Array("org.wartremover.warts.JavaSerializable", "org.wartremover.warts.Serializable", "org.wartremover.warts.Product"))
override def getSound() = "Wan wan"
}
case class Cat() extends Animal {
@SuppressWarnings(Array("org.wartremover.warts.JavaSerializable", "org.wartremover.warts.Serializable", "org.wartremover.warts.Product"))
override def getSound() = "Nyah Nyah"
}
解決策2
最初からProductとSerializableを継承してしまう。
trait Animals extends Product with Serializable
こうすれば、getSound
メソッドは未定義ではなくなるので解決します。
どちらがいいか
どちらがいいかは分かりません。
個人的には、毎回アノテーションを書くくらいなら最初から継承しておけば、同じ罠にハマることもないからよさそうです。
公式の対応
Scala 2系では残念ながら公式の便利機能はありません。
Scala 3系では、Transparantというトレイトが定義されています。
これを使うと、解決策1のようにProductとSerializableを無視するようにコンパイラに知らせることができます。
https://docs.scala-lang.org/scala3/reference/other-new-features/transparent-traits.html を参照