动态类型的静态化实现
为便于开发,larva采用动态类型,于是首先遇到的一个问题就是,在转化为java的时候,如何处理动态类型带来的问题。乍一看,这个不是很复杂,已经有前车之鉴了,Cython就可以把python代码直接转化成C代码,我的做法和Cython有相似之处,但考虑效率问题,做了一点修改 (虽然第一版本并不实现class语法来自定义类,不过在论述这个问题的时候,假定有自定义类,因为主要矛盾就在自定义类的属性方面)
从实现方式来看,动态类型静态化可以比较直观,前面已经讲过,动态类型语言实际上是一个所有对象的类型的基类都是Object的语言,而所有的变量和引用的类型都是Object,可以引用任意对象,这是动态类型实现的技术基础,需要保证的就是当通过Object类型的变量引用来操作对象时,能正常体现每个对象实际的行为,这一点可以用多态来实现
关键的矛盾,前面已经说过了,再重复一遍,以java为例,不考虑基础类型,除Object类外的所有类都是Object的子类,因此Object类型可以引用各种对象,如前所述这也是java泛型实现的基础。因为这个原因,龙书中甚至将java划为动态类型语言。但是java无法处理这种情况:
def f(a, b):
return a + b
这个python代码如果转成java,可能是:
Object f(Object a, Object b)
{
return a.add(b);
}
这个代码当然是错的,因为Object没有add方法,即便传入的实际类型有也没用
这个问题的解决办法也很简单,这也是静态化的关键所在:只要Object有这些接口即可,事实上,python就是这么干的,如果把python的object类型实现用java的方式描述,大概是这样:
class Object
{
Object add(Object other) //加法,双下划线开头和结尾是python的格式
{
throw new Exception("Not implemented");
}
... //其他各种操作方法
}
对于其他类型,如果需要加法,则只要重写add即可,通过Object类型引用可正常执行到实际对象的方法,这是典型的多态应用
接下来的问题就是,Object可能实现所有接口吗,简单说,是可能的,不过也需要分情况讨论
对于运算符,Object直接实现所有运算即可,由于运算符是语法规定的,而且一般也不会很多,直接实现没问题,如果源语言本身没有类和对象的概念,则这样做就可以了,因为对对象的操作只有运算符可用;但是,如果引入面向对象,每个对象除了运算符外,还有属性和方法,而这两者是无法在语言设计的时候就确定好的,因为可以随便取名
python对这个问题的解决办法,就是将属性和方法放在一个dict里面,通过“.”运算符间接操作的方式来实现动态性,换句话说,对python的对象而言只有运算符操作,属性和方法被认为是和一个名字字符串做“.”运算,因此,python的object类的所有接口可以静态实现。Cython在做代码转换的时候,也是这么干的,所有代码(除了特有的pyrex)都原样翻译成在虚拟机里面应该执行到的序列,这样一来只是省了字节码解释流程而已,因此效率提升比较有限(dhrystone的结果,约只提升一倍)
要采用效率高的实现,则最好Object拥有所有可能的属性和方法,在做语言的时候不可能确定程序员会用到哪些属性和方法,但是在编译的时候就可以知道,对于一个程序涉及的所有代码进行全文分析,即可得到所有(可能)出现的属性和方法了
这也就是说,Object这个对象的实现并不是在语言实现的时候进行,它如何实现和语言本身没关系,而是和具体的程序有关,采用转化代码的办法实现语言,就可以很方便实现这种静态化,比如,larva的类型系统就类似这样实现(java):
class LarObj
{
//可静态化的接口,比如运算实现,内置类型的方法等,仅列接口(都是public),实现代码略
LarObj op_add(LarObj other) //加法运算
...
LarObj get_attr_a() //获取属性a
void set_attr_a(LarObj obj) //修改属性a
...
LarObj call_method_f_ret_LarObj() //调用方法f,返回LarObj对象
LarObj call_method_f_ret_LarObj(LarObj obj) //f的一个重载
}
class BuiltinObj_A extends LarObj //一个内置类型A
{
LarObj a; //属性
LarObj get_attr_a() //获取属性a
void set_attr_a(LarObj obj) //修改属性a
LarObj call_method_f_ret_LarObj() //调用方法f,返回LarObj对象
}
class BuiltinObj_B extends LarObj //一个内置类型B
{
LarObj a;
LarObj b;
LarObj call_method_f_ret_LarObj(LarObj obj) //调用方法f,返回LarObj对象
}
class UserObj extends LarObj //用户自定义类
{
LarObj call_method_f_ret_LarObj(LarObj obj) //调用方法f,返回LarObj对象
}
可以看到,所有类,无论是内置还是用户定义的,都继承自LarObj类,而LarObj类中,需要实现(当前程序)所有可能的接口,对于属性的存取,采用get/set接口进行,当然,也可以把属性都写在LarObj里面,不过这样一来太多了,浪费内存,而且在代码中访问不存在的属性的时候不好处理异常;对于方法的调用,也采用接口的方式实现。这样一来,就保证了转化后的代码中,所有变量和引用可通过LarObj来操作
所有内置类型在实现的时候,都继承于LarObj,但它们不能直接使用,因为LarObj只能在编译期动态生成,然后整体联编成一个完整的程序,和一般的代码流程反过来的,先创建子类,再生成合适的基类,只要基类兼容子类的所有操作即可,实际编译larva代码的时候也是这样,先综合所有(内置和自定义)类型,然后最后生成基类
由于这个类型系统没有采用python那种用hash表的方式,因此不支持动态增减属性,更不支持getattr这种根据属性名找属性的方式(要做其实也可以,作为一个附加功能即可,不过感觉没必要)
虽然已经尽量消除了动态特性,但是结合语言的特点,像动态查错还是保留了,比如:
func f(a):
print a.x
在编译阶段,LarObj会有一个get_attr_x的方法,如果传入的参数a引用的对象没有属性x,则直接执行LarObj的方法会报一个No attribute 'x'的错误
假如希望在编译期能检测出这种错误,即保证f传入的a有x属性,也是可以做到的,但是这样一来就需要比较复杂的类型推导了(参考前述《静态类型推导》)。不过,由于这种动态性只有在代码有bug的时候才会触发,因此还是可以容忍的,只需要保证在程序正常运行期间没有太多的动态性,运行效率已经能得到比较大的提高了