11

私はコンピュータープログラムの構造と解釈に取り組んでおり、Haskell の演習を完了しています。最初の 2 つの章は問題ありませんでしたが (コードは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 でこれをエミュレートする方法がわかりません。私は最初に State モナドを使ったいくつかの単純な関数を考えました:

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。何かアドバイス?

4

2 に答える 2

8

w1Scheme コードはこっそりと状態の 2 つのビットを使用していw2ます。もう 1 つは、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')

これにより、次のことが可能になります。

*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]

もう 1 つのオプションは、状態の両方の部分を明示的にすることです。たとえば、各アカウントを一意のIntID に関連付けます。

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
于 2012-04-06T19:50:32.687 に答える
4

ここには、システム内の「アカウント」ごとに 1 つずつ、独立した変更可能な状態が複数あります。モナドでは、状態を1 つStateだけ持つことができます。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モナドは非常に単純です: s を作成して変更するSTRefだけで、通常の変更可能な変数と同じように機能します。実際、それらのインターフェースは基本的にIORefs のインターフェースと同じです。

唯一のトリッキーなビットは、状態スレッドsと呼ばれる追加の型パラメーターです。これは、それぞれをそれが作成されたコンテキストに関連付けるために使用されます。アクションからを返し、それを別のコンテキストに運ぶことができるとしたら、それは非常に悪いことです。 ofですが、s がエスケープできた場合、すべての操作を! したがって、それぞれとは同じ型パラメーターを持ち、型はです。これにより、特定の値を選択できなくなりますSTRefSTSTRefST STSTIOSTRefrunSTSTSTRefsrunSTrunST :: (forall s. ST s a) -> as: コードは のすべての可能な値で動作する必要がありますs。特定のタイプが割り当てられることはありません。状態スレッドを分離しておくためのトリックとして使用されます。

于 2012-04-06T19:49:09.083 に答える