Алгоритм построения естественной карты
дает конструктивное определение естественной карты, Этот пост на Reddit от Эдварда Кметтавзятой из бесплатной теоремы (которую я прочитал в еще одном сообщении Эдварда Кметта ):
Для данного
f
,g
,h
а такжеk
, так чтоf . g = h . k
:$map f . fmap g = fmap h . $map k
, где$map
естественная карта для данного конструктора.
Я не совсем понимаю алгоритм. Подойдем к этому постепенно:
Мы можем определить «естественное отображение» индукцией по любому конкретному выбору
F
вы даете. В конечном итоге любой такой ADT состоит из сумм, произведений,1
с,0
s, 's, вызовы других функторов и т. д.
Рассмотреть возможность:
data Smth a = A a a a | B a (Maybe a) | U | Z Void deriving ...
Стрелок нет. Давайте посмотрим, как (что я считаю естественным выбором для любого ADT без s в нем) будет работать здесь:
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
Что кажется естественным . Говоря более формально, мы обращаемся к каждому
x
, применять
fmap xy
каждому
T x
, где a, мы оставляем без изменений каждую единицу и передаем каждую
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)
Эта часть ясна.
Когда мы позволяем, у нас появляется первое, что смешивает дисперсию. Аргумент левого типа находится в контравариантном положении, правая часть находится в ковариантном положении. Таким образом, вам нужно отслеживать переменную последнего типа по всему ADT и видеть, находится ли она в положительной и / или отрицательной позиции.
Теперь мы включаем
(->)
с. Попробуем продолжить эту индукцию:
Мы каким-то образом вывели естественные карты для и. Таким образом, мы хотим рассмотреть следующее:
data T2S a = T2S (T a -> S a)
instance ?Class? T2S where
?map? ?? (T2S tx2sx) = T2S $ \ ty -> ???
И я считаю, что именно здесь мы начинаем выбирать. У нас есть следующие возможности:
- встречается ни в
T
ни вS
. вT2S
является фантомом, поэтому мы можем реализовать как (Фантом) неconst phantom
. - (Ковариант) встречается и не встречается в. Таким образом, это что-то среди строк
ReaderT
с (которая на самом деле не зависит от) в качестве среды, которая заменяет?Class?
с участиемFunctor
, с участиемfmap
, , с участиемxy
с участием:let tx = phantom ty sx = tx2sx tx sy = fmap xy sx in sy
- (Контравариант) встречается и не встречается в. Я не вижу способа сделать это ковариантным функтором, поэтому мы реализуем
Contravariant
пример здесь, который заменяется на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...
id
или что-то. Во всяком случае, ставимinvmap
вместо?map?
,xy yx
вместо??
, и следующее вместо???
:let tx = fmap yx ty sx = tx2sx tx sy = fmap xy sx in sy
Итак, правильно ли я понимаю такой алгоритм? Если да, то как нам правильно обработать инвариантный случай?
1 ответ
Я считаю, что ваш алгоритм слишком сложен, потому что вы пытаетесь написать один алгоритм. Вместо этого написание двух алгоритмов значительно упрощает задачу. Один алгоритм построит естественную fmap, а другой построит естественную contramap. НО оба алгоритма должны быть недетерминированными в следующем смысле: будут типы, которые не могут быть успешными, и поэтому не возвращают реализацию; и будут типы, в которых есть несколько способов добиться успеха, но все они эквивалентны.
Для начала давайте внимательно определим, что значит быть параметризованным типом. Вот различные типы параметризованных типов, которые мы можем иметь:
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
Это нормально, но это также первый момент, когда мы получаем некоторый недетерминизм. Один из способов сделать это функтором действительно вложенный s; но другой способ - вложенный s.
fmap @(F . F') f x = contramap @F (contramap @F' f) x
Если возможны оба предложения, то нет
Id
s в любом
F
или же
F'
, поэтому оба экземпляра вернутся
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)
является единственным листовым случаем, который фактически использует свой первый аргумент функции.)