【问题标题】:Parsing function in Haskell, trouble making a map functionHaskell中的解析功能,制作地图功能时遇到麻烦
【发布时间】:2019-10-04 07:01:14
【问题描述】:

我是 Haskell 的新手,正在做一个任务,我正在尝试为一种简单的计算器语言制作解析函数。

我得到了一个语法,我不能改变它。我试图通过遍历字符串并递归地使用我的解析函数来解决它。

语法应该是

Expr -> Int | -Expr | + Expr Expr | * Expr Expr
Int -> Digit | Digit Int 
Digit -> 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9

所以我的函数将 Expr 语言中的字符串作为参数,并以这种格式生成抽象语法树

data Ast = Tall Int | Sum Ast Ast | Mult Ast Ast| Min Ast| Var String deriving (Eq, Show)

Ast 应该是一个抽象语法树

这就是我到目前为止在解析函数中得到的结果

parseEx :: [String] -> (Ast, [String])
parseEx [] = error "empty string"
parseEx (s:ss) | all isDigit s = (Tall (read s), ss)
               | s == "-"      = let (ast, ss') = parseEx ss in (Min ast, ss') 
               | s == "+"      = let (ast, ss'), let(ast',ss'') = parseEx ss in (Sum ast ast', ss') parseEx ss' (ast', ss'')  
               | s == "*"      = (Mult ast ast', ss'') where
                   (ast, ss'')   = parseEx ss'
                   (ast', ss''') = parseEx ss'' 

我可以清楚地看到+ 的条件是错误的,我不能有两个let。我也有点迷失在所有这些列表中。我在想map-function 可能是我的问题的解决方案,也许它会让我的代码看起来更整洁。但我不确定如何开始,因为它必须采用[String]->Ast 的形式。简单地坚持我拥有的代码并尝试使其工作更容易吗?

【问题讨论】:

  • @n.m.好点子。 Vpe,你能澄清一下这种语言的输入字符串应该是什么样子吗?
  • @luqui 显然它确实应该是波兰符号的前缀。奇怪的。这是一种非常容易解析的语言,但不是最容易使用的语言。
  • 抱歉不清楚。例如,输入可能看起来像“+ 5 6”,所以是的,我想它是前缀波兰表示法。我不知道这个词,所以谢谢你:)

标签: string parsing haskell abstract-syntax-tree


【解决方案1】:

parseEx 只能返回 两个 东西,根据类型签名,所以

let (ast, ast', ss') = parseEx ...

没有意义。您需要链接 lets——也就是说,将一个绑定在一个变量中的变量提供给下一个解析的输入:

let (ast, ss') = parseEx ss
    (ast', ss'') = parseEx ss'
in ...

(确保让 let 的子句对齐,这在 Haskell 中很重要!)

请注意我们如何将第一个解析的余数 ss' 作为第二个解析的输入。这表示“从ss 解析一个AST,然后将ss' 中的字符串的剩余部分还给我;在剩余部分中,再解析一个AST”。

仔细考虑在解析完整的+-表达式后您将返回什么余数。

另外,由于这个功能相当复杂,我建议开发它你在周围撒上undefined,以便让它一点一点地进行类型检查。例如,以

开头
parseEx :: [String] -> (Ast, [String])
parseEx [] = error "empty string"
parseEx (s:ss) | all isDigit s = (Tall (read s), ss)
               | otherwise     = undefined

编译它,(修复它),并在ghci 中测试它(解释结果可能需要对undefined 和懒惰有一点细微的了解——但它也会建立这种直觉)。然后做下一个子句,编译,测试,冲洗,重复。

【讨论】:

    【解决方案2】:

    我不知道如何开始,因为它必须是 [String] -> Ast 的形式。

    简单地坚持我拥有的代码并尝试使其工作更容易吗?

    一件事是您导出的类型签名,另一件事是您的内部实现。

    例如,您可以像这样构建解析器:

    parseEx :: String -> Ast
    parseEx s = parseTokens (tokenize s [])
    
    data Token = TokDigit Int | TokPlus | TokMinus | TokMult
    type DigitStack = String
    
    tokDigit :: DigitStack -> Token
    tokDigit s = TokDigit (read (reverse s))
    
    tokenize :: String -> DigitStack -> [Token]
    tokenize [] digits =
      if null digits
      then []
      else [tokDigit digits]
    
    tokenize (c:cs) digits
      | isDigit c = tokenize cs (c:digits)
      | not (null digits) = tokDigit digits : tokenize (c:cs) []
      | isSpace c = tokenize cs digits
      | otherwise = case c of
          '+' -> TokPlus : tokenize cs digits
          '-' -> TokMinus : tokenize cs digits
          '*' -> TokMult : tokenize cs digits
          _ -> error ("Unknown symbol " + show c)
    
    parseTokens :: [Token] ->  Ast
    parseTokens (t:ts) = ...
    

    并不是说标记化对于您的简单语法是绝对必要的,但关键是您的解析器的内部表示不必受[String] -> Ast 签名的限制。您甚至可以使用 Megaparsec 之类的解析器组合库,并且仍然导出函数 [String] -> Ast

    【讨论】:

    • 我相信你想要| not (null digits) = tokDigit digits : tokenize (c : cs) [],这样一个非数字字符就会终止一个数字,但不会消耗那个字符。例如。 +1*2 3 将错误地消耗 *。你的| otherwise = case c of 保护看起来也应该被移除,否则c == … 保护变成case 分支。
    • @JonPurdy:感谢您的关注。看来我做的很匆忙。
    猜你喜欢
    • 2018-10-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-10-30
    • 1970-01-01
    • 2022-10-18
    • 1970-01-01
    相关资源
    最近更新 更多