框架还有个问题是控制了用户代码的结构。一个典型的例子是如果你正在使用一个框架,它会要求你继承一些抽象基类然后运行具体的方法。例如XNA框架中的Game类(虽然XNA框架终止了,但是其模式在另一个框架继续使用着): 在Initialize()中,需要对游戏用到的资源进行预载;Update()在进行状态刷新时会被反复调用;Draw()则在更新屏幕时用到。这难道不正是命令式编程吗?所以我们很可能会写出类似的如下代码: ![]() 代码作用是让人物往右移动。基于框架结构进行编程是有难度的,而这里我使用了最直接的方法来实现。变量x表示的是人物位置,而mario则用于存放人物图像资源。 虽然这在C#中或许会更加简洁,但前提是要忽略全部的检查。使用option目的是让代码更加安全(避免mario没有定义就在Draw()中使用)。此外,谁能保证Initialize()一定在Draw()执行前就调用完毕? 如何避免框架错误 接下来我会讲述如何使用库而不是框架的具体原因。
即使你没有使用F#来编写库,但是F#的交互性仍非常值得一试。F#不但可以用来编写库,其强大的交互性更使得库的运用变得十分简便。(如果是.NET平台,可以尝试 LINQPad)。 请看下面这个例子,它展示了如何使用F#格式库来把包含在文件夹中的F#脚本转为HTML或对某单一文件进行操作。![]() 如果是第一次接触,我会首先看有关库的说明,然后打开命名空间找到Literate,然后进行尝试,例如输入“.”。 我认为良好的库都支持类似的探索步骤。再看另外一个例子, FunScript ;用于把F#代码转为JavaScript。以下生成的JavaScript代码作用是为异步循环进行计数,在<tiltle>页面按秒执行: ![]() 类似地,我们都可以遵循上一个例子的学习途径掌握到相关用法。
接下来再看两个例子。第一个是以标准链表的方式对数据进行处理;另一个是使用上述链表方式读取输入,然后检查数据,最后再进行处理。 ![]() 这两个例子有什么不同之处呢?对于链表List函数,常常是一个单一函数作为一个参数使用。而这个函数是无状态的。 在第二个例子中,指定了两个函数。于我而言,这通常预示着有复杂的事情发生。其次,readAndProcess要求我们返回例子1中的字符串状态,然后把字符串作为下一函数的输入。这会引起一个潜在的问题。如果例子2需要例子1转入其它状态,该如何处理呢? 让我们进入readAndProcess来看它执行了什么操作;首先是进行异常处理,然后对输入进行检查。![]() 如果对其进行改进,要如何做呢?我们不妨把它分解为两个函数: ![]() 现在,validateInput变得简化了,如果输入是有效的则返回Some()的处理结果。而ignoreIOErrors函数仍作为参数使用。结合新函数,可以写成: ![]() 代码还是三行,但是更加清晰了,虽然比之前长了些。这样一来程序变得到简化,方便弄清楚其来龙去脉。 总的来说把函数作为参数使用是可以的,但是要注意尽量做到简化。特别是牵涉到状态的多次变化时,换另外一种处理方式或许会更好。
前面我们结合一个简单的游戏引擎讲述了框架是如何影响我们编程的,如果在不使用可变域和执行指定类的情况下,又该如何处理呢?在F#中,可以尝试异步工作流和基于事件的编程模型来代替。 其思路是使用触发事件而不是编写虚方法。因此,Game的定义变为:![]() 结合F#异步处理以及库的主动控制特长,我们可把代码改写为: ![]() 代码中首先对资源和Game对象进行了初始化;然后做了循环处理,使用AwaitObservable对Update或Draw事件进行监听。虽然我们无法对游戏状态和屏幕更新进行控制,但是在初始化时我们是可以做到的,检查游戏何时运行以及等待事件的发生。 asnc{..}的使用是关键所在。我们可以使用AwaitObservable来实现在更新或重绘需要时恢复计算。这样做的好处是可以实现更加复杂的操作,具体可参考这个例子Phil Trelford's Fractal Zoom。另外F#的agents代理可以实现类似的逻辑控制。 如果对F#不熟悉,或许会对上述代码困惑。但我的目的是说明控制权掌握在自己手中的重要性,这样可以写出自己的抽象逻辑,这也是接下来要说的。 |