我最近完成了A Taste of Curry,然后决定通过编写一个更充实的解析器来测试这个简单的算术解析器示例:一个原始但正确且功能强大的 HTML 解析器。
我最终得到了一个node2string
可以操作的工作函数Node
(带有属性和子项),然后我inverse
将获得一个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 中无法正常工作(我可能在这方面错了,实际上我希望是这样)。
所以我最终得到了以下结果。我认为 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")] [])
(Curry 没有类型类,所以我还不知道如何[NameChar]
更好地打印)
但是,我的问题是:
有没有办法使用类似的东西isAlpha
(或者更符合实际 HTML 规范的函数,当然)来实现与此等效的结果,而不必通过冗长的样板NameChar
及其“支持成员”?似乎没有办法在 ADT 中的任何地方放置“功能限制”。
在依赖类型的函数逻辑编程语言中,我只会在类型级别表达约束并让推理引擎或约束求解器处理它,但在这里我似乎不知所措。