退货被认为有害吗?没有它,代码可以起作用吗?


45

好的,标题有点clickbaity,但是我已经很认真地告诉别人,请不要踢一会儿。我喜欢它如何鼓励以真正的面向对象的方式将方法用作消息。但是,这个棘手的问题一直困扰着我。

我开始怀疑编写良好的代码是否可以同时遵循OO原则和功能原则。我正在设法调和这些想法,而我所坚持的最大症结是return

纯函数具有两种性质:

  1. 使用相同的输入重复调用它总是得到相同的结果。这意味着它是不可变的。其状态仅设置一次。

  2. 它不会产生副作用。调用它引起的唯一变化就是产生结果。

那么,如果您发誓要使用return它来传达结果,那么如何才能完全发挥功能呢?

出来,不问用什么有些人会考虑副作用思想工作。当我处理一个对象时,我不会询问它的内部状态。我告诉它我需要做的事情,它使用其内部状态来弄清楚该如何处理我已经告诉它要做的事情。一旦我告诉了我,我就不会问它做了什么。我只是希望它对它被告知要做的事情有所作为。

我认为“告诉,不要问”不仅仅是封装的另一个名称。当我使用时,return我不知道叫我什么。我不能说这是协议,我必须强迫它处理我的协议。在许多情况下,这表示为内部状态。即使暴露的不是确切的状态,通常也只是对状态和输入args进行一些计算。拥有一个响应界面,可以将结果整理成比内部状态或计算更有意义的结果。那就是信息传递请参阅此示例

回想过去,当磁盘驱动器中实际上装有磁盘,而拇指驱动器是您在汽车上做的,而车轮太冷而无法用手指触摸时,我就被教给了令人讨厌的人们如何考虑没有参数的功能。void swap(int *first, int *second)似乎很方便,但是我们鼓励我们编写返回结果的函数。因此,我坚信信念,并开始遵循。

但是现在我看到人们在构建体系结构,在该体系结构中,对象可以通过其构造方式控制结果的发送位置。这是一个示例实现。再次注入输出端口对象似乎有点像out参数的想法。但这就是告诉对象不要告诉其他对象他们所做的事情的方式。

当我第一次了解副作用时,我将其视为输出参数。有人告诉我们不要以令人惊讶的方式进行某些工作,也就是说,不遵守return result公约,以使人们感到惊讶。现在可以肯定,我知道有很多并行异步线程问题会带来副作用,但是返回实际上只是一个约定,您可以将结果压入堆栈,以便以后调用时可以将其弹出。真的就是这些。

我真正想问的是:

return避免所有这些副作用的苦难并获得没有锁等的线程安全性的唯一方法。还是我可以跟着说,不要以纯粹的功能性方式提出要求


3
如果您选择不忽略命令查询分隔,您是否认为问题已解决?
rwong

30
考虑到踢自己可能意味着您正在从事教条驱动的设计,而不是在每种情况下都说出利与弊。
Blrfl

3
通常,当人们谈论“返回”是有害的时,他们说的是对结构化编程而非功能的反对,并且在例程的末尾(可能在if / else块的两侧末尾都使用了一个 return语句)本身就是最后一个元素)。
Random832 '18

16
@jameslarge:错误的二分法。不能让自己被教条式的思维驱使去设计,这与您在谈论的“狂野西部” /“一切都行”的方法不同。关键是不允许教条妨碍好的,简单的,显而易见的代码。普遍法是针对缺乏者的;语境适合国王。
Nicol Bolas

4
对我来说,有一个尚不清楚的问题是:您说您已经使用'return'宣誓就职,但据我所知,您并没有明确地将其与您的其他概念联系起来,也没有说您为什么这样做。与纯函数的定义(包括产生结果)结合使用时,您将创建无法解决的问题。
丹尼科夫'18

Answers:


96

如果一个函数没有任何副作用并且不返回任何东西,那么该函数就没有用了。它是如此简单。

不过,我想你可以使用一些骗子,如果你想跟着的规则,而忽略了根本理由。例如,严格来说,使用out参数不使用返回值。但是它仍然与收益完全一样,只是以更复杂的方式。因此,如果您认为return由于某种原因是不好的,那么出于相同的根本原因,使用out参数显然是不好的。

您可以使用更多复杂的作弊技巧。例如,Haskell以IO monad技巧而闻名,您可以在实践中产生副作用,但从理论上讲,从严格的角度讲,它仍然没有副作用。连续传递样式是另一个技巧,它可以使您避免以将代码转换为意大利细面条为代价的回报。

最重要的是,没有愚蠢的技巧,副作用自由函数和“无回报”这两个原则根本不兼容。此外,我首先要指出两者都是错误的原则(实际上是教条),但这是不同的讨论。

诸如“告诉,不要问”或“没有副作用”之类的规则无法普遍应用。您始终必须考虑上下文。没有副作用的程序实际上是无用的。即使是纯函数式语言也承认这一点。相反,他们努力将代码的纯净部分与具有副作用的部分分开。Haskell中State或IO monad的意义不是要避免副作用(因为不能这样做),而是要通过函数签名明确指示副作用的存在。

“不告诉”规则适用于另一种体系结构,即程序中的对象是相互通信的独立“角色”的样式。每个参与者基本上都是自治的并且被封装。您可以向它发送一条消息,并决定如何对此做出反应,但是您不能从外部检查角色的内部状态。这意味着您无法确定消息是否更改了参与者/对象的内部状态。状态和副作用是设计隐藏


20
@CandiedOrange:方法是直接还是间接产生副作用,尽管多层调用在概念上没有任何改变。这仍然是一个副作用。但是,如果要指出的是,副作用仅通过注入的对象发生,因此您可以控制可能发生的副作用,那么听起来这是一个不错的设计。它不是没有副作用的。这是一个很好的面向对象,但它不只是功能。
JacquesB

12
@LightnessRacesinOrbit:安静模式下的grep具有将使用的进程返回代码。如果没有它,那就真的没用了
unperson325680 '18

10
@progo:返回代码不是副作用。它效果。我们称其为副作用的任何事物都称为副作用,因为它不是返回码;)
莫妮卡(Monica)

8
@Cubic就是重点。没有副作用的程序是没有用的。
詹斯·肖德

5
@mathreadler这是一个副作用:)
Andres F.

32

告诉,不要问带有一些基本假设:

  1. 您正在使用对象。
  2. 您的对象具有状态。
  3. 对象的状态会影响其行为。

这些都不适用于纯函数。

因此,让我们回顾一下为什么我们有规则“告诉,不要问”。此规则是警告和提醒。可以这样总结:

允许您的班级管理自己的状态。不要询问它的状态,然后根据该状态采取措施。告诉班级您想要什么,然后让班级根据自己的状态决定要做什么。

换句话说,类只负责维护自己的状态并对其执行操作。这就是封装的全部意义所在。

福勒

Tell-Don't-Ask是一个原则,可以帮助人们记住面向对象是将数据与对该数据进行操作的功能绑定在一起。它提醒我们,与其向对象询问数据并对该数据执行操作,不如告诉对象该怎么做。这鼓励我们将行为转移到与数据一起使用的对象中。

重申一下,这些都与纯函数,甚至是不纯函数无关,除非您将类的状态暴露给外界。例子:

违反TDA

var color = trafficLight.Color;
var elapsed = trafficLight.Elapsed;
If (color == Color.Red && elapsed > 2.Minutes)
    trafficLight.ChangeColor(green);

不是违反TDA

var result = trafficLight.ChangeColor(Color.Green);

要么

var result = await trafficLight.ChangeColorWhenReady(Color.Green);     

在后两个示例中,交通信号灯均保持对其状态及其动作的控制。


等一下,闭包可以是纯净的。他们有状态(他们称其为词法范围)。词汇范围会影响他们的行为。您确定TDA仅在涉及对象时才相关吗?
candied_orange

7
@CandiedOrange闭包仅在不修改封闭式绑定的情况下才是纯闭包。否则,在调用从闭包返回的函数时,您将失去引用透明性。
贾里德·史密斯

1
@JaredSmith和您谈论对象时不是一样吗?这不只是不变性问题吗?
candied_orange

1
@bdsl-现在您无意中指出了与trafficLight.refreshDisplay示例有关的此类讨论的确切位置。如果遵守这些规则,则会导致一个高度灵活的系统,只有原始编码人员才能理解。我什至打赌,经过两年的中断,即使是原始编码人员也可能不理解他们的所作所为。
Dunk

1
“傻”,如“不要打开其他对象并且不看它们的胆量,而是将自己的胆量(如果有的话)洒到其他对象的方法中;这就是方法”-傻。
Joker_vD

30

当我处理一个对象时,我不会询问它的内部状态。我告诉它我需要做的事情,它使用其内部状态来弄清楚该如何处理我已经告诉它要做的事情。

您不仅要询问其内部状态,也不问它是否也具有内部状态

还请告诉,不要问!意味着没有得到一个返回值(由提供形式的结果return声明的方法内)。它只表示我不在乎您如何做,而是进行处理!。有时您立即想要处理结果...


1
CQS虽然将意味着修改状态和取得成果应该分开
JK。

7
@jk。像往常一样:通常,您应该将状态更改结果分开,但是在极少数情况下,有充分的理由将其合并。例如:iterators next()方法不仅应返回当前对象,还应更改迭代器的内部状态,以便下一次调用返回下一个对象...
Timothy Truckle,

4
究竟。我认为OP的问题仅仅是由于误解/误用了“告诉,不要问”。解决这种误解会使问题消失。
康拉德·鲁道夫'18

@KonradRudolph另外,我认为这不是唯一的误解。他们对“纯功能”的描述包括“其状态仅设置一次”。另一则评论表明这可能意味着关闭的上下文,但是措辞对我来说很奇怪。
伊兹卡塔'18

17

如果您认为return“有害”(留在自己的照片中),则不要像

ResultType f(InputType inputValue)
{
     // ...
     return result;
}

以消息传递方式构建它:

void f(InputType inputValue, Action<ResultType> g)
{
     // ...
     g(result);
}

只要fg没有副作用,将它们链接在一起也将没有副作用。我认为此样式类似于也称为Continuation-passing样式

如果这真的导致“更好”的程序值得商,,因为它违反了某些约定。德国软件工程师Ralf Westphal围绕此建立了一个完整的编程模型,他将其称为“基于事件的组件”,并使用一种称为“流程设计”的建模技术。

要查看一些示例,请从此博客条目的“翻译为事件”部分开始。对于完整的方法,我推荐他的电子书“作为编程模型进行消息传递-像您想要的那样进行OOP”


24
If this really leads to "better" programs is debatable我们只需要研究本世纪前十年用JavaScript编写的代码。jQuery及其插件很容易出现这种范式回调。在某个时候,太多的嵌套回调使调试成为噩梦。不管软件工程及其“原理”如何古怪,人类仍然必须阅读代码
Laiv

1
即使在那时,您仍需要提供执行副作用的动作,或者需要某种方式退出CPS部分
jk。

12
@Laiv CPS是作为一种针对编译器的优化技术而发明的,实际上没有人期望程序员以这种方式手工编写代码。
Joker_vD

3
@CandiedOrange以口号形式,“返回只是告诉继续该做什么”。确实,创建Scheme的动机是试图了解休伊特的演员模型,并得出结论,演员和封闭者是同一回事。
Derek Elkins '18

2
假设地,假设您可以将整个应用程序编写为一系列不返回任何内容的函数调用。如果您不介意其紧密耦合,则甚至不需要端口。但是大多数理智的应用程序都使用返回值的函数,因为...好吧,它们是理智的。而且,正如我相信已经在回答中充分展示的那样return,只要您不指挥对象的状态,就可以从函数中获取数据,并且仍然遵守“告诉不要问”的原则。
罗伯特·哈维

7

消息传递本质上是有效的。如果您告诉某个对象执行某项操作,则您希望它对某项操作有影响。如果消息处理程序是纯净的,则无需向其发送消息。

在分布式参与者系统中,操作的结果通常作为消息发送回原始请求的发送者。消息的发件人要么由actor运行时隐式提供,要么(按照惯例)作为消息的一部分显式传递。在同步消息传递中,单个响应类似于return语句。在异步消息传递中,使用响应消息特别有用,因为它允许在多个参与者同时进行处理,同时仍交付结果。

将结果明确传递给的“发件人”基本上是对连续传递样式或可怕的参数进行建模-除了它将传递消息给他们而不是直接对它们进行突变。


5

这整个问题使我感到“违反级别”。

您在一个主要项目中具有(至少)以下级别:

  • 系统级别,例如电子商务平台
  • 子系统级别,例如用户验证:服务器,AD,前端
  • 单个程序级别,例如上述组件之一
  • 演员/模块级别[根据语言而变模糊]
  • 方法/功能级别。

依此类推,直到单个令牌。

实际上,在方法/功能级别上没有任何实体不返回(即使只是返回this)也没有必要。而且(在您的描述中)不需要在Actor级别的实体返回任何东西(取决于可能甚至不可能的语言)。我认为混淆是混淆这两个级别,并且我认为应该对它们进行明确的推理(即使任何给定的对象实际上跨越了多个级别)。


2

您提到要同时符合“告诉,不要问”的OOP原则和纯函数的功能原则,但是我不太明白这是如何导致您避开return语句的。

遵循这两个原则的一种相对通用的替代方法是全力以赴地使用return语句,并且仅将不可变对象与getter一起使用。然后的方法是让某些吸气剂返回具有新状态的相似对象,而不是更改原始对象的状态。

这种方法的一个示例是Python内置tuplefrozenset数据类型。这是frozenset的典型用法:

small_digits = frozenset([0, 1, 2, 3, 4])
big_digits = frozenset([5, 6, 7, 8, 9])
all_digits = small_digits.union(big_digits)

print("small:", small_digits)
print("big:", big_digits)
print("all:", all_digits)

它将打印以下内容,表明union方法创建了一个具有其自身状态的新冻结集,而不会影响旧对象:

小:Frozenset({0,1,2,3,4})

大:Frozenset({5,6,7,8,9})

全部:frozenset({0,1,2,3,4,5,6,7,8,8,9})

类似的不可变数据结构的另一个广泛示例是Facebook的Immutable.js库。在这两种情况下,您都将从这些构建块开始,并且可以按照相同的原理构建更高级别的域对象,从而实现功能性的OOP方法,该方法可帮助您更轻松地封装数据并对其进行推理。不变性还使您获得了在线程之间共享此类对象而不必担心锁的好处。


1

我开始怀疑编写良好的代码是否可以同时遵循OO原则和功能原则。我试图调和这些想法,而我坚持的最大症结就是回报。

我一直在尽力调和命令式和函数式编程的某些好处(自然不会获得所有好处,而试图获得两者的最大份额),尽管return实际上对于在许多情况下对我来说是一种直接的方式。

为了避免return直接发表声明,我在过去约一个小时的时间里仔细考虑了这一点,基本上,很多次我的脑袋都溢出了。我可以看到它的吸引力在于,它强制执行最强的封装和信息隐藏功能,以支持非常自治的对象,这些对象只是被告知要做什么,而且我喜欢探索思想的极端性,如果只是为了寻求更好的解决方案了解他们的工作方式。

如果我们以交通信号灯为例,那么天真地尝试就想让这种交通信号灯了解周围的整个世界,从耦合的角度来看,这当然是不希望的。因此,如果我理解正确,那么您将其抽象并解耦,以便推广I / O端口的概念,这些I / O端口将进一步通过管道传播消息和请求,而不是数据,并以相互之间期望的交互/请求的方式注入这些对象同时彼此忘却。

节点管道

在此处输入图片说明

该图大约是我试图勾勒出来的(虽然简单,但我必须不断对其进行更改和重新思考)。我立即倾向于认为,具有这种去耦和抽象级别的设计将很难以代码形式进行推理,因为将所有这些东西连接到一个复杂世界的协调者可能会发现很难跟踪所有这些交互和请求,以创建所需的管道。但是,以视觉形式,将这些东西绘制为图形并将所有内容链接起来并看到交互发生的事情可能相当简单。

在副作用方面,我可以看到它没有“副作用”,因为这些请求可以在调用堆栈上导致为每个线程执行的命令链,例如(我不算这在实际中是“副作用”,因为在实际执行此类命令之前,它不会更改与外界相关的任何状态-在大多数软件中,我的实际目标不是消除副作用,而是延迟并集中它们。而且,命令执行可能会输出一个新世界,而不是改变现有世界。只是想理解所有这些内容,我的大脑真的很费劲,却没有任何原型制作这些想法的尝试。我也没有

这个怎么运作

因此,澄清一下,我在想像您如何实际对此进行编程。我看到它工作的方式实际上是上面捕获用户端(程序员)工作流程的图表。您可以将交通信号灯拖到世界上,拖动计时器,给它经过的时间(“构建”时)。计时器具有一个On Interval事件(输出端口),您可以将其连接到交通信号灯,以便在此类事件中,它告诉信号灯在其颜色之间循环。

然后,红绿灯在切换为某些颜色时,可能会发出输出,例如,,On Red这时我们可能将行人拖到我们的世界中,并使该事件告诉行人开始走...或者我们可能将鸟拖入我们的场景并做到这一点,以便当灯光变成红色时,我们告诉鸟类开始飞行并拍打翅膀……或者也许当灯光变成红色时,我们告诉炸弹爆炸-无论我们想要什么,物体都在完全相互忘却,除了通过这种抽象的输入/输出概念间接告诉彼此该做什么之外,什么也不做。

并且他们完全封装了状态,并且什么也没有透露(除非这些“事件”被认为是TMI,在这一点上我不得不重新考虑很多事情),它们彼此告诉对方要做的事情,他们不问。而且它们是超级解耦的。除了这种通用的输入/输出端口抽象外,一无所知。

实际用例?

我可以看到这种类型的东西在某些领域中用作高级特定于领域的嵌入式语言,以协调所有这些对周围环境一无所知的自治对象,不暴露其内部状态后构造,并且基本上只是传播请求彼此之间,我们可以改变和调整我们内心的内容。此刻,我觉得这是非常特定于领域的,或者也许我只是没有足够的思考,因为我很难将自己的大脑与我经常开发的事物类型联系在一起(我经常与相当低端的代码),如果我要解释“告诉,不要问这样的极端”,并想要可以想象到的最强封装级别。但是,如果我们正在特定领域中使用高级抽象,

信号和插槽

对我来说,这种设计看起来很奇怪,直到我意识到如果不考虑如何实现它的细微差别,它基本上就是信号和插槽。我要问的主要问题是,我们如何有效地编程图中的这些单个节点(对象),使其严格遵守“告诉,不要问”的原则,以达到避免return语句的程度,以及我们是否可以对图进行评估而不会产生突变(在平行,例如没有锁定)。那就是神奇的好处不在于我们如何将这些东西潜在地连接在一起,而是如何在不存在突变的情况下将它们实现到这种封装程度。这两种方法对我来说似乎都是可行的,但是我不确定它的适用范围如何,这就是我为尝试使用潜在用例而感到困惑的地方。


0

我在这里清楚地看到确定性的泄漏。似乎“副作用”是众所周知的且通常被理解的术语,但实际上并非如此。根据您的定义(OP中实际上缺少的定义),副作用可能是完全必要的(正如@JacquesB设法说明的那样),或者无情地被接受。或者,正在朝着澄清一步,有必要区分期望的 一个副作用不喜欢隐藏(在这点著名Haskell的IO出现了:这只不过是一个办法是明确的)和不希望的 副作用作为代码错误之类的结果。这些是完全不同的问题,因此需要不同的推理。

因此,我建议从表述自己开始:“我们如何定义副作用,给定的定义对它与“ return”语句的相互关系有何看法?”。


1
“在这一点上,著名的Haskell IO出现了:它不过是一种明确的方式”-明确无疑是Haskell单子IO的一个好处,但它还有另外一点:它提供了一种将副作用完全隔离的方法语言之外的语言-尽管常见的实现实际上并没有这样做,主要是出于效率方面的考虑,但从概念上讲是正确的:IO monad可以被认为是将指令返回到完全不在语言环境中的某种方式。 Haskell程序以及对函数的引用在完成后将继续。
Jules
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.