contramap的目的是什么?

3

我阅读了许多关于contramap的文章,并发现https://hackage.haskell.org/package/contravariant-1.4.1/docs/Data-Functor-Contravariant.html#g:3是最好的。

无论如何,我发现如何使用例如:

*Lib Data.Functor.Contravariant> a = Predicate (\x -> x > 20)
*Lib Data.Functor.Contravariant> :t contramap
contramap :: Contravariant f => (a -> b) -> f b -> f a
*Lib Data.Functor.Contravariant> :t contramap (\x -> x * 2)
contramap (\x -> x * 2) :: (Num b, Contravariant f) => f b -> f b
*Lib Data.Functor.Contravariant> :t contramap (\x -> x * 2) a
contramap (\x -> x * 2) a :: (Ord b, Num b) => Predicate b
*Lib Data.Functor.Contravariant> x = contramap (\x -> x * 2) a
*Lib Data.Functor.Contravariant> getPredicate x 45
True 

但是我无法想象它在哪里有用。

在我上面发布的网站上,它说:

而在Haskell中,人们可以将Functor视为包含或生成值,协变Functor是一种可以被视为消耗值的Functor。

看看Functor的定义:

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

它消耗了类型 a 的值,并生成了类型 b 的值。在 contramap 中,它消耗了该值。

class Contravariant (f :: * -> *) where
  contramap :: (a -> b) -> f b -> f a

它使用哪种变量类型,a 还是 b

还有一个来自于https://www.fpcomplete.com/blog/2016/11/covariance-contravariance的问题,关于正位置和负位置。网站上说:

  • 正位置:类型变量是函数的结果/输出/范围/值域
  • 负位置:类型变量是函数的参数/输入/定义域

看一下 contramap 类型定义:

contramap :: (a -> b) -> f b -> f a

作者指的是哪种类型的变量?

你误解了引用部分的意思;但它也不是很清晰的语言,你应该避免阅读任何以“可以想象…”开头的解释。话虽如此,它的意思是,“一个函子”(一个值x :: F A,其中F具有Functor F和一些A)可以被看作是产生类型为A的值的计算;而一个逆变函子可以被看作是消耗类型为A的值。 - user2407038
3个回答

9
一个函子 f 被认为是“包含”或“生成”值,因为 f a 就像一个包含 a 值的容器。fmap 允许你转换 f 持有的值。
例子:
  • [a] “包含”了一定数量的类型为 a 的值
  • IO a 可能会进行一些 IO 并“返回”或“生成”一个类型为 a 的值
  • (->) r a 对于每个 r 的值,“包含”一个类型为 a 的值
现在,一个 Contravariant f 是可以“接收”或“消耗”值的东西。 contramap 允许你在它们被消耗之前转换 f a 中的内容。
这方面的主要例子通常使用类似于
newtype Op r a = Op { runOp :: a -> r }
(注意你使用的 Predicate 看起来只是 Op Bool
现在我们有了一个可以“消耗”类型为 a 的值的东西。(这个类比对于 Op (IO ()) 可能更有意义)
继续考虑值的“消耗”这个例子,考虑 o = Op (\x -> putStrLn x) :: Op (IO ()) String,现在如果我们想要使用 o,但是对于类型为 Show a => a 的值呢?这就是 contramap 的用处!
contramap show o :: Show a => Op (IO ()) a
(注意,在这种简单情况下,runOp (contramap show o) 就是 print

编辑:
关于 Contravariant 的另一个有趣的事情是它的组合方式。
给定 Contravariant c, Contravariant d, Functor f
newtype Compose f g a = Compose { runCompose :: f (g a) }
我们有:

  • Compose f c 同样是 Contravariant
    contramap f (Compose fca) = Compose $ fmap (contramap f) fca
  • Compose c f 同样是 Contravariant
    contramap f (Compose cfa) = Compose $ contramap (fmap f) cfa
  • Compose c d 事实上也是一个Functor
    fmap f (Compose cda) = Compose $ contramap (contramap f) cda

1
数据类型的定义必须像 Op { runOp :: a -> r } 这样,而不是像 Maybe a 上的 contramap,因为它会产生一个值,这是没有意义的。 - softshipper
1
Maybe a can be thought of as a list of length 0 or 1, that is, it 'contains' 0 or 1 values of type a. You cannot define contramap for Maybe because if you did, you would have phantom = contramap (const ()) . fmap (const ()) :: (Functor f, Contravariant f) => f a -> f b, and that doesn't make sense since then we could convert any a to any b by using fromJust . phantom . Just. For an example of a Functor that is also Contravariant, there's newtype Const c a = Const { getConst :: c }, where the fmap f = contramap f = Const . getConst - Mor A.

5

Look at the definition of the functor:

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

It consumes the value of type a and it produces the value of type b.

这不够精确,这也是你困惑的根源。

特别地,在fmap类型中有三个不同的对象:一个函数a -> b,一个“函子”值f a和一个“函子”值f b,并且无论a是否被消耗或产生,这些对象之间都存在差异,以及b是否被消耗或产生。

  • 类型为a -> b的函数消耗a并产生b。(这是您说过的句子,但加上了一个新的说明,指定它应用于哪个对象。)
  • 如果f是一个Functor,那么类型为f a的值“产生”a
  • 如果f是一个Functor,那么类型为f b的值“产生”b

我们可以将这些观察结果扩展到更大的函数类型。

  • 如果f是一个Functor,那么类型为f a -> f b的函数会消耗f a并产生f b;因为functor是正向的,所以这意味着该函数会消耗a并产生b
  • 如果f是一个Functor,那么类型为(a -> b) -> f a -> f b的函数会消耗一个a -> b -- 也就是说,在实现中,我们将生成a来提供给a -> b函数,并消耗它返回的b!它这样做是为了产生一个f a -> f b,它消耗a并产生b
注意,在上述描述中,“consume”和“produce”的角色经常交替出现,甚至在被称为fmap的单个对象内部也是如此;因此,说“fmap消耗a并产生b”并不能说明全部情况。
下面的陈述也以类似的方式不够精确:
在Haskell中,可以将Functor视为包含或生成值,而逆变Functor则是可以被认为是消耗值的Functor。
你将其解释为(函数)对象fmap包含值,而contramap则生成值。但这不是原意;相反,该陈述是关于(逆变)functorial值本身而非应用于它们的(contra)maps的。更准确的表述是:
在Haskell中,一个类型为f a(其中f是一个Functor)的值可以被视为包含或生成a值,而一个逆变协变函子f产生的类型为f a的值可以被视为消耗a值。关于你对正负位置的问题,你可能会喜欢我在SO上此主题的一些先前讨论

2

首先,查看签名,所有信息都在那里。

其次,如果您想要"感受"这些事情,请考虑一下函子并试图用语言描述它们所做的事情。

函子对其包含类型执行某些操作。这可以是该类型的消费或生产。明显List“产生”该类型的实例。Printer显然“消耗”该类型的实例。

当给定一个函数时,生产者会构建一个将产量“之后”转换的函数。从一个简单的函数中,您可以获得一个为该函数域提供生产者的生产者。 fmap的签名是"协变"的。

当给定一个函数时,消费者会构建一个将消费量“之前”转换的函数。提供了一个从int到string的转换器函数,已经持有string消费者的消费者将轻松地产生一个int消费者:通过在消费之前应用该函数。 contramap的签名是"逆变"的。

我们可以说函数"转换",而函子"消费"或"生产"。


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