编程是最好的逻辑能力训练方法! - thoughtbot


程序编程调试是最好训练培养一个人逻辑推理能力的方法,没有之一。数学推理过于严谨,人性化不足,普及性不够,想通过数学普及提升普通人逻辑能力,容易引起抵触,数学存在天赋论之说影响其普及;其他办法如科学实验与研究成本太高,因此编程训练逻辑能力最有效,比听一万遍罗振宇的“逻辑思维”讲座更有效。原文点击标题,大意如下:
当古典哲学家研究人类如何思考时,他们发现可以建立有关复杂证明或反驳一个想法的各种方式,这催生了对逻辑推理作为主题的研究。他们确定了一些广泛的逻辑思考方法。这些可以在调试时为我们提供有用的框架,因为它为我们提供了一种方法来有条不紊地寻找真相:为什么会这样?
古典哲学中没有明确的推理类型列表。以下是我发现对自己的经历特别有帮助的一些内容。为了使示例易于访问和有用,我还对每种类型的定义进行了一些松散的介绍。
 
类推推理
这是我最喜欢的方法之一!类推推理是一个三步过程:

  1. 将您的难题转化为等效的难题(称为 模拟)。
  2. 解决简单的问题。
  3. 将简单的解决方案转换为针对您的难题的解决方案。

在类比调试过程中,一种特别有用的方法是将错误减少到 最简单的形式。在您的上下文中,“最小”的含义可能并不总是很明显。没关系。您可以一次慢慢地消除一些复杂性,同时确认该错误仍然存​​在。最终,您将剩下一小段代码。现在应该更容易找到导致错误的原因。对于整个问题,解决方案可能是相同的。
当处理较大系统中的错误而不是单个文件中的错误时,这样做可能会更困难,因为此类错误通常是由多个组件之间的交互产生的 。假设您在添加图像处理gem之后注意到了Rails应用程序中的损坏行为。Ruby坏了吗?您配置不正确吗?您现有的其他一些代码是否与新的gem不兼容?
您如何简化这个问题?一种方法可能是生成全新的空白Rails应用程序并添加可疑的gem。如果问题仍然存在,您现在可以调试一个小得多的应用程序。找到解决方案后,可以将其移植回您的真实应用程序。
除了简化现有上下文之外,类比还允许您将错误转移到共享一些相似之处并且可以更好地进行调试的其他上下文中。
 
排除法
有时,消除错误答案比找到正确答案要容易。消除过程属于归纳推理类别。
在调试中,这种方法的一种特别有用的形式是二进制搜索。这个想法是您进行了一系列实验来验证错误是否是由特定的代码段引起的。设计实验时,无论结果如何,您都可以消除大约一半的可能性。不断重复,直到找到错误的根源。
尽管名字花哨,但可以很简单地完成。例如,我正在尝试查找特定文件中发生的错误。我可以使用注释或条件语句来阻止一半文件执行。我还能重现该错误吗?如果是这样,那么我知道该错误在活动的另外一半文件中。如果不是,它必须位于文件的非活动部分。无论哪种方式,我都消除了一半的可能性,并且更接近于发现错误的来源。

许多程序不只是一个又一个线性的指令集。我们有条件和分支逻辑,导致流看起来更像树而不是列表。我们仍然可以使用二进制搜索方法。与其使用“将列表分成两半”的思维模型,不如从树上修剪树枝方面进行思考。您可以消除的每个分支都缩小了继续寻找错误来源所需的区域。

二进制搜索不仅可以发现空间中的错误,还可以及时发现错误!git bisect命令使您可以在git历史上使用相同的方法来有效地找出何时引入了特定的错误。
 
演绎推理
人们通常在谈论“逻辑”或“推理”时会想到这些。调试时,我们始终在头脑中进行此操作。给定一系列起始事实(称为前提),我们构建了一个逻辑链以得出结论。可能看起来像:

  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.then.then的长链,例如x ⇒ y ⇒ z可以简化为一个if..then x ⇒ z。在上面的示例中,这意味着:

duplicate_error ⇒ create_order_invoked

但是要当心一些陷阱。如果您的任何起始前提是错误的,那么整个事情就会分崩离析。在上面的示例中,如果还有其他文件也写入数据库,那么我们的结论可能是错误的!
一个更微妙的陷阱是,您的直觉可能会导致您误用某些法律。这种情况的一个特别常见的例子是将⇒这种关系视为双向有效。仅仅因为索引冲突意味着发生数据库写入并不意味着反过来也必然,数据库写入并不意味着发生索引冲突。
弄清楚这些错误真的很容易。通常,从此类错误中恢复的最佳方法是放慢脚步,写下您的前提 以及得出结论的方式。知道一些命题逻辑符号可能会有所帮助,但是普通的旧散文也可以。
 
矛盾证明
在拉丁语短语中矛盾证明也称为“ reducio ad absurdum ”,意思是“减少到荒谬”。这是演绎推理的一种变体,但是不是去尝试证明事实是正确的,而是试图证明它是相反的,是错误的。
该过程通常如下所示:
选择一个您想证明是正确的假设。现在,假设相反的说法是正确的。使用演绎推理从相反的假设中得出合理的结论。得出一个不可能或荒谬的结论。
目的是得出不可能正确的结论(矛盾)。因为它不能成立,所以您的相反假设不能成立,因此原始假设必须成立。用命题逻辑表示法:

P ⇒ Q
¬Q
∴ ¬P

实际上这是什么样的?
假设您在尝试将错误数据从表单保存到数据库时遇到错误。它可以是很难展现演绎其中的错误发生,但可以很容易地显示在那里没有发生,这是以一种迂回的方式,告诉我们在错误发生在哪里。
您怀疑该错误是在数据库外部发生的。通过矛盾推理将尝试遵循相反的推理(错误发生在数据库内部),并表明这导致了荒谬。

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

与演绎逻辑一样,如果您的前提不正确,那么您的结论也可能是错误的。特别是,“这不可能发生”的假设常常是错误的。在上面的示例中,如果发现该特定数据库表上没有唯一索引,我们的逻辑将崩溃。糟糕!
 
归纳推理
当我们看一堆具体示例并尝试推导出更广泛的原理时,将使用归纳推理。在调试中通常是这种情况。我们并非总有一套“真相”可以作为推理依据。相反,我们只有:“在情况X中,发生这种奇怪的行为,但在情况Y中,发生了另一种奇怪的行为”。
有更多的样本案例可以进行推理特别有用。因此,我们尝试在本地重现问题。我们更改一些输入,然后看它如何影响结果。我们做很多笔记。我们甚至可能在生产中累积日志。然后,我们尝试检测模式。
17世纪的科学名称是“自然哲学”,这并非毫无道理。一旦我们认为我们看到了一些模式,我们就会尝试证明我们的直觉是错误的。为此,我们可以进行建议的修复,然后尝试手动重现该错误。我们还可以添加一些自动化测试用例,以检查触发问题的一系列已知方法。如果我们始终无法通过更改触发该错误,则我们的修复很可能是正确的。
请注意,与科学一样,归纳推理并不能证明 任何东西。相反,它是给定我们可以访问的方案的最佳解决方案的最佳近似值。可能有一些我们未曾考虑过的极端情况。我们的“解决方案”可能只是掩盖了症状,而没有解决根本问题。最终,我们可能会得到更多表明我们错了的示例案例。
我在一段代码中抛出了很多不同的数据,然后看它们如何反应。我甚至可以对代码本身进行一堆半随机的更改,然后看看它如何改变结果。我们的目标不是要找到解决方案,而是要生成一系列示例案例,以便我有足够的数据点来开始观察模式。
 
谬论谬误
当我们探索每种类型的推理时,总是存在陷阱,推理会错误地将我们引向错误的道路。但是,仅仅因为您的推理存在缺陷并不一定意味着您的结论是错误的。这就是谬论谬误。有时候你会很幸运。在其他时候,即使您用来证明其合理性的逻辑是错误的,您的直觉也会引导您找到正确的解决方案。
始终验证您的假设,前提和结论!
 
总结
这里的推理形式并非彼此排斥。在典型的调试会话中,您可能希望将它们全部一起使用。实际上,即使阅读本文,您可能也认为所描述的某些技术来自其他部分的推理方法。
我们直观地使用所有这些推理方法,并且每天进行调试时都会使用更多方法。但是,对我们使用的方法有一个清晰的了解,可以使我们以更加结构化的方式来寻求解决方案,并且避免四处走动。知道我们正在使用的技术的陷阱也可以使我们保持警惕并避免某些逻辑上的死胡同。
 
这篇文章是我们正在进行的2021年调试系列的一部分。