iT邦幫忙

2023 iThome 鐵人賽

DAY 12
0

這幾天會使用 隨機數字產生器 來介紹 functional programming 如何操作狀態變更,我們就能學會如何讓那些有狀態的程式純粹化,進而符合 Referential Transparency。

使用 side effect 來產生隨機數

在 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 這個數字。

pure (純粹) 的隨機數產生器

要讓 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 的隨機數。


上一篇
Strictness 和 Laziness (3)
下一篇
純粹的 functional 狀態 (2)
系列文
用 Scala 3 寫的 Functional Programming 會長什麼樣子?30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
hlb
iT邦新手 5 級 ‧ 2023-11-02 20:40:06

rng.nextInt(5) 會回傳 0 ~ 5 的隨機數

rng.nextInt(5) 應該回傳 0 到 4 的隨機數喔。

tshine73 iT邦新手 4 級 ‧ 2023-11-02 22:56:05 檢舉

已修正,感謝提醒。

我要留言

立即登入留言