iT邦幫忙

2023 iThome 鐵人賽

DAY 14
0

對最初的 rollDie function 有什麼影響

延續 昨天 最後設計的產生非負數且小於特定值的隨機數 nonNegativeLessThanViaFlatMap function,

我們可以用它來改寫 Day 12 的 rollDie function,此實現跟原本的一樣,會骰出 0~5 的數字,

def rollDie: Rand[Int] = nonNegativeLessThanViaFlatMap(6)

但因為該 function 變純粹了,不會在 RNG 類別內部有任何狀態改變,所以現在我們可以成功地覆現骰出 0 的問題,

scala> val num = rollDie(SimpleRNG(5))._1
val num: Int = 0

能成功覆現就可以將它加入測試案例中,然後調整 rollDie 程式後,它就能真正的骰出 1~6 的數字啦!

def rollDie: Rand[Int] = map(nonNegativeLessThanViaFlatMap(6))(_ + 1)

一般化的 State 型態

昨天我們透過 Rand 型態來簡化型態表示,如果不用別名的話,map function 會長下面這個樣子,

// with alias
type Rand[+A] = RNG => (A, RNG)

def map[A, B](s: Rand[A])(f: A => B): Rand[B]

// without alias
def mapNonAlias[RNG, A, B](s: RNG => (A, RNG))(f: A => B): RNG => (B, RNG)

但其實 map 並不在意它處理的是 RNG 還是誰,所以我們可以進一步把 RNG 抽象成泛型型態 S,

def map[S, A, B](s: S => (A, S))(f: A => B): S => (B, S)

然後我們應該要在更一般化的定義 S => (A, S),來處理所有的狀態改變,

type State[S, +A] = S => (A, S)

因為 State 跟 Rand 一樣,都是輸入某物件後,將結果隨同改變狀態後的物件回傳,但 State 更一般化,所以我們需要一個地方來包裹所有會用到的 function,在 Scala 3 我們可以這樣定義:

opaque type State[S, +A] = S => (A, S)

object State:

  extension[S, A] (underlying: State[S, A])

    def run(s: S): (A, S) = underlying(s)

    def flatMap[B](f: A => State[S, B]): State[S, B] =
      s =>
        val (a, s1) = underlying(s)
        f(a)(s1)

    def map[B](f: A => B): State[S, B] =
      flatMap(a => unit(f(a)))

  def unit[S, A](a: A): State[S, A] =
    s => (a, s)

  def apply[S, A](f: S => (A, S)): State[S, A] = f
    

關鍵字 opaque 和 extension 都是 Scala 3 才有的東西,opaque 可以讓 JVM 減少 boxing 開銷,所以就不會發生以下這種情況,

scala> object Logarithms:
  |   opaque type Logarithm = Double
  |
  |   object Logarithm:
  |     def apply(d: Double): Logarithm = math.log(d)
  |

scala> val log2 = Logarithm(2.0)
scala> val d: Double = log2
-- [E007] Type Mismatch Error:

沒加 opaque 的話,這個 val d: Double = log2 的宣告會因為 boxing 而執行成功;

而 extension 在 Scala 2 可以視為 implicit,就是在不改變原類別定義的情況下,加入擴充功能,

scala> case class Circle(x: Double, y: Double, radius: Double)

scala> extension (c: Circle)
  |   def circumference: Double = c.radius * math.Pi * 2

scala> val aCircle = Circle(2, 3, 5)
scala> aCircle.circumference
val res0: Double = 31.41592653589793

我能直接用變數 aCirlce 來調用 circumference function。

現在我就能用 State 來包裹 RNG,然後用 map function 將 int 轉為 double,

scala> val stateA: State[RNG, Int] = State(_.nextInt)
scala> val rng = SimpleRNG(-1)

scala> val (d, nextRng) = stateA.map(_.toDouble)(rng)
val d: Double = -384749.0
val nextRng: RNG = SimpleRNG(281449761806750)

然後我們就可以依樣畫葫蘆,把一些好用的 function 加到 object State 底下來做各種轉換。

總結

在 functional programming 中改變狀態的方法就是將新的狀態跟著原來要回傳的值綁在一起返回,確保不會發生 side effect,最終產生了 State 型別來包裹所有需要改變狀態的物件,其中的轉換 function 如 map, flatMap 可以組合式的把 State 轉換成下一個 State,雖然寫起來有點麻煩,但適當的使用我相信能在越來越複雜的系統中幫助工程師更好除錯以及完善測試。


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

尚未有邦友留言

立即登入留言