登录 用户中心() [退出] 后台管理 注册
   
您的位置: 首页 >> CLQ工作室开源代码 >> 主题: TypedLua 入坑指南[zt]     [回主站]     [分站链接]
标题
TypedLua 入坑指南[zt]
clq
浏览(434) + 2023-01-10 09:10:35 发表 编辑

关键字:

TypedLua 入坑指南[zt]

https://zhuanlan.zhihu.com/p/40300705

首发于
Within Lua & C++
TypedLua 入坑指南
PaintDream
PaintDream
望山出海月,听梦入潮声。

更新:

TypedLua的新版本teal基本已经文中绝大多数坑,编译速度也大大提升,推荐直接使用teal:
https://github.com/teal-language/tl​
github.com/teal-language/tl


引子

脚本语言常常被人所诟病“开发一时爽,重构火葬场”,这通常和它们的动态类型系统有很大的关系。虽然动态类型本不是脚本语言的必需品,但动态类型大大降低了一次开发的上手难度,提升了原型开发的速度,主流的脚本语言中大部分都会支持它。

不论是metatable构造原型链的Lua,还是自带class的ECMAScript和Python,都不能从代码运行前发现所有潜在的类型安全问题。a + b这条语句会不会挂掉需要等到真正执行到这一行的时候才知道 ,这给测试和维护带来了不少的麻烦。现代IDE可以尽可能地帮助程序员在开发程序时避免一些明显的错误,但是对于这样的检查也没有什么好的办法——因为动态类型使得IDE没有办法区分一个地方是写得确实有风险,还是程序员本意如此。

因此,像TypeScript这样的在动态类型上加上静态类型检查的语言就显得尤为重要。在开发大型项目或者维护已有项目时,在代码功能上做大规模重构或者是扩展是常有的事。对于C++/C/C#/Java这样的语言,编译器会在语义层面上保证修改后代码的类型安全。如果程序员善于使用static_assert的话,可以进一步强化这种约束。

举个例子,如果我想安全地删去一个结构中的某个我认为不再会用到的成员,如果用静态语言的话,直接删掉然后编译一次(带智能提示的IDE甚至直接能在你删完后就提示)就能看到哪些地方还在依赖这个成员,然后一一改正。如果我想改变某个整形类型的长度(比如说uint32改uint16,那么static_assert在typealiasing地方做长度检查也可以让你及时地发现问题。

TypedLua是一个与TypeScript类似的语言扩展,由andremm设计并开发(andremm/typedlua),采用MIT协议授权。TypedLua也采用了将代码(TypedLua)编译到目标语言(Lua)的方式。甚至在语法上也很类似:

local var : number = 3.1
var = "this is a string" -- error: unmatched basic type
local point : { "x" : number, "y" : number } = { x = 1, y = 2 } -- OK
point.z = 3 -- error: unable to add member to a fixed type

不管各位看官怎么想,我看到这样的代码会比较安心——因为point和var在定义的那一刻就绑定了它的语义:var必须是一个数字,point必须是一个二维平面上的点。如果之后的代码中不小心犯了错误,typedlua编译时会提示我。


写在入坑前的话

与TypeScript不同,TypedLua只是andremm的个人学术的原型产品,没有微软爸爸的强推,也没有大规模应用的实例产品(也许用了但没说?),文档资料也比较缺(这也是为什么要写此文的目的之一)。唯一权威的官方文档是作者本人在DLS上发表的论文、学位论文和一些slices,其中学位论文和slices可以在其github项目(andremm/typedlua)中clone得到。由于TypedLua前后语法有变动,所以本文以最新的代码配合DLS15的论文《Scriptable operating systems with Lua》的部分延续内容以及学位论文的最终描述来介绍。

TypedLua在有些地方的设计对于使用者来说不够自然,因此本文在介绍其基本语法之后还会尝试去改造一点点语法。改造后的部分不能直接用原版的typedlua运行,这一点请切记。我改造后的typedlua项目在这里(paintdream/typedlua)。

TypedLua支持Lua 5.1 - Lua5.3,最新的Lua 5.4马上即将推出,语法和Lua 5.3差异不大。如果读者想使用Lua 5.4的话,可以按文末给出的办法添加Lua 5.4的支持。本文的内容基于Lua 5.3。


基本类型

我们可以使用冒号来补充变量的类型约束:

local t : string = "Hello, world"

定义后t将不能再绑定到其它类型的变量上,nil也不可以。因此如果我们需要一个可为空的类型为string的变量t,需要在声明类型后面加个问号:

local t : string? = nil
t = "Hello, world"

请谨慎使用这样的类型,除非你确实需要一个可空的值。因为在t可能为空的情况下,你在使用它之前必须进行检查。否则TypedLua会拒绝编译:

local t : string? = FunctionThatReturnsStringOrNil()
if t then
local s : string = t
end

检查的方式很简单,你必须用if去判断一下是否为空,才能正确地将t绑定在string类型的变量s上。这会增加函数一点点的执行成本。

同样地,你也可以定义类型为boolean、number或者integer(仅能用于Lua 5.3+的版本)的变量:

local b : boolean = true
local n : number = 13.2
local d : integer = 4

你可以在函数中使用相同的语法来约定参数的类型:

local function add(a : number, b : number)
return a + b
end

这样声明的函数仅支持传入两个number值。typedlua会根据return表达式来计算出结果的类型number。当然你也可以用“:”来显式地声明它:

local function add(a : number, b : number) : number
return a + b
end

同样地,如果接收返回值的变量没有特别声明的话,会继承返回值的类型,同时,当不显式声明类型时,TypedLua也会自动绑定其初始化时的类型:

local c = add(1, 3) -- c is number
local s : string = c -- error
local d = c -- ok, now d is a number

这样就方便很多了,我们只需要在确定的时机明确变量的类型就可以了,能推导的就让TypedLua自动推导好了。为了表明语法细节,本文后很多示例代码没有使用自动类型推导,读者在实际应用的时候不需要那么麻烦了(毕竟写类型名也很费劲)。

local b : boolean = true -- same as local b = true
local n : number = 13.2 -- same as local n = 13.2
local d : integer = 4 -- same as local d = 4

在Lua中,函数(或者说闭包)是第一类值,可以作为普通的变量使用。函数本身的类型由其参数的类型和返回值的类型决定。比如上面的add函数,语法上这样写:

local padd : (number, number) -> (number) = add

注意参数和返回值类型都需要用括号包起来。

可空类型

如果一个变量可以为nil,那么需要类型声明时添加一个问号。当需要它的非nil值时,必须使用if进行判断,然后再使用:

local a : number? = 1
a = nil
if a then
local b : number = a -- OK!
end
local c : number : a -- Error!

复合类型

Lua中是没有类这个概念的,如果想要创建结构化的数据就需要用到表。同时,创建数组也需要表。在TypeLua中它们的类型声明如下:

local point : { "x" : number, "y" : number } = { x = 1, ["y"] = 2 }
local arrayOfNubmers : { number } = { 1, 3, 5, 7, 9 }

不建议在表中混用数组和结构化数据,这样会使得语义变得混乱。

除此之外,你还可以创建纯字典型的表:

local map : { string: number } = { "abc" = 1, "def" = 2 }

这三种类型的表之间是不可以互相转化的,因为它们的语义本就不同。

为了兼顾常见的写法,在声明结构化数据时,TypedLua允许增量式地为变量增加成员:


local t = {} -- now t is of open type {}
t.value = 1 -- now t is of open type { "value" : number }
t.str = "abc" -- now t is of open type { "value" : number, "str" : string }
local d = SomeOtherOperations(...) -- now t is of closed type { "value" : number, "str" : string }
t.newvalue = 2 -- error! cannot add field to closed type!

在t声明的时候,默认的类型是{},同时,它是一个开放(open)变量,意味着我们可以在接下来的语句中为它添加成员,每添加一个成员,t的类型都会改变。如注释中所示。

但是,一但在为添加t成员的语句中夹杂了其他的语句,TypedLua就会认为你对t的成员已经添加完成,t的状态就会变成closed,不能再添加成员。

关于结构化数据,还有一点需要特别说明:结构化数据的类型只表明变量所确保拥有的成员及其类型,而不限制它有其他的成员,举个例子:

local p : { "x" : number, "y" : number } = { x = 1, y = 2, z = 3 }

这个是合法的,只是这样绑定的p不能访问z成员而已。


any

TypedLua中可以使用any类型,用来表示任意类型的值,TypedLua将不会在any类型上作检查,程序员必须自行处理运行时可能到来的各种类型。

local a : any = 123
a = "456"


不定长参数

在函数声明中,可以使用*语法来表示不定长参数:

local func : (string, number*) -> (number) = function (s : string, ...) return 1 end

如果要表示任意类型的不定长参数,就可以使用any*。


多选类型

在Lua的设计中经常能看到复用现有的函数,根据参数类型的不同来选择不同的子功能,比如说Lua 5.3中的string.gsub这个函数:

gsub : (string, string, string|{string:string}|(string*) -> (string), integer?) -> (string, integer)

string.gsub用于返回原字符串模式替换后的结果。它的第三个参数表示目标pattern,它可以是string,也可以是string的替换映射表,还可以是一个自定义的函数。

因此TypedLua提供了多选类型,可以允许一个变量的类型在几种类型中选择,同时为了保证它在使用时的正确性,我们在访问它的值之前需要判断它的类型。如:

local a : number | string = 123
a = "123"
if type(a) == "number" then
local c : number = a
print("Number!")
else
local s : string = a
print("String!")
end

如果不用type+if判断的话,TypedLua会提示类型不匹配。


类型别名

我们刚刚的例子都是在变量声明的时候绑定它的类型,但是如果我要声明多个一样的结构化数据,那岂不是要写多份?

local point1 : { "x" : number, "y" : number } = { x = 1, y = 2 }
local point2 : { "x" : number, "y" : number } = { x = 3, y = 4 }

这自然是不需要那么麻烦的,我们可以使用类型别名typealias或者接口interface来解决这个问题:

-- deprecated syntax
interface Point
x : number
y : number
end

-- recommended syntax
typealias NewPoint = {
"x" : number,
"y" : number
}

local p : Point = { x = 1, y = 2 }
local m : Point = { x = 3, y = 4 }
local q : NewPoint = p
local d : { "x" : number, "y" : number } = p

interface是早期的TypedLua所使用的语法,typealias是后来版本实现的。所以在最初的论文中是没有typealias的影子的。两者功能上接近,推荐使用typealias,因为它的语法更贴近嵌入在声明语句中的类型写法。

注意,只要两个类型事实上是一样的,即使用了不同的别名名称,也是可以相互赋值的,如上面的d变量可以直接用p赋值。


Metatable

在lua中,我们经常使用metatable来模拟原型的继承关系。通过metatable的__index域,我们可以把对不存在域的访问请求转发到metatable上。这在TypedLua中也是支持的:

local meta = { value = 4 }
local p = setmetatable({}, { __index = meta })
print(p.value)

注意TypedLua只支持setmetatable(..., { __index = meta })这种把metatable构造在表达式内的写法。

标准库

TypedLua为标准库中的函数提供了原型声明(但似乎漏了几个,可以自己补上),这些原型声明在.tld文件里,举个例子,这个是table.tld中定义的函数原型:

--[[
Typed Lua description file for table manipulation
]]

concat : ({string}|{number}|{integer}|{string|number}|{string|integer}, string?, integer?, integer?) -> (string)
insert : ({any}, any, any) -> ()
move : ({any}, integer, integer, integer, {any}?) -> ({any})
pack : (any*) -> ({"n":number, number:any})
remove : ({any}, integer?) -> (any)
sort : ({any}, nil|(any, any) -> (boolean)) -> ()
unpack : ({any}, integer?, integer?) -> (any*)


Bug修复

标准版本的TypedLua有个bug,那就是嵌套匿名函数内不能访问外部函数的self变量(会提示变量未定义)。为此我提了一个PR到TypedLua('self' derivation in nested anonymous function declaration. by paintdream · Pull Request #114 · andremm/typedlua),不过由于作者要在TypedLua上开发新的大招Titan-Compiler,因此他直接把这个修改合titan上去了。导致现在TypedLua项目里这个bug还是存在的。如果读者不想用我修改的typedlua的话,可以打开typedlua/tlchecker.lua,定位到这里:

local function check_function (env, exp, tself)
local oself = env.self
env.self = tself -- env.self = tself or oself
local idlist, ret_type, block = exp[1], replace_names(env, exp[2], exp.pos), exp[3]
local infer_return = false
-- ...
end

按注释里的写法改成 env.self = tself or oself即可。


改造

Lua 5.4已经发布了work版本,为了让TypedLua支持Lua 5.4,可以简单地在loader.lua和tlchecker.lua中出现Lua 5.3的地方添加Lua 5.4即可:

loader.lua:

if _VERSION == "Lua 5.3" or _VERSION == "Lua 5.4" then opts.INTEGER = true end

tlchecker.lua:

local function load_lua_env (env)
local path = "typedlua/"
local l = {}
if _VERSION == "Lua 5.1" then
path = path .. "lsl51/"
l = { "coroutine", "package", "string", "table", "math", "io", "os", "debug" }
elseif _VERSION == "Lua 5.2" then
path = path .. "lsl52/"
l = { "coroutine", "package", "string", "table", "math", "bit32", "io", "os", "debug" }
elseif _VERSION == "Lua 5.3" or _VERSION == "Lua 5.4" then -- here!
path = path .. "lsl53/"
l = { "coroutine", "package", "string", "utf8", "table", "math", "io", "os", "debug" }
else


如果读者已经开始使用TypedLua开发demo的话,不难发现现有的TypedLua有一些比较麻烦的限制。举个例子,如果我定义一个新模块,按Lua 5.3的标准用法,新建一个TestModule.tl,然后编写以下内容:

local TestModule = { } -- to make it simpler, we do not add { __index = _ENV } and local _ENV = TestModule trick to make sandbox
function TestModule:foo() end
function TestModule:bar() end
return TestModule

当我们require这个模块时,可以顺利拿到模块的返回值:

local TestModule = require("TestModule")
TestModule:foo()

这都没问题,TestModule的类型是{ "foo" : (self) -> (), "bar" : (self) -> () } 但是这个类型信息是隐含的,如果我们要定义一个函数,其中一个变量就要求是TestModule的类型,应该怎么办呢?这样写吗?

typealias TestModuleType = {
"foo" : () -> (),
"bar" : () -> ()
}
local function Use(instance : TestModuleType)
instance:foo()
end
Use(TestModule)

那万一我在TestModule里加了个函数,这里也要把TestModuleType再改一遍吗?

是的。

那么怎么样才能避免这个麻烦呢?我在我的TypedLua版本中稍改造了一点,使得确定类型的变量本身也可以作为类型说明使用,上面的写法只需要这么改就行了:

local function Use(instance : TestModule)
instance:foo()
end

虽然TestModule是个变量,不是类型,但是我改造后的TypedLua在匹配类型失败时,会尝试查找有没有同名的变量,如果有,就把这个变量的类型拿来用。这样的话会方便很多,也真正地让TypedLua在我的个人实践中变得可用。


在接下来的章节中,我会简单介绍这个改造的编码思路,讨论TypedLua的一些实现细节,并且就TypedLua整合到宿主程序提供一些实践参考。

编辑于 2020-11-22 19:34
Lua
静态语言
TypeScript
评论千万条,友善第一条

21 条评论
默认
最新
hhhhhhhhh
hhhhhhhhh
typedlua 不是已经没了么?变成 titan 了
2018-07-22
hhhhhhhhh
hhhhhhhhh
PaintDream
可是 typedlua 已经没人管了
2018-07-22
PaintDream
PaintDream
作者
那个还在开发中。。。
2018-07-22
天猫Skycat
天猫Skycat
近来是动态类型加强类型是潮流呐
2018-07-22
天猫农场
天猫农场

github.com/TiancJester/
2019-11-06
YivanLee
YivanLee

大大大大佬!
2018-07-22
PaintDream
PaintDream
作者
YivanLee

感觉能学不少东西
2018-07-22
YivanLee
YivanLee
PaintDream

9,12,7 感受一下
2018-07-22
乔治的恐龙
乔治的恐龙
转译后可以混淆吗?可以加密吗?
2021-10-10
薛涵
薛涵

没有VSCode 插件啊
2020-04-01
班德尔城门口
班德尔城门口
官方有插件 but 没有 自动补全
2020-11-25
endless
endless

和Terra比呢
2019-05-20
熊起
熊起

有没有办法获得指定后的类型?

比如 typelua.getargstype(padd) 返回 {number,number}
2019-04-19
熊起
熊起
PaintDream

能hook就够了,需要的差不多是这个效果:

编译的时候根据类型信息,把局部声明的函数注册上;调用函数的时候,根据参数数量和类型,查找到注册的函数并调用。
2019-04-19
PaintDream
PaintDream
作者
熊起

但是应该不是typelua.gettype(padd)这种用法,得在编译的时候做hook
2019-04-19
leinlin
leinlin

没有强大的编辑器或者编译阶段进行查错提示,走不远的。
2018-07-23
评论千万条,友善第一条

文章被以下专栏收录

Within Lua & C++







总数:0 页次:1/0 首页 尾页  
总数:0 页次:1/0 首页 尾页  


所在合集/目录
开发语言转换器 更多



发表评论:
文本/html模式切换 插入图片 文本/html模式切换


附件:



NEWBT官方QQ群1: 276678893
可求档连环画,漫画;询问文本处理大师等软件使用技巧;求档softhub软件下载及使用技巧.
但不可"开车",严禁国家敏感话题,不可求档涉及版权的文档软件.
验证问题说明申请入群原因即可.

Copyright © 2005-2020 clq, All Rights Reserved
版权所有
桂ICP备15002303号-1