Как эффективно преобразовать индуктивный тип в коиндуктивный тип (без рекурсии)?
> {-# LANGUAGE DeriveFunctor, Rank2Types, ExistentialQuantification #-}
Любой индуктивный тип определяется так:
> newtype Ind f = Ind {flipinduct :: forall r. (f r -> r) -> r}
> induction = flip flipinduct
induction
имеет тип (f a -> a) -> Ind f -> a
. Существует двойное понятие, называемое coinduction.
> data CoInd f = forall r. Coinduction (r -> f r) r
> coinduction = Coinduction
coinduction
имеет тип (a -> f a) -> a -> CoInd f
. Обратите внимание, что induction
и coinduction
являются двойственными. В качестве примера индуктивных и коиндуктивных типов данных посмотрите на этот функтор.
> data StringF rec = Nil | Cons Char rec deriving Functor
Без рекурсии Ind StringF
- конечная строка, а CoInd StringF
- конечная или бесконечная строка (если мы используем рекурсию, они являются как конечными, так и бесконечными или undefined). В общем случае можно преобразовать Ind f -> CoInd f
для любого Functor f
. Например, мы можем обернуть значение функтора вокруг коиндуктивного типа
> wrap :: (Functor f) => f (CoInd f) -> CoInd f
> wrap fc = coinduction igo Nothing where
> --igo :: Maybe (CoInd f) -> f (Maybe (CoInd f))
> igo Nothing = fmap Just fc
> igo (Just (Coinduction step seed)) = fmap (Just . Coinduction step) $ step seed
Эта операция добавляет дополнительную операцию (соответствие шаблону Maybe
) для каждого шага. Это означает, что он наносит накладные расходы O(n)
.
Мы можем использовать индукцию на Ind f
и wrap
, чтобы получить CoInd f
.
> convert :: (Functor f) => Ind f -> CoInd f
> convert = induction wrap
Это O(n^2)
. (Получение первого уровня O(1)
, но n-й элемент O(n)
из-за вложенных Maybe
s, что делает это O(n^2)
total.)
Двойственно мы можем определить cowrap
, который берет индуктивный тип и показывает его верхний слой Functor.
> cowrap :: (Functor f) => Ind f -> f (Ind f)
> cowrap = induction igo where
> --igo :: f (f (Ind f)) -> f (Ind f)
> igo = fmap (\fInd -> Ind $ \fold -> fold $ fmap (`flipinduct` fold) fInd)
induction
всегда O(n)
, так что это cowrap
.
Мы можем использовать coinduction
для создания CoInd f
из cowrap
и Ind f
.
> convert' :: (Functor f) => Ind f -> CoInd f
> convert' = coinduction cowrap
Это снова O(n)
каждый раз, когда мы получаем элемент, в общей сложности O(n^2)
.
Мой вопрос заключается в том, что без использования рекурсии (прямо или косвенно) мы можем преобразовать Ind f
в CoInd f
в O(n)
время?
Я знаю, как это сделать с рекурсией (конвертировать Ind f
в Fix f
, а затем Fix f
в CoInd f
(начальное преобразование O(n)
, но тогда каждый элемент из CoInd f
равен O(1)
, делая второе преобразование O(n)
total и O(n) + O(n) = O(n)
)), но я хотел бы видеть, возможно ли его без.
Обратите внимание, что convert
и convert'
никогда не использовали рекурсию, прямо или косвенно. Нифти, не так ли?
Ответы
Ответ 1
Да, это формально возможно:
https://github.com/jyp/ControlledFusion/blob/master/Control/FixPoints.hs
Однако преобразование по-прежнему требует создания промежуточного буфера, который может выполняться только с помощью цикла во время выполнения.
Причина, лежащая в основе этого ограничения, заключается в том, что значение "индуктивного" типа отвечает на заданный порядок оценки (*), а значение "коиндуктивного" типа фиксирует порядок оценки. Единственный способ сделать переход возможным без принуждения многих повторных вычислений состоит в том, чтобы выделить некоторый промежуточный буфер для memoize промежуточных результатов.
Кстати, переход от "коиндуктивного" к "индуктивному" не требует никакого буфера, но требует повторения порядка оценки с использованием явного цикла.
Кстати, я изучил основные понятия в двух статьях:
1. В Haskell для эффективных потоков: https://gist.github.com/jyp/fadd6e8a2a0aa98ae94d
2. В классической линейной логике для массивов и потоков. http://lopezjuan.com/limestone/vectorcomp.pdf
(*) Это предполагает строгий язык. Вещи немного меняются в присутствии ленивой оценки, но концепции и окончательный ответ одинаковы. В исходном коде есть несколько комментариев относительно ленивой оценки.