让Haskell函子沉入其中。

时间:2010-07-01 06:17:40

标签: haskell functor

Learn You a Haskell有一个关于仿函数的例子。我可以阅读LYAH和文本,并弄清楚应该发生什么 - 但我不知道写这样的东西。我经常在Haskell中发现这个问题。

instance Functor (Either a) where  
    fmap f (Right x) = Right (f x)  
    fmap f (Left x) = Left x

然而,我很困惑..为什么不这个补充

instance Functor (Either a) where  
    fmap f (Right x) = Right (x)  
    fmap f (Left x) = Left (f x)

如果最高定义中没有使用f,那么还有什么限制x使其无法满足Left

10 个答案:

答案 0 :(得分:41)

这是仿函数类:

class Functor f where
  fmap :: (a -> b) -> f a -> f b

请注意,“f”本身是一个类型构造函数,因为它应用于fmap行中的类型变量。以下是一些清楚说明的例子:

类型构造函数:

IO
Maybe
Either String

类型:

IO Char
Maybe a
Either String String

“也许a”是具有一种类型构造函数(“Maybe”)和一种类型变量(“a”)的类型。它不是具体的东西,但它可用于多态函数的类型签名。

“Either”是一个带有两个类型参数的类型构造函数,所以即使你应用了一个(例如Either String它仍然是一个类型构造函数,因为它可以采用另一个类型参数。

重点是:当您定义Functor实例时,类型构造函数f无法更改。这是因为它由同一个变量{{ 1}},作为f的参数和结果。唯一允许更改的类型是应用于fmap构造函数的类型。

当您撰写f时,instance Functor (Either c)声明Either c填写f。这为fmap提供了以下类型的实例:

fmap

使用fmap :: (a -> b) -> (Either c) a -> (Either c) b 的定义,获得此类型的唯一有用方法是将Either值应用于函数。请记住,“Either”有两个可能的值,可能有不同的类型。这里Right值的类型为'c',所以你不能将它应用于函数(期望'a')[1],结果也不正确因为你是留下Left,与类定义不匹配。

用“Either c”替换“f”以使用“Either c”实例获得fmap的上述类型签名后,接下来编写实现。有两种情况需要考虑,左派和右派。类型签名告诉我们左侧的类型“c”不能改变。我们也没有办法改变价值,因为我们不知道它实际上是什么类型。我们所能做的就是不管它:

Either b a

对于右侧,类型签名表示我们必须从类型为“a”的值更改为类型为“b”的值。第一个参数是一个完全相同的函数,因此我们使用带有输入值的函数来获取新的输出。将两者放在一起给出了完整的定义

fmap f (Left rval) = Left rval

这里有一个更通用的原则,这就是为什么编写一个调整左侧的Functor实例是不可能的,至少使用Prelude定义。从上面复制一些代码:

instance Functor (Either c) where
  fmap f (Right rval) = Right (f rval)
  fmap f (Left lval) = Left lval

即使我们在实例定义中有一个类型变量'c',我们也不能在任何类方法中使用它,因为它在类定义中没有提到。所以你不能写

class Functor f where
  fmap :: (a -> b) -> f a -> f b

instance Functor (Either c) where ...

leftMap和fmap的结果现在是leftMap :: (c -> d) -> Either c a -> Either d a leftMap mapfunc (Left x) = Left (mapfunc x) leftMap mapfunc (Right x) = Right x instance Functor (Either c) where --fmap :: (c -> d) -> Either c a -> Either d a fmap = leftMap (Either d) a已更改为(Either c),但这是不允许的,因为无法在Functor类中表达它。为了表达这一点,你需要一个有两个类型变量的类,例如

(Either d)

在这个类中,由于左侧和右侧类型变量都在范围内,因此可以编写在任一侧(或两侧)上运行的方法。

class BiFunctor f where
  lMap :: (a -> b) -> f a c -> f b c
  rMap :: (c -> d) -> f a c -> f a d
  biMap :: (a -> b) -> (c -> d) -> f a c -> f b d

虽然在实践中人们通常只为BiFunctor类编写“biMap”,如果需要左或右映射,则使用“id”作为其他函数。

[1]更确切地说,Left值的类型为'c',函数需要'a',但是类型检查器无法统一这些类型,因为'c'类型不在类的范围内定义

答案 1 :(得分:9)

左和右不是类型,Left xRight y属于同一类型。他们只是Either的构造者。你可以考虑

Left :: c -> Either c d
Right :: d -> Either c d

您可以拥有2个fmap声明,因为我们知道Left和Right是不同的值。这就像

g :: Int -> Int
g 1 = 2
g 2 = 4
g n = n

这里我们不能说1和2和n是不同的“类型”只是因为模式匹配有效。


Functor类定义为

class Functor f where
  fmap :: (a -> b) -> f a -> f b

请注意,ab是任意类型。为清楚起见,我们将您实例中的a重命名为c,将函数f重命名为func

instance Functor (Either c) where  
    fmap func (Right x) = Right (x)  
    fmap func (Left x) = Left (func x)

假设您的Either遵循默认定义

data Either c d = Left c | Right d

然后按照你的定义,

fmap    func     (Right x) = Right x
-- # (a -> b) ->      f a       f  b
-- # f = Either c

这会强制a = b

fmap    func     (Left x) = Left (func x)
-- # (a -> b) ->     f a       f b
-- # f = Either c

强制c = a = b。考虑到abc是独立的任意类型,两者都无效。

答案 2 :(得分:8)

好的,这是另一个非常简单的尝试。

你问为什么这不编译:

instance Functor (Either a) where
    fmap f (Right x) = Right (x)
    fmap f (Left x) = Left (f x)

因此,我们尝试通过尝试定义相同的函数来简化问题,而不将其作为类实例声明的一部分:

这给了我们

foo f (Right x) = Right (x)
foo f (Left x) = Left (f x)

确实编译了。 ghci告诉我们类型签名:

*Main> :t foo
foo :: (t1 -> a) -> Either t1 t -> Either a t

我们将重命名一些变量以获得更加统一的外观:

foo :: (a -> b) -> Either a c -> Either b c

这很有道理。它需要一个函数并将其应用于Either的左侧。

但是fmap的签名是什么?

*Main> :t fmap
fmap :: (Functor f) => (a -> b) -> f a -> f b

让我们用Either c替换fmap签名中的f(我将Either a重命名为Either c,以防止我们两个不同的a混淆):

fmap :: (a -> b) -> Either c a -> Either c b

你看到了问题吗?您的函数完全有效 - 它的类型与Either a 的fmap必须具有的类型不同。

这是关于类型的一种美好的事情。鉴于fmap的签名,{A}上的fmap实际上只有一个有意义的实现。

有时,当我们幸运和谨慎时,我们可能会遇到类似的情况 - 给定一个类型签名,该函数几乎会自行编写。

修改:尝试回答以下问题。

1)没有“两个功能的组合”正在进行中。要获取fmap超过Either a的类型签名,只需浏览fmap函数签名,并查看f的每个位置,将其替换为Either a。我们称之为fmap类型签名的“特化”。也就是说,它比普通类型的fmap 严格不那么通用 - 任何需要更专业类型的函数的地方,你可以传递一般类型的东西而没有任何问题。 / p>

2)你在左侧绘图的功能(我在上面的例子中命名为“foo”)就好了。它工作正常,它做你想要的。您无法将其命名为fmap并在Functor实例中使用它。就个人而言,我将其命名为onLeftmapLeft

以下所有内容均可忽略/是供参考,但不建议将来阅读/实际使用:

如果想要获得非常技术性的,因为你可以在左侧和右侧进行映射(尽管你只能为后者声明Functor),要么不仅是一个Functor,而是一个Bifunctor。这在例如ekmett的Category-Extras库中提供(参见http://hackage.haskell.org/packages/archive/category-extras/0.44.4/doc/html/Control-Bifunctor.html)。

有许多很酷的东西,包括用程序计算,以及“折纸编程”更严格地使用bifunctors。你可以在这里阅读:http://lambda-the-ultimate.org/node/1360。但是,你可能不想,至少在你对Haskell更熟悉之前。这是计算机技术,肮脏,研究和非常酷,但不是根本来理解惯用的Haskell编程。

答案 3 :(得分:3)

(编辑以尝试更好地回答问题)

Either的定义是:

data Either a b = Left a | Right b

所以“Either”需要两个类型的参数。顺便说一下,技术上“Either”实际上不是一个类型而是一个类型构造函数;它需要类型参数来创建一个类型。

Functor的定义是:

class Functor f where
   fmap :: (p -> q) -> f p -> f q

因此,在此类定义中,作为Functor实例的任何类型“f”必须采用类型参数。这没有宣布;它是从“f p”和“f q”推断出来的;因为“f”在这里给出了一个类型参数,所以它必须是一个带有一个的类型。

(注意:原来的定义使用了“a”和“b”而不是“p”和“q”。我使用不同的字母来保持与“Either ab”不同的东西,当我稍后到达时)< / p>

在大多数情况下,“f”是容器类型,如列表或树。所以例如你有

data Tree a = ...

instance Functor Tree where
   fmap func_a2b tree_of_a = ... --  tree of b.

但是“Either”需要两个类型参数,那么我们如何才能将其纳入此方案?答案是类型可以像函数一样具有部分应用程序。在相同的方式 我可以声明一个函数

foo x y = ...

然后说“foo 2”以获得一个需要第二个参数的新函数,所以我可以说“Either a”来获得一个需要第二个类型参数的新类型。

现在看一下原始实例:

instance Functor (Either a) where ....

所以这里“Either a”是一个类型构造函数,它需要一个参数,就像Functor期望它的实例一样。所以“Either a”的“fmap”类型将是

fmap :: (p -> q) -> Either a p -> Either a q

所以现在在“where”子句中你必须给出具有这种类型的“fmap”的定义。您引用的第一个类型具有此类型,因为第二个类型参数用于“Right”构造函数,并且该函数应用于该构造函数。第二个将无法工作,因为它将具有类型

fmap :: (p -> q) -> Either p a -> Either q a

这不是Functor课所说的那样。

答案 4 :(得分:3)

虽然我最终会改编你的格式,但我会从稍微不同的格式开始,因为我认为它会让我的解释更加清晰。

让我们考虑一个不同的数据类型

data Choice a = Default Integer | Chosen a

-- This corresponds to your top, working, instance.
instance Functor Choice where
  fmap f (Default i) = Default i
  fmap f (Chosen  a) = Chosen  (f a)

应该清楚为什么这个实例有效。但是,以下内容如何:

-- Broken!
instance Functor Choice where
  fmap f (Default i) = Default (f i)
  fmap f (Chosen  a) = Chosen  a

你应该能够看到为什么这不起作用。 fmap的类型为Functor f => (a -> b) -> f a -> f b;在这种情况下,它是(a -> b) -> Choice a -> Choice b。因此,f参数的类型为a -> b。但是,在第二个(失败的)实例声明中,您编写了f i。我们知道,由于数据类型声明,i必须是Integer,因此我们无法对其应用f。同样,由于a的类型为aChosen a的类型为Chosen a,而不是类型Chosen b。因此,底部的Functor实例无效。

嗯,Either的顶级实例有效,因为,就像在Choice示例中一样,它遵循类型。让我们看看它,并进行一些重命名:

instance Functor (Either c) where  
  fmap f (Left  c) = Left  c
  fmap f (Right a) = Right (f a)

此实例声明不会为Functor声明Either的实例 - 它不能。作为Functor实例的东西必须采用一个类型参数。因此,Int不能成为仿函数,因为Int不接受任何类型参数,但[]Maybe可以是[a]和{{1}是完整的类型。但是,Maybe a需要两个类型参数:Either。因此,此实例所做的声明Either a b是任何可能的Either c的仿函数。在实例声明的持续时间内,c 已修复。所以让我们来看看并添加类型(这不是合法的语法!):

c

由于instance Functor (Either c) where fmap :: forall a b. (a -> b) -> (Either c) a -> (Either c) b fmap f (Left (c :: c)) = Left c fmap f (Right (a :: a)) = Right (f a :: b) 的类型为f,但a -> b的类型固定为c,我们无法写c;即使我们可以,也希望Left (f c)保持不变,以便我们可以返回c。同样,我们必须(Either c) b应用于f才能获得a类型的内容。

这也是你的底层实例不起作用的原因:你有一个函数需要适用于只应用于固定类型b的任何类型,并且你保留需要的类型< / em>单独改造。我们再来看一下类型签名:

c

在这里,函数定义的第一部分尝试将函数instance Functor (Either c) where fmap :: forall a b. (a -> b) -> (Either c) a -> (Either c) b fmap f (Left (c :: c)) = Left (f c) fmap f (Right (a :: a)) = Right a 应用于固定类型f :: a -> b的某些东西,这不能正常工作,所以这已经失败了。但是让我们来看看它产生的类型。在这种情况下,我们希望(某种程度上)c具有类型f c,而b将具有类型a。在这种情况下,我们将返回a类型的值,但仍然不允许这样做。

基本上,问题源于此。首先,请注意Either b a在两个函数定义子句之间是相同的,因此它不能在行之间更改。其次,请注意我们修复f ,并声明 c 的实例。对于任何c都是如此,但我们一次只看一个。最后,正因为如此,c的参数是 not 参数化Left期望的类型;它保证有一些固定类型f。这意味着(a)您无法对其应用c,并且(b)您必须将其应用于f的论点,否则您将不会改变你期望改变的类型。

答案 5 :(得分:3)

不管你信不信,这不是魔术。这一切都与Either a b类型Either b a的类型不同。data Either a b = Left a | Right b 。以下是来自Prelude

的定义
instance Functor (Either a) where  
    fmap f (Right x) = Right (f x)  
    fmap f (Left x) = Left x

...注意首先是类型变量a,然后是b,并注意到我们只在Either Functor的声明中包含了一个:

instance Functor Maybe where
    fmap = map

现在让我们看看Maybe Functor的定义

Maybe

这里没有类型变量,虽然Maybe Int有一个类型参数(如*->*)。我想要的是类型不是仿函数,类型构造函数是仿函数(仿函数有类f :: b -> c)。

因此,让x在有效的Either Functor版本中,来自(Left x)的{​​{1}}类型为a,这很好,因为{{1}这是一个仿函数,(Either a)中的x属于(Right x)类型,因此b的类型为(Right x)((Either a) b)属于键入(Right (f x)),因此fmap的类型为((Either a) c),视需要而定。

在您失败的版本中,我们(b->c) -> ((Either a) b) -> ((Either a) c)中的x不是(Right (x))类型,而是a类型,所以b是<类型(Right (x))的强>不,它不符合fmap的类型。

总结一下:有效的fmap出现了:((Either a) c), 但是那个不起作用的那个出现了:(b -> c) -> (Either a) b -> (Either a) c并且这不是fmap的正确类型。

答案 6 :(得分:2)

希望这会有所帮助......

首先,一些背景:

1)Functor需要一个“类型构造函数”,一个(好吧,不是一个类型本身,...)类型,“需要”另一个常规类型给它形成一个“完整类型”,就像一个通用在Java / C ++中。 因此,例如,List是一个Functor(顺便说一下),或Array,因为(除此之外)完整类型不仅仅是{ {1}},但List。所以,:   Functor采用“类型构造函数”,不完整类型。

2)List<A>是一个构造函数类型,Haskell人(阅读:Edward Kmett,以及其他数学良好的全明星)称为bifunctor。它需要两种类型才能完成。例如,完全使用Either是:Either这意味着(是的,是的,“呃!”)它是(Either Integer String)整数或(Left)字符串。因此,这意味着Right是一个不完整的类型,当您使用Either IntegerLeft Integer时 决定“b”应该是什么。

现在,为了有趣的部分!

最重要的东西是有效的,因为Right...b使用了某种类型的构造函数,并将其与fmap函数一起使用,以便从a -> bf a创建类似的函数 - 手-Haskell中最喜欢的例子是列表,AKA地图f b,其中Functor是:: (a -> b) -> ([a] -> [b])部分。现在,使用像[ ]这样的东西(让我们继续使用前面的Either a),fmap的类型签名如下所示:

Either Integer

并且两个示例(来自顶部)显示了fmap对fmap :: (a -> b) -> (Either Integer a -> Either Integer b) 类型的代表值所做的事情,以获得Either Integer a类型的值。

现在,你的-bottom-不起作用,因为:

  1. 你有一个功能Either Integer b fa s。
  2. 您的b类型必须是类型 整数,并保持整数(或 键入Float,并保持Float,什么 永远的类型是之一 两种类型的Left类型 你正在使用)。
  3. 您的Either类型必须是 无论什么类型的功能 取(“Right”),然后转到该类型 该函数使用(“a”)。
  4. 它必须这样做(但你的东西没有 - 这就是为什么它不起作用),因为那是b需要的类型。具体来说,你有这些方程式:

    fmap

    您的等式给出fmap f (Right x) = Right (x) fmap f (Left x) = Left (f x) 类型:

    fmap

    这不仅不符合fmap :: (a -> b) -> Either c d -> Either c d fmap :: (a -> b) -> Either a d -> Either b d 想要的东西,而且彼此之间甚至不一致!

    对不起,我写了半本书,但我希望能给你一些见解。

答案 7 :(得分:2)

热门作品因为fmap::(b -> c) -> Either a b -> Either a c和你的-bottom-不起作用,因为这需要fmap::(a -> c) -> Either a b -> Either a c。但是,如果您将Either更改为

,它将起作用
data Either' a b = Left' b | Right' a deriving (Eq, Show)

instance Functor (Either' a) where  
    fmap f (Right' x) = Right' (x)  
    fmap f (Left' x) = Left' (f x)

答案 8 :(得分:1)

您尝试编写的实例,我们暂时将其称为fmap2,具有以下类型:

fmap2 :: (a -> b) -> Either a c -> Either b c

如果设置LANGUAGE pragma TypeOperators,GHC也接受

类型
fmap2 :: (a -> b) -> (a `Either` c) -> (b `Either` c)

在一个理想的世界中,这也会起作用:

fmap2 :: (a -> b) -> (`Either` c) a -> (`Either` c) b

这将为(`Either` c)提供一个Functor实例,但是正常运算符(及其部分)和类型运算符之间的相似性在这一点上会崩溃(除非我有一个GHC选项,我错过了!)

简而言之:你对仿函数的理解是可以接受的,但是由于缺乏类型级的lambda,你会感到很痛苦。

答案 9 :(得分:1)

嗯......关于“种类”的几句话怎么样?... 我想这可能会简化理解 记住什么是currying。即在ghci:

Prelude> let f x y z = x + y * z
f :: (Num a) => a -> a -> a -> a
Prelude> :t f 1
f 1 :: (Num t) => t -> t -> t
Prelude> :t f 1 2
f 1 2 :: (Num t) => t -> t
Prelude> :t f 1 2 3
f 1 2 3 :: (Num t) => t

与类型相同的东西。当你说Either那种类型是* -> * -> *时(即它需要两种类型并生成类型),当你说Either a种类是* -> *和{{1}时它是Either a b(顺便说一句*Monad a要求Functor a属于a,我记得。 因此,当您说类型* -> *表示仍然不完整的类型(需要绑定一些“参数”)时,Either afmap :: (a -> b) -> f a -> f b被{fmap :: (a -> b) -> (Either c) a -> (Either c) b替换时变为f 1}}。