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.
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.
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:
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))]
_ -> []
}
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
}
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.
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
foo
as a lens accessor to Stuff
fooBar
(changing the capitalized name to lowercase);makeFields
baz
and a class HasBaz
; it will make Stuff
an instance of that class.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.
Iso
.Lens
.Traversal
.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(..))
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With