不知道大家的圣诞节过的如何?有没有玩点啥有趣的东西?上次的文章中我们主要分析了使用Google Translate进行文字翻译的方式,并使用C#写了一个简单的的翻译程序,效果良好。不过,在平时开发过程中,对于此类问题我常用F#来解决这样的问题。那么使用F#来实现此类任务有什么优势吗?不错,我们现在便来看看这个问题。 简单的F#实现
话说F#和.NET框架可以无缝集成,因此理论上之前我们的C#代码怎么写,便可以使用F#来照着抄一遍。例如,我们先定义一个Sync模块,其中先定义一个获取全部目标语言的getLanguages函数:
#light
module Sync
open System
open System.Text.RegularExpressions
open System.Net
open System.Web
open System.Web.Script.Serialization
open System.Collections.Generic
open System.Text
type private LangMap = Dictionary<string, string>
let private getLanguages() =
let url =
"http://translate.googleapis.com" +
"/translate_a/l?client=te&hl=zh-CN&cb=_callbacks_._0g3mb650r"
let webClient = new WebClient()
let script = webClient.DownloadString(url)
let json = Regex.Match(script, @"'tl':({.+})}\)").Groups.Item(1).Value
let serializer = new JavaScriptSerializer()
serializer.Deserialize<LangMap>(json)
这个函数与之前C#版本的GetLanguages方法可谓一模一样,其返回结果便是一个字典,存储了目标语言的代号和名称——当然,这边我为Dictionary<string, string>取了一个别名LangMap,这样使用起来感觉更为方便,语义也更加清晰。
同样的,我们“翻译”一遍之前的Translate方法,变成如今的translateText函数:
let private translateText (text:string) sl tl =
let url =
"https://translate.googleapis.com" +
"/translate_a/t?client=te&format=html&v=1.0"
let encoded = text |> HttpUtility.UrlEncode
let data = sprintf "q=%s&sl=%s&tl=%s&tc=1" encoded sl tl
let webClient = new WebClient()
webClient.Encoding <- Encoding.UTF8
let userAgent = "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0;)"
webClient.Headers.Add(HttpRequestHeader.UserAgent, userAgent)
let json = webClient.UploadString(url, data)
let serializer = new JavaScriptSerializer()
serializer.Deserialize<string>(json)
而最终进行多种语言翻译的,则是使用公有的translate函数:
let translate text sl =
let languages = getLanguages()
let translated =
languages
|> Seq.map (fun p -> translateText text sl p.Key)
let names = languages |> Seq.map (fun p -> p.Value)
Seq.zip names translated |> Seq.toArray
首先,所有语言会存入一个叫做languages的容器中,它是一个字典,实现了IEnumerable<KeyValuePair<string, string>>,在F#中便是seq<KeyValuePair<string, string>>,因此我们使用Seq模块的map方法将其转化为翻译后的结果,并存放在translated中。然后,我们获得每种语言的名称,最终与翻译的结果zip即可。translate函数最终返回的是一个存放着元组的数组,元组的类型是string * string。
至于调用,自然是很容易的。F#控制台程序也需要定义main函数,如下:
[<EntryPoint>]
let main args =
let output =
Sync.translate "圣诞快乐" "zh-CN"
|> Array.map (fun (lang, text) -> sprintf "%s:o%s" lang text)
File.WriteAllLines("output.txt", output)
Console.ReadLine() |> ignore
0
type WebClient with
member c.GetStringAsync(url) =
async {
c.DownloadStringAsync(new Uri(url))
let! args = c.DownloadStringCompleted |> Async.AwaitEvent
return args.Result
}
member c.PostStringAsync(url, data) =
async {
c.UploadStringAsync(new Uri(url), data)
let! args = c.UploadStringCompleted |> Async.AwaitEvent
return args.Result
}
我们在F#中可以“打开”任意一个类型,为其添加新的成员——不过自然这只是如C#中扩展方法那样的语法糖而已。我们为WebClient添加了GetStringAsync与PostStringAsync两个方法,它们返回的都是结果为一个字符串的异步数据流(即Async<string>),执行这个异步数据流之后便可以得到最终的结果。值得注意的是,async块只是在构造一个逻辑块,并没有真正调用。
于是,我们的getLanguages函数也可进行改写:
let private getLanguages() =
async {
let url =
"http://translate.googleapis.com" +
"/translate_a/l?client=te&hl=zh-CN&cb=_callbacks_._0g3mb650r"
let webClient = new WebClient()
// let script = webClient.DownloadString(url)
let! script = webClient.GetStringAsync(url)
let json = Regex.Match(script, @"'tl':({.+})}\)").Groups.Item(1).Value
let serializer = new JavaScriptSerializer()
// serializer.Deserialize<LangMap>(json)
return serializer.Deserialize<LangMap>(json)
}
您是否发现这段代码与之前的变化?没错,两者几乎一模一样。除了它变成了async构造块之外,唯一的区别只是被注释的两行代码。第一行代码中,原本我们使用的是同步的DownloadString函数,而现在我们则通过执行一个异步数据流(使用let!)来得到结果script——当然,执行的形式却是同步的。除此之外,最后返回结果需要使用return来标明。let!和return(以及其他的如return!)等操作符,都是F#编译器对“工作流”构造块(如async)使用中的“强制措施”,以此避免开发人员的误操作。
同样,translateText函数也将构造一个异步工作流:
let private translateText (text:string) sl tl =
async {
let url =
"https://translate.googleapis.com" +
"/translate_a/t?client=te&format=html&v=1.0"
let encoded = text |> HttpUtility.UrlEncode
let data = sprintf "q=%s&sl=%s&tl=%s&tc=1" encoded sl tl
let webClient = new WebClient()
webClient.Encoding <- Encoding.UTF8
let userAgent = "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0;)"
webClient.Headers.Add(HttpRequestHeader.UserAgent, userAgent)
let! json = webClient.PostStringAsync(url, data) // changed
let serializer = new JavaScriptSerializer()
return serializer.Deserialize<string>(json) // changed
}
使用异步工作流的另一个好处在于,我们可以轻易地将异步工作流并行地执行。例如在目前的场景中,我们可以同时发起50多个异步IO请求,但同时却不占用任何线程——它们可能都在等着IO设备(即网卡)发送设备信号呢。于是乎,我们的translate函数便可以这样写:
let translate text sl =
async {
let! languages = getLanguages()
let! translated =
languages
|> Seq.map (fun p -> translateText text sl p.Key)
|> Async.Parallel
let names = languages |> Seq.map (fun p -> p.Value)
return Seq.zip names translated |> Seq.toArray
}
并这样调用:
let output =
Parallel.translate "圣诞快乐" "zh-CN"
|> Async.RunSynchronously
|> Array.map (fun (lang, text) -> sprintf "%s:%s" lang text)
这句代码中的Parallel便是指我们刚才开发的模块。这段代码的执行速度会比Sync模块要快很多,因此此时50多个翻译的请求并不是依次发出,而是同时发出,并通过响应IO设备的事件来工作。不阻塞,高效。