基于编译原理了解规则引擎设计| 青训营笔记

277 阅读10分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第7天。

认识规则引擎

什么是规则引擎

定义

规则引擎是一种嵌入在应用服务中的组件,可以将灵活多变的业务决策从服务代码中分离出来。通过使用预定义的语义模块来编写业务逻辑规则。在执行时接受数据输入、解释业务规则,并做出决策。规则引擎能大大提高系统的灵活性和扩展性。简而言之,就是输入规则与属性,输出该规则下对应的输出。

由此,开发者将规则作为独立开发的部分而从应用代码中分离出来,当业务改变时,只需修改规则决策即可。
image.png

优点

  • 解决开发人员重复编码的问题
  • 业务决策与服务本身解耦,提高服务的可维护性
  • 缩短开发路径,提高效率

组成部分

  • 数据输入
    支持接受使用预定义的语义编写的规则作为策略集,比如“price > 500”,接受业务的数据作为执行过程中的参数,比如价格、标签等。
  • 规则理解
    能够按照预先定义的词法、语法、优先级、运算符等正确理解业务规则所表达的语义。
  • 规则执行
    根据执行时输入的参数对策略集中的规则进行正确的解释和执行,同时对规则执行过程中的数据类型进行检查,确保执行结果正确。

应用场景

image.png

  • 风控对抗:实现较好的风控识别效果
  • 活动策略运营:由于服务代码与业务运营逻辑解耦,可以提高运营策略的迭代效率
  • 数据分析和清洗:实现便捷的对数据进行整理、清洗和转换,根据自定义规则高效产出数据

编译原理基本概念

规则引擎的核心原理 --> 编译原理的相关概念

什么是编译

编译的过程就是 把某种语言的源程序,在不改变语义的条件下,转换成另一种语言程序(目标语言程序)。 image.png

  • 如果源代码编译后要在操作系统上运行,那目标代码就是汇编/机器代码。
  • 如果编译后是在虚拟机里执行,那目标代码就可以不是汇编代码,而是一种解释器可以理解的中间形式的代码即可。

执行过程

根据第一章中提到的组成部分,基于实现来说,编译原理在其中各部分的体现是:

1.理解规则 ----> 词法分析:把源代码字符串转换为词法单元(Token)的这个过程。
语法分析:在词法分析的基础上识别出表达式的语法结构。
该部分所做的事即把表达式转化为规则引擎能理解的事物。
2.执行规则 ----> 表达式抽象语法结构的树状表示,对于一个表达式,抽象语法树一定时是唯一确定的
确立了唯一的表达式,保证的执行结果的唯一性。
执行过程:对树做了遍历,遍历的过程中根据设定的好的语法做计算
3.输入输出 ----> 类型检查:验证执行的结果是否为合适的数据类型。在抽象语法树中,通常会验证某节点的子节点的数据类型是否合法
参数注入:在规则执行过程中,使用输入的参数值来计算语法树中的标识符节点值过程

词法分析(Lexical Analysis)

词法分析就是把源代码字符串转换为词法单元(合法Token)的这个过程。

image.png 比如上图这个例子中,根据每个元素所代表的类划分了单元,这有些像我们平时划分句子的主谓宾一样,即把句子中的成分确定出来。
词法单元是根据一定的逻辑对句子的成分进行了切分。

有限自动机(Finite-State-Automaton)

那么每一个Token是根据什么识别出来的呢?每个编译器都有自己的状态机设定,而该部分进行识别是基于一个有限自动机,它的状态数量是有限的。该状态机在任何一个状态,基于输入的字符,都能做一个确定状态转换,类似于一种映射关系。

image.png

语法分析(Syntax Analysis)

语法分析在词法分析的基础上,识别表达式的语法结构的过程,根据不同的优先级分出操作符、操作数等。

image.png

抽象语法树(Abstract Syntax Tree)

在编译中,以上这种将表达式的结构分解、识别操作符等元素的机制,是基于抽象语法树来实现的,也是规则引擎设计中的重点。由此就实现将Token转化为了一颗AST。
如上述例子所形成的抽象语法树为:

image.png

抽象语法树的构建

概念 - 上下文无关语法(Context-Free Grammar)

比如

.....
r := a > b
.....

像这样的语句由于执行完它时已经完成了对于其中各Token的逻辑判断,故是不需要考虑上下文就可以判断出其正确性的,就可以使用巴克斯范式(BNF)来表达。 那么,编程语言为什么不用人类的语言(自然语言),而是用上下文无关的文法呢? 因为

  1. 便于设计编译器。 客观上技术目前无法实现,如果使用了上下文相关文法,那就是真正实现了人工智能,NLP领域将会有重大突破。
  2. 便于代码开发维护。 如果开发出来的代码像高考的语文阅读理解一样,每个人都有不同的理解,那么,到底哪个才是作者真正想要表达的?如果人类都确定不了含义,那计算机同样也确定不了,最终结果就是错误执行或无法执行。
  3. 汇编语言/机器语言是上下文无关的。CPU执行指令时,读到哪条执行哪条。如果CPU需要考虑上下文,来决定一个语句到底要做什么,那么CPU执行一条语句会比现在慢千倍万倍。考虑上下文的事情,完全可以用户在编程的时候用算法实现。既然机器语言是上下文无关的,那高级语言也基本上是上下文无关的,可能有某些个别语法为了方便使用,设计成了上下文相关的,比如脚本语言的弱类型。在便于使用的同时,增加了解析器的复杂度。

上下文无关语法G:终结符集合T + 非终结符集合N + 产生式集合P + 起始符号S
G由T、N、S和P组成,由语法G推导出来的所有句子的集合称为G语言!
终结符: 组成串的基本符号。可以理解为词法分析器产生的token集合。比如 + Id ( ) 等
非终结符:  表示token的的集合的语法变量。比如 stmt varDecl 等等

start:blockStmts ;               //起始
block : '{' blockStmts '}' ;      //语句块
blockStmts : stmt* ;              //语句块中的语句
stmt = varDecl | expStmt | returnStmt | block;   //语句
varDecl : type Id varInitializer? ';' ;         //变量声明
type : Int | Long ;                              //类型
varInitializer : '=' exp ;                       //变量初始化
expStmt : exp ';' ;                              //表达式语句
returnStmt : Return exp ';' ;                    //return语句
exp : add ;                                      //表达式       
add : add '+' mul | mul;                         //加法表达式
mul : mul '*' pri | pri;                         //乘法表达式
pri : IntLiteral | Id | '(' exp ')' ;            //基础表达式 

作者:青训营官方账号
链接:juejin.cn/post/719336…
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

概念 - 巴克斯范式

简单来说,巴克斯范式是一种推导表达式

image.png BNF由两部分组成,以;结尾,以:为分割,左边的为编译的表达式的类型,可以用右边的(已知的表达式,比如常见编程语言中的关键字、常量等)来表示。当一个Token进入,是一层一层进行推导的。

<expr> ::= <expr> + <term>
| <expr> - <term>
| <term>

<term> ::= <term> * <factor>
| <term> / <factor>
| <factor>

<factor> ::= ( <expr> )
| Num
  • 每个产生式就是一个子树,在写编译器时,每个子树对应一个解析函数。
  • 叶子节点叫做 终结符,非叶子节点叫做 非终结符

产生式:一个表达式可以由另外已知类型的表达式或者符合推导产生。

  • 内置符号:字面量(string、bool、number)标识符、运算符
  • 一个基础表达式可以由 常量(string、bool、number)或标识符(identifier)组成
  • 一个乘法表达式可以由 基础表达式 或 乘法表达式 * 基础表达式 组成 image.png

比如上图,一个乘法表达式可以有一个乘法表达式乘一个基础表达式表示,也可以只由一个基础表达式表示,即递归的表示自己,直到推出到终结符。

递归下降算法(Recursive Descent Parsing)

是从根节点开始自顶向下构成出语法树的过程,即对Token的处理机制。
不断的对Token进行语法展开(下降),展开过程中可能会遇到递归的情况。

image.png 树的层数可以反映是被Token的优先级,连续根据规则判断Token的过程中进行递归和回溯,且是一种贪婪的算法,即可能早的匹配其对应项,操作符的左右子树为其左操作数和右操作数,注意:括号是一个基础表达式,表示优先级,图中省略了推导过程。

解析算法:

匹配一个数据类型(types)
匹配一个标识符(Id),作为变量名称
匹配初始化部分(varInitializer),而这会导致下降一层,使用一个新的语法规则:
匹配一个等号
匹配一个表达式(在这个步骤会导致多层下降:exp->add->mul->pri->IntLiteral)
创建一个varInitializer对应的AST节点并返回
如果没有成功地匹配初始化部分,则回溯,匹配ε,也就是没有初始化部分。
匹配一个分号
创建一个varDecl对应的AST节点并返回
  • 对于一个非终结符,要从左到右依次匹配其产生式中的每个项,包括非终结符和终结符。

  • 在匹配产生式右边的非终结符时,要下降一层,继续匹配该非终结符的产生式。

  • 如果一个语法规则有多个可选的产生式,那么只要有一个产生式匹配成功就行。如果一个产生式匹配不成功,那就回溯回来,尝试另一个产生式。

抽象语法树的执行

预先定义好每种操作符的执行逻辑。

对抽象语法树进行后序遍历执行,即:

  • 先执行左子树,得到左节点的值
  • 再执行右子树,得到右节点的值
  • 最后根据根节点的操作符执行得到根节点的值

类型检查

类型综合

根据子表达式的类型构造出父表达式的类型,比如:表达式A + B的类型是根据A和B的类型定义的

image.png

编译时检查 & 运行时检查

类型检查可以发送在表达式的编译阶段,即在构造语法树的阶段,也可以发送在执行时的阶段

  • 编译时:需要提前声明参数的类型,在构建语法树过程中进行类型检查
  • 运行时:可以根据执行时的参数输入的值类型,在执行过程中进行类型检查

以上内容若有不正之处,恳请您不吝指正!