粗略概述
在函数式编程中,函子本质上是一种将普通一元函数(即具有一个参数的函数)提升到新类型变量之间的函数的构造。在普通对象之间编写和维护简单的函数并使用函子来提升它们,然后在复杂的容器对象之间手动编写函数要容易得多。另一个优点是只编写一次普通函数,然后通过不同的函子重新使用它们。
仿函数的示例包括数组、“可能”和“任一”仿函数、期货(参见例如https://github.com/Avaq/Fluture)等等。
插图
考虑从名字和姓氏构造完整人名的函数。我们可以将它定义fullName(firstName, lastName)
为两个参数的函数,但是这不适用于只处理一个参数的函数的函子。为了补救,我们将所有参数收集在一个对象name
中,现在它成为函数的单个参数:
// In JavaScript notation
fullName = name => name.firstName + ' ' + name.lastName
现在,如果数组中有很多人怎么办?无需手动查看列表,我们可以通过为具有短单行代码的数组提供fullName
的方法简单地重用我们的函数:map
fullNameList = nameList => nameList.map(fullName)
并像使用它一样
nameList = [
{firstName: 'Steve', lastName: 'Jobs'},
{firstName: 'Bill', lastName: 'Gates'}
]
fullNames = fullNameList(nameList)
// => ['Steve Jobs', 'Bill Gates']
这将起作用,只要我们nameList
中的每个条目都是提供firstName
和lastName
属性的对象。但是如果有些对象没有(甚至根本不是对象)怎么办?为了避免错误并使代码更安全,我们可以将对象包装到Maybe
类型中(例如https://sanctuary.js.org/#maybe-type):
// function to test name for validity
isValidName = name =>
(typeof name === 'object')
&& (typeof name.firstName === 'string')
&& (typeof name.lastName === 'string')
// wrap into the Maybe type
maybeName = name =>
isValidName(name) ? Just(name) : Nothing()
whereJust(name)
是一个只携带有效名称的容器,Nothing()
是用于其他所有内容的特殊值。现在,无需中断(或忘记)检查参数的有效性,我们可以简单地使用fullName
另一行代码重用(提升)我们的原始函数,再次基于map
方法,这次为 Maybe 类型提供:
// Maybe Object -> Maybe String
maybeFullName = maybeName => maybeName.map(fullName)
并像使用它一样
justSteve = maybeName(
{firstName: 'Steve', lastName: 'Jobs'}
) // => Just({firstName: 'Steve', lastName: 'Jobs'})
notSteve = maybeName(
{lastName: 'SomeJobs'}
) // => Nothing()
steveFN = maybeFullName(justSteve)
// => Just('Steve Jobs')
notSteveFN = maybeFullName(notSteve)
// => Nothing()
范畴论
范畴论中的函子是两个范畴之间关于其态射组成的映射。在计算机语言中,感兴趣的主要类别是其对象是类型(某些值集)并且其态射是从一种类型到另一种类型的函数的类别。f:a->b
a
b
例如,取a
为String
类型,b
数字类型,f
为将字符串映射到其长度的函数:
// f :: String -> Number
f = str => str.length
这里a = String
表示所有字符串b = Number
的集合和所有数字的集合。从这个意义上说,两者都a
表示b
Set Category中的对象(与类型的类别密切相关,这里的区别无关紧要)。在集合范畴中,两个集合之间的态射正是从第一个集合到第二个集合的所有函数。所以我们这里的长度函数f
是从字符串集合到数字集合的态射。
由于我们只考虑集合范畴,从它到自身的相关函子是映射发送对象到对象和态射到态射,满足某些代数定律。
例子:Array
Array
可能意味着很多东西,但只有一件事是 Functor - 类型构造,将类型映射a
到 type[a]
的所有数组的类型a
。例如,Array
函子将类型映射String
到类型[String]
(所有任意长度的字符串数组的集合),并将类型映射Number
到相应的类型[Number]
(所有数字数组的集合)。
重要的是不要混淆 Functor 映射
Array :: a => [a]
带态射a -> [a]
。函子只是将类型映射(关联)a
到类型[a]
中,作为一个事物到另一个事物。每种类型实际上都是一组元素,在这里无关紧要。相反,态射是这些集合之间的实际函数。例如,有一个自然态射(函数)
pure :: a -> [a]
pure = x => [x]
它将一个值发送到 1 元素数组中,该值作为单个条目。该函数不是Array
Functor的一部分!从这个函子的角度来看,pure
它只是一个和其他函数一样的函数,没什么特别的。
另一方面,Array
函子有它的第二部分——态射部分。它将态射映射f :: a -> b
到态射[f] :: [a] -> [b]
:
// a -> [a]
Array.map(f) = arr => arr.map(f)
这里arr
是任意长度且值为 type 的任意数组a
,并且arr.map(f)
是具有相同长度且值为 type 的数组b
,其条目是应用于f
的条目的结果arr
。为了使它成为一个函子,映射恒等式到恒等式和组合映射到组合的数学定律必须成立,这在这个Array
例子中很容易检查。