孜孜不倦的程序员

分析器组合子

Ted Neward

Ted Neward
Multiparadigmatic 系列的结论,似乎时间去闯到新的地面。命运得很,不过,一些客户端工作最近离开我的有趣的材料负有的讨论,涉及到软件的设计,作为另一个例子的通用性/变异性分析为核心的 multiparadigmatic 系列,和 … … 嗯,最终,它是真的很酷。

问题

我有客户端是光神经科学世界的脖子深又问我和他一起工作的一个新的项目,旨在方便做实验对光学组织。具体来说,我正在控制 rig 显微镜,它将推动各种刺激设备 (指示灯、 灯等等),将触发响应从光学组织,然后捕获结果测定看光学组织的硬件软件系统。

如果这一切隐约听矩阵-y 给你,你不是完全孤独的。当我第一次听说这个项目,我的反应是同时,"哦,哇,真酷 !","哦,等等,我只是吐了我嘴里有点。"

无论如何,对钻井平台的关键内容之一是,它将有相当复杂的配置与运行,每个实验关联和导致我们仔细考虑如何指定该配置。一方面,它似乎明显的问题,为 XML 文件。但是,平台运营的人们不会去是计算机程序员,而是科学家和实验室助理,所以似乎有点严厉期望他们写格式正确的 XML 文件 (和每次都得到正确)。思想的产生某种形式的基于 GUI 的配置系统达成我们作为高度 over-engineered,特别是因为它会很快变成讨论如何最有效地捕获的数据的类型不限成员名额。

最后,似乎更合适,给他们一种自定义配置格式,这意味着吨分析我的部分文本。(对某些人来说,这意味着我建筑的 DSL ; 这是最好的一场辩论左哲学家和酒精消费严重任务中涉及的其他)。幸运的是,解决方案比比皆是,在这一领域。

思想

解析器是有趣且有用的两个目的: 将文本转换为一些其他的、 更有意义的形式,并验证/验证文本将按照某些结构 (这通常是把它转换成一种更有意义的形式帮助的一部分)。例如,一个电话号码,这只是一个数字序列的核心是,所以,仍有需要验证,它的结构。这种格式差异从大陆到大陆,但这些数字仍然是数字。事实上,电话号码是"更有意义的形式"并不是一个整数值的情况的一个伟大的例子 — — 数字并不是一个整数值,它们通常是更好地代表作为一种域类型的符号表示。(视其为"只是"数量很难提取的国家/地区代码或地区代码,例如。)

如果一个电话号码组成的数字,所以,数字 (薪俸,雇员 Id 等等),然后那里将会在代码中,我们分析和验证数字,一些重复,除非我们某种程度上扩展了解析器。然后,这意味着,我们希望无论我们建立不限成员名额,要让一个人来扩展它以不同的方式 (加拿大邮政编码,例如) 而无需修改源本身中使用解析器/库的解析器。这被称为"打开关闭原则": 软件实体应该对扩展开放,但对修改关闭。

Solution: Generative Metaprogramming

一种解决方案是传统"lex/yacc"的做法,已知更正式的"解析器发电机"。这种抽象的格式指定配置文件的语法 — — 通常一些变化对巴科斯范式形成 (BNF) 语法/语法用于描述形式的语法,如什么大多数编程语言的使用 — — 然后运行生成代码以拆分字符串输入并因此产生某种结构或对象树的一个工具。一般情况下,此所涉及的过程分为两个步骤,"词法分析"和"解析,"在其中词法分析器首先转换输入的字符串标记,验证字符做事实上形成合法的令牌,一路走来。然后解析器标记并验证令牌适当的顺序出现,并且包含相应的值,,等等,通常将标记类转换为一些抽象的树状结构进行进一步的分析。

解析器发电机的问题是相同的任何生成的元编程方法: 生成的代码将需要重新生成,语法更改。但更重要的是这种情况下,生成的代码将生成计算机,所有精彩变量命名随计算机生成的代码 (有人准备站起来的变量如"integer431"和"string$ $x$ y$ z"?),因此很难调试。

Solution: Functional

在某一种光,解析是从根本上功能: 它需要输入、 执行某种操作上和生成输出结果。关键的洞察力,事实证明,是一个解析器可以创建出的很多小的解析器,每个解析一点点的字符串输入,然后返回一个标记和另一个函数解析字符串输入下的一点。这些技术,我相信这在 Haskell 引入的称为 combinators 解析器,正式和他们变成"中型"解析问题优雅的解决方案 — — 解析器不一定一样复杂,需要一种编程语言,但一些超出 String.Split (或黑客攻击了一连串的 regex 扫描) 能做些什么。

在解析器 combinators 的情况下打开的扩展要求被通过创建小的函数,然后使用功能的方法"组合"成为更大的功能 (这是我们在那里名称"combinators")。大解析器可以由任何人都有足够的技能,了解函数组合组成。这项技术是一般一个需要探索,但我会保存,为未来的列。

事实证明,有几个解析器组合库供 Microsoft。NET 框架,很多基于写在 Haskell 这种设置解析器的标准组合库模块秒差距。这两个库是 FParsec,为 F # 和语言,写 C# 编写的。每一个是开源和相对较详细的记录,这样,他们有两个目的的都有用出框中,作为一个模式,从中学习设计思想。我还将去 FParsec 以后的专栏。

"解析"的语言诗吗?

语言,可在 code.google.com/p/sprache,自称为"简单、 轻型库构建直接在 C# 代码中,解析器"的"不和工业实力' 语言工作台上竞争。它适合某个正则表达式和一个全功能的工具集如 ANTLR 之间。"(ANTLR 是一个解析器发电机,拟合到生成元编程的类别,如 lex/yacc)。

入门语言非常简单: 下载代码、 生成项目,然后将 Sprache.dll 程序集复制到您的项目依赖项目录和添加到项目的引用。从这里,解析器定义工作声明 Sprache.Parser 实例,然后将它们合并以特定的方式创建 Sprache.Parser 实例,所做的一切,反过来可能,如果需要 (和通常是),返回包含部分或所有的已解析的值的域对象。

简单的语言

若要首先,让我们从解析器,它知道如何解析为一个电话号码域类型的用户输入电话号码。为简单起见,我永远坚持 format—(nnn) nnn nnnn 美式 — — 但我们想专门识别故障区号、 前缀和行,并允许字母数字的位置 (因此有人可以输入自己的电话号码作为"(800) 吃坚果",如果他们不希望)。理想情况下,电话号码域类型将 alpha 和需求,全数字形式之间转换但这功能将作为练习留给读者 (意味着,基本上,我不想打扰它)。

(指出只需将所有字母转换为数字方式不完全兼容的解决方案,要求我墨守成规的人。在大学,正是共同在尝试来阐明"酷"的电话号码的朋友圈 — — 一个前室友仍在等待 1-800-CTHULHU 成为免费的事实上,所以他可以赢得这场比赛,永远在一起。)

最简单的开始位置是与电话号码域类型:

class PhoneNumber
{
  public string AreaCode { get; set; }
  public string Prefix { get; set; }
  public string Line { get; set; }
}

这是"真正的"域类型,区号、 前缀和线将验证代码在其属性设置的方法,但这会造成重复的代码分析器和域类 (其中,顺便说一句,我们会解决之前,这都是) 之间。

接下来,我们需要知道如何创建一个简单的解析器知道如何解析 n 数字的位数:

public static Parser<string> numberParser =
  Parse.Digit.AtLeastOnce().Text();

定义的 numberParser,这是简单的。 开始与原始分析器数字 (<T> 解析器实例 类上定义 Sprache.Parse),并介绍我们想要的输入流中的至少一个数字、 隐式要么直到输入流消耗所有数字干涸或解析器遇到一个非数字字符。 该文本的方法转换为单个字符串为我们消费流的分析结果。

测试这是很容易 — — 它喂食的字符串并开始执行:

[TestMethod]
public void ParseANumber()
{
  string result = numberParser.Parse("101");
  Assert.AreEqual("101", result);
}
[TestMethod]
public void FailToParseANumberBecauseItHasTextInIt()
{
  string result = numberParser.TryParse("abc").ToString();
  Assert.IsTrue(result.StartsWith("Parsing failure"));
}

在运行时,该文件存储"101"成结果。 如果 Parse 方法美联储的"abc"的输入的字符串,它将产生异常。 (如果 nonthrowing 的行为是首选,语言也有 TryParse 方法返回一个对象,可以询问有关成功或失败的结果对象。)

电话号码分析情况是有点复杂,虽然 ; 它需要解析只是三或四位数字 — — 不多也不少。 定义一个这种分析器 (三位解析程序) 是有点棘手,但是仍然可行:

public static Parser<string> threeNumberParser =
  Parse.Numeric.Then(first =>
    Parse.Numeric.Then(second =>
      Parse.Numeric.Then(third =>
        Parse.Return(first.ToString() +
          second.ToString() + third.ToString()))));

数字解析器需要的字符,而且,如果它是一个数字,前进到下一个字符。 然后方法所需的功能 (在 lambda 表达式的形式) 执行。 返回的方法收集每个单个字符串,正如其名称所示,将其作为返回值 (请参见图 1)。

图 1 解析电话号码

[TestMethod]
public void ParseJustThreeNumbers()
{
  string result = threeNumberParser.Parse("123");
  Assert.AreEqual("123", result);
}
[TestMethod]
public void ParseJustThreeNumbersOutOfMore()
{
  string result = threeNumberParser.Parse("12345678");
  Assert.AreEqual("123", result);
}
[TestMethod]
public void FailToParseAThreeDigitNumberBecauseItIsTooShort()
{
  var result = threeNumberParser.TryParse("10");
  Assert.IsTrue(result.ToString().StartsWith("Parsing failure"));
}

成功。 到目前为止。 (是的 threeNumberParser 的定义是尴尬 — — 肯定有更好的方式来定义此 ! 不要害怕: 有,但若要了解如何扩展解析器,我们要深入探讨语言如何构造的,就是在这一系列的下一部分。)

但是,现在,我们需要做的是处理左-parens,右-­parens 智勇双全,和一切转换为电话号码对象。 它看起来有点尴尬,与我们的看得很远,但下一步,如图所示,在发生了什么图 2

将输入转换成一个电话号码对象图 2

public static Parser<string> fourNumberParser =
  Parse.Numeric.Then(first =>
    Parse.Numeric.Then(second =>
      Parse.Numeric.Then(third =>
        Parse.Numeric.Then(fourth =>
          Parse.Return("" + first.ToString() +
            second.ToString() + third.ToString() +
              fourth.ToString())))));
public static Parser<string> areaCodeParser =
  (from number in threeNumberParser
  select number).
XOr(
  from lparens in Parse.Char('(')
  from number in threeNumberParser
  from rparens in Parse.Char(')')
  select number);
public static Parser<PhoneNumber> phoneParser =
  (from areaCode in areaCodeParser
  from _1 in Parse.WhiteSpace.Many().Text()
  from prefix in threeNumberParser
  from _2 in (Parse.WhiteSpace.Many().Text()).
Or(Parse.Char('-').Many())
  from line in fourNumberParser
  select new PhoneNumber() { AreaCode=areaCode, Prefix=prefix, Line=line});
Using the parser becomes pretty straightforward at this point:
[TestMethod]
public void ParseAFullPhoneNumberWithSomeWhitespace()
{
  var result = phoneParser.Parse("(425) 647-4526");
  Assert.AreEqual("425", result.AreaCode);
  Assert.AreEqual("647", result.Prefix);
  Assert.AreEqual("4526", result.Line);
}

最重要的是,解析器是完全可扩展的因为它,也可以组合成更大的解析器将文本输入转换为地址的对象或 ContactInfo 对象或任何其他可以想象。

组合数学概念

从历史上看,解析文本已"语言研究人员"和学术界,生成的元编程解决方案所固有的复杂和困难编辑生成的编译-测试-调试周期要归功于省。 试图通过计算机-行走­生成的代码 — — 特别是基于有限状态机版本的许多解析器发电机推出 — — 在调试器中是一项挑战,即使是最顽强的开发人员。 为此原因,大多数开发人员认为有关沿解析行时提出了一个基于文本的问题的解决方案。 而且,事实上,大部分时间,分析器基于发电机的解决方案是激烈大材小用。

解析器 combinators 作为一个很好的中间解决方案: 具有足够的灵活性和强大到足以处理一些非平凡解析,而无需博士 了解如何使用它们的计算机科学。 更有趣的是,组合数学的概念是一个有趣的问题,并导致一些其他有趣想法,稍后我们将探讨其中的一些。

在此列出生的精神,使确保将"眼"我的下一篇专栏 (对不起,忍不住),其中我会扩展语言是触摸,以减少在这里定义的三、 四位数字解析器的丑恶。

编码愉快 !

Ted Neward Neudesic LLC 建筑顾问。 他已写了一百多篇和编著或合著了十几本书,包括"专业 F # 2.0"(Wrox,2010年)。 他是 C# MVP,在世界各地的会议上发言。 他咨询、 定期指导 — — 达到他在 ted@tedneward.comTed.Neward@neudesic.com 如果你有兴趣,让他来和你的团队,工作或阅读他的博客,在 blogs.tedneward.com

多亏了以下技术的专家,检讨这篇文章: Luke Hoban