什么是有状态和无状态属性以及它们的优势?
什么是有状态和无状态属性以及它们的优势?
我从各种来源中了解到一个关于“状态”的定义,大致如下:\n
- \n
- “程序在存储输入方面的条件”;
- “程序执行时任意时刻内存位置的内容”
\n
\n
\n但是,当我查询“无状态”的特征时(例如,“Haskell是无状态的”):\n
- \n
- “应用程序不依赖于其状态”;
- “物理状态不能改变”
- “相同输入产生相同输出 - 内存中的地址始终保持不变”
- “方法不依赖于实例及其对应的实例变量”
\n
\n
\n
\n
\n现在,我一定误解了(模糊的?)前一种定义,因为与“无状态”模型相结合的函数式编程语言也存储输入,对吗?还是这只是函数评估而不是数据变异的问题?\n
\n嗯,我有点明白这种模型有时候很强大 - 在程序验证、调试和并发方面的应用。但是当我阅读关于以下内容时,情况变得相当复杂:\n
- \n
- “它消除了与竞态条件相关的一整类多线程错误”
- 更具表现力的代码(不管这是什么?)
- “静态评估……可以用于有利地指导计算机在井字棋游戏树中的位置” https://www.cs.kent.ac.uk/people/staff/dat/miranda/whyfp90.pdf(或者静态类型是另一个完全不同的问题吗?)
\n
\n
\n
\n
\n因此,我也想知道在迭代式编程语言中能够操作状态的优势,许多论坛给出了例如通过调用add()来改变“年龄”将会改变“年龄”变量在其作用域之外的示例。\n也许这是因为我对面向对象编程缺乏经验,但在更广泛的应用中使用状态的确切优势是什么?\n如果可以提供示例代码,请尽量使用Python和Haskell作为这两种对立学科的代表。由于我不擅长阅读其他语言,这似乎阻碍了我对其他帖子解释的理解。
什么是有状态和无状态属性以及其优缺点?
在一个没有任何状态的程序中,程序不能读取外部状态(例如,检查系统时间),这意味着除了可以作为参数传递的值之外,任何函数都无法区分不同的情况,因为不可能存在不同的情况。
对于这类程序,运行时实际上是多余的,因为优化编译器可以静态地推导出任何结果输出。它基本上等同于将一个大型数学方程写出来。你可以解决它,但“运行程序”的想法是多余的,因为它具有程序本身固有的不可变的输出。
显然,这与大多数程序不同,即使是被描述为“无状态”的程序也是如此。通常有一些最小的状态,例如最初传递给程序的输入。例如,想象一个输出数字K的平方根的前N位的程序。该程序现在具有一个初始状态,但除了知道N和K是什么之外,程序不需要跟踪程序级别的状态,例如一周的第几天,用户是否喜欢MDY还是DMY日期格式等。但是,由于该程序几乎肯定涉及某种动态循环来查找N位数,它需要与循环相关联的某种状态(例如迭代次数)。
所以当代码被称为“无状态”时,它并不是一个100%的承诺,而是对代码依赖状态程度的一种限定。
那么这里有什么优点和缺点?你的代码依赖状态越多,它做出程序员未预料到的行为的可能性就越大。记住,状态是运行时的内在属性。但我们不是在运行时编写代码。我们可以尝试想象可能发生的所有不同运行时状态,但这很快就会失控。
以下是一个在有状态和无状态方式下相同的Python程序的示例。
有状态版本:
import math
angle_type = 'radians'
def cosecant(x):
if angle_type == 'radians':
return 1/math.sin(x)
elif angle_type == 'degrees':
return 1/math.sin(x*math.pi/180)
else:
raise NotImplementedError('cosecant is not implemented for angle type', angle_type)
无状态版本:
import math
def cosecant(x):
return 1/math.sin(x)
def cosecant_degrees(x):
return 1/math.sin(x*math.pi/180)
这个例子可能看起来很奇怪,但是同样的原则和陷阱也适用于更复杂的程序。我们可以看到使用状态会导致问题。考虑有状态程序。其中一个问题是调用cosecant的人并不一定知道angle_type的当前状态,他们必须在使用cosecant之前每次设置它,以确保它会按照他们想要的方式工作。由于程序员不喜欢重复自己,有人可能会在程序开始时声明mymathlib.angle_type = 'degrees',并假设没有其他东西会改变它。例如,如果你正在使用度数进行工作,但是然后调用一个子程序,该子程序将angle_type更改为“弧度”,并在完成后没有更改为度数。
如果程序的其他部分正在更改它,那么程序员实际上必须在调用之前每次设置值。即使如此,如果代码在多个线程上运行,也不能保证在将angle_type设置为“度数”之后,另一个线程没有在您的cosecant调用执行之前立即将其设置为其他值(竞态条件)。
在我们无状态的程序版本中,所有这些问题都消失了。代价是什么?现在我们有两个不同的函数而不是一个。为什么这是一个代价?嗯,一般来说,保持API较小而不是较大被认为是一个好的做法,拥有多个完成相同工作的工具对于使用您的库的人来说很令人困惑。基于这个原则,有时候在一个函数中将两个或多个不同的功能合并在一起似乎是诱人的,如果它们似乎在做同样的事情。这并不是太糟糕的,事实上,它通常有助于库的维护和可用性。但在这种情况下,它导致的问题比好处更多,因为在有状态程序中,虽然程序员可以有效地使用它来做同样的事情,但我们不再拥有简单的保证,即函数每次执行时都会按照预期执行。
这个例子表面上看起来可能有点愚蠢,但是考虑到Python的decimal模块几乎做了同样的事情。在decimal模块中,状态变量,包括小数位数精度和舍入规则,存储在当前线程的上下文中。这不像之前的例子那样有问题。由于每个线程都有自己的上下文,我们不必担心竞态条件,但仍然存在潜在的麻烦点,比如如果子程序在不好好更改的情况下改变状态。
很容易合理化这种设计,并说“一位有能力的程序员应该能够分析代码并能够避免这些与状态相关的问题,如果他们花时间思考的话”。从理论上讲,这是正确的,但如果我们看看人们实际编写代码的方式,最小努力的原则大多数时候都优先于其他一切。看看官方文档中对decimal模块的使用示例,或者任何教程,你会注意到一个常见的模式。对decimal.getcontext()的引用比对decimal.setcontext()的引用多出10倍——如果有提到setcontext的话,通常根本没有。为了以一种称职的“安全第一”的方式管理状态,这两个工具同等重要,甚至更应该更经常使用decimal.setcontext(),因为它可以用来保证一致的行为。因为这个最小努力的原则,不可避免地会有程序员,尤其是初学者,编写像这样的代码,在测试中可能工作,但在程序发展过程中没有硬性的安全保证。
结论
那么,有状态的代码是邪恶的,无状态的代码是我们的救世主吗?也许吧。将这个小格言视为避免某些陷阱的指导原则可能会有所帮助。现实情况是,有时很难避免使用有状态的代码,甚至是程序设计的固有部分。例如,如果没有状态,我们怎么制作视频游戏?我们如何制作一个生命值计数器?玩家如何在关卡中移动并跟踪其位置?我们可以有分数或胜利条件吗?有状态的代码不会消失,但了解以这种方式设计程序的常见陷阱确实有所帮助。
谢谢,这解决了很多问题 🙂