エムオーテックス株式会社が運営するテックブログです。

Scalaのlazyを使用すると変数の初期化が「早くなる」ことがあるという話

Scalaのlazyを使用すると変数の初期化が「早くなる」ことがあるという話

はじめに

こんにちは、アプリケーションチームの小西です。

アプリケーションチームでは開発言語の1つとしてScalaを使用しています。Scalaを使った開発を進める中で、lazyの使い方で新たな発見がありましたので、今回はその内容を皆さんにお伝えしたいと思います。

lazyとは

まず、lazyとはなにかについて説明します。
lazyは「怠惰な」「ゆっくりとした」などの意味を持つ英単語です。

Scalaでは、変数の前にlazyキーワードをつけることによって、その変数がはじめて参照されるまで評価タイミングを遅延させられます。
このような方法を「遅延評価」と呼びます。
たとえば、重めの初期化処理をする変数があり、それが条件分岐次第で必要ない場合などに、lazyを使用して処理を効率化できます。

lazyを使用することで評価を遅延できることを試すコードを以下に示します。

// lazyを使用しない場合
object Main extends App {
    val hoge = {
        println("hogeを初期化")
        "hoge"
    }
    val fuga = {
        println("fugaを初期化")
        "fuga"
    }
    
    println(hoge)
    println(fuga)
}
// 実行結果(hogeが先に初期化される)
hogeを初期化
fugaを初期化
hoge
fuga
// lazyを使用する場合
object Main extends App {
    lazy val hoge = {              // lazyをつける
        println("hogeを初期化")
        "hoge"
    }
    val fuga = {
        println("fugaを初期化")
        "fuga"
    }
    
    println(hoge)
    println(fuga)
}
// 実行結果(fugaが先に初期化される。hogeの初期化はprintln(hoge)まで遅延される)
fugaを初期化
hogeを初期化
hoge
fuga

lazyとトレイトの組み合わせ

lazyの動作について知った上で、次は下記のコードを見てください。

// lazyを使用しない場合
trait Hoge {
    val x: String
    val y = "hoge_" + x
}

trait Fuga {
    val x = "fuga"
}

object Main extends App with Hoge with Fuga {
    println(y)
}
// 実行結果(fugaにしたい箇所がnullになる)
hoge_null

「fuga」を表示したい部分が「null」になってしまいました。
これはFugaトレイトでxが初期化されるより前に、Hogeトレイトで"hoge_" + xが評価されているためです。
では、lazyを使用して以下のようにします。

// lazyを使用する場合
trait Hoge {
    val x: String
    val y = "hoge_" + x
}

trait Fuga {
    lazy val x = "fuga" // lazyをつける
}

object Main extends App with Hoge with Fuga {
    println(y)
}
// 実行結果(fugaにしたい箇所がfugaになる)
hoge_fuga

"hoge_" + xが評価される時点でlazy val xが評価され、"fuga"となります。
このコードであれば、「null」ではなく「fuga」を表示できます。

実行順を見てみる

上記のコードの実行順をわかりやすくするために、Hoge、Fuga、x、yを初期化するときにログが出るようprintlnを仕込みます。

// lazyを使用しない場合
trait Hoge {
    println("Hogeを初期化")
    val x: String
    val y = {
        println("yを初期化")
        "hoge_" + x
    }
}

trait Fuga {
    println("Fugaを初期化")
    val x = {
         println("xを初期化")
        "fuga"
    }
}

object Main extends App with Hoge with Fuga {
    println(y)
}
// 実行結果(上から評価される)
Hogeを初期化
yを初期化
Fugaを初期化
xを初期化
hoge_null

Hoge→Fuga→Mainの順に初期化を行い、それぞれの中では上から順に評価されています。

// lazyを使用する場合
trait Hoge {
    println("Hogeを初期化")
    val x: String
    val y = {
        println("yを初期化")
        "hoge_" + x
    }
}

trait Fuga {
    println("Fugaを初期化")
    lazy val x = {              // lazyをつける
         println("xを初期化")
        "fuga"
    }
}

object Main extends App with Hoge with Fuga {
    println(y)
}
// 実行結果(xがFugaより先に評価される)
Hogeを初期化
yを初期化
xを初期化
Fugaを初期化
hoge_fuga

「Fugaを初期化」と「xを初期化」の順番が入れ替わっています。
Hogeの初期化時にxを参照したときに、lazyがついているためFugaのxの評価が行われています。

Scalaのlazyを使用すると変数の評価が「早くなる」ことがあるという話

実行順から、lazyを使用することで変数を評価するタイミングが「早くなる」ことがある、という新たな気づきを得ることができました。 上記のコードでは「xを初期化」が本来より早くなり、「Fugaを初期化」より前になっています。

「lazy=遅延評価=評価を参照時まで遅延させる」という固定観念を持ってしまっていたので、このようなパターンがあることを知って少し驚きました。
オブジェクトを扱う場合などはNullPointerExceptionが発生する危険性もあるため、注意しなければならないと感じました。
ただ実際には開発中にエラーとなることの方が多いと思うので、通常の評価より早く評価されるパターンもあることを知っておくと、よりスムーズにトラブルを解決できるかもしれません。

おわりに

今回の記事では、lazyの動作について解説しました。
lazyの動作を考えると当然の内容ではあるのですが、lazyという名称から実際にその動作に合うまで意識できていませんでした。
値が入るはずなのになぜかnullが出てくる、などの場合はlazyを含めて初期化処理などの順番を再確認するとよさそうです。

ここまでお読みいただきありがとうございました。