Can i haz? Part 3: extending the Has pattern
Once we scrapped the boilerplate for
Has pattern, the next obvious question is if we can
further. And, turns out, we can!
In this post we’ll see how some algebraic considerations help us to discover one more pattern
MonadError (and a
Generic implementation thereof),
and we’ll also update our
Has class with one more method that brings it closer to
something lens-like and makes it useful with writable environments like
Inverting the arrows
Given some structure, a true algebraist (which I’m not by any means) always wonders what happens if one turns the arrows in that struсture, obtaining a categorical dual.
In this particular case we have a single arrow
extract in the structure
class Has part record where extract :: record -> part
Let’s invert it and prepend
class CoHas copart corecord where coextract :: copart -> corecord
You might already have an intuition about what
CoHas might be,
but let’s proceed turning the arrows based on pure formalism for now.
Combine are boring and have no good applicable structure,
so we leave them as is.
Search, on the other hand, is more interesting.
Recall the definition we used when we built the generic implementation for
type family Search part (grecord :: k -> *) :: MaybePath where Search part (K1 _ part) = 'Found 'Here Search part (K1 _ other) = 'NotFound Search part (M1 _ _ x) = Search part x Search part (l :*: r) = Combine (Search part r) (Search part r) Search _ _ = 'NotFound
Note the next-to-last case, which mentions
f :*: g. The dual of a product is a sum,
so the object dual to
Search would be:
type family CoSearch copart (gcorecord :: k -> *) :: MaybePath where CoSearch copart (K1 _ copart) = 'Found 'Here CoSearch copart (K1 _ other) = 'NotFound CoSearch copart (M1 _ _ x) = CoSearch copart x CoSearch copart (l :+: r) = Combine (CoSearch copart l) (CoSearch copart r) CoSearch _ _ = 'NotFound
GCoHas follows the same pattern:
class GCoHas (path :: Path) copart gcorecord where gcoextract :: Proxy path -> copart -> gcorecord p
And so do the instances:
instance GCoHas 'Here corecord (K1 i corecord) where gcoextract _ = K1 instance GCoHas path copart corecord => GCoHas path copart (M1 i t corecord) where gcoextract proxy = M1 . gcoextract proxy instance GCoHas path copart l => GCoHas ('L path) copart (l :+: r) where gcoextract _ = L1 . gcoextract (Proxy :: Proxy path) instance GCoHas path copart r => GCoHas ('R path) copart (l :+: r) where gcoextract _ = R1 . gcoextract (Proxy :: Proxy path)
Note that nothing happens with the position of
Proxy here, which totally makes sense:
Proxy is only used as a hint to the type checker to help deducing the right type,
and it doesn’t bear any deep semantics within the structure we’re considering.
The implementation of the type class is obvious given the above:
class CoHas copart corecord where coextract :: copart -> corecord default coextract :: forall path. (Generic corecord, SuccessfulSearch copart corecord path) => copart -> corecord coextract = to . gcoextract (Proxy :: Proxy path)
Now let’s think about the meanings.
recordis some product type,
corecordis dual to it, so it’s some sum type. Indeed,
summight be a better name for it.
partis a field in the
copartis dual to it, so it’s one of the options in the
optionmight be a better name for it.
recordand produces a
coextract, on the other hand, takes
optionand produces a
sum. One obvious interpretation (that also follows the implementation) is that
coextractjust creates a value of the
sumtype given one of the
injectmight be a better name for it.
So, to sum it up:
CoHas option sum means that we can
option into the
Either as a prime example:
instance SuccessfulSearch a (Either l r) path => CoHas a (Either l r)
l is not the same as
r, we can create
Either l r
l (and out of
r, of course)
CoHas class and its
Yes, we also have the same type safety and soundness guarantees:
CoHas option sum is derivable (via
iff there is one and only one way to construct
sum set to the
So, for example, the following won’t work:
data AppError = QaDbError DbError | ProdDbError DbError deriving (CoHas DbError)
CoHas be useful? One might consider
MonadReader r m, Has FooEnv r to denote that
r contains a value of type
Similarly we might write
MonadError e m, CoHas FooErr e to denote
that the error context could be used to report about errors of type
This again might help modularity: assume our good old modules we considered earlier (the DB layer, the web server and the cron scheduler) might each report an error of their own type. Then we could have an application-level error type
data AppError = AppDbError DbError | AppWebServerError WebServerError | AppCronError CronError deriving (Generic, CoHas DbError, CoHas WebServerError, CoHas CronError)
that could be used to collect errors from all of the components,
just as we used
AppConfig to provide the configuration for all of the components at once.
Bottom line: using purely algebraic considerations, we arrived at yet another useful (and free, in a sense) generalization of our approach!
Alright, back to the
We’ve learned how to extract the values of some type
part out of a
record containing them.
What else can we do?
Let’s say we have a function
f :: part -> part.
Then we can obtain a function transforming
records “for free”,
merely by applying
f to the
part contained in the
update :: (part -> part) -> record -> record
If you’re familiar with the concept of the lenses, you’ve quite likely noticed that
update paired with
extract essentially allows defining one’s own lenses,
and this is not a coincidence, but we will not delve too deep into that.
Anyway, after all our exercises with
the generic code for
update is a routine.
First we add the
gupdate method to the
GHas helper class.
The only real difference is that we get the generic representation back
as opposed to a “real” type for
class GHas (path :: Path) part grecord where ... gupdate :: Proxy path -> (part -> part) -> grecord p -> grecord p
Then we add the implementation of
gupdate to the instances
GHas we already had, keeping in mind we should produce
instance GHas 'Here rec (K1 i rec) where ... gupdate _ f (K1 x) = K1 $ f x instance GHas path part record => GHas path part (M1 i t record) where ... gupdate proxy f (M1 x) = M1 (gupdate proxy f x) instance GHas path part l => GHas ('L path) part (l :*: r) where ... gupdate _ f (l :*: r) = gupdate (Proxy :: Proxy path) f l :*: r instance GHas path part r => GHas ('R path) part (l :*: r) where ... gupdate _ f (l :*: r) = l :*: gupdate (Proxy :: Proxy path) f r
Our final step is to add the corresponding
update method to
and write a default implementation for it:
class Has part record where ... update :: (part -> part) -> record -> record default update :: forall path. (Generic record, SuccessfulSearch part record path) => (part -> part) -> record -> record update f = to . gupdate (Proxy :: Proxy path) f . from
And that’s it! Easy-peasy.
extract has a nice use with
MonadReader Foo m with
(MonadReader r m, Has Foo r)
extract appropriately in the bodies of the corresponding functions.
What uses does
Not much with
MonadReader, but there is a very similar monad that supports modifications:
So, now instead of
MonadState FooState m we also write
(MonadState s m, Has FooState s),
obtaining nice composable and reusable stateful functions!
What we’ve achieved is basically a generalizaton of the
Has pattern to writable environments!
It’s also tempting to reverse the arrows once again and seeing what happens with
but that requires some less trivial math, so we’ll leave that out for another post. Stay tuned!