2010-11-06 38 views
41

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

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

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

在我看来,OOP与对物体状态作用的方法大多与变异对象有很大关联。在不需要或不需要的纯FP计划中。

一个实际的原因可能是能够与OOP代码交互,就像F#与.NET一样。除此之外,是否有任何原因? Haskell世界的经验是什么,编程更纯粹的FP?

我会很感激任何关于这个问题的论文或反事实现实例子。

回答

52

您看到的断开连接不是FP与OOP。它主要是关于不变性和数学形式主义与可变性和非正式方法。首先,让我们放弃可变性问题:你可以让FP具有可变性,而OOP具有不变性就好。即使功能更强大,Haskell也可以让你用所有你想要的可变数据来玩,你只需要明确什么是可变的,以及事件发生的顺序;和效率问题放在一边,几乎任何可变对象都可以构造并返回一个新的“更新”实例,而不是改变它自己的内部状态。

这里的一个更大的问题是数学形式,特别是在很少使用lambda微积分的语言中大量使用代数数据类型。你已经用Haskell和F#标记了这一点,但意识到这只是函数式编程领域的一半;与ML风格的语言相比,Lisp家族有一个非常不同的,更自由的角色。目前广泛使用的大多数面向对象系统本质上都是非正式的 - 形式化确实存在于面向对象,但它们没有明确地以FP形式主义在ML风格语言中的方式被调用。

如果您删除了形式主义不匹配,许多明显的冲突就会消失。想要在Lisp之上构建一个灵活的,动态的,特殊的OO系统?继续,它会工作得很好。想要将正式的,不可变的OO系统添加到ML风格的语言中?没问题,只是不要指望它能很好地与.NET或Java搭配。


现在,你可能想知道,什么是 OOP适当的形式主义?好吧,这里有一句妙语:在很多方面,它比ML风格的FP更加以功能为中心!我将回头看one of my favorite papers看起来关键的区别:像ML风格语言中的代数数据类型这样的结构化数据提供了数据的具体表示以及定义数据的能力;对象提供了超过行为的黑盒抽象以及轻松替换组件的功能。

这里有一个比FP和OOP更深层次的二元性:它与一些编程语言理论家称之为the Expression Problem的东西密切相关:使用具体的数据,您可以轻松地添加与它一起工作的新操作,但更改数据的结构比较困难。使用对象可以轻松添加新数据(例如,新的子类),但添加新操作很困难(想想为具有许多后代的基类添加新的抽象方法)。

我说OOP更加以功能为中心的原因是功能本身代表了一种行为抽象的形式。事实上,你可以通过使用记录来持有一堆函数作为对象来模拟OO风格的结构,让记录类型成为一种“接口”或“抽象基类”,并且具有创建记录替换的功能类构造函数。所以从这个意义上来说,面向对象的语言使用的是高阶函数,这比Haskell所说的要多得多。

对于这种类型的设计在Haskell中实际使用非常好的例子,请阅读the graphics-drawingcombinators package的源代码,特别是它使用包含函数的不透明记录类型的方式,行为。


编辑:最后几点东西,我忘了提及以上。

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

什么在Haskell中不能很好地工作是子类型。而且,虽然我认为继承和子类型在OO语言中经常被滥用,但是某些形式的子类型对于能够以灵活的方式组合对象非常有用。 Haskell缺乏子类型的固有概念,而手卷替换往往非常笨拙。另外,大多数带有静态类型系统的面向对象语言通过对可替代性过于宽松以及不能为方法签名中的方差提供适当的支持来完成子类型的完整散列。事实上,我认为唯一完全没有搞砸的OO语言,至少我知道的是Scala(F#似乎让.NET有太多让步,尽管至少我不认为它使任何新的错误)。尽管如此,我对许多此类语言的使用经验有限,所以我在这里肯定会出错。

在一个Haskell特有的注释中,它的“类型”对OO程序员来说通常很诱人,我说:不要去那里。试图以这种方式实施OOP只会以泪结束。将类型类看作是重载函数/运算符的替代,而不是OOP。

+4

+1,好答案! – missingfaktor 2010-11-06 18:44:47

+0

感谢您给出了令人印象深刻的答案(和链接)。阅读材料后,我可能会跟进问题。 – 2010-11-07 18:25:09

6

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

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

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

在开始的时候,人们通常不会宣布在F#类型 - 可以通过编写函数刚开始,后来发展你的代码使用它们(当你更好地理解该领域并知道什么是构建代码的最佳方式)。上面的例子:

  • 依然是纯功能(状态为字符的列表,它不被突变)
  • 它是面向对象的 - 唯一不同寻常的是,所有的方法返回一个新的实例“世界”
+2

我对此不太积极,所以让我发表一些意见。组织和构建程序可以通过名称空间或F#中的模块完成。点补全优势可以通过在管道中使用“Seq.map”而不是'fun x - > x.map'来抵消。这是一个风格问题,返回新实例的函数是否需要成为类的一部分,而不仅仅是模块“名称空间”。当然,与C#/ .Net的兼容性是一个很好的理由。 – 2010-11-06 14:33:05

8

对于Haskell来说,类没那么有用,因为有些面向对象的功能更容易以其他方式实现。

封装或“数据隐藏”通常通过函数关闭或存在类型来完成,而不是私有成员。例如,这里是一个具有封装状态的随机数生成器的数据类型。 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 = ... 
+0

我不知道Haskell,那么你能解释一下动态方法调度和基于标准类的OOP之间的区别吗? – 2010-11-06 15:05:12

+0

“动态方法调度”我的意思是选择在运行时调用正确方法的策略。在这里展示的两种策略中,子类型策略就像虚拟方法在OOP中使用虚拟方法表一样。参数多态策略不是面向对象的,在Haskell中更常用。 – Heatsink 2010-11-06 15:37:19

+3

关于术语的小问题:“参数多态性”意味着对不透明类型进行通用量化,OO语言通常称为泛型,而Haskell默认具有这种功能。基于类型的重载,无论是编译时还是运行时,都称为“ad-hoc多态性”,并且是Haskell类型类的用途。 – 2010-11-06 19:25:44