Search code examples
haskellhaskell-lens

What's the difference between makeLenses and makeFields?


Pretty self-explanatory. I know that makeClassy should create typeclasses, but I see no difference between the two.

PS. Bonus points for explaining the default behaviour of both.


Solution

  • Note: This answer is based on lens 4.4 or newer. There were some changes to the TH in that version, so I don't know how much of it applies to older versions of lens.

    Organization of the lens TH functions

    The lens TH functions are all based on one function, makeLensesWith (also named makeFieldOptics inside lens). This function takes a LensRules argument, which describes exactly what is generated and how.

    So to compare makeLenses and makeFields, we only need to compare the LensRules that they use. You can find them by looking at the source:

    makeLenses

    lensRules :: LensRules
    lensRules = LensRules
      { _simpleLenses    = False
      , _generateSigs    = True
      , _generateClasses = False
      , _allowIsos       = True
      , _classyLenses    = const Nothing
      , _fieldToDef      = \_ n ->
           case nameBase n of
             '_':x:xs -> [TopName (mkName (toLower x:xs))]
             _        -> []
      }
    

    makeFields

    defaultFieldRules :: LensRules
    defaultFieldRules = LensRules
      { _simpleLenses    = True
      , _generateSigs    = True
      , _generateClasses = True  -- classes will still be skipped if they already exist
      , _allowIsos       = False -- generating Isos would hinder field class reuse
      , _classyLenses    = const Nothing
      , _fieldToDef      = camelCaseNamer
      }
    

    What do these mean?

    Now we know that the differences are in the simpleLenses, generateClasses, allowIsos and fieldToDef options. But what do those options actually mean?

    • makeFields will never generate type-changing optics. This is controlled by the simpleLenses = True option. That option doesn't have haddocks in the current version of lens. However, lens HEAD added documentation for it:

       -- | Generate "simple" optics even when type-changing optics are possible.
       -- (e.g. 'Lens'' instead of 'Lens')
      

      So makeFields will never generate type-changing optics, while makeLenses will if possible.

    • makeFields will generate classes for the fields. So for each field foo, we have a class:

      class HasFoo t where
        foo :: Lens' t <Type of foo field>
      

      This is controlled by the generateClasses option.

    • makeFields will never generate Iso's, even if that would be possible (controlled by the allowIsos option, which doesn't seem to be exported from Control.Lens.TH)

    • While makeLenses simply generates a top-level lens for each field that starts with an underscore (lowercasing the first letter after the underscore), makeFields will instead generate instances for the HasFoo classes. It also uses a different naming scheme, explained in a comment in the source code:

      -- | Field rules for fields in the form @ prefixFieldname or _prefixFieldname @
      -- If you want all fields to be lensed, then there is no reason to use an @_@ before the prefix.
      -- If any of the record fields leads with an @_@ then it is assume a field without an @_@ should not have a lens created.
      camelCaseFields :: LensRules
      camelCaseFields = defaultFieldRules
      

      So makeFields also expect that all fields are not just prefixed with an underscore, but also include the data type name as a prefix (as in data Foo = { _fooBar :: Int, _fooBaz :: Bool }). If you want to generate lenses for all fields, you can leave out the underscore.

      This is all controlled by the _fieldToDef (exported as lensField by Control.Lens.TH).

    As you can see, the Control.Lens.TH module is very flexible. Using makeLensesWith, you can create your very own LensRules if you need a pattern not covered by the standard functions.