什么是函数组合??在回答你之前,让我们看一下组合的定义:
把东西放在一起或排列的方式:组成某物的部件或元素的组合。
----Merriam Webster (www.merriam-webster.com/dictionary/composition)
当你进行组合时候,你将各部分组合成一个整体。在数学中,把一个函数应用到另一个函数中来产生第三个函数是很明智的。很明显,这是创造力的含义----你创造了一个新的功能,它结合了两个功能的作用。举例子来说,两个函数f:X-
>
Y和g:Y-
>
Z可以组合在一起,得到一个新的函数,它将每一个X中的x映射到g(f(x))的Z上。
现在,让我们把这个例子转换成函数式编程的领域。假设您有一个函数,square:Int-
>
Int,它将一个整数作为参数,并产生另一个整数,它的值做平方操作并作为输出。然后你还有另一个函数,add2:Int-
>
Int,它获取一个Integer的入参,并对其进行加2操作。现在你对这连个函数进行组合操作,形成了add2(square(x:Int))这样的组合函数,它会对我们当入参传入这个组合函数的Integer值进行平方操作后,再执行加2操作。
这是认识到函数式编程是基于函数组合的第一步,这与我们在数学中处理函数的方式类似。这被称为函数程序的compositionality property(复合性属性)。
问1.4:假设你有两个函数:f:String->Int和g:Int->Int.你该如何组合函数f和g呢??你能想到一些满足这些签名的真实函数吗?
使用复合性的属性,您可以从较小的函数中构建更大的函数。这本书的主要主题之一是探索各种可以使函数组合在一起的不同的方法。我们将使用Scala,它提供了使这种组合易于实现的功能。对于Scala函数式编程的详细处理,请参考由Paul Chiusano和Runar Bjarnason编著的优秀书籍: Functional Programming in Scala(2014年出版)。
您将使用Scala REPL来查看在Scala中编写函数的示例,这是与Scala解释器进行交互的环境。但是首先,小问答的主人已经准备好了回答之前的问题。
答1.4:以g(f(x:String))的方式进行函数的组合。一个实际的例子是把f函数当作一个计算字符串长度的函数,g函数则作为一个队输入的Integer值进行双倍操作的函数。因此, double(length(x: String))就是一个实际的例子,它将两个函数组合起来,返回的是输入字符串的长度的两倍。
在你们已经熟悉了函数构成的基本技巧,现在是采取下一步行动的时候了。到目前为止,在讨论组合时,我已经将单个的函数连接到一起,其中一个函数接收到另一个函数产生的输出作为它的输入。但是,当我们在函数式编程中讨论复合性属性时,它远远不止于此。让我们看一下图1.7中的示例.
函数map有两个入参----一个String的列表和另一个函数,length: String -> Int。map遍历这个列表,并对每一个元素应用length这个函数。最终生成的结果是另一个列表啦,而这个结果列表的每个元素都是应用length函数的结果,这是一个整数列表。这是一个很好的例子,说明了如何从函数的方式思考,并指出了这个范式的一些有趣的特性,如表1.2所示。像map这样的高阶函数也被称为combinators(组合器)。
**Table表 1.2 **
map函数的特性 | 它与函数式编程有什么关系 |
---|---|
你可以将函数作为一个参数传递来。在我们的例子中,map函数接受了一个叫做length的函数。 | 函数是头等函数。(以scala来解释的话,我们不仅可以定义和定义函数,而且可以将其写成匿名的字面量,并将其作为值进行传递) |
map是一个函数,它将另一个函数作为输入 | map是一个高阶函数 |
map函数遍历字符串列表,但是循环是从API用户中抽象出来的。 | 通过函数式编程,您可以告诉函数该做什么。如何将它从API用户中抽象出来。您还可以对其他类型的序列进行map(不仅仅是一个列表),而且迭代是由map实现处理的 |
问1.5:如果传递给map的函数也会更新一个共享的可变状态,会发生什么呢?这是否意味着多次迭代一个列表将导致不同的输出?
下面的清单中的代码使用了高阶的函数,比如Scala中的map,演示了几种组合方式。每个例子都遵循表1.2中所示的函数思想的指导原则。
现在是时候使用这些组合来丰富我们的 Balance领域模型了。在复杂的领域建模中,您将自己定义许多组合器。但是,标准库附带的那些程序是非常有用的,而且你经常会发现自己在构建自己的程序时,又会重新回到它们当中。毕竟,它是你所追求的复合性。
答1.5:当涉及到像map和其他的组合时,不要违反纯洁性。你传递给map的函数必须是没有任何副作用或突变的。我们很快就会讲到副作用。
现在,让我们尝试在我们的个人银行系统中实现一些领域行为。假设您想要为交易(如借记卡和信用)添加审计功能,从而生成审计日志,并将它们写到某个地方。在示例中省略了细节,它们对于理解手边的概念并不重要。假设您有以下两个函数:
>generateAuditLog: (Account, Amount) => Try[String]
>write: String => Unit
这将是一个具有命令式编程模型的简单练习。但这里的想法是使用函数组合和高阶函数来实现相同的结果。下面的清单演示了实现。
需求是定义一个执行以下操作顺序的函数:
>
从一个账户取款
>
如果取款操作通过了,那么生成审计日志;否则,停止
>
日志写入存储区
您需要使用函数式编程提供的组合器的组合性来实现这个顺序序列。理想情况下,您应该将您的领域行为建模的与上述工作流的序列是一致的。由于您到目前为止所做的函数式思考以及之前讨论的组合器,清单1.8提供了对这一逻辑的忠实描述。
下面的序列描述了清单1.8中的操作流程。如果您像我一样喜欢以图表的形式查看这些交互,请查看图1.8。
1.对于debit的调用将会产生一个Failure(在异常状态下)或者是由于修改Account成功而产生Successful。
2.一旦失败了,整个序列都会被破坏而结束掉。这里没有对失败的明确检查。所有这些样板文件都隐藏在map组合的实现背后。
3.如果debit操作生成了一个成功的的Account,那么这个值就会被嵌入flatmap组合器,并传递一个 generateAuditLog给它。
4.generateAuditLog又是一个纯函数,它会生成一个字符串作为日志行,而这个字符串最终也会被送给foreach。如果生成日志行出错了,那么你就会停止,序列也会被破坏掉。
5.foreach是用于副作用操作的组合器。管道的最后一个阶段是将日志记录写入数据库或文件系统,这必然是一个副作用。您可以使用foreach来实现它。
本节的主要内容是了解如何通过组合器组合函数,从而使领域行为得到丰富。将较小的组合器组合起来产生更大的行为和函数思维,这两方面是实现这一目标的方法。在下一节中,您将了解函数式思维如何帮助你推理代码的功能,就像数学中的函数一样。