我开始学习Haskell,但我遇到了一个我无法理解的问题。我有一个方法用于从键值列表列表中查找值(来自this page):
let findKey key xs = snd . head . filter (\(k,v) -> key == k) $ xs
我试着摆弄一下,决定以这种方式摆脱$ sign:
let findKey key xs = snd . head . filter (\(k,v) -> key == k) ( xs )
然而,它甚至没有解析(过滤器应用于太多的争论错误)。我已经读过$ sign用于简单地替换括号,我无法弄清楚为什么这种简单的代码更改很糟糕。有人可以向我解释一下吗?
答案 0 :(得分:29)
中缀运算符($)
只是"函数应用程序"。换句话说
f x -- and
f $ x
是一样的。因为在Haskell中,括号只用于消除优先级(对于元组符号和a few other minor places, see comments),我们也可以用其他几种方式编写上述内容
f x
f $ x
(f) x
f (x)
(f) (x) -- and even
(f) $ (x)
在每种情况下,上述表达式都表示相同的事情:"将函数f
应用于参数x
"。
那么为什么要使用这些语法呢? ($)
有两个原因
在第一种情况下,请考虑以下深度右嵌套函数应用程序
f (g (h (i (j x))))
阅读本文有点困难,有点难以知道你有正确数量的括号。然而,它只是"只是"一堆应用程序所以应该使用($)
来表示这个短语。确实有
f $ g $ h $ i $ j $ x
有些人发现这更容易阅读。更现代的风格也包含(.)
,以强调这个短语的整个左侧只是一个组合的功能管道
f . g . h . i . j $ x
如上所述,这句话与
相同(f . g . h . i . j) x
有时读起来更好。
有些时候我们希望能够传递功能应用的想法。例如,如果我们有一个功能列表
lof :: [Int -> Int]
lof = [ (+1), (subtract 1), (*2) ]
我们可能希望通过值对应用程序进行映射,例如将数字4
应用于每个函数
> map (\fun -> fun 4) lof
[ 5, 3, 8 ]
但由于这只是函数应用程序,我们还可以使用($)
上的节语法更加明确
> map ($ 4) lof
[ 5, 3, 8 ]
答案 1 :(得分:17)
运营商$
的优先级最低,因此
snd . head . filter (\(k,v) -> key == k) $ xs
读作
(snd . head . filter (\(k,v) -> key == k)) xs
而你的第二个表达是
snd . head . ( filter (\(k,v) -> key == k) xs )
答案 2 :(得分:7)
$
符号不是用于替换括号的神奇语法。它是一个普通的中缀运算符,在某种程度上像+
这样的运算符。
将括号括在( xs )
这样的单个名称周围总是等同于xs
1 。如果那是$
所做的,那么你会以任何方式得到同样的错误。
试着想象如果你有其他熟悉的操作符会发生什么,例如+
:
let findKey key xs = snd . head . filter (\(k,v) -> key == k) + xs
忽略+
对数字起作用的事实,所以这没有意义,只需考虑表达式的结构;哪些术语被识别为函数,哪些术语作为参数传递给它们。
事实上,使用+
确实可以成功解析和检查! (它为您提供了一个具有无意义类型类约束的函数,但如果您实现它们,它确实意味着什么)。让我们来看看中缀运算符是如何解决的:
let findKey key xs = snd . head . filter (\(k,v) -> key == k) + xs
最高优先级始终是正常的函数应用程序(只是相互编写术语,不涉及中缀运算符)。这里只有一个例子,filter
应用于lambda定义。这解决了#34;并且就解析其余运算符而言变成了单个子表达式:
let findKey key xs
= let filterExp = filter (\(k,v) -> key == k)
in snd . head . fileterExp + xs
下一个最高优先级是.
运算符。我们在这里有几个选择,都具有相同的优先权。 .
是右关联的,所以我们先取最右边的一个(但实际上不会改变我们选择的结果,因为{em>意味着 {{1} }是一个关联操作,但解析器无法知道):
.
请注意,let findKey key xs
= let filterExp = filter (\(k,v) -> key == k)
dotExp1 = head . filterExp
in snd . dotExp1 + xs
向右和向右抓取条。这就是优先权如此重要的原因。还剩下.
,其优先级仍然高于.
,所以接下来是:
+
我们已经完成了! let findKey key xs
= let filterExp = filter (\(k,v) -> key == k)
dotExp1 = head . filterExp
dotExp2 = snd . dotExp1
in dotExp2 + xs
在此处具有最低的运算符优先级,因此它最后得到它的参数,并最终成为整个表达式中最顶层的调用。请注意,+
优先级较低会阻止+
声明"声明"作为左边任何更高优先级应用程序的参数。如果他们中的任何一个优先级较低,那么他们最终会将整个表达式xs
作为一个参数,所以他们仍然无法进入dotExp2 + xs
;在xs
之前放置一个中缀运算符(任何中缀运算符)可以防止它被左边的任何东西声称为参数。
这实际上完全与在此表达式中解析xs
的方式相同,因为$
和.
碰巧具有相同的相对优先级, $
和.
有; +
旨在具有极低的优先级,因此它将以这种方式与左右相关的几乎所有其他运算符一起工作。
如果我们没有在$
电话和filter
之间放置一个中缀运营商,那么就会发生这种情况:
xs
正常功能应用程序首先出现。在这里,我们简单地将3个术语放在一起:let findKey key xs = snd . head . filter (\(k,v) -> key == k) xs
,filter
和(\(k,v) -> key == k)
。函数应用程序是左关联的,所以我们先取最左边的一对:
xs
还剩下另一个正常的应用程序,它仍然优先于let findKey key xs
= let filterExp1 = filter (\(k,v) -> key == k)
in snd . head . filterExp1 xs
,所以我们这样做:
.
现在是第一个点:
let findKey key xs
= let filterExp1 = filter (\(k,v) -> key == k)
filterExp2 = filterExp1 xs
in snd . head . filterExp2
我们已经完成了,这次整个表达式中最顶级的调用是最左边的let findKey key xs
= let filterExp1 = filter (\(k,v) -> key == k)
filterExp2 = filterExp1 xs
dotExp = head . filterExp2
in snd . dotExp
运算符。这次.
作为xs
的第二个参数被吸引了;这是我们想要的地方,因为filter
确实需要两个参数,但它会在函数组合链中留下filter
的结果,filter
应用于两个参数不能返回一个函数。我们想要的是将它应用于一个参数来提供一个函数,让该函数成为函数组合链的一部分,然后将整个函数应用于{{1 }}
使用filter
时,最终形式反映出我们使用xs
时的情况:
$
它的解析方式与我们+
完全相同,所以唯一的区别是let findKey key xs
= let filterExp = filter (\(k,v) -> key == k)
dotExp1 = head . filterExp
dotExp2 = snd . dotExp1
in dotExp2 $ xs
表示"将我的左参数添加到我的右参数&#34 ;,+
表示"将我的左参数作为函数应用于我的正确参数"。这就是我们想要发生的事情!好哇!
TLDR:坏消息是+
只是用括号括起来不起作用;它比这复杂得多。好消息是,如果您了解Haskell如何解析涉及中缀运算符的表达式,那么您就能理解$
的工作原理。就语言而言,没有什么特别之处;如果它不存在,你可以自己定义一个普通的操作员。
1 对$
之类的运算符进行括号也只是为$
提供了完全相同的函数,但现在它没有特殊的中缀语法,所以这确实会影响在这种情况下解析事物的方式。 (+)
不是这样,因为它只是一个名字。
答案 3 :(得分:4)
“$
符号用于简单地替换括号”非常正确 - 但是,它有效地将所有内容括在两个方面!所以
snd . head . filter (\(k,v) -> key == k) $ xs
实际上是
( snd . head . filter (\(k,v) -> key == k) ) ( xs )
当然,xs
周围的parens在这里是不需要的(无论如何都是“原子”),因此在这种情况下相关的左侧是左侧。实际上,这通常发生在Haskell中,因为一般的哲学是尽可能地将函数视为抽象实体,而不是将函数应用于某个参数时所涉及的特定值。你的定义也可以写成
let findKey key xs' = let filter (\(k,v) -> key == k) $ xs
x0 = head xs'
v0 = snd x0
in v0
这将非常明确,但所有这些中间值都不是很有趣。所以我们更喜欢简单地将函数“无点”链接到.
。这通常会让我们摆脱很多样板,实际上对于你的定义,可以做到以下几点:
η-减少xs
。这个论点刚刚传递给函数链,所以我们不妨说“findKey key
”是那个链,无论你提供什么参数“!
findKey key = snd . head . filter (\(k,v) -> key == k)
接下来,我们可以避免这种明确的lambda:\(k,v) -> k
只是fst
函数。然后,您需要与key
进行后期比较。
findKey key = snd . head . filter ((key==) . fst)
我会停在这里,因为过多的无点是没有意义且难以理解的。但是你可以继续下去:现在围绕这个论点出现了新的问题到key
,我们可以再次摆脱那些$
的人。但要小心:
"findKey key = snd . head . filter $ (key==) . fst"
不正确,因为$
会再次为双方括起来,但(snd . head . filter)
的输入不是很好。实际上snd . head
只应在filter
的两个参数之后出现。使用函数仿函数进行此类后期合成的可能方法是:
findKey key = fmap (snd . head) . filter $ (key==) . fst
...我们可以继续前进并摆脱key
变量,但它看起来不太好看。我想你已经明白了......
答案 4 :(得分:4)
其他答案详细评论了($)
如何替换括号,因为($)
被定义为具有正确优先权的应用程序运算符。
我想添加GHC,为了能够用($)
替换括号,在($)
的定义中可以看到更多的魔法。 }。当涉及更高级别的函数时,类型系统在通过标准应用程序时可以处理更高级别的参数(如在f x
中),但是当通过应用程序运算符传递时不是(如f $ x
)。为了解决这个问题,GHC在类型系统中以特殊方式处理($)
。实际上,以下代码显示,如果我们定义并使用自己的应用程序运算符($$)
,则类型系统不会应用相同的魔术处理。
{-# LANGUAGE RankNTypes #-}
-- A higher rank function
higherRank :: (forall a. a -> a -> a) -> (Int, Char)
higherRank f = (f 3 4, f 'a' 'b')
-- Standard application
test0 :: (Int, Char)
test0 = higherRank const -- evaluates to (3,'a')
-- Application via the ($) operator
test1 :: (Int, Char)
test1 = higherRank $ const -- again (3, 'a')
-- A redefinition of ($)
infixr 0 $$
($$) :: (a -> b) -> a -> b
($$) = ($)
test2 :: (Int, Char)
test2 = higherRank $$ const -- Type error (!)
-- Couldn't match expected type `forall a. a -> a -> a'
-- with actual type `a0 -> b0 -> a0'