Always add an explicit type to your public members, even when you’re happy with what’s being inferred.
The compiler relies on implementation details to infer types, and implementation details can change - which can in turn break binary compatibility.
Type inference tries to work out the most specific subtype of all types a value could be. Take the following code, for example:
def foo(i: Int) = {
if(i % 2 == 0) Some(i)
else None
}
foo(1)
// res0: Option[Int] = None
Some[Int]
and None
share a common direct supertype: Option[Int]
(well, not quite, but close enough for our purposes). This allows the compiler to correctly infer foo
’s return type, but what if we were to change the implementation in a later version?
def foo(i: Int) = Some(i)
foo(1)
// res2: Some[Int] = Some(1)
The return type is no longer Option[Int]
but Some[Int]
, and just like that, we’ve broken binary compatibility.
Explicit type annotations on public members ensure that implementation details don’t leak out and that we don’t accidentally break things without meaning to, or even realizing.
A common assumption that turns out to be incorrect is that implementing abstract members (or overriding concrete ones) is fine, since the parent type will be inferred.
This turns out to be incorrect:
abstract class Foo {
def getOpt[A](a: A): Option[A]
}
class FooImpl extends Foo {
override def getOpt[A](a: A) = Some(a)
}
It’s usually assumed that FooImpl.getOpt
will return the right type, but this is wrong:
new FooImpl().getOpt(1)
// res3: Some[Int] = Some(1)
Changing FooImpl.getOpt
implementation to return either a Some[Int]
or an Option[Int]
will break binary compatibility just as much as the general case.
Linter | Rule |
---|---|
WartRemover |