脚本语言:Xmas(二)

时间:2023-03-10 06:20:22
脚本语言:Xmas(二)

  本篇,来谈谈类型系统,以及部分与垃圾收集器相关的内容。

  一、基本类型

  Xmas的基本类型:Null、Boolean、Label、String、Ref、Function、Integer、Float、Decimal、Array、List、Set、Map、Object;14个,相对于其他的脚本语言是有些多的;但,这些类型都是“原子的”,少了其中一种,多少会有些不方便(但并非不完备,如Decimal可以代替所有算数类型;Map则可以可以替代所有容器类型)。如果,你看过上一篇,会发现有那么一点点的不同——一星期前,Xmas只有13种基本类型。

  是的,【Label】是昨晚刚加入的(嗯,但经过了一段时间的思虑);其在Xmas代表了:一种编译期常量,或者说是可命名字符串常量的整数映射。加入的原因有二:

  1、Object和Struct函数,缺少一种合适的参数类型:Object{ field : value} -> Object("field", value),这是之前的做法(当然,这个转换由编译器完成);但是,当我们想要纯手写时,每个字段的名字都使用字符串来代替,是一种不自然的方式;而非更自然的:Object(field, value)。这里【field】便是一个Label。

  2、为了性能,有了Label,我们可以在运行时,大量使用Label来代替字符串;这将会有一个可观的回报(其实,也不会太大;部分字符串,是在编译器创建的,并且是共享的)。

  唯一需要纠结的是:如何创建/申明一个Label?

  其他语言中典型的有 :field(Ruby)、‘field(Lisp);很丑,说真的。本来Ruby的“:”还可以接受,但Xmas中的“:”已经有了几种用途了,再加入一种会给编译器带来一定的负担(有歧义的语法,处理需要费一些脑细胞)。

  然后,想到了Xmas中的“统一的类型创建”:new Type() / new Type{}。后者是统一初始化的语法(比如Map可以:new Map{ val1 : 12};而不可以:new Map(val : 12));那么,Label也是需要支持这种形式的创建,那么其便可以这样:

    var = new field;//创建一个Label(“field”),没有()/{}

  于是便有了下面的 Object创建方式:

    var0 = new Object{ field : value}; //统一初始化
    var1 = new Object(new field, value); //手动函数调用

  这比使用字符串的方式干净一些;当然,也有妥协,毕竟Label本身和其他的字面量(变量、参数、函数名)形式上没有任何的区别;而,我们却必须要去区别(否则,必须要使用类似统一初始化的附属结构来区分)

  需要注意的是,new本身是带有在创建对象的意思,在Xmas中也没有例外;但Label是个例外,其是在编译期创建的,所以并没有任何额外的消耗。

  二、创建

  Xmas中每种类型都有至少3种创建的方式(除了前面的Label,其无法无歧义地申明):字面量、new Type()、new Type{}、Type()。

  基本类型一般便有4中(至少形式上):

    var0 = ;
    var1 = );
    var2 = };
    var3 = );

  其中让人困惑的是:new 是否有必要? 有:

  1、只有加上new,才能够使用统一的初始化方式(这对于Map和Object很重要的方式)。

  2、只有加上new,编译器才能够知道:你是在创建一个对象;才能够执行相应的额外处理(后面会讲)。

  3、让你自己和别人知道,这句代码是在创建一个对象。

  至于为什么不让编译器自己去识别对象的创建(如:var = Integer(12));可惜的是这是可不能的:

;};
    );

  Xmas中对象的创建函数并不是关键字(也不可能,如自定义类);所以,上面的这段代码,编译器并不知道“Integer(12)”是创建一个整数,还是调用一个函数——实际上,正确的是函数调用。这就是,支持函数式编程的代价之一:函数调用的静态性没有了,因为任何变量都可能是一个函数。

  所以,new便有了存在的必要:其强制声明一个对象的创建,其后面的是该类型的名字。当然,有了这个信息后(确定是否是对象创建),编译器便有了更多可以做的事情:自定义类型的创建优化。

  本质上,一个自定义类型的创建需要,下面完整的四个步骤:

    //1、创建一个空对象(类似于C++的分配内存)
    obj = new Object();               //这是一个函数调用,可以不加“new”
    //2、设置类信息(等价于C++的设置虚函数表)
    obj.class = new Class("AClass");  //这也是一个函数调用,并不需要“new”
    //3、调用自定义类的构造函数
    obj.__init();                     //这个字段是新加的,其保存了构造函数
    //4、设置对象的final属性
    try_final(obj);                   //这个比较复杂,后面会讲(涉及到GC)

  这也是一个使用Reflection(反射)的完整案例;当然,这是手动的方式,也并非推荐的方法。有了new作为声明后,编译器可以将所有的这些操作,直接映射到一个系统函数的调用上:

    obj = class("AClass"); //使用了和关键字“class”同名的函数

  因为和关键字同名,所以代码中是不可以直接调用的;当然,你可以这样:

    func = new Function("class"); //创建一个函数,通过函数名
    obj = func("AClass");

  当然,这不是重点;重点在于,如果不使用new来告诉编译器“这是在创建对象”;而是用:

    obj = AClass(); //直接调用类的创建函数(由编译器生成的)

  当然,如果前面没有一个名为“AClass”的变量的情形;编译器是可以推断出这是一次对象创建的,只是我懒;而且这并不是推荐的做法。更关键的是,为了识别出“这是一次对象创建”,编译器需要知道额外的信息:知道有这么一个类型,名叫“AClass”。而非,可以推迟一些时间。

  三、面向对象

  我们都知道,本质上面向对象,只是对象的内存布局如何设置(父类和子类),以及虚函数表如何实现。

  在前面,我们已经知道了Xmas中的虚函数表:一个名为“class”的字段——其中放置了自定义类型的所有信息(成员函数,类型信息)。那么,【.class】中究竟有什么?

class AClass :Xmas{
    function AClass(){
        this.Xmas(); //调用父类的构造函数
    }
    function func(){}
}

  上面的定义,究竟会生成怎样的class?

class:
    ....父类Xmas的东西....
    __init  = Function : AClass.AClass //AClass的构造函数
    __type  = Function : AClass        //编译器生成的创建函数(其内部调用:class("AClass");)
    __name  = String   : “AClass”      //类型名字
    __final = Boolean  : true/false    //类型的final属性
    AClass  = Function : AClass
    func    = Function : AClass.func   //自定义的函数

  很多东西是显而易见的:新加的__init是为使用反射创建对象时的方便,否则就需要如下的方式:

    .......
    obj.constructor = get_value(obj.class, "AClass"); //批量反射创建时,我们可能只知道字符串形式的类型名称
    obj.constructor();
    .......
    // 以上对应了:
    obj.AClass();

  关于虚函数表这个东西,只要知道有这么一个东西;便也能够很自然构造出对应的。但另一个问题:对象的内存布局,一直是面向对象的一个大问题。除了C++其他语言使用interface来解决这个。是的,只要禁止了多重继承,这个问题便变得很简单。

  对此Xmas,并没有给出合适的方案:简单的成员覆盖——父类的重名字段和函数,都只是简单地覆盖了,不可访问;即使在父类的空间也不可能。原因,很简单我个人暂时不会用到面向对象的那些功能;所以,也不会耗费精力去做.......懒癌,晚期。

  当然,Xmas不止这些;通过反射,我们可以很轻易地构造出一个对象;但,或许我们可以这样:

    obj = new Object();
    obj.;  //这算是恶作剧?

  Xmas并不会让这段代码得以运行;在执行到“obj.class = 12;”时,会检测【右值】是否真的是一个class。这意味着,我们不能够通过反射,手动地创建出一个新的类型(在几天前,还是可以的)。Xmas限制了反射的能力——变成“只读”;但同时,也禁止了创建出无效对象的可能——“class”字段是一个内部字段,只能够用来放置类型信息。

  需要额外一提的是,Xmas的类型信息是按需生成的——在第一次创建该自定义类型时,才构建对应的类型信息。

  四、final属性

  在Xmas中,一个对象有三重意义上的【不可变】:

  1、除了容器类型(Array、List、Set、Map、Object),其他类型都是【不可变的】。

  2、容器类型,拥有一个可设置的final属性;而且只可设置一次:将其设置为【不可变】(默认是可变的)。

  3、自定义类型,在继承了第二点的同时(因为其使用Object作为容器);额外有一个由编译器生成的final属性(即:class.__final)。

  对于第一点,很容易理解:类似Java中String,其是不可变的,对其任何的操作都将创建一个新的String。同样,Xmas中的不可变类型,也是如此——你没有任何办法改变一个Integer内部的值(这很有函数式风格)。这点是有两面性的:

  1、不可变性,意味着任何的计算都将创建新的对象;而非复用,这无疑加重了GC的负担(最典型的是循环中的游标,循环1万次,需要创建1万个游标)。

  2、不可变性,意味着对象复用没有任何的负担;一个String可以在整个代码中到处复用,而不用担心其被改变;相反,这减少了GC的负担(但减少的并不多)。

  这对于编译器而言,其意义更大:编译时,可以放心地复用任何对象(编译期只会创建简单的不可变对象),而不用担心被污染。

  而第二点,类似C++的const修饰:所修饰的对象不可变,但其成员中指针所指向的对象,并不能限制。就一门语言而言,这样的属性,并没有必要的存在(许多语言都没有);而且这个不可变属性,是绝对的——一旦设置了【不可变】,不可更改,该对象永远都不再可变!

list = , 'hh', 2.2};
final(list);  //设置对象为不可变
list[] =   //错误!运行时,会抛出异常

  第三点,是继承了第二点的概念,但有所扩展:一个自定义对象创建后,其会被自动地设置final属性(根据class.__final中的值):

    try_final(obj);
  // 等价于
  if(obj.__final){
    final(obj);
  }

  其目的同样是,为了完成相同的目的:设置对象为【不可变】,自动地。

  所以,唯一的疑问就是:class.__final从何而来?

class ClassA{
    function ClassA(){
        ;
    }
}

class ClassB: ClassA{
    function ClassB(){}
    function setValue(val){
        this.value = val;
    }
}

  其中ClassA.class._final = true,而ClassB.class.__final = false。为什么?  就该字段的意义而言,其意味着:该类型的实例,是否允许被更改。Xmas,有这么一个简单的逻辑——如果类定义中,没有任何函数更改【this】的内容;那么,该类型便是不可变的。

  所以,想要定义一个可变的类,是需要对应的【setXXX】系类函数的。 所以,ClassB有一个函数改变了this的内容,因此是可变的。当然,构造函数内部的任何操作,并不影响final属性——因为,构造函数只会被调用一次。

  五、GC

  为什么会有垃圾收集器出现在这里? 因为,【final】本身是为了GC存在的。

  前面有讲过Xmas的GC是基于标记的分代式收集器;因此,为了完成真正意义上的分代:进行young GC时,并不会去标记老年代的对象(尽可能地)。Xmas提供final抽象:一个final对象其直接所持有的对象,其所处的分代并不会,从老年代降到年轻代,只会从年轻代升到老年代。

  这句话很是抽象;其大致上代表了:

    val = ;
    obj = new Object{ val : val};
    final(obj);
    .....gc......
    、obj,val同时进入老年代
    、obj为final,那么obj不可能有指向年轻代的引用(任何时刻)
    .....young gc.....
    //因为obj为老年代,且没有指向年轻代的引用,所以不必扫描

  就结果而言;在有大规模的存活对象时(多数已晋升到老年代;因为年轻不可能有大量的对象);进行young GC时,其所需要扫描的对象比例在10:1~1000:1之间(Xmas得到的结果)。这样的效果是卓越的,GC总时间减少了80%-90%。

  当然,这样的假设只有在:拥有大量不可变对象的背景下,才会成立。Xmas为此付出大量努力。比如对象的创建有两种方式:new Object{}、new Struct{}。区别在于,Struct创建的对象,其被设置了不可变。Xmas通过GC免去了内存管理的烦恼;但同样地也给予了一定的干涉GC的能力,当然,或者说是辅助GC的更好地工作(Xmas的自定义类的final属性,是自动化辅助GC的能力;并不需要手动干预,也不能)。

  至于原因嘛?因为,GC并非Xmas的一个子模块,而是另一个完全独立的系统;其存在并非只是为了Xmas(大部分是),还有我的库中的其他模块。

  PS:否则需要另外的技术来避免大量的老年代对象被扫描,而且其有着额外的代价。

  六、类型系统

  Xmas有一个获取对象类型的关键字函数:typeof。其返回的东西,很有意思:函数。在最早的版本中,返回的是字符串(当然,现在的话,也可以考虑Label)。

  但返回函数,能够更优雅地解决问题。

    obj = new ClassA();
    if(typeof(obj) == ClassA){
        print('he he');
    }

  通过返回该类型的创建函数;我们可以写出更直白和方便的代码:不仅免除了字符串的引号,而且可以这样:

    var = typeof(obj)("I'm a param");

  上面的代码创建了一个obj类型的新的实例,即使我们并不知道obj的类型是什么(函数作为第一值类,还有很有价值的)。