Skip to content

proposal: 联合类型 union types #12

@weiwenhao

Description

@weiwenhao

nullable

这是 golang 中一个示例

var foo *int // 可以将值 decode 到指针类型,因为指针类型可以表达出 nil 的含义  
foo = nil  
println(foo)  
  
var a := []int{1, 2, 3}  
a = nil  
println(a)

bar := a[0]

在 golang 中 null 可以作为一个值赋值给任意的复合类型,所以最后一行 bar := a[0] 会产生一个运行时 panic,在编译时并不能很容易的检测出这种错误。在 golang 中一个复合类型的值是不是 null 只有我们的用户自己知道,当我们明确知道一个复合类型不为 null 时,我们可以放心的编写代码。

在实际编码中,我们总是会和弱类型的语言如 mysql/json 打交道,比如使用 mysql 存储 nat 数据时,如果 nat 还没有探测出来,我们应该如何创建一条记录存储 nat 数据呢?

  • 0 🤔 No,nat=0 是 nat 允许的值,所以没有测出来不能使用 nat=0 表示
  • -1 🤔 No,nat 总是一个大于等于 0 的值,我们不能为了存储一个数据而将代码中所有的 u8 类型改成 i8 类型
  • null 😄 Yes, null 非常好的表达了值还不存在的情况。

我依旧拿我比较熟悉的 golang 进行举例,看看如何在 golang 中如何存储 nat 数据

var nat *int8 // 可以将值 decode 到指针类型,因为指针类型可以表达出 nil 的含义

// logic...
if nat == nil {
	// nil handle
}

// if foo == nil
// panic: runtime error: invalid memory address or nil pointer dereference
foo := *nat + 1 

当一个值允许为 null 时,golang 中通常使用指针加类型存储这个数据,因为指针包含了 nil 的含义。*nat 只要只要足够谨慎就完全没有问题,当你确定一个 *nat 类型的数据一定不为 null 时,你可以放心的使用 *nat 这样的操作,而不需要担心空指针引用的问题。所以其实 golang 给了用户最大的自由,让我们能够编写出足够简洁的代码。

但越来越多的强类型语言已经将 null 值作为一个特殊的值进行处理,即不允许将 null 赋值给除了 null 以为的其他类型。这样虽然增加了代码编写的复杂度(需要键入更多的字符),但是可以很大程度上避免基于 null 引用而产生的运行时错误,我想这应该是值得的。

语法支持

那 nature 中应该怎么做呢?虽然在上一个版本中 [int] list = null 是被允许的和 golang 一样。

但是我认为 *int8 不是一种合适的用来表达不存在的含义,我们应该使用更加明确的方式来表达某个值允许为 null,而不是使用 *int8 去模拟这种情况。

恰好 nature 目前还没有指针,那不妨学习一下 TS 中的实现,使用 union types 来表达一个值允许为 null。

Union types(联合类型)是一种类型系统中的概念,它允许一个值具有多个可能的类型。在许多编程语言中,包括 TypeScript 和 Python 的类型提示中,都支持联合类型。所以 union types 中虽然有多个类型,但是只有一个值。与之相对的是 Product type

nature 中使用 union type 表达 nullable,在 nature 中 null 的类型和值都适用关键字 'null' 表示

i8|null nat

// logic...

if (nat == null) {  // 这是值比较,后续将会有 nat is T 这样的类型判断语法
	// .. handle null
}

// 在明确知道不 nat 不为 null 的情况下可以使用类型断言语法 as 将 nat 的类型断言为 i8, 并作为 i8 类型使用
// 如果 nat 此时不是 int 类型,则会在运行时产生一个 panic
foo := (nat as i8) + 1

// 如果后续会频繁的使用,当然也可以这样赋值给一个变量进行使用
var n = nat as i8

union types 将作为一种标准的语法进行支持,any 其实就是一种 union 了所有类型的 union type。类似的

type numbers = int|float|uint

也将是被允许的,但是有什么用还是未知数,毕竟目前 nature 没有基于类型类型的扩展函数的语法。对于复合类型,上一个版本其还有默认值 null,下一个版本将不会再支持啦 🙇

[i8] list = null // x, null 不能赋值给 [i8] 类型
[i8] list // x, 这相当于 [i8] list = null

[i8]|null list // v, 此时 list 的值为 null
list = null // v, 允许的

string str // x,同上,这是不被允许的
string str = "" // v 这也是被允许的
string|null str // v 这是允许的

var s = str as string // v,当你明确 str 不包含 null 时,可以使用 as 语法进行断言

❗️as 同时也用于强制类型转换语法

有了 union types 就必须考虑其中的赋值操作。在没有想明白之前我们总是按照最严格的模式进行限制,严格限制意味着开放操作后总是可以兼容当前的操作。

string|null a
null|string b

a = b // x union type 不同,不允许进行相互赋值


string|null|bool a
string|null b
a = b // x
b = a // x


string|null a
string|null b
a = b // v
b = a // v

语法简化

ts 属于运行时的动态语言,所以其包含一个求值环境模型来追踪变量当前的实际类型,所以类似这样的语法是可以做到的

let foo: number|string = "hello"

console.log(foo.length); // 5

foo = 24

console.log(foo.length) // Property 'length' does not exist on type 'number'.

但是在编译形语言中,基本无法在编译时确定某一个阶段变量的值时多少,除非 foo 是一个不可变量。

int|string foo = "hello"

if (...) {
	foo = "int"
} else {
	foo = 24
}

// Is foo an int or a string?

所以我们必须能够检测出 foo 此时是什么类型才能进行具体的操作,所以这里首次引入类型判断的语法,大多数语言中使用 typeof(foo) == int 这样的判断,但是我希望能够重用类似 as 表达式基于类型的操作。我们使用 is 表达式来进行判断。

语法 bool b = foo is int ,判断 foo 的类型是否为 int 并返回一个 bool 类型的值。

int|string foo = "hello"

if (...) {
	foo = "int"
} else {
	foo = 24
}

if (foo is int && ... logic) {
	// x 虽然你已经知道了 foo 此时就是 int 类型,但是编译器此时并不知道。
	int bar = foo + 1 // x
	int bar = (foo as int) + 1 // v
	return
}

// 接下来又是一个语法糖
// 如果你已经明确知道了 foo 是 string 类型,并且后续需要频繁的操作 foo,并且不希望重新声明一个变量的名字,毕竟起名是一件很困难的事情
// 那么你可以明确的告诉编译器,后续请把 foo 当成 string 类型处理
// 其本质上等于 var foo = foo as int,但是如果你真的这么做,你会得到编译时的变量重复定义的错误
// let vs assert vs local 当然这是一个提案语法,我暂时优先选择最简短的 let
let foo as string 

string bar = foo + "bar" // v, foo 此时明确为 bar 类型

foo = null // x, 此时 foo 具有明确的 string 类型,所以不可以再将 null 值赋值给 foo 变量。

语法 let foo as string 让 foo 在当前作用域中具备明确的 string 类型。就像注释说明的一样,其本质上就是 var foo = foo as string

但是需要注意的是

type foot = struct {
	int|null bar
}

var foo = foot {
	bar = 12
}

// 不能通过这种语法来让 foo.bar 作为 int 类型
// var foo.bar = foo.bar as int 是一种不合法的语法声明方式
let foo.bar as int // x
var bar = foo.bar as int // v

nullable 简化

T|null 作为 union types 最常用的一种情况,所以通常会给予一定的语法糖进行语法简化

string|null a 可以改写成 string? a 表示 a 允许为空。很多语言都选择了这么做。

💡 因为泛形语法还在思考中,基于泛形语法也许可以进行如 type nullable<T> = T|null 类似这样的简化。所以 T? 的方式是否需要支持还需要进一步确定。至少会延期到泛形语法开发时才会确定的进行支持

error handle

使用 union type 我们同样可以进行 error 处理。nature 中函数中总是包含一个返回值,且返回值的类型是确定的,但是由于 throw 语法的存在,所以函数并不总是返回确定的类型,其可能会返回一个 errort 类型,所以现在,对于任意一次函数调用,我们可能会得到类似于 type result<T> = T|errort 这样的返回值。

但是我们应该时时刻刻的在每一次 call 时关心错误么?我觉得不需要,我们应该只关心我们能够处理的错误,对于不能处理或者预料之外的错误,我们没有必要去拦截或者处理它,应该将它继续向上传递,直到遇到一个能够处理这种错误的上级。

所以 nature 将选择一种和 golang 截然不同的更加传统的 try catch 的解决错误的方式。

fn call():int {
	// logic...call->call1->call2->call3...
	return 1
}

// call 的调用链可能非常的深,并存在了一个异常,比如有一个虫子钻进了内存中导致的内存访问异常
// 但是我只是一个小小的 caller,我能做的就是读取 call 中的数据,我无法处理类似虫子钻进了内存中导致的错误,所以只有当 call 能够返回时我才继续向下执行,否则我将不做任何的处理。
// 错误将沿着调用链向上级传递,直到遇到了一个能够处理这个错误的 caller
var foo = call()

作为一个 caller 当我能够处理可能的错误时,我将进行可能的错误的拦截,但并不是每一次调用都会产生错误,所以我需要进行适当的判断


// 通过 catch 我们可以得到一个 union type, int|errort foo
// foo 可能是其中一种类型,所以可能会有如下的写法
var foo = catch call()

if (foo is errort) {
	let foo as errort
	// log and abort to user
	log(foo.msg)
	abort(foo.msg)
	return
}

let foo as int
// normal  handle ....

但是遇到错误时,我们可能需要这么判断,那能不能进一步进行语法书写上的优化呢?

既然 union type 同一时间总是表示一种类型,在编译形语言中我们又不能向 TS 一样做类型跟踪。那不妨牺牲一点空间,将 union type 转换为 product type,从而可以减少类型断言带来的编码负担。

Product type(积类型)是指将多个类型的值组合在一起形成一个新的类型。它由多个成员组成,每个成员都有其自己的类型。可以同时获取这些成员的值。常见的 Product type 的例子是 struct 或 tuple,其中可以同时包含多个具有不同类型的字段。

var result = catch call()
var (foo, err) = 💥 result

if (err) {
	log(err.msg)
	abort(err.msg)
	return
}
var bar = foo + 1 

今天引入的语法有点多了,所以 💥 语法将暂时不会被集成到 nature 中。💥 将被集成在 catch 中。如下所示

var (foo, err) = catch call()

if (err) {
	log(err.msg)
	abort(err.msg)
	return
}
var bar = foo + 1 

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions