204

今天教授的软件工程完全专注于面向对象的编程和“自然的”面向对象的世界观。有一个详细的方法描述了如何通过几个步骤和许多(UML)工件(如用例图或类图)将域模型转换为类模型。许多程序员已经内化了这种方法,并且对如何从头开始设计面向对象的应用程序有很好的想法。

新的炒作是函数式编程,它在许多书籍和教程中都有讲授。但是功能软件工程呢?在阅读有关 Lisp 和 Clojure 的信息时,我想到了两个有趣的陈述:

  1. 函数式程序通常是自下而上而不是自上而下开发的('On Lisp',Paul Graham)

  2. 函数式程序员使用映射,而 OO 程序员使用对象/类('Clojure for Java Programmers',Rich Hickley 的演讲)。

那么,在 Lisp 或 Clojure 中系统化(基于模型?)设计功能应用程序的方法是什么?常见步骤是什么,我使用什么工件,如何将它们从问题空间映射到解决方案空间?

4

13 回答 13

168

感谢上帝,软件工程人员还没有发现函数式编程。以下是一些相似之处:

  • 许多面向对象的“设计模式”被捕获为高阶函数。例如,Visitor 模式在功能世界中被称为“折叠”(或者如果您是一个头脑敏锐的理论家,则称为“catamorphism”)。在函数式语言中,数据类型主要是树或元组,每种树类型都有与之相关的自然变态。

    这些高阶函数通常带有某些编程定律,也就是“自由定理”。

  • 与 OO 程序员相比,函数式程序员使用图表的次数要少得多。OO 图中表达的大部分内容都以类型或“签名”表示,您应该将其视为“模块类型”。Haskell 还有“类型类”,有点像接口类型。

    那些使用类型的函数式程序员通常认为“一旦你得到了正确的类型,代码实际上就是自己编写的”。

    并非所有的函数式语言都使用显式类型,但是How To Design Programs这本书是一本学习 Scheme/Lisp/Clojure 的优秀书籍,它在很大程度上依赖于与类型密切相关的“数据描述”。

那么,在 Lisp 或 Clojure 中系统化(基于模型?)设计功能应用程序的方法是什么?

任何基于数据抽象的设计方法都行之有效。我碰巧认为当语言具有显式类型时这会更容易,但即使没有它也可以工作。一本关于抽象数据类型设计方法的好书,很容易适应函数式编程,是Barbara Liskov 和 John Guttag 的程序开发中的抽象和规范,第一版。Liskov 获得图灵奖的部分原因是这项工作。

Lisp 独有的另一种设计方法是确定哪些语言扩展在您正在工作的问题域中有用,然后使用卫生宏将这些构造添加到您的语言中。阅读这种设计的好地方是 Matthew Flatt 的文章Creating Languages in Racket。该文章可能位于付费墙后面。您还可以通过搜索术语“特定领域的嵌入式语言”找到有关此类设计的更多通用材料;对于 Matthew Flatt 所涵盖的特定建议和示例,我可能会从 Graham 的On LispANSI Common Lisp开始。

有哪些常见步骤,我使用哪些工件?

常用步骤:

  1. 识别程序中的数据及其上的操作,并定义一个抽象的数据类型来表示这些数据。

  2. 识别常见的操作或计算模式,并将它们表示为高阶函数或宏。期望将此步骤作为重构的一部分。

  3. 如果您使用类型化的函数式语言,请尽早并经常使用类型检查器。如果你使用 Lisp 或 Clojure,最好的做法是首先编写函数契约,包括单元测试——这是最大的测试驱动开发。而且您将希望使用已移植到您的平台的任何版本的 QuickCheck,在您的情况下,它看起来像是称为ClojureCheck。它是一个非常强大的库,用于构建使用高阶函数的代码的随机测试。

于 2011-02-05T06:17:21.723 回答
46

对于 Clojure,我建议回到良好的旧关系建模。Out of the Tarpit是一本鼓舞人心的读物。

于 2011-01-31T15:07:54.500 回答
38

就我个人而言,我发现 OO 开发中所有常见的良好实践也适用于函数式编程——只需进行一些小的调整以考虑到函数式世界观。从方法论的角度来看,你真的不需要做任何根本不同的事情。

我的经验来自近年来从 Java 迁移到 Clojure。

一些例子:

  • 了解您的业务领域/数据模型——无论您是要设计对象模型还是使用嵌套映射创建功能数据结构,都同样重要。在某些方面,FP 可能更容易,因为它鼓励您将数据模型与功能/流程分开考虑,但您仍然必须同时做这两件事。

  • 设计中的面向服务- 从 FP 的角度来看,实际上工作得很好,因为典型的服务实际上只是一个带有一些副作用的函数。我认为 Lisp 世界有时支持的软件开发“自下而上”的观点实际上只是另一种形式的面向服务的 API 设计原则。

  • 测试驱动开发——在 FP 语言中运行良好,实际上有时甚至更好,因为纯函数非常适合编写清晰、可重复的测试,而无需设置有状态的环境。您可能还想构建单独的测试来检查数据完整性(例如,此映射中是否包含我期望的所有键,以平衡在 OO 语言中类定义将在编译时为您强制执行这一事实)。

  • 原型设计/迭代- 与 FP 一样有效。如果您非常擅长构建工具/DSL 并在 REPL 中使用它们,您甚至可以与用户一起进行现场原型制作。

于 2011-01-31T15:27:02.280 回答
13

OO 编程将数据与行为紧密结合。函数式编程将两者分开。所以你没有类图,但你有数据结构,特别是代数数据类型。可以编写这些类型以非常紧密地匹配您的域,包括通过构造消除不可能的值。

所以没有关于它的书籍和书籍,但有一种成熟的方法,正如俗话所说,使不可能的价值无法代表。

这样做,您可以做出一系列选择,将某些类型的数据表示为函数,相反,将某些函数表示为数据类型的联合,以便您可以获得例如序列化、更严格的规范、优化等.

然后,鉴于此,您在 adts 上编写函数,以便建立某种代数- 即有适用于这些函数的固定定律。有些可能是幂等的——多次应用后相同。有些是关联的。有些是传递的,等等。

现在你有了一个域,在这个域上你有根据良好行为规律组成的函数。一个简单的嵌入式 DSL!

哦,给定属性,您当然可以编写它们的自动随机测试(ala QuickCheck).. 这只是开始。

于 2011-02-02T01:25:05.537 回答
7

面向对象设计与软件工程不同。软件工程与我们如何从需求到工作系统、准时和低缺陷率的整个过程有关。函数式编程可能与 OO 不同,但它并没有消除需求、高级和详细设计、验证和测试、软件度量、估计以及所有其他“软件工程内容”。

此外,功能程序确实表现出模块化和其他结构。您的详细设计必须根据该结构中的概念来表达。

于 2012-04-02T21:22:03.483 回答
5

一种方法是在所选的函数式编程语言中创建内部 DSL。“模型”是一组以 DSL 表示的业务规则。

于 2011-01-31T15:06:18.383 回答
5

请参阅我对另一篇文章的回答:

Clojure 如何处理关注点分离?

我同意关于如何构建使用 FP 方法的大型应用程序的主题需要编写更多内容(此外,还需要做更多工作来记录 FP 驱动的 UI)

于 2011-01-31T15:14:32.023 回答
3

虽然这可能被认为是幼稚和简单化的,但我认为“设计秘诀”(Felleisen 等人在他们的书HtDP中倡导的应用于编程的系统化解决问题的方法)将接近您似乎正在寻找的东西。

在这里,一些链接:

http://www.northeastern.edu/magazine/0301/programming.html

http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.86.8371

于 2011-02-01T16:46:49.600 回答
3

我最近发现了这本书: 函数式和反应式域建模

我认为完全符合你的问题。

从书中的描述:

函数式和反应式域建模教你如何从纯函数的角度来思考域模型,以及如何组合它们以构建更大的抽象。您将从函数式编程的基础开始,逐步学习实现复杂领域模型所需了解的高级概念和模式。这本书展示了高级 FP 模式(如代数数据类型、基于类型类的设计和副作用的隔离)如何使您的模型组合起来具有可读性和可验证性。

于 2015-11-04T17:11:19.777 回答
2

与 Richard Bird 教授和英国牛津大学编程代数小组相关的“程序计算”/“通过计算设计”风格,我认为将其视为一种方法论并不牵强。

就我个人而言,虽然我喜欢 AoP 小组制作的作品,但我自己并没有以这种方式实践设计的纪律。然而这是我的缺点,而不是程序计算之一。

于 2011-02-06T17:50:15.977 回答
2

我发现行为驱动开发非常适合在 Clojure 和 SBCL 中快速开发代码。将 BDD 与函数式语言结合使用的真正好处是,我倾向于编写比使用过程式语言时更细粒度的单元测试,因为我在将问题分解成更小的功能块方面做得更好。

于 2011-04-19T01:02:33.777 回答
1

老实说,如果您想要函数式程序的设计秘诀,请查看标准函数库,例如 Haskell 的 Prelude。在 FP 中,模式通常由高阶过程(对函数进行操作的函数)本身捕获。因此,如果看到一个模式,通常会简单地创建一个更高阶的函数来捕获该模式。

一个很好的例子是 fmap。此函数将函数作为参数并将其应用于第二个参数的所有“元素”。由于它是 Functor 类型类的一部分,因此 Functor 的任何实例(例如列表、图形等)都可以作为第二个参数传递给该函数。它捕获了将函数应用于其第二个参数的每个元素的一般行为。

于 2011-02-06T17:09:19.533 回答
-7

好,

一般很多函数式编程语言在大学里长期用于“小玩具问题”。

它们现在变得越来越流行,因为 OOP 由于“状态”而难以处理“并行编程”。有时函数式风格更适合手头的问题,例如 Google MapReduce。

我敢肯定,当功能强大的人碰壁时[尝试实现大于 1.000.000 行代码的系统],他们中的一些人会采用带有流行语的新软件工程方法:-)。他们应该回答一个老问题:如何将系统划分为多个部分,以便我们可以一次“咬”每个部分?[工作迭代,渐进式进化方式]使用功能样式。

可以肯定的是,函数式风格会影响我们的面向对象风格。我们“仍然”有许多来自函数式系统的概念,并适应了我们的 OOP 语言。

但是功能程序会被用于这么大的系统吗?它们会成为主流吗?这就是问题所在

如果没有实施如此庞大的系统,没有人可以提出现实的方法,使他的双手变脏。首先你应该弄脏你的手然后提出解决方案。解决方案——没有“真正的痛苦和污垢”的建议将是“幻想”。

于 2013-05-31T20:38:49.887 回答