原文出处:http://lorgonblog.spaces.live.com/blog/cns!701679AD17B6D310!165.entry
Translated by Sean Wang
在以前的博文中, 我曾经提到管道操作符和类型推断之间的交互. 我想找一个比较自然的例子来说明如何在F#中使用管道操作符, 最终在Wikipedia中找到了. 在Wiki中的这一页描述了Unix系统命令行的管道操作符, 而且提供了一个例子,其功能是根据一个给定的URL,使用Unix管道操作符进行拼写检查. 那么,让我们来用F#写一个小工具. F#中每一步的逻辑和Unix的例子中有些小小的不同, 但它对于了解较长的管道操作符来说仍然是个不错的例子.
open System.IO
open System.Net
open System.Text.RegularExpressions
// fetch a web page
let HttpGet (url: string) =
let req = System.Net.WebRequest.Create(url)
let resp = req.GetResponse()
let stream = resp.GetResponseStream()
let reader = new StreamReader(stream)
let data = reader.ReadToEnd()
resp.Close()
data
// Use Word to spellcheck (assumes you have referenced Microsoft.Office.Interop.Word.dll)
let msword = new Microsoft.Office.Interop.Word.ApplicationClass()
let mutable x = System.Reflection.Missing.Value :> System.Object
let Spellcheck text =
msword.CheckSpelling(text, &x, &x, &x, &x, &x, &x, &x, &x, &x, &x, &x, &x)
// find all misspelled words on a particular web page, using a pipeline
printfn "misspelled words:"
HttpGet "http://www-static.cc.gatech.edu/classes/cs2360_98_summer/hw1"
|> fun s -> Regex.Replace(s, "[^A-Za-z']", " ")
|> fun s -> Regex.Split(s, " +")
|> Set.of_array
|> Set.filter (fun word -> not (Spellcheck word))
|> Set.iter (fun word -> printfn " %s" word)
让我们仔细的检查一下这些管道操作符(最后六行的代码). 一开始, 我们使用HTTpGet 函数从网络上获取页面(一个很明显是我随机选择的页面, 绝对没什么特别含义), 函数返回了一个字符串,里面是网页的内容:
HttpGet http://www-static.cc.gatech.edu/classes/cs2360_98_summer/hw1
接着我们将字符串输入一个函数,它可以将字符串依据空白处分解成一个由单词构成的数组:
|> fun s -> Regex.Split(s, " +")
接着我想将数组按顺序排列并且删除重复的项, 通过创建一个包含这些单词的Set,我们可以很简单地实现这个功能:
|> Set.of_array
然后我们想过滤这个Set, 去掉那些拼写错误的单词:
|> Set.filter (fun word -> not (Spellcheck word))
最后, 我们要打印出Set中每一个剩下的单词(拼写正确的):
|> Set.iter (fun word -> printfn " %s" word)
这是一个不错的例子, 利用长管道操作符, 阅读起来非常自然.
(管道操作符同样也说明了一条,在定义函数时使用柯里化参数比使用元组参数好的原因. 不过,我将在其他的博文中进一步讨论这方面细节)
当然,我们不用管道操作符也可以实现拼写检查功能. 一个巨大的表达式, 看起来是这样:
(Set.iter (fun word -> printfn " %s" word)
(Set.filter
(fun word -> not (Spellcheck word))
(Set.of_array
(Regex.Split(
Regex.Replace(
HttpGet "http://www-static.cc.gatech.edu/classes/cs2360_98_summer/hw1",
"[^A-Za-z']",
" "
), " +"
))
)
)
)
哦…这看起来就像你在其他任何一门语言(C#)中写出的巨型表达式一样, 而且它也揭示出了使用巨型表达式的问题: 可读性差. 尤其是表达式是内部求值的, 这意味着”第一件事情的发生(最内层的表达式)”嵌套在表达式中央, 接着它的结果作为参数传递给包裹在它外面的函数, 接着再传递给更外面一层… 带有深层嵌套函数的代码对人来说可读性很差, 因为按照事物发生的顺序去了解它的细节问题,会更加容易. 所以,如果不使用管道操作符,那 还有一个主要的方法, 就是把每一个中间表达式命名:
let page = HttpGet "http://www-static.cc.gatech.edu/classes/cs2360_98_summer/hw1"
let pageWords = Regex.Replace(page, "[^A-Za-z']", " ")
let wordArray = Regex.Split(pageWords, " +")
let wordSet = Set.of_array wordArray
let filteredWordSet = Set.filter (fun word -> not (Spellcheck word)) wordSet
Set.iter (fun word -> printfn " %s" word) filteredWordSet
这些代码读起来舒服多了. 现在函数按它们执行的顺序出现, 每一行只实现一个功能. 每一个中间结果都被命名, 然后被通过名字引用. 至于他们的命名是好是坏,则取决于你命名是否自然, 我想,可能既有帮助也有阻碍. 我觉得, 只有当你取的名字可读性好,并且能够解释每一步发生了什么,才有必要进行命名.除非读者对Regex类和Set模块有着一定的了解,那么这个例子就会有问题. 因此, 你必须调整自己的代码风格, 要么像上例中使用一大堆”let”, 要么使用管道操作符风格:
HttpGet "http://www-static.cc.gatech.edu/classes/cs2360_98_summer/hw1"
|> fun s -> Regex.Replace(s, "[^A-Za-z']", " ")
|> fun s -> Regex.Split(s, " +")
|> Set.of_array
|> Set.filter (fun word -> not (Spellcheck word))
|> Set.iter (fun word -> printfn " %s" word)
当然, 也不是说处处都要这么写. 一般来说, 混合的风格会使可读性更好,我就比较喜欢这样:
let page = HttpGet "http://www-static.cc.gatech.edu/classes/cs2360_98_summer/hw1"
let words = Regex.Replace(page, "[^A-Za-z']", " ")
|> fun s -> Regex.Split(s, " +")
|> Set.of_array
words |> Set.filter (fun word -> not (Spellcheck word))
|> Set.iter (fun word -> printfn " %s" word)
我之所以喜欢这样的格式, 是因为它是我们的眼睛能够从左边检查代码, 看看主要执行了哪些任务: 我们获取了一个页面, 我们提取了一些单词, 我们要对这些单词做一些处理.
就这样了. 希望现在你对管道操作符”|>” 能有一个更好的了解 – 它如何运作以及你什么时候应该使用它. 这个东西是如此的简单(“x|>f”就等同于”f x”), 却又引起了多少的争议.