Что такое параморфизмы?

Читая эту классическую статью, я зациклился на параморфизмах. К сожалению, раздел довольно тонкий, и на странице Википедии ничего не говорится.

Мой перевод Haskell:

para :: (a -> [a] -> b -> b) -> b -> [a] -> b
para f base = h
  where
    h []       =   base
    h (x:xs)   =   f x xs (h xs)

Но я не понимаю, что у меня нет интуиции для сигнатуры типа или желаемого результата.

Какой параморфизм и какие полезные примеры в действии?


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

Ответ 1

Да, это para. Сравните с катаморфизмом или foldr:

para  :: (a -> [a] -> b -> b) -> b -> [a] -> b
foldr :: (a ->        b -> b) -> b -> [a] -> b

para  c n (x : xs) = c x xs (para c n xs)
foldr c n (x : xs) = c x    (foldr c n xs)
para  c n []       = n
foldr c n []       = n

Некоторые люди называют параморфизмы "примитивной рекурсией" в отличие от катаморфизмов (foldr), являющихся "итерацией".

Где foldr двум параметрам дается рекурсивно вычисленное значение для каждого рекурсивного подобъекта входных данных (здесь, что хвост списка), параметры para получают как исходный подобъект, так и значение, вычисленное рекурсивно из него.

Примерная функция, которая хорошо выражена с помощью para, представляет собой набор правильных достаточных значений списка.

suff :: [x] -> [[x]]
suff = para (\ x xs suffxs -> xs : suffxs) []

так что

suff "suffix" = ["uffix", "ffix", "fix", "ix", "x", ""]

Возможно, еще проще

safeTail :: [x] -> Maybe [x]
safeTail = para (\ _ xs _ -> Just xs) Nothing

в котором ветвь "cons" игнорирует свой рекурсивно вычисленный аргумент и просто возвращает хвост. Оценочно лениво, рекурсивное вычисление никогда не происходит, и хвост извлекается в постоянное время.

Вы можете легко определить foldr с помощью para; немного сложнее определить para из foldr, но это, безусловно, возможно, и каждый должен знать, как это делается!

foldr c n =       para  (\ x  xs  t ->           c x    t)       n
para  c n = snd . foldr (\ x (xs, t) -> (x : xs, c x xs t)) ([], n)

Тройкой определения para с foldr является восстановление копии исходных данных, чтобы мы получили доступ к копии хвоста на каждом шаге, хотя у нас не было доступа к оригиналу. В конце snd отбрасывает копию ввода и дает только выходное значение. Это не очень эффективно, но если вы заинтересованы в явной выразительности, para дает вам не более foldr. Если вы используете эту foldr -кодированную версию para, тогда safeTail займет линейное время, скопировав элемент tail по элементу.

Итак, это: para - более удобная версия foldr, которая дает вам немедленный доступ к хвосту списка, а также значение, вычисленное из него.

В общем случае работа с типом данных, созданным как рекурсивная фиксированная точка функтора

data Fix f = In (f (Fix f))

у вас есть

cata :: Functor f => (f         t  -> t) -> Fix f -> t
para :: Functor f => (f (Fix f, t) -> t) -> Fix f -> t

cata phi (In ff) = phi (fmap (cata phi) ff)
para psi (In ff) = psi (fmap keepCopy   ff) where
  keepCopy x = (x, para psi x)

и снова, эти два являются взаимно определяемыми, причем para определяется из cata тем же "сделать копию" трюк

para psi = snd . cata (\ fxt -> (In (fmap fst fxt), psi fxt))

Опять же, para не более выразителен, чем cata, но более удобно, если вам нужен легкий доступ к подструктурам ввода.

Изменить: Я вспомнил еще один хороший пример.

Рассмотрим двоичные деревья поиска, заданные Fix TreeF, где

data TreeF sub = Leaf | Node sub Integer sub

и попробуйте определить вставку для двоичных деревьев поиска, сначала как cata, а затем как para. Вы найдете версию para намного проще, так как в каждом node вам нужно будет вставить в одно поддерево, но сохранить другое как было.