Can i haz? Part 3: extending the Has pattern

November 11, 2019 // haskell

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 -> part

Let’s invert it and prepend co everywhere:

class CoHas part 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.

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 _ _ = '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 _ part) = 'Found 'Here
  CoSearch copart (K1 _ other) = 'NotFound
  CoSearch copart (M1 _ _ x) = CoSearch part x
  CoSearch copart (l :+: r) = Combine (CoSearch copart l) (CoSearch copart r)
  CoSearch _ _ = 'NotFound

Generic 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 part 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.

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 -> 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 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 p

Then 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 r

Our 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 . from

And 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!