Can i haz? Part 3: extending the Has pattern
Once we scrapped the boilerplate for
the Has pattern, the next obvious question is if we can
generalize
further. And, turns out, we can!
In this post we’ll see how some algebraic considerations help us to discover one more pattern
useful with 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 MonadState.
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 Has:
class Has part record where
  extract :: record -> partLet’s invert it and prepend co everywhere:
class CoHas copart corecord where
  coextract :: copart -> corecordYou might already have an intuition about what CoHas might be,
but let’s proceed turning the arrows based on pure formalism for now.
Path and 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 Has:
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 _ _ = 'NotFoundNote 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 _ _ = 'NotFoundGeneric GCoHas follows the same pattern:
class GCoHas (path :: Path) copart gcorecord where
  gcoextract :: Proxy path -> copart -> gcorecord pAnd 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)Interpretation
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- record,- copartis dual to it, so it’s one of the options in the- sum. Indeed,- optionmight be a better name for it.
- extracttakes a- recordand produces a- partof that- record.- 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- options. Say,- injectmight be a better name for it.
So, to sum it up: CoHas option sum means that we can inject values
of type option into the sum type.
We have Either as a prime example:
instance SuccessfulSearch a (Either l r) path => CoHas a (Either l r)So, if l is not the same as r, we can create Either l r
out of l (and out of r, of course)
via the CoHas class and its inject method.
Yes, we also have the same type safety and soundness guarantees:
this CoHas option sum is derivable (via Generics)
iff there is one and only one way to construct sum set to the option.
So, for example, the following won’t work:
data AppError = QaDbError DbError
              | ProdDbError DbError
              deriving (CoHas DbError)Where could CoHas be useful? One might consider
CoHas and MonadError
We write MonadReader r m, Has FooEnv r to denote that
the environment r contains a value of type FooEnv.
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 FooErr.
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!
Updating records
Alright, back to the Has class.
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 record:
update :: (part -> part) -> record -> recordIf 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 extract and inject/coextract
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 gextract:
class GHas (path :: Path) part grecord where
  ...
  gupdate :: Proxy path -> (part -> part) -> grecord p -> grecord pThen we add the implementation of gupdate to the instances
of GHas we already had, keeping in mind we should produce
the appropriate Generic value:
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 rOur final step is to add the corresponding update method to Has
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 . fromAnd that’s it! Easy-peasy.
update and MonadState
extract has a nice use with MonadReader:
we replace MonadReader Foo m with (MonadReader r m, Has Foo r)
and use extract appropriately in the bodies of the corresponding functions.
What uses does update have?
Not much with MonadReader, but there is a very similar monad that supports modifications: MonadState.
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 update,
but that requires some less trivial math, so we’ll leave that out for another post. Stay tuned!