The intention of the following method is to remove null values
def filterOutNulls[T](items: Iterable[T | Null]): Iterable[T] =
items.collect { case element: T => element }
The code works at runtime despite the compiler warns with: "the type test for T
cannot be checked at runtime because it refers to an abstract type member or type parameter".
Adding a ClassTag[T]
as context parameter eliminates the warning but I think using ClassTag
for this case is an overkill.
Here is another approach:
def filterOutNulls[T](items: Iterable[T | Null]): Iterable[T] =
items.collect {
case element if element != null => element.asInstanceOf[T]
}
In this case an asInstanceOf[T]
is required or the compiler complains. Apparently it ignores the fact that the null
value is the only instance of the Null
type.
A limited and inefficient workaround would be to define an extractor. It still uses instanceOf
but only one time instead of on every match
in your code.
object NotNull {
def unapply[T <: AnyRef](scrutinee: T | Null): Option[T] =
if scrutinee == null then None
else Some(scrutinee.asInstanceOf[T])
}
That above extractor is limited because it only solves the cases where the match is not exhaustive.
def filterOutNulls[T](items: Iterable[T | Null]): Iterable[T] =
items.collect {
case NotNull(element) => element
}
It is not applicable to exhaustive matches like the following.
(nullable: T <: AnyRef) match { // the compiler complains with "not exhaustive"
case NotNull(nonNullable) => Some(nonNullable)
case null => None
}
The compiler warns the match is not exhaustive. It is not intelligent enough to notice that the match is actually exhaustive.
The extractor is also inefficient because the unapply
method creates a Some
instance at runtime which I consider too much overhead for the only purpose of suppressing a false compiler warning.
What can I do in scala 3.4 to avoid the type check warning without relaying on ClassTag
or asInstanceOf
? Is it possible?
You can switch off the warning "the type test for T
cannot be checked at runtime because it refers to an abstract type member or type parameter" with scala.unchecked
def filterOutNulls[T](items: Iterable[T | Null]): Iterable[T] =
items.collect { case element: T @unchecked => element }
I guess you can be interested in Scala 3 settings
scalacOptions += "-Yexplicit-nulls"
https://docs.scala-lang.org/scala3/reference/experimental/explicit-nulls.html
Explicit nulls work well in some scenarios:
type T
val item: T | Null = null
item match
case null => println("null")
case _ =>
item: T // compiles
println("not null")
// prints: null
if item != null then
item: T // compiles
println("not null")
else println("null")
// prints: null
The problem is that currently this sort of type inference (flow typing) works with stable val (like above item
)
We added a simple form of flow-sensitive type inference. The idea is that if
p
is a stable path or a trackable variable, then we can know thatp
is non-null if it's compared withnull
. This information can then be propagated to thethen
andelse
branches of an if-statement (among other places).
while element
in
def filterOutNulls[T](items: Iterable[T | Null]): Iterable[T] =
items.collect {
case element if element != null => element // compile error: Found: (element : T | Null) Required: T
}
is not stable.
You can use .nn
def filterOutNulls[T](items: Iterable[T | Null]): Iterable[T] =
items.collect {
case element if element != null => element.nn
}
def filterOutNulls[T](items: Iterable[T | Null]): Iterable[T] =
items.collect(Function.unlift(item => Option(item).map(_.nn)))