Генерация парсера с помощью `inverse` с ограничениями по грамматике
Недавно я изучил A Taste of Curry и впоследствии решил протестировать пример простого арифметического синтаксического анализатора, написав несколько более содержательный анализатор: примитивный, но правильный и функциональный анализатор HTML.
Я закончил с работой node2string
функция для работы на Node
(с атрибутами и детьми), которые я тогда inverse
d, чтобы получить parse
функции, как показано в статье.
Первая наивная реализация имела ошибку, что она разбирала что-либо, кроме, например, тривиального <input/>
HTML-фрагмент ровно в один Node
представление; все остальное недетерминировано привело к недействительным вещам, таким как
Node { name = "input", attrs = [Attr "type" "submit"] }
Node { name = "input type=\"submit\"", attrs = [] }
и так далее.
После некоторых первоначальных наивных попыток исправить это изнутри node2string
Я понял момент, который, я полагаю, все опытные программисты логики мгновенно видят, что parse = inverse node2string
был прав, более прав и проницателен в отношении ситуации, чем я: приведенные выше 2 анализа результатов <input type="submit"/>
действительно были именно 2 действительные и конструктивные значения Node
это привело бы к представлениям HTML.
Я понял, что должен был сдерживать Node
разрешить только передачу в алфавитном порядке - ну не совсем, но давайте будем проще - имена (и, конечно, то же самое для Attr
). В менее фундаментальной обстановке, чем логическая программа (например, обычная программа Haskell с гораздо большим количеством написанного от руки и "инструктивного" в отличие от чисто декларативного программирования), я бы просто скрыл Node
конструктор позади, например, mkNode
функция часового, но у меня есть ощущение, что это не сработает в Карри из-за того, как работают механизм логического вывода или решатель ограничений (я могу ошибаться, и на самом деле надеюсь, что да).
Таким образом, я закончил со следующим. Я думаю, что метапрограммирование Curry (или Template Haskell, если Curry поддерживает его) может быть использовано для очистки ручной установки, но косметический подход - это только один выход из ситуации.
data Name = Name [NameChar] -- newtype crashes the compiler
data NameChar = A | B | C | D | E | F | G | H | I | J | K | L | M | N | O | P | Q | R | S | T | U | V | W | X | Y | Z
name2char :: NameChar -> Char
name2char c = case c of A -> 'a'; B -> 'b'; C -> 'c'; D -> 'd'; E -> 'e'; F -> 'f'; G -> 'g'; H -> 'h'; I -> 'i'; J -> 'j'; K -> 'k'; L -> 'l'; M -> 'm'; N -> 'n'; O -> 'o'; P -> 'p'; Q -> 'q'; R -> 'r'; S -> 's'; T -> 't'; U -> 'u'; V -> 'v'; W -> 'w'; X -> 'x'; Y -> 'y'; Z -> 'z'
name2string :: Name -> String
name2string (Name s) = map name2char s
-- for "string literal" support
nameFromString :: String -> Name
nameFromString = inverse name2string
data Node = Node { nodeName :: Name, attrs :: [Attr], children :: [Node] }
data Attr = Attr { attrName :: Name, value :: String }
attr2string :: Attr -> String
attr2string (Attr name value) = name2string name ++ "=\"" ++ escape value ++ "\""
where escape = concatMap (\c -> if c == '"' then "\\\"" else [c])
node2string :: Node -> String
node2string (Node name attrs children) | null children = "<" ++ name' ++ attrs' ++ "/>"
| otherwise = "<" ++ name' ++ attrs' ++ ">" ++ children' ++ "</" ++ name' ++ ">"
where name' = name2string name
attrs' = (concatMap ((" " ++) . attr2string) attrs)
children' = intercalate "" $ map (node2string) children
inverse :: (a -> b) -> (b -> a)
inverse f y | f x =:= y = x where x free
parse :: String -> Node
parse = inverse node2string
Это, на самом деле, работает отлично (по моему мнению):
Parser> parse "<input type=\"submit\"/>"
(Node [I,N,P,U,T] [(Attr [T,Y,P,E] "submit")] [])
Parser> parse "<input type=\"submit\" name=\"btn1\"/>"
(Node [I,N,P,U,T] [(Attr [T,Y,P,E] "submit"),(Attr [N,A,M,E] "btn1")] [])
(У карри нет классов типов, поэтому я пока не знаю, как сделать [NameChar]
печатай приятнее)
Тем не менее, мой вопрос:
есть ли способ использовать что-то вроде isAlpha
(или, конечно, функция, более верная фактической спецификации HTML) для достижения результата, эквивалентного этому, без необходимости проходить через подробный шаблон, который NameChar
а его "поддерживающие члены" есть? Кажется, нет никакого способа даже поместить "функциональное ограничение" где-либо в ADT.
На языке функционально-логического программирования с зависимой типизацией я бы просто выразил ограничение на уровне типов и позволил бы механизму логического вывода или решателю ограничений справиться с ним, но здесь я, похоже, растерялся.
1 ответ
Вы можете достичь тех же результатов, используя только Char
, Как вы уже указали, вы можете использовать isAlpha
определить name2char
как частичная личность. Я изменил следующие строки вашего кода.
type NameChar = Char
name2char :: NameChar -> Char
name2char c | isAlpha c = c
Два примерных выражения затем оценивают следующим образом.
test> parse "<input type=\"submit\" name=\"btn1\"/>"
(Node (Name "input") [(Attr (Name "type") "submit"),(Attr (Name "name") "btn1")] [])
test> parse "<input type=\"submit\"/>"
(Node (Name "input") [(Attr (Name "type") "submit")] [])
Как побочный эффект, имена с не-альфа-символами молча терпят неудачу с nameFromString
,
test> nameFromString "input "
Изменить: так как вы, кажется, поклонник шаблонов функций, вы можете определить генераторы для Node
с и Attr
и использовать их в вашей функции преобразования.
attr :: Name -> String -> Attr
attr name val
| name `elem` ["type", "src", "alt", "name"] = Attr name val
node :: String -> [Attr] -> [Node] -> Node
node name [] nodes
| name `elem` ["a", "p"] = Node name [] nodes
node name attrPairs@(_:_) nodes
| name `elem` ["img", "input"] = Node name attrPairs nodes
node2string :: Node -> String
node2string (node name attrs children)
| null children = "<" ++ name ++ attrs' ++ "/>"
| otherwise = "<" ++ name ++ attrs' ++ ">"
++ children' ++ "</" ++ name' ++ ">"
where
name' = name
attrs' = concatMap ((" " ++) . attr2string) attrs
children' = intercalate "" $ map (node2string) children
attr2string :: Attr -> String
attr2string (attr name val) = name ++ "=\"" ++ escape val ++ "\""
where
escape = concatMap (\c -> if c == '"' then "\\\"" else [c])
Этот подход имеет свои недостатки; он работает довольно хорошо для определенного набора допустимых имен, но с треском проваливается, когда вы используете предикат, как раньше (например, all isAlpha name
).
Edit2: Помимо того, что решение с isAlpha
условие довольно "красивее", чем ваше подробное решение, оно также определяется декларативным способом. Без ваших комментариев не станет ясно (это легко), что вы кодируете буквенные символы NameChar
тип данных. isAlpha
условие, с другой стороны, является хорошим примером для декларативной спецификации требуемого свойства. Отвечает ли это на ваш вопрос? Я не уверен, к чему ты клонишь.