I was looking at an old post of mine on free monads and wondered what it would look like using operational. Here we go!

(Literate Haskell source for this blog post is here.)

First, some imports. We use GADTs for our instruction type.

We want to encode a few instructions that operate on a data store. For the moment we don’t care about the implementation, just the underlying actions that we can perform: create a value, list all values, retrieve a particular value, and delete a value.

Suppose we have an index of type i and values of type v. Then DataStoreInstruction is:

Intuitively, Create takes a value of type v and returns an instruction, which on interpretation gives a value of type i (the last type parameter).

List doesn’t take any argument and produces a list of type v, i.e. [v]. The only odd one is Delete which doesn’t return anything, so it has a () in the last type variable.

A sequence of DataStoreInstructions is a program, which we get using the Program constructor:

type DataStoreProgram i v a = Program (DataStoreInstruction i v) a

where the index has type i, the values have type v, and the overall result of the program has type a.

To more easily construct programs, use singleton:

Now we can write programs in this DSL! All the usual monad things are at our disposal:

The last step is to write an interpreter. We do this using view and pattern matching on the constructors of DataStoreInstruction. We use :»= to break apart the program. As the documentation says, (someInstruction :>>= k) means someInstruction and the remaining program is given by the function k.

Here we interpret the program using a Map as the underlying data store:

If we wanted to we could flatten a program out to a string:

Maybe we have some super-important business need for storing Int/Int key value maps with a Postgresql backend. We could target this by writing an interpreter that uses postgresql-simple:

Running the programs:

*Note> interpretMap doSomeThings M.empty
[4,5]

*Note> interpretString doSomeThings
"Create(3); Create(4); Delete(3); Create(5); List; Return []"

*Note> interpretMap (insertValues [1..10]) M.empty
([1,2,3,4,5,6,7,8,9,10],[])

*Note> interpretString (insertValues [1..10])
"Create(1); Create(2); Create(3); Create(4); Create(5); Create(6);
Create(7); Create(8); Create(9); Create(10); Delete(1); Delete(2);
Delete(3); Delete(4); Delete(5); Delete(6); Delete(7); Delete(8);
Delete(9); Delete(10); List; Return ([1,2,3,4,5,6,7,8,9,10],[])"


### QuickCheck

It’s always good to write some tests:

*Note> quickCheck prop_insert_then_delete
+++ OK, passed 100 tests.

*Note>
*Note>
*Note>
*Note> quickCheck prop_create
+++ OK, passed 100 tests.


Over time we find out that the interpreter that uses Map is too slow, so we write a new one using a fancy data structure:

Now we can compare implementations using QuickCheck. Nice!

## Use of operational in larger projects

Here are a few samples of the operational package in action. For more, see the reverse dependencies on Hackage.

Here is the ConI type from Hadron.Controller:

There is the distinction between the single orchestrator node, which runs OrchIO and can’t run NodeIO, and worker nodes that can’t run OrchIO but can run NodeIO. In the orchestrate, trying to evaluate a NodeIO results in an error:

Meanwhile, worker nodes ignore an OrchIO and lift the NodeIO action.

### Chart

The Graphics.Rendering.Chart.Backend.Impl module defines the the backend instructions:

Then the Graphics.Rendering.Chart.Backend.Cairo module provides a way to run a BackendProgram for the cairo graphics engine:

Meanwhile, Graphics.Rendering.Chart.Backend.Diagrams does the same but for diagrams:

### redis-io

The Data.Redis.Command module defines heaps of commands:

and Database.Redis.IO.Client, an internal module of redis-io, defines how to interpret the redis commands:

### language-puppet

The internal module Puppet.Interpreter.Types of language-puppet defines an interpreter instruction type:

Then Puppet.Interpreter.IO provides:

Since InterpreterInstr is a normal data type, it’s possible to write an instance of Pretty so that warnings or error messages look nicer: