本质上,仿函数只不过是一种数据结构,您可以将函数映射到其上,目的是从容器中提取值、修改它们,然后将它们放回容器中。简单地说,它是一种设计模式,定义了 fmap 应该如何工作的语义。下面是 fmap 的一般定义:
fmap :: (A -> B) -> Wrapper(A) -> Wrapper(B)
函数 fmap 接受一个函数(从 A -> B)和一个函子(包装上下文)Wrapper(A) 并返回一个新的函子 Wrapper(B),其中包含将所述函数应用于值的结果,然后再次关闭它。这是一个使用增量函数作为我们从 A -> B 的映射函数的快速示例(除了在这种情况下 A 和 B 是相同的类型):
图 1 值 1 包含在容器 W 中,使用所述包装器和增量函数调用仿函数,它在内部转换值并将其关闭回容器中。
请注意,因为 fmap 在每次调用时基本上都会返回容器的一个新副本,所以它可以被认为是不可变的。
函子理论
关于函子的讨论很容易变得非常正式和理论化。如果你在网上快速搜索函子,你会发现一些文章会用诸如:态射和范畴之类的术语轰炸你。这样做的原因是,像所有函数式编程技术一样,函子起源于数学——在本例中是范畴论。
在不深入杂草的情况下,我可以解释这个的基本含义。函子被定义为:“类别之间的态射”。所有这一切的真正含义是,仿函数是定义 (fmap) 行为的实体,给定一个值和函数(态射),将所述函数映射到特定类型(类别)的值并生成一个新的仿函数。
确实,这个理解有点理论化。让我们看一个非常简单的例子。考虑使用仿函数的简单 2 + 3 = 5 加法。我可以柯里化一个简单的 add 函数来创建一个 plus3 函数:
fmap :: (A -> B) -> Wrapper(A) -> Wrapper(B)
现在我将把数字 2 存储到一个简单的 Wrapper 仿函数中:
fmap :: (A -> B) -> Wrapper(A) -> Wrapper(B)
调用 fmap 将 plus3 映射到容器上执行加法:
fmap :: (A -> B) -> Wrapper(A) -> Wrapper(B)
fmap 的结果产生了另一个相同类型的上下文,我可以将 R.identity 映射到它上面以提取它的值。请注意,因为值永远不会脱离包装器,所以我可以将任意多的函数映射到它上面,并在每一步转换它的值:
fmap :: (A -> B) -> Wrapper(A) -> Wrapper(B)
这可能很难理解,所以下面是 fmap 如何再次与此图中的 plus3 一起工作的视觉效果:
图 2 值 2 已添加到 Wrapper 容器中。仿函数用于操作这个值,首先将它从上下文中解包,将给定的函数应用到它上面,然后将值重新包装回新的上下文中。
让 fmap 返回相同类型(或将结果再次包装到容器中)的目的是让我们可以继续链接操作。考虑以下示例,该示例将 plus 映射到包装值并记录结果,如以下代码所示:
fmap :: (A -> B) -> Wrapper(A) -> Wrapper(B)
运行此代码会在控制台上打印以下消息:
信息记录器 [信息] 5
这种链接功能的想法听起来很熟悉吗?实际上,您一直在使用函子而没有意识到这一点。这正是 map 和 filter 函数对数组所做的:
fmap :: (A -> B) -> Wrapper(A) -> Wrapper(B)
函数 map 和 filter 是“类别之间的同态”。原因是两个函数都保留相同的类型:
-
同性恋 :相同
-
态射 :维持结构的函数
-
category : 包含的值的类型
将这个概念扩展到函数中,考虑您一直看到的另一种类型的同态仿函数:组合。您可能知道,compose 函数是从函数到其他函数的映射:
fmap :: (A -> B) -> Wrapper(A) -> Wrapper(B)
与任何其他函数式编程工件一样,仿函数由一些重要属性控制:
它们必须没有副作用:映射 R.identity 函数可用于在上下文中获取相同的值。这证明它们没有副作用并且保留了包装值的结构。
fmap :: (A -> B) -> Wrapper(A) -> Wrapper(B)
它们必须是可组合的:此属性表示应用于 fmap 的函数的组合应该与将 fmap 函数链接在一起完全相同。结果,下面的表达式与前面的程序完全等价:
fmap :: (A -> B) -> Wrapper(A) -> Wrapper(B)
仿函数等结构被禁止抛出异常、改变列表中的元素或改变函数的行为。它们的实际目的是创建一个上下文,使您可以安全地操纵值并将操作应用于值,而无需更改原始值。 map 在不改变原始数组的情况下将一个数组转换为另一个数组的方式很明显;这个概念同样适用于任何容器类型。
然而,函子本身并不太引人注目,并且会在空数据存在时失败,就像有效地跳过空元素和组合的数组映射函子一样,它将跳过调用空函数对象。这类似于用一个空的 catch 块来忽略失败。然而,在实践中,您需要正确处理错误,为此您需要一种名为 Monads 的新功能数据类型。您可以在我的《JavaScript 函数式编程》一书中了解有关仿函数和 Monad 的更多信息。
要了解有关函数式编程的更多信息,请下载 DZone 的 Luis Atencio 撰写的使用 JavaScript 进行函数式编程 Refcard 。