15

该类Functor包含一个隐藏的第二个成员:

class Functor f where
  fmap :: (a -> b) -> f a -> f b
  (GHC.Base.<$) :: a -> f b -> f a

文档:

用相同的值替换输入中的所有位置。默认定义是fmap . const,但这可能会被更高效的版本覆盖。

我想知道更多。为什么这个fmap . const成语是一个单独的成员?替代实现如何更有效?这个组合器有什么应用?

4

3 回答 3

11

它作为一个成员包含在允许用户自定义它的速度,我猜是因为它使它与>>.

我认为在 reader monad 的情况下它可能会更快((->) r)

x <$ _ = const x

对比

x <$ fa = fmap (const x) fa = (const x) . fa

虽然,这确实是编译器优化的问题。而且,它似乎没有为 base 中的 reader monad 定义。

它还可能导致严格集合的性能提升。即

data Strict a = Strict !a

instance Functor Strict where
   fmap f (Strict a) = Strict (f a)
   x <$ _ = Strict x

这不符合函子定律,但尽管如此,您可能希望在某些情况下这样做。

第三个例子来自无限集合。考虑无限列表

data Long a = Cons a (Long a)

instance Functor Long where
  fmap f (Cons x xs) = Cons (f x) (fmap f xs)

效果很好,但想想

countUpFrom x = Cons x (countUpFrom (x+1))
ones = 1 <$ (countUpFrom 0)

现在,我们的定义将扩展到

ones = 1 <$ (countUpFrom 0)
   = fmap (const 1) (countUpFrom 0) 
   = Cons (const 1 0) (fmap (const 1) (countUpFrom 1)
   = Cons (const 1 0) (Cons (const 1 1) (fmap (const 1) (countUpFrom 2))

Cons也就是说,当您遍历此列表时,它将分配一大堆单元格。而另一方面,如果你定义

x <$ _ = let xs = Cons x xs in xs

ones = 1 <$ countUpFrom 0
 = let xs = Cons 1 xs in xs

这已经打结了。一个更极端的例子是无限树

data ITree a = ITree a (ITree a) (ITree a)
于 2012-12-30T03:29:31.553 回答
11

另一个<$使用示例:

假设您有一个解析器仿函数P,并且parser :: P A.

f <$> parser意味着您需要解析某些内容然后应用于f结果。

a <$ parser意味着你不需要解析任何东西(你对结果不感兴趣)——你只需要识别,这可以更快。

参见例如regex-applicative库(注意Void构造函数的使用)。

于 2012-12-30T11:20:51.947 回答
3

这是我目前正在编写的一些代码片段,它们可能会让您了解使用此组合器的目的:

pPrimType = choice
    [ WIPrimIntType <$> flag "unsigned" <*> pIntTypeSize
    , WIPrimFloatType <$> flag "unrestricted" <*> pFloatTypeSize
    , WIPrimBoolType <$ "boolean"
    , WIPrimByteType <$ "byte"
    , WIPrimOctetType <$ "octet"
    ]

pConst = WIConst 
     <$  "const"
     <*> pConstType
     <*> pIdent
     <*  "="
     <*> pConstValue
     <*  semicolon

如果字符串文字看起来很奇怪,那是因为我已OverloadedStrings启用并且正在将它们转换为与字符串匹配的解析器,同时执行其他一些操作(吃空格、检查标记边界等)

这看起来很微不足道,但老实说,当您使用不产生您关心的值的解析器(例如必需的关键字等)时,它使Applicative-y 解析器定义更具可读性。否则你必须引入一堆额外pure的 s 或奇怪的括号或其他分散注意力的噪音。

至于为什么它是类型类的一部分,向类型类添加其他多余函数的通常原因是期望某些实例能够优化它,例如(>>). 由于效率差异取决于实例(这就是重点!)那里没有单一的答案。不过,我无法立即想到任何明显的例子,它会产生重大影响。

于 2012-12-30T02:58:39.243 回答