Haskell/Parsec:如何使用Text.Parsec.Inert中的函数?

人气:243 发布:2022-10-16 标签: parsing indentation haskell parsec

问题描述

我在解决如何使用indentsHaskell程序包提供的Text.Parsec.Indent模块中的任何函数时遇到问题,该程序包是Parsec的一种加载项。

所有这些函数都有什么作用?如何使用它们?

我可以理解withBlock的简短Haddock描述,我找到了如何使用withBlockrunIndentIndentParsertypehere、here和here的示例。我还可以理解四个解析器indentBrackets and friends的文档。但很多事情仍然困扰着我。

特别是:

withBlock f a p

有什么区别
do aa <- a
   pp <- block p
   return f aa pp

同样,withBlock' a pdo {a; block p}

有什么区别

在函数族indented and friends中,引用的级别是什么?也就是说,什么是‘参考’?

功能indented和好友,怎么用呢?除了withPos,它们看起来不带参数,并且都是IParser ()类型(IParser的定义类似于this或this),所以我猜它们所能做的就是产生错误或不产生错误,它们应该出现在do块中,但我不知道细节。

我至少在source code中找到了一些关于withPos用法的例子,所以如果我盯着它看足够长的时间,我大概可以弄清楚。

<+/>提供了一个有用的描述"<+/>对于缩进敏感的解析器就像ap对于monad一样",如果您想要花几个会话来尝试理解ap,然后弄清楚这是如何类似于解析器的,这是很棒的。然后参考<+/>定义其他three combinators,使得新手无法接近整个组。

我需要使用这些吗?我可以忽略它们而改用do吗?

/li>

来自Parsec的普通lexeme组合器和whiteSpace解析器将愉快地在多令牌构造的中间使用换行符,而不会有任何抱怨。但是在缩进风格的语言中,有时您希望停止解析词法构造,或者如果一行被打断并且下一行缩进小于应有的缩进,则抛出错误。如何在Parsec中执行此操作?

在我正在尝试解析的language中,理想情况下,允许词法结构继续到下一行的规则应该取决于在第一行的末尾或后续行的开头出现的标记。在Parsec中有实现这一点的简单方法吗?(如果这很困难,那么我现在不需要关心它。)

推荐答案

因此,第一个提示是查看IndentParser

type IndentParser s u a = ParsecT s u (State SourcePos) a

,即它是一个特别关注SourcePosParsecT,这是一个抽象容器,可用于访问当前列编号等。因此,它可能将当前的"缩进级别"存储在SourcePos中。这是我对"参考水平"的初步猜测。

简而言之,indents为您提供了一种新的Parsec,它是上下文敏感的-特别是对当前缩进敏感。我将不按顺序回答您的问题。

(2)"引用级别"是在该缩进级别开始的当前解析器上下文状态中引用的"信念"。为了更清楚,让我在(3)上给出一些测试用例。

(3)为了开始试验这些函数,我们将构建一个小的测试运行程序。它将使用我们提供的字符串运行解析器,然后使用我们要修改的initialPos解包内部的State部分。在代码中

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。通过检查来源,我可以断定它做了两件事。首先,如果当前SourcePos列号小于或等于SourcePosSourcePos中存储的SourcePos中存储的State中存储的"引用级别",它将fail。其次,它神秘地将StateSourcePos的行计数器(而不是列计数器)更新为最新。

据我所知,只有第一种行为是重要的。我们可以在这里看到区别。

>>> 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-组合器--它将输入IndentParser转换为认为"引用列"(State中的SourcePos)正是我们调用withPos时的位置。

这给了我们另一个提示,顺便说一句。它让我们知道我们有权更改引用列。

现在让我们看看blockwithBlock是如何使用我们新的、较低级别的引用列运算符工作的。withBlock是按照block实现的,所以我们从block开始。

-- simplified from the actual source
block p = withPos $ many1 (checkIndent >> p)
因此,block将"Reference Column"重置为任何当前列,然后使用p中的至少1个解析,只要每个解析的缩进与新设置的"Reference Column"相同。现在我们可以看看withBlock

withBlock f a p = withPos $ do
  r1 <- a
  r2 <- option [] (indented >> block p)
  return (f r1 r2)
因此,它将"Reference Column"重置为当前列,解析单个a解析,尝试解析indentedblock,然后使用f组合结果。您的实现几乎正确,只是您需要使用withPos选择正确的"引用列"。

然后,一旦您拥有withBlockwithBlock' = withBlock (\_ bs -> bs)

(5)so、indented和Friends正是执行此操作的工具:如果相对于withPos选择的"参考位置"缩进不正确,它们将导致解析立即失败。

(4)是的,在您学习如何使用Applicative style基本语法分析Parsec之前,不要担心这些家伙。它通常是一种更干净、更快、更简单的指定解析的方法。有时它们甚至更强大,但如果您理解Monad,那么它们几乎总是完全等价的。

(6)这就是症结所在。到目前为止提到的工具只有在您可以使用withPos描述您想要的缩进时才会导致缩进失败。快速地说,我认为不可能根据其他解析的成功或失败来指定withPos...所以你还得再深入一层。幸运的是,使IndentParser工作的机制是显而易见的--它只是一个包含SourcePos的内部State单体。您可以使用lift :: MonadTrans t => m a -> t m a操作此内部状态,并随心所欲地设置"Reference"列。

干杯!

154