Our first example is a very simple hit counter app.
First a bunch of LANGUAGE pragmas and imports:
Next we define a type that we wish to store in our state. In this case we just create a simple record with a single field count:
deriveSafeCopy creates an instance of the SafeCopy class for CounterState. SafeCopy is class for versioned serialization, deserilization, and migration. Since this is the first version of the CounterState type, we give it version number 0 and declare it to be the base type. Later if we change the type, we can change the version number and provide code to migrate the old instances. The migration will happen automatically when the old state is read. For more information on SafeCopy and migration see the haddock docs.
Next we will define an initial value that is suitable for initializing the CounterState state.
Now that we have our types, we can define some update and query functions.
First let's define an update function which increments the count and returns the incremented value:
In this line:
we are using the RecordWildCards extension. The {..} binds all the fields of the record to symbols with the same name. That is why in the next line we can just write count instead of (count c). Using RecordWildCards here is completely optional, but tends to make the code less cluttered, and easier to read.
Also notice that we are using the get and put functions from MonadState to get and put the ACID state. The Update monad is basically an enchanced version of the State monad. For the moment it is perhaps easiest to just pretend that incCountBy has the type signature:
And then it becomes clearer that incCountBy is just a simple function in the State monad which updates CounterState and returns an Integer.
When the incCountBy function is invoked, it will be run in an isolated manner (the 'I' in ACID). That means that you do not need to worry about some other thread modifying the CounterState between the get and the put. It will also be run atomically (the 'A' in ACID), meaning that either the whole function will run, it will not run at all. If the server is killed mid-transaction, the transaction will either be completely applied or not applied at all.
You may also note that Update (and State) are not instances of the MonadIO class. This means you can not perform IO inside the update. This is by design. In order to ensure Durability and to support replication, events need to be pure. That allows us to be confident that if the event log has to be replayed -- it will result in the same state we had before.
We can also define a query which reads the state, and does not update it:
The Query monad is an enhanced version of the Reader monad. So we can pretend that peekCount has the type:
Although we could have just used get in the Update monad, it is better to use the Query monad if you are doing a read-only operation because it will not block other database transactions. It also lets the user calling the function know that the database will not be affected.
Next we have to turn the update and query functions into acid-state events. This is almost always done by using the template haskell function makeAcidic
The makeAcidic function creates a bunch of boilerplate types and type class instances. If you want to see what is happening under the hood, check out the examples here. The examples with names like, HelloWorldNoTH.hs show how to implement the boilerplate by hand. In practice, you will probably never want to or need to do this. But you may find it useful to have a basic understanding of what is happening. You could also use the -ddump-splices flag to ghc to see the auto-generated instances -- but the lack of formatting makes it difficult to read.
Here we actually call our query and update functions:
Note that we do not call the incCountBy and peekCount functions directly. Instead we invoke them using the update' and query' functions:
Thanks to makeAcidic, the functions that we originally defined now have types with the same name, but starting with an uppercase letter:
The arguments to the constructors are the same as the arguments to the original function.
So now we can decipher the meaning of the type for the update' and query' functions. For example, in this code:
The event is (IncCountBy 1) which has the type IncCountBy. Since there is an UpdateEvent IncCountBy instance, we can use this event with the update' function. That gives us:
EventState is a type function. EventState IncCountBy results in the type CounterState. So that reduces to AcidState CounterState. So, we see that we can not accidently call the IncCountBy event against an acid state handle of the wrong type.
EventResult is also a type function. EventResult IncCountBy is Integer, as we would expect from the type signature for IncCountBy.
Finally, we have our main function:
openLocalState starts up acid-state and returns an handle. If existing state is found on the disk, it will be automatically restored and used. If no pre-existing state is found, then initialCounterState will be used.
The shutdown sequence creates a checkpoint when the server exits. This is good practice because it helps the server start faster, and makes migration go more smoothly. Calling createCheckpointAndClose is not critical to data integrity. If the server crashes unexpectedly, it will replay all the logged transactions (Durability). However, it is a good idea to create a checkpoint on close. If you change an existing update event, and then tried to replay old versions of the event, things would probably end poorly. However, restoring from a checkpoint does not require the old events to be replayed. Hence, always creating a checkpoint on shutdown makes it easier to upgrade the server.