【问题标题】:How (fmap . fmap) typechecks(fmap . fmap) 如何进行类型检查
【发布时间】:2014-04-12 12:52:57
【问题描述】:

我一直在浏览一篇文章(http://comonad.com/reader/2012/abstracting-with-applicatives/)并在那里找到了以下sn-p代码:

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 将在哪个类型检查?

【问题讨论】:

    标签: haskell functor


    【解决方案1】:

    首先让我们将类型变量的名称更改为唯一的:

    (.)  :: (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 又名 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)
    

    【讨论】:

    • 谢谢,最后一步如何简化类型? (((g x -> g y) -> (f (g x) -> f (g y))) 变为 (x -> y) ,其余类型签名也是如此。
    • @Sibi 在最后一步中,我所做的就是删除两个参数的类型(因为它们已被应用)。我不会用任何东西替换任何东西,只是在结果类型之前删除任何东西。
    • @Sibi 我更改了格式并添加了 cmets 以使其更加明显。
    • 它也帮助我使用 GHCi 来查询类型。如果你做:t (fmap .),你可以看到应用左参数如何给你(fmap .) :: Functor f => (a1 -> a -> b) -> a1 -> f a -> f b,如果我没记错的话,它本质上等同于右操作数和应用左操作数后的结果。不过很好的答案,有助于改变我对 Haskell 的理解。
    【解决方案2】:

    表达式fmap . fmap 有两个fmap 实例,原则上它们可以有不同的类型。所以假设他们的类型是

    fmap :: (x -> y) -> (g x -> g y)
    fmap :: (u -> v) -> (f u -> f v)
    

    我们的工作是统一类型(这相当于提出这些类型变量之间的相等关系),以便第一个 fmap 的右侧与第二个 @987654325 的左侧相同@。希望你能看到,如果你设置了u = g x 和v = g y,你最终会得到

    fmap :: (  x ->   y) -> (   g x  ->    g y )
    fmap :: (g x -> g y) -> (f (g x) -> f (g y))
    

    现在compose的类型是

    (.) :: (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 类型检查。


    另一种方法是从相反的方向接近它。假设您有一些具有两个功能级别的对象,例如

    >> 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
    

    您可以通过fmap的另一个应用程序转到[Maybe String] -> [Maybe String]

    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 应用到它两次,这样就可以通过两个层次的函数来提升它。

    【讨论】:

      【解决方案3】:

      老问题,但对我来说,从概念上讲,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”once 将函数提升一次。 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)))
      

      任何返回与其输入具有相同元数的函数的“高阶函数”都可以做到这一点。以zipWith :: (a -> b -> c) -> ([a] -> [b] -> [c]) 为例,它接受一个带有两个参数的函数并返回一个带有两个参数的新函数。我们可以用同样的方式链接zipWiths:

      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)
      

      在 lens 库的现代实现中被大量使用的一个相当令人惊讶的例子是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)))
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2014-03-30
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2015-12-24
        • 1970-01-01
        相关资源
        最近更新 更多