Haskell: Yesod and state

2019-07-02 01:06发布

问题:

I was reading through the code for a Toy URL Shortener. However, there's significant parts I just can't get my head around.

It has the following code:

data URLShort = URLShort { state :: AcidState URLStore }

For testing purposes, I wrote something like this in my own app:

data MyApp = MyApp { state :: Int }

I could then compile by changing

main = warpDebug 3000 MyApp

to

main = warpDebug 3000 (MyApp 42)

And I could then do reads of the state in handlers by doing

mystate <- fmap state getYesod 

inspired by acid <- fmap state getYesod in the article. However, I didn't know how to do writes.

I have also tried doing:

data MyApp = MyApp { state :: State Int Int }

But I didn't get far with this.

I was trying to work out how AcidState works just by doing some simple similar examples, figuring that since AcidState keeps everything in memory, I should be able to do the same?

Any sort of general explanation of what is going on here, and also perhaps how I'm missing the point would be very appreciated.

回答1:

The AcidState a data type is not an immutable value all the way down; it contains references to mutable data internally. What is stored in Yesod-land in this case is simply an immutable reference to this data. When you update the state, you don't actually update the value in the foundation data type, but instead the memory that it points to.

Every value in the Haskell world is immutable. However, a lot of things outside of the Realm of Haskell isn't immutable; for example, when you do putStrLn, the terminal mutates its display to show new content. The putStrLn action itself is an immutable pure value, but it describes how to perform an action involving mutation.

There are other functions that also yield actions that perform mutations; if you do ref <- newIORef 0, you get a value describing an action that creates a mutable memory cell. If you then do modifyIORef ref (+1), you get a value describing an action that increments the value in that cell by 1. The ref value is a pure value, it's simply a reference to a mutable cell. The code is also purely functional, because each piece only describes an action; nothing is mutable within the Haskell program.

This is how AcidState implements its state: by using a system that manages state outside of the Haskell world. This is not "as bad as" having full mutability like in languages such as C, because in Haskell, you can control the mutability with the power of monads. Using AcidState is perfectly safe and does not involve the use of unsafePerformIO as far as I know.

With AcidState in this case, you use openAcidState emptyStore in the IO monad to create a new acid state (that line is a value describing an IO action that opens a new acid state). You use createCheckpointAndClose to optionally save the acid state to disk safely. Finally, you use the update' function to mutate the contents of an acid state.

To create a "small state" yourself using IORefs (The simplest form of mutable state, except for maybe the ST monad), you first add a field like this to your foundation data type:

data VisitorCounter = VisitorCounter { visitorCounter :: IORef Int }

You then do:

main = do
  counter <- newIORef 0
  warpDebug 3000 (VisitorCounter counter)

In a handler, you can modify the counter like this:

counter <- fmap visitorCounter getYesod
modifyIORef counter (+1)
count <- readIORef counter
-- ... display the count or something

Note the symmetry to AcidState.

For a site counter, I would actually recommend using TVars instead of IORefs, because of the possibility that multiple clients might modify the variable simultaneously. The interface to TVars is very similar, however.


Follow up question from question author?

I've placed { visitorCounter :: TVar Int } in my foundation type, and the following code in the handler:

counter <- fmap visitorCounter getYesod
count <- readTVar counter

The first line compiles fine, but the second throws this error:

Couldn't match expected type `GHandler sub0 Middleware t0'
            with actual type `STM a0'
In the return type of a call of `readTVar'
In a stmt of a 'do' expression: count <- readTVar counter

How could I fix this?