规则引擎的设计与实现 | 青训营笔记

127 阅读6分钟

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

规则引擎

一种嵌入在应用服务中的组件,可以将灵活多变的业务决策从服务代码中分离出来,并使用预定义的语义模块编写决策业务。接受数据输入,解释业务规则,并根据业务规则做出业务决策

image-20230131212121109

组成部分:

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

编译原理的基本概念

image-20230131213335795

词法分析

就是把源代码字符串转换为词法单元(Token)的过程

image-20230131213742777

这里使用有限自动机。 有限自动机就是一个状态机。它的状态数量就是有限的。该状态机在任何一个状态,基于输入的字符,都能做一个确定的状态转换。

image-20230131213959669

状态机就是对于规则引擎支持的词法的描述

词法转换就是基于状态机去挨个的遍历表达式,输出一系列的Token来给下一阶段使用

语法分析

在词法分析的基础上,识别表达式的语法结构的过程

image-20230131215331656

抽象语法树:表达式的语法结构可以用树来表示,其每一个节点是一个语法单元,这个单元的构成规则就叫“语法”

image-20230131215446357

上下文无关语法:语言句子无需考虑上下文,就可以判断正确性。可以使用巴科斯范式来表达

BNF是描述上下文无关理论的一种具体方法

<expr> ::= <expr> + <term>
         | <expr> - <term>
         | <term><term> ::= <term> * <factor>
         | <term> / <factor>
         | <factor><factor> ::= ( <expr> )
           | Num

BNF本质上就是树形分解,分解成一棵抽象语法树

  • 每个产生式就是一个子树,在写编译器时,每个子树对应一个解析函数
  • 叶子节点叫做终结符,非叶子节点叫做非终结符

如:

a = 0;

这是一个赋值语句,无论语句前后是什么代码,这个语句所代表的操作是确定的。

上下文无关语法G:终结符集合T + 非终结符集合N + 产生式集合P + 起始符号S

G由T、N、S和P组成,由语法G推导出来的所有句子的集合称为G语言!

终结符: 组成串的基本符号。可以理解为词法分析器产生的token集合。比如 + Id ( )

非终结符: 表示token的的集合的语法变量。比如 stmt varDecl 等等

image-20230131220153904

产生式:形如 S : AB,就是说S的含义可以用语法AB进行表达

S : AB
A : aA | ε
B : b | bB

展开(expand):将P(A->u )应用到符号串vAw中,得到新串vu **w

折叠(reduce):将P(A->uu )应用到符号串vuu w中,得到新串vAw

推导(derivate):符号串u 应用一系列产生式,变成符号串v ,则u =>v:S => ab | b | bb

递归下降算法:自顶向下构造语法树。不断的对Token进行与发展开,展开过程中可能会遇到递归的情况

如变量声明语句的规则:

varDecl : types Id varInitializer? ';' ;        //变量声明
varInitializer : '=' exp ;                       //变量初始化
exp : add ;                                      //表达式       
add : add '+' mul | mul;                         //加法表达式
mul : mul '*' pri | pri;                         //乘法表达式
pri : IntLiteral | Id | '(' exp ')' ;            //基础表达式

如果写成产生式的格式,就是这样:

varDecl -> types Id varInitializer ';' 
varInitializer -> '=' exp              
varInitializer -> ε
exp -> add
add -> add + mul
add -> mul
mul -> mul * pri
mul -> pri
pri -> IntLiteral
pri -> Id
pri -> ( exp )

基于这个规则做解析的算法如下:

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

举例:

image-20230201160635034

然后将绿色去掉,用树连接起来:

image-20230201160714694

类型检查

  • 类型综合:根据子表达式的类型构造出父表达式的类型。例如表达式A+B的类型是根据A和B的类型定义的image-20230201161244778
  • 编译时检查、运行时检查:类型检查可以发生在表达式的编译阶段,即在构造语法树的阶段;也可以发生在执行时的阶段image-20230201161426223

设计一个规则引擎

设计目标:

image-20230201161806509

词法分析:

image-20230201163142446

语法分析:

image-20230201163322654

优先级的表达:

type precedence struct {
    validSymbols    []Symbol        // 当前优先级支持的运算符类型
    nextPrecedence  *precedence     // 更高优先级的
    planner         planner         // 当前优先级的处理函数
}

image-20230201163638300

语法树的结构:

  • 一元运算符:左子树为空,右子树为右操作数
  • 二元运算符:左子树为左操作数,右子树为右操作数
  • 括号:左子树为空,右子树为内部表达式的AST

image-20230201164115814

语法树的执行和类型检查:

执行:

  • 预先定义好每种操作符的执行逻辑
  • 对抽象语法树进行后续遍历操作

类型检查:

  • 时机:执行时检查
  • 检查方法:在一个节点的左右子节点执行完成后,分别校验左右子节点的类型是否符合对应操作符的类型检查预设规则