先看工具箱
五个程序按日期排:计算.e(2016-03-09)、计算无括号.e(2016-03-11)、简单的猜数字游戏(2016-03-29)、计算器 (2).e(2016-04-03)、XXX乘以XX23等于XX13计算.e(2018-07-26)。跨度两年多,但把源码并排放开,用到的命令翻来覆去就是那几个:
- 寻找文本:找子串第一次出现的位置,找不到返回 -1。这是全套程序里唯一的位置来源;
- 分割文本:按分隔符切成文本数组;
- 取文本左边 / 右边 / 中间:按位置和长度切片;
- 文本替换:按位置替换固定长度;
- 到整数 / 到数值:把文本拽回数字。
没有任何一个程序调用现成的求值接口——求值、解析、运算符分发,全部是这几个字符串原语手工搭出来的。下面按两个方向展开:把文本切出数字来算(求值器),和把数字写进文本里去试(填洞)。
值得先点破一件事:这套手法里唯一产出数字的命令是 寻找文本,而它在五个程序里同时扮演三种角色——跟 -1 比是存在性检查,位置互相比大小就是优先级表,相减就变成切片长度和扫描游标。所谓"手写求值器",剥到底就是把这一个返回位置的命令按不同形状组织起来。
计算无括号.e:一级一级的折叠机
公开子程序就一个:计算(内容 文本型) 返回文本型,测试调用是 计算 (“4*5*4/8”)。流程读下来是一台单层折叠机:
分割文本 (临时, “*” )切出成员,a、b取前两号,临时数(从 1 起步的整数累加器)连乘a × b;- 每乘一轮,用
取文本右边按a、b的长度把剩余文本从左边啃掉,减的那个 2 是两个*分隔符本身; - 文本里再找不到
*时,检查剩下部分:有/就按/切段往下除,没有就临时数 × 到整数 (临时)收摊。
条件表达式里确实出现了 “+” 和 “-” 的字样,但进了分支之后只对 / 做分发——加减没有任何处理路径,剩余文本被 到整数 整个吞掉(到整数从头扫,扫到非数字就停)。也就是说这一版只吃乘除链,文件名里"无括号"三个字是实话。
再留意它推进的方式:“已经吃进多少文本"不单独记变量,而是每轮用成员长度重新算进 取文本右边 的切片参数里,指针状态完全靠长度算术维持。这个靠位置反复重写手中窗口的做法,是后面几个程序共用的底层动作。
半成品痕迹也能直接读到:局部变量 判断 被赋了三次真,从头到尾没有一处读它;累乘式无条件乘 b,可 b 只在成员数 ≥ 2 的分支里才被赋值——写完乘法没兜住除法的半途状态。
计算.e:23KB 摊开的一张优先级表
这批里最大的容器:27KB 的 .e,装着 23KB 手写文本源码,没有窗体,整个工程只有一个公开子程序 计算(计算内容 文本型) 返回文本型,外加一个声明了但在可见代码里从未被用到的程序集变量 小括号。唯一的调用发生在 _临时子程序 里的一句 信息框 (计算 (“(1+1)”))——那是易模块模板自带的测试脚手架,模板注释自己写明这段在编译发布前会被系统自动清空。所以这个工程的真身是单函数库,用例只有这一个。
函数体是把"优先级 + 括号"手工展开成 if 条件链,展开模式是:圆括号、方括号、花括号各摊开一整段,每段内部再按 *、/、+、- 摊开四个判断块。决定"先算谁"的手段不是栈也不是文法,而是寻找文本返回位置的比较链。比如乘法块的进入条件,反着写更清楚:如果括号段的文本里找不到 *,或者 * 的位置排在 / 的后面,就跳过;除法则要求 / 排在 * 前面;加法要求 + 同时不排在 *、/、- 后面;减法同理。把"或"字链拆回数学语言,就是一张比较表:乘除一级先于加减一级,同级谁的位置靠前谁先算。教科书里优先级藏在文法或双栈的比较里,这里被一个条件对一对地手写摊平——23KB 体积的全部来源就是三种括号 × 四个运算符 × 每对运算符一次位置比较的展开。
算某一级运算时,操作数直接用 取文本左边 / 取文本右边 从运算符位置切出来,两个文本切片直接进 × ÷ + -——从函数开头到结尾没有出现过一次显式的到整数/到数值,类型转换整个指望运算符自己消化。所有分支都不命中时,函数走到最后一行 返回 (“NO”):求值器的失败哨兵是一个字符串。
再细看切片本身:左操作数的长度是"括号段里 * 的位置减 ( 的位置”,右操作数是") 的位置减 * 的位置"——全是段内相对偏移,可 取文本左边 / 取文本右边 切的是整条 计算内容,段自己的起点根本没加进去。控制流结构也值得对照:计算无括号.e 是循环啃文本,这台是一个循环都没有的纯 if 瀑布,三种括号自上而下各走一遍,走完就掉进 返回 (“NO”)。
按文本读,这堆条件没有收拾干净:有调用写成了空参数的 寻找文本 ( , , , 假);括号段的长度参数写的是"左括号位置减右括号位置",对 (1+1) 这样的串这差值是负的,起点和长度明显写反了;整篇没有一处 计算 调用自己,所以括号只处理最外一层。能不能编译、算得对不对,我没运行也不打算替你猜——这些痕迹只说明一件事:这是一摞停在半途的稿子。
体积之外要说一句,这 23KB 读起来是像样的结构化程序而不是流水账:入口按易模块模板组织(_启动子程序 调 _临时子程序 做自检),求值逻辑收在单一公开子程序里,参数和返回值都写了类型,还声明了一个程序集变量 小括号——名字像是打算记括号状态,虽然从头到尾没用上。过程抽象、接口可见性、调试脚手架这些概念全都在,缺的只有递归。
文件名说的二分,核实了
计算 与 计算无括号 的分工确实在文件名里:一个摊开括号和优先级比较,一个做单层乘除折叠。真正缺的是中间那一环——递归。把括号段的内部文本原样喂回 计算 自己,嵌套括号就自动解决,优先级表也不用按括号种类乘三。两个文件谁都没接上这一环,于是同一个人对"表达式是文本"想出了两台不相干的机器,而不是一台。
收个尾
五个程序共享的从来不是某个算法,是那个前提:表达式是文本,状态是文本,数字和文本之间靠 到整数 摆渡,位置靠 寻找文本 锚定。2016 年的两台求值器是这个前提的正用,2018 年的谜题机是反用。新手和熟手的差距在这里非常具体:不缺任何原语,缺的只有一个递归和一个"值/文本"边界意识——接上递归,计算.e 那 23KB 会塌缩成几十行。抽象还没长出来的中间态,一般不留档,这批留下了。
归档仓库:https://cnb.cool/t1mmoc/tspace/eyuyan/epl-calculators
| 文件 | 日期 | 体积 | 是什么 |
|---|---|---|---|
| 计算.e | 2016-03-09 | 27,006 B | 带括号的手写求值器:三种括号 × 四运算符的位置比较展开,无递归,失败返回 “NO” |
| 计算无括号.e | 2016-03-11 | 6,033 B | 无括号的乘除链折叠机,加减只在条件里路过 |
| 简单的猜数字游戏(超级新手的软件,不信你看不懂).e | 2016-03-29 | 5,027 B | 猜数字 0–10000,计数存在标签标题文本里 |
| XXX乘以XX23等于XX13计算.e | 2018-07-26 | 4,368 B | 竖式谜题:1–9 同数填洞,编辑框当寄存器,线程后台跑 |
仓库里除五个 .e 原件外,还附带机器抽取的文本版源码,抽取工具是 e-packager。这类抽取文本带两种工具痕迹:#e2txt_text# 注释标记和 krnln:: 支持库前缀——易语言 IDE 的源码视图里不会出现这两样,它们不是 IDE 的显示方式(这批五个工程太小,两种痕迹一次都没出现)。