Haskell多参数函数中美元符号($)的使用问题解析
先看你在GHCi里的交互记录:
Prelude> p a b c=1 Prelude> p $ 2 $ 2 $ 5 tons of error...... Prelude> ($$)=($) Prelude> p $$ 2 $$ 2 $$ 5 1 Prelude> p $$ [2..3] $$ 4 $$ 5 1 Prelude> p $$ [1]++[2..3] $$ 4 $$ 5 tons of error...... Prelude> p $$ [1]++[2..3] $$ [1]++[2..3] $$ 5 of course, tons of error......
接下来逐个解答你的问题:
问题1:为何p $ 2 $ 2 $ 5使用($)会报错?
这完全是$的结合性和优先级导致的。Haskell里内置的$是右结合运算符,优先级是所有运算符里最低的(优先级0)。所以这个表达式会被GHCi解析成:
p $ (2 $ (2 $ 5))
先看最内层的2 $ 5:$的作用是把右边的参数传给左边的函数,但2是个数值,根本不是函数类型,这直接触发类型错误。退一步说,就算这里没问题,最终p也只接收到一个参数,而p需要三个参数,双重错误自然会炸锅。
问题2:为何自定义运算符($$)在p $$ 2 $$ 2 $$ 5中可正常运行?
关键点在于:自定义中缀运算符默认的规则和内置$完全不同。Haskell规定,如果你没有显式声明自定义运算符的优先级和结合性,它默认是左结合,并且优先级是最高的(优先级9)。
所以p $$ 2 $$ 2 $$5会被解析成:
(((p $$ 2) $$ 2) $$ 5)
因为$$等于($)(也就是函数应用),这等价于:
(((p 2) 2) 5)
正好是p接收三个参数的正确方式,自然返回1。
问题3:为何($$)在p $$ [1]++[2..3] $$ 4 $$ 5中会报错?
还是优先级的锅!++运算符的优先级是5,而自定义$$的默认优先级是9(比5高)。优先级高的运算符会先结合,所以这个表达式会被拆成:
((p $$ [1]) ++ [2..3]) $$ 4 $$5
先看p $$ [1],这其实是p [1],返回的是一个需要两个参数的函数(因为p是三元函数)。接下来你试图把这个函数和列表[2..3]用++拼接——函数和列表类型完全不兼容,这就触发了类型错误,自然报错。
问题4:是否存在更优雅的写法来正确实现p $$ [1]++[2..3] $$ [1]++[2..3] $$ 5的功能?
当然有,核心思路是让++的表达式先被计算,再作为参数传给p,这里给你三种常用方案:
方案1:给++表达式加括号
直接用括号把[1]++[2..3]包起来,强制让它先计算:
p $$ ([1]++[2..3]) $$ ([1]++[2..3]) $$ 5
这样因为括号的优先级最高,会先算出两个列表,再通过左结合的$$依次传给p,最终等价于p ([1]++[2..3]) ([1]++[2..3]) 5,完美运行。
方案2:重新声明$$的优先级和结合性
如果你想让$$的行为和内置$类似,但又想用这个名字,可以显式声明它的优先级和结合性,让它比++优先级低:
infixr 0 $$ -- 和内置$一样,右结合,优先级0 ($$) = ($)
这样p $$ [1]++[2..3] $$ [1]++[2..3] $$5会被解析成:
p $$ ([1]++[2..3]) $$ ([1]++[2..3]) $$5
因为$$优先级比++低,所以++会先计算,同样能得到正确结果。
方案3:直接用常规函数应用写法
其实最直白的写法反而可能最优雅,完全不用运算符:
p ([1]++[2..3]) ([1]++[2..3]) 5
没有任何运算符优先级的坑,一眼就能看懂。
内容的提问来源于stack exchange,提问作者MaxC

