This post has a minimal stand-alone example of the classy lenses and prisms from George Wilson’s talk about mtl. The source code for George’s talk is here: https://github.com/gwils/next-level-mtl-with-classy-optics.
Literate Haskell source for this post is here: https://github.com/carlohamalainen/playground/tree/master/haskell/classy-mtl.
Toy program - uses the network and a database
The case study in George’s talk was a program that has to interact with a database and the network. We have a type for the database connection info:
For the network we have a port and some kind of SSL setting:
At the top level, our application has a database and a network configuration:
Types for errors that we see when dealing with the database and the network:
Classy lenses and prisms
Use Template Haskell to make all of the classy lenses and prisms. Documentation for
makeClassyPrisms is in Control.Lens.TH.
makeClassy ''DbConfig makeClassy ''NetworkConfig makeClassy ''AppConfig makeClassyPrisms ''DbError makeClassyPrisms ''NetworkError makeClassyPrisms ''AppError
We get the following typeclasses:
For example, here is the generated class
*Classy> :i HasDbConfig class HasDbConfig c_a6IY where dbConfig :: Functor f => (DbConfig -> f DbConfig) -> c0 -> f c0 dbConn :: Functor f => (DbConnection -> f DbConnection) -> c0 -> f c0 schema :: Functor f => (DbSchema -> f DbSchema) -> c0 -> f c0 instance HasDbConfig DbConfig -- Defined at Classy.lhs:58:3
If we write
HasDbConfig r in the class constraints of a type signature then
we can use the lenses
schema to get the entire config, connection string, and schema,
from something of type
In contrast, the constraint
AsNetworkError r means that we can
use the prisms
on a value of type
r to get at the network error details.
*Classy> :i AsNetworkError class AsNetworkError r_a759 where _NetworkError :: (Choice p, Control.Applicative.Applicative f) => p NetworkError (f NetworkError) -> p r0 (f r0) _Timeout :: (Choice p, Control.Applicative.Applicative f) => p Int (f Int) -> p r0 (f r0) _ServerOnFire :: (Choice p, Control.Applicative.Applicative f) => p () (f ()) -> p r0 (f r0) -- Defined at Classy.lhs:63:3 instance AsNetworkError NetworkError -- Defined at Classy.lhs:63:3
Using the class constraints
The first function is
loadFromDb which uses a reader environment for database
configuration, can throw a database error, and do IO actions.
sendOverNet uses a reader environment with a network config,
throws network errors, and does IO actions.
If we load from the database and also send over the network then we get extra class constraints:
Things that won’t compile
We can’t throw the database error
without the right class constraint:
Could not deduce (AsDbError e) arising from a use of ‘_InvalidConnection’
We can’t throw an application error if we are only allowed to throw network errors, even though this specific application error is a network error:
Could not deduce (AsAppError e) arising from a use of ‘_AppNetError’
We can’t get the network config from a value of type
r if we only have
the constraint about having the database config:
Could not deduce (HasNetworkConfig r) arising from a use of ‘networkConfig’
What is the #?
*Classy> :t review _InvalidConnection () review _InvalidConnection () :: AsDbError e => e *Classy> :t throwError $ review _InvalidConnection () throwError $ review _InvalidConnection () :: (AsDbError e, MonadError e m) => m a
What is the monad transformer stack?
We didn’t specify it! The functions
sendOverNet have the general monad
in their type signatures, not a specific transformer stack like
ReaderT AppConfig (ExceptT AppError IO) a.
Ben Kolera did a
talk at BFPG about stacking monad transformers.
He later modified the code from his talk to use the
classy lens/prism approach. You can see the code before
and after, and also see a diff. As far as I could see
there is one spot in the code where an error is thrown, which motivated me to create the stand-alone example in this post with the body for
sendOverNet sketched out.