趣味编程:在JavaScript中实现简单的yield功能(2 - 循环支持及解释执行)
2010-06-11 20:41 by 老赵, 3378 visits在上一篇文章里,我们构造了$yield和$yieldSeq两个基础编程组件,于是我们可以“在一定程度上”使用C#中的yield功能来构造一个迭代器。从表面上看来似乎不错,不过它的实际价值还是有值得推敲的。例如,我们目前还必须使用递归来代替循环,那么我们有办法改变这种变成方式吗?此外,在评论中有朋友谈到,这样写代码其实不是一个格式化就混乱了吗?至少,您是否觉得这种编程方法的function和括号实在多了些呢?这次我们就来探索这些问题的解决办法。
想法来源
为此,还是先谈一下我这种想法的来源吧——这便是F#里的“序列”:
// F# let fibSeq() = seq { let rec fibSeq' a b = seq { let next = a + b yield next yield! fibSeq' b next } yield 0 yield 1 yield! fibSeq' 0 1 }
瞧,这个无限的斐波那契序列实现方式,和之前的JavaScript做法是否几乎完全一致?其实在F#中,seq本身(从理念上讲)并不是个语言特性,而是基于一种更为抽象的语言特性“(计算表达式)Computation Expressions”而实现的功能。换句话说,seq只是一个“类库”,和我以前经常强调的async一样,都不是语言特性而是类库功能。我们基于这个语言特性也可以写出我们自己的实现,这样我们也可以用上xyz { ... }等等,而不仅仅是seq { ... }或async { ... }等“内置”的功能了。
Computation Expressions其实是一种“语法糖”,如刚才的fibSeq在“解糖”之后就变成了:
// F# let fibSeq() = seq { let rec fibSeq' a b = let next = a + b seq.Combine( seq.Yield(next), seq.Delay(fun () -> seq.YieldFrom(fibSeq' next b))) seq.Combine( seq.Yield(0), seq.Delay(fun () -> seq.Combine( seq.Yield(1), seq.Delay(fun () -> seq.YieldFrom(fibSeq' 0 1))))) }
总体来说,它们都是把顺序的语法拆成了一段一段,再通过回调函数进行连接的结果。事实上,“计算表达式”便是定义了一系列的“基础控制元素”,然后将一系列的表达式“解糖”为这些基础控制元素的调用。有些朋友可能发现这其实便是Monad(类似的东西),这话一点没错,F#的设计者Don Syme便曾经写道:
... Likewise the kinds of operations used under the hood are much like the operations used in both LINQ and Haskell monads. Indeed, computation expressions can be seen as a general monadic syntax for F#.
如果不看实际的代码样式,只看“感觉”,您是否发现这和前文的做法十分接近?其实刚才我之前的JS其实也是利用了这种思路,只是直接写出了“解糖”后语法(且解糖的具体方式有所不同),于是会显得比较丑陋——因此,谁说语法糖不重要?
while循环
其实F#的计算表达式中也提供了对于while循环的基础构造元素,这对我们现在要做的事情来说像是一个指导。那么我们来看看F#是如何“解糖”while语句的吧。假如我们有这样一个计算表达式:
// F# foo { let i = ref 0; while (i.Value < 10) do i.Value <- i.Value + 1 ... }
那么它解糖后的结果便是:
// F# let i = ref 0; bar.While( (fun () -> i.Value < 10), (fun () -> i.Value <- i.Value + 1))
换句话说,当我们使用这样的表达式时:
// F# while (cond-expr) do loop-body
实际上我们执行的是:
// F# // member While: (unit -> bool) * Result<unit> –> Result<unit> foo.While( (fun () -> cond-expr), (fun () -> loop-body))
看到了没?我们不妨也这么做吧。例如当我们在C#里写这样的代码时:
// C# public static IEnumerable<int> Infinite(int start) { while (true) yield return start++; }
它所对应的JavaScript便是:
// JavaScript function infinite(start) { return $while( function () { return true; }, function () { return $yield(start++); }); }
其中$while方法的实现是:
// JavaScript function $while(cond, body, rest) { if (cond()) { var iter = body(); if (iter) { return $yieldSeq(iter, function () { return $while(cond, body, rest); }); } else { return $while(cond, body, rest); } } else if (rest) { return rest(); } else { return null; } }
$while本身也是返回一个迭代器,不过它并不会像$yield或$yieldSeq那样“构造”出一个迭代器,而更像是在做一个“协调”的工作。例如,当cond为false时,它将直接返回后续的迭代器;否则,便会从body生成一个迭代器对象,并将下一个$while,也就是需要重新执行的内容使用$yieldSeq函数“拼接”在最后。由此我们可以看出(至少我感觉如此),使用无副作用的编程方式,在业务逻辑的表现上会比“状态改变”更为清晰一些。
先解析,后执行
现在,我们就试着使用刚才定义的$while函数来重写原来的rangeSeq函数吧:
// JavaScript function rangeWhileSeq(minInclusive, maxExclusive) { var i = minInclusive; return $while( function () { return i < maxExclusive; }, function () { return $yield(i, function () { i++; }); }); }
额,您看明白这段代码的含义了没?总之我是看的够呛,光有yield还好说,一旦加上$while就容易让人神智不清了。更重要的是,我这里真不知道怎么格式化才能显得清晰一些。因此,$while的可用性远不如$yield和$yieldSeq来的好。那么,我们该怎么办才好?先不谈这个,先想想我们究竟希望怎么样的编程方式吧……这样如何?
// JavaScript function rangeWhile(minInclusive, maxExclusive) { var i = minInclusive; while (i < maxExclusive) { return $yield(i); i++; } }
这完全就是一种最为理想的方式,不是么?易于编写,语意良好,格式美观,几乎没有缺点。那么,您有办法将这个rangeWhile方法“改写”成上面rangeWhileSeq的形式吗?还有,您能把这样的代码:
// JavaScript function fibSeq() { function fibSeq$(a, b) { var next = a + b; return $yield(next); return $yieldSeq(fibSeq$(b, next)); } return $yield(0); return $yield(1); return $yieldSeq(fibSeq$(0, 1)); }
改写成如下形式吗?
// JavaScript function fibSeq() { function fibSeq$(a, b) { var next = a + b; return $yield(next, function () { return $yieldSeq(fibSeq$(b, next)); }); } return $yield(0, function () { return $yield(1, function () { return $yieldSeq(fibSeq$(0, 1)); }); }); }
我相信这实现起来不会过于困难。
事实上,我认为这可以说是JavaScript语言最终极的黑魔法。由于JavaScript的语言特性,我们完全可以调用一个函数的toString方法,再进行“改造”,最后再eval回去。利用这种方式,JavaScript可以说拥有了几乎无限的语法糖能力。事实上,类似的事情一直有人在做。例如,已经有人用JavaScript写出Flash播放器以及PostScript解释器。因此,我们为什么不把这种方式直接用在JavaScript自身呢?
提高JavaScript的开发效率
再次回到F#吧。刚才我们提到,计算表达式的试用性很广,除了seq以外,F#的核心库还实现了“异步工作流(Asynchronous Workflows)”。例如下面的代码:
// F# async { let req = WebRequest.Create("http://blog.zhaojie.me/") let! resp = req.GetResponseAsync() use stream = resp.GetResponseStream() let reader = new StreamReader(stream) let! html = reader.ReadToEndAsync() return html }
便会被“解糖”为:
// F# async.Delay(fun () -> let req = WebRequest.Create("http://blog.zhaojie.me/") async.Bind(req.GetResponseAsync(), (fun resp –> async.Using(resp.GetResponseStream(), (fun stream –> let reader = new StreamReader(stream) async.Bind(reader.ReadToEndAsync(), (fun html -> async.Return(html))))))))
这里let!会被视作一次Bind函数的调用,F#在此会发起一个异步操作(如一个基于IOCP的异步操作),并会在操作结束后执行回调函数。这样,我们便可以使用异步工作流轻松地编写异步程序,而不需要在纷繁的回调函数中纠缠不清。循环也好,递归也罢,异步工作流都会帮我们保留各式上下文,按照我们的需要进行逻辑控制。
这就是了。刚才我们提出在JavaScript中解析“迭代器”的生成代码,那么我们能用同样的方式实现一个完整的“计算表达式”特性吗?事实上,这样JavaScript就几乎获得了完整的,与F#一样强大且优雅的特性。更重要的是,“异步操作”是JavaScript的应用领域中最为常见的使用场景(没有之一),如果JavaScript拥有了F#中的“异步工作流”,那么JavaScript的开发体验一定会大大增强。
我认为,首先于语言特性,JavaScript本身的生产力似乎已经遇到了瓶颈,必须借助“外力”才能带来更好的开发效率。例如GWT和Script#,都是通过编写服务器端的代码(如Java和C#)再编译成JavaScript以获得更好的开发效率(虽然Anders Hejlsberg认为并不认为这是正途)。在这方面,我认为走的更远的是莫过于WebSharper,它是一个基于F#构建的Web开发平台,使用F#构造从前到后的一整套内容。其中利用到F#中许多高级的开发特性,并可以将F#代码直接转化JavaScript,这样服务器端和客户端的通信也变得透明起来。事实上我很看好这种方式,尤其是在HTML 5出现之后,越来越多的东西可以使用JavaScript编写,我认为这是增强Web平台开发效率的方向之一。
当然,如WebSharper这类与平台(.NET,F#)密切相关的产品,其实适用性会受到限制。不过,如我上面说的那样,如果可以用JavaScript开发出一套“解释器”,或是一种可以大大提高生产力的语法糖,其通用性一定会更高,且更容易被前端开发人员所接受。
相关文章
- 趣味编程:在JavaScript中实现简单的yield功能(问题)
- 趣味编程:在JavaScript中实现简单的yield功能(1 - yield与yieldSeq)
- 趣味编程:在JavaScript中实现简单的yield功能(2 - 循环支持及解释执行)
其实每种语言都可以使用这种方式,例如在Pre-Build里改造代码,然后再进行编译,呵呵。