Есть ли что-то вроде ката, но где вы можете сопоставить внутреннюю структуру?

У меня этот язык AST

data ExprF r = Const Int
              | Var   String
              | Lambda String r
              | EList [r]
              | Apply r r
 deriving ( Show, Eq, Ord, Functor, Foldable )

И я хочу преобразовать его в строку

toString = cata $ \case
  Const x -> show x
  Var x -> x
  EList x -> unwords x
  Lambda x y -> unwords [x, "=>", y]
  Apply x y -> unwords [x, "(", y, ")"]

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

(x => x)(1)

но я не могу сопоставить внутреннюю структуру с ката

toString :: Fix ExprF -> String
toString = cata $ \case
  Const x -> show x
  Var x -> x
  Lambda x y -> unwords [x, "=>", y]
  Apply (Lambda{}) y -> unwords ["(", x, ")", "(", y, ")"]
  Apply x y -> unwords [x, "(", y, ")"]

Есть ли лучшее решение, чем para?

toString2 :: Fix ExprF -> String
toString2 = para $ \case
  Const x -> show x
  Var x -> x
  Lambda x (_,y) -> unwords [x, "=>", y]
  EList x -> unwords (snd <$> x)
  Apply ((Fix Lambda {}),x) (_,y) -> unwords ["(", x, ")", "(", y, ")"]
  Apply (_,x) (_,y) -> unwords [x, "(", y, ")"]

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


person ais    schedule 23.08.2016    source источник
comment
В общем случае вы можете использовать аргумент приоритета, как в showsPrec. Хотя мне Para не так уж и плохо.   -  person chi    schedule 23.08.2016
comment
Не могли бы вы уточнить? Я не понимаю, куда я могу привести этот аргумент. В этом случае да para в порядке, но когда аст намного больше, это просто слишком много шума.   -  person ais    schedule 23.08.2016
comment
@ais У меня нет времени писать полный ответ прямо сейчас, но вы можете передать Bool рекурсивному вызову, свернув функцию Bool -> a. Заключительные примечания Киселева без тегов касаются этого.   -  person Benjamin Hodgson♦    schedule 23.08.2016
comment
Вы можете создать функцию, используя cata: что-то вроде cata $ \case Var x -> const x ; Apply x y -> \_ -> unwords [x 5, y 5] ; Lambda x y -> (\p -> if p==5 then addParen (x 0) (y 0) else noParen (x 0) (y 0) ; ... Отрегулируйте номера приоритета по мере необходимости и передайте начальный 0 на верхнем уровне, чтобы результат был строкой.   -  person chi    schedule 23.08.2016
comment
@ais Вам может понравиться обсуждение в этом вопросе о красивых логических формулах для получения дополнительной информации о подходе showsPrec.   -  person Daniel Wagner    schedule 23.08.2016
comment
@chi интересно. Но почему Int, Bool не должно быть достаточно?   -  person ais    schedule 23.08.2016
comment
Один из вариантов - обернуть новый тип вокруг Fix ExprF, а затем использовать синонимы шаблонов для его создания / деконструирования. Синонимы шаблонов сделают его менее неудобным, а newtype сделает типы менее пугающими.   -  person dfeuer    schedule 23.08.2016
comment
@ais Bool подойдет, если у вас только два уровня приоритета. В общем случае нужно больше. Ссылка Даниэля Вагнера выше является таким примером.   -  person chi    schedule 23.08.2016


Ответы (3)


Как @chi, @DanielWagner и я указали в комментариях, способ сделать такого рода красивую печать с круглыми скобками структурно рекурсивным способом - это "_ 1_ подход".

Основная идея состоит не в том, чтобы сворачивать синтаксическое дерево в String, а в функцию Bool -> String. Это дает нам определенную степень чувствительности к контексту в свертке: мы будем использовать этот дополнительный параметр Bool, чтобы отслеживать, находимся ли мы в настоящее время в контексте левой части приложения.

parens x = "(" ++ x ++ ")"

ppAlg :: ExprF (Bool -> String) -> (Bool -> String)
ppAlg (Const x) isBeingApplied = show x
ppAlg (Var x) isBeingApplied = x
ppAlg (Lambda name body) isBeingApplied = p ("\\" ++ name ++ " -> " ++ body False)
    where p = if isBeingApplied then parens else id
ppAlg (EList es) isBeingApplied = unwords (sequenceA es False)
ppAlg (Apply fun arg) isBeingApplied = fun True ++ " " ++ arg False

Мы передаем значения isBeingApplied по рекурсивным вызовам в зависимости от того, где мы находимся в синтаксическом дереве прямо сейчас. Обратите внимание, что единственное место, где мы передаем True, - это аргумент fun в теле дела Apply. Затем, в случае Lambda, мы проверяем этот аргумент. Если текущий термин является левой частью приложения, мы заключаем лямбда в скобки; если нет, то нет.

На верхнем уровне, свернув все дерево в функцию Bool -> String, мы передаем ей аргумент False - в настоящее время мы не находимся в контексте приложения - чтобы получить String.

pp :: Expr -> String
pp ex = cata ppAlg ex False

ghci> pp $ app (lam "x" (var "x")) (cnst 2)
"(\\x -> x) 2"

Заменив Bool на Int, этот подход можно обобщить на операторы в скобках с произвольным приоритетом, как описано в Связанный ответ @ DanielWagner.

person Benjamin Hodgson♦    schedule 23.08.2016
comment
Функция Bool -> String - это просто пара (String, String). Таким образом, вы складываете его дважды, со скобками и без них, а затем выбираете, какой из них хотите, во время рекурсии. Это великолепно, +1 - person V. Semeria; 24.08.2016
comment
@ В.Семерия Ага! Совершенно верно. Но версия Bool -> String лучше. Вы делаете меньше работы (потому что вы только фактически складываете его один раз). Он также лучше масштабируется для синтаксиса с более чем двумя приоритетами: если у вас, скажем, 5 уровней приоритета на вашем языке, тогда работать с Ints (или каким-либо типом с пятью значениями, если вы хотите быть полным) намного проще, чем работать с кортежами (String, String, String, String, String) . - person Benjamin Hodgson♦; 24.08.2016

Одним из решений является использование расширения {-# LANGUAGE PatternSynonyms #-} и определение однонаправленных шаблонов, например:

pattern Apply' r1 r2 <- Apply (_,r1) (_,r2)

которые вы затем можете использовать в своих определениях следующим образом:

toString2 :: Fix ExprF -> String
toString2 = para $ \case
  Const x -> show x
  Var x -> x
  Lambda x (_,y) -> unwords [x, "=>", y]
  EList x -> unwords (snd <$> x)
  Apply ((Fix Lambda {}),x) (_,y) -> unwords ["(", x, ")", "(", y, ")"]
  Apply' x y -> unwords [x, "(", y, ")"]

Поскольку ExprF является функтором, другим вариантом будет просто написать:

toString2' :: Fix ExprF -> String
toString2' = para $ \case
  Apply ((Fix Lambda {}),x) (_,y) -> unwords ["(", x, ")", "(", y, ")"]
  other -> case fmap snd other of
      Const x -> show x
      Var x -> x
      Lambda x y -> unwords [x, "=>", y]
      Apply x y -> unwords [x, "(", y, ")"]

С синонимом шаблона и компиляцией с -Wall мне трудно убедить средство проверки полноты, что совпадения с шаблоном являются исчерпывающими.

person danidiaz    schedule 23.08.2016
comment
Это делает код короче, но напрямую не решает вопрос. OP хочет использовать структурную рекурсию для написания красивого принтера - person Benjamin Hodgson♦; 23.08.2016
comment
Программа проверки полноты полностью нарушена в отношении синонимов шаблонов (или, по крайней мере, нетривиальных). Пока ни у кого нет исправления. - person dfeuer; 24.08.2016

Как насчет прямой рекурсии для отсутствующего случая:

toString :: Fix ExprF -> String
toString (Fix (Apply (Fix (Lambda _ x)) y)) = "(" ++ toString x ++ ")(" ++ toString y ++ ")"
toString z = (cata $ \case
  Const x -> show x
  Var x -> x
  EList x -> unwords x
  Lambda x y -> unwords [x, "=>", y]
  Apply x y -> unwords [x, "(", y, ")"]) z
person V. Semeria    schedule 23.08.2016
comment
Это не удастся, поскольку cata не рекурсивно использует первый случай, например. в 1_ - person chi; 23.08.2016
comment
Затем рекурсия для каждого конструктора ExprF. Это не намного длиннее пара и, вероятно, будет быстрее. - person V. Semeria; 23.08.2016
comment
Я думаю, что весь вопрос заключается в том, чтобы избежать написания явной рекурсии. - person Daniel Wagner; 23.08.2016
comment
@DanielWagner не нужен. Главное - найти лучшее решение проблемы. - person ais; 23.08.2016