In bigger applications there are very often multiple layers of IO caching (Hibernate L1 and L2, Spring cache etc.) which usually are abstracted so that caller needs not to be aware that particular implementation does IO. With some caveats (scope, transactions), it allows for simpler interfaces between components.
For example, if component A needs to query database, it needs not to know whether result is already cached. It might have been retrieved by B or C which A knows nothing about, however they would usually participate in some session or transaction - often implicitly.
Frameworks tend to make this call indistinguishable from simple object method call using techniques like AOP.
Is it possible for Haskell applications to benefit like this? How would client's interface look like?
In Haskell there are many ways to compose computations from components that represent 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 every data type, type, function, signature, class, etc as an interface; as long as you have something else of the same type, you can replace a component with something that's compatible.
When we want to reason about computations in Haskell we frequently use the abstraction of a
Monad
. AMonad
is an interface for constructing computations. A base computation can be constructed withreturn
and these can be composed together with functions that produce other computations with>>=
. When we want to add multiple responsibilities to computations represented by monads, we make monad transformers. In the code below, there are four different monad transformers that capture different aspects of a layered system:DatabaseT s
adds a database with a schema of types
. It handles dataOperation
s by storing data in or retrieving it from the database.CacheT s
intercepts dataOperation
s for a schemas
and retrieves data from memory, if it is available.OpperationLoggerT
logs theOperation
s to standard outputResultLoggerT
logs the results ofOperation
s to standard outputThese four components communicate together using a type class (interface) called
MonadOperation s
, which requires that components that implement it provide a way toperform
anOperation
and return its result.This same type class described what is required to use the
MonadOperation s
system. It requires that someone using the interface provide implementations of type classes that the database and cache will rely on. There are also two data types that are part of this interface,Operation
andCRUD
. Notice that the interface doesn't need to know anything about the domain objects or database schema, nor does it need to know about the different monad transformers that will implement it. The monad transformers don't know anything about the schema or domain objects, and the domain objects and example code don't know anything about the monad transformers that build the system.The only thing the example code knows is that it will have access to a
MonadOperation s
due to its typeexample :: (MonadOperation TableName m) => m ()
.The program
main
runs the example twice in two different contexts. The first time, the program talks to the database, with itsOperations
and responses being logged to standard out.The second run logs the responses the program receives, passes
Operation
s through the cache, and logs the requests before they reach the database. Due to the new caching, which is transparent to the program, the requests to read the article never happen, but the program still receives a response:Here's the entire source code. You should think of it as four independent pieces of code: A program written for our domain, starting at
example
. An application that is the complete assembly of the program, the domain of discourse, and the various tools that build it, starting atmain
. The next two sections, ending with the schemaTableName
, describe a domain of blog posts; their only purpose is to illustrate how the other components go together, not to serve as an example for how to design data structures in Haskell. The next section describes a small interface by which components could communicate about data; it's not necessarily a good interface. Finally, the remainder of the source code implements the loggers, database, and caches that are composed together to form the application. In order to decouple the tools and interface from the domain, there are some somewhat hideous tricks with typeable and dynamics in here, this isn't meant to demonstrate a good way to handle casting and generics either.To build this example, you'll need the
mtl
andcontainers
libraries.In Haskell, you do need to (and want to!) be aware of anything that does IO.
That is one of the strong points about it.
You can use the
MonadIO
type class to write functions that work in any monad that is allowed to perform IO actions:As many programming interfaces in Haskell are expressed via monads, functions like this might work in more contexts.
You can also use
unsafePerformIO
to secretly run IO actions from pure code - however this is not advisable in almost all cases. Being pure allows you to immediately see whether side effects are used or not.IO caching is a side effect, and you are well off if your types reflect that.