- 论坛徽章:
- 0
|
本文可以被任意转载、优化修改,无须注明
译文中如有不妥之处,请参照原文
原文: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 编辑 ] |
|