跳到主要内容

论运算符优先级的重要性

· 阅读需 6 分钟
Chengyu HAN
Open Source Contributor

没定义运算符优先级而引发的一系列锅

日常看书,看到Applicative typeclass,遂动手实现一下:

{-# LANGUAGE NoImplicitPrelude #-}

import Prelude hiding (Applicative(..))

class Functor f => Applicative f where
pure :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b

instance Applicative Maybe where
pure = Just
Just f <*> m = fmap f m
Nothing <*> _ = Nothing

main :: IO ()
main = do
putStr $ (shows $ Just (+) <*> Just 1 <*> Just 2) "\n"
putStr $ (shows $ (+) `fmap` Just 1 <*> Just 2) "\n"
-- 以下这句会报错
putStr $ (shows $ (+) <$> Just 1 <*> Just 2) "\n"

之前一直都这么写,都没问题,然后这里最后一句会报错,编译不通过

09-Typeclass.hs:17:13: error:
? No instance for (Show (a0 -> a0)) arising from a use of ‘shows’
(maybe you haven't applied a function to enough arguments?)
? In the expression: shows $ (+) <$> Just 1 <*> Just 2
In the second argument of ‘($)’, namely
‘(shows $ (+) <$> Just 1 <*> Just 2) "\n"’
In a stmt of a 'do' block:
putStr $ (shows $ (+) <$> Just 1 <*> Just 2) "\n"

09-Typeclass.hs:17:21: error:
? Ambiguous type variable ‘a0’ arising from a use of ‘+’
prevents the constraint ‘(Num a0)’ from being solved.
Probable fix: use a type annotation to specify what ‘a0’ should be.
These potential instances exist:
instance Num Integer -- Defined in ‘GHC.Num’
instance Num Double -- Defined in ‘GHC.Float’
instance Num Float -- Defined in ‘GHC.Float’
...plus two others
...plus three instances involving out-of-scope types
(use -fprint-potential-instances to see them all)
? In the first argument of ‘(<$>)’, namely ‘(+)’
In the second argument of ‘($)’, namely ‘(+) <$> Just 1 <*> Just 2’
In the expression: shows $ (+) <$> Just 1 <*> Just 2

09-Typeclass.hs:17:37: error:
? No instance for (Num (a1 -> a0)) arising from the literal ‘1’
(maybe you haven't applied a function to enough arguments?)
? In the first argument of ‘Just’, namely ‘1’
In the first argument of ‘(<*>)’, namely ‘Just 1’
In the second argument of ‘(<$>)’, namely ‘Just 1 <*> Just 2’

09-Typeclass.hs:17:48: error:
? Ambiguous type variable ‘a1’ arising from the literal ‘2’
prevents the constraint ‘(Num a1)’ from being solved.
Probable fix: use a type annotation to specify what ‘a1’ should be.
These potential instances exist:
instance Num Integer -- Defined in ‘GHC.Num??
instance Num Double -- Defined in ‘GHC.Float’
instance Num Float -- Defined in ‘GHC.Float’
...plus two others
...plus three instances involving out-of-scope types
(use -fprint-potential-instances to see them all)
? In the first argument of ‘Just’, namely ‘2’
In the second argument of ‘(<*>)’, namely ‘Just 2’
In the second argument of ‘(<$>)’, namely ‘Just 1 <*> Just 2’

但单独拿出来,不重定义ap,则可以正常运行

main :: IO ()
main = do
putStr $ (shows $ Just (+) <*> Just 1 <*> Just 2) "\n"
putStr $ (shows $ (+) `fmap` Just 1 <*> Just 2) "\n"
putStr $ (shows $ (+) <$> Just 1 <*> Just 2) "\n"

输出结果

ghci> main
Just 3
Just 3
Just 3

ghci给出的报错信息不明确,稍作尝试:

先注释掉最后一句

ghci> :t (+) `fmap` Just 1 <*> Just 2
(+) `fmap` Just 1 <*> Just 2 :: Num b => Maybe b
ghci> :t (+) <$> Just 1 <*> Just 2
(+) <$> Just 1 <*> Just 2
:: (Num (a -> a1), Num a, Num a1) => Maybe (a1 -> a1)

发现返回了一个函数,上面两句的差别只有fmap<$>但后者就是前者的别名

继续做尝试:

ghci> :t (+) `fmap` Just 1
(+) `fmap` Just 1 :: Num a => Maybe (a -> a)
ghci> :t (+) <$> Just 1
(+) <$> Just 1 :: Num a => Maybe (a -> a)

看上去也没问题,但是继续传值,结果就不一样了

看类型签名也没问题

ghci> :t fmap
fmap :: Functor f => (a -> b) -> f a -> f b
ghci> :t (<$>)
(<$>) :: Functor f => (a -> b) -> f a -> f b
ghci> :t (<*>)
(<*>) :: Applicative f => f (a -> b) -> f a -> f b

又怀疑是<*>的定义有问题,翻了翻文档(Control.Applicative) 该有的定义也不少,而且Just (+) <*> Just 1 <*> Just 2也能正常计算

尝试定义liftA2也是报错

liftA2 :: Applicative f => (a -> b -> c) -> f a -> f b -> f c
liftA2 f a b = f <$> a <*> b

报错

09-Typeclass.hs:19:16: error:
? Couldn't match type ‘c’ with ‘b -> c’
‘c’ is a rigid type variable bound by
the type signature for:
liftA2 :: forall (f :: * -> *) a b c.
Applicative f =>
(a -> b -> c) -> f a -> f b -> f c
at 09-Typeclass.hs:18:11
Expected type: f c
Actual type: f (b -> c)
? In the expression: f <$> a <*> b
In an equation for ‘liftA2’: liftA2 f a b = f <$> a <*> b
? Relevant bindings include
b :: f b (bound at 09-Typeclass.hs:19:12)
f :: a -> b -> c (bound at 09-Typeclass.hs:19:8)
liftA2 :: (a -> b -> c) -> f a -> f b -> f c
(bound at 09-Typeclass.hs:19:1)

09-Typeclass.hs:19:22: error:
? Couldn't match type ‘a’ with ‘b -> a’
‘a’ is a rigid type variable bound by
the type signature for:
liftA2 :: forall (f :: * -> *) a b c.
Applicative f =>
(a -> b -> c) -> f a -> f b -> f c
at 09-Typeclass.hs:18:11
Expected type: f (b -> a)
Actual type: f a
? In the first argument of ‘(<*>)’, namely ‘a’
In the second argument of ‘(<$>)’, namely ‘a <*> b’
In the expression: f <$> a <*> b
? Relevant bindings include
b :: f b (bound at 09-Typeclass.hs:19:12)
a :: f a (bound at 09-Typeclass.hs:19:10)
f :: a -> b -> c (bound at 09-Typeclass.hs:19:8)
liftA2 :: (a -> b -> c) -> f a -> f b -> f c
(bound at 09-Typeclass.hs:19:1)

依旧是参数类型不匹配

中间看官方库实现时,看到(GHC-Base.html#Applicative)里开头的 NOTA BENE

NOTA BENE: Do NOT use ($) anywhere in this module! The type of ($) is
slightly magical (it can return unlifted types), and it is wired in.
But, it is also *defined* in this module, with a non-magical type.
GHC gets terribly confused (and *hangs*) if you try to use ($) in this
module, because it has different types in different scenarios.

This is not a problem in general, because the type ($), being wired in, is not
written out to the interface file, so importing files don't get confused.
The problem is only if ($) is used here. So don't!

果然haskell到处都是魔法,但最后一段又说导出的库不受影响。 我把$换为括号也没什么效果。

然后我想到群里提问,想了一下,还是先小黄鸭debug一下,于是假装已经提问了,出去走了几圈,半路上想ap的定义和库里的一样,应该没什么问题,估计锅还在<$>上,<$>的定义就是(<$>) = fmap,理论上应该没问题,fmap是一个函数要加反引号才能当做中缀运算符,进一步联想到到运算符有优先级,函数默认优先级最高。想到这一点就有继续做尝试。

然后果然是这样。加上 infixl 4 <*>就好使了