nature
nature copied to clipboard
proposal: 泛型 generic
nature 的首个泛型版本只会支持简单的功能与严格的限制,这样才能在未来有更多的可能,而没有太多的历史负担。💪
泛型类型
其实泛型类型称为自定义类型参数更加的准确,在之前的语法中,我们已经有 type T = ...
这是和变量定义非常相似的一种自定义类型(类型别名)的语句,其原型参考自 var v = ...
。 所以实现泛类型非常的简单,只需要进一步模仿 fn f() = ...
对 type T
声明进行优化即可,语法如下
type box<t> = struct {
t width
t length
}
type case<t> = (t, t, string)
type nullable<t> = t|null
type errorable<t> = t|errort
虽然类似自函数调用,但是自定义类型参数部分选择了尖括号作为参数,一方面是和函数定义能够更好的区分,另外则是大多数编程语言都使用了尖括号作为泛型参数。
在使用上则和函数调用一样
var b = box<i8> {
width = 13,
length = 26
}
case<u8> c = (1, 1, "hello world")
nullable<i8> foo = null
foo = 12
类型参数不支持运输条件的填写。
泛型函数
我们先来看看 golang 和 rust 中的一个简单的泛型的使用示例
golang
package main
import "fmt"
type Case[T int | uint | float32] struct {
Width T
Length T
}
// 泛型函数定义
func area[T int | uint | float32](c Case[T]) T {
return c.Width * c.Length
}
func main() {
fcase := Case[float32]{
Width: 1.15,
Length: 2.15,
}
icase := Case[int]{
Width: 10,
Length: 20,
}
fmt.Printf("%f\n", area(fcase))
fmt.Printf("%d\n", area(icase))
}
rust
struct Case<T> {
width: T,
length: T,
}
fn area<T: std::ops::Mul<Output = T> + Copy>(case: &Case<T>) -> T {
case.width * case.length
}
fn main() {
let fcase = Case {
width: 1.15f32,
length: 2.15f32,
};
// 在参数足够的情况下, rust 可以自主推断出类型。
let icase: Case<i16> = Case {
width: 10,
length: 20,
};
println!("{:?}", area(&fcase));
println!("{:?}", area(&icase));
}
泛型的设计其实基本上已经有成熟的方式,大家也都能接受这种方式,所以 nature 也将沿用前辈们的设计方案。
不过考虑到函数声明是非常频繁的操作,所以我不希望在函数声明中进一步增加语法,如泛型参数语法 <T, E>
,所以我选择简单地将泛型函数中的类型参数与约束进行提取并扩展到 type T
语法中。
// 定义了一个泛型类型 numbert, 并限定了其约束
// 💡 右值的声明虽然意思对了,但是 generic 这个单词太过复杂了,希望能够进一步精简
type numbert = generic i8|i16|i32|i64|u8|u16|u32|u64|int|uint|float|f32|f64
// numbert 是一个泛型类型参数,原则上其可以不受调用的影响而直接通过泛型约束生成所有类型的函数
fn sum(numbert a, numbert b):numbert {
return a + b
}
// 也就是编译器将会去将上面的泛型函数泛化成下面的函数(并借助重载功能定位具体的函数)
// 重载功能将会在编译器内部被支持,但是暂时不会用户端开放
// fn sum(i8 a, i8 b): i8 {}
// fn sum(i16 a, i16 b): i16 {}
由于没有明确的使用 fn sum<numbert>(numbert a, numbert b):numbert
的方式去声明泛型函数,所以所有的类型变量都需要在函数的输入参数中明确声明,当收个类型变量,如 numbert 的值被推断出来后,后续在函数定义域内的所有的 numbert 类型变量都将沿用该值不可该变。
那能不能编写如 type t0 = generic
, 此时 t0 不受到任何的约束? 🙅♂️ 这是不允许的。
类型约束有必要吗? 有,类型约束让我们知道这是一个强类型且具有约束的函数调用,而不是 caller 传入什么类型,callee 就泛化成什么类型。泛形函数本质上依旧是协助我们减少代码量编写的一种方式,让我不需要编写如 sumi8(),sumi16(),sumi32() ... 这样重复的工作。并且我们不能将类型当做一种可以传入的参数,如 fn box(t v)
,这会使得类型约束变得可有可无。
而泛型类型,如 nullable<t>
总是会传入一个确定的类型,所以不违反强类型的原则。并且类似 nullable 这种声明,t 可以是任意值,而不需要受到任何的约束,因为类型本来就是用来约束值的,所以没有必要重复的约束。相反的,类似 fn box(t v)
中的 t 部分将不再是接收调用者传递的参数来赋值,这将没有任何的意义,其总是接受一个明确的泛型类型以及相应的约束。
再多看几个泛型函数的示例
还在提案阶段没有经过实际的编码,所以还不确定复合类型变量在技术上能不能实现
type numbert = generic i8|i16|i32|i64|u8|u16|u32|u64|int|uint|float|f32|f64
// 基本使用
fn sum(numbert a, numbert b):numbert {
return a + b
}
fn cmp(numbert a, numbert b):bool {
return a > b
}
// 这是复合类型的情况下使用类型变量,其实和泛型类型具有相同的含义 type numbers<t> = [t]
fn list_sum([numbert] list):numbert {
numbert sum = 0 // 使用类型变量 number
for k,v in list {
sum += v
}
return sum
}
// 当然,根据实际的情况需要,你可以优化类型参数,比如这是一个通用的求数组的长度,由于不需要关心 list_t 的具体类型,所以我们不需要像上面一样声明一个 [numbert] 的变量情况。此时 list_t 就是一个泛型类型变量。
type numberst = generic [i8]|[i16]|[i32]|[i64]...
fn list_len(numberst list):int {
int sum = 0
for k in list {
sum += 1
}
return sum
}
// 一开始提过的 box 面积的例子,同样可以支持,这是一个通用的求盒子的面积
// 这里的 t 其实也是一个类型变量,和函数中的类型变量没有太大的区别。
type box<t> = struct {
t width
t length
}
// 所以我们可以有 box<int<[int]>> 这样的复杂的解析模式。都将会进行支持,并将会在编译时得到一个确定的类型
// 从这一版的定义来看,numbert 此时是一个类型变量,等着调用方赋值
fn area(box<numbert> b):numbert {
return b.width * b.length
}
var b = box<i8> {
width: 5,
length: 10
}
// 类型变量 numbert 被赋值为 i8,所以此时值 a 的类型为 i8
var a = area(b)
👋👋👋
type numbert = generic i8|i16|i32|i64|u8|u16|u32|u64|int|uint|float|f32|f64
中的 generic 关键字简化成 gen
gen 既可以理解成 generic 也可以理解成 generate
示例
type numbert = gen i8|i16|i32|i64|u8|u16|u32|u64|int|uint|float|f32|f64
#17 merge