逻辑编程入门--clojure.core.logic
此文已由作者张佃鹏授权网易云社区发布。
欢迎访问网易云社区,了解更多网易技术产品运营经验。
1. 逻辑编程思维:
逻辑编程(逻辑程序设计)是种编程范型,它设置答案须匹配的规则来解决问题,而非设置步骤来解决问题。过程是:
事实+规则=结果
简单的说,事实就是一些已知输入,规则是应该遵循的限定条件,输入和输出应该遵循一定的规则,然后找出所有满足这些规则的输出,便是结果。在逻辑编程时,我们没必要去关心寻找结果的过程,而注重的是输出结果。 逻辑编程的要点是将正规的逻辑风格带入计算机程序设计之中,数学家和哲学家发现逻辑是有效的理论分析工具,很多问题可以自然地表示成一个理论。说需要解答一个问题,通常与解答一个新的假设是否跟现在的理论无冲突等价。逻辑提供了一个证明问题是真还是假的方法。创建证明的方法是人所皆知的,故逻辑是解答问题的可靠方法。逻辑编程系统则自动化了这个程序,人工智能在逻辑编程的发展中发挥了重要的影响。
2. clojure.core.logic概述:
clojure.core.logic是clojure一个库,专门为了实现逻辑编程,core.logic是miniKanren的一个实现。miniKanren中一个重要的概念就是goal,根据goal来推测可能的结果,这就是它的核心概念。 我们在使用core.logic编程时,获取逻辑编程的结果,一般都遵循以x下形式:
- (run* [logic-variable]
- logic-expressions)
- ;;or
- (run n [logic-variable]
- logic-expressions)
run或者run函数可以执行逻辑表达式,返回满足条件的结果。返回的结果是一个关于logic-variable的数组。如果是run则返回所有满足逻辑表达式条件的逻辑变量,如果是run n,则返回前n个满足条件的变量。这里要注意的一点是:普通的逻辑表达式的返回结果只有两种:succeed/fail,如果找到满足条件的结果,则返回succeed,否则返回fail。只有使用run函数才能返回最终我们想要的输出结果。下面是run函数的使用示例:
- ;;首先要导入clojure.core.logic库
- (use 'clojure.core.logic)
- ;;使用run*,会返回所有满足条件的结果
- (run* [q]
- ;;这里是逻辑表达式,后面会讲到
- (conde
- [(== q 1)]
- [(== q 2)]
- [(== q 3)]
- ))
- ;;=>(1 2 3)
- ;;返回前两个结果
- (run 2 [q]
- (conde
- [(== q 1)]
- [(== q 2)]
- [(== q 3)]
- ))
- ;;=> (1 2)
- ;;如果n大于所有满足条件结果的总数,则与run*返回结果一致
- (run 4 [q]
- (conde
- [(== q 1)]
- [(== q 2)]
- [(== q 3)]
- ))
- ;;=> (1 2 3)
当然run函数也可以接收多个参数,如果是多个参数,则以数组的形式返回:
- ;;接收两个参数,最后返回结果是一个包含数组的序列
- (run* [q1 q2]
- (== q1 1)
- (== q2 1)
- )
- ;;=> ([1 1])
3. 逻辑表达式以及常用函数:
逻辑表达式的返回结果都是succeed或者fail,一个逻辑表达式中可以继续嵌套另外一个逻辑表达式,下面在介绍clojure中的常用逻辑函数同时穿插的介绍如何使用逻辑编程:
(1)==/!=/membero函数:
"=="/"!="函数用于判断两个逻辑变量是否相等或者不相等,是最常用的逻辑函数,而(membero x l)函数则表示只有x属于数组l时才会返回succeed:
- ;;找出等于1的数字
- (run* [q] (== q 1))
- ;;=> (1)
- ;;找出不等于1且属于数组[1 2 3]中的数字
- ;;;;这里的(!= q 1)与(membero q [1 2 3])两个逻辑表达式是与的关系(也就是必须两个逻辑表达式同时满足才返回succeed)
- (run* [q]
- (!= q 1)
- ;;q只有是数组[1 2 3]中的一个元素,才会返回succeed
- (membero q [1 2 3]))
- ;;=> (2 3)
(2)fresh函数:
这个函数在逻辑编程时特别重要,如果我们想在逻辑表达式中用到其他变量怎么办?怎么像其它语言一样去声明一个局部变量??fresh函数正是来解决以上疑问的,fresh函数可以声明几个局部的逻辑变量,有了这些局部逻辑变量以后,您就可以在逻辑表达式中随意发挥了:
- ;;q是一个包含两个元素的数组,第一个元素等于1,第二个元素是[2 3 4]中的一个
- ;;fresh函数和run函数使用规则类似,都接收一个[参数],参数后面是并列的表示与关系的逻辑表达式
- (run* [q]
- (fresh [q1 q2]
- (== q1 1)
- (membero q2 [2 3 4])
- (== q [q1 q2])
- ))
- => ([1 2] [1 3] [1 4])
(3)succeed/fail
这两个逻辑表达式也比较常用,succeed就表示执行成功的逻辑表达式,fail就表示执行失败的逻辑表达式,下面我们定义一个逻辑表达式函数,输入参数是一个逻辑表达式,然后返回该输入参数的相反的结果:
- ;;该函数返回参数g的相反结果:
- (defn l-not [g]
- (conda
- (g fail)
- (succeed)
- ))
- ;;=> #'insight.main/l-not;;在run*函数中使用l-not函数
- (run* [q]
- (l-not (== 3 1))
- (== q 1)
- )
- ;;=> (1)
(4)conde/conda/condu函数/all函数:
在介绍fresh函数和run函数时提到这两个函数的主体部分的表达式是“与”的关系,那么怎么可以表达“或”的关系呢,conde/conda/condu这三个函数都可表示“或”的关系,但三者之间又有细微的差别,都是很实用的函数。 这里之所以要把all函数和它们三个函数放在一起,是因为在conda和condu函数中,会经常使用all函数。
-->conde函数:接收逻辑表达式组,只要满足其中任意一个表达式组,多个表达式组之间是or的关系,不像其他语言中的or函数,conde函数不存在求值短路现象,会返回分别满足所有表达式组的结果:
- ;;返回满足等于1,或者同时等于2和3,或者是字符串"yes"/"no"的结果
- (run* [q]
- (conde
- ;;其中[]表示的是一组表达式,[]中逻辑表达式默认是“与”的关系,这里[]也可以用()替换,但是为了区别,最好用[]
- [(== q 1)]
- [(== q 2) (== q 3)]
- [(membero q ["yes" "no"])]
- ))
- ;;=> (1 "yes" "no")
-->conda函数:该函数返回第一个满足条件的表达式组中的所有结果,但是特别要注意的是,相邻两个表达式组之间切换的条件,也就是如果第n个表达式组返回fail的情况下,什么情况下去继续求解第n+1个表达式组,假设第n个表达式组中有3个表达式[expr1 expr2 expr3],只有当expr1失败的情况下,才会执行第n+1个表达式组,否则即使expr1返回succeed,expr2返回fail,conda函数就会直接返回fail,不会执行第n+1个表达式组。 那么如果我们想要使只要expr1、expr2,expr3有一个不成立就会执行第n+1个表达式组,怎么办??使用all函数,把这三个表达式包含进去。这里逻辑有点混乱,看代码会清楚很多:
- ;;情况1:conda函数中,第一个表达式组中两个表达式都返回succeed,所以直接返回(2 3),不去执行第二个表达式组
- (run* [q]
- (conda
- ;;表达式组1:
- [succeed
- (conde
- [(== q 2)]
- [(== q 3)]
- )]
- ;;表达式组2:
- [(membero q ["yes" "no"])]
- ))
- ;;=> (2 3)
- ;;情况2:第一个表达式组中第一个表达式返回fail,则会继续执行第二个表达式组,所以返回("yes" "no")
- (run* [q]
- (conda
- ;;表达式组1:
- [fail
- ;;表达式嵌套
- (conde
- [(== q 2)]
- [(== q 3)]
- )]
- ;;表达式组2:
- [(membero q ["yes" "no"])]
- ))
- ;;=> ("yes" "no")
- ;;情况3:第一个表达式组中第一个表达式返回succeed,但是整个表达式返回fail,则不会执行第二个表达式,直接返回fail
- (run* [q]
- (conda
- [succeed
- fail
- (conde
- [(== q 2)]
- [(== q 3)])
- ]
- [(membero q ["yes" "no"])]
- ))
- ;;=> ()
- ;;情况4:可以用all函数改变情况3的执行结果
- (run* [q]
- (conda
- [(all
- succeed
- fail
- (conde
- [(== q 2)]
- [(== q 3)]))
- ]
- [(membero q ["yes" "no"])]
- ))
- => ("yes" "no")
-->condu函数:condu函数是conda函数的子集,它只返回conda函数返回结果集中的结果:
- ;;情况1:返回结果是conda返回结果的第一个元素
- (run* [q]
- (condu
- ;;表达式组1:
- [succeed
- (conde
- [(== q 2)]
- [(== q 3)]
- )]
- ;;表达式组2:
- [(membero q ["yes" "no"])]
- ))
- ;;=> (2)
- ;;情况2:返回结果是conda返回结果的第一个元素
- (run* [q]
- (condu
- ;;表达式组1:
- [fail
- (conde
- [(== q 2)]
- [(== q 3)]
- )]
- ;;表达式组2:
- [(membero q ["yes" "no"])]
- ))
- ;;=> ("yes")
- ;;情况3:返回结果是conda返回结果的第一个元素
- (run* [q]
- (condu
- [succeed
- fail
- (conde
- [(== q 2)]
- [(== q 3)])
- ]
- [(membero q ["yes" "no"])]
- ))
- ;;=> ()
- 当然以上所有逻辑表达式都可以嵌套使用。
(5)matche/matcha/matchu函数:
这三个函数的第一个参数是一个元素,该元素用于匹配后面的表达式组中第一个元素,类似于其它语言中的case函数,如果匹配成功,则执行该表达式组中的其它表达式。该三个函数与conde/conda/condu相对应:matche返回使任意一个表达式组成立的所有元素,matcha只返回所有表达式组中第一个使条件成立的所有元素,而matchu返回所有表达式组中第一个使条件成立的第一个元素: -->matche函数:
- ;;用p去匹配每个表达式组中的第一个元素(这里的第一个不能是表达式,只能是元素),匹配成功则会执行后面的表达式,有没有很像case的赶脚,抛去第一个元素以后的匹配规则与conde一模一样:
- (run* [q]
- (fresh [p]
- (matche [p]
- (['virgin] succeed fail)
- (['olive] (conde
- [(== q 1)]
- [(== q 2)]
- ))
- (['oil] (== q 3)))
- ))
- ;;=> (3 1 2)
-->matcha函数:
- ;;这里的matcha函数还是有一点与conda函数不一样的地方,matcha不会关心每个表达式组中第一个表达式是否匹配成功,就像加了all一样:
- (run* [q]
- (fresh [p]
- (matcha [p]
- ;;这里虽然第一个表达式成功,但是还是会匹配后面的表达式组
- (['virgin] succeed fail)
- (['olive] (conde
- [(== q 1)]
- [(== q 2)]
- ))
- (['oil] (== q 3)))
- ))
- ;;=> (1 2)
-->matchu函数:
- ;;matchu则是返回所有可能匹配的第一个元素,执行规则与matcha类似:
- (run* [q]
- (fresh [p]
- (matchu [p]
- ;;这里虽然第一个表达式成功,但是还是会匹配后面的表达式组
- (['virgin] succeed fail)
- (['olive] (conde
- [(== q 1)]
- [(== q 2)]
- ))
- (['oil] (== q 3)))
- ))
- ;;=> (1)
(6)defne/defna/defnu函数:
这三个函数都是用来定义一个比较特殊的logic函数,这三个函数分别与matche/matcha/matchu三个函数的pattern相匹配,它们的匹配规则如下:
- ;;用defn定义含有matche的函数:
- (defn fun1 [val]
- (match [val]
- ([val1] expr11 expr12)
- ([val2] expr21 expr22)
- ))
- ;;fun1函数与下面的fun2函数等价:
- (defne fun2 [val]
- ([val1] expr11 expr12)
- ([val2] expr21 expr22)
- )
下面是这三个函数的使用示例:
- ;;defne函数:
- (defne exampleo [a b]
- ([:a y] (membero y [:x :y]))
- ([:b x] (membero x [:x :y :z])))
- ;;=> #'insight.main/exampleo(run* [q]
- (fresh [a b]
- (== q [a b])
- (exampleo a b)
- ))
- ;;=> ([:a :x] [:b :x] [:a :y] [:b :y] [:b :z])
- ;;defna函数:
- (defna exampleo [a b]
- ([:a y] (membero y [:x :y]))
- ([:b x] (membero x [:x :y :z])))
- ;;=> #'insight.main/exampleo(run* [q]
- (fresh [a b]
- (== q [a b])
- (exampleo a b)
- ))
- ;;=> ([:a :x] [:a :y])
- ;;defnu函数:(defnu exampleo [a b]
- ([:a y] (membero y [:x :y]))
- ([:b x] (membero x [:x :y :z])))
- ;;=> #'insight.main/exampleo(run* [q]
- (fresh [a b]
- (== q [a b])
- (exampleo a b)
- ))
- ;;=> ([:a :x])
(7)组合类函数:appendo/conjo/conso/featurec函数
-->conjo/conso/appendo: conjo函数/conso函数与clojure.core中的conj函数/cons函数相似,只不过conj/cons返回组合后的结果,而conjo/conso是将组合后的结果当作参数传入,判断三个参数能否组合成功,这两个函数都是元素与数组的组合,而appendo函数则是数组与数组的组合,判断两个数组连接是否可以组成第三数组:
- ;;conjo函数判断将第二个参数加到第一个参数数组的末尾是否等于第三个参数
- (run* [q]
- (conjo q 5 [1 2 3 4 5])
- )
- ;;=> ([1 2 3 4])
- ;;conso函数判断将第一个参数加到第二个参数数组的首部,是否等于第三个参数
- (run* [q]
- (conso 1 q [1 2 3 4 5])
- )
- ;;=> ((2 3 4 5))
- ;;appendo函数判断前两个数组连接是否哦等于第三个数组
- (run* [q]
- (appendo [1 2] q [1 2 3 4 5])
- )
- => ((3 4 5))
至于为什么上述两个返回结果一个是vector,一个是list,这与conj/cons函数的形式一致,可能往首部插时转换为链表形式,而尾部则是数组(我猜的) -->membero函数/featurec函数: membero/featurec函数主要是判断元素的存在性,membero函数某个元素是否属于某个数组,而featurec函数则用于判断某个map是否为另一个map的子集:
- ;;判断第二个map是否为第一个map的子集,这里特别要注意,map中的key一定是要给定的,只能value是未知数
- (run* [q]
- (fresh [q1 q2]
- (featurec {:a 1 :b 2 :c 3} {:a q1 :b q2})
- (== q [q1 q2])
- ))
- ;;=> ([1 2])
- ;;membero函数:
- (run* [q]
- (membero q [1 2 3 4 5])
- )
- => (1 2 3 4 5)
(8)and/or函数
and和or是另一个版本的的all和conde,它们的区别是: ->and和or: 只接收一个列表作为参数,列表里的多个表达式之间的关系是“与”/“或”, ->all函数: 接收多个表达式作为参数,多个表达式之间的关系是“与”, ->conde函数: 接收多个表达式列表作为参数,多个表达式列表之间的关系是“或”
- ;;四个函数输入参数的区别:
- (and* [a b c])
- (or* [a b c])
- (all a b c)
- (conde [a] [b] [c])
下面我们用五重点内容种不同的形式表达:q属于[1 2 3]和[2 3 4]的交集或者q=5
- ;;or*/and*:
- (run* [q]
- (or* [
- (and* [(membero q [1 2 3])
- (membero q [2 3 4])])
- (== q 5)])
- )
- ;;=> (5 2 3)
- ;;or*/all:
- (run* [q]
- (or* [
- (all (membero q [1 2 3])
- (membero q [2 3 4]))
- (== q 5)])
- )
- ;;=> (5 2 3)
- ;;conde/all:
- (run* [q]
- (conde [(all (membero q [1 2 3])
- (membero q [2 3 4]))]
- [(== q 5)])
- )
- ;;=> (5 2 3)
- ;;conde/and*:
- (run* [q]
- (conde [
- (and* [(membero q [1 2 3])
- (membero q [2 3 4])])]
- [(== q 5)])
- )
- ;;=> (5 2 3)
- ;;conde的每个表达式组中的表达式之间默认情况下是“与”的关系
- (run* [q]
- (conde [
- (membero q [1 2 3])
- (membero q [2 3 4])]
- [(== q 5)])
- )
- ;;=> (5 2 3)
(9)clojure普通函数在logic代码中的使用:
通过前面的叙述我们知道,逻辑表达式的返回结果只有两种情况:succeed/fail,如果我们想在clojure.core.logic中调用let、mapv、reduce等clojure普通函数怎么办??我们是不是需要将这些函数的返回结果转换为succeed或者fail。其实这些事情core.logic中已经为我们做好了,它提供了一个project宏专门为我们解决logic代码中调用普通函数的问题:
- ;;因为逻辑变量a/b/c都要被普通函数调用,所以在project的第一个参数列表中将其声明,然后我们就可以在let中调用其他普通函数,最后将结果返回便可,返回的结果一定也是一个逻辑表达式结果
- (run* [q]
- (fresh [a b c]
- (membero a [[1 7] [2 10]])
- (== b [3 8])
- (== c [5 6])
- (project [a b c]
- (let [result (concat a b c)
- result-sum (apply + result)
- result-avg (/ result-sum (count result))]
- (== q result-avg)))
- ))
- ;;输出结果是两个组数组的平均值
- ;;=> (5 17/3)
更多网易技术、产品、运营经验分享请点击。
相关文章:
【推荐】 一个内部增长案例的分享
【推荐】 项目前端打包工具从 NEJ 切换成 webpack
逻辑编程入门--clojure.core.logic的更多相关文章
- Minecraft Forge编程入门三 “初始化项目结构和逻辑”
经过前面两个教程Minecraft Forge编程入门一 "环境搭建"和Minecraft Forge编程入门二 "工艺和食谱",我们大体知道了如何自定义合成配 ...
- 《Web编程入门经典》
在我还不知道网页的基础结构的时候,我找过很多本介绍Web基础的书籍,其中这本<Web编程入门经典>,我认为是最好的. 这本书内容很全面.逻辑很严谨.结构很清晰.语言文字浅显易懂. 看这本书 ...
- 【浅墨著作】《OpenCV3编程入门》内容简单介绍&勘误&配套源码下载
经过近一年的沉淀和总结,<OpenCV3编程入门>一书最终和大家见面了. 近期有为数不少的小伙伴们发邮件给浅墨建议最好在博客里面贴出这本书的文件夹,方便大家更好的了解这本书的内容.事实上近 ...
- 脑残式网络编程入门(五):每天都在用的Ping命令,它到底是什么?
本文引用了公众号纯洁的微笑作者奎哥的技术文章,感谢原作者的分享. 1.前言 老于网络编程熟手来说,在测试和部署网络通信应用(比如IM聊天.实时音视频等)时,如果发现网络连接超时,第一时间想到的就是 ...
- 脑残式网络编程入门(三):HTTP协议必知必会的一些知识
本文原作者:“竹千代”,原文由“玉刚说”写作平台提供写作赞助,原文版权归“玉刚说”微信公众号所有,即时通讯网收录时有改动. 1.前言 无论是即时通讯应用还是传统的信息系统,Http协议都是我们最常打交 ...
- 08 bash特性--shell脚本编程入门
shell脚本编程入门 编程语言介绍 编程语言分为:机械语言.汇编语言和高级语言: 计算机能识别的语言为机械语言,而人类能学习的并且能够方便掌握的为高级语言,所以,我们所编写的程序就要通过编译来转换成 ...
- VS2010/MFC编程入门之五十(图形图像:GDI对象之画笔CPen)
上一节中鸡啄米讲了CDC类及其屏幕绘图函数,本节的主要内容是GDI对象之画笔CPen. GDI对象 在MFC中,CGdiObject类是GDI对象的基类,通过查阅MSDN我们可以看到,CGdiObje ...
- VS2010/MFC编程入门之四十七(字体和文本输出:CFont字体类)
上一节中鸡啄米讲了MFC异常处理,本节的主要内容是字体CFont类. 字体简介 GDI(Graphics Device Interface),图形设备接口,是Windows提供的一些函数和结构,用于在 ...
- python人工智能爬虫系列:怎么查看python版本_电脑计算机编程入门教程自学
首发于:python人工智能爬虫系列:怎么查看python版本_电脑计算机编程入门教程自学 http://jianma123.com/viewthread.aardio?threadid=431 本文 ...
随机推荐
- Centos 7.2基础安装和配置(含分区方案建议)
景:windows桌面运维为主的我,前几天接到一个去某客户上架安装服务器的工作任务,含糊的说要上架几台服务器顺便安装Centos系统,于是我便下载了一个Centos7.2版本的镜像,顺利的用USBwr ...
- 【转】mac os、linux及unix之间的关系
mac os.linux及unix之间的关系 unix 是由贝尔实验室开发的多用户.多任务操作系统 linux是一类Unix操作系统的统称,严格来说,linux系统只有内核叫“linux”,而li ...
- 关于进程exit后,内存释放释放的实践
最近碰到一个问题,或许也是小猿们都会碰到的问题:内存泄露. 都知道malloc后需要free才能释放内存,shmat后需要shmdt才能断掉内存区并使用IPC_RMID命令删除共享内存.那么如果是当前 ...
- 《C语言基础日常笔记》
1. 类型转换-----------------20130902 a, 浮点数(包括单精度与双精度)赋值给整型变量时,舍弃浮点数的小数部分,直接将其整数部分存放在整型变量里. b, 整型变量赋值给浮点 ...
- 前端开发之jQuery位置属性和筛选方法
主要内容: 1.jQuery的位置属性及实例 (1)位置属性 (2)实例 --- 仿淘宝导航栏 2.jQuery的筛选方法及实例 (1)筛选方法 (2)实例一:嵌套选项卡 (3)实例二:小米官网滑动 ...
- Common Lisp
[Common Lisp] 1.操作符是什么? 2.quote. 3.单引号是quote的缩写. 4.car与cdr方法. 5.古怪的if语句. 6.and语句. 7.判断是真假. null 与 no ...
- Go Packages、Variables、functions
[Go Packages.Variables.functions] 1.定义包名. 2.引入Package. 3.定义导出的变量.首字母必须大写. 4.函数.Notice that the type ...
- jmeter 使用cookie管理器
1.jmeter.properties 中 将CookieManager.save.cookies 设置为true 2.添加一个cookie管理器,什么都不用填 3.把需要用到的请求放到登录后面.后 ...
- Linux TCP拥塞控制算法原理解析
这里只是简单梳理TCP各版本的控制原理,对于基本的变量定义,可以参考以下链接: TCP基本拥塞控制http://blog.csdn.net/sicofield/article/details/9708 ...
- KbmMW 4.40.00 测试发布
经过漫长的等待,支持移动开发的kbmmw 4.40.00 终于发布了,这次不但支持各个平台的开发, 而且增加了认证管理器等很多新特性,非常值得升级.具体见下表. 4.40.00 BETA 1 Oct ...