There are a few ways of reaching into records that each have their own advantages and drawbacks. I keep reevaluating these methods every time I want to reach into a record so I'm writing down the cons for each method so I can remember them later. Most of these methods will be in the context of storing multiple effects, because that is the most common reason for me to want to reach into a record without caring about the record type.
The simplest way of reaching into a record is to use a Has*** typeclass. For example if I have an effectful
record of functions UserRepo m, then a typeclass to reach into an arbitrary record and pull out a UserRepo m would
look like:
class HasUserRepo effs m | effs -> m where
userRepoL :: Lens' effs (UserRepo m)Then if I want to create an instance for HasUserRepo for some record Effs with field effsUserRepo,
then it would look like:
instance HasUserRepo (Effs m) m where
userRepoL = lens effsUserRepo (\e u' -> e { effsUserRepo = u' })If you don't want to have a dependency on lens you can just have a getter and setter instead:
class HasUserRepo effs m | effs -> m where
getUserRepo :: effs -> UserRepo m
setUserRepo :: UserRepo m -> effs -> effsThe main drawback to using plain typeclasses is boilerplate. If you have many records that look similar but each have a lot of fields, you will have to manually create instances for every single field. Each instance can take multiple lines so it's a lot of boilerplate.
Since the problem with manually creating instances for Has typeclasses is boilerplate, the next logical step is to use
Template Haskell (Haskell's form of macros) to generate the boilerplate code automatically. The lens library includes
a macro called makeFields that does this. For example, if I have a record like this:
data Effs k v m = Effs
{ effsUserRepo :: UserRepo m
, effsKvStore :: KVStore k v m
}makeFields ''Effs will generate something like this:
class HasKvStore s a | s -> a where
kvStore :: Lens' s a
class HasUserRepo s a | s -> a where
userRepo :: Lens' s a
instance HasKvStore (Effs k v m) (KVStore k v m) where
kvStore = lens effsKvStore (\e s -> e { effsKvStore = s })
instance HasUserRepo (Effs k v m) (UserRepo m) where
userRepo = lens effsUserRepo (\e u -> e { effsUserRepo = u })If you already have typeclasses like HasKvStore and HasUserRepo imported in scope, then makeFields will just create the instances
for these instead of generating a typeclass. This allows you to skip the boilerplate even across multiple modules.
The drawback to makeFields is that it uses Template Haskell, which causes slower compilation time, especially with
different compilers like GHCJS. A solution is to keep all the Template Haskell code in one file, but this limits flexibility.
Even if you cannot use makeFields, you can still manually create Has typeclasses in the same format that makeFields uses. If you do that then other users can
use makeFields with your typeclasses imported and it will automatically create the instances.
Is there any way to work around the drawbacks of Template Haskell? A library called generic-lens exists that allows you to derive these types of instances without needing Template Haskell.