Понимание Data.Functor.Constant конструктор и применимые законы

Я запутался в конструкторе типа Data.Functor.Constant, а также о том, как он работает с аппликативным.


Сначала конструктор:

Когда я исследую тип Constant :: a -> Constant a b

Я вижу, что он принимает a, но возвращает Constant a b

Откуда возникает b и почему он существует?


Во-вторых, я борюсь с Аппликатором:

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

Закон, которому он должен подчиняться, равен: pure id <*> Constant x = x

Я думал, что это то же самое, что: Constant id <*> Constant x = x

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

:t pure id <*> Constant "hello" // Constant [Char] b

:t Constant id <*> Constant "hello" // Couldn't match expected type `a0 -> a0' with actual type `[Char]'

:t pure id <*> Constant reverse //  Constant ([a] -> [a]) b

:t Constant id <*> Constant reverse // Constant ([a] -> [a]) b

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

Подведем итог двум вопросам:

  • Что делает b в конструкторе Constant?

  • Почему чистая работа, даже если моноиды различны внутри?

Большое спасибо!

Ответ 1

Хорошо, поэтому у вас есть этот тип

data Const a b = Const { getConst :: a }

Ваш первый вопрос: "Откуда приходит b?"

Ответ заключается в том, что он не приходит нигде. Точно так же, как вы можете думать о Maybe b как контейнер, который содержит либо 0, либо 1 значение типа b, a Const a b - это контейнер, который содержит ровно 0 значений типа b (но определенно выполняется значение типа a).

Ваш второй вопрос: "Почему он там?"

Хорошо, иногда полезно иметь функтор, который говорит, что он может содержать значения типа b, но на самом деле имеет что-то другое (например, думать о функторе Either a b), разница в том, что Either a b может содержать значение типа b, тогда как Const a b определенно не соответствует).

Затем вы спросили о фрагментах кода pure id <*> Const "hello" и Const id <*> Const "hello". Вы думали, что это одно и то же, но это не так. Причина в том, что экземпляр Applicative для Const выглядит как

instance Monoid m => Applicative (Const m) where
  -- pure :: a -> Const m a
  pure _ = Const mempty

  -- <*> :: Const m (a -> b) -> Const m a -> Const m b
  Const m1 <*> Const m2 = Const (m1 <> m2)

Поскольку на самом деле нет значений, имеющих тип второго параметра, нам нужно иметь дело только с теми, у кого есть тип первого параметра, который, как мы знаем, является моноидом. Вот почему мы можем сделать Const экземпляр Applicative - нам нужно вытащить значение типа m откуда-то, а экземпляр Monoid дает нам способ сделать его из ниоткуда (используя mempty).

Итак, что происходит в ваших примерах? У вас есть pure id <*> Const "hello", который должен иметь тип Const String a с id :: a -> a. Моноид в этом случае равен String. Имеем mempty = "" для a String и (<>) = (++). Таким образом, вы получаете

pure id <*> Const "hello" = Const "" <*> Const "hello"
                          = Const ("" <> "hello")
                          = Const ("" ++ "hello")
                          = Const "hello"

С другой стороны, когда вы пишете Const id <*> Const "hello", левый аргумент имеет тип Const (a -> a) b, а правый имеет тип Const String b, и вы видите, что типы не совпадают, поэтому вы получаете тип ошибка.

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

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

то есть. если вы дадите ему функцию, которая преобразует значения типа a, она вернет вам функцию, которая преобразует значения типа b. Для чего это полезно? Итак, возьмем случайную функцию типа a -> f a для конкретного функтора f. Если мы выберем функтор Identity, который выглядит как

data Identity a = Identity { getIdentity :: a }

то если l является линзой, определение

modify :: Lens b a -> (a -> a) -> (b -> b)
modify l f = runIdentity . l (Identity . f)

предоставляет вам способ выполнения функций, которые преобразуют a и превращают их в функции, преобразующие b s.

Другая функция типа a -> f a, которую мы могли бы передать, - Const :: a -> Const a a (обратите внимание, что мы специализируемся, чтобы второй тип был таким же, как и первый). Тогда действие линзы l состоит в том, чтобы превратить ее в функцию типа b -> Const a b, которая говорит нам, что она может содержать b, но на самом деле она скрытно содержит a! Как только мы применили его к типу b, чтобы получить Const a b, мы можем нажать его с помощью getConst :: Const a b -> a, чтобы вытащить значение типа a из шляпы. Таким образом, это дает нам способ извлечь значения типа a из a b - i.e это геттер. Определение выглядит как

get :: Lens b a -> b -> a
get l = getConst . l Const

В качестве примера объектива вы можете определить

first :: Lens (a,b) a
first f (a,b) = fmap (\x -> (x,b)) (f a)

чтобы вы могли открыть сеанс GHCI и написать

>> get first (1,2)
1
>> modify first (*2) (3,4)
(6,4)

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