class EntityStore b m => TagStore b m where
-- | Add a new tag
addTag :: b -> Tag -> m (Either DBError (ModelWithID Tag))
-- | Get all tags
getAllTags :: b -> Maybe Limit -> Maybe Offset -> m (Either DBError (PaginatedList (ModelWithID Tag)))
-- | Find tags by a given ID
findTagsByIDs :: b -> [TagID] -> m (Either DBError (PaginatedList (ModelWithID Tag)))
That bit at the start (`EntityStore b m => TagStore b m`) roughly reads "Given that types 'b' and 'm' exist that satisfy the typeclass 'EntityStore', they satisfy the typeclass 'TagStore' if they implement the following methods".If your eyes glazed reading the sentence above -- this is a way of composing interfaces. Any TagStore-capable thing is required to be EntityStore-capable.
And here's that being used:
createTag :: ( HasDBBackend m db
, HasCacheBackend m c
, MonadError ServantErr m
, TagStore db m
) => SessionInfo -> Tag -> m (EnvelopedResponse (ModelWithID Tag))
createTag s t = requireRole Administrator s
>> validateEntity t
>> getDBBackend
>>= \db -> getCacheBackend
>>= \cache -> addTag db t
>>= ifLeftEnvelopeAndThrow Err.failedToCreateEntity
-- invalidate cached tag listing & FTS results
>>= \res -> invalidateTagListing cache
>> invalidateTagFTSResults cache
>> pure (EnvelopedResponse "success" "Successfully created new tag" res)
I prefer the explicit 'bind' syntax (>>/>>= are pronounced 'bind') to do notation, because of the clarity you get when the code is laid out, and how it encourages modular functionsAll those `HasDBBackend m db` and `TagStore db m` incantations mean that this function knows about the world is that it has a DB backend, a cache backend, a tag store, and it knows something about the kind of errors it can throw (`ServantErr`). Then come what the actual function does (after the =>) -- Take in a SessionInfo, a Tag, and produce an "action" that when run will produce a EnvelopedResponse (ModelWithID Tag) value.
This style is called mtl and it's just one of the big patterns in the haskell community, but the big feature here is that isolation -- this function can only do things that it knows about by way of constraints (`TagStore db m` means a `TagStore`-capable thing is accessible to you) -- this is much safer than having functions that can just do anything at any time.
I've said it numerous other times, but the kind of stuff you do in haskell trickles into other languages -- see Florian Gilcher's talk from RustLatam 2019[0] -- it's basically on this same concept but in Rust (one of the reasons I absolutely love rust, they've bolted on a fantastic type system).