Подтвердить что ты не робот

Примеры гистоморфизмов в Haskell

Недавно я прочитал [1] и [2], в которых говорится об гистоморфизме (и динаморфизмах), которые являются схемами рекурсии, которые могут быть выражены, например, динамическое программирование. К сожалению, документы недоступны, если вы не знаете теорию категорий, хотя там есть код, похожий на Haskell.

Может ли кто-нибудь объяснить гистоморфизмы с примером, который использует реальный код Haskell?

4b9b3361

Ответ 1

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

data Nat = S Nat | Z

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

Мы можем легко построить новые натуральные числа из целых чисел:

-- Allow us to construct Nats
mkNat :: Integer -> Nat
mkNat n | n < 0 = error "cannot construct negative natural number"
mkNat 0 = Z
mkNat n = S $ mkNat (n-1)

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

Катаморфизм позволяет "свернуть" или "снести" структуру. Он ожидает только функцию, которая знает, как сложить структуру, когда все рекурсивные термины уже сфальсифицированы. Пусть определим такой тип, похожий на Nat, но со всеми рекурсивными экземплярами, замененными некоторым значением типа a:

data NatF a = SF a | ZF -- Aside: this is just Maybe

Теперь мы можем определить тип нашего катаморфизма для Nat:

cata :: (NatF a -> a)
     -> (Nat -> a)

Для функции, которая умеет сворачивать нерекурсивную структуру NatF a в a, cata превращает это в функцию, чтобы сбрасывать целое Nat.

Реализация cata довольно проста: сначала сверните рекурсивный подтерм (если есть) и примените нашу функцию:

cata f Z = f ZF -- No subterm to fold, base case
cata f (S subterm) = f $ SF $ cata f subterm -- Fold subterm first, recursive case

Мы можем использовать этот катаморфизм для преобразования Nat обратно в Integer s, например:

natToInteger :: Nat -> Integer
natToInteger = cata phi where
  -- We only need to provide a function to fold
  -- a non-recursive Nat-like structure
  phi :: NatF Integer -> Integer
  phi ZF = 0
  phi (SF x) = x + 1

Итак, с cata мы получаем доступ к значению непосредственного субтерма. Но представьте, что нам нравится получать доступ к значениям транзитивных субтерм, например, при определении функции фибоначчи. Затем нам нужен не только доступ к предыдущему значению, но и к 2-м предыдущему значению. Именно здесь вступают в действие гистоморфизмы.

Гистоморфизм (гисто звучит очень похоже на "историю" ) позволяет нам получить доступ ко всем предыдущим значениям, а не только к последним. Это означает, что теперь мы получаем список значений, а не только один, поэтому тип гистоморфизма:

-- We could use the type NatF (NonEmptyList a) here.
-- But because NatF is Maybe, NatF (NonEmptyList a) is equal to [a].
-- Using just [a] is a lot simpler
histo :: ([a] -> a)
      -> Nat -> a
histo f = head . go where
  -- go :: Nat -> [a]  -- This signature would need ScopedTVs
  go Z = [f []]
  go (S x) = let subvalues = go x in f subvalues : subvalues

Теперь мы можем определить fibN следующим образом:

-- Example: calculate the n-th fibonacci number
fibN :: Nat -> Integer
fibN = histo $ \x -> case x of
 (x:y:_) -> x + y
 _       -> 1

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


То, что я не показал в приведенном выше примере, заключается в том, что cata и histo могут быть реализованы очень широко, если вы определяете свой тип как фиксированную точку функтора. Наш тип Nat - это только фиксированная точка Functor NatF.

Если вы определяете histo общим способом, вам также нужно придумать такой тип, как NonEmptyList в нашем примере, но для любого функтора. Этот тип точно Cofree f, где f - функтор, на который вы взяли фиксированную точку. Вы можете видеть, что это работает для нашего примера: NonEmptyList - это просто Cofree Maybe. Вот как вы попадаете в общий тип histo:

histo :: Functor f 
      => (f (Cofree f a) -> a)
      -> Fix f  -- ^ This is the fixed point of f
      -> a

Вы можете думать о f (Cofree f a) как о стеке, где с каждым "слоем" вы можете видеть менее сложную структуру. В верхней части стека каждый непосредственный подтип складывается. Затем, если вы переходите на один уровень глубже, непосредственный субтерм больше не сворачивается, но суб-подтермы все уже свернуты (или оценены, что может иметь смысл сказать в случае АСТ). Таким образом, вы можете в основном увидеть "последовательность сокращений", которая была применена (= история).

Ответ 2

Мы можем думать о нем как о континууме обобщения от cata до histo до dyna. В терминологии recursion-schemes:

Foldable t => (Base t a -> a)                                  -> (t -> a) -- (1)
Foldable t => (Base t (Cofree (Base t) a) -> a)                -> (t -> a) -- (2)
Functor  f => (f      (Cofree f        a) -> a) ->  (t -> f t) -> (t -> a) -- (3)

где (1) cata, (2) - histo, а (3) - dyna. Обзор этого обобщения на высоком уровне состоит в том, что histo улучшает cata, поддерживая историю всех частичных "правильных складок", а dyna улучшает histo, позволяя работать с любым типом t, пока мы можем сделайте для нее f -коалгебру, а не только те Foldable (имеющие универсальные Base t -коалгебры как Foldable, свидетельствующие о том, что типы данных являются окончательными коалгебрами).

Мы можем почти прочитать их свойства, просто посмотрев, что нужно, чтобы выполнить их типы.

Например, классическое использование cata заключается в определении foldr

data instance Prim [a] x = Nil | Cons a x
type instance Base [a] = Prim [a]

instance Foldable [a] where
  project []     = Nil
  project (a:as) = Cons a as

foldr :: (a -> b -> b) -> b -> [a] -> b
foldr cons nil = cata $ \case
  Nil      -> nil
  Cons a b -> cons a b

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

Как histo обобщает cata, мы должны быть в состоянии сделать то же самое с ним. Здесь a histo -based foldr

foldr :: (a -> b -> b) -> b -> [a] -> b
foldr cons nil = histo $ \case
  Nil             -> nil
  Cons a (b :< _) -> cons a b

мы видим, что у нас уже нет немедленного результата предыдущей складки, но вместо этого нужно найти в первом слое Cofree, чтобы найти его. Но Cofree - это поток и содержит потенциально бесконечно много "предыдущих значений свертывания", и мы можем так глубоко погрузиться в него, как нам нравится. Это то, что дает histo его "историческую" власть. Например, мы можем написать довольно прямой tail с помощью histo, который сложнее сделать только с cata:

tail :: [a] -> Maybe [a]
tail = histo $ \case
  Nil             -> Nothing -- empty list
  Cons _ (b :< x) -> case x of
    Nil       -> Just [] -- length 1 list
    Cons a _ -> fmap (a:) b

Стиль немного косвенный, но по существу, потому что мы можем оглянуться назад на два последних шага, мы можем реагировать на списки длины-1 по-разному, чем списки длин-0 или длины - n.

Чтобы сделать последний шаг для обобщения histo на dyna, мы просто заменим естественную проекцию любой коалгеброй. Таким образом, мы могли бы легко реализовать histo в терминах dyna

histo phi = dyna phi project -- project is from the Foldable class

Итак, теперь мы можем применять складки histo к любому типу, который можно даже частично рассматривать как список (ну, пока мы держимся с примером выполнения и используем Prim [a] как Functor, f).

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

(Это отражает идею представления исходных алгебр по их способности project :: t -> Base t t. Если бы это был действительно полный индуктивный тип, вы могли бы проектировать столько раз, прежде чем достигнуть конца.)

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

data NEL  a   = Some  a | More  a (NEL a)
data NELf a x = Somef a | Moref a x deriving Functor

и создайте коалгебру на натуральных числах, называемых natural, которые соответствующим образом развернуты, обратный отсчет NEL

natural :: Int -> NELf Int Int
natural 0 = Somef 0
natural n = Moref n (n-1)

то мы применяем histo -стильный склад к NELf -view натурального числа для создания n -того каталонского номера.

-- here a quick implementation of `dyna` using `recursion-schemes`

zcata
  :: (Comonad w, Functor f) =>
     (a -> f a) -> (f (w (w c)) -> w b) -> (b -> c) -> a -> c
zcata z k g = g . extract . c where
  c = k . fmap (duplicate . fmap g . c) . z

dyna :: Functor f => (f (Cofree f c) -> c) -> (a -> f a) -> a -> c
dyna phi z = zcata z distHisto phi

takeC :: Int -> Cofree (NELf a) a -> [a]
takeC 0 _                 = []
takeC n (a :< Somef v)    = [a]
takeC n (a :< Moref v as) = a : takeC (n-1) as

catalan :: Int -> Int
catalan = dyna phi natural where
  phi :: NELf Int (Cofree (NELf Int) Int) -> Int
  phi (Somef 0) = 1
  phi (Moref n table) = sum (zipWith (*) xs (reverse xs))
    where xs = takeC n table