我一直在阅读一篇文章(http://comonad.com/reader/2012/abstracting-with-applicatives/)并在那里找到以下代码片段:
newtype Compose f g a = Compose (f (g a)) deriving Show
instance (Functor f, Functor g) => Functor (Compose f g) where
fmap f (Compose x) = Compose $ (fmap . fmap) f x
实际上(fmap . fmap)
怎么样?
他们的类型是:
(.) :: (a -> b) -> (r -> a) -> (r -> b)
fmap :: (a -> b) -> f a -> f b
fmap :: (a -> b) -> f a -> f b
现在从这里我可以看出fmap . fmap
将不会进行类型检查?
首先让我们将类型变量的名称更改为唯一:
(.) :: (a -> b) -> (r -> a) -> (r -> b)
fmap :: Functor f => (c -> d) -> f c -> f d
fmap :: Functor g => (x -> y) -> g x -> g y
现在.
的第一个参数有a -> b
类型,我们提供(c -> d) -> (f c -> f d)
类型的参数,所以a
是c -> d
而b
是f c -> f d
。所以到目前为止我们有:
(.) :: Functor f => -- Left operand
((c -> d) -> (f c -> f d)) ->
-- Right operand
(r -> (c -> d)) ->
-- Result
(r -> (f c -> f d))
.
的第二个参数有类型r -> a
a.k.a.r -> (c -> d)
,我们给出的参数有(x -> y) -> (g x -> g y)
类型,所以r
变成x -> y
,c
变成g x
而d
变成g y
。所以现在我们有:
(.) :: (Functor f, Functor g) => -- Left operand
((g x -> g y) -> (f (g x) -> f (g y))) ->
-- Right operand
((x -> y) -> (g x -> g y)) ->
-- Result
(x -> y) -> f (g x) -> f (g y)
fmap.fmap :: (Functor f, Functor g) => (x -> y) -> f (g x) -> f (g y)
表达式fmap . fmap
有两个fmap
实例,原则上它们可以有不同的类型。所以让我们说他们的类型是
fmap :: (x -> y) -> (g x -> g y)
fmap :: (u -> v) -> (f u -> f v)
我们的工作是统一类型(相当于这些类型变量之间的相等关系),以便第一个fmap
的右侧与第二个fmap
的左侧相同。希望你能看到,如果你设置u = g x
和v = g y
,你最终会得到
fmap :: ( x -> y) -> ( g x -> g y )
fmap :: (g x -> g y) -> (f (g x) -> f (g y))
现在组成的类型是
(.) :: (b -> c) -> (a -> b) -> (a -> c)
为了解决这个问题,您可以选择a = x -> y
和b = g x -> g y
以及c = f (g x) -> f (g y)
以便可以编写类型
(.) :: ((g x -> g y) -> (f (g x) -> f (g y))) -> ((x -> y) -> (g x -> g y)) -> ((x -> y) -> (f (g x) -> f (g y)))
这是非常笨拙的,但它只是(.)
的原始类型签名的专业化。现在你可以检查一切是否匹配,以便fmap . fmap
typechecks。
另一种方法是从相反的方向接近它。例如,假设你有一些具有两层functoriality的对象
>> let x = [Just "Alice", Nothing, Just "Bob"]
并且你有一些功能可以将爆炸添加到任何字符串
bang :: String -> String
bang str = str ++ "!"
你想将爆炸添加到x
中的每个字符串中。你可以从String -> String
到Maybe String -> Maybe String
与一级fmap
fmap bang :: Maybe String -> Maybe String
你可以和[Maybe String] -> [Maybe String]
的另一个应用去fmap
fmap (fmap bang) :: [Maybe String] -> [Maybe String]
这样做我们想要的吗?
>> fmap (fmap bang) x
[Just "Alice!", Nothing, Just "Bob!"]
让我们写一个实用函数fmap2
,它接受任何函数f
并将fmap
应用于它两次,这样我们就可以编写fmap2 bang x
了。这看起来像这样
fmap2 f x = fmap (fmap f) x
你当然可以从两边放下x
fmap2 f = fmap (fmap f)
现在你意识到g (h x)
模式与(g . h) x
相同,所以你可以写
fmap2 f = (fmap . fmap) f
所以你现在可以从两边放下f
fmap2 = fmap . fmap
这是你感兴趣的功能。所以你看到fmap . fmap
只需要一个函数,并将fmap
应用于它两次,这样就可以通过两个级别的functoriality来提升它。
老问题,但对我来说,概念上,fmap
代表“把a -> b
带到'一级',到f a -> f b
”。
因此,如果我有一个a -> b
,我可以fmap
它给我一个f a -> f b
。
如果我有一个f a -> f b
,我可以fmap
它再次给我一个g (f a) -> g (f a)
。将f a -> f b
的功能提升到新的高度 - 一个新的水平。
因此,“fmapping”一次提升功能。 fmapping两次举升提升功能...所以,双提升。
放入haskell语法的语言:
f :: a -> b
fmap f :: f a -> f b
fmap (fmap f) :: g (f a) -> g (f b)
fmap (fmap (fmap f)) :: h (g (f a)) -> h (g (f b))
请注意每个连续的fmap
如何将原来的a -> b
提升到另一个新的水平。所以,
fmap :: (a -> b) -> ( f a -> f b )
fmap . fmap :: (a -> b) -> ( g (f a) -> g (f b) )
fmap . fmap . fmap :: (a -> b) -> (h (g (f a)) -> h (g (f a)))
任何返回与其输入相同的arity函数的“高阶函数”都可以做到这一点。以zipWith :: (a -> b -> c) -> ([a] -> [b] -> [c])
为例,它接受一个带两个参数的函数并返回一个带有两个参数的新函数。我们可以用同样的方式链接zipWith
s:
f :: a -> b -> c
zipWith f :: [a] -> [b] -> [c]
zipWith (zipWith f) :: [[a]] -> [[b]] -> [[c]]
所以
zipWith :: (a -> b -> c) -> ( [a] -> [b] -> [c] )
zipWith . zipWith :: (a -> b -> c) -> ([[a]] -> [[b]] -> [[c]])
liftA2
的工作原理大致相同:
f :: a -> b -> c
liftA2 f :: f a -> f b -> f c
liftA2 (liftA2 f) :: g (f a) -> g (f b) -> g (f c)
在镜头库的现代实现中使用的一个相当令人惊讶的例子是traverse
:
f :: a -> IO b
traverse f :: f a -> IO ( f b )
traverse (traverse f) :: g (f a) -> IO ( g (f b) )
traverse (traverse (traverse f)) :: h (g (f a)) -> IO (h (g (f b)))
所以你可以拥有这样的东西:
traverse :: (a -> m b) -> ( f a -> m ( f b ))
traverse . traverse :: (a -> m b) -> (g (f a) -> m (g (f b)))