编程科学/Guzzintas 和其他密码
虽然我们现在有一个不错的表示常量的方法,但我们现在面临一个更大的问题:如何组合项和常量。我们不能使用 Sway 的+运算符,因为它只适用于数字和字符串。[1] 相反,我们将创建一个对象来保存要添加的两个项目。[2] 让我们创建一个名为 plus 的构造函数,以提醒我们它生成一个保存两个对象(潜在)总和的对象
function plus(p,q) //p and q are terms/constants { this; }
但是,除了提取绑定到 p 和 q 的原始项目外,我们无法对 plus 对象做太多事情。
我们应该如何增强我们的 plus 构造函数?就像项和常量一样,plus 对象应该能够
* compute its value * compute its derivative * visualize itself
当然,plus 对象不知道如何执行任何这些操作,因为它只是一个用于保存两个项目的容器。但是,它可以做的是要求这些项目执行计算,然后以有意义的方式组合结果。[3]
现在我们的问题变成了
- 项目的值应该如何组合?
- 项目的微分应该如何组合?
- 项目的可视化应该如何组合?
对于value 方法,p 的值和q 的值应该如何组合?由于 p 的值是一个数字,q 的值是一个数字,并且由于 plus 对象表示其项目的加法,我们可以简单地将项目值加在一起
function plus(p,q) //p and q are terms/constants { function value(x) { p . value(x) + q . value(x); } this; }
因此,要计算两个加在一起的项的y 值,我们只需分别找到项的y 值,然后将结果加在一起。在数学上,
让我们测试一下
sway> var a = term(-5,2); sway> var b = term(7,0); sway> a . value(3) + b . value(3); INTEGER: -38 sway> var z = plus(a,b); sway> z . value(3); INTEGER: -38
请注意,我们的 plus 构造函数实际上没有指定形式参数 p 和 q 必须绑定到的对象类型;唯一的要求是这些对象有一个接受单个参数的 value 方法。我们将在后面利用这一事实,并使用 plus 对象将 terms、constants 和其他 plus 对象随意粘合在一起。
幂法则和求和
[edit | edit source]现在我们将注意力转向 plus 对象需要实现的第二个方法 diff。当然,幂法则不适用于求和,因为该法则只适用于计算单个项的导数。
事实证明,两个加在一起的东西的导数是每个东西单独的导数的总和。在数学上,
现在这里是最棘手的部分(现在看起来很棘手,但过不了多久,你会觉得这就像切蛋糕一样简单)。我们使用 plus 对象来表示两个项的加法,对吗?上面的等式的右侧涉及加法。正在加什么?两个导数。如果 a 和 b 是项对象,那么 a 和 b 的导数是什么样的东西。两者都是项!因此在这种情况下,右侧仅仅是两个项的总和。但是,我们用什么来表示两个项的总和......等等......一个 plus 对象。
哇!然后,一个由两个项组成的 plus 对象的导数是另一个包含这两个项的导数的 plus 对象。这既强大又简单。
如果上面的解释让你感到困惑,也许看一下代码会有所帮助。以下是修改后的 powerRule
function powerRule(obj) { if (obj is :term) { var a = obj . a; var n = obj . n; term(a * n,n - 1); } else if (obj is :plus) { var dp/dx = powerRule(obj . p); var dq/dx = powerRule(obj . q); plus(dp/dx,dq/dx); } else { throw(:calculusError,"powerRule: unknown object"); } }
请注意,dp/dx 是一个变量名,它与 dp / dx 完全不同,dp / dx 是变量 dp 除以 dx。对于 plus 对象,我们提取两个项 p 和 q,使用幂法则对它们进行求导,然后将它们组合成一个 plus 对象。
超过两个项
[edit | edit source]我们的 plus 构造函数非常适合表示直线,因为直线由两项组成,其中一项是常量。对于三个或更多项的总和,我们该怎么办?
如何...什么也不做。
是的,什么也不做。事实证明,我们对 plus 的设计如此精妙,它不仅处理了两个项的总和,而且还处理了 plus 对象和一个项的总和。[4]。回想一下,plus 对象中参数 p 和 q 的唯一要求是它们都绑定到具有 value 方法的对象。plus 对象有 value 方法吗?是的,的确!所以这意味着 p 或 q 或两者都可以绑定到 plus 对象。让我们看看
var a = term(-5,0); var b = plus(term(3,1),a); var y = plus(term(4,2),b);
变量 y 现在是指多项式
或者更简单地说
现在,仅仅因为我们应该(并且可以)能够使用 plus 构造函数随意组合求和和项,并不意味着我们可以假设 plus 按照写的那样是完全正确的。我们需要测试[5] 才能彻底说服自己代码是有效的。让我们在 x 的某个易于验证的值(例如 x = 2)下测试 a、b 和 c
sway> a . value(2); INTEGER: -5 sway> b . value(2); INTEGER: 1 sway> y . value(2); INTEGER: 17
在这个交互中,我们看到 a,它是 ,当 x 的值为 2 时,其值为 -5。对象 b,它是一个表示 的 plus 对象,其值为 6 - 5 或 1。对象 y,它也是一个 plus 对象,表示 ,其值为 16 + 6 - 5 或 17。
对象 a(只是一个项)和对象 b(由两个项组成)都给出正确答案并不令人惊讶;我们完全按照它们预期的方式使用它们的构造函数。为什么 y 有效,因为它由一个项和一个 plus 对象组成,这一点就不那么明显了。下一节将展示 可视化,以帮助你理解“为什么”。
可视化
[edit | edit source]一个非常好的方法来查看为什么 y 有效是可视化 y 对象。我们首先修改 term 构造函数以添加一个 toString 方法。通常,toString 方法用于生成一个表示对象当前状态的字符串。[6] 这样的字符串称为可视化。
在继续之前,请阅读有关 字符串 的内容。
以下是修改后的 term 函数及其新的 toString 方法
function term(a,n) { function value(x) { a * (x ^ n); } function toString() { "" + a + "x^" + n; } this; }
一如既往,我们写一些代码,然后测试、测试、测试!
sway> var t = term(3,2); sway> t . toString(); STRING: "3x^2"
对于任何项,我们都可以快速看到对象的部分,这很容易理解。[7] 这是可视化的目标。
我们还需要在 plus 中添加一个可视化
function plus(p,q) //p and q are terms or plus objects { function value(x) { p . value(x) + q . value(x); } function toString() { p . toString() + " + " + q . toString(); } this; }
请注意,plus 通过使用其组成对象的可视化来进行可视化,并添加一个 '+' 符号来表示加法。
现在,我们重新制作 a、b 和 y,并强制执行这些新的 term 和 plus 定义
var a = term(-5,0); var b = plus(term(3,1),a); var y = plus(term(4,2),b);
让我们看看 y 的样子
sway> y . toString(); STRING: 4x^2 + 3x^1 + -5x^0
看起来完全符合预期。
为了进一步了解为什么可视化有效,有时查看生成结果的调用序列会有所帮助。这些调用被组织成称为“调用树”的东西。调用中的操作从起始调用缩进一个级别。左箭头表示返回值。
以下是 y 的 toString 方法调用的调用树
call y . toString() //object y is plus(term(4,2),b) call p . toString() //object p is term(4,2) <-- "4x^2" //return value " + " call q . toString() //object q is b, plus(term(3,1),a) call p . toString() //object p is term(3,1) <-- "3x^1" //return value " + " call q . toString() //object q is a, term(-5,0) <-- "-5x^0" //return value <-- 3x^1 + -5x^0 //return value <-- 4x^2 + 3x^1 + -5x^0 //return value
将字符串从上到下组合到第一个缩进级别,我们将获得整体返回值
4x^2 + 3x^1 + -5x^0
我们还可以构建一个调用树来确定当x = 2 时y的值。
call y . value(2) call p . value(2) // p is 4x^2 <-- 16 + call q . value(2) // q is 3x^1 + -5x^0 call p . value(2) // p is 3x^1 <-- 6 + call q . value(2) // q is -5x^0 <-- -5 <-- 1 <-- 17
将第一级缩进处的返回值相加得到 17,即总的返回值。
可视化是一个重要的技术。你应该为所有对象都包含可视化方法,以便在程序出现问题时能够轻松地进行调试。在这种情况下,你的可视化可能会指出某个对象没有按照预期那样出现,这是解决问题的重要的第一步。
幂法则修改
[edit | edit source]我们已经对项的和测试了我们的powerRule函数,但它对和的和有效吗?让我们来检查一下代码
function powerRule(obj) { if (obj is :term) { var a = obj . a; var n = obj . n; term(a * n,n - 1); } else if (obj is :plus) { var dp/dx = powerRule(obj . p); var dq/dx = powerRule(obj . q); plus(dp/dx,dq/dx); } else { throw(:calculusError,"powerRule: unknown object"); } }
如果形式参数obj绑定到一个plus对象的和,则powerRule函数会递归地调用组成这个和的两个对象。只要这两个对象中的任何一个都是plus或term对象,似乎如果powerRule能够处理它。在plus对象的情况下,powerRule会再次递归地调用自身。
让我们用绑定到变量y的上面那个多项式来测试powerRule函数。回想一下y的可视化方式为
4x^2 + 3x^1 + -5x^0
我们希望powerRule函数产生一个多项式,可视化为类似下面的东西
8x + 3
让我们来看看。
var dy/dx = powerRule(y); sway> dy/dx . toString(); STRING: 8x^1 + 3x^0 + 0x^-1
观察结果的最后一项,我们注意到零乘以任何东西都是零,所以结果等价于
8x^1 + 3x^0 + 0
或者
8x^1 + 3x^0
注意,正如之前一样, 等于 1,结果变为
8x^1 + 3*1
或者
8x^1 + 3
最后,意识到 仅仅是x,结果变为
8x + 3
如预期的那样。本章末尾的一些问题探讨了如何让plus和term自动执行这些简化操作。
其他数学组合的项
[edit | edit source]如果我们希望减去两项,就像这样
y = 3x^2 - 4x
我们可以编写一个名为minus的函数来完成这个操作
function minus(p,q) { function value(x) { p . value(x) - q . value(x); } function toString() { p . toString() + " - " + q . toString(); } this; }
这就像plus一样,除了在value和toString方法中使用减号而不是加号。[8]
项的减法的幂法则是什么?它类似于,但不完全相同于项的加法的幂法则
因此,如写的那样,powerRule不能用于minus对象,因为powerRule中没有代码来减法或甚至产生minus对象。我们需要在powerRule中添加一个新的子句
function powerRule(obj) { if (obj is :term) { var a = obj . a; var n = obj . n; term(a * n,n - 1); } else if (obj is :plus) { var dp/dx = powerRule(obj . p); var dq/dx = powerRule(obj . q); plus(dp/dx,dq/dx); } else if (obj is :minus) { var dp/dx = powerRule(obj . p); var dq/dx = powerRule(obj . q); minus(dp/dx,dq/dx); } else { throw(:calculusError,"powerRule: unknown object"); } }
虽然这将有效,但有一个线索表明我们做错了什么。每当你看到一个处理对象的if-chain时,这意味着你没有充分利用对象的强大功能,你应该重构你的代码以删除if-chain。下一节将展示如何做到这一点。
对象的方式
[edit | edit source]程序的设计通常随着时间的推移而演变。一些程序员对他们编写的代码感到非常投入,即使有更好的方法出现,他们也会顽固地坚持下去。用肯尼·罗杰斯的话来说,“你必须知道何时该抓住,何时该放弃”。在这种情况下,我们的powerRule函数正在变得难以控制,所以我们要放弃,重新开始。
我们的新方法扩展了下面的语句
- 一个对象应该能够...
在我们的term和plus对象的情况下,我们已经实现了上面语句的以下版本
- 一个term对象应该能够返回它的系数
- 一个term对象应该能够返回它的指数
- 一个term对象应该能够计算给定x值的y值
- 一个term对象应该能够可视化自身
- 一个plus对象应该能够返回它的组件
- 一个plus对象应该能够计算给定x值的y值
- 一个plus对象应该能够可视化自身
在这个列表中,我们将添加以下语句
- 一个term对象应该能够进行微分
- 一个plus对象应该能够进行微分
- 其他类似对象应该能够进行微分
让我们首先更新我们的term构造函数,以便项能够进行微分。我们通过添加一个diff(用于微分)方法来实现这一点
function term(a,n) { function value(x) { ... } function toString() { ... } function diff() { term(a * n,n - 1); } this; }
diff方法的主体仅仅是powerRule函数中发现的项代码的一种形式。
我们也可以对plus构造函数做同样的事情
function plus(p,q) { function value(x) { ... } function toString() { ... } function diff() { var dp/dx = p . diff(); var dq/dx = q . diff(); plus(dp/dx,dq/dx); } this; }
为了使plus更短一些,我们可以用一行代码替换diff方法的主体
function diff() { plus(p . diff(),q . diff()); }
利用plus是两个参数的函数这一事实,我们也可以使用中缀表示法
function diff() { p . diff() plus q . diff(); }
修改minus构造函数类似。那么乘法和除法项呢?
根据 CME,对乘积进行微分的数学规则是
除法的规则更复杂
times和div构造函数的diff方法的实现留作练习。
完成所有这些之后,powerRule函数变得非常简单
function powerRule(obj) { obj . diff(); }
事实上,它太简单了,它不再做任何有用的工作,可以丢弃了。
问题
[edit | edit source]所有公式都是使用小学算术优先级写的。
1. 如果你将一个plus对象传递给原始的powerRule函数,会发生什么?解释会发生什么。
2. 定义并测试一个times构造函数。确保添加toString和diff方法。
3. 定义并测试一个div构造函数。确保添加toString和diff方法。
4. 修改term的toString方法,以便如果指数为零,则仅使用系数。
5. 修改term的toString方法,以便如果指数为 1,则忽略系数和/或指数。也就是说,term(3,1) 应该显示为 3x,而不是 3x^1,而term(1,4) 应该显示为 x^4,而不是 1x^4。
6. 修改term的toString方法,以便如果系数为零,则产生空字符串""。
7. 修改plus构造函数,如果项的系数为零,则将其丢弃。你如何“丢弃”一个项?
8. 修改plus构造函数,以便如果第二个参数是一个系数为负数的项,则它会产生一个适当的minus对象。
9. 使用 Sway 解决 CME 第 64 页问题 6。
10. 使用 Sway 对 y = (x - 3/2) + (x^2 -5x) + (3x^3 + 7x^2 + 3x + 5) 进行微分。定义函数y,然后
11. 使用纸笔完成 CME 第 64 页的练习 1-5。
12. 使用纸笔解决 CME 第 77 页的练习 11。
脚注
[edit | edit source]- ↑ 稍后,我们将学习如何覆盖+运算符,以便它也可以添加项和常量。
- ↑ 我们为什么要这样做并不明显。随着我们继续进行,我们将看到这种方法有效,但它不会给我们太多关于如何一开始就提出这种解决方案的见解。我们本质上是要延迟项目的添加,直到我们真正需要将它们加在一起(例如,当我们试图计算特定的y值时)。延迟的概念是一个强大的计算机科学概念。然而,知道何时延迟,更像是一种艺术,而不是科学。
- ↑ 对象之间主要存在两种关系。在本例中,plus 对象与 p(或 q)之间的关系是 客户关系。对象 p(或 q)被称为 plus 对象的客户,因为 p(或 q)是一个组件。对象之间另一种主要关系是 继承,其中对象共享特征。你可以在 Sway 参考手册 中了解更多关于继承的信息。尽管在本例中没有使用显式继承,但可以认为 项 和 常量 继承了三种方法的思想:值、差 和 toString。
- ↑ 如果你编写简洁优雅的代码,你将经常发现这些令人愉快的事件。
- ↑ 当你成为高级程序员时,与其测试一些代码来查看它是否看起来正确,你可能会 证明 它的正确性。证明代码正确性的优势在于,有时执行所有可能的测试非常困难或不可能。
- ↑ 使用名称 toString 只是一个约定。我们以 Java 编程语言为基础,它使用 toString 方法来实现这个目的。
- ↑ 请注意,这种表示使用你在小学学到的运算符优先级,即求幂运算先于系数的乘法运算。这与 Sway 不同,Sway 会将表达式 3 * x ^ 2 评估为 (3 * x) ^ 2。要了解更多关于 Sway 如何计算算术表达式的知识,请参阅 Sway 参考手册中的优先级和结合性。
- ↑ 通过类比编程 的一个很好的例子。请注意,通过复制现有函数并进行少量修改来创建新函数通常是一个 "坏主意"。如果你发现自己正在这样做,问问自己这两个函数如何可以合并成一个函数?对于 plus 和 minus,有一种很好的方法可以做到这一点,但为了不影响叙述流程,我们将跳过它。