"Nullable" means that it is a subclass of AnyRef
(and not Nothing
), therefore, you can enforce that MyClass
takes only nullable instances as follows:
case class MyClass[T <: AnyRef](t: T)
MyClass("hey")
MyClass[String](null)
MyClass(null)
// MyClass[Int](3) won't compile, because `Int` is primitive
To determine whether a type is nullable, you could provide implicit methods that generate nullability tokens:
sealed trait Nullability[-T]
case object Nullable extends Nullability[AnyRef] {
def isNull(t: Any): Boolean = t == null
}
case object NotNullable extends Nullability[AnyVal]
object Nullability {
implicit def anyRefIsNullable[T <: AnyRef]: Nullability[T] = Nullable
implicit def anyValIsNotNullable[T <: AnyVal]: Nullability[T] = NotNullable
}
def myFunc[T](t: T)(implicit nullability: Nullability[T]): Unit = {
nullability match {
case Nullable =>
if (t == null) {
println("that's a null")
} else {
println("that's a non-null object: " + t)
}
case NotNullable => println("That's an AnyVal: " + t)
}
}
Now you can use myFunc
as follows:
myFunc("hello")
myFunc(null)
myFunc(42)
// outputs:
// that's a non-null object: hello
// that's a null
// That's an AnyVal: 42
This won't compile if you try to use myFunc
on Any
, because the compiler won't be able to determine whether it's AnyRef
or AnyVal
, and the two implicit methods will clash. In this way, it can be ensured at compile time that we don't accidentally use myFunc
on Any
, for which the nullability cannot be determined at compile time.