In Haskell, there are many ways to compute components, which are their separate responsibilities. This can be done at the data level with data types and functions ( http://www.haskellforall.com/2012/05/scrap-your-type-classes.html ) or using type classes. In Haskell, you can view each data type, type, function, signature, class, etc. As an interface; if you have something else of the same type, you can replace the component with something compatible.
When we want to talk about calculations in Haskell, we often use an abstraction of a Monad . A Monad is an interface for building computations. A basic calculation can be built using return , and they can be composed along with functions that perform other calculations using >>= . When we want to add a few responsibilities to the calculations presented by monads, we make monad transformers. In the code below, there are four different monad transformers that capture various aspects of a tiered system:
DatabaseT s adds a database with a schema of type s . It processes Operation data, storing data or retrieving it from the database. CacheT s intercepts Operation data for schema s and retrieves data from memory, if available. OpperationLoggerT writes Operation to standard output ResultLoggerT writes Operation results to standard output
These four components are combined together using a class (interface) of type MonadOperation s , which requires that the components implementing it provide a way to perform a Operation and return its result.
The same type describes what is required to use the MonadOperation s system. This requires that someone using the interface provide an implementation of the type classes on which the database and cache will be based. There are also two types of data that are part of this interface, Operation and CRUD . Please note that the interface should not know anything about domain objects or the database schema, and also should not know about the different monad transformers that will implement it. Monad transformers do not know anything about circuit or domain objects, and domain objects and sample code do not know anything about monad transformers that build the system.
The only thing the example code knows is access to MonadOperation s due to its type example :: (MonadOperation TableName m) => m () .
The main program runs the example twice in two different contexts. The first time the program talks to the database, its Operations and responses are recorded in the standard version.
Running example program once with an empty database Operation Articles (Create (Article {title = "My first article", author = "Cirdec", contents = "Lorem ipsum dolor sit amet."})) ArticleId 0 Operation Articles (Read (ArticleId 0)) Just (Article {title = "My first article", author = "Cirdec", contents = "Lorem ipsum dolor sit amet."}) Operation Articles (Read (ArticleId 0)) Just (Article {title = "My first article", author = "Cirdec", contents = "Lorem ipsum dolor sit amet."})
The second run registers the responses received by the program, passes Operation through the cache, and logs the requests before they reach the database. Due to the new caching transparent to the program, requests for reading an article never occur, but the program still gets a response:
Running example program once with an empty cache and an empty database Operation Articles (Create (Article {title = "My first article", author = "Cirdec", contents = "Lorem ipsum dolor sit amet."})) ArticleId 0 Just (Article {title = "My first article", author = "Cirdec", contents = "Lorem ipsum dolor sit amet."}) Just (Article {title = "My first article", author = "Cirdec", contents = "Lorem ipsum dolor sit amet."})
Here is the whole source code. You should think of it as four independent pieces of code: a program written for our domain, starting with example . An application, which is a complete assembly of the program, the discourse area and various tools that create it, starting with main . The following two sections, ending with the TableName schema, describe the blog post domain; their sole purpose is to illustrate how other components are combined, rather than serve as an example of designing data structures in Haskell. The following section describes a small interface through which components can communicate with data; this is not necessarily a good interface. Finally, the remainder of the source code implements the logs, database, and caches that are put together to form the application. To separate the tools and the interface from the domain, there are several disgusting tricks with the type and dynamics here, this does not mean that you can demonstrate a good way to handle castings and generics.
{-
To create this example, you will need the mtl and containers libraries.