函数式编程中的对象 - 不变性

Baj*_*k80 4 c# oop erlang functional-programming scala

最近我一直在学习在Erlang中作为C和C#开发人员的长期编程.我对函数式编程很新.现在,我试图了解Scala等语言中的对象是如何工作的.我被告知OOP是关于使用其公共方法改变给定对象的状态.这些方法改变了公共财产和私人成员的状态.但现在我听说在函数式编程中,所有对象都应该是不可变的.好吧,我同意,一旦赋值变量(在给定函数中)应该保持指向同一个对象.但这种"不变性"是否意味着我无法使用公共方法更改给定对象的内部(属性,私有成员)?这使对象就像简单的数据容器一样.它提取了它们之外的所有功能.这使得对象更像C中的结构.这对我来说很奇怪.也许我错过了什么?是否有可能以旧式方式使用对象并仍将其视为函数式编程?

Vik*_*ova 5

OOP 就是使用其公共方法更改给定对象的状态。

这是一个谎言!

在函数式编程中,所有对象都应该是不可变的

这是一个谎言!

但是这种“不变性”是否意味着我不能使用它们的公共方法更改给定对象的内部(属性、私有成员)?

有严格的不变性和“模拟”不变性。通过“模拟”不变性,您可以更改内部结构,但不应产生可见的更改。例如,大量计算的缓存仍然是可以接受的。

是否有可能以老式的方式使用对象并仍然将其视为函数式编程?

这取决于您如何准确地改变对象以及您如何定义 FP。实际上,该死的,你可以在 FP 中改变对象。

也许我错过了什么?

是的,有很多东西你应该学习。


关于不可变对象,您不了解以下几点:

  1. 不可变对象仍然可以具有计算属性。所以它不仅仅是 C 中的结构。有关详细信息,请参阅统一访问原则。注意:在 C# 结构中可以有计算属性
  2. 不可变对象仍然可以有有用的方法。所以它不仅仅是 C 中的结构。你可以在不可变对象中有一些逻辑,但是这个逻辑不能改变状态。注意:在 C# 结构中可以有计算属性。
  3. 此外,内存安全语言(如 C#、Scala、Erlang)中的对象和结构之间存在巨大差异:您无法轻松使用结构。通常,安全代码中没有指针(除了不安全区域之类的东西)。所以你不能只是在不同的结构之间共享相同的结构。
  4. 不可变对象仍然可以封装一些数据。封装是进行抽象(好或坏)的关键

以下是您应该了解的有关 OOP 的内容:

  1. OOP 不需要改变状态。没有突变的 OOP 仍然是 OOP。
  2. OOP 有很好的用处:继承。纯 FP 中没有这样的东西。

以下是您应该了解的关于 FP 的事情:

  1. FP 不要求您使用不可变数据。但是在 FP 中处理不可变数据要容易得多。

  2. 如果一棵树倒在树林里,它会发出声音吗?如果一个纯函数为了产生一个不可变的返回值而改变一些本地数据,这样可以吗?”

    好的 FP 语言有很多方法来处理可变数据以满足性能需求。FP 只是喜欢隐藏突变。

  3. 只有纯 FP 语言迫使您使用不可变数据。

  4. FP 因其作为一等公民的功能而得名。在wiki上查看更多信息。您可以像使用 & 传递对象一样使用 & 传递函数。

  5. 好吧,很多 FP 与函数无关。


您还应该知道 OOP 和 FP 的关系:

  1. 封装和多态在 OOP 和 FP 中的工作方式不同。封装在 OOP 中是如何工作的非常清楚:私有/受保护和另一个访问修饰符。但是在纯 FP 中,封装一些数据的唯一方法是使用闭包。
  2. 多态在 OOP 和 FP 中的工作方式不同。在 OOP 中,您使用的是子类型多态性。但是在纯 FP 中,您应该使用协议(接口)或多方法之类的东西。
  3. “闭包是穷人的对象,反之亦然”。你可以用闭包和对象来表达相同的代码。
  4. C# 和 Scala 是多范式语言。它们都支持 OOP 和 FP。OOP 和 FP 混合使用也不错。实际上,这是生成可读代码的最佳方式。

现在,我想解释一下为什么有很多人认为某些范式不能组合:

  1. OOP 在不变性变得有用之前就诞生了。在过去,程序更简单,我们非常担心内存。
  2. 在我们的 CPU 能够并行代码之前,OOP 就诞生了。因此,不变性(这使得编写并行代码更容易)没有帮助。
  3. 在最近 FP 流行之前,它主要只对学者有用。学者们不喜欢不受控制的突变。
  4. 潜在的 FP 程序可以以如此疯狂的方式重组,以至于我们不能依赖副作用的时间顺序(以及作为副作用一部分的突变)。没有时间排序 = 无用的突变。但是像 C#、Scala 这样的语言并没有那么疯狂。但请看下一点。
  5. FP 在几乎诞生时就有惰性计算。他们也有 monads 或类似的机制来代替直接控制流。并且函数可以随时执行,因为它可以存储在任何地方。这使得副作用(和突变)更难控制。

澄清一下:FP 风格的创造者不是白痴。他们希望副作用是明确的和可控的。所以有一个很好的概念:纯函数


总结

  • OOP 通常与突变一起使用。但这不是必需的。OOP 专注于对象。
  • FP 通常与不变性一起使用。但这不是必需的。FP 专注于功能。
  • 您可以混合使用 OOP、FP、突变和不变性——所有这些。


And*_*kin 5

你混合了三个不同的概念.函数式编程,可变性和OOP是三个不同的东西.

我被告知OOP是关于使用其公共方法改变给定对象的状态.

是的,不是.O OP中最重要的是你有对象,可以同时携带数据代码(它们的成员方法),并且你可以使用它们的界面与对象交谈,这样对象就可以了

  • 调度到被调用方法的具体实现
  • 提供存储在对象携带的数据中的附加信息(this在方法实现中访问)
  • 执行方法实现的代码

没有人规定你这个方法调用做了什么,或者它必须修改一些状态.

碰巧是OOP在与州合作时有助于恢复一些基本的理智.这是因为应用程序的可怕的复杂全局状态可以被切割成更小的部分并隐藏在可变对象中.此外,这些可变对象还可以通过禁止直接访问其状态并仅提供可以修改该状态的一组受限操作来尝试维护至少一些本地不变量.


但现在我听说在函数式编程中,所有对象都应该是不可变的.

他们应该尊重参考透明度.如果您的对象没有可变状态且只有方法没有任何副作用,那么它就足以引用透明度.足够但不必要:对象可以具有更复杂的内部结构,但从外部看起来完全不可变.缓存就是一个很好的例子.

此外,即使是函数式程序也不仅限于使用不可变数据结构.有纯粹的可变状态这样的东西.我们的想法是,您的功能程序用于构建类似于处理可变状态的复杂行动计划.该计划本身是一个不可变的实体.这个计划可能非常复杂,但由于它是由纯函数构建的,因此它仍然可以很容易地推理出来.该计划仅使用纯函数构建,然后可以提供给一个小而简单的解释器,该解释器在可变内存上执行计划.通过这种方式,您可以获得两个世界的好处:在构建计划时,您具有纯函数的概念简单性,但是当您在可变数据结构上执行此计划时,您还具有接近金属计算的性能.


但这种"不变性"是否意味着我无法使用公共方法更改给定对象的内部(属性,私有成员)?

一般来说,是的.但是,在Scala中,默认情况下不会强制执行不变性.您可以决定应用程序的哪些部分足够复杂,因此将自己限制为纯函数可能是值得的.如果这更容易,其他所有东西都可以使用普通的可变结构来实现.

这使对象就像简单的数据容器一样.它提取了它们之外的所有功能.

不,因为对象仍然随身携带虚拟调度表.然后,您可以从外部请求对象调用该方法apply(integer i),并且该对象(取决于它的对象类型)可能会调用完全不同的东西,例如

/** get i-th character */ 
String.apply(integer i) 
Run Code Online (Sandbox Code Playgroud)

要么

/** get value for key `i` */ 
TreeMap.apply(integer i)
Run Code Online (Sandbox Code Playgroud)

如果C没有基本上重新实现子类多态作为设计模式,则不能使用结构.


是否有可能以旧式方式使用对象并仍将其视为函数式编程?

这不是一个全有或全无的游戏.你可以从经典的oop语言开始(具有可变状态和所有这些),它在某种程度上支持函数式编程范例.然后,您可以查看您的应用程序,并隔离它需要更精确控制副作用的那些方面.您必须确定哪些副作用很重要,哪些不太重要.然后你可以使用更纯粹的函数来表达真正关键的部分(纯粹的意义:纯粹的你需要它们,即不执行任何关键的副作用而不在其签名中明确声明它).结果将是在一个应用程序中混合使用经典OOP和FP.

使用纯函数编写程序可以被认为是在某种原始逻辑中构造证明.如果你能在需要的时候做到这一点很好(使用更严格的纯函数方法),但是如果你不需要它就可以省略它(使用通常的方法,带有杂乱的副作用函数) ).