我在解决如何使用Haskell的 Text.Parsec.Indent
软件包提供的 indents
模块中的任何功能方面遇到麻烦,这是Parsec的一种附加组件。
所有这些功能做什么?如何使用它们?
我可以理解withBlock
的简短Haddock描述,并且找到了有关如何使用withBlock
,runIndent
和IndentParser
类型here,here和here的示例。我还可以了解四个解析器 indentBrackets
and friends的文档。但是很多事情仍然让我感到困惑。
特别是:
withBlock f a p
和do aa <- a
pp <- block p
return f aa pp
同样,
withBlock' a p
和do {a; block p}
indented
and friends函数家族中,“引用水平”是什么?也就是说,什么是“引用”? indented
和 friend 功能,如何使用它们?除了withPos
,它们似乎没有参数,并且都是IParser ()
类型(IParser定义为this或this),所以我猜测它们所能做的就是是否产生错误,并且应该出现在do
块,但我无法弄清楚细节。我至少在source code中找到了有关
withPos
用法的一些示例,因此,如果我凝视了足够长的时间,我大概可以弄清楚。 <+/>
附带了一个有用的描述:“<+/>
是缩进敏感的解析器,什么ap
是monads”,如果您想花费几个 session 尝试将头围绕ap
,然后弄清楚它与解析器的相似之处,则非常有用。然后引用<+/>
定义另一个three combinators,从而使整个组都不适合新来者。我需要使用这些吗?我可以不理会它们而改用
do
吗? lexeme
组合器和 whiteSpace
解析器将在多 token 结构中间愉快地使用换行符而不会提示。但是在缩进风格的语言中,有时您希望停止解析词法构造,或者如果一行被折断并且下一行的缩进少于应有的范围,则抛出错误。我该如何在Parsec中执行此操作? 最佳答案
所以,第一个提示是看IndentParser
type IndentParser s u a = ParsecT s u (State SourcePos) a
IE。这是一个
ParsecT
,它特别关注 SourcePos
(一个抽象容器),该容器可用于访问(除其他外)当前列号。因此,它可能会将当前的“缩进级别”存储在SourcePos
中。那是我对“引用水平”的含义的最初猜测。简而言之,
indents
为您提供了一种新的Parsec
,它与上下文有关,尤其是对当前缩进敏感。我会不按顺序回答您的问题。(2)“引用级别”是在此缩进级别开始的当前解析器上下文状态中引用的“信仰”。更清楚地说,让我在(3)上给出一些测试用例。
(3)为了开始尝试这些功能,我们将构建一个小的测试运行器。它将使用我们提供的字符串运行解析器,然后使用我们要修改的
State
解开内部initialPos
部分。在代码中import Text.Parsec
import Text.Parsec.Pos
import Text.Parsec.Indent
import Control.Monad.State
testParse :: (SourcePos -> SourcePos)
-> IndentParser String () a
-> String -> Either ParseError a
testParse f p src = fst $ flip runState (f $ initialPos "") $ runParserT p () "" src
(请注意,这几乎是
runIndent
,除了我给后门修改initialPos
。)现在我们来看看
indented
。通过检查源,我可以知道它做了两件事。首先,如果当前fail
列号小于或等于存储在SourcePos
中的SourcePos
中存储的“引用级别”,它将为State
。其次,它有点神秘地将State
SourcePos
的行计数器(不是列计数器)更新为当前。据我了解,只有第一个行为才是重要的。我们可以在这里看到区别。
>>> testParse id indented ""
Left (line 1, column 1): not indented
>>> testParse id (spaces >> indented) " "
Right ()
>>> testParse id (many (char 'x') >> indented) "xxxx"
Right ()
因此,为了使
indented
成功,我们需要消耗足够的空格(或其他任何东西!),以将我们的列位置压出“引用”列位置。否则,它会说“不缩进”。接下来的三个函数存在类似的行为:除非当前位置和引用位置在同一行上,否则same
失败;如果当前列严格小于引用列,除非它们在同一行上,否则sameOrIndented
失败;除非当前列和引用位置在同一行上,否则checkIndent
失败。当前列和引用列匹配。withPos
略有不同。它不仅是IndentParser
,而且是IndentParser
-combinator,它将输入的IndentParser
转换为认为“引用列”(即SourcePos
中的State
)与我们称为withPos
的位置完全相同的代码。顺便说一句,这给了我们另一个提示。它使我们知道我们有权更改引用列。
(1)现在,让我们看一下使用新的较低级别的引用列运算符
block
和withBlock
的工作方式。 withBlock
是根据block
实现的,因此我们从block
开始。-- simplified from the actual source
block p = withPos $ many1 (checkIndent >> p)
因此,
block
将“引用列”重置为当前列,然后从p
消耗至少1个解析,只要每个缩进都与新设置的“引用列”相同。现在我们来看看withBlock
withBlock f a p = withPos $ do
r1 <- a
r2 <- option [] (indented >> block p)
return (f r1 r2)
因此,它将“引用列”重置为当前列,解析单个
a
解析,尝试解析indented
的block
p
,然后使用f
合并结果。您的实现几乎是正确的,除了您需要使用withPos
选择正确的“引用列”。然后,一旦有了
withBlock
,withBlock' = withBlock (\_ bs -> bs)
。(5)因此,
indented
和friends正是执行此操作的工具:如果解析不正确地相对于withPos
选择的“引用位置”缩进,它们将导致解析立即失败。(4)是的,在您学习如何在基础
Applicative
中使用 Parsec
style解析之前,请不要担心这些家伙。它通常是一种更简洁,更快,更简单的解析方法。有时它们甚至更强大,但是如果您了解Monad
,那么它们几乎总是完全等效的。(6)这就是症结所在。到目前为止提到的工具只有在您可以使用
withPos
描述您想要的缩进时,才可以缩进失败。很快,我认为不可能根据其他解析的成功或失败来指定withPos
,因此您必须更进一步。幸运的是,使IndentParser
起作用的机制是显而易见的-它只是一个内部State
monad,其中包含SourcePos
。您可以使用lift :: MonadTrans t => m a -> t m a
操纵此内部状态,并根据需要设置“引用列”。干杯!