Lambdaカクテル

京都在住Webエンジニアの日記です

Invite link for Scalaわいわいランド

Scala 3でクラスフィールドの初期化順の困りを回避できるSafe Initialization機能が便利なので紹介したい

突然だがこのScala 3のコードはコンパイルするだろうか。先に正解を言っておくと「コンパイルする」(Scala 3.5.0)。

class Foo:
  val x = y
  val y = 42

では、このFooに向けて以下の呼び出しを行うとどうなるだろう?

Foo().x

驚くなかれ、この式は0に評価される。

!?!?!?!?!?!?

ナンデ?

Scalaのクラスにおけるコンストラクタ

Scalaでは、classobjectの上にいきなり地で書かれたコードはコンストラクタとしてすぐ実行してもらえる。

class Foo(x: Int, y: Int):
  private val computed = x + y
  val field = computed

Foo(1, 2).field // => 3

これにより、他の言語のようにconstructorといった名前のメソッドを特別に用意する必要がない。

そのままvalでフィールドを定義していくことにより、フィールドが初期化されずにnullで埋まっている宙ぶらりんな状態を文法上回避できるようになっている。

TypeScriptでは、コンパイラがフィールドの未初期化を検出できる:

class Foo {
    x: number
    y: number
    constructor () {
        this.x = 42
        // Property 'y' has no initializer and is not definitely assigned in the constructor.
    }
}

対してScalaは「その場で」定義と初期化を行うため、フィールドが初期化されていないということはない(コンパイラが検出する):

class Foo {
    val x: Int = 42
    val y: Int // 初期化していないのが一目瞭然
}
// => class Foo needs to be abstract, since val y: Int in class Foo is not defined 

docs.scala-lang.org

初期化順序

前述の仕様により、自明なことに、フィールドは定義した順に初期化しなければならない(定義した場所で初期化するのだからそうなる)。

しかし初期化していないフィールドを後方参照してしまうようなコードはコンパイルしてしまう:

class Foo:
  val x = y // この時点でyの値は不明だが、コンパイルが通って0になってしまう
  val y = 42

このことをScalaわいわいランドでしたところ、id:tanishiking24 に教えてもらった:

内部的にはまず各フィールドを0値で初期化したインスタンスを生成して、そのあとコンストラクタで各フィールドをx=y、y=42みたいな代入が発生するのでこうなる〜

Discord

ScalaはJVM言語なのでコンパイルされた後のコードの都合でこうなってしまうようだ。自分はこれを年に1度くらい踏んでいる気がする・・・。

lazy valによる回避

当たり前だが、「順序に気をつける」というのも立派な回避策だ。Scalaでは基本的に「小さい要素から順に」フィールドを書いていくのが良さそう。

しかし順序が混み合っていたりする場合はlazy valにすることでもこの問題を回避できる。フィールドが初めて呼ばれたタイミングで中身を評価し、その後はその値が使われるようになる。

class Foo {
  lazy val x = y
  val y = 42
}

Foo().x // => 42

Safe Initialization

ところで、Scala 3ではSafe Initialization機能がコンパイラオプションとして追加された。これはフィールドの呼び出し順を追跡して、初期化していないフィールドの呼び出しを検知して警告する機能だ。

このオプションは-Wsafe-initscalacOptionsに付けることで利用できる:

// build.sbt
lazy val root = project
  .in(file("."))
  .settings(
    // ...
    scalacOptions ++= Seq("-Wsafe-init"),
  )

docs.scala-lang.org

この機能は継承関係が挟まっていても正常に動作するようだ。

まとめ

  • Scalaでは、クラスフィールドは書いたタイミングで初期化する
  • Scalaコンパイラは、あるフィールドを初期化するタイミングで別のフィールドが初期化されているかを検知できない
  • Safe Initializationはコンパイラの機能を強化し、フィールドの呼び出しタイミングを追跡して危険な呼び出しを察知できる
★記事をRTしてもらえると喜びます
Webアプリケーション開発関連の記事を投稿しています.読者になってみませんか?