来自Haskell中解析器组合子的异常消息无效

46qrfjad  于 2022-11-30  发布在  其他
关注(0)|答案(1)|浏览(158)

我正在学习使用Haskell语言的函数式编程。作为练习,我需要实现一个函数来解析String中的原始算术表达式。该函数必须能够处理双精度字面值、具有通常优先级和括号的运算+-*/

parseExpr :: String -> Except ParseError Expr

具有下一个定义的数据类型:

data ParseError = ErrorAtPos Natural
  deriving Show

newtype Parser a = P (ExceptState ParseError (Natural, String) a)
  deriving newtype (Functor, Applicative, Monad)

data Prim a
  = Add a a 
  | Sub a a 
  | Mul a a 
  | Div a a 
  | Abs a   
  | Sgn a
  deriving Show

data Expr
  = Val Double      
  | Op (Prim Expr)  
  deriving Show

其中ExceptState是修改后的State单子,允许引发指向错误位置的异常。

data Annotated e a = a :# e
  deriving Show
infix 0 :#

data Except e a = Error e | Success a 
  deriving Show

data ExceptState e s a = ES { runES :: s -> Except e (Annotated s a) }

此外,ExceptState还定义了FunctorApplicativeMonad示例,这些示例在前面已经过全面测试,因此我对它们的正确性持肯定态度。

instance Functor (ExceptState e s) where
  fmap func ES{runES = runner} = ES{runES = \s ->
    case (runner s) of
      Error err   -> Error err
      Success ans -> Success (mapAnnotated func $ ans) }

instance Applicative (ExceptState e s) where
  pure arg = ES{runES = \s -> Success (arg :# s)}
  p <*> q = Control.Monad.ap p q

instance Monad (ExceptState e s) where
  m >>= f = joinExceptState (fmap f m)
    where
      joinExceptState :: ExceptState e s (ExceptState e s a) -> ExceptState e s a
      joinExceptState ES{runES = runner} = ES{runES = \s ->
        case (runner s) of
          Error err -> Error err
          Success (ES{runES = runner2} :# s2) ->
            case (runner2 s2) of
              Error err           -> Error err
              Success (res :# s3) -> Success (res :# s3) }

为了实现函数parseExpr,我使用了基本的解析器组合子:

pChar :: Parser Char
pChar = P $ ES $ \(pos, s) ->
  case s of
    []     -> Error (ErrorAtPos pos)
    (c:cs) -> Success (c :# (pos + 1, cs))

parseError :: Parser a
parseError = P $ ES $ \(pos, _) -> Error (ErrorAtPos pos)

instance Alternative Parser where
  empty = parseError

  (<|>) (P(ES{runES = runnerP})) (P(ES{runES = runnerQ})) =
    P $ ES $ \(pos, s) ->
      case runnerP (pos, s) of
        Error _     -> runnerQ (pos, s)
        Success res -> Success res

instance MonadPlus Parser

用来构建更复杂的模型:

-- | elementary parser not consuming a character, failing if input doesn't
-- reach its end
pEof :: Parser ()
pEof = P $ ES $ \(pos, s) ->
  case s of
    [] -> Success (() :# (pos, []))
    _  -> Error $ ErrorAtPos pos

-- | parses a single digit value
parseVal :: Parser Expr
parseVal = Val <$> (fromIntegral . digitToInt) <$> mfilter isDigit pChar

-- | parses an expression inside parenthises
pParenth :: Parser Expr
pParenth = do
  void $ mfilter (== '(') pChar
  expr <- parseAddSub
  (void $ mfilter (== ')') pChar) <|> parseError
  return expr

-- | parses the most prioritised operations
parseTerm :: Parser Expr
parseTerm = pParenth <|> parseVal

parseAddSub :: Parser Expr
parseAddSub = do
  x <- parseTerm
  ys <- many parseSecond
  return $ foldl (\acc (sgn, y) -> Op $
    (if sgn == '+' then Add else Sub) acc y) x ys

  where
    parseSecond :: Parser (Char, Expr)
    parseSecond = do
      sgn <- mfilter ((flip elem) "+-") pChar
      y <- parseTerm <|> parseError
      return (sgn, y)

-- | Parses the whole expression. Begins from parsing on +, - level and
-- successfully consuming the whole string.
pExpr :: Parser Expr
pExpr = do
  expr <- parseAddSub
  pEof
  return expr

-- | More convinient way to run 'pExpr' parser
parseExpr :: String -> Except ParseError Expr
parseExpr = runP pExpr

因此,如果给定的String表达式有效,则此时函数按预期工作:

ghci> parseExpr "(2+3)-1"
Success (Op (Sub (Op (Add (Val 2.0) (Val 3.0))) (Val 1.0)))
ghci> parseExpr "(2+3-1)-1"
Success (Op (Sub (Op (Sub (Op (Add (Val 2.0) (Val 3.0))) (Val 1.0))) (Val 1.0)))

否则ErrorAtPos不指向所需位置:

ghci> parseExpr "(2+)-1"
Error (ErrorAtPos 1)
ghci> parseExpr "(2+3-)-1"
Error (ErrorAtPos 1)

我做错什么了?先谢谢你。
我的主要假设是Alternative Parser的函数(<|>)出了问题,它错误地更改了pos变量。

(<|>) (P(ES{runES = runnerP})) (P(ES{runES = runnerQ})) =
    P $ ES $ \(pos, s) ->
      case runnerP (pos, s) of
        -- Error _     -> runnerQ (pos, s)
        Error (ErrorAtPos pos')     -> runnerQ (pos' + pos, s)
        Success res -> Success res

但这导致了更奇怪的结果:

ghci> parseExpr "(5+)-3"
Error (ErrorAtPos 84)
ghci> parseExpr "(5+2-)-3"
Error (ErrorAtPos 372)

尽管我已经运行了instance Monad (ExceptState e s)joinExceptState函数,但是更多的怀疑是针对instance Monad (ExceptState e s)joinExceptState函数,怀疑它在(Natural, String)类型的s上不起作用,就像我在这个例子中缩进的那样。但是我不能只为这个具体的类型改变它。

vzgqcmou

vzgqcmou1#

非常好的问题,尽管如果它真的包含了 * 所有 * 代码的话会更好。我填补了缺失的部分:

mapAnnotated :: (a -> b) -> Annotated s a -> Annotated s b
mapAnnotated f (a :# e) = (f a) :# e

runP :: Parser a -> String -> Except ParseError a
runP (P (ES {runES = p})) s = case p (0, s) of
  Error e -> Error e
  Success (a :# e) -> Success a

为什么parseExpr "(5+)-3"等于Error (ErrorAtPos 1)?下面是发生的情况:我们调用parseExprparseExpr(最终)调用parseTermparseTerm就是pParenth <|> parseVal。当然,pParenth失败了,所以我们查看<|>的定义来解决问题。该定义表示:如果左边的东西失败了,就试试右边的东西。2所以我们试试右边的东西(也就是parseVal),它也失败了,我们报告第二个错误,实际上是在位置1。
为了更清楚地看到这一点,您可以将pParenth <|> parseVal替换为parseVal <|> pParenth,并观察到您得到的是ErrorAtPos 2
这几乎肯定不是你想要的行为。Megaparsec的p <|> qhere的文档说:
如果[parser] p失败 * 而没有消耗任何输入 *,则尝试解析器q。
(着重号在原文,意思是:在其他情况下不尝试parser q)。这是一件更有用的事情。如果你尝试解析一个带括号的表达式,并且 then 得到了一个错误,那么你可能想报告这个错误,而不是抱怨'('不是一个数字。
既然你说这是一个练习,我就不告诉你如何解决这个问题了,但我会告诉你一些其他的东西。
首先,这不是错误报告的唯一问题。(在位置0的有问题的字符 * 之后 ),而pParenth "(5+)-3"在位置2报告错误( 在有问题的字符之前 *,它在位置3)。理想情况下,两者都应该给予有问题的字符本身的位置。(当然,如果解析器声明它所期望的字符就更好了,但这更难做到。)
其次,我发现问题的方法是将import Debug.Trace的定义替换为

pChar :: Parser Char
pChar = P $ ES $ \(pos, s) -> traceShow (pos, s) $
  case s of
    []     -> Error (ErrorAtPos pos)
    (c:cs) -> Success (c :# (pos + 1, cs))

并穆尔一下输出。Debug.Trace有时并不像人们希望的那么有用,因为它的求值比较迟缓,但对于这样的程序来说,它可以帮助很大。
第三,如果修改<|>的定义以匹配Megaparsec的定义,则可能需要Megaparsec的try组合子。(不适用于 * 现在 * 要解析的语法,但以后可能会用到)try解决了

(singleChar 'p' *> singleChar 'q') <|> (singleChar 'p' *> singleChar 'r')

在具有Megaparsec的<|>的字符串“pr”上失败。
第四,你有时会写someParser <|> parseError,我认为它等价于你定义的<|>和兆秒差距的someParser
第五,你不需要void;忽略结果,这是一样的。
第六,你的Except似乎只是Either

相关问题