Алгоритм получения естественной карты

Эта запись Reddit Эдварда Кметта дает конструктивное определение < strong>естественная карта, та, что из теоремы о свободе для fmap (которую я прочитал в еще одном сообщение Эдварда Кметта):

Для заданных f, g, h и k таких, что f . g = h . k: $map f . fmap g = fmap h . $map k, где $map — естественная карта для данного конструктора.

Я не совсем понимаю алгоритм. Давайте подойдем к этому шаг за шагом:

Мы можем определить естественную карту с помощью индукции по любому конкретному выбору F, который вы даете. В конечном счете любой такой АТД состоит из сумм, произведений, (->), 1, 0, a, вызовов других функторов и т. д.

Рассмотреть возможность:

data Smth a = A a a a | B a (Maybe a) | U | Z Void deriving ...

Никаких стрел. Давайте посмотрим, как fmap (который я считаю естественным выбором для любого АТД без (->)) будет работать здесь:

instance Functor Smth where
  fmap xy (A x x1 x2)  = A (xy x) (xy x1) (xy x2)
  fmap xy (B x xPlus1) = B (xy x) (fmap xy xPlus1) 
  -- One can pattern-match 'xPlus1' as 'Just x1' and 'Nothing'.
  -- From my point of view, 'fmap' suits better here. Reasons below.
  fmap _xy U     = U 
  fmap _xy (Z z) = absurd z

Что кажется естественным. Говоря более формально, мы применяем xy к каждому x, применяем fmap xy к каждому T x, где T — это Functor, оставляем все единицы без изменений и передаем каждое Void в absurd. Это работает и для рекурсивных определений!

data Lst a = Unit | Prepend a (Lst a) deriving ...

instance Functor Lst where
    fmap xy Unit             = Unit
    fmap xy (Prepend x lstX) = Prepend (xy x) (fmap xy lstX)

А для неиндуктивных типов: (Подробное объяснение в этом ответе под связанным постом.)

Graph a = Node a [Graph a]

instance Functor Graph where
    fmap xy (Node x children) = Node (xy x) (fmap (fmap xy) children) 

Эта часть ясна.

Когда мы разрешаем (->), у нас появляется первое, что смешивает дисперсию. Аргумент левого типа (->) находится в контравариантной позиции, правая часть находится в ковариантной позиции. Таким образом, вам нужно отслеживать конечную переменную типа по всему АТД и смотреть, встречается ли она в положительной и/или отрицательной позиции.

Теперь включаем (->)s. Попробуем продолжить эту индукцию:

Каким-то образом мы получили естественные карты для T a и S a. Таким образом, мы хотим рассмотреть следующее:

data T2S a = T2S (T a -> S a)

instance ?Class? T2S where
    ?map? ?? (T2S tx2sx) = T2S $ \ ty -> ???

И я считаю, что это тот момент, когда мы начинаем выбирать. У нас есть следующие варианты:

  • (Phantom) a не встречается ни в T, ни в S. a в T2S является фантомным, поэтому мы можем реализовать как fmap, так и contramap как const phantom.
  • (Ковариант) a встречается в S a и не встречается в T a. Таким образом, это что-то из строк ReaderT с S a (которое на самом деле не зависит от a) как окружение, которое заменяет ?Class? на Functor, ?map? на fmap, ???, ?? на xy на:
    let tx = phantom ty 
        sx = tx2sx tx
        sy = fmap xy sx
    in sy
    
  • (контравариант) a встречается в T a и не встречается в S a. Я не вижу способа сделать эту штуку ковариантным функтором, поэтому мы реализуем здесь экземпляр Contravariant, который заменяет ?map? на contramap, ?? на yx, ??? на:
    let tx = fmap yx ty 
        sx = tx2sx tx
        sy = phantom sx 
    in sy
    
  • (неизменяемый) a встречается как в T a, так и в S a. Мы больше не можем использовать phantom, что очень кстати. Существует модуль Data.Functor.Invariant от Эдварда Кметта. . Он предоставляет следующий класс с картой:
    class Invariant f where
      invmap :: (a -> b) -> (b -> a) -> f a -> f b
      -- and some generic stuff...
    
    И все же я не вижу способа превратить это во что-то, что мы могли бы включить в свободную теорему для fmap — этот тип требует дополнительной функции-аргумента, которая мы не можем отмахнуться как id или что-то в этом роде. Так или иначе, мы ставим invmap вместо ?map?, xy yx вместо ?? и следующее вместо ???:
    let tx = fmap yx ty 
        sx = tx2sx tx
        sy = fmap xy sx
    in sy
    

Итак, правильно ли я понимаю такой алгоритм? Если да, то как нам правильно обработать случай Invariant?


person Zhiltsoff Igor    schedule 29.04.2021    source источник
comment
Чтобы превратить T a -> S a в (ковариантный) функтор, чтобы определить fmap, вам по существу нужно, чтобы T был Contravariant, а S был (ковариантным) Functor. Тогда вы получите что-то вроде fmap (f :: a->b) (myFunction :: T a -> S a) = fmap f . myFunction . contramap f :: T b -> S b.   -  person chi    schedule 29.04.2021
comment
@chi, значит, в случае Invariant есть несколько вариантов? Соответственно, фантомные функторы являются одновременно ковариантными и контравариантными, что мы используем для ковариантных и контравариантных случаев. — Таким образом, мы можем вывести Functor из случая Инвариант.   -  person Zhiltsoff Igor    schedule 29.04.2021
comment
@ZhiltsoffIgor Ну, я думаю, Чи говорит, что ваше дело invariant названо неправильно. Кажется, вы последовательно предполагаете, что a появляется в t на самом деле означает, что a появляется ковариантно в t, что является неверным предположением. Тип S a -> T a может иметь a, появляющийся как в S a, так и в T a, и по-прежнему быть ковариантным, контравариантным или инвариантным.   -  person Daniel Wagner    schedule 29.04.2021
comment
@DanielWagner ага, значит, 16 случаев вместо 4?   -  person Zhiltsoff Igor    schedule 29.04.2021
comment
@DanielWagner Отсюда интуиция Эдварда Кметта относительно отрицательных/положительных позиций. Если a находится в отрицательной позиции в T a и фантом в S a, он находится в отрицательном кратном отрицательном, то есть положительном, позиция в T a -> S a и положительном кратном отрицательном, то есть отрицательном , позиция в S a -> T a. Я правильно это объясняю? Действительно, data T a = T ((a -> Int) -> Int) deriving Functor работает.   -  person Zhiltsoff Igor    schedule 29.04.2021
comment
@ZhiltsoffIgor Ну, я думаю, что есть только 1 случай вместо 4, см. Мой ответ ниже. ^_^ Но да, остальная часть вашего объяснения кажется мне правильной.   -  person Daniel Wagner    schedule 29.04.2021


Ответы (1)


Я думаю, что ваш алгоритм слишком сложен, потому что вы пытаетесь написать один алгоритм. Вместо этого написание двух алгоритмов значительно упрощает задачу. Один алгоритм построит естественную fmap, а другой построит естественную контракарту. НО оба алгоритма должны быть недетерминированными в следующем смысле: будут типы, в которых они не могут быть успешными, и поэтому не возвращают реализацию; и будут типы, где есть несколько способов добиться успеха, но все они эквивалентны.

Для начала давайте подробно определим, что значит быть параметризованным типом. Вот различные виды параметризованных типов, которые мы можем иметь:

F ::= F + F'
    | F * F'
    | F -> F'
    | F . F'
    | Id
    | Const X

В Const X диапазон X охватывает все конкретные непараметризованные типы, такие как Int и Bool и так далее. И вот их интерпретация, то есть конкретный тип, которому они изоморфны после заданного параметра:

[[F + F']] a = Either ([[F]] a) ([[F']] a)
[[F * F']] a = ([[F]] a, [[F']] a)
[[F -> F']] a = [[F]] a -> [[F']] a
[[F . F']] a = [[F]] ([[F']] a)
[[Id]] a = a
[[Const X]] a = X

Теперь мы можем дать наши два алгоритма. Первый бит вы уже написали сами:

fmap @(F + F') f (Left x) = Left (fmap @F f x)
fmap @(F + F') f (Right x) = Right (fmap @F' f x)
fmap @(F * F') f (x, y) = (fmap @F f x, fmap @F f y)
fmap @(Id) f x = f x
fmap @(Const X) f x = x

Они соответствуют положениям, которые вы дали в первом случае. Затем в вашем примере [Graph a] вы дали предложение, соответствующее этому:

fmap @(F . F') f x = fmap @F (fmap @F' f) x

Это хорошо, но это также первый момент, когда мы получаем некоторую недетерминированность. Один из способов сделать из этого функтор действительно вложенный fmaps; но другой способ - вложенный contramaps.

fmap @(F . F') f x = contramap @F (contramap @F' f) x

Если оба предложения возможны, то ни в F, ни в F' нет Id, поэтому оба экземпляра вернут x без изменений.

Единственное, что осталось сейчас, это футляр для стрел, тот самый, о котором вы спрашиваете. Но оказывается, в этом формализме это очень просто, есть только один выбор:

fmap @(F -> F') f x = fmap @F' f . x . contramap @F f

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

contramap @(F + F') f (Left x) = Left (contramap @F f x)
contramap @(F + F') f (Right x) = Right (contramap @F' f x)
contramap @(F * F') f (x, y) = (contramap @F f x, contramap @F' f y)
contramap @(F -> F') f x = contramap @F' f . x . fmap @F f

contramap @(F . F') f x = contramap @F (fmap @F' f) x
-- OR
contramap @(F . F') f x = fmap @F (contramap @F' f) x

-- contramap @(Id) fails
contramap @(Const X) f x = x

Лично меня интересует один момент: оказывается, что contramap @(Id) — единственный случай листа, который терпит неудачу. Все дальнейшие сбои являются индуктивными сбоями, в конечном счете вытекающими из этого - факт, о котором я никогда раньше не думал! (Двойственное утверждение заключается в том, что оказывается, что fmap @(Id) — это единственный случай листа, который фактически использует свой первый аргумент функции.)

person Daniel Wagner    schedule 29.04.2021
comment
Я, конечно, не торопился, чтобы ответить: P. Назначит небольшую награду в качестве процентов. В любом случае, почему вы опускаете третий алгоритм для invmap? Каждые Functor и Contravariant можно превратить в Invariant (invmap ab _ba x = fmap ab x), но не наоборот. Таким образом, все эти случаи будут неудачными, скажем, для Endo a = a -> a. Я сам записал этот алгоритм. Беда только в том, что у меня 7 подкейсов в F -> F' кейсе... Могу я добавить свой алгоритм в правку к вашему сообщению, чтобы вы могли отклонить его, если я не прав, или принять его, если Я не ошибаюсь? - person Zhiltsoff Igor; 24.07.2021
comment
@ZhiltsoffIgor Нет серьезной причины, по которой я это пропустил; просто он реже используется. Или, чтобы ответить: почему вы не спросили об отсутствующем алгоритме для Phantom (который также можно было бы назвать Equivariant)? ;-) Я думаю, что обучение такому мышлению — определение строительных блоков, из которых состоят ваши типы, а затем написание простой индуктивной функции — более важно, чем детали фактической индуктивной функции, построенной в конце с использованием этой техники. Даже в уже написанном ответе я спорил о том, чтобы явно не писать определение contramap. - person Daniel Wagner; 24.07.2021
comment
ну, фантомный тип, как вы сказали в другом посте, с которым мы познакомились, Const замаскирован. И мы уже получили как fmap, так и contramap для Const, которые можно объединить в phantom: () <$ x $< (). Я думаю, это то, что мы делаем, когда оба fmap и contramapinvmap) являются производными. - person Zhiltsoff Igor; 24.07.2021
comment
Дополнительный вопрос - person Zhiltsoff Igor; 28.07.2021