跳转至

Unit 2.0 (设计稿)

目的

  • 简化、优化Unit的代码编写
  • 可以按需动态加载插件
  • 可以输出变量
  • 可以输出引入的其他Unit
  • 允许把多个Unit组装成包,可以自动下载安装相关依赖

Unit 2.0 的编码原则

  • 每个.tsl文件都是一个Unit,无需专门设置Unit名称,以文件路径名来引入,支持相对路径。

    UnitA.tsl:

    x := 1;
    
    function My(y)
    begin
        return x + y;
    end;    
    

    Main.tsl:

    Uses "./UnitA";
    
    println("x={}", UnitA.x);
    
  • 不需要定义Initialization和Finalization。

    • Unit开头的代码就是Initialization
    • Finalization 可以通过Unit的变量析构来实现。
  • 不区分Interface和Implementation,默认全部输出,要控制输出可以使用{$export}

    {$export x, y} // 只输出x和y,My不会输出
    
    x := 1;
    y := 2;
    
    function My(y)
    begin
        return x + y;
    end;  
    
  • 按需加载插件,用{$plugin}来加载插件。

    A.tsl:

    {$plugin MyPlugin} 
    

    如果不指定文件名后缀会自动加上,例如:

    文件名 操作系统
    MyPlugin.dll Windows
    libMyPlugin.so Linux
    libMyPlugin.dylib macOS

    会在A.tsl所在目录加载相应的动态库,并引入里面的函数。 如果有多个Unit都加载同一个插件,插件实际只加载一次。

  • 如何既可以做Unit来引入使用又可以做主程序来运行?

    {$main begin}{$main end} 来包裹作为主程序运行的代码。这样在引入时不会编译执行这些代码,只有作为主程序直接运行时才会编译执行。

    My.tsl:

    MyVar := 1;
    function Myfunc()
    begin
        ...
    end;
    
    {$main begin}
    println("run as main!");
    {$main end}
    

    这样在uses "./My"时不会打印run as main!TSL My.tsl直接运行时才会打印run as main!。这样方便TSL代码既可以做库用,也可以做工具用。

注意

  • {$main begin}{$main end}包裹的代码在作为引入时并不会做编译,直接忽略。 所以不要在其中定义想要输出的内容。
  • TSL解释器可以考虑提供命令行参数允许直接执行某个Unit。

Unit的引入

  • 用路径名引入。

    Uses "std", // 引入std,可以通过std.来引用,例如std.last
       * "std", // 直接引入std的所有输出,例如first, last,...
         "std" as xyz, // 用别名xyz来引入std,可以通过xyz.来引用,例如xyz.last
         "std" -> (first, last) // 只引入std的first和last,可以用first和last来引用
         ;
    
  • 查找路径,支持绝对路径和相对路径。

    Uses "./a",   // 相对路径
         "/my/a", // 绝对路径,会在预设的目录中查找
         "std"    // 不指定默认是绝对路径查找,例如查找d:\funcext\std.tsl
         ;
    
    Unit的查找是非常快的,例如指定在d:\funcext中查找,直接就可以通过d:\funcext\my\a.tsl来定位。

  • 只允许在文件头引入,其他地方的引入失去意义(作为兼容,此时允许unit 1.0的引入)。

  • 不支持循环引入,例如不支持A引入B,而B又引入A。

  • 将来可以支持一些引入的扩展,例如:

    Uses "tpi:MyPackage", // 从tpi这样的包索引服务中引入MyPackage包
         "builtin:std",   // 强制使用内置的std单元
         "std@v1.1",      // 指定导入的版本
         
    

Unit的输出

  • 默认全部输出。

  • {$export}控制输出。

    {$export 
      x,
      y,
      "./B", // 会输出B中全部输出
    }
    
  • 输出引入的其他Unit。

    例如可以用ui.tsl来输出ui_*.tsl

    ui.tsl:

    Uses * "./ui_window",
         * "./ui_frame",
         * "./ui_font",
         * "./ui_edit",
         ...
         ;
    

  • 定义的类可以作为Unit的属性输出(同理,unit中定义的变量和函数也是unit的属性的一种)。

    A.tsl:
    Type MyClass = class
        x := 1;
        y := 2;
    end;
    
    B.tsl:
    Uses "./A";
    
    obj := new A.MyClass();
    
    // 也可以继承
    type ZClass = class(A.MyClass)
        z := 3;
    end;
    

    由于在编译时已经知道是类了,我们还可以做到更多:

    Type MyClass = class
        x := 1;
        y := 2;
    end;
    
    println("MyClass.classinfo={}", MyClass.Classinfo()); // 可以轻松区分类的方法和类实例的方法
    
    obj := MyClass(); // 构造可以省略new
    obj := MyClass(x: 3, y: 4); // 甚至构造时就可以直接给成员变量赋值
    v := MyClass.x; // 直接使用类的值,可以轻松和类实例的值区分开来
    

    关于类的改进,会在后续的提案中给出。

Unit中定义的变量在其子函数中的可见性

  • 子函数中如果赋值,都是定义局部变量。

    x := 1; // x是这个unit的变量,为了便于和子函数中的局部变量区分,我们在这里称之为unit.x
    
    function sub()
    begin
        println("x={}", x); // 打印x=1,x相当于unit.x
        x = x + 1;        // 编译错误:x是局部变量,和unix.x可能发生冲突
    end
    
  • 如何解决局部变量可能和unit变量冲突的问题?

    • 子函数中用var开头来强制声明成局部变量。
    x := 1;
    
    function sub()
    begin
      var x = 2; // 编译成功:x是局部变量,unit.x仍然是1
    end
    
    • 子函数中用uplevel#0来声明使用unit的变量。

    x := 1;
    
    function sub()
    begin
      uplevel#0 x;
      x := 2; // 编译成功:x是unit.x,unit.x等于2
    end
    
    uplevel#0此时的寓意是在本文件的最高层(也就是unit层)中定义的变量。

  • 为什么不使用global?

    • 因为这个不是global的,只是unit的。
    • global有global的问题,例如不知道应该在哪里来初始化它。
  • 小结

    • 子函数的变量不要和unit的变量重名。
    • 在子函数中如果需要修改unit变量的值,用uplevel#0

package

package包含多个unit。

package的组织

  • 可以是一个目录,目录中包含tpi.ini文件。tpi.ini文件的内容至少包含版本和依赖等信息。
  • 可以是一个zip文件,相当于目录的打包。

package的使用

  • tsl解释器可以直接运行打包的package,例如天软服务器可以执行打包好的代码。
  • 命令行工具tpi可以上传、下载package和它的相关依赖。

兼容性

  • Unit 2.0只查找.tsl文件,Unit 1.0只查找.tsf文件,所以完全不会有冲突。
  • Unit(UnitName).UnitFunction的方式只支持Unit 1.0,Unit 2.0已经在编译的时候就已经确定了调用,不需要这个模式。
  • Unit 1.0所有行为都不变。

    • 不能输出变量,只能输出函数。
  • 可以混用Unit 2.0和Uint 1.0的引入,但不推荐这样使用。

Uses "std", unitA;
unitA将会使用Unit 1.0的查找,查找.tsf文件。

额外的好处

  • 编译时就可以确定相关的变量、类定义和函数调用,所以运行时就没有额外的开销。
  • 由于变量、类定义和函数调用在编译期间就已经确定了,所以在编译的时候就可以做很多优化的工作,例如inline;同时编译时也能做更多的检查,例如变量未初始化就引用、类或函数找不到等等,而不用等到运行时再报错。
  • 为将来有可能直接编译成机器指令做准备。

Unit 2.0 的应用范例

获取公共配置信息

settings.tsl:

Config := $[
    db: 'mysql://.....',
    root: '/var/web/mysite'
];

main.tsl:

Uses * "./settings";

db := Config.db;

设计原则和取舍

最简化原则,Less is more

  • 如果用.tsl可以解决,不会引入其他文件名后缀,例如.tsf.tsu.tsp, ...

    • .tsl能解决吗?当然可以。PHP只有.php,Python也只有.py一个文件名后缀。
他山之石

不熟悉Python的人可能会认为在每个目录中必须要有__init__.py才可以import其他.py文件。
这个是不对的,即使没有__init__.py,Python仍然会导入包。

如果可以在编译期间解决,就不要留到运行时

  • 越早发现错误,错误带来的损失就越小。

把控制权交给真正写代码的开发人员(也就是TSL语言的用户)

  • 用户想要把这个.tsl文件做Unit引入就引入,做主程序执行就执行。
  • 不要限制用户这个.tsl只能执行,不能作为Unit引入;或者限制用户只能做Unit引入,不能直接执行。

强调便利性

人生苦短,我用....TSL!