实现基于数据结构的语言
创建任何 DSL 都应该从定义需要解决的问题开始。这里,我们需要定义一个 DSL 库(有时也称为组合库,combinators library),用于二维图形,这是一个很明显的选择。这个示例演示如何用大量简单的基本图形构建出复杂的结构。在计算机屏幕上的图像本质上就是线条和多边形的集合,尽管显示出来的图形可能极其复杂。这个示例用四个模块表现:第一,清单 12-1,提供创建图片的基本操作(primitives);第二,清单12-2,如何实现解释图片;清单 12-3 和清单 12-4 用示例演示如何使用这些库,需要把清单12-1 和12-2 与清单 12-3 或12-4 一起使用,才能看到结果。我们先浏览一下设计过程的要点,看一下整个清单的结论。
注意
这个示例是受使用 A6 系统(http://a6systems.com/)的人的启发,这是一个相似而更复杂的系统,用来渲染三维动画场景,他们把这个库广泛地用于工业用途。
我们首先设计几个描述图形的类型,这些类型构成图形的基本操作(primitives):
// represents the basic shapes that willmake up the scene
type Shape =
|Line of Position * Position
|Polygon of List<Position>
|CompersiteShape of List<Shape>
这个类型是递归的,CompersiteShape 联合情况包含了开头列表,这将构成树形结构。在编译器开发领域,这种树形结构被称为抽象语法树(Abstract Syntax Tree (AST),在本章的最后,我们会看到另一个示例,使用抽象语法树来表示程序。
至此,我们已经创建了图形的三个基本元素:线、多边形和形状。用三种简单元素组成类型的事实是一种重要的设计思想,把基本操作简单化,使得实现渲染图形的引擎更简单;基本操作的简单也意谓着不需要用户花时间与之进行直接交互,相反,提供了一组高级包装函数,返回形状(Shape)类型的值,这就是组合(combinators)。联合中的CompersiteShape 情况是一个很重要的示例,它可以通过简单的元素构建出复杂的形状。通过 compose 函数把它公开:
// allows us to compose a list of elementsinto a
// single shape
let compose shapes = CompersiteShape shapes
用这个函数可以实现许多高级函数,例如,函数 lines,参数为位置列表,返回的形状是由这些位置经过的路径,利用 compose 函数把大量单独的线组合成一条线:
// a line composed of two or more points
let lines posList =
//grab first value in the list
letinitVal =
matchposList with
|first :: _ -> first
|_ -> failwith "must give more than one point"
//creates a new link in the line
letcreateList (prevVal, acc) item =
letnewVal = Line(prevVal, item)
item,newVal :: acc
//folds over the list accumlating all points into a
//list of line shapes
let_, lines = List.fold createList (initVal, []) posList
//compose the list of lines into a single shape
composelines
接下来,再用 lines 函数去实现几个高级形状,比如square 函数:
let square filled (top, right) size =
letpos1, pos2 = (top, right), (top, right + size)
letpos3, pos4 = (top + size, right + size), (top + size, right)
iffilled then
polygon[ pos1; pos2; pos3; pos4; pos1 ]
else
lines[ pos1; pos2; pos3; pos4; pos1 ]
square 函数使用 lines 函数画出经过计算的点的正方形轮廓。可以在清单 12-1 中看到完整的模块,虽然更实际的库实现可能包含更多的基本形状,以供用户选择。要编译这个程序,需要引用System.Drawing.dll 和System.Windows.Forms.dll:
清单 12-1 创建图形的组合库
namespace Strangelights.GraphicDSL
open System.Drawing
// represents a point within the scene
type Position = int * int
// represents the basic shapes that willmake up the scene
type Shape =
|Line of Position * Position
|Polygon of List<Position>
|CompersiteShape of List<Shape>
// allows us to give a color to a shape
type Element = Shape * Color
module Combinators =
//allows us to compose a list of elements into a
//single shape
letcompose shapes = CompersiteShape shapes
//a simple line made from two points
letline pos1 pos2 = Line (pos1, pos2)
//a line composed of two or more points
letlines posList =
//grab first value in the list
letinitVal =
match posList with
| first :: _ -> first
| _ -> failwith "must give more than one point"
//creates a new link in the line
letcreateList (prevVal, acc) item =
let newVal = Line(prevVal, item)
item, newVal :: acc
//folds over the list accumlating all points into a
//list of line shapes
let_, lines = List.fold createList (initVal, []) posList
//compose the list of lines into a single shape
composelines
//a polygon defined by a set of points
letpolygon posList = Polygon posList
//a triangle that can be either hollow or filled
lettriangle filled pos1 pos2 pos3 =
iffilled then
polygon[ pos1; pos2; pos3; pos1 ]
else
lines[ pos1; pos2; pos3; pos1 ]
//a square that can either be hollow or filled
letsquare filled (top, right) size =
letpos1, pos2 = (top, right), (top, right + size)
letpos3, pos4 = (top + size, right + size), (top + size, right)
iffilled then
polygon [ pos1; pos2; pos3; pos4; pos1 ]
else
lines [ pos1; pos2; pos3; pos4; pos1 ]
现在我们已经有了语言的基本元素,下面需要做的是实现解析器,以显示图形。本章描述的解析器是一个 Windows 窗体,这种方法的好处是还可能用WPF、Silverlight 和GTK# 实现解析器,就是说,在图形界面库与平台之间[ 切换]是相当方便的。实现解析器非常简单,只需要实现联合中的每一种情况就可以了,在Line 和 Polygon 情况中,使用 Windows 窗体的基本对象 GDI+ 绘制图形。幸运的是,GDI + 绘制线条或多边形也很简单。第三种情况CompositeShape 也很简单,只要简单地递归调用绘制函数。在清单 12-2 中可以看到完整的源代码,要编译程序,需引用System.Drawing.dll 和 System.Windows.Forms.dll。
清单 12-2 用组合库实现渲染图形的解析器
namespace Strangelights.GraphicDSL
open System.Drawing
open System.Drawing.Drawing2D
open System.Windows.Forms
// a form that can be used to display thescene
type EvalForm(items: List<Element>)as x =
inheritForm()
//handle the paint event to draw the scene
dox.Paint.Add(fun ea ->
letrec drawShape (shape, (color: Color)) =
match shape with
| Line ((x1, y1), (x2, y2)) ->
// draw a line
let pen = new Pen(color)
ea.Graphics.DrawLine(pen, x1, y1, x2,y2)
| Polygon points ->
// draw a polygon
let points =
points
|> List.map (fun (x,y) -> new Point(x, y))
|> Array.ofList
let brush = new SolidBrush(color)
ea.Graphics.FillPolygon(brush, points)
| CompersiteShape shapes ->
// recursively draw the other contained elements
List.iter (fun shape -> drawShape(shape, color)) shapes
// draw all the items we have been passed
items |> List.iter drawShape)
现在,把两个正方形和一个三角形放到一起组成一个图形就很简单了,只要调用我们组合库中适当的函数,把它们组合起来,再加上颜色,就得到了场景的完整描述了。清单 12-3 展示了实现的过程,图 12-0 是运行得到的结果。
[
原文中没有给出这个示例的图形,图 12-1 是清单 12-4 的图示。因此,为了不改变原文的图示排序,此图示就编为图 12-0。
]
清单 12-3 利用组合库的简单示例
open System.Drawing
open System.Windows.Forms
open Strangelights.GraphicDSL
// two test squares
let square1 = Combinators.square true (100,50) 50
let square2 = Combinators.square false (50,100) 50
// a test triangle
let triangle1 =
Combinators.trianglefalse
(150,200) (150, 150) (250, 200)
// compose the basic elements into apicture
let scence = Combinators.compose [square1;square2; triangle1]
// create the display form
let form = new EvalForm([scence,Color.Red])
// show the form
Application.Run form
[
要编译成功,在程序的前面要加上 module name,name 是可以任意的,只要不与前面定义的命名空间相同即可。
]
图 12-0 由正方形和三角形组合成的图形
清单 12-3 中给出的这个简单示例并不可能代表如何利用组合库创建图形,在清单 12-4 中我们将一个更实际的情况。利用组合库的最好方法是按照原来写组合库的风格编程,就是说,应该构建一些可以在图形中重用的简单元素。接下来,我们将看一下如何创建一个场景,组合七星。很明显,开始是创建星,在清单 12-4 中可以看到,我们是如何创建星(Star)函数的定义的,这个函数创建了镜像的三解形,然后,把它们组合在一起,加上一点点偏移,构成了一个六边形的星。从这个示例,我们可以得到一些有关通过简单图形构建复杂图形的概念。有了这个星的定义以后,只需要一个简单的位置列表,告诉星应该在哪里打印出来,清单 12-4 中有点的列表。有了这两个元素之后,就可以用函数List.map和compose 把它们组合起来,创建需要的场景了。下面,就可以用前面清单中的方法来显示场景了。
清单 12-4 用组合库创建更复杂的图形
module 12-4
open System.Drawing
open System.Windows.Forms
open Strangelights.GraphicDSL
// define a function that can draw a6 sided star
let star (x, y) size=
let offset = size /2
// calculate the first triangle
let t1 =
Combinators.triangle false
(x, y -size- offset)
(x -size, y+ size - offset)
(x +size, y+ size - offset)
// calculate another inverted triangle
let t2 =
Combinators.triangle false
(x, y +size+ offset)
(x +size, y- size + offset)
(x -size, y- size + offset)
// compose the triangles
Combinators.compose [ t1; t2 ]
// the points where stars should beplotted
let points = [ (10, 20); (200,10);
(30,160); (100, 150);(190, 150);
(20,300); (200, 300);]
// compose the stars into a singlescene
let scence =
Combinators.compose
(List.map (funpos-> star pos 5) points)
// show the scene in red on theEvalForm
let form = newEvalForm([scence, Color.Red],
Width =260, Height =350)
// show the form
Application.Run form
图 12-1 那显示了结果图形。
图 12-1 由组合库渲染的场景
我们已经看到了用两种方法创建组合库(通过数据结构,库创建小语言)。至此,可能已经看出如何把一个问题分解成抽象的描述,它是基于一组很小的基本操作的,这对于建于这些基本操作上的其他库可能是有帮助的。
注意
如果打算用更深入的视角研究组合库,应该看一下由Simon Peyton Jones、Jean-Marc Eber 和 JulianSeward 写的本皮书《Composing contracts: an adventure in financial engineering》,白皮书深入、易懂地研究了用组合库描述 derivatives contracts,白皮书中的示例是用的Haskell 而不是 F#,但是,可以把它转换成 F#。白皮书的地址:
http://research.microsoft.com/en-us/um/people/simonpj/papers/financial-contracts/contracts-icfp.htm。
原文地址:http://blog.csdn.net/hadstj/article/details/27677801