Racket:用33行解析命题逻辑

2020-10-14 00:32:55

我最近开始对两个看似完全不同的东西感兴趣:方案/球拍和符号逻辑,所以我决定把这两个结合到一个小项目中,以获得一些乐趣。这场联姻实际上没有看起来那么奇怪,因为由于Sracket专注于元编程,所以它有非常健壮的词法分析和解析工具。在这篇文章中,我们将回顾一些实现简单解析器的策略,这些策略的概念可以很容易地扩展到解析其他东西,如JSON,甚至您自己的编程语言。

在我们进一步讨论之前,最好先介绍一下我们要解析的内容。命题逻辑是符号逻辑的一个分支,它定义了陈述命题的语法和规则集。有了这一点,我们可以定义如下参数:

约翰·斯图尔特去参观世界公园。玛丽亚·斯图尔特去了世界公园。因此,约翰和玛丽亚将一起去参观公园。\textNormal{约翰去公园。}\newline\textNormal{玛丽亚去公园。}\newline\textNormal{因此,约翰和玛丽亚去公园。}\newline约翰和玛丽亚去公园。玛丽亚·斯图尔特去了世界公园。因此,约翰和玛丽亚将一起去参观公园。问:=玛丽亚去公园P  。  Q  ∴P∧Q P:=\textNormal{约翰去公园}\newline Q:=\textNormal{玛丽亚去公园}\newlineP\;\ldotp\;Q\;\因此P\land Q P:=约翰去公园Q:=玛丽亚去公园P。Q∴P∧Q命题逻辑被限制在属于观察性陈述的句子中,比如约翰去公园。没有变量,也没有像全部或部分这样的量词。

随着命题逻辑的非正式引入,让我们看看我们拥有的符号,这些符号将需要放入我们的解析器中:

→  ,  ⟺  \r arr\;,\if→,⟺。

请注意,P→Q\Text{P}\rarr\Text{Q}P→Q读作";如果是P,则读取为Q\Text{如果P,则为Q}如果是P,则读取为Q";。

好了,理论说得够多了,让我们开始编程吧。我们可以使用的主要库可以在parser-tools包中找到,但是这些工具的文档让我感到困惑,而且设置一个简单的解析器也不是很符合人体工程学。幸运的是,还有另一种构建解析器的方法:吹嘘包。

Brag(更好的球拍AST生成器)是一个包,它允许我们以标准的BNF形式定义语法,然后轻松地对该语法进行lex和解析。只需用Raco pkg install brag安装,我们就可以使用它了。

让我们在我们的目录中创建一个名为gramar.rkt的新文件,并将#lang brag放在顶部。这是我们将用来定义BNF语法的文件。对于命题逻辑,该语法可以很好地表示,如下所示:

#lang brag语句:ATOM|Complex Complex:LPAR语句RPAR|语句连接语句|NOT语句连接语句:AND|OR|IF|IFF。

在BRAG中,任何全部大写的字符串都被解释为要包括在词法分析器中的标记,而那些不是大写的字符串是我们必须在语法的其他地方定义的表达式类型。我们将标记ATOM、NOT、AND、OR、IF、IFF定义为我们前面讨论的符号的类似项。我们还使用标记LPAR和RPAR来表示括号。

在这里,我们将语言的语法依次分解为其组成部分。句子可以是原子的(例如P、P、P或Q、Q),也可以是复合句。复杂表达式是括号内的任何句子,或者是由连接词连接的两个句子,或者是句子的否定。最后,连接词可以是AND、OR、IF或IFF。

让我们在同一个名为parser.rkt的目录中创建另一个球拍文件。我们还需要导入brag/support和包br-parser-tools/lex,后者是Brag使用的默认球拍解析器工具的分支,语法来自gramar.rkt。要安装它,只需Raco pkg install br-parser-tools即可。您现在应该拥有以下内容:

现在,我们可以定义我们的tokenize函数,该函数将接受一个输入字符串,并输出一个令牌列表。它的形式是这样的:

(DEFINE(TOKENIZE IP)(DEFINE LEXER(LEXER-src-pos;;在此定义令牌规则![(EOF)(Void)]))(DEFINE(NEXT-TOKEN)(LEXER IP))NEXT-TOKEN)。

我们在这里所做的就是设置导入的函数lexer-src-pos,该函数将获取匹配令牌的规则列表,并返回一个对其进行词法分析的函数。然后,通过调用Next-Token,我们在输入端口上递归运行此lexer函数,直到到达特殊的令牌eof,此时我们返回void。

现在,我们可以为要对其执行lex的令牌添加规则:

(定义(Tokenize IP)(定义词法分析器(lexer-src-pos[(char-range#\P#\Z)(TOKEN';原子词位)];匹配P和Z之间的字符[";^";(TOKEN';和词位)];匹配";^";作为逻辑AND令牌[";v";(TOKEN';或词位)];匹配";V";AS逻辑OR令牌[";~";(TOKEN';NOT LIMEME)];匹配";~";AS NOT TOKEN[";->;";(TOKEN';如果有词项)];匹配";-&&>;";好像令牌[";<;-&>;";(TOKEN';IFF lexeme)];匹配";<;->;";作为IFF标记[";(";(TOKEN';LPAR词位)];匹配";(";作为左括号(";)";(TOKEN';RPAR词位));匹配";)";作为右括号[空格(TOKEN';空格词位#:跳过?#t)];跳过空格[(Eof)(Void)]));在文件末尾返回void,这将停止解析器(Define(Next-Token)(Lexer IP));移动lexer以查找下一个令牌Next-Token)。

请注意这些规则是如何括在方括号中的,其中第一个表达式是lex的模式,第二个表达式是该模式返回的内容。您可以返回任何您想要的东西(或者添加副作用),但是我们将返回内置于BRAG中的令牌结构,因为这将与包的解析功能很好地集成。

令牌结构有两个输入:名称(我们在语法中定义了它!)和词位,它是从我们刚才词法分析的模式返回的字符串。因此,当我们添加[";^";(TOKEN';and lememe)]时,我们是在语法文件中定义的令牌语法规则下对字符串";^";进行词法分析。

还要注意,lex包附带了一些用于匹配令牌的函数,比如我们在这里也使用的char-range和空白。

我们现在有了一个函数,可以对任何输入字符串进行标记化,所以现在让我们解析这些标记,这实际上非常简单,只需:

(DEFINE STX(parse(TOKENIZE(OPEN-INPUT-STRING";P^Q";);生成语法结构(SYNTAX-&>DATUM STX);将语法转换为它包含的数据。

解析器完成!正如承诺的那样,语法和解析器文件只有33行。

这正是我们想要的数据结构。整个字符串是一个句子,其中有一个复合句,包含标记P、^和Q。

';(句子(复杂(复杂";(";(句子(复杂(句子";P";))(连词";-&>;&34;)(句子";Q";))";)";))(连词";-&>;";)(句子(复杂";~";(句子";R";)。

我们现在可以分析命题逻辑中的任何符号句子。解析是很好的第一步,但就其本身而言,它做得并不多。使用我们生成的数据结构,我们可以创建可视化逻辑树、用求解器导出定理和论点等的程序。我们在这里使用的相同策略也可以应用于解析JSON或其他结构化语言。