这是我参与「第五届青训营 」伴学笔记创作活动的第7天。
认识规则引擎
什么是规则引擎
定义
规则引擎是一种嵌入在应用服务中的组件,可以将灵活多变的业务决策从服务代码中分离出来。通过使用预定义的语义模块来编写业务逻辑规则。在执行时接受数据输入、解释业务规则,并做出决策。规则引擎能大大提高系统的灵活性和扩展性。简而言之,就是输入规则与属性,输出该规则下对应的输出。
由此,开发者将规则作为独立开发的部分而从应用代码中分离出来,当业务改变时,只需修改规则决策即可。
优点
- 解决开发人员重复编码的问题
- 业务决策与服务本身解耦,提高服务的可维护性
- 缩短开发路径,提高效率
组成部分
- 数据输入
支持接受使用预定义的语义编写的规则作为策略集,比如“price > 500”,接受业务的数据作为执行过程中的参数,比如价格、标签等。 - 规则理解
能够按照预先定义的词法、语法、优先级、运算符等正确理解业务规则所表达的语义。 - 规则执行
根据执行时输入的参数对策略集中的规则进行正确的解释和执行,同时对规则执行过程中的数据类型进行检查,确保执行结果正确。
应用场景
- 风控对抗:实现较好的风控识别效果
- 活动策略运营:由于服务代码与业务运营逻辑解耦,可以提高运营策略的迭代效率
- 数据分析和清洗:实现便捷的对数据进行整理、清洗和转换,根据自定义规则高效产出数据
编译原理基本概念
规则引擎的核心原理 --> 编译原理的相关概念
什么是编译
编译的过程就是 把某种语言的源程序,在不改变语义的条件下,转换成另一种语言程序(目标语言程序)。
- 如果源代码编译后要在操作系统上运行,那目标代码就是汇编/机器代码。
- 如果编译后是在虚拟机里执行,那目标代码就可以不是汇编代码,而是一种解释器可以理解的中间形式的代码即可。
执行过程
根据第一章中提到的组成部分,基于实现来说,编译原理在其中各部分的体现是:
1.理解规则 ----> 词法分析:把源代码字符串转换为词法单元(Token)的这个过程。
语法分析:在词法分析的基础上识别出表达式的语法结构。
该部分所做的事即把表达式转化为规则引擎能理解的事物。
2.执行规则 ----> 表达式抽象语法结构的树状表示,对于一个表达式,抽象语法树一定时是唯一确定的
确立了唯一的表达式,保证的执行结果的唯一性。
执行过程:对树做了遍历,遍历的过程中根据设定的好的语法做计算
3.输入输出 ----> 类型检查:验证执行的结果是否为合适的数据类型。在抽象语法树中,通常会验证某节点的子节点的数据类型是否合法
参数注入:在规则执行过程中,使用输入的参数值来计算语法树中的标识符节点值过程
词法分析(Lexical Analysis)
词法分析就是把源代码字符串转换为词法单元(合法Token)的这个过程。
比如上图这个例子中,根据每个元素所代表的类划分了单元,这有些像我们平时划分句子的主谓宾一样,即把句子中的成分确定出来。
词法单元是根据一定的逻辑对句子的成分进行了切分。
有限自动机(Finite-State-Automaton)
那么每一个Token是根据什么识别出来的呢?每个编译器都有自己的状态机设定,而该部分进行识别是基于一个有限自动机,它的状态数量是有限的。该状态机在任何一个状态,基于输入的字符,都能做一个确定状态转换,类似于一种映射关系。
语法分析(Syntax Analysis)
语法分析在词法分析的基础上,识别表达式的语法结构的过程,根据不同的优先级分出操作符、操作数等。
抽象语法树(Abstract Syntax Tree)
在编译中,以上这种将表达式的结构分解、识别操作符等元素的机制,是基于抽象语法树来实现的,也是规则引擎设计中的重点。由此就实现将Token转化为了一颗AST。
如上述例子所形成的抽象语法树为:
抽象语法树的构建
概念 - 上下文无关语法(Context-Free Grammar)
比如
.....
r := a > b
.....
像这样的语句由于执行完它时已经完成了对于其中各Token的逻辑判断,故是不需要考虑上下文就可以判断出其正确性的,就可以使用巴克斯范式(BNF)来表达。 那么,编程语言为什么不用人类的语言(自然语言),而是用上下文无关的文法呢? 因为
- 便于设计编译器。 客观上技术目前无法实现,如果使用了上下文相关文法,那就是真正实现了人工智能,NLP领域将会有重大突破。
- 便于代码开发维护。 如果开发出来的代码像高考的语文阅读理解一样,每个人都有不同的理解,那么,到底哪个才是作者真正想要表达的?如果人类都确定不了含义,那计算机同样也确定不了,最终结果就是错误执行或无法执行。
- 汇编语言/机器语言是上下文无关的。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…
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
概念 - 巴克斯范式
简单来说,巴克斯范式是一种推导表达式
BNF由两部分组成,以;结尾,以:为分割,左边的为编译的表达式的类型,可以用右边的(已知的表达式,比如常见编程语言中的关键字、常量等)来表示。当一个Token进入,是一层一层进行推导的。
<expr> ::= <expr> + <term>
| <expr> - <term>
| <term>
<term> ::= <term> * <factor>
| <term> / <factor>
| <factor>
<factor> ::= ( <expr> )
| Num
- 每个产生式就是一个子树,在写编译器时,每个子树对应一个解析函数。
- 叶子节点叫做 终结符,非叶子节点叫做 非终结符。
产生式:一个表达式可以由另外已知类型的表达式或者符合推导产生。
- 内置符号:字面量(string、bool、number)标识符、运算符
- 一个基础表达式可以由 常量(string、bool、number)或标识符(identifier)组成
- 一个乘法表达式可以由 基础表达式 或 乘法表达式 * 基础表达式 组成
比如上图,一个乘法表达式可以有一个乘法表达式乘一个基础表达式表示,也可以只由一个基础表达式表示,即递归的表示自己,直到推出到终结符。
递归下降算法(Recursive Descent Parsing)
是从根节点开始自顶向下构成出语法树的过程,即对Token的处理机制。
不断的对Token进行语法展开(下降),展开过程中可能会遇到递归的情况。
树的层数可以反映是被Token的优先级,连续根据规则判断Token的过程中进行递归和回溯,且是一种贪婪的算法,即可能早的匹配其对应项,操作符的左右子树为其左操作数和右操作数,注意:括号是一个基础表达式,表示优先级,图中省略了推导过程。
解析算法:
匹配一个数据类型(types)
匹配一个标识符(Id),作为变量名称
匹配初始化部分(varInitializer),而这会导致下降一层,使用一个新的语法规则:
匹配一个等号
匹配一个表达式(在这个步骤会导致多层下降:exp->add->mul->pri->IntLiteral)
创建一个varInitializer对应的AST节点并返回
如果没有成功地匹配初始化部分,则回溯,匹配ε,也就是没有初始化部分。
匹配一个分号
创建一个varDecl对应的AST节点并返回
-
对于一个非终结符,要从左到右依次匹配其产生式中的每个项,包括非终结符和终结符。
-
在匹配产生式右边的非终结符时,要下降一层,继续匹配该非终结符的产生式。
-
如果一个语法规则有多个可选的产生式,那么只要有一个产生式匹配成功就行。如果一个产生式匹配不成功,那就回溯回来,尝试另一个产生式。
抽象语法树的执行
预先定义好每种操作符的执行逻辑。
对抽象语法树进行后序遍历执行,即:
- 先执行左子树,得到左节点的值
- 再执行右子树,得到右节点的值
- 最后根据根节点的操作符执行得到根节点的值
类型检查
类型综合
根据子表达式的类型构造出父表达式的类型,比如:表达式A + B的类型是根据A和B的类型定义的
编译时检查 & 运行时检查
类型检查可以发送在表达式的编译阶段,即在构造语法树的阶段,也可以发送在执行时的阶段
- 编译时:需要提前声明参数的类型,在构建语法树过程中进行类型检查
- 运行时:可以根据执行时的参数输入的值类型,在执行过程中进行类型检查
以上内容若有不正之处,恳请您不吝指正!