Magnolia Tech

いつもコードのことばかり考えている人のために。

Scala3学習メモ: Scalaとnull, Scala2とScala3と

Rustの公式ドキュメントにnullに関する興味深い記事("Null References: The Billion Dollar Mistake"(Null参照: 10億ドルの間違い))が引用されています。

Enumを定義する - The Rust Programming Language

nullは、インタフェースにおける双方の合意として取り得る値とするか、しないかがいつもあいまいになってしまう点が問題。ある変数の型を見てもnullを取り得るかは明示されていないにも関わらず、人によって「nullを考慮すべき」「nullは取り得ない」の二つの状態が混在してしまう。たとえ、同じ人が双方のコードを書いていたとしても。

ScalaにおけるNull型のクラス階層における位置付けと、nullの代入

https://docs.scala-lang.org/resources/images/tour/unified-types-diagram.svg

https://docs.scala-lang.org/tour/unified-types.html

Scalaにおける型の階層ではNull型はAnyRefのサブタイプになっています。そして、AnyValのサブタイプではありません。

scala> class Person(name: String, age: Int)
// defined class Person

scala> val person: Person = null
val person: Person = null

scala> val num: Int = null
1 |val num: Int = null
  |               ^^^^
  |               Found:    Null
  |               Required: Int

つまり、AnyRefを継承しているクラス型の変数にはNull型の唯一の値であるnullを代入できますが、AnyValを継承しているInt型の変数にはnullの代入はできない、ということを示します。

Option型によるnullへの対応

しかし、nullはやっかいです。参照すればすぐにNull pointer exceptionが発生します。意図してインタフェースとしてnullを取り得る設計としたとしても、受け取る側がnullに対応するコードを十分に書けないこともあるし、意図してnullは取り得ない設計としたとしても渡す側がうっかりnullを渡すことが有ります。

前者は、requireなどで引数のnullチェックを行うルールにすべきですが、残念ながら強制力がありません。

強制力を持たせるためにはインタフェースの境界を、Option型でラップすることです。

scala> case class Person(name: String, age: Int)
// defined case class Person

scala> def printPerson(person: Option[Person]): Unit =
     |   person match {
     |     case Some(p) => println(s"name:${p.name}, age: ${p.age}")
     |     case None    => println("no person")
     |   }
     |
def printPerson(person: Option[Person]): Unit

scala> val p1 = null
val p1: Null = null

scala> printPerson(Option(p1))
no person

scala> val p2 = new Person("Arthur", 42)
val p2: Person = Person(Arthur,42)

scala> printPerson(Option(p2))
name:Arthur, age: 42

Option型でラップされていることで、Some、Noneに対応するコードを必ず書くための強制力が働き、nullが有る世界と、nullが無い世界を分離されました。

(Optionのapplyにnullを渡すとNoneが生成され、null以外を渡すと、その値をラップしたSomeが生成されます)

世界は平和になりました...

いや、そうか?では全ての外部からもたらされる変数をいちいち全部Option型でラップするのか?意図しないnullの混入を防ぐためにはtoo muchではないか?

EXPLICIT NULLS

nullにおける問題は、その変数がnullを取り得るか、明示されていないことにある...インタフェースの設計として明示に書いてあればいいけど、ドキュメントに頼るのも限界があるし、内部のインタフェースでいちいち文書化もしていられないだろう。

そもそもAnyRefがnullを許容しているからいけない...ということで、Scala3からAnyRefのサブタイプへのnull代入を不可とするモードが追加になりました。

https://docs.scala-lang.org/resources/images/scala3/explicit-nulls/explicit-nulls-type-hierarchy.png

Explicit Nulls | Scala 3 Language Reference | Scala Documentation

クラス階層が変更になっています。

ただし、デフォルトで有効になるのではなく、コンパイラオプションとして-Yexplicit-nullsを設定したときだけ有効になります。

$ scala -Yexplicit-nulls
scala> case class Person(name: String, age: Int)
// defined case class Person

scala> val person: Person = null
1 |val person: Person = null
  |                     ^^^^
  |                     Found:    Null
  |                     Required: Person

scala> val num: Int = null
1 |val num: Int = null
  |               ^^^^
  |               Found:    Null
  |               Required: Int

もうnullを代入することはできません。

どうしてもNullを代入したい時は、明示的にNull型を許容することを指定します。

scala> val person: Null = null
val person: Null = null

しかし、これではNull型に固定されてしまうため、あまり役に立ちません。

Scala3から導入されたUnion型が使えます。

scala> val person: Person | Null = null
val person: Person | Null = null

こうして、「うっかり」nullが混入し、Null Pointer Exceptionが発生すること限りなく抑制することができ、かつ記述も簡潔になりました。

世界は少し平和になりました。