免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
最近访问板块 发新帖
查看: 4016 | 回复: 2
打印 上一主题 下一主题

Haskell 中的 $ 操作符(译) [复制链接]

论坛徽章:
0
跳转到指定楼层
1 [收藏(0)] [报告]
发表于 2009-05-01 18:45 |只看该作者 |倒序浏览
本文可以被任意转载、优化修改,无须注明
译文中如有不妥之处,请参照原文
原文The $ Operator
链接:http://www.libra-aries-books.co.uk/software/dollar

可参考:>>>haskell 中 '$' 能用在哪些地方呢?(点击此处)<<<

过去,一直困惑于 $ 的使用,其它人用 $ 写的代码(当然,我从未用过,因为我不懂)看起来非常漂亮。于是我查阅了$ 的用法。

$ 的使用

$ 就像一个左小括号和一个在表达式末省略的右小括号。就这么简单,没有什么神奇的地方,它只是另一种使用小括号的方法。

(在这以后的摸索中,我发现以上所说的并没有把 $ 的全部用法展现出来。在本手册结束之前,我们试图讲解 $ 的大部分用法,所以,如果你和我一样困惑于 $ 的使用,本文将是一个很好的选择。)

示例:
先来看一个 $ 的例子。一个函数,从路径名中提取文件名,并把此文件名转化为小写形式,类型为 filefold :: FilePath -> String 。如果提供参数路径为 "/etc/README" ,那么返回结果为 "readme"(小写形式!)。下面是第一个版本:

import Data.Char (toLower)

filefold0 fp = map toLower (reverse (takeWhile ('/' /=) (reverse fp)))


此函数有很多括号,有点杂乱一团,我们来看看 $ 是如何更优雅的实现此函数。

去除上面表达式末的三个右括号,并把与之对应的左括号替换为 $ 。这样,就产生了 filefold7.hs:

import Data.Char (toLower)

filefold7 fp = map toLower $ reverse $ takeWhile ('/' /=) $ reverse fp


注意:不能去除 ('/' /=) 中的小括号,因为其中的右括号不在表达式末。(更准确的说,这里的小括号是一种特殊语法形式,用来产生一个 section ,并不仅仅是为了改变优先级)

喜欢用 filefold0 还是 filefold7 基于个人喜好,这两个完全等同。
我认为 $ 在上例中并没有对程序易读性做任何贡献。可能我们要更好一点:少用一些 $ 。这样,得到 filefold4.hs(强调表达式被分割为两部分,其中一个调用了几个函数):

import Data.Char (toLower)

filefold4 fp = map toLower $ reverse (takeWhile ('/' /=) (reverse fp))


另外,这个有三层嵌套的函数用 $ 和小括号共有8种写法。这几种写法如下。我认为最明了的是 filefold0 和filefold4,其次是 filefold7 和 filefold3,剩下的都不大易读。

import Data.Char (toLower)

filefold0 fp = map toLower (reverse (takeWhile ('/' /=) (reverse fp)))
filefold1 fp = map toLower (reverse (takeWhile ('/' /=) $ reverse fp))
filefold2 fp = map toLower (reverse $ takeWhile ('/' /=) (reverse fp))
filefold3 fp = map toLower (reverse $ takeWhile ('/' /=) $ reverse fp)
filefold4 fp = map toLower $ reverse (takeWhile ('/' /=) (reverse fp))
filefold5 fp = map toLower $ reverse (takeWhile ('/' /=) $ reverse fp)
filefold6 fp = map toLower $ reverse $ takeWhile ('/' /=) (reverse fp)
filefold7 fp = map toLower $ reverse $ takeWhile ('/' /=) $ reverse fp


当然,优化这个函数有许多方法。下面这个例子就强调了计算被分割为两部分:

import Data.Char (toLower)

filefoldn fp = map toLower f
    where f = reverse (takeWhile ('/' /=) (reverse fp))


也可以利用 function composition 去除那些标识符和 $。如下就是一个最简洁的写法。每个经验丰富的 Haskell 程序员都应如此使用 $ (我第一个此函数的写法当然不是这个^_^):

import Data.Char (toLower)

filefold = map toLower . reverse . takeWhile ('/' /=) . reverse


更多例子

回到 $ 。正确使用 $ 来分割复杂的表达式(就像 filefold4 那样)是非常有用的。比如,用在一个只有一个参数的函数(与 putStr 一样)中。

例如下面代码中,最后一个 putStr 参数必须用小括号括住:

main = do
  putStr "Type a number: "; x <- readLn
  put; y <-Str "and another: " readLn
  let s = x + y
  putStrLn (show x ++ " + " ++ show y ++ " = " ++ show s)


可以用 $ 来去除 putStrLn 周围的括号。如下:

main = do
  putStr "Type a number: "; x <- readLn
  putStr "and another: "; y <- readLn
  let s = x + y
  putStrLn $ show x ++ " + " ++ show y ++ " = " ++ show s


表达式越复杂,$ 的好处越明显。
有些函数以一个 IO action 为参数,比如下面的 when (和 unless)。这个 IO action 可能像 putStrLn 那样简单,使用 $ 和使用小括号没有太大的区别。

when debug (putStrLn "initialization started")
initialize
when debug $ putStrLn "initialization completed"


但是 IO action 可能是多行的 do 表达式:

when debug $ do
    h <- openFile "debug.out" appendMode
    hPutStrLn h "dump of parse tree"
    hPutStr h (show parseTree)
    hClose h


上面用到了 $ ,也可以用小括号括住整个 do 表达式,但这样没有上面更优雅。
这种风格也常用在 GUI 编程中。一个 gtk2hs package 中的例子如下:

renderWithDrawable win $ do
  scale (realToFrac width'  / realToFrac width)
        (realToFrac height' / realToFrac height)
  svgRender svg


$ 的另一个用法是在 constructors 中,用 Just $ x + y 可替代 Just (x + y)。

再提 $ 使用规则

看下面例子:

showSum x y = show x ++ " + " ++ show y ++ " = " ++ show (x + y)

在此之前,我们只知 $ 可去除 show (x + y) 中的小括号,这样,下面就变成:

showSum x y = show x ++ " + " ++ show y ++ " = " ++ show $ x + y -- wrong!

但是," = " ++ show 周围产生一个类型错误,因为此前只关注 $ 右面发生了什么,但 $ 也影响左边。
这样,可以再修补一下 $ 的使用规则。

$ 使用规则修补版(?)

用 $ 就相当于 $ 前的表达式部分用括号包起来,$ 操作符后表达式部分也用括号包起来。

现在,可以看到前面所给例子错误之处了。修补版规则应用于上面例子如下:

showSum x y = (show x ++ " + " ++ show y ++ " = " ++ show) (x + y) -- wrong!

很明显,这仍是错误的: $ 分割了 show 函数。

下一个关于 $ 的使用规则是正确的,不过,它有点难懂。可能我们已经知道一些 $ 的实际应用,那么,来重新看一下它的使用规则吧。


$ 使用规则终极版


已经讨论了 $ 像是一个语法糖,实际上并不是,它在 Prelude 模块中的定义如下:

infixr 0 $
($) :: (a -> b) -> a -> b
f $ x = f x


第一行指定 $ 是右结合性的,优先级是 0。
右结合性意味着:a $ b $ c == a $ (b $ c)。优先级 0 是最低等级,也意味着对于任何操作符 ⊕ 和 ?:a ⊕ b $ c ? d == (a ⊕ b) $ (c ? d)

第二行指定 $ 的类型。
$ 有两个参数,第一个参数为任意一个只有一个参数的函数 f,第二个参数必须符合函数 f 的参数类型。整个表达式的返回类型是函数 f 的返回值的类型。

第三行就是 $ 的具体定义:把第二个参数作为第一个参数(函数 f)的参数来计算。

令人迷惑的是第三行:它好像是说 $ 没有做任何事情。任意一个 f $ x 都可以写作 f x。对,就是这么简单,把 x 作为 f 的参数来计算。但是,当表达式很复杂时(有其他的操作符和函数),低优先级和右结合性使 $ 变得非常有用!

再来看一下 $ 的使用规则,因为其优先级最低(0),在它两边的任何表达式都会优先组合在一起来作为 $ 的参数,就像 3 + 4 * 5 得到 23<3 + (4 * 5)>而不是 35<(3 + 4) * 5> 一样,是因为 * 的优先级高于 + 的优先级,进而使 4*5 先计算,结果作为 + 的参数, 3 + 20 得到 23。这种优先级组合应用在操作符右边特别有用,尤其是对于右结合性的 $ 操作符来说,其右边其他 $ 操作符将优先被组合。

各种不同的优先级描述是从不同的角度来看的。与一个低优先级的操作符相比,一个高优先级的操作符把它周围“捆的”更紧,这也意味着更少组合周围的参数。隐藏的小括号会更少。( the implied parentheses are smaller.) The higher precedence operator is also lower in the parse tree (assuming the parse tree is drawn in the usual Computer Science way: with its root at the top of the page!)

小插曲

上面介绍了 $ 的使用,下面看一些例子。如果你仍未理解 $ 的使用,建议再回头看一下上面的介绍,直至懂了。如果还是不理解,请电邮给我以改进这篇文章。

$ 的左边

我们已经知道到在 $ 的右边发生了什么。$ 组合它左边的任何东西(直至前一个 $ 或者表达式开头),并且其左边必须是一个函数。

除用在简单的函数(像 $ x + y)中,大多数用在 partial application(例如 map toLower)的表达式中。因为 partial application 的优先级高于任何一个操作符(其实它的优先级是 10,并且是右结合性),partial application 都是这样词法分析(parsed)的。换句话说,你不必用 map toLower $ s,因为它等同于 map toLower s 。

另一种产生函数的方法是函数组合(function composition)'.',此操作符优先级为 9(并且是右结合性的),高于任何其他操作符,但是低于 function application。因此,$ 非常适合组合一个 function composition,可以把 (reverse . takeWhile ('/' /=) . reverse) fp 成为 reverse . takeWhile ('/' /=) . reverse $ fp。

sections 中的 $

作为操作符,用 $ 来产生 section 是可选的。section 是为操作符提供一个参数的语法糖。两个简单的 section 例子:('/' /=) 是一个测试 Char 类型参数是否不为 '/' 的函数,如果不为,返回 True,否则返回 False。(+ n) 是一个返回其参数加 n 的函数(当然,n 必须被定义)。

在 right section 中,$ 是多余的。section (f $) 完全等同于 (f),其中 f 为一个简单或复杂的表达式。$ 所做的就是把它右边的组合在一起。

left section 中,$ 表现的非常有趣。section ($ x) 是参数为函数的一个函数,并且把 x 作为参数(函数)的参数来计算。例如,map ($ "foobar") [take 3, drop 3] 得到 ["foo","bar"]。很难想象这里不使用 $ ,我也确实用到它了。我测试几个同一功能而不同实现的函数的效率。这些函数被列在一个名为 timeFunctions 的列表中。在测试之前,我想最好测试一下这几个函数是否等同(至少测试一下参数),对于单一参数 x 的测试函数如下:

check x = all (r ==) rs
    where
      (r:rs) = map ($ x) timeFunctions


所有其他的时间测试代码在 timing.hs 中,以上不仅演示了 $ section 的使用,而且可以从中看出序列和 lazy 的强大:最简单的定义总是最快的!

$ 作为一个函数

最后,所有的操作符都可以通过用小括号括住来转化为函数。($) 作为一个函数有用吗?Haskell report 有一个使用: zipWith ($) fs xs,它的参数是一个包含多个函数的列表和一个包含多个参数的列表,返回对应函数与对应参数相结合的结果的列表。(我太惊奇这个了!)事实上,这个表达式就是 ap(和 liftM2 一起被定义在 Control.Monad 模块中) 的简单定义。所以,我们可以简单的写作:ap fs xs。
一个 Unix wc 命令(计算出一个文件的行数、字数、字符数)的简单实现。

import Control.Monad (ap, liftM2)
import System.Environment (getArgs)

fs = [ length . lines, length . words, length ]

main = do
  as <- getArgs
  xs <- mapM readFile as
  let p = [ f x | f <- fs, x <- xs ]
      q = liftM2 ($) fs xs
      r = ap fs xs
  print p; print q; print r


不幸的是,这个版本计算出的是所有行数,然后是所有字数,最后是所有字符数(换句话说,xs 比 fs 变化快)。如果我们想输出第一个文件的所有这三个要素,然后是第二个文件的,然后...(fs 变化快一些),改变 list comprehension 是很容易实现的:只是简单的把里面的产生器交换一下顺序。改变 liftM2 的话,我们可以交换一下 fs 和 xs,并且使用 ($) 函数,不过这样看起来有一点艰涩。但对于 ap 就没有一个简单的办法解决了。

最后,还有一些情况下,我们可以使 $ 操作符,但很少见。

如果你有很好的 $ 使用方法,请告诉我!

[ 本帖最后由 izhier 于 2009-5-6 21:05 编辑 ]

论坛徽章:
0
2 [报告]
发表于 2009-05-01 18:46 |只看该作者
原文链接忘了:wink:

论坛徽章:
95
程序设计版块每日发帖之星
日期:2015-09-05 06:20:00程序设计版块每日发帖之星
日期:2015-09-17 06:20:00程序设计版块每日发帖之星
日期:2015-09-18 06:20:002015亚冠之阿尔艾因
日期:2015-09-18 10:35:08月度论坛发贴之星
日期:2015-09-30 22:25:002015亚冠之阿尔沙巴布
日期:2015-10-03 08:57:39程序设计版块每日发帖之星
日期:2015-10-05 06:20:00每日论坛发贴之星
日期:2015-10-05 06:20:002015年亚冠纪念徽章
日期:2015-10-06 10:06:482015亚冠之塔什干棉农
日期:2015-10-19 19:43:35程序设计版块每日发帖之星
日期:2015-10-21 06:20:00每日论坛发贴之星
日期:2015-09-14 06:20:00
3 [报告]
发表于 2009-05-01 20:07 |只看该作者
原帖由 izhier 于 2009-5-1 18:46 发表
原文链接忘了:wink:

http://www.libra-aries-books.co.uk/software/dollar
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP