如何确定类型是否为haskell中的函子?

如何确定类型是否为haskell中的函子?,haskell,functor,Haskell,Functor,我有一个数据类型: data Tree a = Leaf | Branch a (Tree a) (Tree a) 我不仅要确定这个数据类型,还要确定其他数据类型,比如字符串,这些数据类型是否是functor的守法实例。该链接表明,如果一个类型有一个函数fmap,则可以证明它是函子。给定任何类型a和b,该函数允许您应用类型a->b的任何函数将fa转换为fb,从而保留f的结构。如何对我的树数据类型或字符串数据类型进行测试?尝试编写这样的函数。如果你成功了,它肯定是一个函子。如果你失败了,可能不是

我有一个数据类型:

data Tree a = Leaf | Branch a (Tree a) (Tree a)

我不仅要确定这个数据类型,还要确定其他数据类型,比如字符串,这些数据类型是否是functor的守法实例。该链接表明,如果一个类型有一个函数fmap,则可以证明它是函子。给定任何类型a和b,该函数允许您应用类型a->b的任何函数将fa转换为fb,从而保留f的结构。如何对我的树数据类型或字符串数据类型进行测试?

尝试编写这样的函数。如果你成功了,它肯定是一个函子。如果你失败了,可能不是,也可能你没有足够的创造力。对于Tree,它的实现相对简单,当然初学者可能需要寻求帮助

将fmap的签名专门化到树中,您需要具有此签名的函数:

mapTree :: (a -> b) -> Tree a -> Tree b
mapTree f Leaf = _
mapTree f (Branch value left right) = _

1实际上,有许多类型,你可以通过查看它们的构造函数的字段来证明它们不是函子,但因为这不适用于树,所以我们不会进入它。

尝试编写这样的函数。如果你成功了,它肯定是一个函子。如果你失败了,可能不是,也可能你没有足够的创造力。对于Tree,它的实现相对简单,当然初学者可能需要寻求帮助

将fmap的签名专门化到树中,您需要具有此签名的函数:

mapTree :: (a -> b) -> Tree a -> Tree b
mapTree f Leaf = _
mapTree f (Branch value left right) = _
1实际上,有许多类型的函数,你可以通过查看它们的构造函数的字段来证明它们不是函子,但因为这不适用于树,所以我们就不会进入它。

Short non-response 在您自己思考之前,请尝试让GHC为您编写实例:

{-# LANGUAGE DeriveFunctor #-}

data Tree a = Leaf | Branch a (Tree a) (Tree a)
  deriving (Functor)
在这种情况下,这正好起作用,然后你就保证有一个守法的例子

说真的,这是通常为数据类型获取函子实例的方法。但是当它有意义的时候,你仍然应该了解你自己

实际答案 我不仅要确定这个数据类型,还要确定其他数据类型,比如字符串,这些数据类型是否是Functor的守法实例

因此,对于函子实例,首先需要一个参数化类型,即“容器”,它不关心存储在其中的类型。因此,严格来说,函子不应该是类型,而应该是类型构造函数或类型级函数★. 实际上,通过检查数据声明是否具有类型变量,您可以看到:对于树,您可以立即在代码中看到它

data Tree a = ... ✓
不过,在这种情况下,您可能不应该编写函子实例,因为幻象参数通常是常量唯一标记

如果它直接用作类型构造函数中某个字段的一部分,则可以编写函子实例。然后应将fmap ped函数应用于所有这些值

data Maybe a = Nothing
| Just a

instance Functor Maybe where
fmap _ Nothing = Nothing
fmap f (Just a) = Just $ f a
如果它在数据结构中嵌套得更深,但所有嵌套都在函子本身中,那么它就是函子。如果它是您只是试图定义自己的同一个函子,也就是说,对于递归类型,这同样适用

data TwoLists a = TwoLists {listL :: [a], listR :: [a]}

instance Functor TwoLists where
fmap f (TwoLists ll lr) = TwoLists (fmap f ll) (fmap f lr)
[高级,如果您暂时忽略此项,可能是最好的]如果嵌套不是由正常协变函子组成,而是由偶数个逆变函子组成,那么您的整个类型也是协变函子

★类型构造函数实际上是一种非常特殊的类型级函数,尤其是内射函数。从数学上讲,函子不需要内射地映射其对象。在Hask中,对象是类型,但在Haskell中,这对于类型检查器是必要的

†这些语法扩展导致GHCi将类型类型显示为类型;从历史上看,它会改为显示*,这仍然是较旧GHC版本中的默认设置,但现在已弃用

‡然而,String实际上是[Char]的同义词,即字符列表,list是函子。因此,您实际上可以对字符串执行fmap,但这并不意味着您使用的是“字符串函子”:您使用的是列表函子,即使以字符串开头,结果也可能不是字符串,而是整数列表。

Short non-answer 在您自己思考之前,请尝试让GHC为您编写实例:

{-# LANGUAGE DeriveFunctor #-}

data Tree a = Leaf | Branch a (Tree a) (Tree a)
  deriving (Functor)
在这种情况下,这正好起作用,然后你就保证有一个守法的例子

说真的,这是通常为数据类型获取函子实例的方法。但是当它有意义的时候,你仍然应该了解你自己

实际答案 我不仅要确定这个数据类型,还要确定其他数据类型,比如字符串,这些数据类型是否是Functor的守法实例

因此,对于函子实例,首先需要一个参数化类型,即“容器”,它不关心存储在其中的类型。因此,严格来说,函子不应该是类型,而应该是类型构造函数或类型级函数★. 实际上,通过检查数据声明是否 s类型变量:在树的情况下,您可以立即在代码中看到它

data Tree a = ... ✓
不过,在这种情况下,您可能不应该编写函子实例,因为幻象参数通常是常量唯一标记

如果它直接用作类型构造函数中某个字段的一部分,则可以编写函子实例。然后应将fmap ped函数应用于所有这些值

data Maybe a = Nothing
| Just a

instance Functor Maybe where
fmap _ Nothing = Nothing
fmap f (Just a) = Just $ f a
如果它在数据结构中嵌套得更深,但所有嵌套都在函子本身中,那么它就是函子。如果它是您只是试图定义自己的同一个函子,也就是说,对于递归类型,这同样适用

data TwoLists a = TwoLists {listL :: [a], listR :: [a]}

instance Functor TwoLists where
fmap f (TwoLists ll lr) = TwoLists (fmap f ll) (fmap f lr)
[高级,如果您暂时忽略此项,可能是最好的]如果嵌套不是由正常协变函子组成,而是由偶数个逆变函子组成,那么您的整个类型也是协变函子

★类型构造函数实际上是一种非常特殊的类型级函数,尤其是内射函数。从数学上讲,函子不需要内射地映射其对象。在Hask中,对象是类型,但在Haskell中,这对于类型检查器是必要的

†这些语法扩展导致GHCi将类型类型显示为类型;从历史上看,它会改为显示*,这仍然是较旧GHC版本中的默认设置,但现在已弃用


‡然而,String实际上是[Char]的同义词,即字符列表,list是函子。因此,您实际上可以对字符串执行fmap,但这并不意味着您使用的是“字符串函子”:您使用的是列表函子,即使以字符串开头,结果也可能不是字符串,而是整数列表。

警告:这是不完整的;我希望有人能填补我留下的关于不动点函数性的漏洞。事实5和我对它的使用感觉不稳定

字符串不是函子,因为它的类型错误。String::Type,但函子必须具有种类类型->类型

在我们讨论树之前,让我们先确定一些事实

任何类型a的常数a都是函子:

-- From Data.Functor.Constant
newtype Constant a b = Constant { getConstant :: a }
instance Functor (Constant a) where
    fmap _ (Constant x) = Constant x
恒等式是函子

-- Adapted from Data.Functor.Identity
newtype Identity a = Identity { runIdentity :: a }
instance Functor Identity where
    fmap f (Identity x) = Identity (f x)
如果组件是函子,则和类型是函子

-- From Data.Functor.Sum
data Sum f g a = InL (f a) | InR (g a)
instance (Functor f, Functor g) => Functor (Sum f g) where
    fmap f (InL x) = InL (fmap f x)
    fmap f (InR y) = InR (fmap f y)
-- From Data.Functor.Fixedpoint
newtype Fix f = Fix { unFix :: f (Fix f) }

instance (Functor f, Functor t) => Functor (Fix (f t)) where
    fmap g (Fix h) = Fix (fmap g (unfix h))
如果组件是函子,则产品类型是函子

-- From Data.Functor.Product
data Product f g a = Pair (f a) (g a)
instance (Functor f, Functor g) => Functor (Product f g) where
    fmap f (Pair x y) = Pair (fmap f x) (fmap f y)
某些不动点是函子

-- From Data.Functor.Sum
data Sum f g a = InL (f a) | InR (g a)
instance (Functor f, Functor g) => Functor (Sum f g) where
    fmap f (InL x) = InL (fmap f x)
    fmap f (InR y) = InR (fmap f y)
-- From Data.Functor.Fixedpoint
newtype Fix f = Fix { unFix :: f (Fix f) }

instance (Functor f, Functor t) => Functor (Fix (f t)) where
    fmap g (Fix h) = Fix (fmap g (unfix h))
考虑到这些事实,我们将把我们的树类型分解为已知函子的和和与积的组合,从而确定我们的类型同构于函子,因此也是函子本身

首先,叶只是的一个描述性别名,我们可以用另一个类型参数替换对树a的递归引用

-- This is slightly different from some explanations of
-- recursive types, where t would be the subtree type itself, not
-- a type constructor.
data TreeF t a = () | Branch a (t a) (t a)
接下来,我们注意到类型同构于常数a,而a同构于恒等式a,从而去掉了a。此外,三向积同构于两个双向积,即a,b,c~a,b,c:

上面的事实1-4允许我们得出结论,只要t是函子,TreeF t就是函子


最后,我们可以使用事实5得出结论,Fix-TreeF-Fix-TreeF~Tree是一个函子。

警告:这是不完整的;我希望有人能填补我留下的关于不动点函数性的漏洞。事实5和我对它的使用感觉不稳定

字符串不是函子,因为它的类型错误。String::Type,但函子必须具有种类类型->类型

在我们讨论树之前,让我们先确定一些事实

任何类型a的常数a都是函子:

-- From Data.Functor.Constant
newtype Constant a b = Constant { getConstant :: a }
instance Functor (Constant a) where
    fmap _ (Constant x) = Constant x
恒等式是函子

-- Adapted from Data.Functor.Identity
newtype Identity a = Identity { runIdentity :: a }
instance Functor Identity where
    fmap f (Identity x) = Identity (f x)
如果组件是函子,则和类型是函子

-- From Data.Functor.Sum
data Sum f g a = InL (f a) | InR (g a)
instance (Functor f, Functor g) => Functor (Sum f g) where
    fmap f (InL x) = InL (fmap f x)
    fmap f (InR y) = InR (fmap f y)
-- From Data.Functor.Fixedpoint
newtype Fix f = Fix { unFix :: f (Fix f) }

instance (Functor f, Functor t) => Functor (Fix (f t)) where
    fmap g (Fix h) = Fix (fmap g (unfix h))
如果组件是函子,则产品类型是函子

-- From Data.Functor.Product
data Product f g a = Pair (f a) (g a)
instance (Functor f, Functor g) => Functor (Product f g) where
    fmap f (Pair x y) = Pair (fmap f x) (fmap f y)
某些不动点是函子

-- From Data.Functor.Sum
data Sum f g a = InL (f a) | InR (g a)
instance (Functor f, Functor g) => Functor (Sum f g) where
    fmap f (InL x) = InL (fmap f x)
    fmap f (InR y) = InR (fmap f y)
-- From Data.Functor.Fixedpoint
newtype Fix f = Fix { unFix :: f (Fix f) }

instance (Functor f, Functor t) => Functor (Fix (f t)) where
    fmap g (Fix h) = Fix (fmap g (unfix h))
考虑到这些事实,我们将把我们的树类型分解为已知函子的和和与积的组合,从而确定我们的类型同构于函子,因此也是函子本身

首先,叶只是的一个描述性别名,我们可以用另一个类型参数替换对树a的递归引用

-- This is slightly different from some explanations of
-- recursive types, where t would be the subtree type itself, not
-- a type constructor.
data TreeF t a = () | Branch a (t a) (t a)
接下来,我们注意到类型同构于常数a,而a同构于恒等式a,从而去掉了a。此外,三向积同构于两个双向积,即a,b,c~a,b,c:

上面的事实1-4允许我们得出结论,只要t是函子,TreeF t就是函子


最后,我们可以使用事实5得出结论,Fix-TreeF-Fix-TreeF~Tree是一个函子。

我不知道如何完成这个函数。我的意思是,目标是什么?你的问题中给出了目标。一个函数fmap,在给定任何类型a和b的情况下,它允许您应用类型a->b的任何函数将fa转换为fb,从而保留f的结构。对于f=Tree,您正在寻找一个保留树结构但修改其中包含的元素的函数。我不知道如何完成此函数。我的意思是,目标是什么?你的问题中给出了目标。函数fmap,给定任何类型的a和b,允许您应用类型a->b的任何函数来转向
将fa转化为fb,保持f的结构。使用f=Tree,您正在寻找一个保留树结构,但修改其中包含的元素的函数。您可以通过显示树可以用更原始的函子定义来进行分析。例如,类型TreeF t=Sum常量乘积Identity Product t,类型Tree=Fix TreeF,您可以显示此树与您的定义同构。因为常数、恒等式以及函子的和和与积都是函子,所以只要t是函子,TreeF t也是函子。我不确定声称TreeF t的不动点是函子本身的确切论点。@chepner你的别名没有丢失a参数吗?@amalloy我想是的;我试图充实我的评论作为答案,发现自己在原始草稿中使用t作为类型和函子。欢迎对答案进行评论和更正。您可以通过显示树可以用更原始的函子来定义来分析。例如,类型TreeF t=Sum常量乘积Identity Product t,类型Tree=Fix TreeF,您可以显示此树与您的定义同构。因为常数、恒等式以及函子的和和与积都是函子,所以只要t是函子,TreeF t也是函子。我不确定声称TreeF t的不动点是函子本身的确切论点。@chepner你的别名没有丢失a参数吗?@amalloy我想是的;我试图充实我的评论作为答案,发现自己在原始草稿中使用t作为类型和函子。欢迎对答案进行评论和更正。谢谢@leftaroundabout。我真的很喜欢简短的不回答方法。别误会我的意思,我很欣赏实际答案背后的逻辑,但在考试中,它太长了,无法完成。我的问题是,如何对已经定义的数据类型(如String、Maybe、IO、Gen等)执行简短的非应答方法。这将是一个孤立实例,这通常是一个坏主意。通常,您可以相信,如果一个类型有一个函子实例是有意义的,那么它确实在自己的模块中定义了一个函子实例。如果不是,那么可能有一个很好的理由。也就是说,总是有在考试中。。。最好确保这是一个公认的答案!如果我问了一个关于如何在某处实现Functor的问题,我不会给派生Functor打满分。谢谢@leftaroundabout。我真的很喜欢简短的不回答方法。别误会我的意思,我很欣赏实际答案背后的逻辑,但在考试中,它太长了,无法完成。我的问题是,如何对已经定义的数据类型(如String、Maybe、IO、Gen等)执行简短的非应答方法。这将是一个孤立实例,这通常是一个坏主意。通常,您可以相信,如果一个类型有一个函子实例是有意义的,那么它确实在自己的模块中定义了一个函子实例。如果不是,那么可能有一个很好的理由。也就是说,总是有在考试中。。。最好确保这是一个公认的答案!如果我问一个关于如何在某处实现函子的问题,我不会给派生函子打满分。