Haskell 中的函数和仿函数有什么区别?只有定义?

What's the difference between a function and a functor in Haskell? Only definition?

在Haskell中,当写一个函数时,意味着我们将某物(输入)映射到另一物(输出)。我试过 LYAH 来理解 Functor 的定义:看起来和普通的 Functor 一样。

  1. 函数可以称为 Functor 有什么限制吗?
  2. Functor 是否允许具有 I/O 或任何其他副作用?
  3. 如果在Haskell、"everthing is a function"中,那么引入"Functor"概念有什么意义呢?功能的限制版本,还是功能的增强版本?

很迷茫,求指教。 谢谢

首先,Haskell中的"everything is a function"是不正确的。很多东西都不是函数,比如 4。或者字符串 "vik santata".

在Haskell中,函数是将一些输入映射到输出的东西。函数是一个值,您可以将其应用于其他一些值以获得结果。如果一个值在其类型中有一个 ->,那么它很可能是一个函数(但这个经验法则有无数个例​​外 ;-))。

以下是函数的一些示例(引用自 GHCI 会话):

λ: :t fst
fst :: (a, b) -> a
λ: :t even
even :: Integral a => a -> Bool

这里有一些不是函数的例子:

  • 一个(多态)值,可以采用任何类型 a,前提是该类型是 Num class 的成员(例如 Int 将是一个有效的类型)。确切的值将从数字的使用方式中推断出来。

    请注意,此类型中包含 =>,这与 -> 完全不同。它表示一个 "class constraint".

    λ: :t 5
    5 :: Num a => a
    
  • 函数列表。请注意,它的类型中有一个 ->,但它不是顶级类型构造函数(顶级类型是 [],即 "list"):

    λ: :t [fst, snd]
    [fst, snd] :: [(a, a) -> a]
    

仿函数 不是可以应用于值的东西。函子是其值可以与 fmap 函数一起使用(并由其返回)的类型(前提是 fmap 函数符合某些规则,通常称为 'laws')。您可以使用 GHCI 找到属于 Functor 的基本类型列表:

λ: :i Functor
[...]
instance Functor (Either a) -- Defined in ‘Data.Either’
instance Functor [] -- Defined in ‘GHC.Base’
instance Functor Maybe -- Defined in ‘GHC.Base’
[...]

这意味着您可以将 fmap 应用于列表、Maybe 值或 Either 值。

其实一个functor两个函数,但只有一个是Haskell函数(而且我不确定这是你怀疑的功能)。

  1. 一个类型级别的函数Hask 类别的 objects 是种类 * 的类型,函子将此类类型映射到其他类型。您可以在 ghci 中看到仿函数的这一方面,使用 :kind 查询:

    Prelude> :k Maybe
    Maybe :: * -> *
    Prelude> :k []
    [] :: * -> *
    Prelude> :k IO
    IO :: * -> *
    

    这些函数的作用相当无聊:它们映射,例如,

    • IntMaybe Int
    • ()IO ()
    • String[[Char]].

    我的意思并不是说它们将 整数映射到可能是整数 等等——那是 a more specific operation,并不是每个函子都可以。我的意思是,他们将 type Int 作为一个实体映射到 Maybe Int.

  2. 类型
  3. 一个值级函数,它将态射(即Haskell函数)映射到态射。目标态射总是在将类型级函数应用到原始函数的域和余域所产生的类型之间映射。
    这个函数是你用 fmap:

    得到的
    • fmap :: (Int -> Double) -> (Maybe Int -> Maybe Double)
    • fmap :: (() -> Bool) -> (IO () -> IO Bool)
    • fmap :: (Char -> String) -> String -> [String].

要使某个东西成为函子 - 你需要两件事:

  • 容器类型*
  • 一个特殊函数,将一个函数从containees转换为一个函数converting container

第一个取决于您自己的定义,但第二个已在名为 Functor 的 "interface" 中编码,转换函数已命名为 fmap.

因此你总是从

这样的东西开始
data Maybe a = Just a | Nothing

instance Functor Maybe where
    -- fmap :: (a -> b) -> Maybe a -> Maybe b
    fmap f (Just a) = Just (f a)
    fmap _ Nothing = Nothing

另一方面,函数 - 不需要容器来工作 - 所以它们与 Functor 没有那种关系。另一方面,每个 Functor 都必须实现函数 fmap 才能得名。

此外,公约要求 Functor 遵守某些法律 - 但这不能由 compiler/type 检查器强制执行。

*:这个容器也可以是幻影类型,例如data Proxy a = Proxy 在这种情况下,名称 container 值得商榷,但我仍会使用该名称

  1. 并非 Haskell 中的所有内容都是函数。非函数包括 "Hello World"(3 :: Int, 'a')Just 'x'。类型包括 => 的事物也不一定是函数,尽管 GHC(通常)将它们转换为中间表示中的函数。

什么是仿函数?给定类别 C 和 D,从 C 到 D 的函子 f 由从 C 的对象到 D 的对象的映射 fo 和从 C 的态射到 D 的态射的映射 fm 组成

  1. 如果 x 和 y 是 C 中的对象并且 p 是从 x 到 y 的态射,那么 fm(p) 是从 fo(x) 到 fo(y) 的态射。
  2. 如果 x 是 C 中的一个对象,id 是从 x 到 x 的恒等态射,那么 fm(id) 是从 fo(x) 到 fo(x) 的恒等态射。
  3. 如果x、y、z是C中的对象,p是y到z的态射,q是x到y的态射,则fm(p . q) = fm(p).fm (q),其中点代表态射组合。

这与 Haskell 有什么关系?我们喜欢将 Haskell 类型和它们之间的 Haskell 函数视为一个类别。由于各种原因,这只是大致正确,但它是直觉的有用指南。然后 Functor class 表示从这个 Hask 类别到它自己的单射内函子。特别是,Functor 包含从类型到类型的映射(特别是类型构造函数或类型构造函数的部分应用),以及从函数到函数的映射(fmap 函数),它遵循凌驾于法律之上。

了解一点范畴论会很有帮助。类别只是一组对象,它们之间有箭头。他们可以对数学中的许多事物进行建模,但出于我们的目的,我们对类型的范畴感兴趣; Hask是Haskell类型的范畴,每个类型都是Hask中的一个对象,每个函数是参数类型之间的箭头和 return 类型。例如,IntChar[Char]Bool都是Haskord :: Char -> Int中的对象, odd :: Int -> Boolrepeat :: Char -> [Char]Hask.

中箭头的一些示例

每个类别都有几个属性:

  1. 每个对象都有一个标识箭头。

  2. 箭头组成,所以如果a -> bb -> c是箭头,那么a -> c也是。

  3. 恒等式箭头是左右恒等式组合。

  4. 组合是关联的。

之所以Hask是一个范畴,是因为每一个类型都有一个恒等函数,函数组合。也就是说,id :: Int -> Intid :: Char -> Char 是类别的恒等箭头,odd . ord :: Char -> Bool 是组合箭头。

(暂时忽略我们认为 id 是类型为 a -> a 的多态函数,而不是一堆具有具体类型的独立函数。这证明了范畴论中的一个概念,称为 你现在不需要考虑的自然变换。)


在范畴论中,函子F是两个范畴之间的映射;它将一个类别的每个对象映射到另一个类别的对象,并且将一个类别的每个箭头映射到另一个类别的箭头。如果a是一个类别中的对象,我们说F a 是另一个类别中的对象。我们还说,如果f是第一类中的箭头,则对应的是另一类中的箭头if F f.

不仅仅是任何映射都是仿函数。它必须遵循两个看起来应该很熟悉的属性。

  1. F 必须将对象 a 的标识箭头映射到对象 F a 的标识箭头。
  2. F 必须保留构图。这意味着第一类中两个箭头的组合必须映射到另一类中相应箭头的组合。也就是说,如果 h = g ∘ f 在第一个类别中,则 h 映射到另一个类别中的 F h = F g ∘ F f

最后,endofunctor 是将一个类别映射到 自身 的函子的特殊名称。在 Hask 中,类型类 Functor 捕获了从 HaskHask 的内函子的概念.类型构造函数本身映射类型,fmap用于映射箭头。


我们以Maybe为例。类型构造函数Maybe是一个endofuntor,因为它将Hask(类型)中的对象映射到Hask(其他类型)中的其他对象. (这一点有点模糊,因为我们没有目标类型的新名称,因此可以将 Maybe 视为 Int 到类型 Maybe Int 的映射。)

为了将箭头 a -> b 映射到 Maybe a -> Maybe b,我们在 Maybe Int 的实例中提供了 fmap 的定义。 Maybe 也映射函数,但使用名称 fmap 代替。它必须遵守的函子定律与函子定义中列出的两个相同。

  1. fmap id = id(映射 id :: Int -> Intid :: Maybe Int -> Maybe Int
  2. fmap f . fmap g = fmap f . g(也就是说,对于 [=35= 类型的任何可能值 xfmap odd . fmap ord $ x 必须 return 与 fmap (odd . ord) $ x 相同的值].

作为一个不相关的切线,其他人指出Haskell中的某些东西不是函数,即像4"hello"这样的字面值。虽然在编程语言中为真(例如,您不能将 4 与另一个以 Int 作为值的函数组合),但它 真在范畴论中,您可以用从单位类型 () 到值类型的函数替换值。也就是说,字面值 4 可以被认为是一个箭头 4 :: () -> Int,当应用于类型 () 的(唯一)值时,它 return 是类型 [=10] 的值=] 对应于整数 4。这个箭头可以像其他任何箭头一样组合; odd . 4 :: () -> Bool 会将单位类型的值映射为布尔值,指示整数 4 是否为奇数。

从数学上讲,这很好。我们不必为类型定义任何结构;它们只是 ,并且由于我们已经定义了类型,因此我们不需要单独定义类型的值;我们只是根据功能来定义它们。 (不过,您可能会注意到我们仍然需要单位类型的实际值。在我们的定义中可能有一种方法可以避免这种情况,但我对范畴论的了解还不足以解释这种方式。)

对于我们的编程语言的实际实现,将文字值视为一种优化,以避免每次我们只是使用 4 () 代替 4 的概念和性能开销想要一个常数值。