This is a bit of a design question.
I currently have an "App" class which is a monad transformer stack, and what I want to add to that stack is a mutable Key/Value store.
Exactly how that store is implemented I want to be able to easily change. Initially I want it to be just an in memory Map, but later on I'll probably change it to be some database based thing.
The first thing that jumped to mind was just to add a StateT to my monad transformer stack. That works fine for an in memory Map, but I don't think this will work for a database implementation, as I don't want to get/update the entire state each time, but just one key/value combination.
The second thought was writing my own monad transformer. But when I looked at existing transformers, they all have classes with instances with all other monad transformers so they work with others. I think adding my own monad transformer and allowing it to interact nicely with all other transformers would involve writing n (or maybe 2 * n instances actually) so it can poke through other transformers and other transformers can poke through it. Technically I'll only have to do this with the transformers I'm using, but that seems a bit hacky. There's probably another argument here about whether monad transformer stack is a good idea at all, but I'd rather not completely change my App architecture at this point.
So my third idea was to have a ReaderT with a record of functions (or maybe a record with a typeclass constraint like data Blah where Blah :: c => Blah) that I can replace. This I think I could get to work, but I'm not sure it's the best approach, I'd have to layer a ReaderT on top of a StateT for the in memory approach I think.
As a I guess fourth approach, I did write out a bunch of typeclasses like so:
data HadKey = KeyFound | KeyNotFound
class Monad m => LookupMapM key value m where
lookupM :: key -> m (Maybe value)
class LookupMapM key value m => InsertMapM key value m where
insertM :: key -> value -> m HadKey
class InsertMapM key value m => UpdateMapM key value m where
updateM :: key -> value -> m HadKey
Which I then started implementing interfaces, like so:
instance LookupMapM key value (StateT (Map key value) m) where ...
instance (...) => LookupMapM key value IO where ...
This seemed promising, but then the second instance seems problematic... like, I can only have one IO implementation (presumably different databases should be able to have their own implementation).
I guess I could work around the above with the package tagged identity, so I could tag different implementations.
I note I'll then still need to lift my "map" transformer if it's not at the top of the stack, but that's not a big issue, I could just maintain a liftKV function that just needs an extra lift every time a layer is added to the monad transformer stack, and I don't think that's a big deal.
But maybe there's a better way of doing this than the ways I've suggested? Or maybe one of the ways I've suggested is the best way, I'm just not sure which one.
By other thought here was instead of having:
class Monad m => LookupMapM key value m where
lookupM :: key -> m (Maybe value)
going with this style:
class Monad m => LookupMapM t m where
type Key t
type Value t
lookupM :: Key t -> m (Maybe (Value t))
But I'm not sure if this solves anything or just adds extra complication for no benefit.
Any thoughts?
You are going to have to put
IOat the bottom of your stack. Above that you will need aReaderTwith the database connection information and functions to access the database through it. Then put whatever else you need on top of that.For an example of this kind of thing, see the Render monad in the Cairo library. It's storing a render context instead of a database connection, but the concept is the same.