规则引擎 clara-rules
Posted willwillie
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了规则引擎 clara-rules相关的知识,希望对你有一定的参考价值。
本文的主题是规则引擎,主要内容包括规则引擎的实现算法 rete算法,clojure开源的规则引擎clara-rules对规则的处理方式和特点,以及clojure edn文件格式处理等内容。
(在一段时间的思考之后,发现本文还是越写越乱,最后还是决定采用问答式来组织)
1.那么什么是规则引擎呢?
规则引擎由推理引擎发展而来,是一种嵌入在应用程序中的组件,实现了将业务决策从应用程序代码中分离出来,并使用预定义的语义模块编写业务决策。接受数据输入,解释业务规则,并根据业务规则做出业务决策。—百度
这个定义说的已经比较充分了,可见规则引擎是一个比较通俗易懂的概念。规则引擎就是将推理逻辑从业务代码分离,使得业务代码更加模块化的一个软件,接受一组规则和输入的事实,输出业务决策。
2.规则引擎是怎样工作的?
我们知道,当规则变得复杂后,事实变得庞大后,有很多东西就很难绕清楚了,这个时候就需要算法来帮忙。很多规则引擎使用的算法都是rete算法,那么rete算法的核心思想是什么呢?rete算法其实是一个模式匹配算法,规则引擎的核心是Pattern Matcher(模式匹配器)。不管是正向推理还是反向推理,首先要解决一个模式匹配的问题。(推理是数学证明中非常常见的一个名词),比如说正向推理:
正向推理又称为正向链接推理,其推理基础是逻辑演绎的推理链,它从一组表示事实的谓词或命题出发,使用一组推理规则,来证明目标谓词公式或命题是否成立。实现正向推理的一般策略是:先提供一批数据(事实)到总数据库中,系统利用这些事实与规则的前提匹配,触发匹配成功的规则(即启用规则),把其结论作为新的事实添加到总数据库中。继续上述过程,用更新过的总数据库中的所有事实再与规则库中另一条规则匹配,用其结论再修改总数据库的内容,直到没有可匹配的新规则,不再有新的事实加到总数据库为止。
3.模式匹配看起来其实挺简单的,一般的匹配例子有哪些?
对于规则的模式匹配,可以定义为: 一个规则是一组模式的集合。如果事实/假设的状态符合该规则的所有模式,则称该规则是可满足的。 模式匹配的任务就是将事实/假设的状态与规则库中的规则一一匹配,找到所有可满足的规则。
比如说,正则表达式就是一种模式匹配(学习正则表达式的成本很高,学正则表达式那么酸爽的过程应该不会忘记(不过如果能从推理的角度来看正则表达式,就溶图很多));
/<\\s*(\\S+)(\\s[^>]*)?>[\\s\\S]*<\\s*\\/\\1\\s*>/
匹配 html 标记。
\\s*匹配空白字符0次或多次;
(\\S+)匹配可连续非空白字符串,同时这个group就是后续被使用到的\\1;
(\\s[^>]*)?匹配一个以空白开始的不带”>”符号的字符串0次或者1次;这个是group2,\\2;
另外,正则表达式也是clojure中的一种非常基本的数据类型,clojure中的匹配re-find、re-matcher等函数也是提供了一个模式匹配的过程。
举另外一个例子来说明匹配的这个概念,这个例子是浏览器渲染,这里面将CSS规则树匹配到DOM树,最终生成渲染树的过程就是一个匹配的过程,说不定很多浏览器的原理就是采用规则引擎来做的(:)要是我来做这个方面的设计,说不定我就使用规则引擎来做了。)
4.那么模式是什么?
模式其实就是规则中最小的原子单位。
5.模式匹配看起来听简单的,但是觉得rete算法很难,rete算法是什么?rete算法有什么特别的地方?
rete算法也就是一个网络,将传入的对象在网络上传播。首先根据规则集建立一个网络(包括alpha节点和beta节点),然后将fact对象输入这个网络,看最终得到什么。
下面来看看规则引擎的算法rete,首先看一些概念
一些基本的概念
规则:一组规则的集合或者多组规则的集合
session:一组规则的集合,并且还可以往里面添加规则,移除规则,然后fire-rules(运行规则,如果一个规则被fired了,那么这个规则就是fired的状态了,不会再被fired了,除非有新的影响这个rule的fact加入或者移除。),以及查询。那么查询session能够查到什么呢?其实query就可以认为是正则表达式了,会查询满足一定规则的fact:当然还可以限定一些参数(暂时可以先不关心query,用处不大,而且可能会大致偏题)。
rete算法也就是说根据一定的规则建立一个网络,然后对于某一个用户,他可能有一个fact,你只要将这个fact放入这个网络生成的session中,并且通过fire-rules,就可以取得根据规则得到的结果;或者你也可以查询某个规则是否符合你的fact.
working memory:那么working memory到底是什么?我认为就是fact,可以认为每一个fact就是一个Java中的对象,对应相应的class();
rete网络:见下一个问题
6.在rete中,所有的rules组成了一张网络,这张网络是怎样组成的呢?
这张网络主要包括这些节点:
RootNode:Rete 网络的根节点,所有对象通过 Root 节点进入网络。
ObjectTypeNode:对象类型节点,保证所传入的对象只会进入自己类型所在的网络,提高了工作效率。
AlphaNode:Alpha 节点是规则的条件部分的一个模式,一个对象只有和本节点匹配成功后,才能继续向下传播。
JoinNode:用作连接(jion)操作的节点,相当于 and,相当于数据库的表连接操作,属于 betaNode
类型的节点。BetaNode 节点用于比较两个对象和它们的字段。两个对象可能是相同或不同的类型。我们将这两个输入称为左和右。BetaNode 的左输入通常是一组对象的数组。BetaNode 具有记忆功能。左边的输入被称为 BetaMemory,会记住所有到达过的语义。右边的输入成为 Alpha Memory,会记住所有到达过的对象。
这些概念再多也还是不够形象,下面结合一张经典的图片来说
这里的agenda是准备要选择一条规则来执行(可能会按照一定的规则);
这里的act rhs是指执行规则的右半部分。
7.上图形象的说明了rete算法,但是规则匹配的具体步骤是什么(可能会涉及到非常具体的一些东西)?你觉得这样合理不?
推理引擎在进行模式匹配时,先对事实进行断言,为每一个事实建立working memory,然后将WME(working memory)从RETE鉴别网络的根结点开始匹配,下面对alpha结点和beta结点处理WME的不同情况分开讨论。
(1)如果WME的类型和根节点的后继结点TypeNode(alpha结点的一种)所指定的类型相同,则会将该事实保存在该TypeNode结点对应的alpha存储区中,该WME被传到后继结点继续匹配,否则会放弃该WME的后续匹配;
(2)如果WME被传递到alpha结点,则会检测WME是否和该结点对应的模式相匹配,若匹配,则会将该事实保存在该alpha结点对应的存储区中,该WME被传递到后继结点继续匹配,否则会放弃该WME的后续匹配;
(3)如果WME被传递到beta结点的右端,则会加入到该beta结点的right存储区,并和left存储区中的Token进行匹配(匹配动作根据beta结点的类型进行,例如:join,projection,selection),匹配成功,则会将该WME加入到Token中,然后将Token传递到下一个结点,否则会放弃该WME的后续匹配;
(4)如果Token被传递到beta结点的左端,则会加入到该beta结点的left存储区,并和right存储区中的WME进行匹配(匹配动作根据beta结点的类型进行,例如:join,projection,selection),匹配成功,则该Token会封装匹配到的WME形成新的Token,传递到下一个结点,否则会放弃该Token的后续匹配;
(5)如果WME被传递到beta结点的左端,将WME封装成仅有一个WME元素的WME列表做为Token,然后按照(4)所示的方法进行匹配;
(6)如果Token传递到终结点,则和该根结点对应的规则被激活,建立相应的Activation,并存储到Agenda当中,等待激发。
(7)如果WME被传递到终结点,将WME封装成仅有一个WME元素的WME列表做为Token,然后按照(6)所示的方法进行匹配;
这样看起来是很合理的。但是我们似乎忽略了一个细节,那么就是下一个问题要回答的内容了。
8.rete网络是怎么建立起来的?是否可以结合代码说明?
mk-session
的过程就是根据加入session中的所有规则,解释生成一个包括alpha和beta节点的rete网络。
(sc/defn mk-session*
"Compile the rules into a rete network and return the given session."
[productions :- #schema/Production
options :- sc/Keyword sc/Any]
(let [
productions (with-meta (into (sorted-set-by production-load-order-comp) productions)
;; Store the name of the custom comparator for durability.
:clara.rules.durability/comparator-name `production-load-order-comp)
beta-graph (to-beta-graph productions)
beta-tree (compile-beta-graph beta-graph)
beta-root-ids (-> beta-graph :forward-edges (get 0)) ; 0 is the id of the virtual root node.
beta-roots (vals (select-keys beta-tree beta-root-ids))
alpha-nodes (compile-alpha-nodes (to-alpha-graph beta-graph))
;; The fact-type uses Clojure's type function unless overridden.
fact-type-fn (or (get options :fact-type-fn)
type)
;; The ancestors for a logical type uses Clojure's ancestors function unless overridden.
ancestors-fn (or (get options :ancestors-fn)
ancestors)
;; The default is to sort activations in descending order by their salience.
activation-group-sort-fn (eng/options->activation-group-sort-fn options)
;; The returned salience will be a tuple of the form [rule-salience internal-salience],
;; where internal-salience is considered after the rule-salience and is assigned automatically by the compiler.
activation-group-fn (eng/options->activation-group-fn options)
rulebase (build-network beta-tree beta-roots alpha-nodes productions
fact-type-fn ancestors-fn activation-group-sort-fn activation-group-fn)
get-alphas-fn (:get-alphas-fn rulebase)
transport (LocalTransport.)]
(eng/assemble :rulebase rulebase
:memory (eng/local-memory rulebase transport activation-group-sort-fn activation-group-fn get-alphas-fn)
:transport transport
:listeners (get options :listeners [])
:get-alphas-fn get-alphas-fn)))
具体过程不外乎循环,网络相关,不具体阐述。
9.clara-rules对规则处理的API有哪些特点?
一般调用api的步骤:
1.mk-session 根据规则创建session
2.在session中insert一些fact到session中。
inset “Inserts one or more facts into a working session. It does not
modify the given session, but returns a new session with the facts
added.”
通过上一部分我们已经知道了rete算法的原理。clara-rules也是采用rete算法实现的,在调用mk-session的时候就会生成一张rete网络,包括alpha节点和beta节点,并且将session中的fact放入到working memory中。这样一个session就生成好了,通过fire-rules就可以将里面的fact传入网络,使得网络最终运行到terminal节点的时候,再依次选择一条条规则来执行,最后执行规则右侧的rhs,因为网络是一个循环网络,直到结束为止。另外,defquery我认为query的过程就是加入一个规则(某些边和节点),然后查询满足了这个规则的session中的fact;也就是说query也会发起一次匹配,但是最终没有rhs需要去执行。
clara另外提供的一些api还有:
insert-all
“Inserts a sequence of facts into a working session
retract
“Retracts a fact from a working session”
fire-rules还有点特别:
“Fires are rules in the given session. Once a rule is fired, it is
labeled in a fired state and will not be re-fired unless facts
affecting the rule are added or retracted.This function does not modify the given session to mark rules as
fired. Instead, it returns a new session in which the rules are
marked as fired.”
10.clojure edn文件
clojure.edn提供了两个处理edn文件的api:read 和read-string
那么什么是clojure的edn格式呢?全称是extensible data notation
edn is an extensible data notation. A superset of edn is used by Clojure to represent programs, and it is used by Datomic and other applications as a data transfer format.
也就是edn是可以代表clojure 程序代码的,也可以作为数据的传输格式。edn非常适合用于程序的交互。
那么一个能被clojure程序识别并使用的edn有什么特征呢?其实我觉得edn和json文件格式很像,只不过edn完全就是使用了clojure的语法
在edn文件中是没法直接识别clojure的正则表达式#""
的;
#
dispatch character
Tokens beginning with # are reserved. The character following # determines the behavior. The dispatches # (sets), #_ (discard), #alphabetic-char (tag) are defined below. # is not a delimiter.
也就是说在edn文件中,#只接受{ _ 以及数字和字符等。#"..."
在edn中被认为是不正确的格式。
这个时候就需要用到re-pattern
(re-pattern s)
Returns an instance of java.util.regex.Pattern, for use, e.g. in
re-matcher.
那么什么是java.util.regex.Pattern呢?
这是java中表示正则表达式的一个类,clojure中正则表达式用到的其实就是这个类。例如:
user=> (re-pattern "\\\\d+")
#"\\d+"
这里\\
前面也需要用到\\
作为转义字符。
既然使用到了edn文件,当然希望可以热加载文件中的内容,但是就像前面经历的那样,一个session只能位于主线程中,没法在每次rpc调用的那个线程中再次读取并创建,那么这个时候设计上就要注意两点了:
1:session设计为动态变量
2.需要检测文件,如果edn文件内容发生改变,则通过一个函数去改变session的值。
11.规则的设计有什么要注意的?
规则的设计也是个技术活,当然要设计的简单易懂,扁平化;
尽量减少冲突;
我的一点想法:编程的时候好的做法其实可以是只创建一个session,因为不同的事实匹配的规则是不同的,这个一般来说都不会相互冲突的。对性能的影响一般来说也不大。
现在并没有写大量的规则,后续经验丰富了应该会有更多可以分享的。
以上是关于规则引擎 clara-rules的主要内容,如果未能解决你的问题,请参考以下文章