函数式编程会增加问题和解决方案之间的“代表性差距”吗?[关闭]


18

由于机器语言(例如0110101000110101)计算机语言通常已经演化为更高形式的抽象,因此通常将其应用于问题时更易于理解代码。汇编器是对机器代码的抽象,C是对汇编器的抽象,等等。

面向对象的设计似乎非常擅长于使我们能够根据对象对问题进行建模,例如,可以使用Course类,Student类等对大学课程注册系统的问题进行建模。然后,当我们编写解决方案时在OO语言中,我们有类似的类来承担责任,并且通常对设计特别是模块化代码有帮助。如果我将这个问题交给使用OO方法解决此问题的10个独立团队,通常这10个解决方案将具有与该问题相关的类。当您开始接触这些类的耦合和相互作用时,可能会有很多差异,因此不存在“零代表间隙”之类的问题。

我对函数式编程的经验非常有限(没有实际使用,只有Hello World类型的程序)。我没有看到这样的语言如何像OO语言那样轻松地将FP解决方案映射到问题(具有较小的表示差距)。

我了解FP在并发编程方面的优势。但是我是否缺少某些东西,或者FP是否不是要缩小代表差距(使解决方案更容易理解)?

提出另一种方式是:10个不同团队解决同一现实问题的FP代码有很多共同点吗?


摘自维基百科上的抽象(计算机科学)(重点是我的):

函数式编程语言通常会展示与函数相关的抽象,例如lambda抽象(将术语变成某些变量的函数),高阶函数(参数是函数),括号抽象(将术语变成变量的函数)。

由于[某些]现实世界中的问题很难用这种抽象模型来建模,因此代表性的差距可能会增加。


我看到减小代表性差距的另一种方法是将解决方案元素追溯到问题所在。在0年代和1S IN的机器代码是很难追溯,而Student类是容易追溯。并非所有的OO类都可以轻松地跟踪到问题空间,但是许多类都可以。

FP抽象不是总是需要解释以找出他们要解决的问题空间的哪一部分(除了数学问题)?好的-这方面我很好。在查看了更多示例之后,我将看到FP抽象对于在数据处理中表达的问题的一部分非常清晰。


对相关问题的可接受答案UML可以用于对功能程序进行建模吗?-说“功能程序员在图表中没有太多用处。” 我真的不在乎它是否是UML,但是这使我想知道,如果没有被广泛使用的图,那么FP抽象是否易于理解/交流(假设这个答案是正确的)。同样,我对FP的使用/理解水平是微不足道的,因此我了解不需要简单FP程序的图表。

OO设计具有抽象的功能/类/包级别,每个级别都有封装(访问控制,信息隐藏),这使得管理复杂性变得更加容易。这些是使问题从解决方案回到解决方案的要素。


许多答案都谈到了如何以类似于OO的方式在FP中完成分析和设计,但是到目前为止,没有人引用任何高级信息(保罗引用了一些有趣的东西,但它是低级的)。昨天我做了很多谷歌搜索,发现了一些有趣的讨论。以下摘自Simon Thompson(2004)的“重构功能程序”(重点是我的)

在设计面向对象的系统时,理所当然的是,设计将优先于编程。设计将使用诸如Eclipse之类的工具支持的UML之类的系统编写。入门程序员可能会使用BlueJ之类的系统很好地学习视觉设计方法。FAD:Functional Analysis and Design中报告了类似的函数编程方法的工作,但几乎没有其他工作。可能有许多原因。

  • 现有的功能程序具有不需要设计的规模。许多功能程序都很小,但其他功能程序(例如格拉斯哥Haskell编译器)则很实用。

  • 功能程序直接为应用程序领域建模,因此使设计无关紧要。虽然功能语言提供了各种强大的抽象,但是很难说这些提供了对现实世界建模所需的全部和唯一的抽象。

  • 功能程序是作为一系列不断发展的原型而构建的。

以上引用博士学位论文中,概述了使用分析和设计方法(ADM)的好处,而与范式无关。但是有人提出ADM应该与实现范例保持一致。也就是说,OOADM最适合用于OO编程,并且不能很好地应用于诸如FP之类的另一范式。我认为这是一个很好的报价,这就是我所说的代表性差距:

人们可以就哪种范式为软件开发提供最好的支持进行详尽的争论,但是当一个范式停留在从问题描述到实施和交付的单一范式中时,就可以实现最自然,有效和有效的开发包。

以下是FAD提出的一组图表:

  • 功能依赖关系图,提供功能及其在实现中使用的功能;
  • 类型依赖图,为类型提供相同的服务;和,
  • 模块依赖关系图,显示系统模块结构的视图。

FAD论文的第5.1节中有一个案例研究,该系统可以自动生成与足球(足球)联赛有关的数据。这些要求具有100%的功能,例如输入足球结果,产生联赛表,得分表,出勤表,在团队之间转移球员,在新结果后更新数据等。没有记录FAD如何解决非功能性要求除了声明“应该以最低的成本允许使用新功能”之外,几乎无法进行测试。

遗憾的是,除了FAD之外,我没有看到任何针对FP提出的现代建模语言(可视化)参考。UML是另一个范例,因此我们应该忘记这一点。


1
评论不作进一步讨论;此对话已转移至聊天
maple_shaft

Answers:


20

在几乎所有范例中,基本数据的结构都相同。你将有一个Student,一个Course,等它是否是一个对象,一个结构,一个记录,或什么的。OOP的区别不在于数据的结构方式,而在于功能的结构方式。

实际上,我发现功能程序与我对问题的看法更加接近。例如,要计划下学期的学生时间表,您需要考虑以下内容:学生已完成的课程清单,学生学位课程中的课程,本学期提供的课程,学生已完成前提条件的课程,时间不符合要求的课程不冲突等

突然之间,不清楚哪个类应该创建并存储所有这些列表。当您具有这些列表的复杂组合时,情况更是如此。但是,您必须选择一个班级。

在FP中,您编写的函数可以接收学生和课程列表,然后返回经过过滤的课程列表。您可以将所有这些功能组合在一个模块中。您不必将其与一个或另一个班级相结合。

因此,您的数据模型最终看起来更像OOP程序员如何思考他们的模型,然后再被没有其他目的的类所污染,而不仅仅是提供方便的位置来放置可对其他类的组合进行操作的函数。CourseStudentFilterList您不会在OOP中最终遇到任何奇怪或类似的类,但在开始设计时就不要考虑了。


5
@BenAaronson我所遇到的问题是,与学生相关的所有功能都将添加到“学生”类中,并且其职责在不断增长,直到您需要引入StudentCourseFilterer以使事情易于管理
为止

3
@Den混合方法有其优点。但是,这种区分是人为的不是真的。为了适应OOP和FP,Scala必须做出很多妥协(类型推断不那么强大。另一种是复杂性:是的,混合了OOP和FP的语言往往更复杂 -如“难于使用并理解”-而不是两个极端的纯正兄弟之一)。
Andres F.

3
@Den另请参阅Erik Meijer的““大多数功能”编程无效”,该程序反对使用混合方法,并建议使用完整的“原教旨主义者” FP。
Andres F.

4
@Den恐怕您描述的受欢迎程度是历史的偶然。我在工作中使用Scala,由于它尝试混合FP和OOP的方式,因此它是一个复杂的野兽。切换到像Haskell这样的真正的FP语言就像是呼吸新鲜空气-我也不是从学术角度讲!
Andres F.

3
@Den我不知道这些功能的作用。但是,我可以告诉您FP中的第一个函数是sort还是filter,它将被命名为filtersort,而不是func(就像您将其命名一样IFilter,等等)。一般来说,在FP你命名funcf当任一sort或者filter是一个有效的选择!例如,在编写func以参数为单位的高阶函数时。
安德列斯·F

10

几年前,当我参加Java课程时,我们期望向整个课程展示我们的解决方案,因此我看到了人们的想法。他们如何从逻辑上解决问题。我完全希望解决方案能够围绕三个或四个常见解决方案进行聚类。相反,我看着30名学生以30种完全不同的方式解决了问题。

自然地,随着刚起步的程序员积累经验,他们将接触到常见的软件模式,开始在代码中使用这些模式,然后他们的解决方案可以结合一些最佳策略。这些模式形成了一种技术语言,有经验的开发人员可以通过这种语言进行交流。

支持函数式编程的技术语言是数学。因此,最适合使用函数式编程解决的问题本质上是数学问题。 在数学上,我并不是指您在业务解决方案中会看到的那种数学,例如加法和减法。相反,我是在谈论您可能在Math Overflow上看到的数学类型,或者在Orbitz的搜索引擎(用Lisp编写)中看到的数学类型。

对于那些试图解决实际编程问题的函数式程序员,这种定位有一些重要的影响:

  1. 函数式编程比命令式更具声明性。它主要涉及告诉计算机该怎么做,而不是如何去做。

  2. 面向对象的程序通常是自上而下构建的。创建了一个类设计,并填充了细节。功能程序通常是自下而上地构建的,首先从小的,详细的功能开始,这些功能又被组合为更高级别的功能。

  3. 函数式编程对于以下方面可能具有优势:对复杂逻辑进行原型设计,构建可以自然变化和发展的灵活程序以及在初始设计不清楚的情况下构建软件。

  4. 面向对象的程序可能更适合于业务领域,因为类,对象之间的消息以及软件模式都提供了映射到业务领域,捕获其业务智能并对其进行记录的结构。

  5. 因为所有实用软件都会产生副作用(I / O),所以纯函数式编程语言需要一种机制来产生那些副作用,同时保持数学上的纯正性(单子)。

  6. 功能程序由于其数学性质,可以更容易地得到证明。Haskell的类型系统可以在编译时找到大多数OO语言无法找到的东西。

等等。如同计算中的许多事物一样,面向对象的语言和功能性的语言具有不同的权衡。

一些现代的OO语言采用了一些有用的函数式编程概念,因此您可以兼得两者。Linq以及为支持它而添加的语言功能就是一个很好的例子。


6
@thepacker:您确实在函数式编程中具有状态。只有表示形式有所不同:在OOP中,您使用可变变量;而在函数式编程中,您可以使用在程序需要检查它们时动态创建的无限状态流。不幸的是,状态经常用可变变量来标识,好像可变变量是表示状态的唯一方法一样。结果,许多人认为没有可变变量的编程语言无法为状态建模。
乔治

12
“因此,最适合使用函数式编程解决的问题本质上是数学问题。”:我不同意这一说法(另请参见我以前的评论),至少我不明白为什么它应该是正确的或者是什么。其实意味着。您可以将遍历目录结构视为一个数学问题,并使用递归函数来实现它,但是由于客户看不到源代码,因此您正在解决一个实际问题,例如在文件系统中的某个位置查找文件。我发现数学和非数学问题都是人为的。
乔治

5
+1,但我没有在第2点和第4点上卖了。我看过很多Haskell教程和博客文章,它们从上至下解决问题,定义了所有类型和操作,以查看它们如何融合在一起,保留所有未定义的值和函数定义(好,定义为引发异常)。在我看来,Haskell程序员倾向于对问题进行更彻底的建模,因为他们更倾向于出于语义的考虑而添加琐碎的类型(例如,添加一种SafeString类型来表示经过HTML转义的字符串),并且通常会更多地跟踪效果。
2015年

4
“最适合使用函数式编程解决的问题本质上是数学问题。” 那根本是不对的。Monads概念来自类别理论的事实并不意味着数学任务是功能语言最自然/最适合的领域。这意味着用强类型功能语言编写的代码可以使用数学进行描述,分析和推理。这是一项强大的附加功能,但并不能阻止您在Haskell中编写“芭比马历险记”。
itsbruce

6
@itsbruce芭比马历险记(Baby Horse Adventures)是对最高口径的严肃数学模拟,它是将皮格顿将FP皮基洞成过于复杂的数字投影,例如矩阵描述了在各种可能的现实世界环境中离散时间段内芭比娃娃的头发的物理闪烁,这使人们相信完全将其视为与他们每天的业务问题编码无关。
Jimmy Hoffa,2015年

7

我想强调一个我认为很重要的方面,而其他答案未涉及这一方面。

首先,我认为,根据程序员的背景和他们所熟悉的概念,问题和解决方案之间的代表性差距可能会更大。

正如Robert Harvey已经指出的那样,OOP和FP从两个不同的角度看待数据和操作,并提供了不同的权衡。

它们不同的一个重要方面是它们允许扩展软件的方式。

考虑这样一种情况:您有一组数据类型和一组操作,例如,您有不同的图像格式,并且维护了一个用于处理图像的算法库。

函数式编程使向软件中添加新操作变得更加容易:您只需要对代码进行本地更改,即添加一个新函数即可处理不同的输入数据格式。另一方面,添加新格式会涉及更多:您需要更改已经实现的所有功能(非本地更改)。

面向对象的方法与此对称:每种数据类型都具有自己的所有操作实现,并负责在运行时选择正确的实现(动态调度)。这使得添加新的数据类型(例如,新的图像格式)变得容易:您只需添加一个新类并实现其所有方法。另一方面,添加新操作意味着更改所有需要提供该操作的类。在许多语言中,这是通过扩展接口并修改实现该接口的所有类来完成的。

这种不同的扩展方法是OOP更适合于某些问题的原因之一,在某些问题中,操作集的变化频率低于这些操作所处理的数据类型集的变化频率。一个典型的例子是图形用户界面:你有一个固定的一组操作的所有构件都必须实现(paintresizemove,等)和小工具,你要扩展的集合。

因此,根据此维度,OOP和FP只是组织代码的两种镜面方式。请参阅SICP,尤其是第2.4.3节,表2.22和“ 消息传递”段落。

总结:在OOP中,您使用数据来组织操作,在FP中,您使用操作来组织数据。根据上下文,每种方法都更强或更弱。通常,两者在问题和解决方案之间都没有更大的代表性差距。


1
实际上,“访问者模式”(在OOP中)具有与FP相同的功能。它使您可以添加新操作,同时增加添加新类的难度。我想知道您是否可以在FP中执行相同的操作(例如,添加新格式而不是操作)。
欣快感

1
图像处理方案似乎不是最好的例子。当然,您会将图像转换成某种内部格式和过程,而不是为每种图像类型编写所有图像处理内容的单独实现吗?
2015年

1
@Giorgio也许我没有从这部分中获得预期的含义:“增加新格式涉及更多:您需要更改已经实现的所有功能。”
2015年

2
@Hei Giorgio可能指的是所谓的表达问题。“添加新格式”是指“向数据类型添加新的构造函数(又名'cases')”。
Andres F.

1
@Euphoric:我没有研究细节,但是FP模式的访问者模式应该Other在您的数据类型中涉及一个变量/构造函数,以及一个额外的函数参数,该参数将传递给所有函数以处理一种情况。当然,相对于OOP解决方案(动态调度)而言,这很尴尬/不太自然。同样,访问者模式比FP解决方案(高阶函数)笨拙/不自然。
乔治

5

大多数功能语言都不是面向对象的。这并不意味着它们没有对象(就复杂类型而言,它们具有与之关联的特定功能)。与Java一样,Haskell具有列表,地图,数组,各种树和许多其他复杂类型。如果查看Haskell列表或Map模块,您将看到一组与大多数等效的OO语言库模块中的方法非常相似的函数。如果检查代码,您甚至会发现类似的封装,其中包含某些类型(或更精确地说,是它们的构造函数)和只能由模块中其他函数使用的函数。

在Haskell中使用这些类型的方法通常与OO方法几乎没有什么不同。我在Haskell中说

  null xs
  length xs

在Java中,您说

  xs.isEmpty
  xs.length

番茄,番茄。Haskell函数未绑定到对象,但与对象的类型紧密相关。“啊,但是,”您说,“在Java中,它正在调用哪种长度方法都适合该对象的实际类”。惊喜-Haskell 也使用类型类(和其他东西)进行多态

在Haskell,如果我 -整数集合-它并不重要,是否收集是一个列表或集或数组,这个代码

  fmap (* 2) is

将返回所有元素加倍的集合。多态性意味着将为特定类型调用适当的映射函数。

实际上,这些示例并不是真正的复杂类型,但在更困难的问题中也是如此。功能语言不允许您对复杂对象建模以及将特定功能与其关联的想法是完全错误的。

还有功能和面向对象的风格之间的重要差异显著,但我不认为这个答案有对付他们。您询问功能语言是否阻止(或阻碍)问题和任务的直观建模。答案是不。


实际上,您可以在Java / C#中使用类型类。您只是要显式地传递函数字典,而不是让编译器根据类型推断正确的字典。
2015年

哦,是的 正如William Cook所说的那样,尽管编码人员可能不知道,但是许多OO代码实际上比功能代码更频繁地使用高阶函数。
itsbruce 2015年

2
您正在做出错误的区分。列表只不过是一个惰性数据结构,而不是堆栈,队列或井字游戏模拟器。列表可以保存任何内容(在Haskell中,它很可能是部分应用的函数),并定义了如何与这些内容进行交互。您可以将充满部分应用函数的列表应用于另一个值列表,结果将是一个新列表,其中包含来自第一个函数和来自第二个函数的输入的所有可能组合。这不仅仅是C结构。
itsbruce

3
在功能语言中,类型用于比命令式/ OO语言更多样化的事物(一方面,类型系统倾向于更强大和更一致)。例如,类型及其功能用于调整代码路径(这就是为什么几种功能语言根本没有用于循环的关键字-不需要)的原因。
itsbruce

4
@Fuhrmanator“我不知道如何在FP中完成此操作”。去学习它,彻底了解它,然后它才有意义。也许在有意义之后,您会认为它是胡扯,并且您不需要其他人向您证明它,尽管上述其他人确实理解它,并且没有得出这个结论。您似乎在枪战中带来了一把刀,现在您正在告诉所有人,枪没用,因为它们不锋利。
Jimmy Hoffa

4

FP确实在努力缩小代表差距:

在功能语言中,您会看到很多东西,这是将语言向上(使用自下而上的设计)构建为嵌入式领域特定语言(EDSL)的实践。这样一来,您就可以开发一种表达您的业务问题的方式,这种方式对于您的编程语言领域来说是很自然的。Haskell和Lisp都为此感到自豪。

启用此功能的一部分(如果不是全部)是基本语言本身具有更大的灵活性和表现力;具有一流的功能,高阶功能,功能组合,以及某些语言,代数数据类型(区分AKA的联合)和定义运算符*的能力,与OOP相比,表达事物的灵活性更大。意味着您可能会找到更自然的方式来表达问题领域中的事物。(应该说的是,即使不是从一开始,许多OOP语言最近就已经采用了许多这些功能。)

*是的,在许多OOP语言中,您可以覆盖标准运算符,但是在Haskell中,您可以定义新的运算符!

根据我使用函数式语言(主要是F#和Haskell,一些Clojure,以及一些Lisp和Erlang)的经验,我比使用OOP更加容易地将问题空间映射到语言中-特别是对于代数数据类型,我发现它比类更灵活。在开始使用FP(尤其是Haskell)时,使我陷入困境的一环是,我必须比在命令式或OOP语言中所惯用的水平更高的思想/工作;您可能也会遇到这种情况。


(来自客户的)业务问题很少在高级功能中表达。但是,客户可以编写一个单行的用户案例并为您提供业务规则(或者您可以使他们脱离客户)。我对(自上而下的)域建模(从这些工件中获得的)感兴趣,这使我们能够得出映射到一流函数等问题的抽象概念。您可以引用一些参考文献吗?您可以使用域,抽象等的具体示例吗?
Fuhrmanator 2015年

3
@Fuhrmanator 无论您是否使用OOP和FP,客户仍然可以编写一线用户故事和业务规则。我不希望客户对高阶函数的理解比对他们理解继承,组合或接口的理解多。
Andres F.

1
@Fuhrmanator您有多少次让客户就Java泛型或抽象基类和接口表达他们的担忧?亲爱的上帝。Paul给了您一个很好的答案,解释了一种实用且可靠的建模方法,该方法使用的可视化技巧肯定不太难。但是您坚持要用一种针对特定范式的特定建模技术来描述它,就好像这是代表设计的最自然的方式,其他任何事物都可以用其术语重绘。
itsbruce

@Fuhrmanator这可能不像您想要的那样高级,但是应该使用功能性技术对设计过程提供一些见解:Designing With Types。我建议一般浏览该网站,因为它有一些出色的文章。
保罗

@Fuhrmanator还有这个功能上的思考系列
paul,

3

正如Niklaus Wirth所说,“算法+数据结构=程序”。函数式编程是关于组织算法的方法,它并没有太多地介绍组织数据结构的方法。实际上,存在具有可变(Lisp)和不可变(Haskell,Erlang)变量的FP语言。如果要比较FP与某些东西,请选择命令式(C,Java)和声明式(Prolog)编程。

另一方面,OOP是一种构建数据结构并将算法附加到它们的方法。FP不会阻止您建立类似于OOP的数据结构。如果您不相信我,请查看Haskell类型声明。但是,大多数FP语言都同意函数不属于数据结构,而应该在同一名称空间中。这样做是为了简化通过函数组合构建新算法的过程。的确,如果您知道一个函数接受什么输入以及它产生什么输出,为什么它也应该属于哪个数据结构就很重要?例如,为什么add必须在Integer类型的实例上调用函数并传递一个参数,而不是简单地传递两个Integer参数?

因此,我不明白为什么FP应该使解决方案总体上变得更难以理解,而且我也不认为它会这样做。但是,请记住,经验丰富的命令式程序员一定会发现比命令式程序更难理解的功能程序,反之亦然。这类似于Windows-Linux二分法,当人们花了十年的时间适应Windows环境后,发现他们在使用一个月后发现Linux变得困难,而习惯Linux的人们却无法在Windows中获得相同的生产力。因此,您所谈论的“代表性差距”是非常主观的。


由于封装和数据隐藏本身不是面向对象的原理,因此我并不完全同意面向对象是数据结构+算法(仅内部视图)。任何好的抽象的优点在于,它提供了一些服务来解决部分问题,而无需了解太多细节。我认为在FP中分离功能和数据时您正在谈论封装,但是我实在太绿了,无法确定。另外,我编辑了我的问题,以指从解决方案追溯到问题(以阐明代表性差距的含义)。
Fuhrmanator

if you know what input a function takes and what output it produces, why should it matter which data structure it belongs too?这听起来很像凝聚力,对于任何模块化系统而言,凝聚力都很重要。我认为,不知道在哪里可以找到一个函数将使理解解决方案变得更加困难,这是由于较高的代表性差距所致。许多OO系统都存在此问题,但这是由于不良的设计(低内聚性)所致。再说一次,名称空间问题超出了我在FP中的专业知识,因此也许它并不像听起来那样糟糕。
Fuhrmanator

1
@Fuhrmanator但是您确实知道在哪里可以找到函数。而且,每种数据类型仅定义一个函数,而没有多个具有相同名称的函数(同样,请参见“表达式问题”)。例如,对于Haskell库函数(建议您使用它,而不是重新发现轮子或制作所述函数的有用版本,而不是使用它们),您拥有出色的发现工具,例如Hoogle,它如此强大,可让您搜索签名(尝试!:))
Andres F.

1
@Fuhrmanator,在“听起来很像凝聚力”之后,我希望您从逻辑上得出结论,即FP语言可以让您编写具有更高凝聚力的程序,但令我感到惊讶。:-)我认为您应该选择一种FP语言,然后学习并自己决定。如果您是我,我建议您从Haskell开始,因为它非常纯正,因此不会让您以命令式风格编写代码。Erlang和一些Lisp也是不错的选择,但是它们混合了其他编程风格。IMO的Scala和Clojure从一开始就非常复杂。
mkalkov
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.