Подтвердить что ты не робот

Разве это не избыточно для Control.Lens.Setter для обертывания типов в функторах?

Я смотрю Control.Lens введение видео.
Это заставляет меня задаться вопросом, почему это необходимо для типа Setter, чтобы обернуть вещи в функторах.
Он (примерно) определяется следующим образом:

type Control.Lens.Setter s t a b = (Functor f) => (a -> f a) -> s -> f t

Скажем, у меня есть данные, называемые Point, которые определены следующим образом:

data Point = Point { _x :: Int, _y :: Int } deriving Show

Затем я могу написать свой собственный xlens следующим образом:

type MySetter s t a b = (a -> b) -> s -> t
xlens :: MySetter Point Point Int Int
xlens f p = p { _x = f (_x p) }

И я могу использовать его следующим образом:

p = Point 100 200
xlens (+1) p -- Results in Point { _x = 101, _y = 200 }

Используя Control.Lens, тот же эффект достигается посредством:

over x (+1) p

где следующие позиции:

x :: Functor f => (Int -> f Int) -> Point -> f Point
over :: Setter Point Point Int Int -> (Int -> Int) -> Point -> Point

Итак, мой вопрос заключается в том, что тот же эффект может быть достигнут более простым способом, почему Control.Lens обертывает вещи в функторах? Это выглядит излишним для меня, так как мой xlens делает то же самое, что и Control.Lens over x.

Только для записи я также могу связать объективы таким же образом:

data Atom = Atom { _element :: String, _pos :: Point } deriving Show
poslens :: MySetter Atom Atom Point Point
poslens f a = a { _pos = f (_pos a) }

a = Atom "Oxygen" p
(poslens . xlens) :: (Int -> Int) -> Atom -> Atom
(poslens . xlens) (+1) a -- Results in Atom "Oxygen" (Point 101 200)
4b9b3361

Ответ 1

Это замечательный вопрос и потребует немного распаковки.

Я хочу аккуратно исправлять вас в одной точке сразу с места в карьер: тип Setter в пакете lens по состоянию на последние версии

type Setter s t a b = (a -> Identity b) -> s -> Identity t

Нет Functor в поле зрения... еще.

Это не отменяет ваш вопрос. Почему не просто тип

type Setter s t a b = (a -> b) -> s -> t

Для этого нам сначала нужно поговорить о lens.

Lens

A lens - это тип, который позволяет нам выполнять как работу с геттером, так и сеттер. Эти две комбинации образуют одну красивую функциональную ссылку.

Простой выбор для типа lens:

type Getter s a = s -> a
type Setter s t a b = (a -> b) -> s -> t
type Lens s t a b = (Getter s a, Setter s t a b)

Этот тип, однако, очень неудовлетворен.

  • Он не скомпонован с ., который, возможно, является самой лучшей точкой продаж пакета lens.
  • Это скорее неэффективная память для создания множества кортежей, а только для разлома их позже.
  • Большая: функции, которые принимают геттеры (например, view) и сеттеры (например, over), не могут воспринимать линзы, потому что их типы настолько разные.

Без этой последней проблемы решить, зачем даже писать библиотеку? Мы не хотели бы, чтобы пользователям приходилось постоянно думать о том, где они находятся в иерархии оптики UML, каждый раз, когда они перемещаются вверх или вниз, корректируют свои функциональные вызовы.

Тогда возникает вопрос о том, существует ли тип, который мы можем записать для lens так, чтобы он автоматически и Getter и a Setter? И для этого мы должны преобразовать типы Getter и Setter.

Getter

  • Прежде всего заметим, что s -> a эквивалентно forall r. (a -> r) -> s -> r. Это преобразование в стиль продолжения прохождения далеко не очевидно. Возможно, вы сможете реализовать это преобразование следующим образом: "Функция типа s -> a - это обещание, которое дано любому s, вы можете передать мне a. Но это должно быть эквивалентно обещанию, которое дает функцию, которая maps a to r вы можете передать мне функцию, которая отображает s в r." Может быть? Возможно, нет. Здесь может произойти скачок веры.

  • Теперь определите newtype Const r a = Const r deriving Functor. Обратите внимание, что Const r a совпадает с r, математически и во время выполнения.

  • Теперь обратите внимание, что type Getter s a = forall r. (a -> r) -> s -> r можно переписать как type Getter s t a b = forall r. (a -> Const r b) -> s -> Const r t. Хотя мы вводили новые типы переменных и душевные муки для себя, этот тип по-прежнему математически идентичен тому, с чего мы начали (s -> a).

сеттер

  • Определите newtype Identity a = Identity a. Обратите внимание, что Identity a совпадает с a, математически и во время выполнения.

  • Теперь обратите внимание, что type Setter s t a b = (a -> Identity b) -> s -> Identity t по-прежнему совпадает с типом, с которого мы начали.

Все вместе

С этой документацией в стороне, можем ли мы объединить сеттеры и геттеры в один тип lens?

type Setter s t a b = (a -> Identity b) -> s -> Identity t
type Getter s t a b = forall r. (a -> Const r b) -> s -> Const r t

Хорошо, это Haskell, и мы можем абстрагироваться от выбора Identity или Const к количественной переменной. Как wiki-видоискатель, все, что Const и Identity имеет общее, состоит в том, что каждый из них является Functor. Затем мы выбираем это как своего рода унификацию для этих типов:

type Lens s t a b = forall f. Functor f => (a -> f b) -> s -> f t

(Есть и другие причины, чтобы выбрать Functor, например, чтобы доказать законы функциональных ссылок, используя свободные теоремы, но мы немного поговорим здесь о времени.) Это forall f похоже на forall r. выше - это позволяет потребителям типа выбирать, как заполнить переменную. Заполните Identity, и вы получите сеттер. Заполните Const a, и вы получите получателя. Это было путем выбора небольших и осторожных преобразований на том пути, по которому мы смогли достичь этого момента.

Предостережения

Возможно, важно отметить, что этот вывод не является оригинальной мотивацией для пакета lens. Как объясняется страницами wiki-страниц деривации, вы можете начать с интересного поведения (.) с определенными функциями и оттуда оптика оттуда. Но я думаю, что этот путь, который мы вырезали, немного лучше объясняет вопрос, который вы задали, и это был большой вопрос, который я тоже начинал. Я также хочу передать вам объектив поверх чая, который предоставляет еще один вывод.

Я думаю, что эти множественные деривации - это хорошая вещь и своего рода измерительный щуп для здорового дизайна lens. То, что мы можем прийти к тому же изящному решению под разными углами, означает, что эта абстракция надежна и хорошо поддерживается различными интуициями и математикой.

Я также немного солгал о типе Setter в недавнем lens. Это на самом деле

type Setter s t a b = forall f. Settable f => (a -> f b) -> s -> t b

Это еще один пример абстрагирования типа более высокого порядка в оптических типах, чтобы предоставить пользователю библиотеки лучший опыт. Почти всегда f будет создаваться на Identity, так как существует instance Settable Identity. Однако время от времени вам может понадобиться передать сеттеры функции backwards, которая фиксирует f как Backwards Identity. Вероятно, мы можем классифицировать этот абзац как "более подробную информацию о lens, чем вы, вероятно, хотели бы знать".

Ответ 2

В некотором смысле причина lens обертывает сеттеры в функторах-возвратах, так как они были бы слишком сильными в противном случае.

Фактически, когда используется сеттер, в любом случае функтор будет создан в Identity, что в точности соответствует вашей предлагаемой подписи. Однако реализация сеттера не должна использовать этот факт. С вашей подписью я мог бы написать что-то вроде

zlens :: MySetter Point Point Int Int
zlens _f p = p  -- no z here!

Ну, это просто невозможно с подписями Functor, потому что zlens нужно будет количественно оценивать по функтору, он не мог бы знать, как вставить результат в обертку f. Единственный способ получить результат типа функтора - сначала применить функцию setter к полю нужного типа!

Итак, это просто хорошая свободная теорема & dagger;.

Более практично, нам нужна оболочка-функтор для совместимости. Хотя вы можете определить сеттеры без этой оболочки, это невозможно для геттеров, поскольку они используют Const, а не Identity, и для добавления аргумента этого конструктора типа нужен дополнительный полиморфизм. Требуя такой обертки для всех ароматов линз (только с разными ограничениями класса), мы можем использовать одни и те же комбинаторы для всех из них, но система типов всегда будет сворачивать функциональность вплоть до того, какие функции действительно применимы к ситуации.


& dagger; Думая об этом, гарантия на самом деле не очень сильная... Я все еще могу подорвать ее с помощью fmap (const old) cheatery, но это, конечно, не то, что реально может произойти по ошибке.