如果我的对象是可变的,在函数式编程的上下文中会出什么问题?


9

我可以看到可变对象与不可变对象(如不可变对象)的好处,消除了由于共享和可写状态而导致的多线程编程中的许多疑难解答问题。相反,可变对象有助于处理对象的身份,而不是每次都创建新的副本,因此特别是对于较大的对象,还可以提高性能和内存使用率。

我想了解的一件事是,在函数式编程的上下文中拥有可变对象可能会出错。像告诉我的要点之一是,以不同顺序调用函数的结果不是确定性的。

我正在寻找一个真正的具体示例,其中很明显在函数编程中使用可变对象会导致什么问题。基本上,如果它不好,那么不管是面向对象还是功能编程范例都不好,对吗?

我相信,在我自己的发言下面,我可以回答这个问题。但是我仍然需要一些例子,以便我能更自然地感受到。

OO通过诸如封装,多态等工具帮助管理依赖性并编写更容易维护的程序。

函数式编程也具有促进可维护代码的动机,但是通过使用样式消除了使用OO工具和技术的需要-我认为其中之一是通过最小化副作用,纯函数等。


1
@Ruben我想说大多数函数语言都允许可变变量,但是使用它们却有所不同,例如,可变变量具有不同的类型
jk。

1
我认为您可能在第一段中混合了不可变和可变性?
jk。

1
@jk。,他当然做到了。编辑以纠正这一点。
David Arno '18

6
@Ruben 函数式编程是一个范例。因此,它不需要功能性的编程语言。并且某些fp语言(例如F#)具有此功能
Christophe

1
@Ruben没有,我在haskellhackage.haskell.org/package/base-4.9.1.0/docs/…中特别考虑过Mvars。不同的语言当然也有不同的解决方案,或者IORefs hackage.haskell.org/package/base-4.11.1.0 /docs/Data-IORef.html虽然你当然会从内部单子同时使用
JK。

Answers:


7

我认为通过与面向对象的方法进行比较可以最好地证明其重要性

例如说我们有一个对象

Order
{
    string Status {get;set;}
    Purchase()
    {
        this.Status = "Purchased";
    }
}

在OO范式中,方法被附加到数据上,并且该方法对数据进行突变是有意义的。

var order = new Order();
order.Purchase();
Console.WriteLine(order.Status); // "Purchased"

在功能范式中,我们根据功能定义结果。购买订单购买功能应用于订单的结果。这意味着我们需要确定一些事情

var order = new Order(); //this is a 'new order'
var purchasedOrder = purchase(order); // this is a 'purchased order'
Console.WriteLine(order.Status); // "New" order is still a 'new order'

您是否需要order.Status ==“ Purchased”?

这也意味着我们的功能是幂等的。即。两次运行它们应该每次产生相同的结果。

var order = new Order(); //new order
var purchasedOrder = purchase(order); //purchased order
var purchasedOrder2 = purchase(order); //another purchased order
var purchasedOrder = purchase(purchasedOrder); //error! cant purchase an order twice

如果通过购买功能更改了订单,则purchaseOrder2将失败。

通过将事物定义为函数的结果,它使我们可以使用那些结果而无需实际计算它们。在编程方面,这是延迟执行。

这本身可能很方便,但是一旦我们不确定某个函数何时真正发生并且对此表示满意,我们可以比在OO范例中更多地利用并行处理。

我们知道运行一个函数不会影响另一个函数的结果。因此我们可以让计算机以它选择的任何顺序执行它们,并使用任意数量的线程。

如果一个函数改变了它的输入,我们必须更加注意这些事情。


谢谢 !!非常有帮助。因此,购买的新实现看起来像是Order Purchase() { return new Order(Status = "Purchased") } ,状态为只读字段。?同样,为什么这种做法在函数编程范例的上下文中更有意义?您提到的好处也可以在OO编程中看到,对吗?
rahulaga_dev '18

在OO中,您期望object.Purchase()修改对象。您可以将其设为不变,但是为什么不转向完整的功能范式
Ewan

我认为问题必须可视化,因为纯自然的c#开发人员是面向对象的。因此,您在使用包含函数式编程的语言时所说的内容将不需要'Purchase()'函数返回购买的订单以附加到任何类或对象上,对吗?
rahulaga_dev '18

3
您可以编写函数式c#,将您的对象更改为结构,使其不可变,并编写Func <Order,Order> Purchase
Ewan

12

理解不可变对象为何有益的关键并不在于试图在功能代码中找到具体的例子。由于大多数功能代码都是使用功能语言编写的,并且默认情况下大多数功能语言都是不可变的,因此该范式的本质是为了避免发生您想要的东西。

要问的关键是,不变性的好处是什么?答案是,它避免了复杂性。假设我们有两个变量,xy。两者均以的值开头1y虽然每13秒翻一番。它们在20天后的价值是多少?x将会1。这很容易。尽管要解决,但要花点力气,y因为它要复杂得多。20天时间中的什么时间?我必须考虑夏令时吗?yvs 的复杂性x要高得多。

而且这也在真实代码中发生。每次在混合中添加一个变异值时,当您尝试编写,读取或调试代码时,该值就会变成另一个复杂的值,供您在头脑中或在纸上进行计算。复杂性越高,您犯错误和引入错误的机会就越大。代码很难编写;难以阅读;难以调试:很难正确编写代码。

可变性还不错。可变性为零的程序不会有结果,这是毫无用处的。即使可变性是将结果写入屏幕,磁盘或其他内容,它也必须存在。不好的是不必要的复杂性。降低复杂度的最简单方法之一是默认情况下使事物不变,并且由于性能或功能原因,仅在需要时使它们可变。


4
“降低复杂度的最简单方法之一是使事物默认不可变,并且仅在需要时使它们可变”:非常简洁的摘要。
Giorgio '18

2
@DavidArno您描述的复杂性使代码难以推理。当您说“这些代码很难编写;很难阅读;很难调试;……”时,您也谈到了这一点。我喜欢不可变对象,因为不可变对象不仅使我自己,而且使观察者在不了解整个项目的情况下,都更易于推理代码。
disassemble-number-

1
@RahulAgarwal,“ 但是为什么这个问题在函数式编程的背景下变得更加突出 ”。没有。我想也许我对您的要求感到困惑,因为FP鼓励不变性从而避免了该问题,因此在FP中问题远没有那么突出。
David Arno '18

1
@djechlin,“ 怎么能由13第二个例子变得更容易不变的代码分析? ”这不可能:y有变异; 这是一个要求。有时我们必须具有复杂的代码才能满足复杂的要求。我要说明的一点是,应避免不必要的复杂性。突变值本质上比固定值复杂,因此-为避免不必要的复杂性-仅在必要时才突变值。
David Arno

3
可变性造成身份危机。您的变量不再具有单一标识。相反,其身份现在取决于时间。因此,具有象征意义的是,我们现在有了一个族x_t,而不是单个x。现在,任何使用该变量的代码都将不得不担心时间,从而导致答案中提到的额外复杂性。
Alex Vong

8

在函数式编程中会出错的地方

在非功能性编程中可能会出错的同样的事情:您可能会得到不想要的意外副作用,这是自范围编程语言的发明以来众所周知的错误原因。

恕我直言,函数式编程和非函数式编程之间唯一真正的区别是,在非函数式代码中,您通常会期待副作用,而在函数式编程中,则不会。

基本上,如果它不好,那么不管是面向对象还是功能编程范例都不好,对吗?

当然-不管使用哪种范例,有害的副作用都是一类错误。反之亦然-故意使用的副作用可以帮助处理性能问题,并且对于大多数实际程序而言,在涉及I / O和处理外部系统时通常是必需的-也不考虑其范例。


4

我刚刚回答了一个StackOverflow问题,很好地说明了您的问题。可变数据结构的主要问题是它们的身份仅在一个确切的时间点有效,因此人们倾向于尽可能多地塞入代码中知道身份不变的小点。在这个特定的示例中,它在for循环中做了很多日志记录:

for (elem <- rows map (row => s3 map row)) {
  val elem_str = elem.map(_.toString)

  logger.info("verifying the S3 bucket passed from the ctrl table for each App")
  logger.info(s"Checking on App Code: ${elem head}")

  listS3Buckets(elem_str(1), elem_str(2)) match {

    case Some(allBktsInfo) =>
      logger.info(s"App: ${elem_str head} provided the bucket name as: ${elem_str(3)}")
      if (allBktsInfo.exists(x => x.getName == elem_str(3))) {
        logger.info(s"Provided S3 bucket: ${elem_str(3)} exists")
        println(s"s3 ${elem_str(3)} bucket exists")
      } else {
        logger.info(s"WARNING: Provided S3 bucket ${elem_str(3)} doesn't exists")
        logger.info(s"WARNING: Dropping the App: ${elem_str.head} from backup schedule")
        excludeList += elem_str.head // If the bucket is invalid then we exclude from backup
        println(s"s3 bucket ${elem_str(3)} doesn't exists")
    }

    case None =>
      logger.info(s"WARNING: Provided S3 bucket ${elem_str(3)} doesn't exists")
      logger.info(s"WARNING: Dropping the App: ${elem_str.head} from backup schedule")
      excludeList += elem_str.head // If the bucket is invalid then we exclude from backup
}

当您习惯了不变性时,如果等待太久,就不必担心数据结构会发生变化,因此您可以在闲暇时以更分离的方式执行逻辑上分开的任务:

val (exists, missing) = rows partition bucketExists
missing foreach {row =>
  logger.info(s"WARNING: Provided S3 bucket ${row("s3_primary_bkt_name")} doesn't exist")
  logger.info(s"WARNING: Dropping the App: ${row("app")} from backup schedule")
}

3

使用不可变对象的优点是,如果接收者检查对象时收到具有该对象的引用将具有某种属性,并且需要给其他代码提供对该对象具有相同属性的引用,则可以简单地传递沿着对对象的引用,而不考虑其他人可能已经收到引用或他们可能对对象执行的操作(因为没有其他人可以对对象执行任何操作),或者接收方可以检查对象(由于对象的所有内容)属性将是相同的,无论何时检查。

相比之下,需要给某人一个可变对象的引用的代码在接收者检查它时将具有一定的属性(假设接收者本身没有改变它)要么需要知道,除了接收者之外其他什么都不会改变该属性,或者知道接收方何时访问该属性,并且知道在接收方最后一次检查该属性之前,什么都不会更改。

对于一般的编程(不只是函数式编程),我认为将不可变对象分为三类是最有用的:

  1. 即使有引用,也无法通过任何方式更改对象。这样的对象以及对它们的引用表现为value,并且可以自由共享。

  2. 允许自己被具有引用的代码更改的对象,但是其引用永远不会暴露给任何实际更改它们的代码。这些对象封装了值,但是它们只能与可以信任的代码共享,而不会将其更改或公开给可能会这样做的代码。

  3. 将被更改的对象。最好将这些对象视为容器,并将其引用作为标识符

一种有用的模式通常是让一个对象创建一个容器,使用可以信任的代码填充该容器,然后再不保留引用,然后在宇宙中任何地方都存在唯一的引用,而这些引用永远不会修改该代码。对象填充后。尽管容器可能是可变的类型,但可以将它当作(*)来当作不可变的,因为实际上没有任何东西可以对其进行突变。如果所有对容器的引用都保存在不会改变其内容的不可变包装器类型中,则可以安全地传递此类包装器,就像将其中的数据保存在不可变对象中一样,因为对包装器的引用可以在以下位置自由共享和检查任何时候。

(*)在多线程代码中,可能有必要使用“内存屏障”来确保在任何线程可能看到对包装器的任何引用之前,该线程对容器的所有操作的影响都是可见的,但是这是一种特殊情况,仅出于完整性考虑。


感谢令人印象深刻的答案!我想我可能感到困惑的原因是因为我来自c#背景并且正在学习“用c#编写功能样式代码”,这种说法到处都在说避免可变对象-但我认为采用功能编程范例的语言会促进(或强制执行-不确定)如果强制执行正确使用)不变性。
rahulaga_dev '18

@RahulAgarwal:可能有一个对对象的引用封装了一个,该的含义不受同一个对象的其他引用的存在的影响,或者具有将它们与同一个对象的其他引用相关联的标识,或者没有。如果实字状态发生变化,则与该状态关联的对象的值或身份可以是恒定的,但不能两者都不变-一个必须更改。$ 50,000是应该做什么。
超级猫

1

如前所述,可变状态问题基本上是较大的副作用问题的子类,在副作用中,函数的返回类型不能准确描述函数实际执行的操作,因为在这种情况下,它还会进行状态突变。一些新的研究语言已经解决了这个问题,例如F *(http://www.fstar-lang.org/tutorial/)。这种语言创建了一个类似于类型系统的效果系统,其中函数不仅静态声明其类型,而且还声明其效果。这样,函数的调用者知道调用该函数时可能发生状态突变,并且这种影响会传播到其调用者。

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.