粗略的概述
在函数式编程中,函子本质上是将普通一元函数(即只有一个参数的函数)提升为新类型变量之间的函数的构造。在普通对象之间编写和维护简单函数并使用函子来提升它们要容易得多,而在复杂的容器对象之间手动编写函数要容易得多。进一步的好处是只编写一次普通函数,然后通过不同的函子重用它们。
函子的例子包括数组,“maybe”和“either”函子,期货(参见例如https://github.com/Avaq/Fluture),以及许多其他函子。
插图
考虑从姓和名构造完整人名的函数。我们可以像fullName(firstName, lastName)那样将其定义为两个参数的函数,但这不适用于只处理一个参数的函数的函子。为了补救,我们将所有参数收集到一个对象名称中,现在它成为了函数的单个参数:
// In JavaScript notation
fullName = name => name.firstName + ' ' + name.lastName
如果数组中有很多人呢?不需要手动遍历列表,我们可以通过为数组提供的map方法重用函数fullName,该方法只有短短的一行代码:
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()
其中Just(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()
范畴论
范畴论中的函子是两个范畴之间关于它们的态射组成的映射。在计算机语言中,主要感兴趣的范畴是其对象是类型(某些值集),其形态是从一种类型a到另一种类型b的函数f:a->b。
例如,设a为String类型,b为Number类型,f为将字符串映射到其长度的函数:
// f :: String -> Number
f = str => str.length
这里a = String表示所有字符串的集合,b = Number表示所有数字的集合。在这种意义上,a和b都表示集合类别中的对象(这与类型类别密切相关,在这里没有区别)。在集合范畴中,两个集合之间的态射正是从第一个集合到第二个集合的所有函数。所以这里的长度函数f是从字符串集合到数字集合的态射。
由于我们只考虑集合范畴,从它到它自身的相关函子是满足某些代数定律的映射,将对象发送到对象,将态射发送到态射。
例如:数组
数组可以有很多含义,但只有一种是Functor——类型构造,将类型a映射到类型a的所有数组的类型[a]。例如,Array Functor将类型String映射到类型[String](所有任意长度的字符串数组的集合),并将类型Number映射到相应的类型[Number](所有数字数组的集合)。
重要的是不要混淆Functor映射
Array :: a => [a]
a -> [a]。函数子只是将类型a映射(关联)到类型[a],作为一种东西到另一种东西。每种类型实际上是一组元素,这在这里无关紧要。相反,态射是这些集合之间的实际函数。例如,有一个自然形态(函数)
pure :: a -> [a]
pure = x => [x]
它将一个值发送到一个元素数组,并将该值作为单个项。该函数不是数组函子的一部分!从这个函子的角度来看,纯函数和其他函数一样,没什么特别的。
另一方面,Array Functor有它的第二部分——形态部分。将一个态射f:: a -> b映射为一个态射[f]:: [a] -> [b]:
// a -> [a]
Array.map(f) = arr => arr.map(f)
这里arr是任意长度的数组,值类型为a, arr.map(f)是相同长度的数组,值类型为b,它的项是对arr的项应用f的结果。要使它成为函子,必须遵守单位到单位、组合到组合的映射数学定律,在这个Array示例中很容易检查。