0utputab1e

Scalaの「lazy val」について調査してみた

 2020-07-15
 

過去の記事「Ubuntu18.04環境にScala(sbt)をインストールする」でScalaの実行環境が整いました。

そこからさまざまなコードを試していくなか、「lazy val」の性質が気になったのでブログに残しておこうと思います。

[ 検証1 ] 変数とメソッド

比べてみるコード一覧

記述は先頭の「lazy」が変わるのみで中身を同じ処理にしてみました。

val a = {
  println("test")
  12
}

def b = {
  println("test")
  12
}

lazy val c = {
  println("test")
  12
}

これらの変数、メソッドをsbtコンソール上で叩いてみようと思います。

「sbt console」を使う

ターミナル上から「sbt」コマンドでsbtを起動し、「console」と入力して実行すると、scalaの対話コンソールが立ち上がります。

$ sbt
[warn] No sbt.version set in project/build.properties, base directory: /home/<user_name>/container_scala
[info] welcome to sbt 1.3.13 (Private Build Java 1.8.0_252)
[info] set current project to container_scala (in build file:/home/<user_name>/container_scala/)
[info] sbt server started at local:///home/<user_name>/.sbt/1.0/server/0eb12872a2adcc4eb29f/sock
sbt:container_scala> console
[info] Compiling 1 Java source to /home/<user_name>/container_scala/target/scala-2.12/classes ...
[info] Starting scala interpreter...
Welcome to Scala 2.12.10 (OpenJDK 64-Bit Server VM, Java 1.8.0_252).
Type in expressions for evaluation. Or try :help.

scala>

ここまできたら、先程の2つの変数を順番に定義し、呼び出してみます。

普通の「val」の場合

scala> val a = {
     |   println("test")
     |   12
     | }
test
a: Int = 12

scala> a
res0: Int = 12

scala> a
res1: Int = 12

定義した直後にprintln文が評価されています。その後は何度呼び出してもprintlnは実行せず、最終的な戻り値「12」のみを返すようです。

「def」の場合

こちらは変数ではなく「メソッド」なので、実行のたびに処理が走っています。
定義直後は何も起こらず、呼び出すと毎回printlnの結果と最後の戻り値を返しています。

scala> def b = {
     |   println("test")
     |   12
     | }
b: Int

scala> b
test
res4: Int = 12

scala> b
test
res5: Int = 12

「lazy val」の場合

では、「lazy」にするとどうでしょうか?

scala> lazy val c = {
     |   println("test")
     |   12
     | }
c: Int = <lazy>

scala> c
test
res2: Int = 12

scala> c
res3: Int = 12

定義直後はlazyであることがわかるだけで、println文は実行されてないです。この点ではメソッドと同様に見えます。

しかし、printlnの出力「“test”」があるのは初回の呼び出し時のみで、以降は普通のvalと同じく、戻り値「12」のみが返されています。

 
処理が「初回一回のみ」しか実行されないのがポイントです。

[ 検証2 ] オブジェクトとクラス

未定義のオブジェクトをクラスのメンバに含められる

また、以下の例を見てみてください。

これは、一旦コンソールから退出して、エディタで作成したコードです。

object compareAnimals {
  def main(args:Array[String]): Unit = {
    animals.bird.talk()
    animals.dog.talk()
  }
}

class Bird(val name: String) {
  val my_name = name
  lazy val dog = animals.dog
  def talk(): Unit = {
    println(s"僕は${my_name}${dog.name}より自由に飛べるよ!")
  }
}

class Dog(val name: String) {
  val my_name = name
  lazy val bird = animals.bird
  def talk(): Unit = {
    println(s"僕は${my_name}${bird.name}より自由に走れるよ!")
  }
}

object animals {
  val bird: Bird = new Bird("ピヨ太郎")
  val dog: Dog = new Dog("ポチ")
}

クラス「Bird」と「Dog」をオブジェクト「animals」内で初期化していますが、これらのクラスが初期化完了するまで、オブジェクト「animals」は未定義の状態です。

 
このコードからlazy抜きで呼び出すと、2つのクラス内で未定義の(存在しない)オブジェクトを参照していることになるため、「java.lang.NullPointerException」が返され実行に失敗します。

 
しかし「lazy」によってcompareAnimals.main()が実行されるまで評価を遅らせる(animalsの参照を待たせる)ことで、animals定義完了後の状態で参照できるようになり、このコードは実行に成功します。

 
再度sbt consoleに戻り、「runMain メインオブジェクト名」で実行してみると、以下のように返ってきます。

sbt:container_scala> runMain compareAnimals
[info] Compiling 1 Scala source to /home/<user_name>/container_scala/target/scala-2.12/classes ...
[warn] Multiple main classes detected.  Run 'show discoveredMainClasses' to see the list
[info] running compareAnimals
僕はピヨ太郎。ポチより自由に飛べるよ!
僕はポチ。ピヨ太郎より自由に走れるよ!
[success] Total time: 2 s, completed 2020/07/15 23:46:20

ちょっと変わった使い方ができますね。

最後に

Scalaチュートリアルサイトでも言われている通り、遅延valは、valの初期化が初めてアクセスされるまで遅延される言語機能で、その後は通常のvalと同じように機能する変わった性質があるようです。

  • 初期化に時間がかかるなど、コストの高い処理
  • 常には使用されない処理

の場合、適所でlazyを利用すると不必要な計算を削減できて便利そうです。

また、

  • 未定義のオブジェクトをクラスの初期化に含める必要があるケースなど

でも利用できて大変便利ですね。

 
うまく使いこなすには練習が必要そうですが、知っておけばきっと今後の役に立つと思います。

 
ではこの辺りで!

 

あわせて読みたい記事

>> Homeに戻る