Functor
类包含隐藏的第二个成员:
class Functor f where
fmap :: (a -> b) -> f a -> f b
(GHC.Base.<$) :: a -> f b -> f a
文档:
用相同的值替换输入中的所有位置。默认定义为
fmap . const
,但可以使用更高效的版本覆盖。
我想知道更多。为什么这个fmap . const
成语是一个单独的成员?替代实施如何更有效?这个组合器的应用是什么?
答案 0 :(得分:9)
它作为成员包含在内,允许用户根据速度进行自定义,我想这是因为它与>>
保持一致。
我认为在读者monad ((->) r)
的情况下可能会更快。
x <$ _ = const x
VS
x <$ fa = fmap (const x) fa = (const x) . fa
尽管如此,这确实是编译器优化的问题。并且,它似乎没有为读者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)
答案 1 :(得分:9)
<$
用法的另一个例子:
假设您有一个解析器仿函数P
和parser :: P A
。
f <$> parser
表示您需要解析某些内容,然后将f
应用于结果。
a <$ parser
表示您不需要解析任何内容(您对结果不感兴趣) - 您只需要识别,这可以更快。
参见例如regex-applicative库(注意Void
构造函数的用法)。
答案 2 :(得分: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
并且正在转换为与字符串匹配的解析器,同时做一些其他事情(吃空格,检查标记边界,&amp; c。)
这看起来非常简单,但老实说,当你使用一个不会产生你关心的值的解析器时,它会使Applicative
- y解析器定义更容易阅读 lot 必需的关键字等。否则你必须引入一堆额外的pure
或奇怪的括号或其他分散注意力的噪音。
至于为什么它是类型类的一部分,将类型多余的函数添加到类型类的通常原因是期望某些实例能够对其进行优化,例如: (>>)
。由于效率差异取决于实例(这就是重点!),那里没有单一的答案。但是,我无法立即想到任何可能会产生重大影响的明显例子。