Skip to content

Instantly share code, notes, and snippets.

@DarinM223
Last active September 13, 2019 11:08
Show Gist options
  • Select an option

  • Save DarinM223/8ef66980487a29031c73cc203756f2d2 to your computer and use it in GitHub Desktop.

Select an option

Save DarinM223/8ef66980487a29031c73cc203756f2d2 to your computer and use it in GitHub Desktop.
Various methods of reaching into records in Haskell

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.

Has Typeclasses

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

The 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.

makeFields (lens)

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.

HasType (generic-lens)

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.

HasField' (generic-lens)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment