$和()之间的区别

时间:2014-06-17 18:43:52

标签: haskell functional-programming

我开始学习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用于简单地替换括号,我无法弄清楚为什么这种简单的代码更改很糟糕。有人可以向我解释一下吗?

5 个答案:

答案 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"。

那么为什么要使用这些语法呢? ($)有两个原因

  1. 它的优先级非常低,所以它有时可以代表很多括号
  2. 很高兴有一个明确的功能应用程序操作名称

  3. 在第一种情况下,请考虑以下深度右嵌套函数应用程序

    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

这将非常明确,但所有这些中间值都不是很有趣。所以我们更喜欢简单地将函数“无点”链接到.。这通常会让我们摆脱很多样板,实际上对于你的定义,可以做到以下几点:

  1. η-减少xs。这个论点刚刚传递给函数链,所以我们不妨说“findKey key那个链,无论你提供什么参数“!

        findKey key = snd . head . filter (\(k,v) -> key == k)
    
  2. 接下来,我们可以避免这种明确的lambda:\(k,v) -> k只是fst函数。然后,您需要与key进行后期比较。

        findKey key = snd . head . filter ((key==) . fst)
    
  3. 我会停在这里,因为过多的无点是没有意义且难以理解的。但是你可以继续下去:现在围绕这个论点出现了新的问题到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'