函数式编程是否忽略了“关于将系统分解为模块所用的准则”(数据隐藏)所带来的好处?


27

我第一次读过一篇经典的文章,名为《关于将系统分解为模块的标准》。这对我来说很有意义,并且可能是OOP所基于的那些文章之一。结论:

我们试图通过这些示例来证明,根据流程图将系统分解为模块几乎总是不正确的。...然后每个模块都被设计为对其他模块隐藏这样的决定

以我未受过教育和缺乏经验的观点,函数式编程与本文完全相反。我的理解是函数式编程使数据流变得惯用了。数据从一个函数传递到另一个函数,每个函数都密切了解数据并在此过程中“更改”它。而且我想我已经看过Rich Hickey的演讲,他在演讲中谈到数据隐藏是如何被高估或不必要的,但我不确定。

  1. 首先,我想知道我的评估是否正确。FP范式和本文在哲学上是否不一致?
  2. 假设他们不同意,FP如何“弥补”缺乏数据隐藏的能力?也许他们牺牲了数据隐藏,但获得了X,Y和Z。我想知道为什么X,Y和Z比数据隐藏更有益的原因。
  3. 或者,假设他们不同意,也许FP认为数据隐藏很糟糕。如果是这样,为什么会认为数据隐藏不好?
  4. 假设他们同意,我想知道什么是FP数据隐藏实现。很明显在OOP中看到了这一点。您可以拥有一个private班级以外的人无法访问的字段。在FP中,没有明显的比喻。
  5. 我觉得还有其他问题要问,但我不知道我要问。也可以随意回答。

更新资料

我发现尼尔·福特(Neal Ford)的演讲中有一个非常相关的幻灯片。我将屏幕截图嵌入此处:

在此处输入图片说明


1
我无法回答完整的问题,但至于(4),有些FP语言中的模块系统可以提供封装。
Andres F.

@AndresF。是的,是的。我忘了Haskell有模块,您可以在其中隐藏数据类型和函数。也许当我说FP时,我真的是在说Clojure。您可以在Clojure中使用私有函数和“字段”,但是我觉得让您的数据可见并传递到任何地方都是一种习惯。
Daniel Kaplan 2013年

6
您通常要做的是使您的类型可见,但隐藏构造函数。这些抽象类型在OCaml模块系统中做得特别好
Daniel Gratzer

6
在类似于ML的语言中,无法访问构造函数意味着您无法通过模式匹配该类型的值来对其进行解构。这些值唯一可以做的就是将其传递给任何可用的函数。就像C中一样,它也具有相同的数据抽象,C中也没有关于公共或私有概念的一流概念。
吕克·丹顿

1
@ SK-logic:从“表达式问题”的角度来看,当您将来想要使用新功能扩展时,显示数据是好的(并且可以保持数据固定),而在需要时隐藏数据是很好的在将来扩展新数据类型(以保持功能接口固定为代价)
hugomg

Answers:


23

您提到的文章通常是关于模块化的,并且同样适用于结构化,功能化和面向对象的程序。我以前是OOP大人物听说过该文章,但我将其作为一篇有关编程的文章而非特定于OOP的文章来阅读。有一篇关于函数式编程的著名文章,《为什么函数式编程如此重要》,结论的第一句话说:“在本文中,我们认为模块化是成功编程的关键。” 因此,(1)的答案是否定的。

设计良好的函数不会对它们的数据承担过多的假设,因此有关“密切了解数据”的部分是错误的。(或者至少与OOP一样错误。您不能严格按照高抽象级别进行编程,而无法在任何范例中永久忽略所有细节。最后,程序的某些部分实际上确实需要了解数据的具体细节。)

数据隐藏是OOP专用术语,它与本文中讨论的信息隐藏不完全相同。本文中隐藏的信息与难以做出或可能会更改的设计决策有关。并非每个关于数据格式的设计决策都很难或可能会更改,并非每个难以或可能更改的决策都与数据格式有关。就个人而言,我不明白为什么OO程序员希望所有东西都成为对象。有时,您只需要一个简单的数据结构。

编辑:我从Rich Hickey的一次采访中找到了一条相关的报价。

Fogus:遵循了这个想法-Clojure并未针对其类型进行数据隐藏封装,这一事实使某些人感到惊讶。您为什么决定放弃数据隐藏?

Hickey:让我们清楚一点,Clojure强烈强调对抽象的编程。但是在某个时候,某人将需要访问数据。并且,如果您有“私有”的概念,则需要相应的特权和信任的概念。这就增加了一大堆的复杂性和很少的价值,在系统中产生了僵化,并经常迫使事物生活在不应有的地方。这是将简单信息放入类时发生的其他损失的补充。在一定程度上,数据是不可变的,提供访问的危害很小,除了有人可能会依赖于可能发生变化的事物之外。好吧,人们在现实生活中一直都在这样做,并且当事情发生变化时,他们就会适应。如果他们是理性的,他们知道,当他们基于可能会改变的事物做出决定时,将来可能需要适应。因此,这是一项风险管理决定,我认为程序员应该可以自由地做出决定。如果人们不希望对抽象编程,也不愿意与实现细节结合,那么他们永远都不会成为优秀的程序员。


2
OO程序员希望一切都成为对象。但是有些(很多东西)受益于封装。我无法理解您的答案如何或在何处真正解决了该问题。似乎只是在断言该概念不是特定于OOP的,而且OOP还有其他问题,等等等等-即使只是几行伪代码,您也可以提供一个清晰的示例吗?还是考虑到设计的白板描述?或有什么可以证实这里的主张?
2013年

2
@Aaronaught:我解决了问题中提出的许多(尽管不是全部)问题,并引用了一篇有关函数式编程的论文,该论文以与问题中的论文相似的方式看待模块化。在很大的程度上,其实这个概念是不特定OOP 在回答他的问题(除非我误解了完全的问题)。我真的不是在说OOP在这里还有其他问题。您提供示例很好。我看看能否提出一个好的建议。
萧伯纳

2
“有时候,您只需要一个简单的数据结构”。+1。OOP有意义,有时是FP。
Laurent Bourgault-Roy 2013年

1
@Aaronaught这个答案的确指出了模块化(既是封装又是重用)是FP的目标之一(如“为什么进行功能编程很重要”中所述),因此将问题的第(1)点的答案设为“没有”。
Andres F.

2
@JimmyHoffa信息隐藏即使在OO之外也是理智的原则。在haskell中,我仍然希望让用户对任何数据结构的了解最少。当然,接触内部部件的危险性较小,因为没有任何东西是可变的。但是用户对一个模块/数据结构/任何抽象概念的了解越少,您获得的重构机会就越多。我不关心地图是平衡的二叉树还是计算机中一个小盒子中的鼠标。这是隐藏数据的主要动机,并且在OO外有效。
西蒙·贝格

12

...可能是OOP所基于的那些文章之一。

并非如此,但这确实增加了讨论的范围,特别是对于当时受过训练的从业人员,使用他在论文中描述的第一个标准分解系统的从业人员。

首先,我想知道我的评估是否正确。FP范式和本文在哲学上是否不一致?

不,而且,在我看来,您对FP程序的外观的描述与使用过程或函数的其他程序没有什么不同:

数据从一个函数传递到另一个函数,每个函数都密切了解数据并在此过程中“更改”它。

... 除了 “亲密关系”部分外,因为您可以(而且经常这样做)具有对抽象数据进行操作的功能,以精确地避免这种亲密关系。因此,您确实可以控制“亲密关系”,并且可以通过设置要隐藏的内容的界面(即功能)来随意调节它。

因此,我认为没有理由为什么我们不能使用函数式编程来遵循Parnas的信息隐藏标准,而最终实现了KWIC索引的实现,其收益与他的第二种实现相似。

假设他们同意,我想知道什么是FP数据隐藏实现。很明显在OOP中看到了这一点。您可以有一个私有字段,班级以外的任何人都不能访问。在FP中,没有明显的比喻。

就数据而言,您可以使用FP来详细说明数据抽象和数据类型抽象。这些功能中的任何一个都隐藏了混凝土结构,并使用函数作为抽象来隐藏这些混凝土结构。

编辑

越来越多的断言指出,在FP上下文中“隐藏数据”并不是那么有用(或OOP式(?))。因此,让我在此标记SICP中一个非常简单清晰的示例:

假设您的系统需要使用有理数。您可能想要表示它们的一种方法是成对或两个整数列表:分子和分母。从而:

(define my-rat (cons 1 2)) ; here is my 1/2 

如果您忽略数据抽象,很可能会使用car和获得分子和分母cdr

(... (car my-rat)) ; do something with the numerator

按照这种方法,操纵有理数的系统的所有部分都将知道有理数是cons-他们将对cons数进行编号以创建有理数并使用列表运算符提取它们。

您可能面临的一个问题是何时需要简化形式的有理数-整个系统都需要进行更改。同样,如果您决定在创建时减少,则以后可能会发现,访问一个有理术语时减少会更好,从而产生另一次全面变化。

另一个问题是,假设是否首选它们的替代表示形式,而您决定放弃该cons表示形式,请再次进行满量程更改。

处理这些情况的任何明智的努力都可能会开始隐藏界面背后的理性表示。最后,您可能会得到如下结果:

  • (make-rat <n> <d>)返回其分子为整数<n>且分母为整数的有理数<d>

  • (numer <x>)返回有理数的分子<x>

  • (denom <x>)返回有理数的分母<x>

并且系统将不再(也应该不再)知道构成什么理性。这是因为conscarcdr不是固有的有理数,但是make-ratnumerdenom 。当然,这很容易成为FP系统。因此,“数据隐藏”(在这种情况下,更好地称为数据抽象,或者是努力封装表示和具体结构)是相关的概念和广泛使用和探索的技术,无论是在OO,函数式编程还是在随你。

重点是……尽管人们可能会试图区分他们正在执行的“某种隐藏”或封装(无论是隐藏设计决策,数据结构还是算法,在过程抽象的情况下),所有这些都具有相同的主题:它们是由一个或多个点帕纳斯作出了明确的动机。那是:

  • 可变性:所需的更改是可以在本地进行还是通过系统传播。
  • 独立开发:在多大程度上可以并行开发系统的两个部分。
  • 可理解性:需要了解多少系统才能理解其一部分。

上面的示例取自SICP的书,因此,为了在书中对这个概念进行完整的讨论和介绍,我强烈建议您阅读第2章。我还建议您在FP上下文中熟悉Abstract Data Types,这会带来其他问题。


我同意数据隐藏与FP相关。而且,正如您所说的,有一些方法可以实现这一目标。
Andres F.

2
您刚刚说得很清楚:您具有这些功能,这些功能不会隐藏数据,它们是描述如何获取数据的表达式,因此,通过将抽象包含在表达式中而不是数据字段中,您无需担心隐藏问题通过使用私有成员创建一些复杂的对象或使您的缺点值不可访问来表达数据,表示生成,检索和与理性数据进行交互的活动因此表示实际的理性数据不需要隐藏,因为更改数据不会改变你的表情。
吉米·霍法

8

您认为函数式编程缺少数据隐藏功能是错误的。只是采用另一种方法来隐藏数据。在函数式编程中最常见的隐藏数据的方法之一是使用将函数作为参数的多态函数。例如这个功能

map :: (a -> b) -> [a] -> [b]
map _ [] = []
map f (x:xs) = f x : map f xs

只能看到数据的最外层结构(即它是一个列表),看不到有关列表包含的数据的任何信息,并且只能通过传递给它的单个函数对数据进行操作。

作为参数传递的函数类似于列表包含的数据类型上的公共方法。它提供了一种有限的数据操作方式,但没有公开数据类型的内部工作原理。


5

在这里,我要大胆地说,这个概念与OO中的方式在FP中并不相关。

tl; dr; 数据隐藏的目的是确保将职责保持在应有的位置,并且您不会让外部参与者混乱他们没有知识的数据。在FP中,数据是由表达式生成的,因此,您不会弄乱数据,因为它不是可变属性,而是可组合计算完全改变了游戏规则,而不是可变属性。


根据我与FP的经验;这些都是微不足道的,我倾向于在表示良好/通用数据建模方面与OO形成鲜明对比。

这种对比是,通常在OO中,您对事物进行建模以表示数据。强制性汽车类比:

面向对象

  • 您有一个汽车对象,可以正确隐藏有关汽车的详细信息,例如AC实施(它是皮带驱动还是气压驱动?消费者不需要知道,就隐藏起来)。
  • 该汽车对象具有许多属性和方法,可描述有关汽车的所有事实以及您使用汽车的方式。
  • 该汽车对象具有作为汽车组件的属性,这些属性进一步从整体汽车中隐藏了其特定的实现方式以及其数据事实,从而使汽车的组件可以互换。

这里要注意的事情是,当您以OO格式对事物进行建模时,其全部内容就是将事物表示为数据。您的对象具有属性,其中许多属性是具有更多属性的对象。您在这里和那里有几个附加到这些对象的方法,但是它们真正要做的通常只是通过这种方式来微调对象的属性,而且这再次是以数据为中心的建模。也就是说,您要建模要交互的数据,重点在于构造数据以使数据的所有点都可用,以便消费者可以以此方式更改数据。

FP

  • 您进行了大量的计算,可以描述行为
  • 行为的这些表达方式可以转换为与汽车行为彼此相关的方式,例如一辆汽车具有加速/减速的状态,有两种行为以相似的方式彼此相对。

OO和FP的最大区别一直困扰着我,就像我在数据建模方法中所说的那样。在如上所述的OO中,将数据建模为数据,在FP中,将数据建模为计算,表达式,算法,更多的是关于对数据活动进行建模,而不是对事实进行建模。考虑一下数学中的基本数据建模,它总是与获取可以生成数据的方程式有关,该方程式将数据建模为导致数据生成的活动,而不是OO建模提出了一种表示您拥有的数据的方法。这是FP和OO之间的大部分区别。

请记住,LISP是一种基本的FP语言,在很长一段时间内,它们只使用少量的原始数据类型。之所以行之有效,是因为这种方法与其说是对数据的复杂表示建模,不如说是生成并表达系统行为的计算。

当我开始在FP中编写一些代码时,我会先编写会执行某些操作的代码,而当我开始在OO中编写代码时,便会开始编写描述某些内容的模型。事务通过被表达式隐藏在FP中,事物在OO中通过用数据描述而暴露,隐藏此数据限制了暴露。


回到当前的问题,FP对数据隐藏说了什么,它欣赏还是不同意?

我说没关系,在OO中,您的数据是程序中的胆量和重要部分,应避免被干预。在FP中,您系统的勇气和知识全都隐藏在表达系统的算法和计算中。从定义上讲,这些变量或多或少是不可变的,对计算表达式进行变异的唯一方法是像宏之类的东西,但即使那样,您也可以将变异定义定义为无法进一步干预的表达式本身。


这很棒,我真的很喜欢阅读它。谢谢您的贡献
克里斯·麦考尔

5

这里有些悖论。尽管函数式编程专注于函数,并且经常具有直接对原始数据类型起作用的函数,但是与面向对象的编程相比,它倾向于隐藏更多的数据。

怎么了 考虑一个很好的OO接口,该接口可以隐藏基础数据-也许是集合(我正在尝试选择几乎无处不在的东西)。您可能不需要知道集合中对象的基础类型或实现集合的对象的类型,只要您知道集合实现了IEnumerable。这样您就可以隐藏数据。

在函数式编程中,您可能会编写一个函数,该函数可与IEnumerable接口有效配合使用,但可对原始数据类型(或任何数据类型)进行操作。但是,如果该类型从未实现IEnumerable方法,该怎么办?这是关键,您始终可以拥有构成“接口”所需部分的“方法”,这些参数是传递给函数的参数。 或者,您可以将函数与数据放在一起,并以类似于OO的方式执行操作。

注意,无论哪种方式,您隐藏的数据都不会比OO中的数据少。我可以在任何类型上使用的通用函数显然都不会访问该类型的数据-发生在作为参数传递给通用函数的函数中,但是通用函数从不偷看那些函数以查看数据。

因此,就您的观点1而言,我认为FP和本文并不存在真正的分歧。我认为您对FP不隐藏数据的描述不正确。当然,可以实现作者偏爱FP的设计。

至于第4点(鉴于我对第1点所说的,第2点和第3点无意义回答),它有所不同。它在OO语言中也有所不同,在许多私有领域中,按惯例是私有的,而不是由语言强制执行的。


换句话说:在函数式编程中,默认情况下更多“隐藏”,仅仅是因为它根本不存在!只有明确带入范围的内容才是“未隐藏”的。
大约

3

首先,感谢您链接到这篇出色的文章,到目前为止我还不了解,它为我提供了很多有用的信息,这些信息是我最近几年与社区中其他软件设计师讨论的。这是我的看法:

首先,我想知道我的评估是否正确。FP范式和本文在哲学上是否不一致?

FP设计非常关注数据流(恕我直言,它并不像本文所暗示的那样糟糕)。如果这是一个完整的“分歧”,则是有争议的。

假设他们不同意,FP如何“弥补”缺乏数据隐藏的能力?也许他们牺牲了数据隐藏,但获得了X,Y和Z。我想知道为什么X,Y和Z比数据隐藏更有益的原因。

恕我直言,它不会补偿。见下文。

或者,假设他们不同意,也许FP认为数据隐藏很糟糕。如果是这样,为什么会认为数据隐藏不好?

我认为大多数FP用户或设计师都不会以这种方式来感受或思考,请参阅下文。

假设他们同意,我想知道什么是FP数据隐藏实现。很明显在OOP中看到了这一点。您可以有一个私有字段,班级以外的任何人都不能访问。在FP中,没有明显的比喻。

这就是重点-您可能已经看到太多以非功能性方式实现的OOP系统,您认为OOP是非功能性的。这是一个谬论,IMHO OOP和FP大多是正交概念,您可以完美地构建功能性OO系统,从而为您的问题提供了明显的答案。FP中经典的“对象”实现是通过使用闭包来完成的,如果您希望对象在功能系统中使用,关键是将它们设计为不可变的。

因此,对于创建更大的系统,恕我直言,您可以使用OO概念来创建模块,类和对象,完全按照本文“模块化2”中所述的方式进行,而无需离开“ FP路径”。您将使用自己喜欢的FP语言的模块概念,使所有对象不变,并使用“两全其美”。


3

TL; DR:否

FP范式和本文在哲学上是否不一致?

不,不是。函数式编程是声明性的,它是“一种构建计算机程序的结构和元素的样式,用于表达计算的逻辑而不描述其控制流程”。 它与遵循流程图无关,而更像是创建让流程自己产生的规则。

过程编程比功能编程更接近流程图的编码。随之而来的是发生的转换,并将这些转换编码为按顺序执行的过程,就像流程图中描述的流程一样。

程序语言将程序的执行建模为一系列命令,这些命令可能会隐式更改共享状态,而功能编程语言将执行建模为对仅在参数和返回值方面相互依赖的复杂表达式的评估。由于这个原因,功能程序可以具有更自由的代码执行顺序,并且语言可能几乎不控制程序各部分的执行顺序。(例如,Scheme中过程调用的参数以任意顺序执行。)

资料隐藏

  • 函数式编程具有其自身的数据隐藏方法,例如thinkclosures。那就是通过封装在闭包中来隐藏数据。字段很难再成为已关闭的私有数据,因为只有闭包具有对数据的引用,而您不能在闭包外部引用它。
  • 数据隐藏的原因之一是通过隐藏变异数据来稳定编程接口。函数式编程没有变异数据,因此不需要太多的数据隐藏。

3
“函数式编程没有变异数据,因此不需要隐藏太多的数据。” -这是一个非常令人误解的断言。你说你自己(我同意),其一个原因封装的行为是有对数据的突变控制。但是得出结论,缺乏突变几乎会使封装无用,这是一个很大的尝试。ADT和数据抽象通常在FP文献和系统中普遍存在。
Thiago Silva

我从未说过“几乎使封装无用”。这些只是您的想法,而只是您的想法。我说过,由于缺少变异变量,您不需要隐藏太多数据。这不会使封装或数据隐藏变得无用,只会减少使用,因为这些情况不存在。所有其他有用数据隐藏和封装的情况仍然有效。
Dietbuddha 2013年
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.