泛函編程(17)-泛函狀態-State In Action
對OOP編程人員來說,泛函狀態State是一種全新的數據類型。我們在上節做了些介紹,在這節我們討論一下State類型的應用:用一個具體的例子來示範如何使用State類型。以下是這個例子的具體描述:
模擬一個自動糖果販售機邏輯:販售機有兩種操作方法:投入硬幣和扭動出糖旋鈕。販售機可以處於鎖定和放開兩種狀態。模擬運作跟蹤販售機內當前的糖果和硬幣數量。販售機的操作邏輯要求如下:
1、如果機內有糖的話,投入硬幣販售機從鎖定狀態進入放開狀態
2、在放開狀態下扭動旋鈕販售機放出一塊糖果後自動進入鎖定狀態
3、在鎖定狀態下扭動旋鈕販售機不做反應
4、在放開狀態下投入硬幣販售機不做反應
5、沒有糖果的販售機對任何操作都不做反應
我們先把涉及到的數據類型設計出來:
1 type candy = Int //方便表達 2 type coin = Int //方便表達 3 sealed trait Input 4 case object Coin extends Input //投幣 5 case object Turn extends Input //旋鈕 6 case class Machine(locked: Boolean, candies: candy, coins: coin) //狀態類
Machine類型就是需要變遷的狀態,狀態內容包括鎖定狀態locked,當前糖果數candies,當前硬幣數coins。
我們的模擬函數款式如下:
1 def simulateMachine(inputs: List[Input]): State[Machine,coin]
輸入一個操作動作List,返回State實例,值是當前硬幣數,狀態是Machine,裏面包括了完成操作後的鎖定狀態、硬幣數、糖果數。
我們先做一個比較直接容易明白的版本。首先是販售機互動邏輯部分:這部分模擬了操作互動流程並在運行過程中對狀態進行變遷,最終以輸出形式返回最新狀態:
1 def transition(input: Input, machine: Machine): Machine = { 2 (input, machine) match { 3 case (_, Machine(_,0,_)) => machine 4 case (Turn, Machine(true,_,_)) => machine 5 case (Coin, Machine(false,_,_)) => machine 6 case (Coin, Machine(true, _, nCoin)) => machine.copy(locked = false, coins = nCoin + 1) 7 case (Turn, Machine(false, nCandy, _)) => machine.copy(locked = true, candies = nCandy - 1) 8 } 9 }
這個transition函數采用了泛函狀態維護風格:傳入一個狀態;返回新狀態。流程邏輯部分是通過分析操作動作及販售機當前狀態來決定如何更新狀態;一切按照設計方案要求進行。狀態machine是個case class實例,這樣我們可以使用machine.copy來復制一個新的狀態,內容包括locked,candies,coins、又或者在某些情況下,原封不動地返回傳入的狀態。很明顯,transition就是我們需要的狀態行為函數,只要嵌入一個State實例就可以隨時實現狀態變遷了。
transition只能處理單個操作動作,那麽如果我們輸入一個List的操作動作該如何連續處理呢?既然涉及List,自然想到用遞歸算法應該能行:
1 def execute(inputs: List[Input], machine: Machine): Machine = { 2 inputs match { 3 case Nil => machine 4 case h::t => execute(t, transition(h,machine)) 5 6 } 7 }
在execute函數裏我們對List裏每個操作元素進行transition運算,狀態machine也在一連串的transition運算中自動更新了。有了這兩個函數我們就很容易推斷出整體流程了:獲取初始狀態 >>> 以此初始狀態輸入操作處理流程並把最終結果設定為當前狀態 >>> 讀取當前狀態:
1 for { 2 s0 <- getState //讀取起始狀態 3 _ <- setState(execute(inputs,s0)) //人工設定新狀態 4 s1 <- getState //讀取當前狀態 5 } yield s1.coins
整個大流程還是比較容易理解的。我們註意到狀態變遷采用了臨時手工設定方式 setState。整個程序代碼和運行結果示範如下:
1 type candy = Int //方便表達 2 type coin = Int //方便表達 3 sealed trait Input 4 case object Coin extends Input 5 case object Turn extends Input 6 case class Machine(locked: Boolean, candies: candy, coins: coin) 7 8 def simulateMachine(inputs: List[Input]): State[Machine,coin] = { 9 def transition(input: Input, machine: Machine): Machine = { 10 (input, machine) match { 11 case (_, Machine(_,0,_)) => machine 12 case (Turn, Machine(true,_,_)) => machine 13 case (Coin, Machine(false,_,_)) => machine 14 case (Coin, Machine(true, _, nCoin)) => machine.copy(locked = false, coins = nCoin + 1) 15 case (Turn, Machine(false, nCandy, _)) => machine.copy(locked = true, candies = nCandy - 1) 16 } 17 } 18 def execute(inputs: List[Input], machine: Machine): Machine = { 19 inputs match { 20 case Nil => machine 21 case h::t => execute(t, transition(h,machine)) 22 23 } 24 } 25 for { 26 s0 <- getState //讀取起始狀態 27 _ <- setState(execute(inputs,s0)) //人工設定新狀態 28 s1 <- getState //讀取當前狀態 29 } yield s1.coins 30 } //> simulateMachine: (inputs: List[ch6.state.Input])ch6.state.State[ch6.state.M 31 //| achine,ch6.state.coin] 32 33 val inputs = List(Coin, Turn, Coin, Turn, Turn, Coin, Coin, Coin, Turn) 34 //> inputs : List[Product with Serializable with ch6.state.Input] = List(Coin, 35 //| Turn, Coin, Turn, Turn, Coin, Coin, Coin, Turn) 36 simulateMachine(inputs).run(Machine(true,3,0)) //> res0: (ch6.state.coin, ch6.state.Machine) = (3,Machine(true,0,3)) 37
以上這段代碼考慮到了OOP編程人員的思維模式,采用了分段表達方式使整個程序變得更容易理解。對比起來,下面的例子就可以說是真正的泛函編程風格了。同樣針對以上的販售機模擬邏輯要求,我們將用典型的泛函風格來編程。我們先看看下面兩個函數:
1 def modify[S](f: S => S): State[S,Unit] = { 2 for { 3 s0 <- getState 4 _ <- setState(f(s0)) 5 } yield () 6 } 7 def sequence[S,A](xs: List[State[S,A]]): State[S,List[A]] = { 8 def go(s: S, actList: List[State[S,A]], acc: List[A]): (List[A],S) = { 9 actList match { 10 case Nil => (acc.reverse, s) //糾正排序 11 case h::t => h.run(s) match {case (a2,s2) => go(s2,t,a2 :: acc) } 12 } 13 } 14 State(s => go(s,xs,List())) 15 }
modify比較直白:取出當前狀態,對這個狀態進行變遷後再設成當前狀態。sequence稍微復雜一點。我們先從它的類型匹配開始分析:接收一個List[State]、輸出State[List],換句話說就是把一連串的狀態變成一個狀態內的一連串值。這不剛好和我們模擬函數要求匹配嗎?我們要求一個函數對一連串的操作動作進行處理後產生一個最終的狀態。sequence函數內部已經包含了處理循環,我們不需要execute函數了。但是這個版本的sequence函數比較低級:我是指它使用了遞歸算法,必須在函數內部實現狀態行為run(s)。如果我們用高價一點的函數實現sequence,有可能不需要理會run(s)了:
1 //用右折疊:輸入與輸出同排序,但不是tail recursive 2 def sequenceByRight[S,A](xs: List[State[S,A]]): State[S,List[A]] = { 3 xs.foldRight(unit[S,List[A]](List())){ (f,acc) => f.map2(acc)(_ :: _) } 4 } 5 //用左折疊:輸入與輸出反排序,是tail recursive 6 def sequenceByLeft[S,A](l: List[State[S, A]]): State[S, List[A]] = { 7 l.reverse.foldLeft(unit[S, List[A]](List()))((acc, f) => f.map2(acc)( _ :: _ )) 8 }
無論用左右折疊算法都可以實現sequence功能。註意:我們沒有使用run(s),因為這個東西是在flatMap裏,而map2是用flatMap實現的。用這種高階函數使程序更加簡潔。
1 def simulateMachineFP(inputs: List[Input]): State[Machine,coin] = { 2 for { 3 _ <- sequence{ 4 inputs.map { 5 input => modify { 6 machine: Machine => { 7 (input, machine) match { 8 case (_, Machine(_,0,_)) => machine 9 case (Turn, Machine(true,_,_)) => machine 10 case (Coin, Machine(false,_,_)) => machine 11 case (Coin, Machine(true, nCandy, nCoin)) => Machine(false,nCandy, nCoin+1) 12 case (Turn, Machine(false, nCandy, nCoin)) => Machine(true, nCandy - 1, nCoin) 13 } 14 } 15 } 16 } 17 } 18 s <- getState 19 } yield s.coins 20 } //> simulateMachineFP: (inputs: List[ch6.state.Input])ch6.state.State[ch6.state 21 //| .Machine,ch6.state.coin] 22 simulateMachineFP(inputs).run(Machine(true,3,0)) //> res1: (ch6.state.coin, ch6.state.Machine) = (3,Machine(true,0,3))
哇,有點過了!不過這裏的確沒有分段編碼,一口氣用sequence完成了編程。那我們還是來分析一下:sequence需要接收一個參數類型是:List[State[S,A]] 我們有一個List[Input],需要把這個List[Input] 變成 List[State[S,A]],很明顯,我們需要用map來做這個轉換 List[Input].map{ Input => State[S,A]}。modify返回類型State[S,Unit],所以我們用了input => modify,這在類型上是匹配的。modify,顧名思義就是更新狀態,我們把狀態變遷邏輯都放到了modify函數裏,它的返回結果就是最終的狀態。
_ <- sequence ...modify 起到了 _ <- setState 的作用,所以我們可以用 s <- getState 把最新的狀態讀出來。
在以上這個例子裏我們采用了泛函編程風格:用類型匹配方式進行了函數組合,雖然說代碼可能簡單了,但清潔可能就說不上了。需要用類型匹配(type line-up)來分析理解,也就是要再熟悉多點泛函編程思考模式。
後面補充一下:如果我來選擇,我會稍退一步;把邏輯部分提出來:
1 def simulateMachineConcise(inputs: List[Input]): State[Machine,coin] = { 2 def transition(input: Input, machine: Machine): Machine = { 3 (input, machine) match { 4 case (_, Machine(_,0,_)) => machine 5 case (Turn, Machine(true,_,_)) => machine 6 case (Coin, Machine(false,_,_)) => machine 7 case (Coin, Machine(true, nCandy, nCoin)) => Machine(false,nCandy, nCoin+1) 8 case (Turn, Machine(false, nCandy, nCoin)) => Machine(true, nCandy - 1, nCoin) 9 } 10 } 11 12 for { 13 _ <- sequence{inputs.map {input => modify {machine: Machine => transition(input, machine)}}} s <- getState 15 } yield s.coins 16 } //> simulateMachineConcise: (inputs: List[ch6.state.Input])ch6.state.State[ch6. 17 //| state.Machine,ch6.state.coin] 18 simulateMachineConcise(inputs).run(Machine(true,3,0)) 19 //> res2: (ch6.state.coin, ch6.state.Machine) = (3,Machine(true,0,3)) 20
這段核心代碼是不是簡潔多了,也比較容易理解:
1 for { 2 _ <- sequence{inputs.map {input => modify {machine: Machine => transition(input, machine)}}} s <- getState 3 } yield s.coins
泛函編程(17)-泛函狀態-State In Action