纯功能vs告诉,不要问?


14

“函数的理想参数个数为零”是完全错误的。理想的参数数目恰好是使函数不受副作用影响所需的数目。少于这个数目,您不必要地导致您的功能不纯净,从而迫使您远离成功之路,攀登痛苦的梯度。有时“鲍勃叔叔”会在他的建议下出现。有时他错得很厉害。他的零论点建议就是后者的一个例子

来源:@David Arno在此站点上的另一个问题下的评论

该评论获得了133个投票的惊人成绩,这就是为什么我想更加关注它的优点。

据我所知,编程有两种不同的方式:纯函数式编程(此评论令人鼓舞)和告诉,不要问(不时建议在本网站上推荐)。AFAIK这两个原则从根本上是不兼容的,彼此之间几乎是相反的:纯函数可以概括为“仅返回值,没有副作用”,而告诉,不问可以概括为“不返回任何东西,只有副作用”。另外,我有点困惑,因为我认为告诉,不要问,它被视为OO范式的核心,而纯函数则被视为功能范式的核心-现在,我看到了OO中推荐的纯函数!

我想开发人员应该选择这些范例之一并坚持下去吗?好吧,我必须承认,我永远也无法跟随自己。通常,返回值对我来说似乎很方便,而且我真的看不到如何仅凭副作用实现我想达到的目标。通常,对我来说副作用很方便,而且我真的看不到如何仅通过返回值就能达到我想要达到的目的。而且,有时(我想这太可怕了),我有两种方法都能做到。

但是,从这133个投票中,我认为当前纯函数式编程正在“取胜”,因为它已成为一种共识,说出来更好,不要问。它是否正确?

因此,在这个反模式化游戏的示例中,我尝试制作:如果我想使其符合纯功能范式-怎么办?!

对我来说,拥有战斗状态似乎是合理的。由于这是基于回合的游戏,因此我将战斗状态保留在字典中(多人游戏-可能有许多玩家同时进行许多战斗)。每当玩家回合时,我都会在战斗状态上调用适当的方法,该方法会(a)相应地修改状态,然后(b)向玩家返回更新,这些更新会序列化为JSON,并且基本上只是告诉他们发生了什么板。我想,这是在公然违反两项原则的同时。

好的-如果我确实想,我可以制定一个方法返回战斗状态,而不是就地修改它。但!然后,我是否将不得不复制战斗状态中的所有内容,只是为了返回一个全新的状态,而不是就地进行修改?

现在,如果此举是一种攻击,那么我可以返回一个更新了HP的角色?问题是,这不是那么简单:游戏规则,一举一动不仅会消除一部分玩家的生命值,而且往往会产生更多的影响。例如,它可能会增加字符之间的距离,应用特殊效果等。

对我来说,只需修改适当的状态并返回更新就简单得多了。

但是,经验丰富的工程师将如何解决这个问题?


9
遵循任何范例都是确保失败的肯定方法。政策决不能胜过智力。解决问题的方法应取决于问题,而不是您对解决问题的宗教信仰。
John Douma

1
我从来没有问过我以前说过的话。我感到很荣幸。:)
David Arno

Answers:


14

像大多数编程格言一样,“讲,不问”牺牲了清晰度以求简洁。完全不建议不要要求计算结果,而建议不要要求计算输入。“不要先获取,然后进行计算,然后进行设置,但是可以从计算中返回一个值,”并不那么费力。

人们通常会先调用getter,然后对它进行一些计算,然后再调用带有结果的setter来实现。这清楚地表明您的计算实际上属于您称为getter的类。“告诉,不要问”是为了提醒人们注意这种反模式而创造的,它的效果如此之好,以至于现在有些人认为这一部分很明显,他们正在寻找其他类型的“问询”来消除。但是,格言仅对这种情况有用。

纯函数式程序永远不会遭受那种确切的反模式影响,原因很简单,即没有那种风格的二传手。但是,在同一函数中不混合不同语义抽象级别的更普遍(更难于理解)的问题适用于每个范例。


感谢您正确解释“告诉,不要问”。
user949300

13

鲍伯叔叔和大卫·阿诺(David Arno)(您所引用引文的作者)都有重要的教训,我们可以从他们的著作中汲取教训。我认为值得学习该课程,然后推断对您和您的项目真正意味着什么。

第一:鲍伯叔叔的教训

Bob叔叔指出,函数/方法中使用的参数越多,使用它的开发人员就必须了解的越多。认知负担不是免费的,并且如果您与论据的顺序不一致,等等,认知负担只会增加。

那是人类的事实。我认为Bob叔叔的“清洁代码”书中的关键错误是语句“函数的理想参数个数为零”。极简主义很棒,直到不是那样。就像您从未在微积分中达到极限一样,您也永远也不会达到“理想”的代码。

正如爱因斯坦(Albert Einstein)所说:“一切都应该尽可能简单,但不要简单”。

第二:David Arno的教训

David Arno描述的开发方式是比面向对象更多的功能样式开发。但是,功能代码的伸缩方式比传统的面向对象的编程更好。为什么?由于锁定。任何时候对象中的状态可变时,您都将面临竞争条件或锁定争用的风险。

编写了用于仿真和其他服务器端应用程序的高度并发系统后,该功能模型发挥了神奇作用。我可以证明该方法已取得的改进。但是,这是一种非常不同的开发风格,具有不同的要求和习惯用法。

发展是一系列的权衡

您比我们任何人都更了解您的应用程序。您可能不需要功能样式编程随附的可伸缩性。上面列出的两个理想之间有一个世界。我们这些处理需要处理高吞吐量和荒谬的并行性的系统的人将倾向于函数式编程的理想状态。

就是说,您可以使用数据对象来保存传递给方法所需的信息集。这有助于解决Bob叔叔正在解决的认知负担问题,同时仍然支持David Arno正在解决的功能理想。

我已经在要求并行性有限的台式机系统和高吞吐量仿真软件上工作。他们有非常不同的需求。我会欣赏编写良好的面向对象的代码,该代码是围绕您熟悉的数据隐藏概念设计的。它适用于多种应用。但是,它并不适用于所有人。

谁是对的?好吧,在这种情况下,大卫比鲍伯叔叔更对。但是,我想在此强调的基本点是,方法应具有尽可能多的自变量。


有并列主义。可以并行处理不同的战斗。但是,是的:一场战斗在进行中时需要锁定。
gaazkam,

是的,我的意思是读者(以您的类比收割者)将从他们的(播种者)著作中收集。就是说,我回头看看过去写的一些东西,或者重新学习了一些东西,或者不同意我以前的自我。我们都在学习和发展,这就是为什么您应该始终通过如何以及是否应用所学知识进行推理的第一原因。
Berin Loritsch '19

8

好的-如果我确实想,我可以制定一个方法返回战斗状态,而不是就地修改它。

是的,就是这个主意。

然后,我将不得不复制战斗状态中的所有内容以返回全新状态,而不是对其进行修改吗?

不可以。您的“战斗状态”可以建模为不可变数据结构,其中包含其他不可变数据结构作为构建块,可能嵌套在不可变数据结构的某些层次结构中。

因此,战斗状态的某些部分在一轮中不必更改,而其他部分则必须更改。不变的部分不必复制,因为它们是不可变的,因此只需复制对这些部分的引用,而不会带来任何副作用。在垃圾收集的语言环境中效果最佳。

Google提供了“有效的不可变数据结构”,您一定会找到一些参考资料,说明它们通常如何工作。

对我来说,仅修改适当的状态并返回更新似乎简单得多。

对于某些问题,这确实可以更简单。考虑到游戏状态从一轮到另一轮的变化,游戏和基于回合的模拟可能属于这一类。但是,对于真正“简单”的理解在某种程度上是主观的,并且在很大程度上还取决于人们习惯了什么。


8

作为评论的作者,我想我应该在这里进行澄清,当然,除了我的评论提供的简化版本以外,还有更多其他内容。

AFAIK这两个原则在根本上是不兼容的,彼此之间几乎是相反的:纯函数可以概括为“仅返回值,没有副作用”,而告诉,不问可以概括为“不返回任何东西,只有副作用”。

老实说,我发现这对“告诉,不要问”这个词确实很奇怪。因此,我读了几年前马丁·福勒(Martin Fowler)在这个问题上所说的话,这很有启发性。我发现它很奇怪的原因是因为“告诉不要问”在我脑中与依赖注入同义,并且最纯粹的依赖注入形式是通过函数的参数传递函数需要的所有内容。

但是看来,我适用于“告诉不要问”的意思来自于采用Fowler面向对象的定义,并使它更不可知。在此过程中,我相信它将概念带入其逻辑结论。

让我们回到简单的起点。我们有“逻辑块”(程序),并且我们有全局数据。过程直接读取该数据以便访问它。我们有一个简单的“询问”方案。

向前走一点。现在,我们有了对象和方法。该数据不再需要是全局的,可以通过构造函数传递并包含在对象中。然后,我们有了对这些数据起作用的方法。因此,现在我们按照福勒的描述“告诉,不要问”。对象被告知其数据。这些方法不再需要向全局范围询问其数据。但这有问题:在我看来,这仍然不是真正的“告诉,不要问”,因为那些方法仍然必须问对象范围。我觉得这更像是一种“先告诉后问”的方案。

因此,前进到现代,放弃“从头到尾的OO”方法,并借鉴函数式编程的一些原则。现在,当调用方法时,所有数据都通过其参数提供给它。可以(并且已经)争论过:“有什么意义,只是在使代码复杂化?” 是的,通过参数传递可以通过对象范围访问的数据,确实会增加代码的复杂性。但是,将数据存储在对象中而不是使其全局可访问性也增加了复杂性。但是很少有人会认为全局变量总是更好,因为它们更简单。关键是,“告诉,不要问”带来的好处,要比减少范围的复杂性更为重要。与将范围限制到对象相比,这更适用于通过参数传递。private static并通过参数传递它所需的一切,现在可以信任该方法,以免偷偷地访问它不应该访问的内容。此外,它鼓励使方法保持较小,否则参数列表将失去控制。并且鼓励编写符合“纯功能”标准的方法。

因此,我认为“纯粹的功能”和“告诉,不要问”彼此相反。就我而言,前者是后者的唯一完整实现。福勒的方法还不完整,“告诉,不要问”。

但是重要的是要记住,这种“完全做到不诉说”的做法确实是一种理想,即实用主义必须减少发挥作用,而我们要成为理想主义者,从而将其错误地视为有史以来唯一可能的正确方法。很少有应用程序可以达到接近100%无副作用的简单原因,原因很简单:如果真正没有副作用,它们将无用。我们需要更改状态,我们需要IO等才能使应用程序有用。在这种情况下,方法必定会引起副作用,因此不能纯粹。但是,这里的经验法则是将这些“不纯”的方法保持在最低限度。只是让它们有副作用是因为它们需要这样做,而不是作为规范。

对我来说,拥有战斗状态似乎是合理的。由于这是基于回合的游戏,因此我将战斗状态保留在字典中(多人游戏-可能有许多玩家同时进行许多战斗)。每当玩家回合时,我都会在战斗状态上调用适当的方法,该方法(a)相应地修改状态,并且(b)向玩家返回更新,这些更新被序列化为JSON,并且基本上只是告诉他们发生了什么板。

对我来说,拥有一个战斗状态似乎是合理的。似乎很重要。这样的代码的全部目的是处理更改状态的请求,管理这些状态更改并将其报告回来。您可以全局处理该状态,可以将其保存在单个播放器对象中,也可以将其传递给一组纯函数。您选择哪种选择取决于哪种情况最适合您的特定情况。全局状态简化了代码的设计,并且速度很快,这是大多数游戏的关键要求。但这使代码难以维护,测试和调试。一组纯函数将使代码的实现更加复杂,并可能由于过度的数据复制而使其速度太慢。但这将是最简单的测试和维护。“ OO方法”介于两者之间。

关键是:没有一种始终有效的完美解决方案。纯功能的目的是帮助您“跌入成功之门”。但是,如果那个坑太浅,由于它可能给代码带来复杂性,那么您就不会像绊倒它那样陷入其中,那么这不是适合您的方法。追求理想,但务实,当这次理想不是一个理想的去处时,就停下来。

最后,重申一下:纯函数和“告诉,不要问”根本不是对立的。


5

不管怎么说,存在一个上下文,您可以在其中放置该语句,这将使其变得荒谬。

在此处输入图片说明

如果您接受零参数建议,那么Bob叔叔是完全错误的。如果您认为这意味着每个附加参数都会使代码更难阅读,那么他是完全正确的。这是有代价的。您无需在函数中添加参数,因为它使函数更易于阅读。您向函数添加参数是因为您无法想到一个使该参数明显依赖的好名字。

例如pi(),它是一个非常好的函数。为什么?因为我不在乎它是如何计算的,甚至也不算。或者,如果使用e或sin()得出返回的数字。我很好,因为名字告诉了我所有我需要知道的。

但是,并不是每个名字都告诉我所有我需要知道的。有些名称并不能说明理解控制函数行为和公开参数的重要信息。那就是使编程的功能风格更容易推理的原因。

我可以以完全的OOP风格保持事物的不变性和副作用。Return是一种简单的机制,用于将值保留在堆栈中以用于下一个过程。您可以使用输出端口将值传递给其他不可变的东西,使其保持不变,直到您碰到最后一个必须更改某些内容的输出端口(如果您希望人们能够读取它)。无论是否使用功能,每种语言都是如此。

因此,请不要声称函数式编程和面向对象的编程是“根本不兼容的”。我可以在功能程序中使用对象,也可以在OO程序中使用纯函数。

但是,将它们混合会产生成本:期望。您可以忠实地遵循这两种范式的机制,但仍然会造成混乱。使用功能语言的好处之一是将副作用(尽管必须存在以获取任何输出)放置在可预测的位置。当然,除非以可变的方式访问可变对象。然后,您所用的那种语言所给定的东西就崩溃了。

同样,您可以支持具有纯函数的对象,也可以设计不可变的对象。问题是,如果您不表示这些功能是纯函数或对象是不可变的,那么除非人们花大量时间阅读代码,否则人们不会从这些功能中获得推理的好处。

这不是一个新问题。多年来,人们以“ OO语言”进行程序编码,以为他们在使用OO语言,因为他们使用“ OO语言”。很少有什么语言能使您免于步履蹒跚。为了使这些想法可行,它们必须生活在您体内。

两者都提供良好的功能。两者都可以。如果您有足够的勇气将它们混合,那么请清楚地标记它们。


0

我有时会努力理解各种范式的所有规则。在这种情况下,他们有时彼此矛盾。

OOP是一种当务之急的范例,它涉及在发生危险事件的世界中用剪刀奔跑。

FP是一种功能范式,其中人们可以在纯计算中找到绝对的安全性。这里什么都没有发生。

但是,所有程序都必须桥接到命令式世界中,以发挥作用。因此,功能核心,势在必行的外壳

当您开始定义不可变对象(其命令返回修改后的副本而不是实际变异的对象)时,事情变得混乱。您对自己说:“这就是OOP”,“我正在定义对象行为”。您回想起久经考验的“告诉,不要问”原理。麻烦的是,您将其应用于错误的领域。

领域完全不同,并且遵循不同的规则。功能领域逐渐发展到它想要向世界释放副作用的地步。为了释放这些效果,所有本应封装在命令式对象中的数据(必须以这种方式编写!)必须要使用命令式外壳。如果无法访问这些数据,而这些数据在另一个世界中将通过封装被隐藏,则无法完成工作。这在计算上是不可能的。

因此,当您编写不可变对象(Clojure称为持久数据结构)时,请记住您在功能域中。扔掉告诉,不要问窗外,只有当您重新进入命令性领域时才让它回到屋子里。

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.