Что такое функция Zap Functor и zap в Haskell?

Я встретил эту конструкцию в Haskell. Я не мог найти никаких примеров или объяснений того, как я могу использовать zap/zapWith и bizap/bizapWith в реальном коде. Связаны ли они с некоторыми стандартными функциями zip/zipWith? Как я могу использовать zap/bizap функторы в коде Haskell? В чем их преимущества?

Ответ 1

Об этом говорит замечательный пост в блоге Kmett, Cofree Comonad и проблема выражения. К сожалению, в этом блоге довольно много терминологии, и я не совсем понимаю все сам, но мы можем попытаться набросать детали.

Числа Peano

Определим естественные числа очень странным образом. Сначала мы определим функции нуля Перо и преемника:

zero :: ()
zero = ()

incr :: a -> Identity a
incr = Identity

Тогда мы можем определить некоторые числа:

one :: Identity ()
one = incr zero

two :: Identity (Identity ())
two = incr . incr $ zero

three :: Identity (Identity (Identity ()))
three = incr . incr . incr $ zero

Необычный, но, похоже, он работает. Вы можете преобразовать 3 :: Int в three и обратно. (Попробуйте.) Можем ли мы написать функцию f для преобразования любого произвольного числа в наше странное представление и обратно? К сожалению нет. Система типа Haskell не позволит нам построить бесконечный тип, который является точным типом, который мы хотим для f.

Еще большая проблема заключается в том, что, как ленивый функциональный программист, я хочу перестать набирать Identity столько раз. При такой скорости я буду вынужден вводить ее O (n ^ 2) раз, чтобы определить n чисел. Это 2016 год, и я считаю это неприемлемым.

Мы можем обратиться к бесплатному типу данных за помощью для обеих наших проблем. (Некоторые люди называют эту Свободную монаду, но нет оснований для нас говорить "монада", когда мы можем просто сказать "тип".)

newtype Free f a = Free (Either a (f (Free f a)))

zero :: a -> Free Identity a
zero x = Free (Left x)

incr :: Free Identity a -> Free Identity a
incr = Free . Right . Identity

one :: a -> Free Identity a
one x = incr (zero x)

two :: a -> Free Identity a
two x = incr (incr (zero x))

three :: a -> Free Identity a
three = incr . incr . incr . zero

Довольно. Это (как ни удивительно, возможно) идентично нашему необычному представлению Identity, описанному выше.

Струйные

Теперь попробуем создать поток. Скажем, поток центриальных високосных лет, начиная с 2000 года (2000, 2400, 2800). Но странным образом.

unfold :: (a -> a) -> a -> (a, (a, (a, ...)))
unfold a2a a = (a, unfold a2a (a2a a))

centurials :: (Int, (Int, (Int, ...)))
centurials = unfold (+ 400) 2000

Предполагая, что компилятор позволил нам записать наш бесконечный тип, это было бы подходящим представлением для потока чисел. К спасению приходит Cofree, "двойной" тип Free. Двойной в категории теоретический смысл, где, если вы потратили время и вытащили учебник теории категорий и отварили много кофе и вытащили категорическую диаграмму, а затем перевернули все стрелки, которые вы переходили от одного к другому (или другое к одному). Это неудовлетворительное объяснение должно быть достаточным в качестве ручного парашюта о двойственности.

newtype Cofree f a = Cofree (a, f (Cofree f a))

unfold :: Functor f => (a -> f a) -> a -> Cofree f a
unfold a2fa a = Cofree (a, fmap (unfold a2fa) (a2fa a))

centurials :: Cofree Identity Int
centurials = unfold (Identity . (+ 400)) 2000

Это (опять-таки удивительно возможно) эквивалентно нашему представлению о кукольной королеве, представленном выше.

Индексирующие потоки с номерами Пеано

Но как искать конкретные элементы в потоке? Воспользовавшись дуальностью между Free и Cofree, мы можем фактически использовать наше представление чисел Пеано для индексации в наше представление потоков.

Оказывается, что в Haskell, если f и g являются двойственными в математическом смысле, то имеет место следующее свойство:

class Zap f g | f -> g, g -> f where
  zap :: (a -> b -> r) -> f a -> g b -> r

Нам придется преодолеть дискуссию о двойственности и почему это свойство справедливо для двойственных функторов.

Мы можем реализовать простейший экземпляр: математическую двойственность между тождественным функтором (представленным в Haskell как newtype Identity a = Identity a) и самим.

instance Zap Identity Identity where
  zap ab2r (Identity a) (Identity b) = ab2r a b

Это свойство может быть распространено и на бифунторы:

class Bizap f g | f -> g, g -> f where
  bizap :: (a -> c -> r) -> (b -> d -> r) -> f a b -> g c d -> r

Мы можем создать экземпляр этого класса для кодировки Хаскелла суммы и произведения, которые (нетривиально!) двойственны в теории категорий:

instance Bizap Either (,) where
  bizap ac2r bd2r (Left a) (c, d) = ac2r a c
  bizap ac2r bd2r (Right b) (c, d) = bd2r b d

instance Bizap (,) Either where
  bizap ac2r bd2r (a, b) (Left c) = ac2r a c
  bizap ac2r bd2r (a, b) (Right d) = bd2r b d

Теперь у нас достаточно механизмов, чтобы показать ту же двойственность в Haskell между Free и Cofree.

instance Zap f g => Zap (Cofree f) (Free g) where
  zap ab2r (Cofree as) (Free bs) = bizap ab2r (zap (zap ab2r)) as bs

instance Zap f g => Zap (Free f) (Cofree g) where
  zap ab2r (Free as) (Cofree bs) = bizap ab2r (zap (zap ab2r)) as bs

Эти случаи используют двойную бифунториальную природу Либо и (,), а также унаследованную zap по двойственности f и g, которая в наших примерах всегда будет Identity и Identity чтобы "отменить" слой с Free и Cofree и рекурсивно вызвать zap на этом нижнем уровне.

Наконец, посмотрим его в действии:

year2800 :: Int
year2800 = zap id (two id) centurials

Используя это свойство zap или "annhilation", мы можем извлекать значения из потока, созданного Cofree, с использованием натуральных индексов чисел, созданных Free. Хотя это далеко не реальный пример, этот код существует как упражнение в том, как мы можем кодировать идеи с высоким falutin из теории категорий в типах Haskell и типах. То, что мы в состоянии сделать это, служит мозговой дразнилкой, а также проверкой здравого смысла для нашего выбора типов для Free и Cofree.

Вы можете найти примеры, которые могут быть полезны для однострочных линий или когда структура данных оказывается в правильном порядке, как указано в ответе Гуркенгласа. Если вы обнаружите, что двойственность является полезным свойством для использования, обязательно достигните этой части пакета категорий. Но даже если мы не сможем найти его для использования, мы, безусловно, можем оценить красоту всего, что аккуратно сочетается.

Что касается zping

Вы правы, думая, что существует связь между Zap и Zip. Они названы в стиле Kmett развития, основанного на словах. Кодирование zippable-функторов приводит к очень похожей категории Zip:

class Functor f => Zip f where
  fzipWith :: (a -> b -> c) -> f a -> f b -> f c

Вы можете получить общую функцию zipping для деревьев и потоков (снова используя Cofree), следуя этой конструкции. Подробнее см. В блоге.

Ответ 2

Zap связывает двойные функторы. В Haskell это означает, что один несет дополнительную информацию, а другой использует ее и что все они делают. (r, a) - значение, несущее дополнительное значение r, r -> b - значение b, которое сначала требует значения r. zapWith (+) read ("123", 321) подключит "123" к считыванию и подключит 123 и 321 к (+). Все Zap это помогает при подключении, если вы найдете фрагмент кода, в котором он подходит. zap = uncurry для этого экземпляра, кстати. Reader и Coreader изоморфны ((->) r) и ((,) r) соответственно. Другие экземпляры были бы одним, обеспечивающим два значения, а другие с использованием обоих, или f, обеспечивающих r, а затем использование s, а g - s, затем использует r.