转载

函数式语言库模式:框架是魔鬼?

编者按: 本文作者 Tomas是F#语言的专家以及导师、计算机科学家,曾出版过有关F#的教程。本文重点介绍了如何设计组合化的库以及如何避免在库设计时进行回调。Tomas倡导以库而不是框架的方式进行开发。以下为译文。

框架 VS. 库

框架和库有什么区别呢?两者的主要不同之处在于如何使用它们以及编写什么样的代码。

  • 框架—— 框架控制了系统的运行,并定义了扩展点 (接口)来让用户进行实施;
  • 库—— 库把系统运行控制权交给用户,并定义了功能和类型供用户使用。

函数式语言库模式:框架是魔鬼?

框架和库之间的区别可用上图表示。框架定义了一个结构,你不得不将其填充好;而库则需要你围绕其提供的结构进行编码。

为什么不建议使用框架?

  • 框架是不可组合的

框架最大最显著的弱点是不可组合。如果你正在使用两个框架,这两者之间往往是很难兼容的;谁包含谁,谁是谁的外延也是不清晰的。

如果是库,情况则有所不同。因为你才是决策人,所以能够同时调用不同的库,虽然这会增加一定的编程复杂度,但至少是能够实现的。

函数式语言库模式:框架是魔鬼?

  • 难以对框架进行探索

框架另一个大问题是很难进行测试和探索。在 F#中,载入一个库并透过不同输入来检查输出和库的运行是很有用的。例如,可以使用Web开发库 Suave 来启动一个简单的 Web服务器,代码如下:

函数式语言库模式:框架是魔鬼?

代码片中首先载入了库,然后以默认方式调用 startWebServer。该方式是非常有用的,因为可以让用户尝试不同的参数来对输出结果进行对比。

  • 框架定型了用户代码

框架还有个问题是控制了用户代码的结构。一个典型的例子是如果你正在使用一个框架,它会要求你继承一些抽象基类然后运行具体的方法。例如 XNA框架中的Game类(虽然XNA框架终止了,但是其模式在另一个框架继续使用着):

class Game {   abstract void Initialize();   abstract void Draw(DrawingContext ctx);   abstract void Update(); }

在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对象进行了初始化;然后做了循环处理,使用 AwaitObservableUpdate或Draw事件进行监听。虽然我们无法对游戏状态和屏幕更新进行控制,但是在初始化时我们是可以做到的,检查游戏何时运行以及等待事件的发生。

asnc{..}的使用是关键所在。我们可以使用 AwaitObservable来实现在更新或重绘需要时恢复计算。这样做的好处是可以实现更加复杂的操作,具体可参考这个例子 Phil Trelford's Fractal Zoom 。另外 F#的 agents 代理可以实现类似的逻辑控制。

如果对 F#不熟悉,或许会对上述代码困惑。但我的目的是说明控制权掌握在自己手中的重要性,这样可以写出自己的抽象逻辑,这也是接下来要说的。

  • 使用多重抽象逻辑

请再看看前述的 Game例子,虽然低阶抽象给予了充分的控制权,但是很多时候,我们希望写出的游戏是同时具备重绘和更新功能的。

这实现起来也不难,只需把某些部分作为参数使用:

函数式语言库模式:框架是魔鬼?

startGame抽象实现了在初始化时把两个函数作为参数使用。 Update函数进行状态刷新,draw函数使用DrawingContext重绘状态。这样一来,我们的例子可变为4行代码:

函数式语言库模式:框架是魔鬼?

因此只要仔细阅读 startGame的代码,按需对其进行改动,便可实现全权控制。对比于建基于一个脆弱库之上的程序,这种方式难道不更稳定可靠吗?

  • 设计可组合的库

对于库来说,可组合属性是我们选择它而不是框架的原因之一。例如 FsLab ,这是一个用于 F#的数据科学库(包括 Deedle , Math.Net Numerics),以单个脚本的方式链接呢其他的库( 源代码 )。

两个简单的例子是矩阵和框架的互转 Matrix.toFrame,Frame.toMatrix。

函数式语言库模式:框架是魔鬼?

该转换操作起来是很简单的,因为 Deedle框架和Math.Net矩阵都能转化为一个2维数组,所以通过数组可方便地实现两者的互转。因此,即使是很复杂的库,我们都应该为用户保留足够的库合成权以实现更强大的功能(或者改写)。

写在最后

本文着重从可组合和避免回调方面对库和框架进行比较。进一步说,框架模式不仅存在于软件,在日常生活也是经常遇到的。例如参团游,从一开始,交通、住宿、游玩行程等都已经被固定了;而自由行则类似于库的组合,任何细节都需要亲力亲为,从而实现全权控制。虽然参团游很方便,但是对于我,特别是软件开发,我还是更倾向于我的地盘我做主!

来自: Tomas Petricek's blog

正文到此结束
Loading...