☰
Programming from A to Z:Session 7 上下文无关文法课后作业实战指南(基于 website-archive 仓库)
2026/10/14 14:55:49 网站建设 项目流程
  • 教程
  • 文档
  • 示例工程

【免费下载链接】website-archive

Archive of the Coding Train website (first version)

项目地址:https://gitcode.com/gh_mirrors/we/website-archive
点击查看免费下载

导读

本篇指南围绕《Programming from A to Z》ITP 课程 Session 7(上下文无关文法)的课后作业展开,结合本仓库中对应的课程文档与 CC_043_ContextFreeGrammar 完整实现,系统讲解上下文无关文法(Context-Free Grammar, CFG)的核心概念、两大 JavaScript 工具库(Tracery 与 RiTa.js)的使用方法、从零手写递归展开器的原理,以及一套可直接上手练习的课后作业清单。读完后,你将能够用纯 JavaScript 定义文法规则、生成随机故事文本,并理解 BNF/EBNF 与乔姆斯基谱系在文本生成中的实际意义。

一、作业背景:Session 7 讲了什么

本节作业文档 _learning/programming-with-text/7.4-homework-assignment-session-7.md 是 Session 7 的收尾视频(video_number: 7.4),它本身只有一句话:"In this last video of Session 7, I discuss exercises you can try related to Context-Free Grammars."—— 即围绕上下文无关文法给出可动手实践的练习。

要理解这些练习,必须先掌握前几节视频打下的基础(这些文档同样保存在本仓库中):

  • 7.1-introduction.md 给出了本 Session 的核心定义:上下文无关文法是一组递归的"替换"规则,用来生成文本。该视频还预告了课程会介绍两个 JavaScript 库(Tracery 与 RiTa.js),并会从零手写一个基本的上下文无关文法系统;
  • 7.2-context-free-grammar-with-tracery.md 演示如何用 Kate Compton 的 Tracery 库,从一组文法规则生成一个故事;
  • 7.3-context-free-grammar-with-ritajs.md 演示如何用 Daniel C. Howe 的 RiTa.js 库进行基于文法的文本生成。

因此,本节课后作业本质上是"验证并深化"以上三块内容:理解概念、用现成库、自己实现。仓库中的 CC_043_ContextFreeGrammar 正是"从零手写"部分的成品代码,下文将以其为源码依据展开讲解。

二、上下文无关文法:递归替换规则的本质

一个上下文无关文法由**非终结符(non-terminal)与终结符(terminal)**构成:

  • 非终结符是需要被进一步展开的符号,通常写作大写字母或缩写(如S、NP、VP);
  • 终结符是最终出现在输出文本中的词(如cat、meows);
  • 每条**产生式规则(production rule)**把一个非终结符替换为一个由终结符和非终结符组成的序列;
  • 规则之间允许递归:一个非终结符的展开中可能再次出现它自身,这正是文法能产生无限多样句子的原因。

以仓库 CC_043_ContextFreeGrammar/P5/sketch.js 中的规则为例,这是一份可以直接放进浏览器的完整文法定义:

var rules = { S: [ ['NP', 'VP'], ['Interj', 'NP', 'VP'] ], NP: [ ['Det', 'N'], ['Det', 'N', 'that', 'VP'], ['Det', 'Adj', 'N'] ], VP: [['Vtrans', 'NP'], ['Vintr']], Interj: [['oh'], ['my'], ['wow'], ['darn']], Det: [['this'], ['that'], ['the']], N: [ ['amoeba'], ['dichotomy'], ['seagull'], ['trombone'], ['overstaffed'], ['corsage'] ], Adj: [ ['bald'], ['smug'], ['important'], ['tame'], ['overstaffed'], ['corsage'] ], Vtrans: [['computes'], ['examines'], ['foregrounds']], Vintr: [['coughs'], ['daydreams'], ['whines']] };

观察这份规则:

  • S(句子)可以被展开为NP VP或Interj NP VP,这里产生了句子结构上的变化;
  • NP(名词短语)有三条候选规则,其中第二条Det N that VP引入了从句,让句子可以嵌套;
  • VP(动词短语)既能接及物动词短语(Vtrans NP),也能接不及物动词(Vintr);
  • N、Adj、Vtrans等是"叶子"符号,只提供词项。

候选规则的选取方式决定输出多样性:每次展开时从该符号的候选数组中随机挑一条。这也是"从零实现"的关键点,见下文第三节。

三、从零手写:递归展开器的工作原理(源码级剖析)

作业的核心练习之一就是"自己写一个基本的上下文无关文法系统"。仓库中的 sketch.js 用一个不到 15 行的递归函数完成了全部展开逻辑:

function expand(start, expansion) { if (rules[start]) { var pick = random(rules[start]); console.log(pick); for (var i = 0; i < pick.length; i++) { expand(pick[i], expansion); } } else { expansion.push(start); } return expansion.join(' '); }

理解这段代码的四个要点:

  1. 入口判断:if (rules[start])检查当前符号是否在规则表中。存在即是非终结符,需要继续展开;
  2. 随机选择:random(rules[start])从该符号的所有候选中随机选一条产生式(此处为 p5.js 提供的全局random());
  3. 递归遍历:遍历选中产生式中的每个元素,对每个元素再次调用expand()。若元素仍是非终结符(如NP),会继续深入展开;若是终结符(如'this'),则执行else分支;
  4. 终止条件与拼接:只有终结符才会被expansion.push(start)收集进结果数组,最终用join(' ')拼成句子。

举例说明一次展开轨迹:从S开始 → 选中['Interj','NP','VP']→Interj展开为'wow';NP选中['Det','Adj','N']→Det→'the'、Adj→'smug'、N→'seagull';VP选中['Vintr']→'daydreams'。最终得到wow the smug seagull daydreams。

配套的 index.html 展示了运行环境:页面通过 CDN 引入 jQuery 与 p5.js,本地引入libraries/tracery.js与sketch.js。交互部分在 setup() 中实现——noCanvas()关闭画布,createButton('generate')创建按钮,点击后调用cfg()从S出发展开并用createP(result)把结果写到页面上。

这就是一个可复制、可运行的完整"手写 CFG"最小示例。课后练习可以先从改这份规则开始(增加词项、增加规则分支),再尝试为展开器添加递归深度限制、去重、统计等能力。

四、用 Tracery 生成故事:库的使用与内部机制

如果不想从零实现,可以直接使用 Kate Compton 的 Tracery 库(视频 7.2 演示了用它从文法规则生成故事)。仓库 CC_043_ContextFreeGrammar/P5/libraries/tracery.js 保存了完整的 Tracery 源码(1223 行),可以从中看清库的设计。

4.1 基本用法

Tracery 的典型工作流是三步:定义文法对象 →tracery.createGrammar(obj)加载 →grammar.flatten('#start#')或grammar.expand()生成文本。库的核心入口位于 tracery.js#L1186-L1190:

tracery.createGrammar = function(obj) { var grammar = new Grammar(); grammar.loadFrom(obj); return grammar; };

Grammar.prototype.loadFrom(tracery.js#L970-L993)会遍历对象的所有键,为每个键创建一个Symbol并加载其规则。Tracery 的文法对象与前面sketch.js的rules结构相似,但规则写法通常是字符串并支持#符号#语法。

4.2 展开与压平:expand 与 flatten

Tracery 把一次生成建模成"展开树":expand(raw)创建一棵以原始模板为根的树并递归展开,返回根节点对象;flatten(raw)在展开后直接返回拼接好的childText字符串(tracery.js#L1131-L1149)。展开树由三类节点组成(tracery.js#L668-L813):

  • RootNode:整棵树的根,负责解析模板并创建子节点;
  • TagNode:对应一个#符号#标签,展开时会从文法中取规则、递归展开子节点,再按需施加修饰符(modifier);
  • TextNode:纯文本叶子节点,finalText就是自身文本。

TagNode.expand的执行顺序(tracery.js#L752-L785)体现了库的精髓:先取规则并解析 → 创建子节点 → 执行前置动作(pre-action)→ 展开子节点 → 执行后置动作 → 对结果文本依次应用修饰符。

4.3 内置修饰符:让文本更自然

Tracery 内置了一批"通用修饰符"(universal modifiers,tracery.js#L550-L619),通过#animal.s#、#name.capitalize#这类语法在标签后加点调用:

修饰符作用源码位置
capitalizeAll每个单词首字母大写tracery.js#L551-L556
capitalize首单词首字母大写tracery.js#L558-L561
inQuotes给文本加引号tracery.js#L563-L565
comma追加逗号(句末标点自动跳过)tracery.js#L567-L578
beeSpeak把s替换为zzz(蜜蜂语趣味效果)tracery.js#L580-L585
a根据元音/辅音自动加a或antracery.js#L587-L592
s复数化(处理y/x/z/h等词尾)tracery.js#L594-L619

修饰符按名称注册在Grammar.prototype.modifiers中,applyMod(modName, text)负责查表并调用对应函数(tracery.js#L1101-L1105)。做"故事生成器"类作业时,组合使用#N#与.s、.a、.capitalize能明显提升文本的自然度。

4.4 动作系统:临时改规则

Tracery 还支持用[key:value]语法在标签内执行动作(action),通过Symbol.prototype.pushRules / popRules(tracery.js#L865-L878)实现"展开期间临时压入/弹出规则集",让上下文可以影响后续展开。库自带的 tracery.test() 中就有大量动作与修饰符组合的解析用例,例如#hero# ate some #color# #animal.s#与#[hero:#name#]story#,是了解语法边界(合法/非法输入)的最佳参考。

五、用 RiTa.js 做文法生成:另一条路线

视频 7.3 演示了用 Daniel C. Howe 的 RiTa.js 完成同样的任务。RiTa.js 是一个面向创意编码的文本处理库(仓库文档 3.1-the-ritajs-library.md 属于同一课程体系的第三章),它提供了包括文法生成(grammar)在内的词性标注、分词、押韵、随机文本等能力。

与 Tracery 相比,两条路线的差异值得作为作业对比点:

  • Tracery:专注文法展开,规则书写直观(#符号#+ 修饰符 + 动作),生态完整,适合快速搭建故事/对话生成器;
  • RiTa.js:库更大、功能更广(NLP 能力),文法生成只是其中一部分,适合与词性分析、押韵等功能组合使用。

从本仓库的课程文档结构看,7.3 文档 仅保留了库的出处与视频定位,实际用法建议结合 RiTa.js 官方示例(后续 Session 3 的 3.1 文档 提供了课程内的使用脉络)进行练习。作业中可以尝试"同一份文法分别用 Tracery 和 RiTa.js 实现",对比两者的规则语法差异与输出效果。

六、阅读作业:从 BNF 到乔姆斯基谱系

本节作业文档在 links 区列出的资料中,有一部分属于"理论阅读"型作业,它们共同构成了理解 CFG 的理论坐标系:

  • Matt Might 的 The Language of Languages:讲解 BNF 与 EBNF(巴科斯-瑙尔范式及其扩展)的文章,让你把"文法即规则"的直觉上升到形式语言理论层面。BNF/EBNF 是描述编程语言语法的事实标准,读完它你就能看懂语言规范文档中的产生式写法;
  • 乔姆斯基谱系(Chomsky Hierarchy):把形式文法分为 0 型(无限制)、1 型(上下文相关)、2 型(上下文无关)、3 型(正则文法)四个层级。上下文无关文法正是谱系中的2 型文法——理解它在谱系中的位置,有助于把握 CFG 的能力边界(例如它无法表达某些上下文相关的约束);
  • Allison Parrish 的 Reading and Writing Electronic Text(rwet-examples):一套创意写作的电子文本课程示例集合,其中包含大量基于文法、概率模型的文本生成实践,适合作为作业灵感库。

阅读建议的验证方式:用本仓库手写的展开器对照 BNF 写法,把sketch.js的rules转写为 EBNF 形式,体会两者在表达力上的对应关系。

七、项目型作业:从"玩具文法"到"创作机器人"

作业文档列出的另外两类资源指向更完整的创作型项目:

  • Jeffrey Thompson 的 Art Assignment Bot:一个在 Twitter 上自动发布艺术作业的机器人(bot),是"文法规则 + 定时发布 + 社交平台"组合的典型范例;
  • Interruption Junction:交互式打断装置作品,展示了文法生成从"文本输出"走向"交互体验"的可能性。

这两类项目揭示了 CFG 作业的三个进阶方向:

  1. 接入真实数据与输出渠道:文法生成的句子通过脚本定时发布(bot 模式);
  2. 加入交互:用户输入影响文法展开路径(interactive 模式);
  3. 组合多个生成层:艺术、叙事、声音等多模态拼接。

在本仓库范围内,你可以先把 sketch.js 的按钮生成改成"连续生成多句、拼成段落",再为文法引入"场景符号"(如Scene、Character、Conflict),逐步向一个小型"故事引擎"演进——这正是 Art Assignment Bot 类项目的最小可行原型。

八、作业清单汇总与验收标准

综合本节作业文档与前序视频(7.1 预告、7.2 Tracery、7.3 RiTa.js),建议按以下清单逐项完成并自检:

序号练习建议工具/依据验收标准
1理解 CFG 概念,能把规则写成产生式7.1 文档、BNF 阅读能解释非终结符/终结符/递归替换,能手写一组规则
2从零实现递归展开器sketch.js输入S可输出语法合理的随机句子,多次运行结果不同
3扩充文法词汇与结构sketch.js的rules对象新增至少 2 个非终结符与 10 个词项,输出无明显"卡死"或空串
4用 Tracery 生成故事tracery.js能写含#符号#、修饰符、动作的模板并flatten出完整故事
5用 RiTa.js 做文法生成7.3 文档同一文法在 RiTa.js 下可运行,并记录与 Tracery 的语法差异
6理论阅读乔姆斯基谱系、BNF/EBNF能用一句话说明 CFG 在乔姆斯基谱系中的位置
7创作型扩展(选做)rwet-examples、Art Assignment Bot 等完成一个包含交互或自动发布的文法生成小作品

一个额外的源码级自检点:Tracery 自带的 tracery.test() 包含多组"合法/非法"解析用例(例如未闭合的#someSymbol应报错),可作为你理解规则解析边界、避免写出错误模板的对照测试集。

九、小结

本节课后作业表面上只有一句"讨论与上下文无关文法相关的练习",但结合 7.1–7.3 课程文档 与仓库中的 CC_043_ContextFreeGrammar 实现,可以拆解出清晰的练习梯度:概念理解(递归替换规则)→ 手写实现(递归展开器)→ 库的使用(Tracery/RiTa.js)→ 理论深化(BNF 与乔姆斯基谱系)→ 创作整合(故事生成器、bot、交互作品)。

其中"手写实现"是理解 CFG 的最佳捷径——仓库 sketch.js 十几行代码就复现了 Tracery 展开树的核心逻辑;而 Tracery 源码(tracery.js)则为"库做了什么"提供了逐行可查的证据。建议按第八节的清单完成作业,并从"改词表"开始,逐步向"多符号、带修饰符、支持动作"的完整文法引擎推进。

  • 教程
  • 文档
  • 示例工程

【免费下载链接】website-archive

Archive of the Coding Train website (first version)

项目地址:https://gitcode.com/gh_mirrors/we/website-archive
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询