Какая разница между объективом и частичным объективом?

"Линза" и "частичная линза" кажутся довольно похожими по названию и по понятию. Как они отличаются? В каких обстоятельствах мне нужно использовать один или другой?

Маркировка Scala и Haskell, но я бы приветствовал объяснения, связанные с любым функциональным языком, который имеет библиотеку объективов.

Ответ 1

Чтобы описать частичные линзы, которые я буду в дальнейшем называть, согласно номенклатуре Haskell lens, призмы (за исключением того, что они не являются! См. комментарий от Ørjan). Я хотел бы начать с другого взгляда у самих линз.

Объектив Lens s a указывает, что при a s мы можем "сосредоточиться" на подкомпоненте s в типе a, просмотрев его, заменив его и (если мы используем вариацию семейства объективов Lens s t a b), даже изменяя его тип.

Один из способов взглянуть на это состоит в том, что Lens s a свидетельствует об изоморфизме, эквивалентности, между s и типом набора (r, a) для неизвестного типа r.

Lens s a ====== exists r . s ~ (r, a)

Это дает нам то, что нам нужно, так как мы можем вытащить a, заменить его, а затем снова запустить обратно через эквивалентность назад, чтобы получить новый s с обновленным a.


Теперь давайте минутку, чтобы обновить нашу алгебру средней школы с помощью алгебраических типов данных. Двумя ключевыми операциями в ADT являются умножение и суммирование. Мы пишем тип a * b, когда у нас есть тип, состоящий из элементов, которые имеют как a, так и b, и мы пишем a + b, когда у нас есть тип, состоящий из элементов, которые либо a, либо b.

В Haskell пишем a * b как (a, b), тип кортежа. Мы пишем a + b как Either a b, любой тип.

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

Наконец, суммы и продукты являются категориальными двойниками. Они сочетаются друг с другом и имеют один без другого, как это делают большинство PL, ставит вас в неудобное место.


Итак, посмотрим, что произойдет, когда мы дуализируем (часть) нашей формулировки линз выше.

exists r . s ~ (r + a)

Это объявление, что s является либо типом a, либо какой-либо другой r. У нас есть lens -подобная вещь, которая олицетворяет понятие опциона (и отказа) глубоко в нем.

Это точно призма (или частичная линза)

Prism s a ====== exists r . s ~ (r + a)
                 exists r . s ~ Either r a

Итак, как это работает в отношении некоторых простых примеров?

Ну, рассмотрим призму, которая "заглушает" список:

uncons :: Prism [a] (a, [a])

это эквивалентно этому

head :: exists r . [a] ~ (r + (a, [a]))

и относительно очевидно, что здесь подразумевается r: полный сбой, так как у нас есть пустой список!

Чтобы обосновать тип a ~ b, нам нужно написать способ преобразования a a в b и a b в a так, чтобы они инвертировали друг друга. Пусть напишем, что для того, чтобы описать нашу призму через мифологическую функцию

prism :: (s ~ exists r . Either r a) -> Prism s a

uncons = prism (iso fwd bck) where
  fwd []     = Left () -- failure!
  fwd (a:as) = Right (a, as)
  bck (Left ())       = []
  bck (Right (a, as)) = a:as

Это демонстрирует, как использовать эту эквивалентность (по крайней мере в принципе) для создания призм, а также предполагает, что они должны чувствовать себя действительно естественными, когда мы работаем с типами типа sum, такими как списки.

Ответ 2

Объектив - это "функциональная ссылка", которая позволяет извлекать и/или обновлять обобщенное "поле" в большем значении. Для обычного, не-частичного объектива всегда требуется, чтобы поле имело значение для любого значения содержащего типа. Это представляет проблему, если вы хотите взглянуть на нечто вроде "поля", которое не всегда может быть там. Например, в случае "n-го элемента списка" (как указано в документации Scalaz при вставке @ChrisMartin) список может быть слишком коротким.

Таким образом, "частичная линза" обобщает объектив на случай, когда поле может или не всегда может присутствовать в большем значении.

В библиотеке Haskell lens есть как минимум три вещи, которые вы можете представить как "частичные линзы", ни одна из которых точно не соответствует Scala версия:

  • Обычный lens, чье "поле" - это тип Maybe.
  • A Prism, как описано @J.Abrahamson.
  • A Traversal.

Все они используют свое использование, но первые два слишком ограничены, чтобы включать все случаи, а Traversal являются "слишком общими". Из трех только Traversal поддерживает пример "n-го элемента списка".

  • Для версии lens, дающей версию Maybe -wrapped value ", что нарушает законы объектива: чтобы иметь подходящую линзу, вы должны установить ее на Nothing, чтобы удалить необязательное поле, затем установите его обратно в то, что было, а затем верните одно и то же значение. Это отлично работает для Map say (и Control.Lens.At.at дает такой объектив для контейнеров Map -like), но не для списка, где удаление, например, элемент 0 th не может не беспокоить более поздние.

  • A Prism в некотором смысле является обобщением конструктора (приблизительно класса case в Scala), а не поля. Таким образом, "поле", которое оно дает, когда присутствует, должно содержать всю информацию для регенерации всей структуры (что вы можете сделать с помощью функции review.)

  • A Traversal может сделать "n-й элемент списка" просто прекрасным, на самом деле есть как минимум две разные функции ix и element, которые работают для этого (но немного отличаются друг от друга в других контейнерах).

Благодаря магии типа lens любые Prism или lens автоматически работают как Traversal, а lens, дающее Maybe -обученное необязательное поле, можно превратить в Traversal простого необязательного поля, используя traverse.

Однако a Traversal в некотором смысле слишком общий, потому что он не ограничен одним полем: A Traversal может иметь любое количество "целевых" полей. Например.

elements odd

является Traversal, который с радостью пройдет через все нечетные элементы списка, обновит и/или извлечет информацию из всех них.

В теории вы можете определить четвертый вариант ( "аффинные обходы" @J.Abrahamson), которые, я думаю, могут более тесно соответствовать версии Scala, но по технической причине вне самой библиотеки lens они не будут хорошо вписываться в остальную библиотеку - вам придется явно преобразовать такой "частичный объектив", чтобы использовать с ним некоторые операции Traversal.

Кроме того, он не будет покупать вас много по сравнению с обычными Traversal s, так как там, например, простой оператор (^?), чтобы извлечь только первый пройденный элемент.

(Насколько я вижу, техническая причина заключается в том, что Pointed typeclass, который необходим для определения "аффинного обхода", не является суперклассом of Applicative, который используется обычным Traversal.)

Ответ 3

Документация Scalaz

Ниже приведены скаладоки для Scalaz LensFamily и PLensFamily, с акцентом на diff.

Lens:

Семейство объективов, предлагая чисто функциональные средства для доступа и получения поля перехода от типа B1 введите B2 в запись, одновременно переходящую из типа A1 в тип A2. scalaz.Lens является удобным псевдонимом для A1 =:= A2 и B1 =:= B2.

Термин "поле" не должен интерпретироваться ограничительно как член класса. Например, семейство объективов может обращаться к членству в Set.

Частичная линза:

Частичные семейство объективов, предлагая чисто функциональные средства для доступа и получения необязательного поля перехода от типа B1 введите B2 в запись, которая одновременно переходит из типа A1 в тип A2. scalaz.PLens является удобным псевдонимом для A1 =:= A2 и B1 =:= B2.

Термин "поле" не должен интерпретироваться ограничительно как член класса. Например, частичное семейство объективов может обращаться к n-му элементу List.

нотации

Для тех, кто не знаком со сказазом, мы должны указать символические псевдонимы типов:

type @>[A, B] = Lens[A, B]
type @?>[A, B] = PLens[A, B]

В нотации infix это означает, что тип объектива, который извлекает поле типа B из записи типа A, выражается как A @> B, а частичный объектив - как A @?> B.

Аргонавт

Argonaut (библиотека JSON) предоставляет множество примеров частичных объективов, потому что схематический характер JSON означает, что попытка получить что-то из произвольного JSON значение всегда имеет возможность отказа. Вот несколько примеров функций построения объективов от Argonaut:

  • def jArrayPL: Json @?> JsonArray — Возвращает значение только в том случае, если значение JSON представляет собой массив
  • def jStringPL: Json @?> JsonString — Возвращает значение только в том случае, если значение JSON представляет собой строку
  • def jsonObjectPL(f: JsonField): JsonObject @?> Json — Возвращает значение только в том случае, если объект JSON имеет поле f
  • def jsonArrayPL(n: Int): JsonArray @?> Json — Возвращает значение только в том случае, если массив JSON имеет элемент с индексом n