我正在学习使用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
还定义了Functor
、Applicative
和Monad
示例,这些示例在前面已经过全面测试,因此我对它们的正确性持肯定态度。
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
上不起作用,就像我在这个例子中缩进的那样。但是我不能只为这个具体的类型改变它。
1条答案
按热度按时间vzgqcmou1#
非常好的问题,尽管如果它真的包含了 * 所有 * 代码的话会更好。我填补了缺失的部分:
为什么
parseExpr "(5+)-3"
等于Error (ErrorAtPos 1)
?下面是发生的情况:我们调用parseExpr
,parseExpr
(最终)调用parseTerm
,parseTerm
就是pParenth <|> parseVal
。当然,pParenth
失败了,所以我们查看<|>
的定义来解决问题。该定义表示:如果左边的东西失败了,就试试右边的东西。2所以我们试试右边的东西(也就是parseVal
),它也失败了,我们报告第二个错误,实际上是在位置1。为了更清楚地看到这一点,您可以将
pParenth <|> parseVal
替换为parseVal <|> pParenth
,并观察到您得到的是ErrorAtPos 2
。这几乎肯定不是你想要的行为。Megaparsec的
p <|> q
,here的文档说:如果[parser] p失败 * 而没有消耗任何输入 *,则尝试解析器q。
(着重号在原文,意思是:在其他情况下不尝试parser q)。这是一件更有用的事情。如果你尝试解析一个带括号的表达式,并且 then 得到了一个错误,那么你可能想报告这个错误,而不是抱怨'('不是一个数字。
既然你说这是一个练习,我就不告诉你如何解决这个问题了,但我会告诉你一些其他的东西。
首先,这不是错误报告的唯一问题。(在位置0的有问题的字符 * 之后 ),而
pParenth "(5+)-3"
在位置2报告错误( 在有问题的字符之前 *,它在位置3)。理想情况下,两者都应该给予有问题的字符本身的位置。(当然,如果解析器声明它所期望的字符就更好了,但这更难做到。)其次,我发现问题的方法是将
import Debug.Trace
的定义替换为并穆尔一下输出。Debug.Trace有时并不像人们希望的那么有用,因为它的求值比较迟缓,但对于这样的程序来说,它可以帮助很大。
第三,如果修改
<|>
的定义以匹配Megaparsec的定义,则可能需要Megaparsec的try
组合子。(不适用于 * 现在 * 要解析的语法,但以后可能会用到)try
解决了在具有Megaparsec的
<|>
的字符串“pr”上失败。第四,你有时会写
someParser <|> parseError
,我认为它等价于你定义的<|>
和兆秒差距的someParser
。第五,你不需要
void
;忽略结果,这是一样的。第六,你的
Except
似乎只是Either
。