【问题标题】:Functors don’t work with data types that require specific types函子不适用于需要特定类型的数据类型
【发布时间】:2014-07-11 19:18:57
【问题描述】:

这很好用:

data Foo a = Foo a

instance Functor Foo where
  fmap f (Foo s) = Foo (f s)

这会引发错误:

data Foo = Foo String

instance Functor Foo where
  fmap f (Foo s) = Foo (f s)

错误:

种类不匹配
    `Functor' 的第一个参数应该是 `* -> *',
    但是 `Foo' 有种 `*'
    在 `Functor Foo' 的实例声明中

我在这里缺少什么?如果 Foo 包含特定类型,为什么我不能使用函子来包装和解包?


更新

我想我可以换个方式问这个问题:

 data Foo = Foo String deriving(Show)

 let jack = Foo "Jack"

 -- Some functory thingy here

 putStrLn $ show $ tail <$> jack
 -- Foo "ack"

为什么我不能这样做?或者这个用例还有其他构造吗?

【问题讨论】:

    标签: haskell


    【解决方案1】:

    那是因为Foo 需要一个单一的类型变量来操作。

    fmap的类型是:

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

    现在尝试为您的Foo 专门处理这个:

    (a -> b) -> Foo -> Foo
    

    你能看出问题出在哪里吗?类型不仅匹配。所以,要使Foo 成为函子,它必须是这样的:

    Foo a
    

    因此,当您将其专门用于 fmap 时,它具有以下正确类型:

    (a -> b) -> Foo a -> Foo b
    

    【讨论】:

    • 这对我来说仍然没有意义。如果我有一个类型 Foo a c 怎么办?函子是否只适用于最多持有一件事的类型?
    • 在这种情况下,fmap 将是这样的(a -&gt; b) -&gt; Foo k a -&gt; Foo k b
    • 所以不是吗?如果使用该类型的映射,我需要编写一个新类型吗?
    • 我认为这个答案可以与 Functor 的示例用法有关,该示例用法不适用于定义的 Foo,例如 fmap length (Foo "Hello")。在任何真正的仿函数(例如,Tree String)上,它可以计算每个字符串,但在 Foo 上这是一个类型错误,因为你不能构造 Foo 5:Foo 只包含字符串!
    • @Fresheyeball 像 Foo 这样的类型构造函数像函数一样被柯里化,并且可以像函数一样被部分应用。这意味着Foo a c(Foo a) c 相同。您可以(可能)为(Foo a) 创建一个Functor 实例。在这种情况下,(Foo a) 是一个类型构造函数,它接受一个类型作为参数并给出一个类型作为结果。这意味着它的“种类”为* -&gt; *。这正是成为Functor 实例所需的类型。种类之于类型就像类型之于值一样。
    【解决方案2】:

    fmap 的类型是泛型的;你不能限制它:

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

    那些as 和bs 必须是完全多态的(在您的Functor 实例的约束内),或者您没有Functor。解释为什么会这样的手动方式是因为Functor 必须遵守一些理论法则才能使它们与 Haskell 的其他数据类型很好地配合:

    fmap id = id
    fmap (p . q) = (fmap p) . (fmap q)
    

    如果您的数据类型被参数化为多种类型,即:

    data Bar a b = Bar a b
    

    你可以为Bar a写一个Functor实例:

    instance Functor (Bar a) where
       fmap f (Bar a b) = Bar a (f b)
    

    你也可以为Bar写一个Bifunctor实例:

    instance Bifunctor Foo where
      first  f (Bar a b) = Bar (f a) b
      second f (Bar a b) = Bar a (f b)
    

    ...这又必须遵守一些法律(在链接页面上列出)。

    编辑:

    您可以编写自己的类来处理您正在寻找的行为类型,但它看起来像这样:

    class FooFunctor f where
      ffmap :: (String -> String) -> f -> f
    

    但在这种情况下,我们必须为我们可能拥有的“内部类型”的每一个排列(如字符串)创建新的整个类,以便覆盖所有基础。

    您还可以编写一个类(称为Endo),它只允许对数据类型的“内部类型”进行自同构(a -&gt; a 类型的函数),如下所示:

    class Endo f where
      emap :: (a -> a) -> f a -> f a
    

    然后,如果您稍微更改了数据类型,并实例化了一个适当的 Endo 实例,例如

    data Foo' a = Foo' a 
    type Foo = Foo' String
    
    instance Endo Foo' where
      emap f (Foo a) = Foo (f a)
    

    ...如果您编写 Foo -&gt; Foo 类型的函数,则可以保证在使用 emap 时保留要映射的内部类型的“字符串性”。对hayoo 的快速搜索发现这种类型的东西是比较普遍的做法,但并不真正作为标准类型类存在。

    【讨论】:

    • 您能解释一下为什么这些理论规律会如此约束事物吗?例如,您不能通过限制fmap 可以应用于哪些类型的函数来查看非完整子类别上的(endo)函子吗?我对这些事情知之甚少。
    • @dfeuer 我无论如何都不是范畴论者,所以很遗憾我无法明智地回答这个问题(对不起)!幸运的是,有一个专门针对此类问题的 wiki 页面,因此您可能会在那里找到您要查找的内容:en.wikibooks.org/wiki/Haskell/Category_theory#Functors
    【解决方案3】:

    来自动态语言,您可能将 Functor 视为 容器 内容,而 fmap 则视为在容器内转换内容的一种方式。然而,在类别理论中,Functor 可以被视为一种将类型转换为另一种类型的方法,以及一种将这些类型上的函数转换为另一种类型上的函数的方法。

    假设您有 2 个不同的世界,一个是地球,一个是虚拟的,地球上的每个身体/事物都有一个化身。 Functor 不是化身,而是魔杖,它将一切都转化为化身,还将现实世界的每一个功能都转化为化身世界中的功能。

    例如,我可以用我的魔杖将人转换为青蛙(或将字符串转换为字符串列表),但我也可以将函数“更改人类帽子”转换为“青蛙帽子”(或大写一个字符串,将列表中的所有字符串大写)。

    fmap 是您将函数转换为另一个函数的方式:您可以将其视为

    • 一个函数,它有两个参数——一个函数和一个容器——并将这个函数应用到这个容器的每个元素上

    • 但也可以作为一个函数,它接受 1 个 argmunt - 一个函数 - 一个返回一个函数,它接受一个容器并返回一个容器。

    从类型创建类型的方式不太明显 在第一个示例中,您可能只是将Foo String 视为新类型,但您也可以将Foo 视为采用String 类型的超函数和返回一个新类型:Foo String。这就是* -&gt; * kind 的含义。 Foo 不是类型,而是从类型创建类型的超函数。

    在您的第二个示例中,Foo 不是类型创建者,而只是一个简单类型(种类:*),因此将其声明为函子是没有意义的。

    如果您真的想在第二个示例中为普通 Foo 定义 fmap,请定义一个真正的函子并为普通类型创建一个类型别名

    data FooFunctor a = FooFunctor a
    instance Functor Foofunctor where
       fmap f (FooFunctor a) = FooFunctor (f a)
    
    
    type Foo = FooFunctor String
    

    【讨论】:

    • 好的。创建一个类型只是为了能够对另一种类型进行 fmap 是否完全合理(如您的示例中)?
    • 有没有像FooFunctor这样的现有类型?
    • +1 我仍然不太明白为什么这是必要的。问题已更新。
    • @Fresheyeball,是的,创建一个“只是”能够在另一种类型上映射的类型确实有意义。一般来说,这种新类型也可能在其他一些情况下有用。我们从data IntList = IntCons Int IntList | IntNil 概括为data [a] = [] | (a:[a]),因为这样我们可以讨论各种事物的列表,而无需重写一堆样板。函子和(更复杂的)单子也是如此。
    • @Fresheyeball FooFunctor 只是Identity,见hackage.haskell.org/package/transformers-0.2.2.0/docs/…
    【解决方案4】:

    一个几乎完全符合您要求的课程是MonoFunctor

    type instance Element Foo = String
    
    instance MonoFunctor Foo where
      fmap f (Foo s) = Foo (f s)
    

    【讨论】:

      【解决方案5】:

      head "Jack" 不是字符串"J",而是字符'J'。因此,您自己的示例说明了为什么这不起作用; head &lt;$&gt; jack 必须给出Foo 'J',这不是Foo 类型的有效值,因为Foo 只能应用于String 值,而不是Char 值。

      此用例的“其他构造”是为Foo 定义一个“map”函数,就像您尝试定义 fmap 一样。但该地图函数不是 fmap,因为它必须具有(String -&gt; String) -&gt; Foo -&gt; Foo 类型。所以没有必要(或不可能)将Foo 设为Functor 的实例并将映射函数命名为fmap;您要使用的映射功能根本不是fmap。

      请注意,这意味着您不能将任意函数映射到您的 Foo 值上;只有接受和返回字符串的函数(所以head 仍然存在)。您也不能将Foo 值传递给接受任何函子中的值的通用函数;这些函数可能会尝试fmap 函数而不是不返回字符串的Foo;他们被允许这样做是因为他们指定了他们需要仿函数,而这正是仿函数的定义。

      【讨论】:

        猜你喜欢
        • 2021-06-05
        • 1970-01-01
        • 2016-08-10
        • 1970-01-01
        • 2015-11-25
        • 1970-01-01
        • 1970-01-01
        • 2020-10-19
        • 2019-08-14
        相关资源
        最近更新 更多