在阅读有关函数式编程的各种文章时,我偶然遇到过“Functor”这个术语,但作者通常认为读者已经理解了这个术语。在网上浏览提供了过多的技术说明(请参阅Wikipedia article)或令人难以置信的含糊描述(请参阅此ocaml-tutorial website上有关Functors的部分)。
有人可以友好地定义术语,解释它的用法,并提供一个如何创建和使用Functors的例子吗?
编辑:虽然我对这个术语背后的理论很感兴趣,但我对这个理论的兴趣不如我对这个概念的实施和实际应用。
编辑2 :看起来有一些交叉的术语:我特别指的是函数式编程的函数,而不是C ++的函数对象。
答案 0 :(得分:261)
“functor”这个词来自于类别理论,它是一个非常通用的,非常抽象的数学分支。功能语言的设计者至少以两种不同的方式借用它。
在ML系列语言中,仿函数是一个将一个或多个其他模块作为参数的模块。它被认为是一种高级功能,大多数初级程序员都很难用它。
作为实现和实际使用的一个例子,您可以一次性定义您最喜欢的平衡二叉搜索树形式作为仿函数,它将作为参数提供一个模块:
二叉树中使用的密钥类型
按键上的总排序功能
完成此操作后,您可以永久使用相同的平衡二叉树实现。 (存储在树中的值的类型通常是多态的 - 树不需要查看除了复制它们之外的值,而树肯定需要能够比较键,它从中获取比较函数仿函数的参数。)
ML仿函数的另一个应用是layered network protocols。这个链接是CMU Fox集团的一篇非常棒的论文;它展示了如何使用仿函数在更简单的层(如IP或甚至直接在以太网上)上构建更复杂的协议层(如TCP)。每个图层都实现为一个仿函数,它将下面的图层作为参数。软件的结构实际上反映了人们对问题的思考方式,而不是仅存在于程序员心中的层。 1994年这项工作发表时,这是一个大问题。
对于ML仿函数的一个疯狂的例子,你可以看到论文ML Module Mania,其中包含一个可发布的(即可怕的)仿函数的例子。要想对ML模块系统进行精彩,清晰,透明的解释(与其他类型的模块进行比较),请阅读Xavier Leroy出色的1994年POPL论文的前几页Manifest Types, Modules, and Separate Compilation。
在Haskell和一些相关的纯函数式语言中,Functor
是类型类。类型属于类型类(或者在技术上,类型“是类型类的实例”,当类型提供具有某些预期行为的某些操作时。如果类型T
具有某些类似集合的行为,则Functor
类型可以属于类T
:
类型T Int
参数化为另一种类型,您应该将其视为集合的元素类型。如果您分别包含整数,字符串或布尔值,则完整集合的类型类似于T String
,T Bool
,a
。如果元素类型未知,则将其写为类型参数 T a
,如a
中所示。
示例包括列表(类型为Maybe
的零个或多个元素),a
类型(类型为a
的零个或一个元素),类型为{{1}的元素集},类型a
的元素数组,包含类型a
的值的各种搜索树,以及许多您能想到的其他搜索树。
T
必须满足的另一个属性是,如果你有一个a -> b
类型的函数(元素上的函数),那么你必须能够获取该函数,产品相关的集合功能。您使用运算符fmap
执行此操作,该运算符由Functor
类型类中的每个类型共享。操作符实际上已经过载,因此如果您的函数even
的类型为Int -> Bool
,那么
fmap even
是一个重载函数,可以做很多很棒的事情:
将整数列表转换为布尔值列表
将整数树转换为布尔树
将Nothing
转换为Nothing
,将Just 7
转换为Just False
在Haskell中,通过给出fmap
:
fmap :: (Functor t) => (a -> b) -> t a -> t b
我们现在有一个小t
,这意味着“Functor
类中的任何类型。”
总而言之,在Haskell 中,仿函数是一种集合,如果给你一个元素函数,fmap
会给你一个关于集合的函数 。正如您可以想象的那样,这是一个可以被广泛重用的想法,这就是为什么它作为Haskell标准库的一部分而受到祝福的原因。
像往常一样,人们继续发明新的,有用的抽象,你可能想要研究 applicative 仿函数,最好的参考可能是Conor McBride的一篇名为Applicative Programming with Effects的论文。和罗斯帕特森。
答案 1 :(得分:58)
此处的其他答案已完成,但我会尝试另外解释仿函数的FP使用情况。以此类推:
仿函数是 a 类型的容器,当受到映射 a → b 的函数时,会生成一个容器输入 b 。
与C ++中使用的抽象函数指针不同,这里的函子不函数;相反,当经历一个函数时,它会表现一致。
答案 2 :(得分:37)
有三种不同的含义,没有多大关系!
在Ocaml中,它是一个参数化模块。见manual。我认为最好的方法是通过示例:(写得很快,可能是错误的)
module type Order = sig
type t
val compare: t -> t -> bool
end;;
module Integers = struct
type t = int
let compare x y = x > y
end;;
module ReverseOrder = functor (X: Order) -> struct
type t = X.t
let compare x y = X.compare y x
end;;
(* We can order reversely *)
module K = ReverseOrder (Integers);;
Integers.compare 3 4;; (* this is false *)
K.compare 3 4;; (* this is true *)
module LexicographicOrder = functor (X: Order) ->
functor (Y: Order) -> struct
type t = X.t * Y.t
let compare (a,b) (c,d) = if X.compare a c then true
else if X.compare c a then false
else Y.compare b d
end;;
(* compare lexicographically *)
module X = LexicographicOrder (Integers) (Integers);;
X.compare (2,3) (4,5);;
module LinearSearch = functor (X: Order) -> struct
type t = X.t array
let find x k = 0 (* some boring code *)
end;;
module BinarySearch = functor (X: Order) -> struct
type t = X.t array
let find x k = 0 (* some boring code *)
end;;
(* linear search over arrays of integers *)
module LS = LinearSearch (Integers);;
LS.find [|1;2;3] 2;;
(* binary search over arrays of pairs of integers,
sorted lexicographically *)
module BS = BinarySearch (LexicographicOrder (Integers) (Integers));;
BS.find [|(2,3);(4,5)|] (2,3);;
您现在可以快速添加许多可能的订单,形成新订单的方式,轻松地对它们进行二元或线性搜索。通用编程FTW。
在像Haskell这样的函数式编程语言中,它意味着可以“映射”的一些类型构造函数(像列表,集合这样的参数化类型)。确切地说,仿函数f
配备了(a -> b) -> (f a -> f b)
。这起源于范畴论。您链接到的维基百科文章就是这种用法。
class Functor f where
fmap :: (a -> b) -> (f a -> f b)
instance Functor [] where -- lists are a functor
fmap = map
instance Functor Maybe where -- Maybe is option in Haskell
fmap f (Just x) = Just (f x)
fmap f Nothing = Nothing
fmap (+1) [2,3,4] -- this is [3,4,5]
fmap (+1) (Just 5) -- this is Just 6
fmap (+1) Nothing -- this is Nothing
所以,这是一种特殊的类型构造函数,与Ocaml中的仿函数几乎没有关系!
答案 3 :(得分:15)
在OCaml中,它是一个参数化模块。
如果您了解C ++,请将OCaml仿函数视为模板。 C ++只有类模板,而仿函数在模块规模上工作。
仿函数的一个例子是Map.Make; module StringMap = Map.Make (String);;
构建一个使用字符串键控映射的映射模块。
你只能使用多态来实现像StringMap这样的东西;你需要对键做一些假设。 String模块包含完全有序的字符串类型的操作(比较等),而functor将链接String模块包含的操作。你可以用面向对象的编程做类似的事情,但你有方法间接开销。
答案 4 :(得分:13)
你有很多好的答案。我会投入:
在数学意义上,算子是代数上的一种特殊函数。它是一个将代数映射到另一个代数的最小函数。 “最小化”由函子法则表达。
有两种方法可以看这个。例如,列表是某种类型的仿函数。也就是说,给定类型'a'上的代数,您可以生成包含类型'a'的列表的兼容代数。 (例如:将元素带到包含它的单例列表的映射:f(a)= [a])同样,兼容性的概念由函子定律表示。
另一方面,给定一个类型为“a”的仿函数,(即,f a是将仿函数f应用于类型a的代数的结果),并且函数来自g:a - > b,我们可以计算一个新的函子F =(fmap g),它将f a映射到f b。简而言之,fmap是F的一部分,它将“functor parts”映射到“functor parts”,而g是将“代数部分”映射到“代数部分”的函数的一部分。它需要一个函数,一个函子,一旦完成,它也是一个函子。
似乎不同的语言使用不同的仿函数概念,但它们不是。他们只是在不同的代数上使用函子。 OCamls有代数模块,而代数上的函子允许你以“兼容”的方式将新的声明附加到模块。
Haskell仿函数不是类型类。它是一个带有自由变量的数据类型,它满足类型类。如果您愿意深入研究数据类型的内容(没有自由变量),您可以将数据类型重新解释为基础代数上的仿函数。例如:
数据F = F Int
与Ints类同构。因此,作为值构造函数,F是一个将Int映射到F Int(一个等效代数)的函数。它是一个算符。另一方面,你没有在这里免费获得fmap。这就是模式匹配的目的。
Functors很适合以代数兼容的方式将事物“附加”到代数元素上。
答案 5 :(得分:7)
在O'Reilly OCaml的一本书中有一个非常好的例子,该书出现在Inria的网站上(不幸的是,写这篇文章的时候很遗憾)。我在caltech使用的这本书中找到了一个非常相似的例子:Introduction to OCaml (pdf link)。相关章节是关于仿函数的章节(本书第139页,PDF中的第149页)。
在书中,他们有一个名为MakeSet的仿函数,它创建一个由列表组成的数据结构,以及添加元素,确定元素是否在列表中以及查找元素的函数。用于确定它是否在集合中的比较函数已被参数化(这使得MakeSet成为仿函数而不是模块)。
它们还有一个实现比较功能的模块,以便它进行不区分大小写的字符串比较。
使用仿函数和实现比较的模块,他们可以在一行中创建一个新模块:
module SSet = MakeSet(StringCaseEqual);;
为使用不区分大小写的比较的集合数据结构创建模块。如果您想创建一个使用区分大小写比较的集合,那么您只需要实现一个新的比较模块而不是新的数据结构模块。
Tobu将仿函数与C ++中的模板进行了比较,我认为这非常合适。
答案 6 :(得分:7)
这个问题的最佳答案可以在Brent Yorgey的“Typeclassopedia”中找到。
本期Monad Reader包含对仿函数的精确定义以及其他概念的许多定义以及图表。 (Monoid,Applicative,Monad和其他概念在仿函数中得到解释和解释)。
http://haskell.org/sitewiki/images/8/85/TMR-Issue13.pdf
摘自Typeclassopedia for Functor: “一个简单的直觉是,Functor代表了某些人的”容器“ 排序,以及将函数统一应用于函数中的每个元素的能力 容器“
但实际上整个类词库都是一个非常容易推荐的强烈推荐的阅读。在某种程度上,您可以看到在那里呈现的类型类与对象中的设计模式并行,因为它们为您提供了给定行为或功能的词汇表。
干杯
答案 7 :(得分:5)
这是一个article on functors from a programming POV,后面跟着更具体的how they surface in programming languages。
仿函数的实际用法是monad,如果你想看的话,你可以在monad上找到很多教程。
答案 8 :(得分:5)
我理解ML-functors和Haskell-functors,但缺乏 将他们联系在一起的见解。这些之间的关系是什么 二,在类别 - 理论意义上?
注意:我不了解ML,所以请原谅并纠正任何相关错误。
我们最初假设我们都熟悉'类别'的定义。和'仿函数'。
紧凑的答案是" Haskell-functors"是(endo-)仿函数F : Hask -> Hask
而#34; ML-functors"是仿函数G : ML -> ML'
。
此处,Hask
是由Haskell类型和函数在它们之间形成的类别,同样ML
和ML'
是由ML结构定义的类别。
注意:有一些technical issues使Hask
成为一个类别,但有很多方法。
从类别理论的角度来看,这意味着Hask
- 仿函数是Haskell类型的映射F
:
data F a = ...
以及Haskell函数的映射fmap
:
instance Functor F where
fmap f = ...
ML几乎是一样的,虽然我没有注意到规范的fmap
抽象,所以让我们定义一个:
signature FUNCTOR = sig
type 'a f
val fmap: 'a -> 'b -> 'a f -> 'b f
end
那是f
地图ML
- 类型和fmap
地图ML
- 功能,所以
functor StructB (StructA : SigA) :> FUNCTOR =
struct
fmap g = ...
...
end
是一个仿函数F: StructA -> StructB
。
答案 9 :(得分:5)
鉴于其他答案以及我现在要发布的内容,我会说这是一个相当沉重的词,但不管怎样......
有关Haskell中“functor”一词含义的提示,请询问GHCi:
Prelude> :info Functor
class Functor f where
fmap :: forall a b. (a -> b) -> f a -> f b
(GHC.Base.<$) :: forall a b. a -> f b -> f a
-- Defined in GHC.Base
instance Functor Maybe -- Defined in Data.Maybe
instance Functor [] -- Defined in GHC.Base
instance Functor IO -- Defined in GHC.Base
所以,基本上,Haskell中的仿函数是可以映射的东西。另一种说法是,仿函数可以被视为一个容器,可以被要求使用给定的函数来转换它包含的值;因此,对于列表,fmap
与map
,Maybe
,fmap f (Just x) = Just (f x)
,fmap f Nothing = Nothing
等等重合。
The Functor typeclass小节和Functors, Applicative Functors and Monoids Learn You a Haskell for Great Good部分提供了一些有关此特定概念有用的示例。 (摘要:很多地方!: - ))
请注意,任何monad都可以被视为仿函数,事实上,正如Craig Stuntz所指出的那样,最常用的仿函数往往是monad ... OTOH,有时候使一个类型成为一个实例很方便Functor类型类没有麻烦使它成为Monad。 (例如ZipList
来自Control.Applicative
,one of the aforementioned pages上提及。)
答案 10 :(得分:4)
&#34; Functor是对象和态射的映射,它保留了一个类别的构成和身份。&#34;
让我们定义什么是类别?
它是一堆物品!
在一个点内画一些点(现在是2个点,一个是&#39;另一个是&#39; b&#39;) 现在圈出A(类别)的圈子和名称。
该类别包含哪些内容?
对象和每个对象的Identity函数之间的组合。
因此,我们必须在应用我们的Functor后映射对象并保留合成。
让我们想象一下&#39; A&#39;是我们的类别,它有对象[&#39; a&#39;,&#39; b&#39;]并且存在态射a - &gt; B'/ P>
现在,我们必须定义一个仿函数,它可以将这些对象和态射映射到另一个类别&#39; B&#39;
让我们说这个仿函数叫做“可能&#39;
”data Maybe a = Nothing | Just a
所以,类别&#39; B&#39;看起来像这样。
请绘制另一个圈子,但这一次是&#39;也许是&#39;并且&#39;也许b&#39;而不是&#39;和&#39; b&#39;。
一切似乎都很好,所有对象都已映射
&#39;一个&#39;变成了'也许是'&#39;和&#39; b&#39;变成了'也许b&#39;。
但问题是我们必须将“态射”映射到“&#39; a&#39;到&#39;&#39;同样。
这意味着态射a - &gt; b在&#39; A&#39;应该映射到态射&#39;也许是一个&#39; - &GT; &#39;也许b&#39;
来自a - &gt;的态射b被称为f,然后来自&#39;也许a&#39; - &GT; &#39;也许b&#39;被称为&#39; fmap f&#39;
现在让我们来看看功能&#39; f&#39;正在做的&#39; A&#39;并看看我们是否可以在&#39; B&#39;
中复制它&#39; f&#39;的功能定义在&#39; A&#39;:
f :: a -> b
f取a并返回b
&#39; f&#39;的功能定义在&#39; B&#39; :
f :: Maybe a -> Maybe b
f采取可能a并返回Maybe b
让我们看看如何使用fmap来映射函数&#39; f&#39;来自&#39; A&#39;使f&#39; fmap f&#39;在&#39; B&#39;
fmap的定义
fmap :: (a -> b) -> (Maybe a -> Maybe b)
fmap f Nothing = Nothing
fmap f (Just x) = Just(f x)
那么,我们在这做什么?
我们正在应用函数&#39; f&#39;到&#39; x&#39;属于&#39; a&#39;。特殊模式匹配&#39; Nothing&#39;来自Functor Maybe
的定义。
因此,我们将对象[a,b]和态射[f]从类别&#39; A&#39;中映射出来。到类别&#39; B&#39;。
Thats Functor!
答案 11 :(得分:2)
不要与之前的理论或数学答案相矛盾,但Functor也是一个Object(面向对象的编程语言),它只有一个方法,并且有效地用作函数。
一个例子是Java中的Runnable接口,它只有一个方法:run。
考虑这个例子,首先在Javascript中,它具有一流的功能:
[1, 2, 5, 10].map(function(x) { return x*x; });
输出: [1,4,25,100]
map方法接受一个函数并返回一个新数组,每个元素都是该函数应用于原始数组中相同位置的值的结果。
为了做同样的事情是Java,使用Functor,你首先需要定义一个接口,比如说:
public interface IntMapFunction {
public int f(int x);
}
然后,如果你添加一个具有map功能的集合类,你可以这样做:
myCollection.map(new IntMapFunction() { public int f(int x) { return x * x; } });
这使用IntMapFunction的内联子类来创建一个Functor,它是早期JavaScript示例中函数的OO等价物。
使用Functors,您可以使用OO语言应用功能技巧。当然,一些OO语言也直接支持函数,因此不需要这样做。
答案 12 :(得分:2)
在函数式编程中,仿函数本质上是将普通的一元函数(即带有一个参数的函数)提升到新类型变量之间的函数的构造。在普通对象之间编写和维护简单的函数并使用函子来提升它们,然后在复杂的容器对象之间手动编写函数要容易得多。进一步的优点是只编写一次普通函数,然后通过不同的函子重复使用它们。
仿函数的例子包括数组,&#34;可能&#34; &#34;要么&#34;仿函数,期货(参见例如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()
其中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()
f:a->b
类型到另一种类型a
的函数b
。
例如,将a
作为String
类型,b
作为数字类型,f
是将字符串映射到其长度的函数:
// f :: String -> Number
f = str => str.length
此处a = String
表示所有字符串的集合,b = Number
表示所有数字的集合。从这个意义上讲,a
和b
都代表集合类别中的对象(与类别类别密切相关,差别在这里是不必要的)。在“设定类别”中,两组之间的态射恰好是从第一组到第二组的所有功能。所以我们的长度函数f
在这里是从字符串集到数字集的态射。
由于我们只考虑集合类别,因此相关的 Functors 本身就是将对象发送到对象的映射,以及态射到态射的映射,它们满足某些代数定律。
Array
Array
可能意味着许多事情,但只有一件事是Functor - 类型构造,将类型a
映射到类型{{1的所有数组的类型[a]
中}}。例如,a
仿函数将类型Array
映射到类型String
(任意长度的所有字符串数组的集合),并将类型[String]
设置为相应的键入Number
(所有数字数组的集合)。
重要的是不要混淆Functor地图
[Number]
具有态射Array :: a => [a]
。仿函数只是将类型a -> [a]
映射(关联)到类型a
中,作为另一个事物。每种类型实际上是一组元素,与此无关。相反,态射是这些集合之间的实际函数。例如,有一种自然的态射(函数)
[a]
将值作为单个条目发送到1元素数组中。该功能不是pure :: a -> [a]
pure = x => [x]
Functor的一部分!从这个仿函数的角度来看,Array
只是一个函数,没有什么特别的。
另一方面,pure
Functor的第二部分 - 态射部分。这将态射Array
映射到态射f :: a -> b
:
[f] :: [a] -> [b]
此处// a -> [a]
Array.map(f) = arr => arr.map(f)
是任意长度的数组,其值为arr
,a
是长度相同的数组,其值为arr.map(f)
,其条目是将b
应用于f
条目的结果。为了使它成为一个仿函数,必须保持将身份与身份和成分映射到成分的数学定律,这在arr
示例中很容易检查。
答案 13 :(得分:0)
JavaScript中的数组实现了map,因此是functor。 Promises,Streams和Trees经常用函数式语言实现map,当它们这样做时,它们被认为是functor。仿函数的map方法使用它自己的内容,并使用传递给map的转换回调转换它们中的每一个,并返回一个新的仿函数,它包含作为第一个仿函数的结构,但具有转换后的值。
src:https://www.youtube.com/watch?v=DisD9ftUyCk&feature=youtu.be&t=76
答案 14 :(得分:-4)
实际上,functor是指在C ++中实现调用操作符的对象。在ocaml中,我认为仿函数指的是将模块作为输入并输出另一个模块的东西。
答案 15 :(得分:-6)
简而言之,函子或函数对象是一个可以像函数一样调用的类对象。
在C ++中:
这就是你编写函数的方法
void foo()
{
cout << "Hello, world! I'm a function!";
}
这就是你编写仿函数的方法
class FunctorClass
{
public:
void operator ()
{
cout << "Hello, world! I'm a functor!";
}
};
现在你可以这样做:
foo(); //result: Hello, World! I'm a function!
FunctorClass bar;
bar(); //result: Hello, World! I'm a functor!
这些如此伟大的原因是你可以在课堂上保持状态 - 想象一下你是否想要问一个函数被调用了多少次。没有办法以整洁,封装的方式做到这一点。对于一个函数对象,它就像任何其他类一样:你有一些在operator ()
中递增的实例变量以及一些检查该变量的方法,而且一切都很整齐,随你而去。
答案 16 :(得分:-10)
Functor与函数式编程没有特别的关系。它只是一个函数或某种对象的“指针”,可以被称为函数。