在函数式编程中,什么是函子?

时间:2010-01-08 21:26:58

标签: functional-programming ocaml functor

在阅读有关函数式编程的各种文章时,我偶然遇到过“Functor”这个术语,但作者通常认为读者已经理解了这个术语。在网上浏览提供了过多的技术说明(请参阅Wikipedia article)或令人难以置信的含糊描述(请参阅此ocaml-tutorial website上有关Functors的部分)。

有人可以友好地定义术语,解释它的用法,并提供一个如何创建和使用Functors的例子吗?

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

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

17 个答案:

答案 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 StringT Boola。如果元素类型未知,则将其写为类型参数 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)

在对排名靠前的answer的评论中,用户Wei Hu会问:

  

我理解ML-functors和Haskell-functors,但缺乏   将他们联系在一起的见解。这些之间的关系是什么   二,在类别 - 理论意义上?

注意:我不了解ML,所以请原谅并纠正任何相关错误。

我们最初假设我们都熟悉'类别'的定义。和'仿函数'。

紧凑的答案是" Haskell-functors"是(endo-)仿函数F : Hask -> Hask而#34; ML-functors"是仿函数G : ML -> ML'

此处,Hask是由Haskell类型和函数在它们之间形成的类别,同样MLML'是由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中的仿函数是可以映射的东西。另一种说法是,仿函数可以被视为一个容器,可以被要求使用给定的函数来转换它包含的值;因此,对于列表,fmapmapMaybefmap 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.Applicativeone 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!

enter image description here

答案 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语言也直接支持函数,因此不需要这样做。

参考:http://en.wikipedia.org/wiki/Function_object

答案 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中的每个条目都是同时提供firstNamelastName属性的对象,那么这将有效。但是,如果某些物体不是(甚至根本不是物体)呢?为避免错误并使代码更安全,我们可以将对象包装到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()

类别理论

类别理论中的 Functor 是两个类别之间关于其态射构成的映射。在计算机语言中,主要感兴趣的类别是对象类型(某些值集),其态射是从f:a->b类型到另一种类型a的函数b

例如,将a作为String类型,b作为数字类型,f是将字符串映射到其长度的函数:

// f :: String -> Number
f = str => str.length

此处a = String表示所有字符串的集合,b = Number表示所有数字的集合。从这个意义上讲,ab都代表集合类别中的对象(与类别类别密切相关,差别在这里是不必要的)。在“设定类别”中,两组之间的态射恰好是从第一组到第二组的所有功能。所以我们的长度函数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) 是任意长度的数组,其值为arra是长度相同的数组,其值为arr.map(f),其条目是将b应用于f条目的结果。为了使它成为一个仿函数,必须保持将身份与身份和成分映射到成分的数学定律,这在arr示例中很容易检查。

答案 13 :(得分:0)

KISS:仿函数是一个具有map方法的对象。

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与函数式编程没有特别的关系。它只是一个函数或某种对象的“指针”,可以被称为函数。