Какая разница между makeLenses и makeFields?

Довольно понятно. Я знаю, что makeClassy должен создавать классы, но я не вижу разницы между ними.

PS. Бонусные баллы для объяснения поведения по умолчанию обоих.

Ответ 1

Примечание. Этот ответ основан на объективе 4.4 или новее. В этой версии были внесены некоторые изменения в TH, поэтому я не знаю, сколько из них относится к более старым версиям объектива.

Организация функций объектива TH

Функции объектива TH основаны на одной функции, makeLensesWith (также называемой makeFieldOptics внутри объектива). Эта функция принимает аргумент LensRules, который точно описывает, что генерируется и как.

Итак, для сравнения makeLenses и makeFields нам нужно сравнить только те LensRules, которые они используют. Вы можете найти их, посмотрев источник :

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
  }

Что это значит?

Теперь мы знаем, что различия находятся в параметрах simpleLenses, generateClasses, allowIsos и fieldToDef. Но что на самом деле означают эти варианты?

  • makeFields никогда не будет генерировать изменяющие тип оптики. Это контролируется опцией simpleLenses = True. Этот вариант не имеет пик в текущей версии объектива. Однако объектив HEAD добавил для него документацию:

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

    Итак makeFields никогда не будет генерировать изменяющую тип оптики, а makeLenses, если возможно.

  • makeFields будет генерировать классы для полей. Итак, для каждого поля foo мы имеем класс:

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

    Это управляется опцией generateClasses.

  • makeFields никогда не будет генерировать Iso, даже если это возможно (управляется параметром allowIsos, который, кажется, не экспортируется из Control.Lens.TH)

  • В то время как makeLenses просто генерирует объектив верхнего уровня для каждого поля, начинающегося с подчеркивания (нижний индекс первой буквы после подчеркивания), makeFields вместо этого генерирует экземпляры для классов HasFoo. Он также использует другую схему именования, объясненную в комментарии в исходном коде:

    -- | 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 @[email protected] before the prefix.
    -- If any of the record fields leads with an @[email protected] then it is assume a field without an @[email protected] should not have a lens created.
    camelCaseFields :: LensRules
    camelCaseFields = defaultFieldRules
    

    Итак, makeFields также ожидает, что все поля не просто префиксны с подчеркиванием, но также включают имя типа данных в качестве префикса (как в data Foo = { _fooBar :: Int, _fooBaz :: Bool }). Если вы хотите создать объективы для всех полей, вы можете оставить символ подчеркивания.

    Все это управляется _fieldToDef (экспортируется как lensField на Control.Lens.TH).

Как вы можете видеть, модуль Control.Lens.TH очень гибкий. Используя makeLensesWith, вы можете создать свой собственный LensRules, если вам нужен шаблон, не охватываемый стандартными функциями.

Ответ 2

Отказ от ответственности: это основано на эксперименте с рабочим кодом; он дал мне достаточно информации для продолжения моего проекта, но я все же предпочел бы лучше документированный ответ.

data Stuff = Stuff {
    _foo
    _FooBar
    _stuffBaz
}

makeLenses

  • Создает foo в качестве объектива для доступа к Stuff
  • Создает fooBar (изменение имени с заглавной буквы в нижний регистр);

makeFields

  • Создает baz и класс HasBaz; он сделает Stuff экземпляр этого класса.

Ответ 3

Normal

makeLenses создает единственную оптику верхнего уровня для каждого поля в типе. Он ищет поля, начинающиеся с символа подчеркивания (_), и он создает оптический объект, который является настолько общим, насколько это возможно для этого поля.

  • Если у вашего типа есть один конструктор и одно поле, вы получите Iso.
  • Если ваш тип имеет один конструктор и несколько полей, вы получите много Lens.
  • Если ваш тип имеет несколько конструкторов, вы получите много Traversal.

Классный

makeClassy создает один класс, содержащий всю оптику для вашего типа. Эта версия используется для упрощения встраивания вашего типа в другой более крупный тип, достигающий своего рода подтипирования. Опция Lens и Traversal будет создана в соответствии с вышеприведенными правилами (исключается Iso, поскольку это препятствует поведению подтипирования.)

В дополнение к одному методу в классе для каждого поля вы получите дополнительный метод, который упрощает вывод экземпляров этого класса для других типов. Все другие методы имеют экземпляры по умолчанию с точки зрения метода верхнего уровня.

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

Это имеет дополнительное преимущество, что легко экспортировать/импортировать все объективы для данного типа. import Module (HasT(..))

Поля

makeFields создает один класс для каждого поля, который предназначен для повторного использования между всеми типами, у которых есть поле с заданным именем. Это скорее решение для записи имен полей, которые не могут быть разделены между типами.