這幾天會使用 隨機數字產生器 來介紹 functional programming 如何操作狀態變更,我們就能學會如何讓那些有狀態的程式純粹化,進而符合 Referential Transparency。
在 Scala 的標準庫中,有個 scala.util.Random 可以用來產生隨機數,
目前電腦上還沒有真正意義上的隨機,都是 Pseudo-Random,有興趣的話看看這篇 介紹 吧。
此 class 十足的依賴 side effect 來產生隨機數,例如下述程式片段(使用 Scala REPL),
scala> import scala.util.Random
scala> val rng = Random
val rng: scala.util.Random.type = scala.util.Random$@62e73ab6
scala> rng.nextDouble
val res0: Double = 0.17978255826607414
scala> rng.nextDouble
val res1: Double = 0.39977332524008824
scala> rng.nextInt
val res2: Int = -798663927
scala> rng.nextInt(10)
val res3: Int = 4
我們可以合理的假設 Random 裡面一定有改變某些值,如此才能讓每一次 nextInt()
的呼叫都能得到不同的結果,代表了每一次的 nextDouble 或 nextInt 調用都有改變 Random 類別裡的成員變數,所以我們可以說這些 function 有 side effect,也就不符合 Referential Transparency,也就難以測試;
假設我們有個 function 會用到隨機數,例如擲骰子好了,
def rollDie: Int =
val rng = scala.util.Random
rng.nextInt(5)
rng.nextInt(5)
會回傳 0 ~ 4 的隨機數,但可能當下工程師不知道這件事,所以我們會有測試程式來測試我們寫的 function 邏輯,但此時會有 1/5 的機率測試會失敗,如果失敗,通常我們會重現該錯誤,然後在測試案例中加入該錯誤,來確認我們的 function 是不是能避免該錯誤,但這裡我們無法簡單的覆現 rollDie 回傳 0 這個數字。
要讓 nextInt function 符合 RT 的關鍵點就是讓狀態改變明確化,也就是不偷偷摸摸的用 side effect 方式改狀態,直接將新的狀態跟著原來要回傳的值綁在一起返回,以下是新隨機數產生器的介面定義,
trait RNG:
def nextInt: (Int, RNG)
Scala 的 trait 可把它當作 Java 的 Interface,但比 Interface 更強大,例如 Scala 3 的 trait 就支援宣告成員變數,想了解更多的話可看此文件。
nextInt 除了回傳隨機數之外,我們也把改變 seed 狀態之後的隨機數產生器一併回傳,這裡不需要維護 global 範圍的變數值,舊產生器的狀態不會被影響,此狀態還是被封裝在物件中,調用者依舊不需要關心隨機數產生器的實現細節,
以下我們用一個跟 scala.util.Random 相同實作的簡單算法 Linear congruential generator 來實現 RNG,
case class SimpleRNG(seed: Long) extends RNG:
def nextInt: (Int, RNG) =
val newSeed = (seed * 0x5DEECE66DL + 0xBL) & 0xFFFFFFFFFFFFL // `&` 是 AND 操作,我們用目前的 seed 來產生新的 seed
val nextRNG = SimpleRNG(newSeed) // 下一個 seed 狀態的 RNG
val n = (newSeed >>> 16).toInt // `>>>` 右移位元運算,然後補 0,`n` 值是我們的 偽隨機數 (pseudo-random)
(n, nextRNG) // 回傳隨機數以及新 seed 狀態的 RNG
現在我們不管怎麼呼叫 nextInt
都會得到一樣的值了,換句話說我們的 function 變純了。
scala> val rng = SimpleRNG(73)
val rng: SimpleRNG = SimpleRNG(73)
scala> val (n1, rng2) = rng.nextInt
val n1: Int = 28086669
val rng2: RNG = SimpleRNG(1840687985952)
scala> val (n2, rng3) = rng2.nextInt
val n2: Int = 1553204112
val rng3: RNG = SimpleRNG(101790784741035)
scala> rng.nextInt
val res11: (Int, RNG) = (28086669,SimpleRNG(1840687985952))
明天繼續。
文章更正 2023-11-02 感謝 hlb 提醒,修正
rng.nextInt(5)
會回傳 0 到 4 的隨機數。
rng.nextInt(5)
會回傳 0 ~ 5 的隨機數
rng.nextInt(5)
應該回傳 0 到 4 的隨機數喔。
已修正,感謝提醒。