纯函数式编程上下文中的面向对象编程?

Muh*_*uri 42 oop f# haskell functional-programming scala

在函数式编程(FP)上下文中使用面向对象编程(OOP)有什么好处吗?

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

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

在我看来,OOP与使用对象状态的方法主要是为了改变对象有很大的联系.在纯FP环境中,不需要也不需要.

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

我将非常感谢有关该问题的论文或反事实现实世界的例子.

C. *_*ann 54

您看到的断开连接不是FP与OOP.它主要是关于不变性和数学形式主义与可变性和非正式方法的关系.

首先,让我们免除可变性问题:你可以让FP具有可变性和OOP具有不变性就好了.甚至比Haskell更具功能性的是让你可以随心所欲地使用可变数据,你只需要明确什么是可变的以及事情发生的顺序; 除了效率问题之外,几乎任何可变对象都可以构造并返回一个新的"更新"实例,而不是改变它自己的内部状态.

这里更大的问题是数学形式主义,特别是在很少从lambda演算中删除的语言中大量使用代数数据类型.你用Haskell和F#标记了这一点,但是意识到这只是函数式编程领域的一半; 与ML风格的语言相比,Lisp家族拥有一个非常不同,更自由的角色.今天广泛使用的大多数OO系统本质上都是非正式的 - 形式主义确实存在于OO中,但它们并没有明确地以FP形式语言中的FP形式主义的方式被提及.

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


现在,你可能想知道,什么对OOP适当的形式主义?好吧,这是一条妙语:在很多方面,它比ML风格的FP更以功能为中心!我将回顾一下我最喜欢的论文之一,看起来是关键的区别:结构化数据如ML风格的语言中的代数数据类型提供了数据的具体表示和定义操作的能力; 对象提供了对行为的黑盒抽象以及轻松替换组件的能力.

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

我之所以说OOP更以函数为中心,其原因在于函数本身就代表了行为抽象的一种形式.实际上,你可以通过使用包含一堆函数作为对象的记录来模拟Haskell之类的OO样式结构,让记录类型成为各种类型的"接口"或"抽象基类",并具有创建记录的函数替换类构造函数.所以从这个意义上说,OO语言使用高阶函数远远超过Haskell.

对于像这种类型的设计实际上在Haskell中使用得非常好的例子,请阅读graphics-drawingcombinators包的源代码,特别是它使用包含函数的opaque记录类型的方式,并仅根据它们的方式组合事物.行为.


编辑:上面我忘了提到的一些最后的事情.

如果OO确实广泛使用了高阶函数,那么它最初似乎应该非常自然地适用于Haskell等函数式语言.不幸的是,事实并非如此.确实,我描述的对象(参见LtU链接中提到的论文)非常合适.实际上,结果是比大多数OO语言更纯粹的OO样式,因为"私有成员"由用于构造"对象"的闭包隐藏的值表示,并且除了一个特定实例本身之外的任何其他内容都是不可访问的.你没有那么私密!

在Haskell中不能很好地工作的是子类型.而且,虽然我认为继承和子类型在OO语言中经常被滥用,但某种形式的子类型对于能够以灵活的方式组合对象非常有用.Haskell缺乏一种固有的子类型概念,而手工更换则往往非常笨拙.

顺便说一句,大多数具有静态类型系统的OO语言都会对子类型进行完整的散列,因为它具有可替代性,并且不能为方法签名中的方差提供适当的支持.事实上,我认为唯一完整的OO语言没有完全搞砸了,至少我知道的是,Scala(F#似乎对.NET做出了太多让步,尽管至少我不认为它会犯任何新的错误).我对很多这样的语言经验有限,所以我在这里肯定是错的.

在Haskell特定的说明中,它的"类型类"对于OO程序员来说往往很诱人,我说:不要去那里.试图以这种方式实现OOP只会以泪流满面.可以将类型类视为重载函数/运算符的替代,而不是OOP.


Hea*_*ink 8

至于Haskell,类在那里不太有用,因为一些OO特性更容易以其他方式实现.

封装或"数据隐藏"通常通过功能闭包或存在类型而不是私有成员来完成.例如,这是具有封装状态的随机数发生器的数据类型.RNG包含生成值和种子值的方法.因为类型'seed'是封装的,所以你唯一可以做的就是将它传递给方法.

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 = ...

  • 对术语的小狡辩:"参数多态"是指对不透明类型的通用量化,OO语言通常称为泛型,并且是Haskell默认的.基于类型的重载(编译时或运行时)称为"ad-hoc多态",是Haskell类型的用途. (3认同)

Tom*_*cek 6

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

我相信在F#中混合使用这两个概念是一种非常有用的方法 - 您可以将不可变状态与处理该状态的操作相关联.您将获得标识符'点'完成的好功能,易于使用C#中的F#代码等功能,但您仍然可以使您的代码完全正常运行.例如,您可以编写如下内容:

type GameWorld(characters) = 
  let calculateSomething character = 
    // ...
  member x.Tick() = 
    let newCharacters = characters |> Seq.map calculateSomething
    GameWorld(newCharacters)
Run Code Online (Sandbox Code Playgroud)

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

  • 仍然是纯粹的功能(状态是一个字符列表,它没有变异)
  • 它是面向对象的 - 唯一不寻常的是所有方法都返回一个"世界"的新实例

  • 我对此并不那么乐观,所以让我发表一些意见.组织和结构化程序可以通过命名空间或F#中的模块来完成.通过在管道中使用`Seq.map`而不是`fun x - > x.map`来抵消点完成优势.返回新实例的函数是否需要成为类的一部分而不仅仅是模块"命名空间",这是一个风格问题.当然,与C#/ .Net的兼容性是一个很好的理由. (2认同)