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

在阅读各种关于函数式编程的文章时,我遇到过几次“Functor”这个术语,但作者通常认为读者已经理解了这个术语。在网络上寻找,提供的要么是过度的技术描述(参见维基百科的文章),要么是难以置信的模糊描述(参见this ocaml-tutorial网站的Functors部分)。

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

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

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

41591 次浏览

在OCaml中,它是一个参数化模块。

如果您了解c++,可以将OCaml函子视为模板。c++只有类模板,函数子在模块规模上工作。

函数子的一个例子是Map.Make;module StringMap = Map.Make (String);;构建了一个映射模块,它使用string键映射。

你不能通过多态性实现StringMap这样的东西;你需要对这些键做一些假设。String模块包含对完全有序字符串类型的操作(比较等),函子将链接到String模块包含的操作。你可以用面向对象编程做一些类似的事情,但是你会有方法间接开销。

这里是关于函子的文章来自编程POV,后面跟着更具体的它们如何在编程语言中出现

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

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

简单地说,函子或函数对象是可以像函数一样调用的类对象。

在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!

这样做的好处是可以将state保存在类中——想象一下,如果您想询问一个函数被调用了多少次。没有办法以简洁、封装的方式做到这一点。对于函数对象,它就像任何其他类一样:你有一些实例变量,你在operator ()中增加,还有一些方法来检查这个变量,一切都很整齐。

在Inria网站上的O'Reilly OCaml书中有一个很好的例子(不幸的是,在写这篇文章时,它被删除了)。我在这本书中发现了加州理工学院使用的一个非常类似的例子:OCaml简介(pdf链接)。相关的部分是关于函子的章节(书中139页,PDF中149页)。

在书中,他们有一个名为MakeSet的函子,它创建了一个由列表组成的数据结构,以及添加元素、确定元素是否在列表中以及查找元素的函数。用于确定它是否在集合中的比较函数已被参数化(这是使MakeSet成为函子而不是模块的原因)。

它们还有一个实现比较函数的模块,这样就可以进行不区分大小写的字符串比较。

使用函子函数和实现比较的模块,它们可以在一行中创建一个新模块:

module SSet = MakeSet(StringCaseEqual);;

这将为使用不区分大小写比较的一组数据结构创建一个模块。如果您想创建一个使用区分大小写比较的集合,那么您只需要实现一个新的比较模块,而不是一个新的数据结构模块。

Tobu将函子与c++中的模板进行了比较,我认为这是非常恰当的。

考虑到其他的答案和我现在要发布的内容,我想说这是一个相当沉重的重载词,但无论如何……

关于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中的函子是可以被映射的。另一种说法是,函子是可以被视为容器的东西,它可以被要求使用给定的函数来转换它所包含的值;因此,对于列表,fmapmap重合,对于Maybefmap f (Just x) = Just (f x)fmap f Nothing = Nothing等。

Functor类型类小节和为了伟大的利益,学习哈斯克尔函子,应用函子和Monoids小节给出了这个特定概念有用的一些例子。(总结一下:很多地方!: -))

请注意,任何单子都可以被视为函子,事实上,正如Craig Stuntz所指出的,最常用的函子往往是单子……对了,有时使一个类型成为Functor类型类的实例是很方便的,而不需要麻烦地使它成为一个单子。(例如,在上面提到的页面之一中提到的ZipList来自Control.Applicative的情况。)

有三种不同的意思,没有太大的联系!

  • 在Ocaml中,它是一个参数化模块。看到手册。我认为最好的方法是通过例子来理解它们:(写得很快,可能会有bug)

    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);;
    

You can now add quickly many possible orders, ways to form new orders, do a binary or linear search easily over them. Generic programming FTW.

  • In functional programming languages like Haskell, it means some type constructors (parametrized types like lists, sets) that can be "mapped". To be precise, a functor f is equipped with (a -> b) -> (f a -> f b). This has origins in category theory. The Wikipedia article you linked to is this usage.

    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
    

So, this is a special kind of a type constructors, and has little to do with functors in Ocaml!

  • In imperative languages, it is a pointer to function.

“函子”这个词来自范畴论,范畴论是数学中一个非常普遍、非常抽象的分支。函数式语言的设计者至少以两种不同的方式借用了它。

  • 在ML语言家族中,函子是一个以一个或多个其他模块作为参数的模块。它被认为是一种高级功能,大多数初级程序员使用它都有困难。

    作为实现和实际使用的例子,你可以一劳永逸地将你最喜欢的平衡二叉搜索树形式定义为一个函子,它将以一个模块作为参数,提供:

    • 二叉树中使用的键的类型

    • 键的总排序功能

    一旦你做到了这一点,你就可以永远使用相同的平衡二叉树实现。(树中存储的值类型通常是多态的;树不需要查看值而只是复制它们,然而树肯定需要能够比较键,并且它从函子的形参中获得比较函数。)

    ML函子的另一个应用是分层网络协议。链接是CMU Fox小组的一篇很棒的论文;它展示了如何使用函子在更简单的层(如IP甚至直接通过以太网)上构建更复杂的协议层(如TCP)。每一层都被实现为一个函子,该函子以它下面的层作为参数。软件的结构实际上反映了人们思考问题的方式,而不是只存在于程序员头脑中的层次。1994年,当这本书出版时,轰动一时。

    对于ML函子在工作中的一个疯狂的例子,你可以看到论文ML模块狂热,其中包含一个可发表的(即可怕的)函子工作的例子。要了解ML模块系统的精彩、清晰、清晰的解释(并与其他类型的模块进行比较),请阅读Xavier Leroy在1994年发表的精彩的POPL论文清单类型、模块和单独编译的前几页

  • 在Haskell和一些相关的纯函数式语言中,Functor是一个类型的类。当类型提供具有特定预期行为的特定操作时,类型就属于类型类(或者更严格地说,类型“是”类型类的实例)。类型T可以属于类Functor,如果它具有某些类似于集合的行为:

    • T类型被参数化在另一个类型之上,你应该认为它是集合的元素类型。如果你分别包含整数、字符串或布尔值,则完整集合的类型类似T IntT StringT Bool。如果元素类型未知,则将其写入类型参数 a,如T a

      例子包括列表(0个或多个a类型的元素),Maybe类型(0个或一个a类型的元素),a类型的元素集,a类型的元素数组,各种包含a类型值的搜索树,以及许多你能想到的其他类型

    • T必须满足的另一个属性是,如果你有一个类型为a -> b的函数(一个元素上的函数),那么你必须能够取这个函数并在集合上积一个相关的函数。你可以使用操作符fmap来实现这一点,该操作符由Functor类型类中的每个类型共享。操作符实际上是重载的,所以如果你有一个even类型为Int -> Bool的函数,那么

      fmap even
      

      是一个重载函数,它可以做很多奇妙的事情:

      • 将整数列表转换为布尔列表

      • 将整数树转换为布尔树

      • Nothing转换为NothingJust 7转换为Just False

      在Haskell中,此属性通过给出fmap类型来表示:

      fmap :: (Functor t) => (a -> b) -> t a -> t b
      

      这里我们现在有一个小的t,意思是“Functor类中的任何类型”。< / p >

    长话短说,在Haskell 函子是一种集合,如果给你一个关于元素的函数,fmap将返回一个关于集合的函数中。可以想象,这是一个可以广泛重用的思想,这就是为什么它被列为Haskell标准库的一部分

像往常一样,人们继续发明新的、有用的抽象,你可能想看看可适用的函子,最好的参考可能是Conor McBride和Ross Paterson的论文应用程序设计与效果

这里的其他答案是完整的,但我将尝试对FP使用函子的另一种解释。我们来做个类比:

函子是类型为一个的容器,当赋给一个从一个b的映射函数时,生成类型为b的容器。

与c++中使用的抽象函数指针不同,这里的函子是函数;相反,它是在受到一个函数的影响

这个问题的最佳答案在布伦特·约吉(Brent Yorgey)的《type eclassopedia》中找到。

这一期的单子阅读器包含了一个精确的定义,什么是一个函子以及许多其他概念的定义以及一个图表。(Monoid, Applicative, Monad和其他概念被解释和看到与函子的关系)。

http://haskell.org/sitewiki/images/8/85/TMR-Issue13.pdf

摘录自Typeclassopedia for Functor: 一个简单的直觉是,一个Functor代表一个容器 类中的每个元素统一应用函数的能力 容器”< / p >

但实际上,所有类型的类目都是强烈推荐的,因为它们出奇地简单。在某种程度上,你可以看到这里的类型类与object中的设计模式是平行的,因为它们为给定的行为或能力提供了词汇表。

干杯

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

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

有两种方式来看待这个问题。例如,列表是某些类型的函子。也就是说,给定类型为“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不是免费的。这就是模式匹配的作用。

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

在对投票最多的回答的评论中,用户魏胡问道:

我理解ml -函子和haskell -函子,但缺乏 将它们联系在一起的洞察力。这两者之间是什么关系 二,在分类理论的意义上?< / p >

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

让我们首先假设我们都熟悉“范畴”和“函子”的定义。

一个紧凑的答案是“Haskell-functors”是(endo-)functors F : Hask -> Hask,而“ML-functors”是functors G : ML -> ML'

这里,Hask是由Haskell类型和它们之间的函数组成的类别,类似地,MLML'是由ML结构定义的类别。

请注意:有一些将Hask作为一个类别的技术问题,但有一些方法可以绕过它们。

从范畴论的角度来看,这意味着Hask-functor是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-types和fmap映射ML-functions

functor StructB (StructA : SigA) :> FUNCTOR =
struct
fmap g = ...
...
end

是一个函子F: StructA -> StructB

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

不是为了与前面的理论或数学答案相矛盾,但Functor也是一个对象(在面向对象编程语言中),它只有一个方法,并且可以有效地用作函数。

Java中的Runnable接口就是一个例子,它只有一个方法:run。

考虑这个例子,首先在Javascript中,它有一级函数:

[1, 2, 5, 10].map(function(x) { return x*x; });
< p >输出: [1,4,25, 100]

map方法接受一个函数并返回一个新数组,其中每个元素都是将该函数应用于原始数组中相同位置的值的结果。

要在Java中使用Functor做同样的事情,你首先需要定义一个接口,比如:

public interface IntMapFunction {
public int f(int x);
}

然后,如果你添加一个集合类,它有一个映射函数,你可以这样做:

myCollection.map(new IntMapFunction() { public int f(int x) { return x * x; } });

这使用了IntMapFunction的一个内嵌子类来创建一个Functor,它是前面JavaScript示例中的函数的OO等效。

使用函子可以在OO语言中应用函数式技术。当然,一些OO语言也直接支持函数,所以这不是必需的。

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

函数子是一个具有映射方法的对象。

JavaScript中的数组实现了map,因此是函子。承诺、流和树通常在函数式语言中实现map,当它们这样做时,它们被认为是函子。函子的map方法获取它自己的内容,并使用传递给map的转换回调对它们进行转换,并返回一个新的函子,该函子包含作为第一个函子的结构,但带有转换后的值。

src: https://www.youtube.com/watch?v=DisD9ftUyCk&feature=youtu.be&t=76

粗略的概述

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

函子的例子包括数组、“maybe”和“either”函子、未来函子(参见例如https://github.com/Avaq/Fluture)以及许多其他函子。

插图

考虑从姓和名构造完整人名的函数。我们可以像fullName(firstName, lastName)那样将它定义为两个参数的函数,但这不适用于只处理一个参数的函数的函子。为了补救,我们将所有实参收集到单个对象name中,现在它成为函数的单个实参:

// 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中的每个条目都是同时提供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()

范畴论

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

例如,设aString类型,b为Number类型,而f是将字符串映射到其长度的函数:

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

这里a = String表示所有字符串的集合,b = Number表示所有数字的集合。从这个意义上说,ab都表示设置类别中的对象(这与类型的类别密切相关,这里的区别是不必要的)。在集合类别中,两个集合之间的恰好是从第一个集合到第二个集合的所有函数。所以这里的长度函数f是一个从字符串集到数字集的态射。

因为我们只考虑集合类别,所以从它到自身的相关是发送对象到对象和morphisms到morphisms的映射,它们满足特定的代数法则。

例如:Array

Array可以有很多含义,但只有一种是Functor——类型构造,将类型a映射到所有类型a的数组的类型[a]。例如,Array函子将类型String映射为类型[String](任意长度字符串数组的集合),并将类型Number映射为相应的类型[Number](所有数字数组的集合)。

重要的是不要混淆Functor映射

Array :: a => [a]

与一个morphism a -> [a]。函数子只是将类型a映射(关联)到类型[a]。每种类型实际上是一组元素,这在这里无关紧要。相反,态射是这些集合之间的实际函数。例如,有一个自然形态(函数)

pure :: a -> [a]
pure = x => [x]

它将一个值发送到一个元素数组,并将该值作为单个项。该函数是,是Array函子的一部分!从这个函子的角度来看,pure只是一个像其他函数一样的函数,没有什么特别的。

另一方面,Array函子有它的第二部分——形态部分。它将一个态射f :: a -> b映射到一个态射[f] :: [a] -> [b]:

// a -> [a]
Array.map(f) = arr => arr.map(f)

这里arr是任意长度的数组,值类型为a,而arr.map(f)是相同长度的数组,值类型为b,其条目是将f应用于arr的条目的结果。要使它成为函子,必须遵守单位到单位和组合到组合的映射数学法则,这在Array例子中很容易检查。

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

让我们定义什么是类别?

是一堆东西!

在a里面画几个点(现在是两个点,一个是“a”,另一个是“b”) 现在将圆圈命名为A(Category)。

这个类别包含什么?

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

因此,在应用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 '。

这函子!

enter image description here

在函数式编程中,错误处理是不同的。抛出和捕获异常是命令式代码。不是使用try/catch块,而是在可能抛出错误的代码周围创建一个安全框。这是函数式编程中的基本设计模式。包装器对象用于封装可能错误的值。包装器的主要目的是提供一种使用被包装对象的“不同”方式

 const wrap = (val) => new Wrapper(val);

包装保护了对值的直接访问,以便可以对它们进行操作 安全而不可改变。因为我们不能直接访问它,所以提取它的唯一方法是使用identity function.

identity :: (a) -> a

这是恒等函数的另一个用例:从封装的类型中功能地提取数据。

enter image description here

Wrapper类型使用映射来安全地访问和操作值。在本例中,我们将恒等函数映射到容器上,以从容器中提取值。使用这种方法,可以在调用函数之前检查是否为null,或者检查是否为空字符串、负数等等。

 fmap :: (A -> B) -> Wrapper[A] -> Wrapper[B]

Fmap,首先打开容器,然后将给定函数应用于它的值,最后将值关闭到相同类型的新容器中。这种类型的函数称为functor

  • fmap在每次调用时返回容器的新副本。

  • 函子是无副作用的

  • 函子必须是可组合的