为了让更高级的语言可以编译到
Vczh Library++ 3.0上面的NativeX语言,原生的泛型支持是必须有的。泛型不仅仅是一堆代码的填空题那么简单,因为编译之后的Assembly(dll)必须可以容纳泛型声明,而且其他的Assembly可以实例化包含在其他Assembly里面的泛型声明。这是非常麻烦的(被.net搞定了,jvm则由于种种原因搞不定,大概是因为jvm对assembly version的支持太差导致的,你知道.net 2.0的东西是不能引用4.0的dll的……)。不过先抛开这个不讲,虽然如何在Assembly里面实现泛型我已经心里有数了,但是这里还是从语义的层面上来考虑泛型的设计。
在讨论之前还是要强调一下一个大前提:
NativeX基本上就是一个语法更加容易看懂的C语言而已,功能完全是等价的。于是我要在NativeX上加泛型,其实也就是等于在C上面加泛型。我们使用泛型完成的事情可以有很多,譬如说定义泛型的结构体,定义泛型的函数,还有泛型的存储空间等等。首先让我们讨论泛型的结构体。最终的语法可能会跟现在不一样,因为NativeX的使命是作为一棵语法树出现的,所以做得太漂亮的价值其实不是很大。
一、泛型结构体 泛型的结构体还是比较容易理解的,举个小例子:
1 generic<type T>
2 structure Vector
3 {
4 T x;
5 T y;
6 }
这样子我们就创建了一个泛型的结构体。任何熟悉C++或C#的人都知道这是什么意思,我就不做解释了。使用的时候跟我们的习惯是一样的:
1 Vector<double> v;
2 v.x = 1.0;
3 v.y = 2.0;
于是我们创建了一个泛型的变量,然后修改了它的成员。
二、泛型全局存储空间 其实泛型的全局存储空间基本上等于编译器替你做好的一个key为类型的大字典。有些时候我们需要给类型加上一些附加的数据,而且是按需增长的。这就代表在编译的时候提供泛型全局存储空间的Assembly并不知道将来有多少个key要提供支持,所以创建它们的工作应该是虚拟机在链接一个想使用其他Assembly提供的全局空间的新Assembly的时候创建的。这里带来了一个问题是不同的Assembly使用相同的类型可以访问相同的全局存储空间,这里先不讨论具体实施的手段。
语法上可能比较混淆:
1 generic<type T>
2 structure TypeStorage
3 {
4 wchar* name;
5 function T() builderFunction;//这是函数指针
6 }
7
8 generic<type T>
9 TypeStorage<T> typeStorage;
在一个变量上面加泛型可能会有点奇怪,不过这里的定义还是很明确的。typeStorage是全局变量的泛型,因此typeStorage<int>、typeStorage<double>甚至typeStorage<Vector<Vector<wchar*>>>等等(啊,>>问题)是代表不同的全局变量。不同的Assembly访问的typeStorage<int>都是相同的全局变量。
三、泛型函数 泛型函数我们也都很熟悉了。一个简单的例子可以是:
1 generic<type T>
2 T Copy(T* pt)
3 {
4 result = *pt;
5 }
需要指出的是,NativeX并没有打算要支持泛型结构、全局存储和函数的特化或偏特化。因此我们会很惊讶的发现这样的话泛型函数唯一能做的就是复制东西了,因为它调用不了其他的非泛型函数(跟C++不一样,NativeX的泛型函数更接近于C#:编译的时候进行完全的语义分析)。虽然泛型函数可以调用其他的泛型函数,但是最终也只能做复制。因此我们要引入一个新的(其实是旧的)概念,才可以避免我们为了提供各种操作在泛型函数的参数上传入一大堆的函数指针:“概念”。
四、泛型concept 泛型结构体和全局存储仅仅用来保存数据,所以泛型concept只能在泛型函数上面使用。这个concept跟C++原本打算支持的concept还是很接近的,只是NativeX没有class,因此只好做一点小修改。
泛型concept主要是为了指定一套操作的接口,然后让编译器可以完成比调用函数指针更加高效的代码。偷偷告诉大家,把他叫concept只是因为NativeX跟C很像,但其实这个概念是从Haskell来的……我们还是来看看concept怎么写吧。
1 generic<type T>
2 concept Addable
3 {
4 operation T add(T a, T b);
5 operation T sub(T a, T b);
6 constant T zero;
7 }
8
9 generic<type T>
10 concept Multible : Addable<T>
11 {
12 operation T mul(T a, T b);
13 operation T div(T a, T b);
14 constant T one;
15 }
这里定义了加法和乘法的两个concept。我们可以看出concept是可以继承的,其实也是可以多重继承的。concept里面可以放操作(operation),也可以放常数(constant)。这里的常数跟全局存储的机制不同,全局存储可以自动为新类型产生可读写的空间,而concept的常数不仅是只读的,而且还不可自动产生空间。之前考虑到的一个问题就是,我们可能需要把外界提供的某个concept的operation的函数指针提取出来,有这种需要的operation可以把这个关键字替换成function,这样在实例化concept的时候,那个标记了function的操作就只能绑定一个函数而不是一个表达式了。我们可以尝试为int创建一个Multible的concept:
1 concept instance IntMultible : Multible<int>
2 {
3 operation T add(T a, T b) = a+b;
4 operation T sub(T a, T b) = a-b;
5 operation T mul(T a, T b) = a*b;
6 operation T div(T a, T b) = a/b;
7 constant T zero = 0;
8 constant T one = 1;
9 }
于是我们可以写一个函数计算a+b*c:
1 generic<type T, concept Multible<T> multible>
2 function T AddMul(T a, T b, T c)
3 {
4 return multible.add(a, multible.mul(b, c));
5 }
然后调用它:
1 int r = AddMul<int, IntMultible>(3, 4, 5);
五、另一种concept instance 虽然我们不允许泛型的结构体、全局存储和函数进行特化,但是因为特化实在是一个好东西。上面的concept instance是没有弹性的,因为你不可能通过一个concept instance拿到另外一个concept instance。考虑一下delphi的带引用计数的嵌套数组,如果我们想让delphi可以编译到NativeX上,则势必要支持那种东西。主要的困难在于delphi支持的带有引用计数的数组和字符串,因此在对array of array of string进行释放的时候,我们首先要拿到array of array of string的concept instance,其次在释放函数里面要拿到array of string的concept instance,最后还要拿到string的concept instance。这个用上面所提出来的方法是做不了的。因此我们引进了一种新的concept instance:叫concept series。这个跟haskell的东西又更接近了一步了,因为haskell的concept instance其实是匿名但是可特化的……
于是现在让我们来实现Array和String,并写几个类型的Increase和Decrease的函数(函数体一部分会被忽略因为这里只是为了展示concept):
1 structure String
2 {
3 int reference;
4 wchar* content;
5 }
6
7 generic<type T>
8 structure Array
9 {
10 int reference;
11 int length;
12 T* items;
13 }
我们从这里可以看出,string跟array的区别就是在于长度上面,string有0结尾而array只能通过记录一个长度来实现。现在我们来写一个用于构造缺省数值、增加引用计数和减少引用计数的concept series:
1 generic<type T>
2 concept Referable
3 {
4 operation T GetDefault();
5 operation void Increase(T* t);
6 operation void Decrease(T* t);
7 }
8
9 generic<type T>
10 concept series DelphiTypeReferable : Referable<T>
11 {
12 }
concept series其实就是专门用来特化的concept instance。但是为了防止不同的Assembly特化出同一个concept series所带来的麻烦,我可能会规定允许特化concept series的地方,要么是在声明该concept series的Assembly,要么是声明涉及的类型的Assembly。因为我的Assembly不允许循环引用,因此对于同一个concept series C<T,U>来讲,就算T和U分别在不同的Assembly出现,那么也只能有一个有权限特化出它。下面来看特化具体要怎么做。首先我们特化一个简单的,string的DelphiTypeReferable:
1 concept series DelphiTypeReferable<String>
2 {
3 operation GetDefault = StringGetDefault;
4 operation Increase = StringIncrease;
5 operation Decrease = StringDecrease;
6 }
StringGetDefault、StringIncrease和StringDecrease都是一些普通的函数,内容很简单,不用写出来。现在让我们来看看Array应该怎么做:
1 generic<type T>
2 concept series DelphiTypeReferable<Array<T>>
3 {
4 operation Array<T> GetDefault() = ArrayGetDefault<T>;
5 operation Increase = ArrayIncrease<T>;
6 operation Decrease = ArrayDecrease<T>;
7 }
看起来好像没什么特别,不过只要想一想ArrayDecrease的实现就知道了,现在我们需要在ArrayDecrease里面访问到未知类型T的DelphiTypeReferable<T>这个concept instance。因为当自己要被干掉的时候,得将引用到的所有对象的引用计数都减少1:
1 generic<type T>
2 function void ArrayDecrease(Array<T>* array)
3 {
4 if(array->reference<=0)exit;
5 if(--array->reference==0)
6 {
7 for int i = 0
8 when i < array->length
9 with i--
10 do DelphiTypeReferable<T>.Decrease(&array->items[i]);
11 free(array->items);
12 array->length=-1;
13 array->items=null;
14 }
15 }
这样一大堆concept series的特化组合在一起就成为会根据类型的变化而采取不同行为的concept instance了。于是我们还剩下最后的一个问题,那么其他类型的DelphiTypeReferable应该怎么写呢?其实只需要玩一个小技巧就行了,不过在这里将会看到NativeX支持泛型的最后一个功能:
1 generic<type T>
2 concept series DelphiTypeReferable<T>
3 {
4 operation GetDefault = GenericGetDefault<T>;
5 operation Increase = null;
6 operation Decrease = null;
7 }
8
9 generic<type T>
10 T GenericGetDefault()
11 {
12 }
返回null的operation可以赋值成null以表示不需要执行任何东西。如果你将一个有副作用的表达式传进去当参数的话,副作用会保证被执行。
关于语义上的泛型就讲到这里了。
posted on 2010-06-12 23:58
陈梓瀚(vczh) 阅读(2519)
评论(2) 编辑 收藏 引用 所属分类:
VL++3.0开发纪事