Управление состоянием - глава 3 SICP
Я работал в Структура и Интерпретация компьютерных программ и завершал упражнения в Haskell. Первые две главы были в порядке (код github), но глава 3 заставляет меня задуматься.
Начнется разговор об управлении состоянием на примере банковского счета. Они определяют функцию make-withdraw
на
(define (make-withdraw balance)
(lambda (amount)
(if (>= balance amount)
(begin (set! balance (- balance amount))
balance)
"Insufficient funds")))
чтобы вы могли выполнить следующий код:
(define w1 (make-withdraw 100))
(define w2 (make-withdraw 100))
(w1 50)
50
(w2 70)
30
(w2 40)
"Insufficient funds"
(w1 40)
10
Я не уверен, как я могу подражать этому в Haskell. Сначала я подумал о некоторой простой функции, используя государственную монаду:
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)
который позволяет мне запускать код
ghci> runState (do { withdraw 50; withdraw 40 }) 100
(Left "Insufficient funds",30.0)
но это делает что-то отличное от кода схемы. В идеале я мог бы запустить что-то вроде
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]
но я не уверен, как написать функцию makeWithdraw
. Любые советы?
Ответы
Ответ 1
Код схемы скрытно использует два бита состояния: one - (неявная) связь между переменными w1
и w2
и ref-cell; другое - это (явное) состояние, сохраненное в ref-cell. Там несколько разных способов моделирования этого в Haskell. Например, мы могли бы вытащить аналогичный рефлекторный трюк с помощью 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')
Что позволяет нам сделать это:
*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]
Другой вариант - сделать обе части состояния явными, например, связав каждую учетную запись с уникальным идентификатором Int
.
type AccountNumber = Int
type Balance = Float
data BankState = BankState
{ nextAccountNumber :: AccountNumber
, accountBalance :: Map AccountNumber Balance
}
Конечно, в этом случае мы бы в основном перепроектировали операции 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')
Что бы тогда написать makeWithdraw
:
makeWithDraw :: Balance -> State BankState (Balance -> State BankState (Either String Balance))
makeWithdraw balance = withdraw <$> newAccount balance
Ответ 2
Ну, у вас есть несколько частей независимого, изменяемого состояния здесь: по одному для каждой "учетной записи" в системе. Монада State
позволяет вам иметь только одну часть состояния. Вы можете хранить что-то вроде (Int, Map Int Cash)
в состоянии, увеличивая Int
, чтобы каждый раз получать новый ключ на карте, и использовать это для хранения баланса... но это так уродливо, не так ли?
К счастью, Haskell имеет монаду для нескольких частей независимого, изменяемого состояния: ST
.
type Account = ST
makeWithdraw :: Cash -> Account s (Cash -> Account s (Either String Cash))
makeWithdraw amount = do
cash <- newSTRef amount
return withdraw
where
withdraw balance
| balance >= amount = do
modifySTRef cash (subtract amount)
return $ Right amount
| otherwise = return $ Left "Insufficient funds"
При этом ваш пример кода должен работать нормально; просто примените runST
, и вы должны получить нужный вам список. Монада ST
довольно проста: вы можете просто создать и изменить STRef
s, которые действуют точно так же, как и обычные изменяемые переменные; на самом деле их интерфейс в основном идентичен интерфейсу IORef
s.
Единственный сложный бит - это дополнительный параметр типа s
, называемый потоком состояния. Это используется для связывания каждого STRef
с созданным им контекстом ST
. Было бы очень плохо, если бы вы могли вернуть STRef
из действия ST
и перенести его в другой контекст ST
вся точка ST
заключается в том, что вы можете запускать ее как чистый код вне IO
, но если STRef
может уйти, у вас будет нечистое, изменяемое состояние вне монадического контекста, просто обернув все ваши операции в runST
! Таким образом, каждый ST
и STRef
несет один и тот же параметр типа s
, а runST
имеет тип runST :: (forall s. ST s a) -> a
. Это останавливает выбор какого-либо определенного значения для s
: ваш код должен работать со всеми возможными значениями s
. Он никогда не назначал какого-либо определенного типа; просто используется как трюк, чтобы поддерживать изолированный поток состояний.