逻辑推理能力训练与程序调试

简介

这篇文章是最近在查资料时发现的一篇文章。我觉得非常有意思。作者在这篇文章中介绍了在平时调试程序时可以使用的一些推理方法,学习这些方法可以更好的帮助我们在脑海中形成一个有用的框架,让我们以一种更有条理的方式调试程序。

需要说明的是,这篇文章是使用最近发现的一个叫DeepL翻译器做机器翻译然后我再适当修改形成的,可能有些地方比较奇怪。但不得不说,这个翻译功能着实强大,翻译效果很不错。有兴趣的朋友也可以自己去尝试一下。

文章来源 thoughtbotClassical Reasoning and Debugging

正文

当古典哲学家们在思考如何思考时,他们开始研究一个人如何建立一个复杂的论证并证明或反驳一个想法的各种方法。这就孕育了推理和逻辑的研究,作为一个学科。

他们确定了一些广泛的逻辑思维方式的类别。在调试时,这些方法可以形成一个有用的框架,因为它给了我们一种方法来有条不紊地寻找真相:为什么会出现这种情况呢?

在古典哲学中,并没有一个明确的推理类型清单。下面是我在自己的经验中发现的一些特别有帮助的类型。我也对每种类型的定义做了一些松散的处理,以保持例子的易懂和有用。

文艺复兴


类比推理法

这是我最喜欢的方法之一! 类比推理是一个三步走的过程。

  1. 将你的难题转化为等价的简单问题(称为类比)。
  2. 解决这个简单的问题。
  3. 将简单问题的解决方案转换为困难问题的解决方案。

通过类比进行调试的过程

在调试中应用这个方法的一个特别有用的方法是将一个错误减小到其最简单的形式。在你的上下文中,"最小 "的意思可能并不总是很明显。没关系。你可以慢慢地把复杂的部分一个一个地去掉,同时验证该错误是否仍然发生。最终,你将会留下一段小得多的代码。现在应该更容易找到错误的原因了。解对于整个问题,解决办法可能是相同的。

当处理一个更大的系统中的错误而不是一个单一的文件时,这样做可能会更困难,因为这类错误往往是由多个组件之间的交互引起的。比方说,在添加了一个图像处理 gem 之后,你注意到了 Rails 应用程序中的错误行为。这个 gem 坏了吗?你是否对它进行了错误的配置?你的一些其他现有代码与新的gem不兼容吗?

你怎么能简化这个问题呢?一种方法可能是生成一个全新的空白Rails应用程序,并添加可疑的gem。如果问题出现在这里,你现在就有一个小得多的应用程序可以调试。一旦你找到了解决方案,就可以把它移植到你真正的应用中。

除了简化你现有的环境外,类比还允许你将你的错误转移到一个有一些相似之处的不同环境中,在那里你可以更好地进行调试。例如,在这个Elm调试故事中,我能够将合成随机生成器时的问题重现为合成函数时的问题。我对函数组合的属性比较熟悉,很快就找到了错误的根源。然后我把这个解决方案回传到我原来的随机生成器问题上,就能解决这个问题了。

通过与函数的类比解决随机生成器的bug


排除法

有时候,排除错误的答案比找到正确的答案更容易。排除过程属于归纳推理的范畴(过于简单了!)。

在调试中,这种方法一个特别有用的形式是二分搜索。这个想法是,你进行一系列的实验来验证一个错误是否是由某段代码引起的。你设计的实验,无论结果如何,你都能否定大约一半的可能性。不断重复,直到你找到错误的源头。

尽管名字很花哨,但这可以非常简单地完成。例如,我试图找到一个发生在某个特定文件中的错误。我可以使用注释或条件语句来阻止一半的文件执行。我还能重现这个错误吗?如果可以,那么我就知道这个错误是在活动的半个文件中。如果不是,那就一定是在非活动的那一半文件中。不管怎样,我已经排除了一半的可能性,并且更接近于找到错误的来源。

使用二分搜索方法来定位文件中的错误

许多程序并不只是一个接一个的线性指令集。我们有条件反射和分支逻辑,导致流程看起来更像一棵树而不是一个列表。我们仍然可以使用二分搜索方法。与其使用 "将列表一分为二 "的心智模型,不如用修剪树上的分支的方式来思考,这样会有帮助。你可以消除的每一个分支都缩小了你需要继续搜索错误来源的区域。

修剪决策树的分支以找到一个bug

二分搜索不仅可以让你在空间上找到bug,而且还可以在时间上找到bug! git bisect命令让你在你的git历史上使用同样的方法来有效地找到某个特定的bug是什么时候引入的。


演绎推理

这通常是人们在谈论 "逻辑 "或 "推理 "时想到的东西。我们在调试时一直在脑海中这样做。给出一系列开始的事实(称为前提),我们建立一个逻辑链来得出结论。这可能看起来像:

  1. 鉴于Postgres在验证唯一性约束时提出了一个重复键错误
  2. 考虑到违反唯一性约束只发生在INSERT或UPDATE语句上
  3. 鉴于唯一的数据库写入发生在CreateOrderService对象中
  4. 因此,错误一定发生在CreateOrderService对象中。

我们怎么知道呢?演绎推理是最数学化的推理形式,可以表示为一种等式:x ⇒ y,读作 “如果x则y”,或 “x意味着y”。

duplicate_error ⇒ index_violation
index_volation ⇒ db_write
db_write ⇒ create_order_invoked

有各种数学定律可以应用于这些 “逻辑方程”。有些只是感觉上的常识,其他的(如,德摩根定律)并不是立即就能直观感受到的。如果你想进一步挖掘这个话题,你需要搜索的术语是 “命题逻辑”。

通过传递属性减少一连串的if...else的前提
这里特别令人感兴趣的是传递属性。它指出,一长串的如果…那么…那么,如x⇒y⇒z,可以简化为一个单一的如果…那么x⇒z,在我们上面的例子中,意味着:

duplicate_error ⇒ create_order_invoked

但要注意一些陷阱。如果你的任何起始前提是错误的,那么整个事情就会崩溃。在上面的例子中,如果还有其他文件也写到数据库中,那么我们的结论就可能是错误的。

一个更微妙的陷阱是,你的直觉可能导致你错误地应用一些规律。这方面一个特别常见的例子是把⇒当作是双向的关系。仅仅因为索引冲突意味着发生了数据库写入,并不意味着反过来也是如此。数据写入并不意味着发生了索引冲突。

在你的脑海中真的很容易犯这些错误。很多时候,从这种错误中恢复过来的最好方法是放慢脚步,写下你的前提和你如何得出结论的。了解一些命题逻辑符号会有帮助,但普通的散文也可以。


矛盾证明

也被称为 “归谬法”(reductio ad absurdum),来自一个拉丁短语,意思是 “归谬法”。这是演绎推理的一个变种,但你不是试图证明某件事情是真的,而是试图证明相反的错误。

这个过程通常是这样的:

选择一个你想证明为真的假设。现在,假设其反面为真。使用演绎推理,从你的相反假设中得出逻辑结论。得出一个不可能或荒谬的结论。

我们的目标是得出一个不可能是真的结论(矛盾)。因为它不可能是真的,所以你的相反假设也不可能是真的,因此,原来的假设一定是真的。用命题逻辑表示法:

P ⇒ Q
¬Q
∴ ¬P

这在实践中是什么样子的呢?

比方说,当你试图将坏数据从表单中保存到数据库中时遇到了一个错误。我们很难推理出错误发生在哪里,但很容易显示出错误没有发生的地方,这就以一种迂回的方式告诉我们错误发生在哪里。

你怀疑错误发生在数据库之外。矛盾推理将尝试遵循相反的推理(错误发生在数据库内),并证明这导致了荒谬的结果。

  1. 鉴于我们试图保存的数据与现有数据重复
  2. 鉴于我们的数据库有一个唯一索引
  3. 鉴于向数据库保存重复的数据会导致唯一索引异常
  4. 鉴于我们没有看到唯一索引异常
  5. 鉴于错误发生在数据库中(我们的相反结论)。
  6. 因此我们的数据库不强制执行唯一索引(矛盾)。

与演绎逻辑一样,如果你的任何前提是错误的,那么你的结论也可能是错误的。特别是,"这种情况不可能发生 "的假设往往是错误的。在上面的例子中,如果发现那个特定的数据库表没有唯一索引,我们的逻辑就会崩溃。糟糕!。


归纳推理

归纳推理是在我们看了一堆具体的例子并试图推导出一个更广泛的原则时使用的。在调试中经常是这样的情况。我们并不总是有一套 "真理 "来推理。相反,我们只是有:“在X情况下会发生这种奇怪的行为,但在Y情况下会发生不同的奇怪行为”。

更多的样本案例来推理是特别有用的。因此,我们尝试在本地重现这些问题。我们改变一些输入,看看这对结果有什么影响。我们做大量的笔记。我们甚至可以积累生产中的日志。然后我们尝试检测模式。

科学方法就属于这个类别,是归纳调试的好方法。17世纪科学的名称是 “自然哲学”,这不是没有道理的。一旦我们认为我们看到了一些模式,我们就试图证明我们的预感是错误的。我们可以通过做一个建议的修复,然后尝试手动重现这个错误来做到这一点。我们还可以添加一些自动化测试案例,检查一堆已知的触发问题的方式。如果我们的修改始终不能触发这个bug,那么我们的修复就很可能是正确的。

请注意,和科学一样,归纳推理并不能肯定地证明什么。相反,它是在我们能够接触到的情况下,对正确解决方案的最佳近似。有可能存在一些我们没有考虑到的边缘情况。有可能我们的 "修复 "只是掩盖了症状,但并没有解决根本问题。最终,我们可能会得到一些更多的表明我们错了的样本案例。

当我陷入困境时,我喜欢使用的归纳技巧是 “搅拌锅”,可以这么说。我向一段代码扔出一堆不同的数据,看看它是如何反应的。我甚至可能对代码本身做一些半随机的改变,看看结果会有什么变化。我们的目标并不是要找到一个解决方案,而是要产生一系列的样本案例,以便我有足够的数据点来开始看到模式。


谬误的谬误

随着我们对每一种推理类型的探索,总是有一些陷阱,推理不正确地把我们引向错误的道路。然而,仅仅因为你的推理有缺陷,并不一定意味着你的结论是错误的。这就是谬误的谬误。有时你很幸运。其他时候,你的直觉引导你找到了正确的解决方案,尽管你用来证明的逻辑是错误的。

始终验证你的假设、前提和结论!


总结

这里的推理形式并不是相互排斥的。在一个典型的调试过程中,你可能想同时使用所有这些方法。事实上,即使在阅读这篇文章时,你也可能认为所描述的一些技术来自其他章节的推理方法。你没有错。

我们每天在调试时都会凭直觉使用所有这些推理方法,甚至更多。然而,对我们正在使用的方法有一个明确的了解,可以让我们以一种更有条理的方式来解决这个问题,避免兜圈子。了解我们正在使用的技术的陷阱也使我们能够保持警惕,避免一些逻辑上的死胡同。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值