我听到过很多次其他开发人员使用该短语“宣传”某些模式或开发最佳做法的消息。在大多数情况下,当您谈论函数式编程的好处时,都会使用此短语。
短语“易于推理”按原样使用,没有任何解释或代码示例。因此,对我而言,它就像下一个“嗡嗡声”一词,更多“经验丰富”的开发人员在演讲中使用。
问题:您能否提供一些“不容易推理的”示例,以便将其与“不容易推理的”示例进行比较?
我听到过很多次其他开发人员使用该短语“宣传”某些模式或开发最佳做法的消息。在大多数情况下,当您谈论函数式编程的好处时,都会使用此短语。
短语“易于推理”按原样使用,没有任何解释或代码示例。因此,对我而言,它就像下一个“嗡嗡声”一词,更多“经验丰富”的开发人员在演讲中使用。
问题:您能否提供一些“不容易推理的”示例,以便将其与“不容易推理的”示例进行比较?
Answers:
在我看来,“易于推理”一词是指易于“在您的脑海中执行”的代码。
在查看一段代码时,如果代码简短,写得清楚,名字很好并且值的变化很小,那么脑力劳动来完成代码的工作是(相对)容易的。
一长串名称不佳的代码,会不断更改值的变量和复杂的分支通常需要例如一支笔和一张纸来帮助跟踪当前状态。因此,这样的代码无法轻易在您的脑海中工作,因此,这样的代码不容易推论。
"Code easy to reason about" almost exclusively alludes to its mathematical properties and formal verification
听起来大概是问题的答案。您可能希望将其发布为答案,而不是不同意注释中的(主观)答案。
一种机制或一段代码很容易推论出何时需要考虑一些事情来预测其作用,而需要考虑的事情也很容易获得。
真正的函数没有副作用且没有状态很容易推论,因为输出完全由输入决定,输入就在参数中。
相反,带有状态的对象很难推理,因为在调用方法时必须考虑对象处于什么状态,这意味着您必须考虑哪些其他情况可能导致该对象处于故障状态。特定状态。
更糟糕的是全局变量:要推理读取全局变量的代码,您需要了解可以在代码中的何处设置该变量以及为什么进行设置-而且可能不容易找到所有这些位置。
最难解释的是具有共享状态的多线程编程,因为不仅有状态,而且有多个线程同时更改它,所以要推断一段代码在被一个线程执行时会做什么?必须考虑到在每个执行点上,某个其他线程(或其中的几个!)可能正在执行代码的几乎任何其他部分,并更改了您正在眼前操作的数据。从理论上讲,可以使用互斥体/监视器/关键部分/随便调用它来进行管理,但实际上,除非有人将共享状态和/或并行性大幅度地限制在很小的范围内,否则没有人能够真正可靠地做到这一点代码部分。
make
甚至C ++模板专门化和函数重载)也可以使您回到考虑整个程序的位置。即使您认为已经找到了某些东西的定义,该语言也允许在程序中的任何位置进行更具体的声明来覆盖它。您的IDE可能对此有所帮助。
sealed
不成为默认值的最终原因是什么?
避免更广泛的讨论,并解决特定的问题:
您能否提供“不容易推理”的一些示例,以便将其与“不容易推理”的示例进行比较?
我指的是“梅尔的故事,一个真正的程序员”,这是一名程序员的民间传说,可以追溯到1983年,因此对我们的职业算是“传奇”。
它讲述了一个程序员编写代码的故事,该代码在任何可能的情况下都首选奥术,包括自引用和自修改代码以及对机器错误的故意利用:
一个明显的无限循环实际上已经以利用进位溢出错误的方式进行了编码。在解码为“从地址x加载”的指令中加1通常会产生“从地址x + 1加载”。但是,当x已经是可能的最高地址时,不仅地址会回零,而且会在读取操作码的位中携带1,从而将操作码从“ load from”更改为“ jump to”,因此完整指令已从“从最后一个地址加载”更改为“跳转到地址零”。
这是“难以推理”的代码示例。
当然,梅尔会不同意...
我可以提供一个例子,一个非常普通的例子。
考虑下面的C#代码。
// items is List<Item>
var names = new List<string>();
for (var i = 0; i < items.Count; i++)
{
var item = items[i];
var mangled = MyMangleFunction(item.Name);
if (mangled.StartsWith("foo"))
{
names.Add(mangled);
}
}
现在考虑这种替代方法。
// items is List<Item>
var names = items
.Select(item => MyMangleFunction(item.Name))
.Where(s => s.StartsWith("foo"))
.ToList();
在第二个示例中,我一眼就知道该代码在做什么。当我看到时Select
,我知道项目列表正在转换为其他项目列表。当我看到时Where
,我知道某些项目已被过滤掉。乍一看,我就能理解names
并有效利用它。
当我看到一个for
循环时,我不知道到底发生了什么,直到我真正阅读了代码。有时我必须进行追溯以确保我已经考虑了所有副作用。我什至需要做一些工作才能理解什么是名字(除了类型定义之外)以及如何有效地使用它。因此,第一个示例比第二个示例更难以推理。
最终,在这里易于推理还取决于对LINQ方法Select
和的理解Where
。如果您不了解它们,那么最初就很难推理第二个代码。但是您只需付出一次就可以理解它们。for
每次使用一个循环时,都要付出一次理解循环的代价。有时成本值得支付,但通常“更容易推理”更为重要。
一个相关的短语是(I释义),
代码没有“ 没有明显的错误 ”是不够的:相反,它应该有“ 显然没有错误 ”。
RAII是一个相对“容易推理”的例子。
另一个示例可能是避免致命的拥抱:如果您可以持有一个锁并获得另一个锁,并且有很多锁,那么很难确定没有发生致命拥抱的情况。添加诸如“只有一个(全局)锁”或“您在持有第一把锁的同时不允许获得第二把锁”之类的规则,可使系统相对容易地进行推理。
CComPtr<>
)和C样式的函数(CoUninitialize()
)。就我记得您在模块作用域和整个模块生命周期内调用CoInitialize / CoUninitialize而言,我也发现它是一个奇怪的示例,例如在main
或中DllMain
,而不是在一些短暂的局部函数作用域中,如示例所示。
main
为应用程序的入口点()函数。您可以在启动时初始化COM,然后在退出前立即对其进行初始化。除了使用RAII范式的全局对象(例如COM智能指针)外。关于混合样式:Raymond提出了一个可以在ctor中初始化COM并在dtor中未初始化的全局对象的可行方法,但这是微妙的,并且不易推论。
编程的关键是案例分析。艾伦·珀利斯(Alan Perlis)在第32号Epigram中对此进行了评论:程序员的能力和逻辑不应该通过程序员的才智和逻辑来衡量,而要通过案例分析的完整性来衡量。
如果案例分析容易,则很容易推断出情况。这要么意味着很少要考虑的案例,要么就是没有几个特殊案例–可能会有一些大的案例空间,但是由于某些规律性而崩溃,或者屈服于诸如归纳法这样的推理技术。
例如,算法的递归版本通常比命令式版本更容易推论,因为它不会贡献多余的情况,这些情况是由于没有出现在递归版本中的支持状态变量的变异而引起的。而且,递归的结构适合于数学上的归纳证明模式。我们不必考虑复杂性,例如循环变体和最弱的严格先决条件等等。
另一个方面是案例空间的结构。与分层案例相比,将案例平分或基本平分为几分的情况更容易推理:带有子案例和子子案例的案例,等等。
简化推理的系统的一个属性是正交性:这是控制子系统的案例在组合在一起时保持独立的属性。没有任何组合会引起“特殊情况”。如果将四格事物与三格事物正交组合,则有十二种情况,但理想情况下每个案例都是两个独立的案例的组合。从某种意义上说,实际上并没有十二种情况。这些组合只是我们不必担心的“突发情况”。这意味着我们仍然可以考虑四种情况,而无需考虑其他子系统中的其他三种情况,反之亦然。如果必须特别标识某些组合并赋予其他逻辑,则推理将更加困难。在最坏的情况下,每个组合都有一些特殊的处理方式,然后除了原始的四个和三个之外,实际上还有十二个新的情况。
当然。并发:
互斥体强制执行的关键部分:易于理解,因为只有一个原则(两个执行线程不能同时进入关键部分),但是容易造成效率低下和死锁。
替代模型,例如无锁编程或参与者:可能会更优雅,更强大,但很难理解,因为您不再可以依赖(貌似)基本概念,例如“现在将此值写入该位置”。
易于推理是方法的一个方面。但是选择使用哪种方法需要综合考虑所有方面。
让我们将任务限制为形式推理。因为幽默,发明或诗意推理具有不同的规律。
即使这样,该表达式仍是模糊定义的,不能严格设置。但这并不意味着它对我们应该保持如此黯淡。让我们想象一个结构正在通过一些测试并获得不同点的分数。每一点的好标记意味着该结构在各个方面都很方便,因此是“易于推理”。
“易于推理”的结构应在以下方面获得良好的评价:
测试是主观的吗?是的,自然是这样。但是表达本身也是主观的。一个人容易做到的事,另一个人却不容易。因此,对于不同的域,测试应该有所不同。
可以推理的功能语言的思想来自它们的历史,特别是ML,它是作为类似于可计算函数逻辑用于推理的结构的编程语言开发的。与命令式语言相比,大多数功能语言更接近于形式化编程语言,因此从代码到推理系统的输入的转换负担较少。
对于推理系统的示例,在pi演算中,命令式语言中的每个可变存储位置都需要表示为单独的并行过程,而一系列功能操作是单个过程。从LFC定理证明者开始的40年中,我们正在使用GB RAM,因此拥有数百个进程已不再是一个问题-我使用pi-calculus消除了几百行C ++的潜在死锁,尽管该表示具有数百个处理推理机确实耗尽了大约3GB的状态空间并解决了间歇性错误。这在20世纪70年代是不可能的,或者在1990年代初期需要一台超级计算机,而类似大小的功能语言程序的状态空间却很小,无法回溯到那时。
从其他答案来看,该短语已成为流行语,即使摩尔定律已侵蚀了许多难以推理命令式语言的困难。
易于推理是一个文化特定术语,这就是为什么很难提出具体示例的原因。这是一个固定在要进行推理的人身上的术语。
“易于推理”实际上是一个非常自我描述的短语。如果有人正在查看代码,并且想推理它的作用,那很容易=)
好的,把它分解。如果您正在查看代码,通常会希望它做一些事情。您想确保它按照您的预期去做。因此,您开发了有关代码应该做什么的理论,然后您要对此进行推理,以试图争论代码确实起作用的原因。您尝试像人类(而不是计算机)那样思考代码,并尝试合理化有关代码可以做什么的参数。
对于“易于推理”而言,最糟糕的情况是,唯一能理解代码作用的方法是像图灵机一样对所有输入进行逐行编码。在这种情况下,推理任何有关代码的唯一方法就是将自己变成计算机并在脑海中执行。在混乱的编程竞赛中很容易看到这些最坏的例子,例如解密RSA的PERL的以下3行:
#!/bin/perl -sp0777i<X+d*lMLa^*lN%0]dsXx++lMlN/dsM0<j]dsj
$/=unpack('H*',$_);$_=`echo 16dio\U$k"SK$/SM$n\EsN0p[lN*1
lK[d2%Sa2/d0$^Ixp"|dc`;s/\W//g;$_=pack('H*',/((..)*)$/)
出于易于理解的理由,该术语还是高度文化性的。您必须考虑:
这些因素对“易于推理”的影响不同。以推理机的技能为例。当我在公司工作时,建议我在MATLAB中开发脚本,因为它“很容易推论”。为什么?好吧,公司中的每个人都知道MATLAB。如果我选择其他语言,那么每个人都很难理解我。没关系,对于某些任务,MATLAB的可读性很差,仅仅是因为它不是为这些任务而设计的。后来,随着我职业的发展,Python变得越来越流行。突然,MATLAB代码变得“难以推理”,而Python是编写易于推理的代码的首选语言。
也要考虑读者可能拥有的一些东西。如果您可以依靠阅读器来识别特定语法的FFT,那么如果坚持使用该语法,则“更容易推理”该代码。它使他们可以将文本文件看成是您在其上绘制FFT的画布,而不必深入了解具体细节。如果您使用的是C ++,请查明您的读者对这个std
库有多满意。他们有多少喜欢函数式编程?容器库中出现的一些习惯用法非常依赖于您喜欢哪种语言风格。
了解读者可能想回答什么样的问题也很重要。您的读者最关心的是对代码的肤浅理解,还是在寻找肠道深处的错误?
读者必须具有的确定性实际上是一个有趣的话题。在许多情况下,朦胧的推理实际上足以使产品脱颖而出。在其他情况下,例如FAA飞行软件,读者可能会想要铁定的推理。我遇到了一个案例,我主张将RAII用于特定任务,因为“您可以设置它,而不必理会它……它会做正确的事情。” 有人告诉我我错了。那些打算对此代码进行推理的人并不是那种“只是想忘记细节”的人。对于他们来说,RAII更像是一个悬而未决的家伙,迫使他们考虑离开示波器时可能发生的所有事情。