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

244

我在阅读一些关于函数式编程的文章时,几次遇到了“Functor”这个术语,但作者通常假设读者已经理解了该术语。在网上搜索提供的是过度技术化的描述(参见维基百科文章)或者是极其模糊的描述(请参见此ocaml-tutorial网站下的Functors部分)。

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

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

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


4
我会尽力做到最好。下面是需要翻译的内容:参见:http://adit.io/posts/2013-04-17-functors,_applicatives,_and_monads_in_pictures.html - Vlad the Impala
还不错的答案:https://stackoverflow.com/a/45149475/1498178 - toraritte
如果你更关心实际的实现和使用,而不是概念背后天文数字般的术语和理论,那么你只需要一句话:函数对象公开了一个“映射”函数。 - Richard Gomes
@RichardGomes 我的想法是,将一个函数对象简化为一个类似于Java接口的东西降低了它的角色。函数对象可以转换数据,从现有的类型中创建新的类型(在Haskell中),这意味着类型也被映射了。 fmap 映射了函数。涉及到两种不同的映射方式,这种思考方式将有助于理解范畴论(更为一般化)。我的意思是,了解基本的范畴论有助于我们理解Haskell中的所有范畴论知识(如函数子、单子等),这很有趣。 - Ludovic Kuty
@VladtheImpala 这篇博客文章非常棒,尽管它有很大的帮助,但我仍然想记住一个函子构建(映射到)另一种类型。我特别喜欢在 单子就像玉米卷 中这句话 "函子 F 对每种类型 T 进行映射,并将其映射到新类型 FT"。在我看来,即使它证明在某些情况下实用(哈斯克尔 PoV vs 类别论 PoV?),它也不仅仅是一个值周围的上下文(盒子)。 - Ludovic Kuty
19个回答

289

“Functor”这个词来源于范畴论,是数学中非常普遍而抽象的一个分支。至少有两种不同的方式,被功能语言的设计者所借用。

  • In the ML family of languages, a functor is a module that takes one or more other modules as a parameter. It's considered an advanced feature, and most beginning programmers have difficulty with it.

    As an example of implementation and practical use, you could define your favorite form of balanced binary search tree once and for all as a functor, and it would take as a parameter a module that provides:

    • The type of key to be used in the binary tree

    • A total-ordering function on keys

    Once you've done this, you can use the same balanced binary tree implementation forever. (The type of value stored in the tree is usually left polymorphic—the tree doesn't need to look at values other than to copy them around, whereas the tree definitely needs to be able to compare keys, and it gets the comparison function from the functor's parameter.)

    Another application of ML functors is layered network protocols. The link is to a really terrific paper by the CMU Fox group; it shows how to use functors to build more complex protocol layers (like TCP) on type of simpler layers (like IP or even directly over Ethernet). Each layer is implemented as a functor that takes as a parameter the layer below it. The structure of the software actually reflects the way people think about the problem, as opposed to the layers existing only in the mind of the programmer. In 1994 when this work was published, it was a big deal.

    For a wild example of ML functors in action, you could see the paper ML Module Mania, which contains a publishable (i.e., scary) example of functors at work. For a brilliant, clear, pellucid explanation of the ML modules system (with comparisons to other kinds of modules), read the first few pages of Xavier Leroy's brilliant 1994 POPL paper Manifest Types, Modules, and Separate Compilation.

  • In Haskell, and in some related pure functional language, Functor is a type class. A type belongs to a type class (or more technically, the type "is an instance of" the type class) when the type provides certain operations with certain expected behavior. A type T can belong to class Functor if it has certain collection-like behavior:

    • The type T is parameterized over another type, which you should think of as the element type of the collection. The type of the full collection is then something like T Int, T String, T Bool, if you are containing integers, strings, or Booleans respectively. If the element type is unknown, it is written as a type parameter a, as in T a.

      Examples include lists (zero or more elements of type a), the Maybe type (zero or one elements of type a), sets of elements of type a, arrays of elements of type a, all kinds of search trees containing values of type a, and lots of others you can think of.

    • The other property that T has to satisfy is that if you have a function of type a -> b (a function on elements), then you have to be able to take that function and product a related function on collections. You do this with the operator fmap, which is shared by every type in the Functor type class. The operator is actually overloaded, so if you have a function even with type Int -> Bool, then

      fmap even
      

      is an overloaded function that can do many wonderful things:

      • Convert a list of integers to a list of Booleans

      • Convert a tree of integers to a tree of Booleans

      • Convert Nothing to Nothing and Just 7 to Just False

      In Haskell, this property is expressed by giving the type of fmap:

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

      where we now have a small t, which means "any type in the Functor class."

    To make a long story short, in Haskell a functor is a kind of collection for which if you are given a function on elements, fmap will give you back a function on collections. As you can imagine, this is an idea that can be widely reused, which is why it is blessed as part of Haskell's standard library.

像往常一样,人们不断发明新的有用的抽象概念,你可能需要了解一下可应用函子,最好的参考资料可能是Conor McBride和Ross Paterson撰写的论文《带效果的可应用编程》(Applicative Programming with Effects)


8
我了解ML-functor和Haskell-functor,但不清楚如何将它们联系起来。从范畴论的角度来看,这两者之间有什么关系? - Wei Hu
7
@Wei Hu: 范畴论从来没有让我感到有意义。最好的描述就是这三个概念都涉及到映射。 - Norman Ramsey
18
根据这个 Haskell wiki:http://en.wikibooks.org/wiki/Haskell/Category_theory,它是这样的:一个范畴是由对象和态射(函数)组成的集合,其中态射从范畴中的一个对象到该范畴中的其他对象。一个函子是一种将一个范畴中的对象和态射映射到另一个范畴中的对象和态射的函数。至少我是这样理解的。对于编程来说,它确切意味着什么我还没有理解。 - paul
5
@norman-ramsey,你看过Lawvere和Schanuel的《概念数学》吗?我对这个领域一窍不通,但这本书非常易读且非常有趣(敢说)。(喜欢你的解释。) - Ram Rajamony
3
Yes, I believe "produce" is the correct word to use in this context instead of "product". The sentence in Chinese would be: "然后,您必须能够将该函数转化为集合上的相关函数。" - problemofficer - n.f. Monica
显示剩余3条评论

71

这里的其他答案已经很全面,但我会尝试用另一种方式来解释FP中functor的用法。可以将其类比为:

一个functor是类型为a的容器,当被应用于从ab的映射函数时,会产生类型为b的容器。

与C++中抽象函数指针的用法不同,在这里functor并不是函数,而是在被应用于函数时表现一致的某种东西。


5
“类型为b的容器”意味着“与输入容器相同类型的容器,但现在填充了b”。因此,如果我们有一个香蕉列表,并且映射一个以香蕉为输入并输出水果沙拉的函数,那么我们现在就有了一个水果沙拉列表。同样,如果我们有一个香蕉树,并且映射相同的函数,我们现在会得到一个苹果树等等。“树”和“列表”都是这里的两个函子。 - Qqwy
3
"一个函子是一种容器类型,当它被应用于一个函数时" -- 实际上情况恰恰相反 -- 函数(态射)被应用于一个函子,以映射成另一个态射。 - Dmitri Zaitsev

40

有三个不太相关的意思!

  • In Ocaml it is a parametrized module. See manual. I think the best way to grok them is by example: (written quickly, might be buggy)

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

现在,您可以快速添加许多可能的订单,以形成新订单的方式,并轻松地对它们进行二进制或线性搜索。泛型编程厉害。

  • 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
    

所以,这是一种特殊类型的类型构造函数,与Ocaml中的functor无关!

  • 在命令式语言中,它是一个指向函数的指针。

1
我一直以为函数对象是容器,但这只是一个简单的错误概括。你的回答终于提供了缺失的链接:函数对象是参数化类型(类型构造函数)的类型类(类型约束)。就是这么简单! - user6445533

16

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

如果您了解C ++,可以将OCaml函数对象视为模板。C ++仅具有类模板,而函数对象可在模块范围内工作。

函数对象的一个示例是 Map.Make;module StringMap = Map.Make (String);; 构建了一个与字符串键映射一起使用的地图模块。

您无法仅通过多态来实现类似于 StringMap 的功能;您需要对键做出一些假设。String 模块包含关于全序字符串类型的操作(比较等),函数对象将连接到 String 模块包含的操作上。您可以使用面向对象编程进行类似的操作,但这会产生方法间接开销。


我从OCaml网站上得到了这个 - 但我不明白参数化模块的用途是什么。 - Erik Forbes
4
@Kornel 嗯,我所描述的是 OCaml 概念。另一个概念只是“函数值”,在 FP 中没有什么特别的。 @Erik 我稍微扩展了一下,但是参考文档加载速度很慢。 - Tobu

16
你得到了很多好的答案。我也来补充一下:
在数学意义上,函数子是代数范畴中的一种特殊函数。它是将一个代数映射到另一个代数的最小函数。"最小性"由函数子定律表示。
有两种方式来看待这个问题。例如,列表是某些类型的函数子。也就是说,给定一个类型为'a'的代数,你可以生成一个包含类型为'a'的元素的兼容列表代数。(例如:将元素映射到包含该元素的单例列表的映射:f(a)=[a])。同样,兼容性的概念由函数子定律表示。
另一方面,给定一个类型为'a'的代数的函数子f(也就是说,fa是应用函数子f到类型为'a'的代数的结果),以及一个从g:a->b的函数,我们可以计算一个新的函数子F=(fmap g),它将f a映射到f b。简而言之,fmap是将"函数子部分"映射到"函数子部分"的部分,g是将"代数部分"映射到"代数部分"的函数部分。它接受一个函数、一个函数子,并在完成后本身也是一个函数子。
不同的语言似乎在使用不同的函数子概念,但实际上它们并不是。它们仅仅是在不同的代数上使用函数子。OCaml有模块代数,而对于该代数的函数子让你以"兼容"的方式将新声明附加到一个模块中。
Haskell中的函数子不是类型类。它是一个带有自由变量的数据类型,满足类型类。如果你愿意深入到一个没有自由变量的数据类型的内部,你可以将一个数据类型重新解释为基础代数的函数子。例如:
data F=F Int

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

函子对于“以代数上的兼容方式”将事物与代数元素相连非常有用。


9

对于这个问题的最佳答案可以在Brent Yorgey的“Typeclassopedia”中找到。

Monad Reader杂志的这个问题包含了什么是函子的精确定义以及其他概念的许多定义和图表。(Monoid、Applicative、Monad和其他概念都被解释并与函子相关联)。

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

从Functor的Typeclassopedia摘录: “一个简单的直觉是,Functor表示某种‘容器’,以及能够统一地将函数应用于容器中的每个元素的能力。”
但实际上,整个Typeclassopedia都是一份强烈推荐的阅读材料,而且非常易懂。在某种程度上,您可以将在其中呈现的类型类视为与对象中的设计模式并行的东西,因为它们为给定的行为或能力提供了词汇表。
干杯

7

O'Reilly的OCaml书籍上有一个非常不错的例子,该书籍可在Inria网站上找到(很遗憾,在撰写本文时该网站无法访问)。我在Caltech使用的这本书中发现了一个非常相似的例子:Introduction to OCaml (pdf link)。相关章节是函数对象(functors)(本书第139页,PDF文档第149页)。

在此书中,他们创建了一个名为MakeSet的函数对象,用于创建一个数据结构,包含列表和添加元素、确定元素是否在列表中以及查找元素的函数。用于判断集合中元素是否存在的比较函数已被参数化(这使得MakeSet成为一个函数对象而不是模块)。

他们还编写了一个模块来实现比较函数,以便进行不区分大小写的字符串比较。

使用该函数对象和实现比较的模块,他们可以在一行代码中创建一个新模块:

module SSet = MakeSet(StringCaseEqual);;

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

Tobu将functors与C ++中的模板进行比较,我认为这很恰当。


6

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

关于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类型类子节和Functors、Applicative Functors和Monoids部分在Learn You a Haskell for Great Good中提供了一些有用的示例。 (总结:很多地方都可以使用!:-))
注意,任何单子都可以被视为函子,实际上,正如Craig Stuntz指出的那样,最常用的函子往往是单子... 另一方面,有时候方便将类型作为Functor typeclass的实例,而不必费力使其成为Monad。(例如,在Control.Applicative中提到的ZipList的情况下,参见之前提到的页面之一。)

6

“Functor是一个将对象和态射进行映射的函数,它保留了范畴的组合性和恒等性。”

那么我们先来定义一下什么是范畴?

它是一堆对象!

在一个圆圈里画几个点(现在只有两个点,一个是‘a’,另一个是‘b’),然后给这个圆圈起个名字叫做A(Category)。

那么范畴都包含哪些内容呢?

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

因此,在应用Functor之后,我们需要映射这些对象并保留其组合性。

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

现在,我们需要定义一个能够将这些对象和态射映射到另一个范畴'B'的Functor。

假设这个Functor叫做'Maybe'。

data Maybe a = Nothing | Just a

所以,类别'B'看起来像这样。

请再画一个圆圈,这次用'Maybe a'和'Maybe b'代替'a'和'b'。

一切似乎都很好,所有对象都被映射了。

'a'变成了'Maybe a','b'变成了'Maybe b'。

但问题是我们还必须映射从'a'到'b'的态射。

这意味着'A'中的态射a -> b应该映射到'Maybe a' -> 'Maybe b'的态射。

a -> b的态射称为f,则'Maybe a' -> 'Maybe b'的态射称为'fmap f'。

现在让我们看看函数'f'在'A'中是如何工作的,并看看我们是否可以在'B'中复制它。

'A'中'f'的函数定义:

f :: a -> b

f接受a并返回b

在'B'中,'f'的函数定义为:

f :: Maybe a -> Maybe b

f接受一个Maybe a类型的参数,并返回一个Maybe 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'。

这就是 Functor!

enter image description here


有趣的回答。我想用 单子就像玉米卷(对于抽象、直觉和“单子教程谬论”的有趣回答)来补充它,以及他的句子“一个函子F将每个类型T映射到一个新类型FT”,也就是一个类型构造器。函数式编程和范畴论 - 范畴和函子也很有用。 - Ludovic Kuty

5
在对排名最高的回答进行评论时,用户Wei Hu提出了以下问题:

我理解ML函子和Haskell函子,但缺乏将它们联系在一起的洞察力。从范畴论的角度来看,这两者之间有什么关系?

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

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

一个简明的答案是,“Haskell函子”是(自)函子F: Hask -> Hask,而“ML函子”是函子G:ML -> ML'

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

注意:有一些技术问题需要解决才能使Hask成为一个范畴,但有办法解决这些问题。

从范畴论的角度来看,这意味着Hask-函子是Haskell类型的映射F

data F a = ...

除此之外,还有一张 Haskell 函数的映射表 fmap

instance Functor F where
    fmap f = ...

机器学习基本上是相同的,不过我不知道是否有官方的fmap抽象概念,因此让我们定义一个:

signature FUNCTOR = sig
  type 'a f
  val fmap: 'a -> 'b -> 'a f -> 'b f
end

那就是fML类型映射,而fmapML函数映射,因此
functor StructB (StructA : SigA) :> FUNCTOR =
struct
  fmap g = ...
  ...
end

is a functor F: StructA -> StructB.


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接