纯函数式编程环境中的面向对象编程?

发布于 2024-10-01 10:26:40 字数 745 浏览 1 评论 0原文

在函数式编程 (FP) 环境中使用面向对象编程 (OOP) 有什么优势吗?

我已经使用 F# 一段时间了,我注意到我的函数是无状态的,我越不需要将它们作为对象的方法。特别是,依靠类型推断使它们可以在尽可能广泛的情况下使用是有优势的。

这并不排除对某种形式的命名空间的需要,这与 OOP 正交。也不鼓励使用数据结构。事实上,FP 语言的实际使用在很大程度上取决于数据结构。如果您查看F Sharp 编程/高级数据结构中实现的 F# 堆栈,你会发现它不是面向对象的。

在我看来,OOP 与作用于对象状态的方法密切相关,主要是为了改变对象。在纯粹的函数式编程环境中,这是不需要或不需要的。

一个实际的原因可能是能够与 OOP 代码交互,就像 F# 与 .NET 一起工作一样。 。然而除此之外,还有什么原因吗?在 Haskell 世界中,编程更纯粹的 FP 是什么体验?

我将不胜感激任何有关该问题的论文或反事实的现实世界例子的参考。

Are there any advantages to using object-oriented programming (OOP) in a functional programming (FP) context?

I have been using F# for some time now, and I noticed that the more my functions are stateless, the less I need to have them as methods of objects. In particular, there are advantages to relying on type inference to have them usable in as wide a number of situations as possible.

This does not preclude the need for namespaces of some form, which is orthogonal to being OOP. Nor is the use of data structures discouraged. In fact, real use of FP languages depend heavily on data structures. If you look at the F# stack implemented in F Sharp Programming/Advanced Data Structures, you will find that it is not object-oriented.

In my mind, OOP is heavily associated with having methods that act on the state of the object mostly to mutate the object. In a pure FP context that is not needed nor desired.

A practical reason may be to be able to interact with OOP code, in much the same way F# works with .NET. Other than that however, are there any reasons? And what is the experience in the Haskell world, where programming is more pure FP?

I will appreciate any references to papers or counterfactual real world examples on the issue.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

叫思念不要吵 2024-10-08 10:26:40

您看到的脱节并不是 FP 与 OOP 之间的脱节。它主要是关于不变性和数学形式主义与可变性和非正式方法。

首先,让我们忽略可变性问题:您可以使用具有可变性的 FP 和具有不变性的 OOP。甚至比你更实用的 Haskell 也可以让你随心所欲地使用可变数据,你只需要明确什么是可变的以及事情发生的顺序;抛开效率问题不谈,几乎任何可变对象都可以构造并返回一个新的“更新”实例,而不是更改其自身的内部状态。

这里更大的问题是数学形式主义,特别是在与 lambda 演算几乎没有关系的语言中大量使用代数数据类型。您已经用 Haskell 和 F# 标记了它,但意识到这只是函数式编程领域的一半;与 ML 风格的语言相比,Lisp 家族具有非常不同、更加随心所欲的特征。如今广泛使用的大多数面向对象系统本质上都是非常非正式的——面向对象确实存在形式主义,但它们并没有像 ML 风格语言中的 FP 形式主义那样明确地被调用。

如果消除了形式主义的不匹配,许多明显的冲突就会消失。想要在 Lisp 之上构建一个灵活的、动态的、临时的 OO 系统吗?继续吧,它会工作得很好。想要向 ML 风格的语言添加形式化的、不可变的 OO 系统?没问题,只是不要指望它能很好地与 .NET 或 Java 配合使用。


现在,您可能想知道,什么是适合 OOP 的形式主义?嗯,重点是:在很多方面,它比 ML 风格的 FP 更以功能为中心!我将参考我最喜欢的论文之一来了解似乎是关键区别:结构化数据(例如机器学习风格语言中的代数数据类型)提供了数据的具体表示以及定义数据操作的能力;对象提供了行为的黑盒抽象以及轻松替换组件的能力。

这里有一个比 FP 与 OOP 更深入的二元性:它与一些编程语言理论家所说的密切相关表达式问题:对于具体的数据,您可以轻松添加与其一起使用的新操作,但更改数据的结构则更加困难。使用对象,您可以轻松添加新数据(例如,新子类),但添加新操作很困难(想象一下向具有许多后代的基类添加新的抽象方法)。

我之所以说OOP更以函数为中心,是因为函数本身代表了一种行为抽象形式。事实上,你可以在 Haskell 之类的东西中模拟 OO 风格的结构,方法是使用包含一堆函数的记录作为对象,让记录类型成为某种“接口”或“抽象基类”,并让创建记录的函数替换类构造函数。因此,从这个意义上说,面向对象语言使用高阶函数的频率远远高于 Haskell 等语言。

有关此类设计实际上在 Haskell 中得到很好使用的示例,请阅读 的源代码Graphics-DrawingCombinators 包,特别是它使用包含函数的不透明记录类型并仅根据其行为组合事物的方式。


编辑:最后一些我忘记在上面提到的事情。

如果 OO 确实广泛使用了高阶函数,那么乍一看它似乎应该非常自然地适合函数式语言(例如 Haskell)。不幸的是,情况并非如此。确实,我所描述的对象(参见 LtU 链接中提到的论文)非常适合。事实上,结果是一种比大多数面向对象语言更纯粹的面向对象风格,因为“私有成员”由用于构造“对象”的闭包隐藏的值表示,并且除了一个特定实例本身之外,其他任何东西都无法访问。没有比这更私密的了!

Haskell 中不太有效的是子类型。而且,尽管我认为继承和子类型在 OO 语言中经常被误用,但某种形式的子类型对于能够以灵活的方式组合对象非常有用。 Haskell 缺乏固有的子类型概念,并且手动替换往往非常笨拙。

顺便说一句,大多数具有静态类型系统的面向对象语言也通过对可替换性过于宽松并且不为方法签名中的差异提供适当的支持来实现子类型的完整散列。事实上,我认为至少据我所知,唯一没有完全搞砸的成熟的面向对象语言是 Scala(F# 似乎对 .NET 做出了太多让步,但至少我不认为它会犯任何错误)。不过,我对许多此类语言的经验有限,所以我在这里肯定是错的。

在 Haskell 特定的注释中,它的“类型类”通常对 OO 程序员来说很有吸引力,对此我想说:不要去那里。尝试以这种方式实现 OOP 只会以泪水告终。将类型类视为重载函数/运算符的替代品,而不是 OOP。

The disconnect you see is not of FP vs. OOP. It's mostly about immutability and mathematical formalisms vs. mutability and informal approaches.

First, let's dispense with the mutability issue: you can have FP with mutability and OOP with immutability just fine. Even more-functional-than-thou Haskell lets you play with mutable data all you want, you just have to be explicit about what is mutable and the order in which things happen; and efficiency concerns aside, almost any mutable object could construct and return a new, "updated" instance instead of changing its own internal state.

The bigger issue here is mathematical formalisms, in particular heavy use of algebraic data types in a language little removed from lambda calculus. You've tagged this with Haskell and F#, but realize that's only half of the functional programming universe; the Lisp family has a very different, much more freewheeling character compared to ML-style languages. Most OO systems in wide use today are very informal in nature--formalisms do exist for OO but they're not called out explicitly the way FP formalisms are in ML-style languages.

Many of the apparent conflicts simply disappear if you remove the formalism mismatch. Want to build a flexible, dynamic, ad-hoc OO system on top of a Lisp? Go ahead, it'll work just fine. Want to add a formalized, immutable OO system to an ML-style language? No problem, just don't expect it to play nicely with .NET or Java.


Now, you may be wondering, what is an appropriate formalism for OOP? Well, here's the punch line: In many ways, it's more function-centric than ML-style FP! I'll refer back to one of my favorite papers for what seems to be the key distinction: structured data like algebraic data types in ML-style languages provide a concrete representation of the data and the ability to define operations on it; objects provide a black-box abstraction over behavior and the ability to easily replace components.

There's a duality here that goes deeper than just FP vs. OOP: It's closely related to what some programming language theorists call the Expression Problem: With concrete data, you can easily add new operations that work with it, but changing the data's structure is more difficult. With objects you can easily add new data (e.g., new subclasses) but adding new operations is difficult (think adding a new abstract method to a base class with many descendants).

The reason why I say that OOP is more function-centric is that functions themselves represent a form of behavioral abstraction. In fact, you can simulate OO-style structure in something like Haskell by using records holding a bunch of functions as objects, letting the record type be an "interface" or "abstract base class" of sorts, and having functions that create records replace class constructors. So in that sense, OO languages use higher-order functions far, far more often than, say, Haskell would.

For an example of something like this type of design actually put to very nice use in Haskell, read the source for the graphics-drawingcombinators package, in particular the way that it uses an opaque record type containing functions and combines things only in terms of their behavior.


EDIT: A few final things I forgot to mention above.

If OO indeed makes extensive use of higher-order functions, it might at first seem that it should fit very naturally into a functional language such as Haskell. Unfortunately this isn't quite the case. It is true that objects as I described them (cf. the paper mentioned in the LtU link) fit just fine. in fact, the result is a more pure OO style than most OO languages, because "private members" are represented by values hidden by the closure used to construct the "object" and are inaccessible to anything other than the one specific instance itself. You don't get much more private than that!

What doesn't work very well in Haskell is subtyping. And, although I think inheritance and subtyping are all too often misused in OO languages, some form of subtyping is quite useful for being able to combine objects in flexible ways. Haskell lacks an inherent notion of subtyping, and hand-rolled replacements tend to be exceedingly clumsy to work with.

As an aside, most OO languages with static type systems make a complete hash of subtyping as well by being too lax with substitutability and not providing proper support for variance in method signatures. In fact, I think the only full-blown OO language that hasn't screwed it up completely, at least that I know of, is Scala (F# seemed to make too many concessions to .NET, though at least I don't think it makes any new mistakes). I have limited experience with many such languages, though, so I could definitely be wrong here.

On a Haskell-specific note, its "type classes" often look tempting to OO programmers, to which I say: Don't go there. Trying to implement OOP that way will only end in tears. Think of type classes as a replacement for overloaded functions/operators, not OOP.

陪你到最终 2024-10-08 10:26:40

至于 Haskell,类在那里不太有用,因为一些 OO 功能可以通过其他方式更容易实现。

封装或“数据隐藏”通常是通过函数闭包或存在类型而不是私有成员来完成的。例如,这是一个具有封装状态的随机数生成器的数据类型。 RNG 包含生成值和种子值的方法。因为类型“种子”是封装的,所以您唯一能做的就是将其传递给方法。

data RNG a where RNG :: (seed -> (a, seed)) -> seed -> RNG a

参数多态性或“泛型编程”上下文中的动态方法分派是由类型类(不是 OO 类)提供的。类型类就像 OO 类的虚拟方法表。但是,没有数据隐藏。类型类不像类方法那样“属于”数据类型。

data Coordinate = C Int Int

instance Eq Coordinate where C a b == C d e = a == b && d == e

子类型多态性或“子类化”上下文中的动态方法分派几乎是 Haskell 中使用记录和函数的类模式的翻译。

-- An "abstract base class" with two "virtual methods"
data Object =
  Object
  { draw :: Image -> IO ()
  , translate :: Coord -> Object
  }

-- A "subclass constructor"
circle center radius = Object draw_circle translate_circle
  where
    -- the "subclass methods"
    translate_circle center radius offset = circle (center + offset) radius
    draw_circle center radius image = ...

As for Haskell, classes are less useful there because some OO features are more easily achieved in other ways.

Encapsulation or "data hiding" is frequently done through function closures or existential types, rather than private members. For example, here is a data type of random number generator with encapsulated state. The RNG contains a method to generate values and a seed value. Because the type 'seed' is encapsulated, the only thing you can do with it is pass it to the method.

data RNG a where RNG :: (seed -> (a, seed)) -> seed -> RNG a

Dynamic method dispatch in the context of parametric polymorphism or "generic programming" is provided by type classes (which are not OO classes). A type class is like an OO class's virtual method table. However, there's no data hiding. Type classes do not "belong" to a data type the way that class methods do.

data Coordinate = C Int Int

instance Eq Coordinate where C a b == C d e = a == b && d == e

Dynamic method dispatch in the context of subtyping polymorphism or "subclassing" is almost a translation of the class pattern in Haskell using records and functions.

-- An "abstract base class" with two "virtual methods"
data Object =
  Object
  { draw :: Image -> IO ()
  , translate :: Coord -> Object
  }

-- A "subclass constructor"
circle center radius = Object draw_circle translate_circle
  where
    -- the "subclass methods"
    translate_circle center radius offset = circle (center + offset) radius
    draw_circle center radius image = ...
温柔少女心 2024-10-08 10:26:40

我认为有几种方法可以理解 OOP 的含义。对我来说,这不是关于封装可变状态,而是更多关于组织和构建程序。 OOP 的这个方面可以与 FP 概念完美结合使用。

我相信在 F# 中混合这两个概念是一种非常有用的方法 - 您可以将不可变状态与处理该状态的操作相关联。您将获得标识符“点”补全的出色功能、轻松使用 C# 中的 F# 代码的能力等,但您仍然可以使代码完美运行。例如,您可以编写如下内容:

type GameWorld(characters) = 
  let calculateSomething character = 
    // ...
  member x.Tick() = 
    let newCharacters = characters |> Seq.map calculateSomething
    GameWorld(newCharacters)

一开始,人们通常不会在 F# 中声明类型 - 您可以从编写函数开始,然后改进代码以使用它们(当您更好地理解域并知道什么是函数时)构建代码的最佳方式)。上面的例子:

  • 仍然是纯函数式的(状态是字符列表,并且没有变异)
  • 它是面向对象的 - 唯一不寻常的是所有方法都返回“世界”的新实例

I think that there are several ways of understanding what OOP means. For me, it is not about encapsulating mutable state, but more about organizing and structuring programs. This aspect of OOP can be used perfectly fine in conjunction with FP concepts.

I believe that mixing the two concepts in F# is a very useful approach - you can associate immutable state with operations working on that state. You'll get the nice features of 'dot' completion for identifiers, the ability to easy use F# code from C#, etc., but you can still make your code perfectly functional. For example, you can write something like:

type GameWorld(characters) = 
  let calculateSomething character = 
    // ...
  member x.Tick() = 
    let newCharacters = characters |> Seq.map calculateSomething
    GameWorld(newCharacters)

In the beginning, people don't usually declare types in F# - you can start just by writing functions and later evolve your code to use them (when you better understand the domain and know what is the best way to structure code). The above example:

  • Is still purely functional (the state is a list of characters and it is not mutated)
  • It is object-oriented - the only unusual thing is that all methods return a new instance of "the world"
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文