2015年9月21日

Scala Puzzlers: Init You Init Me

Scala Puzzlers 是收集很多人貢獻的 scala 的題目,類似 Java Programmer 認證的考題,主要是確認 Programmer 是不是有了解 scala 語言的規格與特性。

Init You, Init Me 的問題是以下的 scala statements 會列印出什麼結果?

object XY {
  object X {
    val value: Int = Y.value + 1
  }
  object Y {
    val value: Int = X.value + 1
  }
}

println(if (math.random > 0.5) XY.X.value else XY.Y.value)

答案是 2

lazy, strict

首先我們要知道 scala 的保留字: lazy,當某個變數宣告為 lazy 時,則表示 scala 會等到要存取該變數時,才會初始化這個變數。跟 lazy 相反的是 strict,strict 表示會在定義時,就馬上將該變數初始化,scala 的 collection 中就有區分 lazy 與 strict 的兩種版本,因大量的 collection elements,並不一定會在定義時,就馬上需要使用到所有的 elements,因此不需要直接將 collection 所有元素初始化,等到需要使用時再處理。

如果以 recursive 的方式定義變數,一開始會發現 REPL 警告要定義資料型別。

scala> val x = y; val y = x
<console>:15: error: recursive value y needs type
       val x = y; val y = x
               ^

當我們將 y 的資料型別,明確地定義出來,compiler 就接受我們宣告的 x 與 y 的定義,另外出現一個警告,告訴我們有個 statement 參考到了沒有定義初始值的 y,然後compiler 就自動將 y 的數值視為 0,所以將 x 也設為 0。

scala> val x: Int = y; val y = x
<console>:12: warning: Reference to uninitialized value y
       val x: Int = y; val y = x
                    ^
x: Int = 0
y: Int = 0

如果加上 lazy 的宣告,則 x 與 y 並不會在宣告時,就被初始化。

scala> lazy val x: Int = y; lazy val y = x
x: Int = <lazy>
y: Int = <lazy>

比較合理的使用方式,應該是在宣告 x 時,宣告為 lazy,但是讓 y 直接初始化,因為一開始可能不知道 y 的數值,後來知道了 y 時,再給予

scala> lazy val x: Int = y; val y=x; println("x="+x+", y="+y)
x=0, y=0
x: Int = <lazy>
y: Int = 0

另外因為 scala 規格中提到 "the value defined by an object definition is instantiated lazily",在 object 中定義的變數,scala 會自動以 lazy 的方式初始化。

結果不一定永遠是 2

因為題目只有運算過一次以下這個 statement

println(if (math.random > 0.5) XY.X.value else XY.Y.value)

如果我們多呼叫幾次,會發現結果也有可能會變成 1

scala> println(if (math.random > 0.5) XY.X.value else XY.Y.value)
2

scala> println(if (math.random > 0.5) XY.X.value else XY.Y.value)
1

scala> println(if (math.random > 0.5) XY.X.value else XY.Y.value)
1

分析題目

把原本的題目 math.random 的部份用變數紀錄起來,會比較清楚處理的過程。

object XY {
  object X {
    val value: Int = Y.value + 1
  }
  object Y {
    val value: Int = X.value + 1
  }
}

val randomnumber=math.random
println( if (randomnumber > 0.5) XY.X.value else XY.Y.value )

當我們在 REPL 執行這些 statement時,可查看到結果為 2,在以下這個情況下,XY.Y.value 為 2 XY.X.value 為1,因為在使用 XY.Y.value 的時候,發現參考到 X.value,但 X 也還沒初始化,發現參考到 Y.value,這時發現 recursive 的狀況,就直接將 Y.value 視為 Integer 的初始值 0,接下來 XY.X.value 就等於 1,而 XY.Y.value 則為 2。

scala>  object XY {
     |   object X {
     |     val value: Int = Y.value + 1
     |   }
     |   object Y {
     |     val value: Int = X.value + 1
     |   }
     | }
defined object XY

scala> val randomnumber=math.random
randomnumber: Double = 0.24469087654673705

scala> println( if (randomnumber > 0.5) XY.X.value else XY.Y.value )
2

我們必須在 REPL 先執行 :reset,然後再運算一次這些 statements,結果才會永遠是 2,在以下這個情況下,XY.X.value 為 2 XY.Y.value 為1。

scala> :reset
Resetting interpreter state.
Forgetting this session history:

scala> object XY {
     |   object X {
     |     val value: Int = Y.value + 1
     |   }
     |   object Y {
     |     val value: Int = X.value + 1
     |   }
     | }
defined object XY

scala> val randomnumber=math.random
randomnumber: Double = 0.7747791382247455

scala> println( if (randomnumber > 0.5) XY.X.value else XY.Y.value )

因為以上兩個狀況都有可能會發生,我們也可以知道,因為 XY.X.value 跟 XY.Y.value 兩個數值,其中一個是 1 另一個是 2,我們多呼叫幾次,就會發現結果也有可能會變成 1。

scala> println(if (math.random > 0.5) XY.X.value else XY.Y.value)
2

scala> println(if (math.random > 0.5) XY.X.value else XY.Y.value)
1