I am writing code that requires some performance considerations. To achieve this, I am using var instead of val to allow dynamic value modification. However, since I want to maintain both performance and maximum safety, I’m employing Explicit Nulls.
For simplicity, let’s assume we have var x: Int | Null.
When context clearly indicates that x cannot be null, I want to subtract 1 from it (spoiler alert: I’m trying to port fast code from another language that uses nulls). With x: Int, I’d write x -= 1, but here the possibility that x might be null prevents this from compiling.
So I try writing x.nn -= 1, but this also fails to compile.
At this point, the only proper way seems to be writing x = x.nn - 1. While this works, I believe there might be a more elegant solution. I’d appreciate your input on this.
var x: Int | Null = null
println(x) // null
x = 42
// x -= 1 // does not compile
println(x) // 42
// x.nn -= 1 // does not compile
if (x != null) {
//x -= 1 // wow!! does not compile!!
}
x = x.nn - 1
println(x) // 41
It would be best to open issue on Scala 3 GH issues so that you can reach directly to the compiler team working on explicit null
As a side note if you’re having a performance requirements you most likely should not use Int | Null - it would require to box primitive scala.Int into java.lang.Integer at all times.
The problem however also applies to cases where we’re dealing with reference types (so there won’t be any additional performance penalty)
//> using options -Yexplicit-nulls
def example = {
var buf: List[Int] | Null = null
buf = List(0)
buf ::= List(1, 2, 3) // error: value ::= is not a member of List[Int] | Null
}
See what compiler would actually need to emit below:
// //> using options -Yexplicit-nulls
def exmaple = {
var x: Int | Null = null
println(x)
x = x.nn + 1
println(x)
}
> scala compile a1.scala -Vprint:genBCode
Compiling project (Scala 3.7.4, JVM (21))
[[syntax trees at end of genBCode]] // /Users/wmazur/projects/scala/scala3/a1.scala
package <empty> {
@SourceFile("a1.scala") final module class a1$package extends Object {
def <init>(): Unit =
{
super()
()
}
private def writeReplace(): Object =
new scala.runtime.ModuleSerializationProxy(classOf[a1$package])
def exmaple(): Unit =
{
var x: Object = null
println(x)
x =
Int.box(
{
val x$proxy1: Object = x
{
(if x$proxy1:Object == null then
scala.runtime.Scala3RunTime.nnFail() else ())
Int.unbox(x$proxy1:Object)
}:Int + 1
}
)
println(Int.box(Int.unbox(x:Object)))
}
}
final lazy module val a1$package: a1$package = new a1$package()
}
For comparsion variant using only primitive Int
> scala compile a1.scala -Vprint:genBCode
Compiling project (Scala 3.7.4, JVM (21))
[[syntax trees at end of genBCode]] // /Users/wmazur/projects/scala/scala3/a1.scala
package <empty> {
@SourceFile("a1.scala") final module class a1$package extends Object {
def <init>(): Unit =
{
super()
()
}
private def writeReplace(): Object =
new scala.runtime.ModuleSerializationProxy(classOf[a1$package])
def exmaple(): Unit =
{
var x: Int = 0
println(Int.box(x))
x = x + 1
println(Int.box(x))
}
}
final lazy module val a1$package: a1$package = new a1$package()
}