编者按:之前,我们曾发表《 函数式语言库模式:框架是魔鬼? 》该文论述了库与框架之间的区别,及如何设计组合化的库。而本文作者在此之前,还发表了一篇《 Library patterns: Multiple levels of abstraction 》,结合具体实例,向大家非常详细地介绍了库设计模式及库设计中的多级抽象思想。
以下为具体译文:
库设计模式
对于库设计理论来说,有几点在我的实际工作过程中体会最深:
我认为上述几点是最能影响库设计好坏的。本文将先就抽象级别一点展开论述。
库是如何被使用的?
每个库都对应着一定的典型应用场合。比方说, F# Formatting格式工具可以对一个目录下的所有文 件进行文档生成,这占到使用频率的80%。有时我们可能需要以不同的方式来处理个别文件(例如使用不同模板)。一个完整的库应能兼顾该需求。还有某些时候我们需要以别的方式来处理某个文件,如添加一个自动生成内容表(TOC)。
对于类似的情况,我找到一种行之有效的处理方法—以多级功能抽象的方式来创建库。在最高级,单一个函数调用应处理 80%的应用场合。然后如果有需要,你可以再多建一级来处理额外15%的应用场合。最后如果还有需要,就再多建一级来处理最后4%的应用场合。对于最后的1%,我的建议是发送一个pull request!
该设计模式在核心功能库中是被深度采用的,例如 F#链表库。遵循该模式,还能使我们的库成为领域的特定语言从而更具可读性。
示例 #1:链表
用链表来阐述多级抽象是很有代表性的。
在高级抽象,可以使用高频函数来对链表进行处理 (或在C#中使用LINQ)。例如从0到100的整数中获取正的sin值链表,可以这样编写:
类似 List.map和List.filter的高频函数在链接处理中有80%频率会用到甚至更高。但有时我们可能需要非高频操作。
例如根据符号变更对链表进行分拆,例如把 [1; 4; -3; 2]分为[1; 4]和[-3; 2]。如果不借助低频API以递归模式匹配进行处理,单靠高频函数是很难实现的:
在 loop中,进行了三种处理:
如果是使用 C#中的IEnumerable<T>,或许操作起来略显复杂,但是仍有一些低级API可供使用(使用GetEnumerator进行临时集合和转化)。
集合 API设计的好处是可以实现从低级到高级的转换。splitAtSignChange函数在根据邻近函数值进行拆分时,可以看成是一个更综合操作的一个实例:
该函数与前个版本十分相似—不同的是增加了额外的参数 f来判定什么时候断开链表。虽然函数本身使用了低级API,但提供了转为高级的途径。
再回头看高级的定义是能包含更简单的编程方式。也就是说,根据 X轴的临界值来对从1到10的sin值进行链表进行拆分:
可见这就实现了到高级的转换—使用两个简单明了的函数处理拆分问题。 示例 #2:3D的领域特定语言 这是另一个典型的例子,特别是在进行自定义对象建模时。
其实现代码如下:
在高级抽象层面,我们仅仅使用 4行代码就把城堡创建好了!但这仅能进行非常受限制的创作(规矩的城墙和塔组成的城堡),或许我们想做得更多。
如果想使用不同的塔外形该如何处理呢?要查看低级 3D渲染代码吗?不必。tower函数本身就是根据另一种语言或抽象等级来进行编写的。一个塔就是一个填色后的圆柱体加上一个填色的有正确朝向的圆锥体:
使用库时,一开始是高级抽象的,但熟悉之后,我们可以进入下一级。在这级中,我们可以打造自己的语言 (抽象)例如前述的List.splitAt。
在进行 3D渲染时存在一个低级操作。以该库为例,调用OpenGL原始操作是低级的(使用OpenTK包装),这稍微有点复杂:
如果想以更简单的方式来生成图形,我们可以在 3D原始操作和渲染之间加多一层,但这不是我们要讨论的。即便如此,我们还是可以看出多级抽象的重要性。最明显的一点是可以从已创建的事物中发现如何实现高级到低级的转换(例如塔是如何组成的),然后以其它方式使用低级原始操作。 换言之,如果你直接以调用 OpenGL的方式来生成塔,那么是很难再生成其它形态塔的。但如果有多级抽象机制,这就不是问题了。 综述 (编译/伍昆 责编/张红月)
英文来自: Tomas Petricek's blog