函数式编程支持者将如何在Code Complete中回答此声明?


30

在第二版的839页上,史蒂夫·麦康奈尔(Steve McConnell)讨论了程序员在大型程序中“克服复杂性”的所有方式。他的提示最终体现在:

“面向对象的编程提供了同时适用于算法和数据的抽象级别,这是仅功能分解无法提供的那种抽象。”

加上他的结论“降低复杂度可以说是成为有效程序员的最重要的关键”(同一页),这似乎对函数式编程构成了很大的挑战。

FP和OO之间的辩论通常由FP支持者围绕复杂性问题进行,这些复杂性问题特别来自于并发或并行化的挑战。但是并发当然不是程序员必须克服的唯一复杂性。也许专注于减少一种复杂性会在其他方面大大增加它的复杂度,以至于在许多情况下,增加收益是不值得的。

如果我们将FP和OO之间的比较术语从并发性或可重用性等特定问题转移到全局复杂性的管理上,那场辩论会如何?

编辑

我想强调的对比是,OO似乎封装和抽象了数据和算法的复杂性,而函数式编程似乎鼓励在整个程序中使数据结构的实现细节更加“暴露”。

例如,参见Stuart Halloway(Clojure FP的支持者)在这里说,“数据类型的过度规范”是“惯用的OO风格的负面结果”,并主张将AddressBook概念化为简单的矢量或映射而不是更丰富的OO对象。以及其他(非向量化和非地图类)属性和方法。(此外,OO和领域驱动设计的支持者可能会说,将地址簿公开为向量或映射会使封装的数据过度暴露于从域的角度来看无关紧要甚至危险的方法)。


3
+1尽管这个问题已经被定为对立的框架,但这是一个很好的问题。
mattnz

16
正如许多人在回答中所说,函数分解和函数编程是两种不同的野兽。因此得出结论“这似乎对函数式编程几乎构成了挑战”,这显然是错误的,与它无关。
法比奥·弗拉卡西

5
显然,麦康奈尔在现代功能数据类型系统和高阶一流模块中的知识有些不完整。他的声明完全是胡说八道,因为我们已经有了一流的模块和函子(请参阅SML),类型类(请参见Haskell)。这只是OO思维方式如何更像是一种宗教而不是一种尊重的设计方法论的另一个例子。而且,顺便问一下,您从哪里获得了有关并发性的信息?大多数函数式程序员根本不关心并行性。
SK-logic

6
@ SK-logic所有的麦康奈尔都说,“仅功能分解”不能提供与OOP相同的抽象方法,这对我来说似乎是一个非常安全的声明。他无处说FP语言没有像OOP一样强大的抽象方法。实际上,他根本没有提到FP语言。那只是OP的解释。
sepp2k 2012年

2
@ sepp2k,好的,我知道了。但是,仍然可以在几乎完全纯的lambda演算的功能分解之上,通过模拟模块的行为,构建一个非常复杂且分层良好的数据结构和处理抽象系统。完全不需要OO抽象。
SK-logic

Answers:


13

请记住,这本书写了20多年了。对于当今的专业程序员而言,FP不存在-它完全在学者和研究人员的范围内。

我们需要在工作的适当范围内构架“功能分解”。作者不是指功能编程。我们需要将其绑定到“结构化编程”以及GOTO它之前的混乱局面。如果您的参考点是没有功能的旧版FORTRAN / COBOL / BASIC(也许,如果您很幸运,您将获得一个单一的GOSUB级别),并且所有变量都是全局变量,则可以分解程序分成几层功能是一大福音。

OOP是对这种“功能分解”的进一步改进。您不仅可以将指令捆绑在功能中,还可以将相关功能与它们正在处理的数据进行分组。结果是一段清晰定义的代码,您可以查看(理解)(理想情况下),而不必全神贯注于代码库中以查找其他可能对数据起作用的代码。


27

我想象函数式编程的支持者会争辩说,大多数FP语言比“单独的函数分解”提供了更多的抽象方法,并且实际上确实允许抽象方法在功能上与面向对象语言相提并论。例如,可以引用Haskell的类型类或ML的高阶模块作为这种抽象方法。因此,该声明(我很确定是关于面向对象而不是过程编程,而不是函数式编程)不适用于它们。

还应该指出的是,FP和OOP是正交的概念,并不相互排斥。因此,将它们相互比较是没有意义的。您可以很好地将“命令式OOP”(例如Java)与“功能性OOP”(例如Scala)进行比较,但是您引用的语句不适用于该比较。


10
+1“功能分解”!=“功能编程”。第一种方法依赖于经典的顺序编码,使用的是原始数据结构,没有(或只有手工滚动)继承,封装和多态性。第二种表达方法使用lambda演算。两件事完全不同。
Binary Worrier 2012年

4
抱歉,但是“过程编程”一词固执地拒绝更早地想到。对我来说,“功能分解”比功能编程更能说明程序编程。
Binary Worrier 2012年

你是对的。我确实假设函数式编程偏向于一遍又一遍地在相同的简单数据结构(列表,树,映射)上操作可重用函数,并且实际上声称这是面向对象的卖点。请参阅Stuart Halloway(Clojure FP的支持者)在这里说“数据类型的过度规范”是“惯用的OO风格的负面结果”,并且倾向于将AddressBook概念化为向量或映射,而不是将其他更丰富的OO对象(非-vectorish和非maplike)属性和方法。
2012年

的链接,司徒哈洛韦报价:thinkrelevance.com/blog/2009/08/12/...

2
@dan可能是在动态类型的语言Clojure中完成的(我不知道,我没有使用Clojure),但是我认为从中得出结论很危险,那就是通常在FP中是这样做的。例如,Haskell人员在抽象类型和信息隐藏方面似乎非常重要(虽然可能不及Java人员那么多)。
sepp2k 2012年

7

我发现函数式编程在管理复杂性方面非常有帮助。但是,您倾向于以不同的方式来考虑复杂性,将其定义为对不同级别的不可变数据起作用的函数,而不是从OOP的角度进行封装。

例如,我最近用Clojure编写了一个游戏,并且游戏的整个状态都在一个不变的数据结构中定义:

(def starting-game-state {:map ....
                          :player ....
                          :weather ....
                          :other-stuff ....}

游戏的主循环可以定义为在循环中将一些纯函数应用于游戏状态:

 (loop [initial-state starting-game-state]
   (let [user-input (get-user-input)
         game-state (update-game initial-state user-input)]
     (draw-screen game-state)
     (if-not (game-ended? game-state) (recur game-state))))

称为的关键函数是update-game,该函数在给定先前的游戏状态和某些用户输入的情况下运行模拟步骤,并返回新的游戏状态。

那么复杂性在哪里呢?在我看来,它已经得到很好的管理:

  • 当然,更新游戏功能可以完成很多工作,但是它本身是由其他功能组成的,因此它实际上非常简单。一旦下降了几级,功能仍然非常简单,可以执行“将对象添加到地图图块”之类的操作。
  • 当然,游戏状态是一个大数据结构。但同样,它只是通过构成较低级别的数据结构而建立的。同样,它是“纯数据”,而不是嵌入任何方法或需要类定义(如果愿意,您可以将其视为非常有效的不可变JSON对象),因此几乎没有样板。

OOP还可以通过封装来管理复杂性,但是如果将其与OOP进行比较,则该功能具有一些非常大的优点:

  • 游戏状态数据结构是不可变的,因此可以轻松地并行执行许多处理。例如,在与游戏逻辑不同的线程中进行渲染调用绘图屏幕是绝对安全的-它们可能不会相互影响或看到不一致的状态。对于大型可变对象图,这是令人惊讶的困难……
  • 您可以随时拍摄游戏状态的快照。重播是微不足道的(感谢Clojure的持久数据结构,由于大多数数据是共享的,因此副本几乎不占用任何内存)。例如,您还可以运行更新游戏来“预测未来”,以帮助AI评估不同的动作。
  • 我无需在任何地方进行任何艰难的权衡以适应OOP范式,例如定义严格的类继承关系。从这个意义上说,功能数据结构的行为更像是一个基于原型的灵活系统。

最后,对于那些对如何管理功能性语言和OOP语言的复杂性有更多见解的人,我强烈推荐Rich Hickey的主题演讲Simple Made Easy的视频(在Strange Loop技术会议上拍摄)


2
我认为游戏是证明强制不变性的所谓“好处”的最糟糕的例子之一。游戏中的事物不断变化,这意味着您必须一直在重建游戏状态。而且,如果一切都是不可变的,则意味着您不仅必须重建游戏状态,还必须重建图形中包含对该状态的引用或保留对该状态的引用的所有内容,然后递归进行,直到将整个状态回收为止。以超过30 FPS的速度运行程序,启动时会有大量的GC流失!您不可能从中获得良好的表现……
Mason Wheeler 2012年

7
当然,游戏具有不变性是很难的-这就是为什么我选择它来证明它仍然可以工作的原因!但是,您会惊奇地发现持久性数据结构可以做什么-大部分游戏状态都不需要重建,只需重新构建即可。并且肯定会有一些开销,但这只是一个很小的常数。给我足够的核心,我将击败您的单线程C ++游戏引擎..
mikera 2012年

3
@Mason惠勒:其实,这有可能得到几乎相同的(好与基因突变)与不可变对象的性能,而无需在所有多GC。Clojure的诀窍是使用持久性数据结构:它们对于程序员是不可变的,但实际上在幕后是可变的。两全其美。
乔纳斯

4
@quant_dev以上核优于核...便宜escapistmagazine.com/news/view/...
deworde

6
@quant_dev-这不是一个借口,这是一个数学和体系​​结构的事实,如果可以通过随内核数近似线性地扩展性能来弥补它,那么最好产生恒定的开销。函数式语言最终将提供卓越性能的原因是,我们已经达到了单核性能的极限,并且将来都将涉及并发性和并行性。功能方法(尤其是不变性)在完成这项工作中很重要。
mikera 2012年

3

“面向对象的程序设计提供了同时适用于算法和数据的抽象层次,这是仅功能分解无法提供的一种抽象。”

仅仅功能分解还不足以构成任何类型的算法或程序:您还需要表示数据。我认为上面的陈述隐含地假定(或至少可以这样理解)功能情况下的“数据”是最基本的类型:仅是符号列表,而没有其他内容。用这种语言编程显然不是很方便。但是,许多语言,尤其是新近现代的功能(或多范式)语言(例如Clojure)提供了丰富的数据结构:不仅是列表,而且还包括字符串,向量,映射和集合,记录,结构以及对象!-具有元数据和多态性。

OO抽象的巨大实际成功是无可争议的。但这是硬道理吗?如您所写,并发问题已经是主要的难题,而经典的OO根本不包含并发的概念。结果,用于处理并发的事实上的OO解决方案只是叠加了胶带:可以工作,但是很容易搞砸,需要大量的脑部资源来处理当前的基本任务,而且伸缩性不好。也许有可能充分利用世界上最好的东西。这就是现代多范式语言所追求的。


1
我曾在某处听到过“大的OO,小的FP”一词-我想迈克尔·费瑟斯(Michael Feathers)引用了它。这意味着FP可能对大程序的特定部分有用,但总的来说应该是OO。
2012年

另外,除了使用Clojure代替所有内容,甚至不使用更传统的OO语法更清楚地表达事物,也不打算将Clojure用于更干净的数据处理位,而将Java或其他OO语言用于其他位。对于程序的所有部分,使用多语言编程而不是使用相同语言的多范式编程。(类似于大多数Web应用程序如何在不同的层上使用SQL和OO。)
2012年

@dan:使用最合适的工具。在多语言编程中,至关重要的因素是语言之间的便捷通信,而Clojure和Java很难一起发挥作用。我相信大多数实质性的Clojure程序至少在这里和那里都使用JDK的标准Java库的某些位。
乔纳斯

2

可变状态是与编程和软件/系统设计有关的大多数复杂性和问题的根源。

OO包含可变状态。FP讨厌可变状态。

OO和FP都有其用途和优点。做出明智的选择。记住这句格言:“封闭是穷人的物件。物件是穷人的封闭物。”


3
我不确定您的开场白是正确的。“最”复杂性的根源是什么?在我完成或看到的编程中,问题不是易变的状态,而是缺乏抽象和代码中的细节过多。
2012年

1
@丹:有趣。实际上,我已经看到了许多相反的情况:问题是由于过度使用抽象引起的,这使得既难以理解又难以在必要时确定实际发生的细节。
梅森惠勒2012年

1

函数式编程可以具有对象,但是那些对象往往是不可变的。然后,纯函数(没有副作用的函数)将在这些数据结构上运行。可以使用面向对象的编程语言来制作不可变的对象,但是它们并不是为了做到这一点而设计的,这并不是它们倾向于被使用的方式。这使得很难对面向对象的程序进行推理。

让我们举一个非常简单的例子。假设Oracle决定Java Strings应该具有反向方法,并且您编写了以下代码。

String x = "abc";
StringBuffer y = new StringBuffer(x);
y.reverse();
x.reverse();
x.toString().equals(y.toString());

最后一行的计算结果是什么?您需要有关String类的特殊知识,才能知道这将得出false。

如果我自己上课WuHoString怎么办

String x = "abc";
WuHoString y = new WuHoString(x);
y.reverse();
x.reverse();
x.toString().equals(y.toString())

不可能知道最后一行的计算结果。

在函数式编程风格中,它的编写方式如下:

String x;
equals(toString(reverse(x)), toString(reverse(WuHoString(x))))

这应该是真的。

如果最基本的类之一中的1个函数很难如此推理,那么一个人想知道引入这种可变对象的想法是否会增加或减少复杂性。

显然,对于面向对象的构成,功能的含义以及两者的含义都有各种各样的定义。对我来说,您可以使用没有一流函数之类的语言的“函数式编程风格”,但可以使用其他语言。


3
有趣的是,您说不是不是为不可变对象构建的OO语言,然后在后面使用带字符串的示例(在包括Java在内的大多数OO语言中都是不可变的)。我还要指出,有一些面向对象(OO)(或更确切地说是多范式)语言在设计时强调不可变对象(例如,Scala)。
sepp2k 2012年

@ sepp2k:习惯它。FP倡导者总是四处乱写,与真实世界的编码无关的人为的例子。这是使核心FP概念(如强制不变性)看起来不错的唯一方法。
梅森·惠勒

1
@梅森:嗯?使不变性看起来最好的最佳方法不是说“ Java(和C#,python等)使用不变的字符串,并且效果很好”吗?
sepp2k 2012年

1
@ sepp2k:如果不可变的字符串工作得如此好,为什么StringBuilder / StringBuffer样式类不断出现呢?这只是妨碍您进行抽象反转的另一个示例。
梅森惠勒2012年

2
许多面向对象的语言允许您制作不可变的对象。但是,从我的角度来看,将方法绑定到类的概念确实不鼓励使用它。字符串示例并不是一个人为的示例。每当我在Java中调用任何方法时,我都会把握机会确定我的参数是否会在该函数中发生突变。
WuHoUnited 2012年

0

我认为在大多数情况下,经典的OOP抽象无法涵盖并发复杂性。因此,OOP(按其原始含义)并不排除FP,这就是为什么我们会看到scala之类的原因。


0

答案取决于语言。例如,Lips确实非常清楚代码数据,而您编写的算法实际上只是Lisp列表!您以与编写程序相同的方式存储数据。同时,这种抽象比OOP更简单,更透彻,可让您做一些真正整洁的事情(检出宏)。

Haskell(我想和类似的语言)有一个完全不同的答案:代数数据类型。代数数据类型就像C结构,但有更多选择。这些数据类型提供了对数据建模所需的抽象。函数提供对算法建模所需的抽象。类型类和其他高级功能提供了更高的抽象级别。

例如,我正在尝试一种名为TPL的编程语言。代数数据类型使表示值非常容易:

data TPLValue = Null
              | Number Integer
              | String String
              | List [TPLValue]
              | Function [TPLValue] TPLValue
              -- There's more in the real code...

以一种非常直观的方式,这就是说TPLValue(我的语言中的任何值)可以是一个Null或一个Number具有Integer值的a 或a ,甚至可以是具有值Function列表(参数)和最终值(主体)的a )。

接下来,我可以使用类型类对一些常见行为进行编码。例如,我可以创建TPLValue实例,Show这意味着可以将其转换为字符串。

另外,当我需要指定某些类型的行为(包括我自己未实现的行为)时,可以使用自己的类型类。例如,我有一个Extractable类型类,可让我编写一个函数,该函数采用a TPLValue并返回适当的普通值。因此,只要和是的实例,extract就可以将a转换Number为an Integer或a 转换String为a 。StringIntegerStringExtractable

最后,我程序的主要逻辑在于几个函数,例如evalapply。这些实际上是核心-它们取TPLValues并将其转换为more TPLValue,以及处理状态和错误。

总的来说,我在Haskell代码中使用的抽象实际上比我在OOP语言中使用的抽象强大。


是的,一定要爱eval。“嘿,看我!我不需要写自己的安全漏洞;我在编程语言中内置了一个任意代码执行漏洞!” 将数据与代码融合是有史以来两种最流行的安全漏洞类别之一。每当您看到有人由于SQL注入攻击(在许多其他事情中)而被黑客入侵时,都是因为那里的某些程序员不知道如何正确地将数据与代码分离。
梅森惠勒2012年

eval并不太依赖Lisp的结构-您可以使用evalJavaScript和Python等语言。真正的威力在于编写宏,它们基本上是作用于数据等程序并输出其他程序的程序。这使该语言非常灵活,并且易于创建强大的抽象。
蒂洪

3
是的,我之前听过很多次“宏伟的设计”。但是我从未见过Lisp宏的实际示例执行1)实用的操作,而您实际上需要在实际代码中执行的操作,以及2)无法在支持功能的任何语言中轻松实现。
梅森惠勒2012年

1
@MasonWheeler短路and。短路orletlet-recconddefn。这些都不能使用应用顺序语言中的功能来实现。 for(列表理解)。 dotimesdoto

1
@MattFenwick:好的,我确实应该在上述两个方面添加第三个要点:3)尚无任何明智的编程语言内置。 因为那是我所见过的唯一真正有用的宏示例,并且当您说“嘿,看着我,我的语言非常灵活,我可以实现自己的短路功能and!” 我听到,“嘿,看着我,我的语言太残废了,甚至连短路都没有and,我必须为所有事情重新发明轮子!”
梅森惠勒

0

据我所知,引用的句子已不再有效。

当代的OO语言不能抽象类型不是*的类型,即更高种类的类型是未知的。它们的类型系统不允许表达“带有Int元素的容器,该容器允许在元素上映射函数”。

因此,像Haskells这样的基本功能

fmap :: Functor f => (a -> b) -> f a -> f b 

例如,至少不能以类型安全的方式编写)。因此,要获得基本功能,您必须编写大量样板,因为您需要

  1. 一种将简单函数应用于列表元素的方法
  2. 一种将相同简单函数应用于数组元素的方法
  3. 一种将相同的简单函数应用于哈希值的方法,
  4. ....集
  5. ....树
  6. ... 10.相同的单元测试

但是,这五个方法基本上是相同的代码,可以使用或可以使用某些方法。相反,在Haskell中,我需要:

  1. 列表,数组,映射,集合和树的Functor实例(通常是预定义的,或者可以由编译器自动派生)
  2. 简单功能

请注意,Java 8并不会改变这种情况(只是可以更轻松地应用函数,但是,恰恰会出现上述问题。只要您没有更高阶的函数,您很可能甚至不会能够了解更高种类的类型的优点。)

即使像锡兰这样的新OO语言也没有更高种类的类型。(我最近问过加文·金,他告诉我,这在当时并不重要。)不过,我不了解科特林。

*)公平地说,您可以拥有一个接口Functor,该接口具有方法fmap。不好的事情是,您不能说:嘿,我知道如何为库类SuperConcurrentBlockedDoublyLinkedDequeHasMap,亲爱的编译器实现fmap,请接受从现在开始,所有SuperConcurrentBlockedDoublyLinkedDequeHasMaps都是Functors。


FTR:锡兰typechecker和JavaScript现在后台支撑更高手类型(以及更高级别的类型)。它被认为是“实验性”功能。但是,我们的社区一直在努力寻找该功能的实际应用程序,因此这是否将成为该语言的“官方”部分是一个悬而未决的问题。我确实希望Java后端在某个阶段能够支持它。
加文·金

-2

曾经在dBase中进行过编程的任何人都会知道单行宏对于制作可重用的代码有多么有用。尽管我还没有用Lisp编程,但是我从许多宣读编译时间宏的人那里读到了很多东西。在每个C程序中使用“ include”指令以一种简单的形式使用在编译时将代码注入代码的想法。由于Lisp可以使用Lisp程序执行此操作,并且由于Lisp具有高度的反射性,因此您可以获得更加灵活的包含。

只会从网络上获取任意文本字符串并将其传递到其数据库的任何程序员都不是程序员。同样,任何允许“用户”数据自动成为可执行代码的人都是愚蠢的。这并不意味着允许程序在执行时操纵数据,然后将其作为代码执行是一个坏主意。我相信,这种技术在将来必不可少,因为它将实际上编写大多数程序的“智能”代码。整个“数据/代码问题”与否取决于语言的安全性。

大多数语言的问题之一是它们是由一个离线人员自己执行的。现实世界中的程序要求许多人始终可以同时访问多个Core和多个计算机集群。安全性应该是语言的一部分,而不是操作系统的一部分,并且在不久的将来它将成为安全性。


2
欢迎来到程序员。请考虑淡化答案中的措辞,并通过外部参考来支持您的某些主张。

1
任何允许用户数据自动成为可执行代码的程序员都是无知的。不傻 这样做通常很容易而且很明显,如果他们不知道为什么这不是一个好主意,并且存在更好的解决方案,那么您就不能真正责怪他们这样做。(在被告知有更好的方法之后,一直这样做的人显然是愚蠢的。)
梅森惠勒
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.