隐含约定的失败:决定函数的破坏性
功能(4), 语言设计(四)所有功能要么是破坏性的,要么是非破坏性的。有什么区别?我们如何区分它们?我们为什么在乎?这些都是很重要的问题,为了开发一种语言、一个库、一个程序或一个函数,没有考虑到破坏性的变化会导致不一致的代码和非直观的错误。
什么是破坏性功能
让我们看看能不能搞清楚破坏性函数的样子:
定义剂量测定()“结构”结束
无损检测。
定义剂量测定(一,b)一*b结束
无损检测。
定义剂量测定(一)一<<“这个”结束
破坏性的。
定义剂量测定(一)b=一.克隆()b<<“这个”结束
无损检测。
内部评级(主要的):033:0>y=[1,2,5,1,2]=>[1,2,5,1,2]内部评级(主要的):034:0>y.分类()=>[1,1,2,2,5]内部评级(主要的):035:0>y=>[1,2,5,1,2]
无损检测。
定义排序2(一)一=一.分类()结束
破坏性的。
班什么定义初始化(名称)@姓名=名称结束
定义剂量测定()@姓名=@姓名<<“是个愚蠢的名字”结束结束
破坏性的。
好的,那么我们可能已经注意到了这一点的区别:破坏性函数会在其函数的词法范围之外引起一些变化,而非破坏性函数不会在其词法范围之外引起变化。
更简单地说,非破坏性函数不会改变它们不会创建的值。所有传入参数都可以检查,但不能更改(如果确实需要更改,则非破坏性函数将创建一个要修改的副本,而不是对原始参数执行操作)。
把他们分开
弄清楚一个函数是破坏性的还是非破坏性的可能很困难。如果您有权访问源代码,则可以通读源代码并查找修改外部变量或传入参数的点。然而,当源代码不可用时,事情就变得更加困难了。
如果没有源代码,就剩下两个工具:实验和约定。
实验可以归结为评估函数在执行前后可能更改的任何变量,并监视这些变量是否已更改。特别是,应该仔细检查参数是否有变化。这看起来不可靠吗?嗯,是的。很容易创建简单的测试用例,其中破坏性功能看起来是非破坏性的。常见的情况如下:
内部评级(主要的):037:0>定义链接(一,b)内部评级(主要的):038:1>[一,b]内部评级(主要的):039:1>结束=>无内部评级(主要的):040:0>一=[1,2,三]=>[1,2,三]内部评级(主要的):041:0>b=[4,5,6]=>[4,5,6]内部评级(主要的):042:0>c=链接(一,b)=>[[1,2,三],[4,5,6]]内部评级(主要的):043:0>一=>[1,2,三]内部评级(主要的):044:0>b=>[4,5,6]内部评级(主要的):045:0>c[0]<<10=>[1,2,三,10]内部评级(主要的):046:0>一=>[1,2,三,10]
现在a和b最初看起来是不变的,但是我们后来意识到它们已经被回收,以帮助形成从中返回的值链接. 在某些情况下,即使函数显式地复制传入的参数,您仍可能遇到此问题(因为发生的是浅拷贝而不是深拷贝)。
这就留给我们最后的工具:约定。
编程语言有自己喜欢的范例,它们选择的习惯用法,它们的良好实践,以及它们关于什么是好的编程风格的内在哲学。通常,这些不成文的规则会逐渐被阅读语言专家编写的代码所吸收。然而,这种含蓄地传授习俗的方法是一种悲剧。
这是悲剧性的,因为不成文的规则是不可靠的,并且从短暂的趋势转向短暂的趋势。当最佳实践的定义模糊不清时,我们不能依赖图书馆作者来全面实施一系列不断发展的最佳实践:隐含的规则具有酒后一夜情的所有永久性。
然而,并不是所有的语言都依赖于不成文的惯例来区分破坏性功能。鼓励函数式编程的语言往往非常明确:Scheme在所有破坏性方法后面加上感叹号。这说明了主动式语言设计师(或语言设计委员会)可以在不修改语法或定义标准库的情况下提高语言的可用性:简单的约定——比如Ruby使用问号表示布尔查询,或者Scheme的感叹号来表示一个破坏性的函数——可以快速地将重要信息传递给其他阅读源代码的人。
为什么我们要关心
在许多语言中,很难区分破坏性函数和非破坏性函数。这真的重要吗?
如果您喜欢编写干净、高效、无冗余的代码,那么是的,这很重要。如果你喜欢有效的代码,或者珍惜你的时间,这也很重要。
如果你不能很容易地区分破坏性和非破坏性的功能,那么你就必须谨慎行事。这意味着您将遇到添加不必要冗余的情况:
定义剂量测定(十)y=十.克隆()y<<“一些文字”结束z=“这是我珍贵的琴弦”z2型=剂量测定(z.克隆())
这是低效的代码(更不用说难看的代码),但是如果您不能快速确定剂量测定是非破坏性的,那么这些情况就会相当频繁地出现。这里的关键是快速确定:通过库函数调用链进行跟踪是对时间的浪费,而且您知道,经常采用快速修复方法,而不是验证函数如何真正运行。随着时间的推移,这些快速修复会累积起来,并且您的代码会变得更长、更复杂,而且效率也会比区分这两种类型的函数要低。
如果我们能够快速区分,我们就知道为了安全起见在哪里克隆数据,以及在哪里未经修改地传递数据。我们可以做出明智的决定,销毁数据以提高效率,或者保持数据的原始状态以备日后使用。
归根结底是这样的:如果我们能够区分破坏性函数和非破坏性函数,我们就可以编写效率更高、冗余更少的代码——更快、更省力。
怎么办
在对抗破坏性模糊的战争中,我们有一些可用的资源。我们可以开始使用真正做出这种区分的语言(什么,没有人愿意开始在Scheme中编写所有代码?),或者我们可以从计划的成功(在这个狭窄的范围内)和创建明确的约定,这些约定存在于纸片上,而不仅仅存在于我们社区的集体记忆中。
让我们考虑一下区分破坏性函数和非破坏性函数的Python约定(据我所知,实际上在任何地方都没有这样写):
- 如果一个函数返回None,那么它就是破坏性的。
- 如果函数返回的值不是None,则它不是破坏性的。
- 规则#1和#2在程序员的自由裁量权下是可选的无效的。
当您阅读文档时,您可以看到此约定的证据,其中说明了“此函数不返回任何值以强调非破坏性”
然后你会看到这样一种方法:
定义增量(自己,加入者):自己.计数=自己.计数+加入者返回自己.计数
它返回的值不是None。它是毁灭性的。没有人会认为这是不好的形式。
从根本上讲,Python约定也是不可取的,因为它要求程序员实际运行该方法(或读取其源代码)以确定它返回什么。仅仅看到方法的名称不足以确定破坏性。
这绝不是一个纯粹的python问题,虽然,几乎没有语言全面解决这个重要的区别。每个编程社区都需要坐下来,找到一种方法来区分破坏性函数和非破坏性函数,这符合他们的语言哲学。将感叹号附加到破坏性函数的Scheme方法并不适用于所有语言,但是找到一个类似的显式约定非常值得花时间进行社区讨论。