管理国家 - SICP第3章

Chr*_*lor 11 state haskell sicp state-monad

我一直在研究计算机程序的结构和解释,并完成Haskell的练习.前两章很好(github上的代码),但第3章让我更难思考.

首先是谈论管理国家,以银行账户为例.他们定义一个函数make-withdraw通过

(define (make-withdraw balance)
    (lambda (amount)
        (if (>= balance amount)
            (begin (set! balance (- balance amount))
                balance)
            "Insufficient funds")))
Run Code Online (Sandbox Code Playgroud)

这样您就可以执行以下代码:

(define w1 (make-withdraw 100))
(define w2 (make-withdraw 100))

(w1 50)
50

(w2 70)
30

(w2 40)
"Insufficient funds"

(w1 40)
10
Run Code Online (Sandbox Code Playgroud)

我不确定如何在Haskell中模仿这个.我首先想到了一个使用State monad的简单函数:

import Control.Monad.State

type Cash    = Float
type Account = State Cash

withdraw :: Cash -> Account (Either String Cash)
withdraw amount = state makewithdrawal where
    makewithdrawal balance = if balance >= amount
        then (Right amount, balance - amount)
        else (Left "Insufficient funds", balance)
Run Code Online (Sandbox Code Playgroud)

这允许我运行代码

ghci> runState (do { withdraw 50; withdraw 40 }) 100
(Left "Insufficient funds",30.0)
Run Code Online (Sandbox Code Playgroud)

但这与计划代码有所不同.理想情况下,我可以运行类似的东西

do
  w1 <- makeWithdraw 100
  w2 <- makeWithdraw 100
  x1 <- w1 50
  y1 <- w2 70
  y2 <- w2 40
  x2 <- w1 40
  return [x1,y1,y2,x2]

[Right 50,Right 70,Left "Insufficient funds",Right 40]
Run Code Online (Sandbox Code Playgroud)

但我不知道如何写这个功能makeWithdraw.有什么建议?

Dan*_*ner 8

Scheme代码偷偷地使用两位状态:一个是变量w1w2ref-cell 之间的(隐式)关联; 另一个是存储在ref-cell中的(显式)状态.在Haskell中有几种不同的方法可以对其进行建模.例如,我们可能会采用类似的ref-cell技巧ST:

makeWithdraw :: Float -> ST s (Float -> ST s (Either String Float))
makeWithdraw initialBalance = do
    refBalance <- newSTRef initialBalance
    return $ \amount -> do
        balance <- readSTRef refBalance
        let balance' = balance - amount
        if balance' < 0
            then return (Left "insufficient funds")
            else writeSTRef refBalance balance' >> return (Right balance')
Run Code Online (Sandbox Code Playgroud)

这让我们这样做:

*Main> :{
*Main| runST $ do
*Main|   w1 <- makeWithdraw 100
*Main|   w2 <- makeWithdraw 100
*Main|   x1 <- w1 50
*Main|   y1 <- w2 70
*Main|   y2 <- w2 40
*Main|   x2 <- w1 40
*Main|   return [x1,y1,y2,x2]
*Main| :}
[Right 50.0,Right 30.0,Left "insufficient funds",Right 10.0]
Run Code Online (Sandbox Code Playgroud)

另一种选择是使状态的两个部分明确,例如通过将每个帐户与唯一IntID 相关联.

type AccountNumber = Int
type Balance = Float
data BankState = BankState
    { nextAccountNumber :: AccountNumber
    , accountBalance :: Map AccountNumber Balance
    }
Run Code Online (Sandbox Code Playgroud)

当然,我们基本上会重新实现ref-cell操作:

newAccount :: Balance -> State BankState AccountNumber
newAccount balance = do
    next <- gets nextAccountNumber
    modify $ \bs -> bs
        { nextAccountNumber = next + 1
        , accountBalance = insert next balance (accountBalance bs)
        }
    return next

withdraw :: Account -> Balance -> State BankState (Either String Balance)
withdraw account amount = do
    balance <- gets (fromMaybe 0 . lookup account . accountBalance)
    let balance' = balance - amount
    if balance' < 0
        then return (Left "insufficient funds")
        else modify (\bs -> bs { accountBalance = insert account balance' (accountBalance bs) }) >> return (Right balance')
Run Code Online (Sandbox Code Playgroud)

那会让我们写makeWithdraw:

makeWithDraw :: Balance -> State BankState (Balance -> State BankState (Either String Balance))
makeWithdraw balance = withdraw <$> newAccount balance
Run Code Online (Sandbox Code Playgroud)