Отдельные положительные и отрицательные вхождения переменных PHOAS при наличии рекурсивных связующих

#haskell #graph #abstract-syntax-tree #dsl

#haskell #График #абстрактное синтаксическое дерево #dsl

Вопрос:

Структурированное кодирование графов в параметрическом абстрактном синтаксисе высшего порядка (PHOAS)

Я читаю статью Оливера и Кука «Функциональное программирование со структурированными графами» (слайды, черновик документа.), предлагающую элегантное решение для кодирования совместного использования и циклов в графоподобных структурах с использованием рекурсивных привязок в PHOAS.

AST (пример потока)

Например, потоки с обратными ребрами могут быть закодированы как :

 -- 'x' is the element type, 'b' is the PHOAS's abstract variable:
data PS0 x b = Var0 b
             | Mu0 (b -> PS0 x b) -- recursive binder
             | Cons0 x  (PS0 x b)
-- Closed terms:
newtype Stream0 x = Stream0 { runS0 :: forall b. PS0 x b }

-- Example : [0, 1, 2, 1, 2, 1, 2, ...
exPS0 = Stream0 $ Cons0 0 (Mu0 $ x -> Cons0 1 (Cons0 2 $ Var0 x))
  

Сворачивание (в список)

AST можно свернуть в список без учета циклов:

 toListPS0 :: Stream0 x -> [x]
toListPS0 = go . runS0
  where
    go (Var0 x) = x
    go (Mu0 h) = go . h $ [] -- nil
    go (Cons0 x xs) =  x : go xs

-- toListPS0 exPS0 == [0, 1, 2]
  

Или создайте бесконечный список, взяв фиксированную точку рекурсивных связующих:

 toListRecPS0 :: Stream0 x -> [x]
toListRecPS0 = go . runS0
  where
    go (Var0 x) = x
    go (Mu0 h) = fix $ go . h -- fixpoint
    go (Cons0 x xs) = x : go xs

-- toListRecPS0 exPS0 == [0, 1, 2, 1, 2, 1, 2, ...
  

Квазимонадический join

Авторы отмечают, что кодирование представляет собой квазимонаду, имеющую оба join и return , но не fmap .

 returnPS0 :: b -> PS0 x b
returnPS0 = Var0

joinPS0 :: PS0 x (PS0 x b) -> PS0 x b
joinPS0 (Var0 b) = b
joinPS0 (Mu0 h) = Mu0 $ joinPS0 . h . Var0
joinPS0 (Cons0 x xs) = Cons0 x $ joinPS0 xs
  

Это можно использовать для развертывания одного уровня рекурсивной привязки:

 unrollPS0 :: Stream0 x -> Stream0 x
unrollPS0 s = Stream0 $ joinPS0 . go $ runS0 s
  where
    go (Mu0 g) = g . joinPS0 . Mu0 $ g
    go (Cons0 x xs) = Cons0 x $ go xs
    go e = e

-- toListPS0 . unrollPS0 $ exPS0 == [0, 1, 2, 1, 2]
  

PHOAS бесплатно

Это напомнило мне отличную статью Эдварда Кметта о FPComplete: PHOAS бесплатно. Идея состоит в том, чтобы сделать AST профунктором, разделяющим отрицательное и положительное вхождение переменной PHOAS.

«Фиксированная точка с порядком размещения» функтора представлена AST, подобным свободной монаде (Fegaras и Sheard):

 data Rec p a b = Place b
               | Roll (p a (Rec p a b))
  

Provided that p is a profunctor (or that p a is a functor), Rec p a is a monad (and a functor !).

The stream AST can be encoded with the non-recursive functor PSF :

 data PSF x a b = MuF (a -> b)
               | ConsF x   b

-- Type and pattern synonyms:
type PS1 x = Rec (PSF x)
pattern Var1 x = Place x
pattern Mu1 h = Roll (MuF h)
pattern Cons1 x xs = Roll (ConsF x xs)

-- Closed terms:
newtype Stream1 x = Stream1 { runS1 :: forall b. PS1 x b b }

-- Example : [0, 1, 2, 1, 2, 1, 2, ...
exPS1 = Stream1 $ Cons1 0 (Mu1 $ x -> Cons1 1 (Cons1 2 (Var1 x)))
  

Проблема

Экземпляр join из Rec экземпляра monad отличается от оригинала joinPS1 из статьи!

Грамотный перевод joinPS0 использования синонимов шаблона:

 joinPS1 :: PS1 x (PS1 x b b) (PS1 x b b) -> PS1 x b b
joinPS1 (Var1 b) = b
joinPS1 (Mu1 h) = Mu1 $ joinPS1 . h . Var1 -- Var1 affects the negative occurrences
joinPS1 (Cons1 x xs) = Cons1 x $ joinPS1 xs
  

Принимая во внимание, что встраивание (>>=) и fmap (>>= id) ввод дают нам :

 joinFreePSF :: PS1 x a (PS1 x a b) -> PS1 x a b
joinFreePSF (Var1 b) = b
joinFreePSF (Mu1 h) = Mu1 $ joinFreePSF . h -- No Var1 !
joinFreePSF (Cons1 x xs) = Cons1 x $ joinFreePSF xs
  

Итак, мой вопрос в том, почему эта разница?

Проблема в том, что такие операции, как unrollPS1 required join , которые «разбивают» как положительные, так и отрицательные вхождения монады (например, в joinPS1 типе).

Я думаю, что это связано с рекурсивным характером связующих. Я пытался переписать unrollPS1 , работая с типами, но я не уверен, что полностью понимаю, что происходит на уровне значений.

Замечание

Полностью общий тип joinPS1 (выводимый компилятором)

 joinPS1 :: PS1 x (PS1 x' a a') (PS1 x a' b) -> PS1 x a' b
  

Он может быть специализирован на a' ~ a ~ b и x' ~ x .

PS:

Я не пытаюсь добиться чего-то конкретного, это скорее вопрос любопытства, например, попытка соединить точки.

Полный код со всеми экземплярами доступен здесь (gist).

Ответ №1:

На самом деле вы можете легко восстановить Olivera и Cook join из моего соединения с монадой «profunctor HOAS»:

 joinPS1 = join . lmap Var
  

Их версия делает единственное, что она может делать в их типе.

Там они должны сохранять a = b , поэтому он делает это, вводя a Var . Здесь мы можем просто включить его отдельно. Это не требуется для монады и не должно выполняться для всех случаев.

Необходимость сохранения a и b синхронизации заключается в том, почему они могут быть только «псевдо-монадой» и почему profunctor HOAS позволяет вам на самом деле иметь настоящую монаду.