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