scalanullablescala-3non-nullablenullability

How to avoid the "type test cannot be checked" waring in pattern matches of nullable types?


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?


Solution

  • 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 that p is non-null if it's compared with null. This information can then be propagated to the then and else 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)))