Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 745
Bartek Banachewicz Avatar asked Aug 30 '14 18:08

Bartek Banachewicz


3 Answers

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.

like image 122
bennofs Avatar answered Nov 14 '22 06:11

bennofs


Disclaimer: this is based on experimenting with the working code; it gave me enough information to proceed with my project, but I'd still prefer a better-documented answer.

data Stuff = Stuff {
    _foo
    _FooBar
    _stuffBaz
}

makeLenses

  • Will create foo as a lens accessor to Stuff
  • Will create fooBar (changing the capitalized name to lowercase);

makeFields

  • Will create baz and a class HasBaz; it will make Stuff an instance of that class.
like image 32
Bartek Banachewicz Avatar answered Nov 14 '22 06:11

Bartek Banachewicz


Normal

makeLenses creates a single top-level optic for each field in the type. It looks for fields that start with an underscore (_) and it creates an optic that is as general as possible for that field.

  • If your type has one constructor and one field you'll get an Iso.
  • If your type has one constructor and multiple fields you'll get many Lens.
  • If your type has multiple constructors you'll get many Traversal.

Classy

makeClassy creates a single class containing all the optics for your type. This version is used to make it easy to embed your type in another larger type achieving a kind of subtyping. Lens and Traversal optics will be created according to the rules above (Iso is excluded because it hinders the subtyping behavior.)

In addition to one method in the class per field you'll get an extra method that makes it easy to derive instances of this class for other types. All of the other methods have default instances in terms of the top-level method.

data T = MkT { _field1 :: Int, _field2 :: Char }

class HasT a where
  t :: Lens' a T
  field1 :: Lens' a Int
  field2 :: Lens' a Char

  field1 = t . field1
  field2 = t . field2

instance HasT T where
  t = id
  field1 f (MkT x y) = fmap (\x' -> MkT x' y) (f x)
  field2 f (MkT x y) = fmap (\y' -> MkT x y') (f y)

data U = MkU { _subt :: T, _field3 :: Bool }

instance HasT U where
  t f (MkU x y) = fmap (\x' -> MkU x' y) (f x)
  -- field1 and field2 automatically defined

This has the additional benefit that it is easy to export/import all the lenses for a given type. import Module (HasT(..))

Fields

makeFields creates a single class per field which is intended to be reused between all types that have a field with the given name. This is more of a solution to record field names not being able to be shared between types.

like image 3
glguy Avatar answered Nov 14 '22 06:11

glguy