跳转到内容

编程科学/Guzzintas 和其他密码

来自 Wikibooks,开放世界中的开放书籍

虽然我们现在有一个不错的表示常量的方法,但我们现在面临一个更大的问题:如何组合项和常量。我们不能使用 Sway 的+运算符,因为它只适用于数字和字符串。[1] 相反,我们将创建一个对象来保存要添加的两个项目。[2] 让我们创建一个名为 plus 的构造函数,以提醒我们它生成一个保存两个对象(潜在)总和的对象

   function plus(p,q) //p and q are terms/constants 
       {
       this;
       }

但是,除了提取绑定到 pq 的原始项目外,我们无法对 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 构造函数实际上没有指定形式参数 pq 必须绑定到的对象类型;唯一的要求是这些对象有一个接受单个参数的 value 方法。我们将在后面利用这一事实,并使用 plus 对象将 termsconstants 和其他 plus 对象随意粘合在一起。

幂法则和求和

[edit | edit source]

现在我们将注意力转向 plus 对象需要实现的第二个方法 diff。当然,幂法则不适用于求和,因为该法则只适用于计算单个项的导数。

事实证明,两个加在一起的东西的导数是每个东西单独的导数的总和。在数学上,

   

现在这里是最棘手的部分(现在看起来很棘手,但过不了多久,你会觉得这就像切蛋糕一样简单)。我们使用 plus 对象来表示两个项的加法,对吗?上面的等式的右侧涉及加法。正在加什么?两个导数。如果 ab 是项对象,那么 ab 的导数是什么样的东西。两者都是项!因此在这种情况下,右侧仅仅是两个项的总和。但是,我们用什么来表示两个项的总和......等等......一个 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 对象,我们提取两个项 pq,使用幂法则对它们进行求导,然后将它们组合成一个 plus 对象。

超过两个项

[edit | edit source]

我们的 plus 构造函数非常适合表示直线,因为直线由两项组成,其中一项是常量。对于三个或更多项的总和,我们该怎么办?

如何...什么也不做。

是的,什么也不做。事实证明,我们对 plus 的设计如此精妙,它不仅处理了两个项的总和,而且还处理了 plus 对象和一个项的总和。[4]。回想一下,plus 对象中参数 pq 的唯一要求是它们都绑定到具有 value 方法的对象。plus 对象有 value 方法吗?是的,的确!所以这意味着 pq 或两者都可以绑定到 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)下测试 abc

   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 通过使用其组成对象的可视化来进行可视化,并添加一个 '+' 符号来表示加法。

现在,我们重新制作 aby,并强制执行这些新的 termplus 定义

   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

看起来完全符合预期。

为了进一步了解为什么可视化有效,有时查看生成结果的调用序列会有所帮助。这些调用被组织成称为“调用树”的东西。调用中的操作从起始调用缩进一个级别。左箭头表示返回值。

以下是 ytoString 方法调用的调用树

   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函数会递归地调用组成这个和的两个对象。只要这两个对象中的任何一个都是plusterm对象,似乎如果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

如预期的那样。本章末尾的一些问题探讨了如何让plusterm自动执行这些简化操作。

其他数学组合的项

[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一样,除了在valuetoString方法中使用减号而不是加号。[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函数正在变得难以控制,所以我们要放弃,重新开始。

我们的新方法扩展了下面的语句

  • 一个对象应该能够...

在我们的termplus对象的情况下,我们已经实现了上面语句的以下版本

  • 一个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,对乘积进行微分的数学规则是

   

除法的规则更复杂

   

timesdiv构造函数的diff方法的实现留作练习。

完成所有这些之后,powerRule函数变得非常简单

   function powerRule(obj)
       {
       obj . diff();
       }

事实上,它太简单了,它不再做任何有用的工作,可以丢弃了。

问题

[edit | edit source]

所有公式都是使用小学算术优先级写的。

1. 如果你将一个plus对象传递给原始的powerRule函数,会发生什么?解释会发生什么。

2. 定义并测试一个times构造函数。确保添加toStringdiff方法。

3. 定义并测试一个div构造函数。确保添加toStringdiff方法。

4. 修改termtoString方法,以便如果指数为零,则仅使用系数。

5. 修改termtoString方法,以便如果指数为 1,则忽略系数和/或指数。也就是说,term(3,1) 应该显示为 3x,而不是 3x^1,而term(1,4) 应该显示为 x^4,而不是 1x^4。

6. 修改termtoString方法,以便如果系数为零,则产生空字符串""

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]
  1. 稍后,我们将学习如何覆盖+运算符,以便它也可以添加项和常量。
  2. 我们为什么要这样做并不明显。随着我们继续进行,我们将看到这种方法有效,但它不会给我们太多关于如何一开始就提出这种解决方案的见解。我们本质上是要延迟项目的添加,直到我们真正需要将它们加在一起(例如,当我们试图计算特定的y值时)。延迟的概念是一个强大的计算机科学概念。然而,知道何时延迟,更像是一种艺术,而不是科学。
  3. 对象之间主要存在两种关系。在本例中,plus 对象与 p(或 q)之间的关系是 客户关系。对象 p(或 q)被称为 plus 对象的客户,因为 p(或 q)是一个组件。对象之间另一种主要关系是 继承,其中对象共享特征。你可以在 Sway 参考手册 中了解更多关于继承的信息。尽管在本例中没有使用显式继承,但可以认为 常量 继承了三种方法的思想:toString
  4. 如果你编写简洁优雅的代码,你将经常发现这些令人愉快的事件。
  5. 当你成为高级程序员时,与其测试一些代码来查看它是否看起来正确,你可能会 证明 它的正确性。证明代码正确性的优势在于,有时执行所有可能的测试非常困难或不可能。
  6. 使用名称 toString 只是一个约定。我们以 Java 编程语言为基础,它使用 toString 方法来实现这个目的。
  7. 请注意,这种表示使用你在小学学到的运算符优先级,即求幂运算先于系数的乘法运算。这与 Sway 不同,Sway 会将表达式 3 * x ^ 2 评估为 (3 * x) ^ 2。要了解更多关于 Sway 如何计算算术表达式的知识,请参阅 Sway 参考手册中的优先级和结合性
  8. 通过类比编程 的一个很好的例子。请注意,通过复制现有函数并进行少量修改来创建新函数通常是一个 "坏主意"。如果你发现自己正在这样做,问问自己这两个函数如何可以合并成一个函数?对于 plusminus,有一种很好的方法可以做到这一点,但为了不影响叙述流程,我们将跳过它。


常量烦恼 · 重复操作

华夏公益教科书