在 Haskell 中使用 Parsec 解析字符串时如何保留阶乘计算?

How to preserve factorial computing when parsing string with Parsec in Haskell?

我 运行 在 Haskell 中尝试使用 Text.Parsec 解决阶乘输出计算时遇到困难。让我们看看到目前为止我使用的一些代码:

import Text.Parsec hiding(digit)
import Data.Functor

type CalcParser a = CalcParsec String () a

digit :: CalcParser Char
digit = oneOf ['0'..'9']

number :: CalcParser Double
number = try calcDouble <|> calcInt  

calcInt :: CalcParser Double 
calcInt = read <$> many1 digit

calcDouble :: CalcParser Double 
calcDouble = do
    integ <- many1 digit
    char '.'
    decim <- many1 digit
    pure $ read $ integ <> "." <> decim

numberFact :: CalcParser Integer     
numberFact = read <$> many1 digit

applyMany :: a -> [a -> a] -> a
applyMany x [] = x
applyMany x (h:t) = applyMany (h x) t

div_ :: CalcParser (Double -> Double -> Double)
div_= do
    char '/'
    return (/)

star :: CalcParser (Double -> Double -> Double)
star = do
    char '*'    
    return (*)  

plus :: CalcParser (Double -> Double -> Double)
plus = do
    char '+'    
    return (+)

minus :: CalcParser (Double -> Double -> Double)
minus = do
    char '-'    
    return (-)  

multiplication :: CalcParser Double
multiplication = do
    spaces
    lhv <- atom <|> fact' <|> negation'
    spaces
    t <- many tail
    return $ applyMany lhv t
    where tail =
                do
                    f <- star <|> div_
                    spaces
                    rhv <- atom <|> fact' <|> negation'
                    spaces
                    return (`f` rhv)

atom :: CalcParser Double
atom = number <|> do
    char '('
    res <- addition
    char ')'
    return res

fact' :: CalcParser Double
fact' = do
    spaces
    rhv <- numberFact
    char '!'
    return $ factorial rhv

factorial :: Integer -> Double
factorial n
    | n < 0 = error "No factorial exists for negative inputs" 
    | n == 0 || n == 1 = 1
    | otherwise = acc n 1
    where
    acc 0 a = a
    acc b a = acc (b-1) (fromIntegral b * a) 

negation' :: CalcParser Double
negation' = do
    spaces
    char '~'
    rhv <- atom
    spaces
    return $ negate rhv         

addition :: CalcParser Double
addition = do
    spaces
    lhv <- multiplication <|> fact' <|> negation'
    spaces
    t <- many tail
    return $ applyMany lhv t
    where tail =
                do
                    f <- plus <|> minus 
                    spaces
                    rhv <- multiplication <|> fact' <|> negation'
                    spaces
                    return (`f` rhv)

这是一种简单的计算器解析器,提供加法/减法/阶乘处理。我将让输入字符串块的任何阶乘链接组件,以便它们中的每一个都包含一些数字,后跟“!”字符(通常在大多数现实世界的计算器中)。但是,当 运行 将解析器测试为:

parseTest addition "3!"

它无法计算阶乘本身,但是 returns 3.0(输出显示为浮点数,这就是我在此程序中使用 CalcParser Double 的原因)。一个奇怪的事实是,每当我设置 '!'在数字之前:

fact' :: CalcParser Double
fact' = do
    spaces
    char '!'
    rhv <- numberFact
    return $ factorial rhv

结果:

parseTest addition "!3"

符合我的预期,即它等于 6.0。在那之后,我假设第一个版本的东西有一些挫折,每当'!'时都无法运行阶乘计算。是一些类似数字的输入元素的旁边。这只是我在这里寻找任何帮助解决的案例。那么,右侧的 '!' 有什么问题?您将如何处理所描述的问题?

普遍的问题是 Parsec 在找到第一个可行的替代方案时停止尝试替代方案。如果你写:

parseTest (atom <|> fact') "3!"

这将导致 3.0。这是因为 atom 解析器成功解析了字符串 "3" 的初始部分,所以 Parsec 甚至不会尝试 fact' 解析器。它将字符串的其余部分 "!" 留给另一个稍后的解析器来处理。

在上面的解析器代码中,如果您尝试解析:

parseTest addition "3!"

addition 解析器首先尝试 multiplication 解析器。 multiplication 解析器有一行:

lhv <- atom <|> fact' <|> negation'

所以它首先尝试 atom 解析器。这个解析器工作正常 returns 3.0,所以它永远不会尝试 fact'negation.

要修复您的解析器,您需要确保在尝试 fact' 之前,您没有成功地解析替代方案中的 atom。您可以先切换顺序:

> parseTest (fact' <|> atom) "3!"
6.0

这可以很好地解析 "3!"(并给出 6.0),但如果您尝试解析其他内容,它会失败:

> parseTest (fact' <|> atom) "3"
parse error at (line 1, column 2):
unexpected end of input
expecting "!"

这是因为,出于效率原因,Parsec 不会自动 "backtrack"。如果您尝试解析某些东西并且它在失败之前 "consumes input" ,它会完全失败而不是尝试更多的选择。在这里,fact' 首先调用 numberFact,它成功地 "consumes" "3",然后它尝试 char '!',但失败了。因此,fact "fails after consuming input" 会立即导致解析错误。

您可以使用 try 函数覆盖此行为:

> parseTest (try fact' <|> atom) "3"
3.0
> parseTest (try fact' <|> atom) "3!"
6.0

此处 try fact' 应用解析器 fact' 但将 "fail after consuming input" 视为 "fail after consuming no input",因此可以检查其他替代方案。

如果您将此更改应用于 multiplication 解析器中的两个位置:

multiplication :: CalcParser Double
multiplication = do
    spaces
    lhv <- try fact' <|> atom <|> negation'
    spaces
    t <- many tail
    return $ applyMany lhv t
    where tail =
                do
                    f <- star <|> div_
                    spaces
                    rhv <- try fact' <|> atom <|> negation'
                    spaces
                    return (`f` rhv)

并对您的 addition 解析器进行了类似的更改:

addition :: CalcParser Double
addition = do
    spaces
    lhv <- try fact' <|> multiplication <|> negation'
    spaces
    t <- many tail
    return $ applyMany lhv t
    where tail =
                do
                    f <- plus <|> minus
                    spaces
                    rhv <- try fact' <|> multiplication <|> negation'
                    spaces
                    return (`f` rhv)

那么效果会更好:

> parseTest addition "3!"
6.0
> parseTest addition "3"
3.0
> parseTest addition "3+2*6!"
1443.0

eof 解析器添加到测试中也是一个好主意,以确保您没有任何未解析的尾随垃圾:

> parseTest addition "3  Hey, I could write anything here"
3.0
> parseTest (addition <* eof) "3  but adding eof will generate an error"
parse error at (line 1, column 4):
unexpected 'b'
expecting space, "*", "/", white space, "+", "-" or end of input