3 ms·
Here's a chunk of code from a haskell project I work on that shows how concise and nice haskell can be: class EntityStore b m => TagStore b m where
by hardwaresofton 7y ago
Here's a chunk of code from a haskell project I work on that shows how concise and nice haskell can be:
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 functions
All 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).
[0]: https://www.youtube.com/watch?v=jSpio0x7024 https://www.youtube.com/watch?v=jSpio0x7024