“易于推理”-这是什么意思?[关闭]


49

我听到过很多次其他开发人员使用该短语“宣传”某些模式或开发最佳做法的消息。在大多数情况下,当您谈论函数式编程的好处时,都会使用此短语。

短语“易于推理”按原样使用,没有任何解释或代码示例。因此,对我而言,它就像下一个“嗡嗡声”一词,更多“经验丰富”的开发人员在演讲中使用。

问题:您能否提供一些“不容易推理的”示例,以便将其与“不容易推理的”示例进行比较?


4
@MartinMaat被广泛使用的更精确的短语是方程式推理,我建议这可能就是Fabio所追求的
jk。

3
对于这种事情,我喜欢使用短语“认知负荷”
Baldrickk

16
您知道程序推理的含义吗?
Bergi

5
从非正式的角度来说,我用它来表示一个解决方案,它足够简单(通常)理解任何给定输入的结果,而无需测试它。这意味着对于任何一组输入,结果都将不足为奇。例如,具有非显而易见的极端情况的解决方案很难推理。主要是我将其用于鲁棒性。
JimmyJames

7
我经常犯“容易推理”的罪恶感。但是我要注意,我尝试谨慎地说比较容易而不是绝对容易。在我的生活中,有一天我根本不需要推理任何软件,因此那天并不容易。只有花费大量的时间和精力,这才变得容易。要说任何编程问题都很容易,就是对那些可能(尚未)觉得容易的人持贬义的态度。说一个模型比另一个模型容易,就是说涉及的概念更少,活动部件更少,等等。
埃里克·利珀特

Answers:


58

在我看来,“易于推理”一词是指易于“在您的脑海中执行”的代码。

在查看一段代码时,如果代码简短,写得清楚,名字很好并且值的变化很小,那么脑力劳动来完成代码的工作是(相对)容易的。

一长串名称不佳的代码,会不断更改值的变量和复杂的分支通常需要例如一支笔和一张纸来帮助跟踪当前状态。因此,这样的代码无法轻易在您的脑海中工作,因此,这样的代码不容易推论。


29
不管您如何命名变量,都有一点警告,试图证明戈德巴赫猜想的程序天生就很难在您的头脑中或其他地方“执行”。但是,从易于说服自己的意义上说,如果它声称找到了反例,那么它就是说实话;-)
史蒂夫·杰索普

4
永远都不想在脑海中执行代码。对我来说,那将是“不容易推理”的最终展示。我希望能够就不执行计算机的情况做出预测性陈述。“易于推理”的代码不必在脑海中执行,而可以推理。
Cort Ammon

1
不提及形式验证的情况下,如何回答有关代码推理的问题?这个答案表明关于代码的推理是非正式的和临时的。事实并非如此,通常要非常小心地使用数学方法。有一些数学特性使代码在客观意义上“易于推理”(纯函数,举一个非常简单的例子)。变量的名称与“推理”代码的难易程度无关,至少在任何形式上都不如此。
Polygnome

3
@Polygnome关于代码的推理通常不会非常谨慎和数学地完成。在我撰写本文时,人们非正式地推理代码,至少在数以百万计的数学方法论者中,我认为是这样。
卡兹(Kaz)

2
@Polygnome- "Code easy to reason about" almost exclusively alludes to its mathematical properties and formal verification听起来大概是问题的答案。您可能希望将其发布为答案,而不是不同意注释中的(主观)答案。
dukeling

47

一种机制或一段代码很容易推论出何时需要考虑一些事情来预测其作用,而需要考虑的事情也很容易获得。

真正的函数没有副作用且没有状态很容易推论,因为输出完全由输入决定,输入就在参数中。

相反,带有状态的对象很难推理,因为在调用方法时必须考虑对象处于什么状态,这意味着您必须考虑哪些其他情况可能导致该对象处于故障状态。特定状态。

更糟糕的是全局变量:要推理读取全局变量的代码,您需要了解可以在代码中的何处设置该变量以及为什么进行设置-而且可能不容易找到所有这些位置。

最难解释的是具有共享状态的多线程编程,因为不仅有状态,而且有多个线程同时更改它,所以要推断一段代码在被一个线程执行时会做什么?必须考虑到在每个执行点上,某个其他线程(或其中的几个!)可能正在执行代码的几乎任何其他部分,并更改了您正在眼前操作的数据。从理论上讲,可以使用互斥体/监视器/关键部分/随便调用它来进行管理,但实际上,除非有人将共享状态和/或并行性大幅度地限制在很小的范围内,否则没有人能够真正可靠地做到这一点代码部分。


9
我确实同意这个答案,但是即使使用纯函数,声明性方法(如CSS或XSLT,make甚至C ++模板专门化和函数重载)也可以使您回到考虑整个程序的位置。即使您认为已经找到了某些东西的定义,该语言也允许在程序中的任何位置进行更具体的声明来覆盖它。您的IDE可能对此有所帮助。
史蒂夫·杰索普

4
我要补充一点,在多线程方案中,您还必须对代码要使用的较低级指令有相当深入的了解:在源代码中看起来很原子的操作在实际执行中可能会出现意外中断点。
加里德·史密斯

6
@SteveJessop:确实,这一点经常被忽略。有一个原因为什么C#会让您说什么时候要使方法可重写而不是悄悄地将可重写性设置为默认方法。我们希望在此时挥舞旗帜,说“程序的正确性可能取决于您在编译时找不到的代码”。(也就是说,我也希望“密封”是C#中的默认类。)
Eric Lippert

@EricLippert sealed不成为默认值的最终原因是什么?
Zev Spitz

@ZevSpitz:这个决定早于我的时间就做出了;我不知道。
埃里克·利珀特

9

在函数式编程的情况下,“易于推理”的含义主要是确定性的。我的意思是,给定的输入将始终导致相同的输出。您可以对程序执行任何操作,只要您不触碰这段代码,它就不会中断。

另一方面,OO通常更难以推论,因为产生的“输出”取决于每个涉及对象的内部状态。它表现出的典型方式是意外的副作用:更改代码的一部分时,看似无关的部分会中断。

...功能编程的缺点当然是实际上,您要做的很多事情是IO和状态管理。

但是,还有很多其他事情很难推理,我同意@Kilian的观点,并发是一个很好的例子。分布式系统也是如此。


5

避免更广泛的讨论,并解决特定的问题:

您能否提供“不容易推理”的一些示例,以便将其与“不容易推理”的示例进行比较?

我指的是“梅尔的故事,一个真正的程序员”,这是一名程序员的民间传说,可以追溯到1983年,因此对我们的职业算是“传奇”。

它讲述了一个程序员编写代码的故事,该代码在任何可能的情况下都首选奥术,包括自引用和自修改代码以及对机器错误的故意利用:

一个明显的无限循环实际上已经以利用进位溢出错误的方式进行了编码。在解码为“从地址x加载”的指令中加1通常会产生“从地址x + 1加载”。但是,当x已经是可能的最高地址时,不仅地址会回零,而且会在读取操作码的位中携带1,从而将操作码从“ load from”更改为“ jump to”,因此完整指令已从“从最后一个地址加载”更改为“跳转到地址零”。

是“难以推理”的代码示例。

当然,梅尔会不同意...


1
+1代表我多年以来最爱的梅尔(Mel)的故事。
John Bollinger

3
在此处阅读Mel的故事,因为Wikipedia文章未链接到该故事
TRiG

页面上的@TRiG脚注3,不是吗?
AakashM

@AakashM莫名其妙地错过了这一点。
TRiG

5

我可以提供一个例子,一个非常普通的例子。

考虑下面的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每次使用一个循环时,都要付出一次理解循环的代价。有时成本值得支付,但通常“更容易推理”更为重要。


2

一个相关的短语是(I释义),

代码没有“ 没有明显的错误 ”是不够的:相反,它应该有“ 显然没有错误 ”。

RAII是一个相对“容易推理”的例子。

另一个示例可能是避免致命的拥抱:如果您可以持有一个锁并获得另一个锁,并且有很多锁,那么很难确定没有发生致命拥抱的情况。添加诸如“只有一个(全局)锁”或“您在持有第一把锁的同时不允许获得第二把锁”之类的规则,可使系统相对容易地进行推理。


1
嗯 我不确定RAII是否这么容易推断。当然,从概念上来说很容易理解,但是要真正推理(即预测)大量使用RAII的代码的行为则变得更加困难。我的意思是,它基本上是作用域级别的不可见函数调用。如果您曾经做过任何COM编程,那么很多人都很难对此进行推理,这一事实很明显。
科迪·格雷

我的意思是相对容易(C ++与C相比):例如,存在一种受语言支持的构造函数,意味着程序员无法创建/拥有/使用他们忘记初始化的对象,等等
。– ChrisW

该基于COM的示例是有问题的,因为它混合了样式,即C ++样式的智能指针(CComPtr<>)和C样式的函数(CoUninitialize())。就我记得您在模块作用域和整个模块生命周期内调用CoInitialize / CoUninitialize而言,我也发现它是一个奇怪的示例,例如在main或中DllMain,而不是在一些短暂的局部函数作用域中,如示例所示。
ChrisW

出于说明目的,这是一个过于简化的示例。完全正确的是,COM是在模块范围内初始化的,但可以将Raymond的示例(如Larry的示例)想象main为应用程序的入口点()函数。您可以在启动时初始化COM,然后在退出前立即对其进行初始化。除了使用RAII范式的全局对象(例如COM智能指针)外。关于混合样式:Raymond提出了一个可以在ctor中初始化COM并在dtor中未初始化的全局对象的可行方法,但这是微妙的,并且不易推论。
科迪·格雷

我认为,在很多方面,COM编程更容易在C语言中进行推理,因为一切都是显式的函数调用。背后没有隐藏或看不见的东西。这需要更多的工作(例如,比较乏味),因为您必须手动编写所有这些函数调用,然后返回并检查您的工作以确保您已正确完成工作,但是所有工作都已暴露,这是关键使其易于推理。换句话说,“有时智能指针太聪明了”
Cody Gray

2

编程的关键是案例分析。艾伦·珀利斯(Alan Perlis)在第32号Epigram中对此进行了评论:程序员的能力和逻辑不应该通过程序员的才智和逻辑来衡量,而要通过案例分析的完整性来衡量。

如果案例分析容易,则很容易推断出情况。这要么意味着很少要考虑的案例,要么就是没有几个特殊案例–可能会有一些大的案例空间,但是由于某些规律性而崩溃,或者屈服于诸如归纳法这样的推理技术。

例如,算法的递归版本通常比命令式版本更容易推论,因为它不会贡献多余的情况,这些情况是由于没有出现在递归版本中的支持状态变量的变异而引起的。而且,递归的结构适合于数学上的归纳证明模式。我们不必考虑复杂性,例如循环变体和最弱的严格先决条件等等。

另一个方面是案例空间的结构。与分层案例相比,将案例平分或基本平分为几分的情况更容易推理:带有子案例和子子案例的案例,等等。

简化推理的系统的一个属性是正交性:这是控制子系统的案例在组合在一起时保持独立的属性。没有任何组合会引起“特殊情况”。如果将四格事物与三格事物正交组合,则有十二种情况,但理想情况每个案例都是两个独立的案例的组合。从某种意义上说,实际上并没有十二种情况。这些组合只是我们不必担心的“突发情况”。这意味着我们仍然可以考虑四种情况,而无需考虑其他子系统中的其他三种情况,反之亦然。如果必须特别标识某些组合并赋予其他逻辑,则推理将更加困难。在最坏的情况下,每个组合都有一些特殊的处理方式,然后除了原始的四个和三个之外,实际上还有十二个新的情况。


0

当然。并发:

互斥体强制执行的关键部分:易于理解,因为只有一个原则(两个执行线程不能同时进入关键部分),但是容易造成效率低下和死锁。

替代模型,例如无锁编程或参与者:可能会更优雅,更强大,但很难理解,因为您不再可以依赖(貌似)基本概念,例如“现在将此值写入该位置”。

易于推理是方法的一个方面。但是选择使用哪种方法需要综合考虑所有方面。


13
-1:非常非常糟糕的例子,使我认为您不理解该短语对您的含义。实际上,“由互斥体强制执行的关键部分”是最难推理的事情之一-几乎每个使用它们的人都会引入竞争条件或僵局。我将为您提供无锁的编程,但是actor模型的全部问题是推理起来非常容易得多。
Michael Borgwardt

1
问题在于,并发本身是程序员思考的一个非常困难的话题,因此并没有一个很好的例子。与无锁编程相比,由互斥体强制执行的关键部分是一种相对简单的实现并发的方法,这是完全正确的,但是大多数程序员就像Michael一样,当您开始谈论关键部分和互斥体时,他们的目光会掠过。当然,这似乎并不容易理解。更不用说所有的错误了。
科迪·格雷

0

让我们将任务限制为形式推理。因为幽默,发明或诗意推理具有不同的规律。

即使这样,该表达式仍是模糊定义的,不能严格设置。但这并不意味着它对我们应该保持如此黯淡。让我们想象一个结构正在通过一些测试并获得不同点的分数。每一点的好标记意味着该结构在各个方面都很方便,因此是“易于推理”。

“易于推理”的结构应在以下方面获得良好的评价:

  • 内部术语具有合理,易于区分和定义的名称。如果元素具有某种层次结构,则父名称和子名称之间的差异应与兄弟姐妹名称之间的差异不同。
  • 结构元件类型数量少
  • 使用过的结构元件类型是我们习惯的简单事物。
  • 难以理解的元素(递归,元步骤,4维以上的几何图形...)是孤立的-不能彼此直接组合。(例如,如果您尝试考虑将递归规则更改为1,2,3,4..n..Dimension多维数据集,这将非常复杂。但是,如果您将这些规则中的每一个都转换为某个公式根据n,您将为每个n-多维数据集分别拥有一个公式,并为该公式分别具有一个递归规则。并且可以很容易地分别考虑两个结构)
  • 结构元素的类型明显不同(例如,不使用从0到1的混合数组)

测试是主观的吗?是的,自然是这样。但是表达本身也是主观的。一个人容易做到的事,另一个人却不容易。因此,对于不同的域,测试应该有所不同。


0

可以推理的功能语言的思想来自它们的历史,特别是ML,它是作为类似于可计算函数逻辑用于推理的结构的编程语言开发的。与命令式语言相比,大多数功能语言更接近于形式化编程语言,因此从代码到推理系统的输入的转换负担较少。

对于推理系统的示例,在pi演算中,命令式语言中的每个可变存储位置都需要表示为单独的并行过程,而一系列功能操作是单个过程。从LFC定理证明者开始的40年中,我们正在使用GB RAM,因此拥有数百个进程已不再是一个问题-我使用pi-calculus消除了几百行C ++的潜在死锁,尽管该表示具有数百个处理推理机确实耗尽了大约3GB的状态空间并解决了间歇性错误。这在20世纪70年代是不可能的,或者在1990年代初期需要一台超级计算机,而类似大小的功能语言程序的状态空间却很小,无法回溯到那时。

从其他答案来看,该短语已成为流行语,即使摩尔定律已侵蚀了许多难以推理命令式语言的困难。


-2

易于推理是一个文化特定术语,这就是为什么很难提出具体示例的原因。这是一个固定在要进行推理的人身上的术语。

“易于推理”实际上是一个非常自我描述的短语。如果有人正在查看代码,并且想推理它的作用,那很容易=)

好的,把它分解。如果您正在查看代码,通常会希望它做一些事情。您想确保它按照您的预期去做。因此,您开发了有关代码应该做什么的理论,然后您要对此进行推理,以试图争论代码确实起作用的原因。您尝试像人类(而不是计算机)那样思考代码,并尝试合理化有关代码可以做什么的参数。

对于“易于推理”而言,最糟糕的情况是,唯一能理解代码作用的方法是像图灵机一样对所有输入进行逐行编码。在这种情况下,推理任何有关代码的唯一方法就是将自己变成计算机并在脑海中执行。在混乱的编程竞赛中很容易看到这些最坏的例子,例如解密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更像是一个悬而未决的家伙,迫使他们考虑离开示波器时可能发生的所有事情。


12
Perl代码很难阅读;没有理由。如果我有一定的兴趣要理解它,我将取消对代码的混淆。实际上难于解释的代码是什么时候仍然难以推理,即何时对它进行了很好的格式化,并为所有内容提供了清晰的标识符,并且没有代码精打细算。
卡兹(Kaz)
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.