十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Prolog实验报告怎么写?从逻辑拆解到回溯验证的完整指南

Prolog实验报告怎么写?从逻辑拆解到回溯验证的完整指南 带过几轮Prolog课程之后我越来越确认一件事大多数学生不是不会写代码而是不会写实验报告。他们的Prolog实验报告通常是“整个代码版粘贴 一句运行成功”然后丢分也不知道丢在哪。Prolog实验报告真正要写的内容不是代码本身而是代码背后的逻辑拆解、查询设计与回溯验证。题目做出来了报告没写明白等于这个实验只完成了一半。这篇文章我想把Prolog实验报告的完整写法拆开讲。从报告的整体骨架到每一部分该写多少字、怎么组织再到递归、列表、搜索回溯、数值计算这四类高频实验题分别怎么下笔最后用一道“家族关系查询”把报告的正文部分整个走一遍。末尾还会整理几个我批改报告时几乎每届都会遇到的坑都是可以直接拿出来对照检查的。1. 写报告前先接受一个事实Prolog的“过程”不是一步一步算而是声明关系很多同学第一次接触Prolog会下意识用C语言或者Python那套“顺序执行”的思路去读程序结果越读越懵。原因在于Prolog不是命令式语言而是声明式语言。你写下来的不是一个“算法流程”而是一组“关于世界的陈述”和一个“如何根据陈述推出结论”的规则。1.1 三条代码就能定义一个程序报告的重点自然也就不一样先看一个最简单的例子parent(tom, bob). ancestor(X, Y) :- parent(X, Y).第一行是事实tom是bob的父/母关系中“上一位”的那个人。第二、三行是规则如果X是Y的parent那么X是Y的ancestor。它没有“开始”“循环”“结束”的概念也没有“赋值”的过程。你写下的是一组约束查询则像是对这道逻辑题提问。所以写报告时如果还按照“程序流程图代码说明”的思路来写就会很别扭。Prolog实验报告要回答的核心问题是这几个题目要求我把问题拆成哪些谓词每个谓词的参数代表什么含义哪些信息用事实表达哪些关系用规则表达为什么这样分查询时预期得到什么结果实际得到什么结果如果不一样原因是回溯顺序、规则顺序还是事实库写错了把这些回答清楚报告的主干就出来了。1.2 老师想从报告里看到“逻辑式拆解能力”我带实验课的时候最不愿意看到的报告样式是把代码从头到尾贴一遍每个子句下面补一句注释然后结论写“运行成功”。这只能说明你复制粘贴了不能说明你理解了Prolog。有区分度的报告通常会有这样一个段落把题目中的自然语言描述转换为“谓词 参数 约束”。比如题目说“写一个判断某元素是否在列表中的程序”你不能只贴代码你要写出member(X, [X|_]). member(X, [_|T]) :- member(X, T).然后在旁边解释第一条子句表示“如果X是列表头那么X在列表里”第二条子句表示“如果X不在当前头部那就递归地在尾部继续查找”。这两句话比十行注释都有用。2. 六段式结构从实验目的到心得体会每一段写多少字都有讲究有同学觉得报告没东西可写其实是不清楚该写哪些板块。一个标准的Prolog实验报告通常在封面之后包含六块内容实验目的、实验环境、题目描述与问题分析、程序实现、运行结果与测试用例、实验总结与心得。我分别说说每一块怎么落笔。2.1 实验目的一句话对应一个知识点这一块不用长篇大论但也不能写空话。常见的错误写法是“了解Prolog”“掌握逻辑式编程语言的基本概念”很泛读起来像凑字数。好的写法是能对应到具体知识点。比如掌握用事实和规则描述实体间关系的方法理解递归式规则在传递性关系如祖先、路径查找中的作用能通过回溯机制观察Prolog的搜索顺序并在报告中解释结果顺序的差异熟练使用is表达式完成数值计算并能区分“合一”与“算术赋值”。每一条都指向你即将要做的实验内容后面写正文的时候这些目的也自然会被验证。2.2 实验环境把版本和运行方式写清楚别让老师去猜Prolog有多个实现最常用的是SWI-Prolog但实验环境不写版本是很常见的疏漏。建议写明操作系统及版本比如 Windows 11 或 Ubuntu 22.04解释器比如 SWI-Prolog 9.x运行方式是在命令行swipl交互式查询还是用内置IDE。如果报告后面涉及调试过程还可以在这里补一句“使用trace.模块观察递归调用过程”。这样老师看到后面的“调试记录”时会知道你的环境支持什么工具。2.3 题目描述与问题分析这是整份报告篇幅最大、最值钱的部分不要直接把题目原文抄一遍而是用自己的话重新描述一遍然后给出“谓词设计表”。这个表非常实用适合大部分Prolog实验谓词参数含义类型事实/规则作用parent/2parent(X, Y)X是Y的父母事实存储已知的亲子关系ancestor/2ancestor(X, Y)X是Y的祖先规则定义祖先的递归规则这样一个表列出来读者一眼就知道你打算怎么建模。后面写规则的时候你只需要说“ancestor规则由两部分组成直接parent关系和递归parent关系”代码就顺理成章地出现不会有“从天而降”的感觉。这一块的篇幅建议占整份报告的四成以上。不要嫌多因为老师最想看的其实是你如何把自然语言转换成Prolog程序而不是你敲了多少行代码。2.4 程序实现代码一定要注释到“行为意图”这一层贴代码本身没有错错的是不注释、不分组。Prolog的代码很短所以良好的组织方式是分成三段事实库辅助规则核心规则。每个子句至少加一行注释说明这个子句“在什么条件下成立”。比如% 基础情况空列表长度为0 my_length([], 0). % 递归情况去掉头元素后长度加1 my_length([_|T], N) :- my_length(T, M), N is M 1.这里重点不是解释语法而是解释“为什么这个子句是必要的”。特别是递归程序里的基础情况必须单独标注因为它对应终止条件。2.5 运行结果与测试用例别只给一个成功的输出很多报告在结果部分就放一张截图里面只有一个查询返回true。这种情况最大的问题是没有说服力因为你只测了一条正常路径边界情况完全没覆盖。我建议用表格来呈现测试用例把每个查询的“预期输出”和“实际输出”写清。比如查询预期结果实际结果说明?- ancestor(ann, sam).truetrue直接亲子关系?- ancestor(bob, sam).truetrue隔一代递归?- ancestor(tom, sam).truetrue隔两代递归?- ancestor(lisa, ann).falsefalse反向关系应不成立这样老师可以快速验证你的程序是否正确也说明你有意识地去测试了边界和反例。2.6 实验总结与心得从“我学会了”升级为“我踩了什么坑、如何解决”学生写实验心得时最常写的两句话是“通过这次实验我进一步了解了Prolog。”这句话本身没有错但是没有信息量。更有价值的写法是记录一个具体问题和一个具体方案。比如“在编写递归规则时我一开始漏写了边界条件导致查询ancestor(sam, X)时程序陷入无限递归。后来使用trace.逐步追踪发现程序不断尝试parent(sam, Z)并继续调用ancestor始终没有遇到能直接成功返回的子句。我在事实库中检查后确认sam没有孩子节点这才意识到需要先列出所有事实再设计递归规则避免把无限域当成有限域处理。”这种文字写到报告里比任何空泛的总结都有分量。因为它证明了你经过了真正的调试和思考。3. 四类高频实验题的写法重点递归、列表、搜索回溯、数值计算不同课程的Prolog实验题五花八门但仔细拆开大部分题目可以归到四类。每一类在写报告的时候侧重点不太一样。3.1 递归类实验把“递推关系 出口”两件事写明白经典的阶乘程序是这样的factorial(0, 1). factorial(N, F) :- N 0, N1 is N - 1, factorial(N1, F1), F is N * F1.写报告时先写数学递推公式factorial(0) 1factorial(N) N * factorial(N - 1)当 N 0然后用文字解释代码如何对应公式第一条子句是出口第二条子句是递推关系。这一步做完代码可以不急贴先让读者看到你的建模过程。运行结果部分给出几个有代表性的查询?- factorial(5, F). F 120. ?- factorial(0, F). F 1.最后留一段简短分析说明为什么必须把factorial(0, 1)写在规则前面。实际原因在于Prolog按子句顺序搜索如果把递归规则写在最前面且边界条件无法直接匹配程序依然可能进入无限递归或产生错误解。3.2 列表操作类head/tail模式匹配是灵魂列表是Prolog里非常高频的数据结构。所有列表都可以用[H|T]拆成头元素和尾列表。比如计算长度my_length([], 0). my_length([_|T], N) :- my_length(T, M), N is M 1.报告里要解释_是什么。_是匿名变量表示“我不关心这个元素具体是什么它唯一的作用是证明列表不为空”。很多初学者会把[H|T]里的H误认为一定要用其实题目如果只关心长度H就用不上但列表结构依然需要匹配所以用_占位。运行结果可以这样展示?- my_length([a, b, c], N). N 3.如果你实验题目是“列表反转”那还要专门分析append的使用。比如my_reverse([], []). my_reverse([H|T], R) :- my_reverse(T, TR), append(TR, [H], R).报告的“问题分析”部分最好写一句这里不是把H直接插入到结果头部而是先反转尾部再把H拼到最后面。这句话说出来说明你真的理解了递归过程而不是背代码。3.3 搜索与回溯类用“答案顺序”证明你理解了回溯搜索类题目的代码通常很短但报告很难写因为“回溯”是个动态过程。拿路径查找来举例link(a, b). link(b, c). link(c, d). path(X, Y) :- link(X, Y). path(X, Y) :- link(X, Z), path(Z, Y).查询?- path(a, d). true.如果报告只写到这一步老师会认为你只是把例子跑通了。更好的做法是写一段“回溯顺序分析”“查询 path(a, d) 时Prolog先尝试第一条子句 link(a, d)匹配失败再进入第二条子句把link(a, Z)与事实库匹配首先得到Z b然后递归调用 path(b, d)。在path(b, d)中先尝试link(b, d)失败再尝试link(b, Z)得Z c继续递归path(c, d)这次第一条子句link(c, d)匹配成功。整个过程是深度优先搜索并沿着事实库的定义顺序选择分支。”这段话看起来很普通但它清晰地呈现了你对算法细节的把握。如果你在实验中还遇到了环路死循环那更要写进去当事实库出现link(b, a)这类反向边时上述程序会反复横跳。解决办法通常有两种限制路径长度或者维护一个“已访问节点”列表。哪怕你的实验不做环路处理在报告中写一句“如果加入环路该程序会失效需要额外维护访问记录”也会让老师觉得你思考得比题面更远。3.4 数值计算与is关键字这里写不写清楚直接影响分数Prolog里的不是赋值而是“合一”它不会自动执行算术运算。例如?- X 2 3. X 23.看到没Prolog返回的是一棵结构树而不是5。想让算术表达式真正求值必须用is?- X is 2 3. X 5.这个区别如果不在报告中交代后面的阶乘、斐波那契、汉诺塔计数就都说不清。建议在报告里专门写一小段“实现数值计算时我使用is而不是因为只做模式匹配不做表达式求值。例如N1 is N - 1会把右侧算术表达式的值计算出来后与N1合一若写成N1 N - 1N1得到的将是一个未求值的表达式后续比较或计算都会出错。”这段话是画龙点睛之笔很多同学在上面吃过亏但报告里几乎没有提到。4. 手把手演示把一道“家族关系”题完整写成报告正文前面的方法都比较抽象我拿一道很常见的“家族关系查询”练习题完整演示一遍报告正文应该怎么组织。这道题也经常被用作文末总测试。4.1 题目与谓词设计题目要求给定一组亲子关系事实实现“祖先查询”并分析查询结果顺序。第一步先设计事实库parent(tom, bob). parent(tom, lisa). parent(bob, ann). parent(ann, sam).这里parent(X, Y)表示X是Y的父母。题目没有要求区分父亲和母亲所以一个谓词就够了。如果你为了让规则更有扩展性也可以拆成father/2和mother/2再用一个辅助规则合并。那种设计在实验报告里值得额外说明因为它体现了对数据建模的思考。祖先规则用递归实现ancestor(X, Y) :- parent(X, Y). ancestor(X, Y) :- parent(X, Z), ancestor(Z, Y).报告里要解释第一条子句表示“父母也是祖先”第二条子句表示“如果你的父母中某人是某人的祖先那么你也是那人的祖先”。这里能明显看出Prolog规则的自指性。4.2 带注释的程序源码报告中按这个格式贴代码注释层级要清晰% 事实库定义直接亲子关系 parent(tom, bob). parent(tom, lisa). parent(bob, ann). parent(ann, sam). % 规则1直接亲子关系即祖先关系 ancestor(X, Y) :- parent(X, Y). % 规则2祖先关系可递归传递 ancestor(X, Y) :- parent(X, Z), ancestor(Z, Y).贴完代码之后不要立刻截图先用一两段话解释为什么规则要分成两条。第一条处理“X就是Y的父辈”第二条处理“隔了多代祖先”。如果未来事实库中有隔了三代、四代的祖先第二条规则会自动适配。4.3 测试用例设计与运行结果同样是表格我建议多设计几个方向查询预期结果实际结果说明?- ancestor(tom, bob).truetrue一级祖先?- ancestor(tom, sam).truetrue通过bob、ann传递?- ancestor(sam, tom).falsefalse反向关系不成立?- ancestor(X, sam).X ann, X bob, X tomX ann, X bob, X tom查询所有祖先?- ancestor(sam, X).falsefalsesam没有后代无解交互式运行结果可以这样粘贴?- ancestor(X, sam). X ann ; X bob ; X tom ; false.看到这个结果很多学生会直接复制粘贴就算完事。但报告里如果加一段“为什么是这个顺序”的分析分量就完全不一样了。4.4 结果顺序分析这才是报告里最出彩的一段为什么会先输出ann再输出bob最后是tom原因是Prolog的深度优先搜索和子句顺序。具体来说查询ancestor(X, sam)时Prolog先尝试第一条子句ancestor(X, Y) :- parent(X, Y)也就是直接匹配parent(X, sam)。事实库中只有parent(ann, sam)匹配成功所以第一个解是X ann。用户按分号要求下一个解后Prolog回到第二条子句尝试parent(X, Z), ancestor(Z, sam)。首先从parent(X, Z)开始按事实库顺序尝试第一次得到X tom, Z bob接着验证ancestor(bob, sam)这又需要递归最终通过parent(bob, ann) parent(ann, sam)得出成立因此第二个解是X tom。parent(X, Z)继续回溯依次得到X bob, Z ann此时ancestor(ann, sam)直接成立于是第三个解是X bob。再往后的组合都无法满足条件最终返回false。这段分析写在报告里老师会认为你确实理解了解释器的搜索机制。而“搜索顺序”正是Prolog课程里面最重要的思想之一。5. 批改常见扣分点这六个坑我每年都会遇到最后说一些带实验课经常看到的问题。这些问题不会直接导致代码错但会直接影响报告质量甚至让人觉得你实验没做透。5.1 变量大写、原子小写报告里出现了手写体错误Prolog对大小写极其敏感。Parent(tom, bob).会被当成变量完全变了个意思。这种低级错误在代码里很容易查但在报告里经常出现“手敲文字时把原子写成大写”的情况。最终报告中的代码一定要从解释器复制不要自己重新输入这是最基础的技巧。5.2 逗号与分号的含义搞混Prolog里的逗号表示“逻辑与”分号表示“逻辑或”。比如规则ancestor(X, Y) :- parent(X, Y); parent(X, Z), ancestor(Z, Y).虽然也能表达出祖先关系但可读性差很多而且括号优先级容易让人误解。查询结果里的分号则是“求下一个解”的用户指令这是另一回事。报告里如果展示多个答案一定要把查询后的分号也保留否则输出不完整。5.3 递归少了终止条件查询直接卡死这是Prolog实验中最常见的运行时问题。递归规则如果没有匹配到基础情况程序会无限递归最后栈溢出。报告里写调试过程时建议把trace输出摘一段Call: (14) ancestor(sam, _G123) ... Call: (15) parent(sam, _G456)从Call层级不断递增就能看出程序正在无限向深处搜索。这个截图或片段出现在报告里就是很扎实的排错证据。5.4 截图和代码版本对不上有的同学代码改了一版截图还是旧版本的输出明眼人一看就不一致这种报告基本会打回重写。我给你的建议是所有结果截图必须在代码最终确认之后再跑一遍。跑完就把截图命名成result_1.png、result_2.png别用“新建文件夹.png”这种名字。5.5 复制教材代码却从不做任何改造实验报告如果真的只是抄了教材代码那至少把谓词名称改掉几个、增加一组自己的测试数据。写报告时可以加一个“我对代码做的扩展”小节比如在家族关系实验中把单层的parent/2扩展成father/2与mother/2在列表反转实验中增加对空列表和单元素列表的测试。这些扩展很小但能体现实验的独立完成度。5.6 只写“运行成功”不写“如何证明正确”这是最普遍的问题。老师关心的不是你的程序跑没跑通而是你敢不敢从多方面验证它。在一个查询返回true之外至少还应该有一个预期为false的查询来证明你的代码不是“所有结果都返回成立”。这类反例一定要写进测试表格同时配一句话解释这个反例验证了规则不会过度推断关系。Prolog实验报告写得好不好很大程度上取决于你有没有把“思考过程”当成实验成果的一部分。代码只是最终的结论而“为什么这样写、查询结果为什么是这个顺序、哪些边界条件需要考虑”才是实验里真正值得沉淀的东西。下次写报告时把重点从“贴代码”移到“讲推理”上你会发现能写的内容比想象中多得多。
返回列表