API和函数式编程


15

从我(有限地)接触函数式编程语言(例如Clojure)开始,似乎数据封装的作用不那么重要。通常,各种本机类型(例如地图或集合)是代表对象的首选数据。此外,该数据通常是不可变的。

例如,这是Clojure的成名人物里奇·希基(Rich Hickey)在接受此事采访时最著名的名言之一:

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

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

来自面向对象的世界,这似乎使我多年来学到的一些基本原则变得复杂。其中包括信息隐藏,德米特定律和统一访问原则等。封装的共同点是使我们能够为其他人定义API,让他们知道应该和不应该接触的内容。从本质上讲,创建一个合同,允许某些代码的维护者自由地进行更改和重构,而不必担心它将如何在用户代码中引入错误(开放/封闭原则)。它还为其他程序员提供了一个干净,精心设计的界面,以使他们知道可以使用哪些工具来获取或建立该数据。

当允许直接访问数据时,该API合同被破坏,所有这些封装好处似乎都消失了。同样,严格不变的数据似乎在遍历特定于域的结构(对象,结构,记录)时,在表示状态和可以对该状态执行的操作的意义上要有用得多。

当代码库的大小变得巨大而需要定义API且许多开发人员参与处理系统的特定部分时,功能代码库如何解决似乎出现的这些问题?是否存在这种情况的示例,以说明在这些类型的代码库中如何处理此情况?


2
您可以定义没有对象概念的正式接口。只需创建记录它们的界面功能即可。不提供实现细节的文档。您刚刚创建了一个界面。
Scara95

@ Scara95并不是说我必须要做一些工作,既要实现接口的代码,又要写足够的文档来警告消费者该做什么和不该做什么?如果代码更改并且文档过时了怎么办?由于这个原因,我通常更喜欢自记录代码。
jameslk

无论如何,您都必须记录该接口。
Scara95

3
Also, strictly immutable data seems to make passing around domain-specific structures (objects, structs, records) much less useful in the sense of representing a state and the set of actions that can be performed on that state.并不是的。唯一更改的是更改最终出现在新对象上。在代码推理方面,这是一个巨大的胜利。传递可变对象意味着必须跟踪谁可能对其进行变异,这个问题随着代码的大小而扩大。
Doval 2015年

Answers:


10

首先,我将对Sebastian的第二点评论,即什么是适当的功能,什么是动态类型。更一般地,Clojure是一个味道的函数式语言和社会的,你不应该在此基础上概括太多。我将从ML / Haskell的角度做一些评论。

正如Basile提到的那样,访问控制的概念确实存在于ML / Haskell中,并且经常使用。“因式分解”与常规的OOP语言略有不同。在OOP中,的概念同时扮演类型模块的角色,而功能(和传统过程)语言则将它们正交对待。

另一个要点是ML / Haskell对具有类型擦除的泛型非常重视,并且可以将其用于提供与OOP封装不同的“信息隐藏”风格。当组件仅将数据项的类型作为类型参数知道时,可以安全地将该组件的值传递给该类型的值,但是由于它不知道也不知道其具体类型,因此将阻止对该组件执行过多操作(instanceof这些语言没有通用或运行时强制转换)。 这篇博客文章是我最喜欢的这些技术的入门示例之一。

下一步:在FP世界中,通常使用透明数据结构作为不透明/封装组件的接口。例如,解释器模式在FP中非常常见,其中数据结构用作描述逻辑的语法树,并馈送到“执行”它们的代码中。正确地说,状态在解释器运行时消耗数据结构时短暂存在。同样,解释器的实现可以更改,只要它仍以相同的数据类型与客户端通信即可。

最后也是最长的:封装/信息隐藏是一种技术,而不是目的。让我们考虑一下它提供了什么。封装是一种调和合同和软件单元实现的。典型情况是这样的:系统的实现根据其合同承认不应该存在的值或状态。

一旦您以这种方式看待它,我们可以指出FP除了提供封装之外,还提供了许多可用于同一目的的其他工具:

  1. 不变性是普遍的默认设置。您可以将透明数据值交给第三方代码。他们无法修改它们并将它们置于无效状态。(卡尔的回答说明了这一点。)
  2. 具有代数数据类型的复杂类型系统,使您无需编写大量代码即可精确控制类型的结构。通过明智地使用这些工具,您经常可以设计出根本不可能出现“不良状态”的类型。(口号:“使非法国家无法代表。”无法)与其使用封装来间接控制类的可允许状态集,不如告诉编译器它们是什么,并为我保证这些状态!
  3. 解释器模式,如前所述。设计好的抽象语法树类型的一个关键是:
    • 尝试设计抽象语法树数据类型,以使所有值均为“有效”。
    • 如果失败,则使解释器显式检测无效组合,并明确拒绝它们。

这个F#“使用类型设计”系列使您可以很好地阅读其中一些主题,尤其是第2个主题。(这是上面的“使非法状态无法表示”链接的来源。)如果仔细观察,您会注意到,在第二部分中,它们演示了如何使用封装来隐藏构造函数并防止客户端构造无效实例。就像我上面说的,它工具包的一部分!


9

我真的不能夸大可变性在软件中引起问题的程度。鼓舞我们的很多做法都是为了弥补变异性引起的问题。当您去除可变性时,您并不需要那么多实践。

当您具有不变性时,您会知道您的数据结构在运行时不会从您的下面改变,因此您可以在向程序添加功能时制作自己的派生数据结构以供自己使用。原始数据结构不需要了解这些派生数据结构的任何知识。

这意味着您的基本数据结构往往非常稳定。根据需要从边缘派生出新的数据结构。在完成重要的功能程序之前,真的很难解释。您会发现自己越来越不在乎隐私,而越来越多地考虑创建持久的通用公共数据结构。


我想补充的一件事是,如果存在一个不变的变量,那么可变变量会使程序员坚持使用分散的数据结构。所有数据的结构都是为了创建逻辑组,以便于发现和遍历,而不是用于运输。一旦完成足够的功能编程,这就是您要进行的逻辑过程。
Xephon 2015年

8

在我看来,Clojure倾向于仅使用哈希和原始元素的倾向,不是其功能遗产的一部分,而是其动态遗产的一部分。我已经在Python和Ruby中看到了类似的趋势(尽管面向对象,命令式和动态的,尽管它们都对高阶函数都提供了很好的支持),但是在Haskell(不是静态类型,而是纯粹的功能)中却没有,并需要特殊的结构以逃避不变性)。

因此,您需要问的不是功能语言如何处理大型API,而是动态语言如何实现。答案是:好的文档和大量的单元测试。幸运的是,现代动态语言通常对这两种语言都提供很好的支持。例如,Python和Clojure都有一种将文档嵌入代码本身的方式,而不仅仅是注释。


关于静态类型的(纯)功能语言,没有像OO编程中那样(简单)的方式来携带具有数据类型的功能。因此文档仍然很重要。关键是您不需要语言支持即可定义接口。
Scara95

5
@ Scara95您能否详细说明“带有数据类型的函数”的含义?
塞巴斯蒂安·雷德尔

6

某些功能语言可以封装或隐藏抽象数据类型模块中的实现细节。

例如,OCaml具有由一组命名的抽象类型和值(特别是在这些抽象类型上运行的函数)定义的模块。因此,从某种意义上讲,Ocaml的模块正在完善API。Ocaml还具有函子,这些函子将一些模块转换为另一个模块,从而提供通用编程。所以模块是组成的。

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.