在阅读各种关于函数式编程的文章时,我遇到过几次“Functor”这个术语,但作者通常认为读者已经理解了这个术语。在网络上你可以看到一些过于技术性的描述(参见维基百科的文章),也可以看到一些非常模糊的描述(参见ocaml-tutorial网站上关于函数函数的部分)。

有没有人可以定义这个术语,解释它的用法,或者提供一个如何创建和使用函子的例子?

编辑:虽然我对这个术语背后的理论很感兴趣,但我对这个概念的实现和实际应用更感兴趣,而不是理论。

编辑2:看起来好像有一些交叉术语:我特别指的是函数式编程的函子,而不是c++的函数对象。


当前回答

Functor与函数式编程没有特别的关系。它只是一个指向函数或某种对象的“指针”,可以像调用函数一样调用它。

其他回答

粗略的概述

在函数式编程中,函子本质上是将普通一元函数(即只有一个参数的函数)提升为新类型变量之间的函数的构造。在普通对象之间编写和维护简单函数并使用函子来提升它们要容易得多,而在复杂的容器对象之间手动编写函数要容易得多。进一步的好处是只编写一次普通函数,然后通过不同的函子重用它们。

函子的例子包括数组,“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示例中很容易检查。

你回答了不少不错的问题。我将加入:

函子,在数学意义上,是代数上一种特殊的函数。它是将一个代数映射到另一个代数的最小函数。“极简性”用函子定律来表示。

有两种方式来看待这个问题。例如,列表是某些类型的函子。也就是说,给定类型为“a”的代数,您可以生成包含类型为“a”的列表的兼容代数。(例如:将一个元素带到包含它的单元素列表的映射:f(a) = [a])同样,兼容性的概念是由函子定律表示的。

另一方面,鉴于函子f / a型,(也就是说,f是应用函子的结果f的代数a型),从g和功能:- > b,我们可以计算一个新的函子f = (fmap g)映射f a到f b。简而言之,fmap是f的一部分映射“函子零件”“函子零件”,和g函数的一部分,“代数”映射到“代数部分”。它接受一个函数,一个函子,一旦完成,它也是一个函子。

看起来不同的语言使用不同的函子概念,但事实并非如此。它们只是在不同的代数上使用函子。OCamls有一个模块代数,这个代数上的函子允许您以一种“兼容”的方式将新声明附加到模块。

Haskell函子不是类型类。它是一个具有满足类型类的自由变量的数据类型。如果您愿意深入挖掘数据类型的精髓(没有自由变量),您可以通过底层代数将数据类型重新解释为函子。例如:

数据F = F Int

是整型类的同构。F,作为一个值构造函数,是一个将Int映射到F Int的函数,一个等价的代数。它是一个函子。另一方面,这里的fmap不是免费的。这就是模式匹配的作用。

函子很适合以一种代数相容的方式将事物“附加”到代数元素上。

实际上,functor是指在c++中实现调用操作符的对象。在ocaml中,我认为函子指的是将一个模块作为输入并输出另一个模块的东西。

函子是对象和态射的映射,它保留了一个类别的组成和身份。

让我们定义什么是类别?

是一堆东西! 在a内部画几个点(现在是两个点,一个是“a”,另一个是“b”) 圆圈,并命名为圆圈A(类别)。

这个类别包含什么?

对象之间的组合和每个对象的恒等函数。

因此,在应用Functor之后,我们必须映射对象并保存组合。

让我们想象‘A’是我们的范畴,它有对象[' A', 'b'],并且存在一个态射A -> b

现在,我们必须定义一个函子,它可以将这些对象和态射映射到另一个类别“B”。

假设这个函子叫做Maybe

data Maybe a = Nothing | Just a

B类是这样的。

请再画一个圆,但这次用“也许a”和“也许b”代替“a”和“b”。

一切看起来都很好,所有的对象都被映射了

“a”变成了“也许a”,“b”变成了“也许b”。

但问题是我们也要把态射从a映射到b。

这意味着' a'中的态态a -> b应该映射到'可能a' -> '可能b'

来自a -> b的形态称为f,然后来自'Maybe a' -> 'Maybe b'的形态称为'fmap f'

现在让我们看看函数f在A中做了什么,看看我们能否在B中复制它

A中f的函数定义:

f :: a -> b

F取a并返回b

f在B中的函数定义:

f :: Maybe a -> Maybe b

f取也许a,返回也许b

让我们看看如何使用fmap将函数'f'从'A'映射到'B'中的函数'fmap f'

fmap的定义

fmap :: (a -> b) -> (Maybe a -> Maybe b)
fmap f Nothing = Nothing
fmap f (Just x) = Just(f x)

那么,我们在这里做什么?

我们将函数“f”应用于类型为“a”的“x”。“Nothing”的特殊模式匹配来自于Functor Maybe的定义。

因此,我们将对象[a, b]和形态[f]从类别' a '映射到类别' b '。

那是富克托!

这是一篇关于函子的编程文章,后面更具体地介绍了它们是如何在编程语言中出现的。

函子的实际使用是在单子中,如果你想找的话,你可以找到很多关于单子的教程。