发明新的编程语言
如果发明一种新语言,它应该有哪些特征?
注意
本文为鸭梨瞎想,短期内(也可能永远不会)并不会实现此语言,暂时也未命名,仅仅是设想未来语言的一些特征。
1. 目的
设计一种安全、简洁、含糖量高的静态语言,保持极端的优雅和原生性能。
这种语言,简单底层又包含高级别的抽象,极端现代化,适用于下面这些人:
- 希望编程更加优雅愉快的人
- 脱离 JVM/PVM/... 虚拟机,寻找系统编程语言的人
- 编写高性能程序,而又担心程序难以维护的人
考虑到社区发展,应该选择兼容某种语言的生态(从部署的适配上或语法的可转换上)。
2. 设计哲学
我们希望追寻 Python 的设计哲学,并为此设计一门静态语言。
- 优雅:一切设计的原则都是优雅,不优雅的设计是不能容忍的。
- 静态语言:我们发现,无论如何优化 Python、JavaScript 等脚本语言,脚本语言也无法融入系统编程的生态,脚本语言无法成为静态语言,任何于此有关的尝试都割裂了相应生态并最终失败。即使是最动态的语言,也不能改变执行机器代码的事实,无论它是使用何种编译器或解释器,在哪种机器上执行的,因此,语言本质都是静态的,静态语言能活得长久。
- 有用的抽象:有一些语言设计者热衷于花里胡哨的写法,但是多数情况下程序员可能根本不会用或者想不到,因此标准库或第三方库能实现的功能尽量使用库。
- 如果不能简洁地描述此语法的使用场景,那么没必要设计,这就是浪费学习者和设计者的时间
- 如果一种语法可以被少量优雅的代码实现,那么它没必要存在
- 如果一种语法使用场景极少,那么它没必要存在
- 如果存在满足一种语法的使用场景,但有更加常规的实现,应该考虑更加常规的实现
- 表达式是一切:我们意识到,对象是 OOP 的一切,但是语句是程序的一切,一切语句都应该是表达式。因为这样更简洁,更接近于函数式编程,我们总是可以将
if
、for
视为函数,任何表达式求值就像函数求值一样优雅。 - 拥抱函数式编程:一个好的语言是多范式的,但是让语言优雅的必要条件是广泛地支持函数式编程。
- 全面支持鸭子类型:像 TypeScript 一样的类型参数和无损失的性能。
void test(int b) {
int a;
if (b > 3) {
a = b * 5 - 6;
} else {
a = b * b;
}
return a % 7;
}
我们发现 a
变量本应该是定值,因为它只会被赋值一次。但是它不能声名为常量,而且还需要犹豫它要不要被初始化。三元表达式可能能缓解这类简单问题,但是三元表达式和 if
语句没有任何区别,而且更难维护。
如果像 Kotlin 一样,这个语句就能产生常量:
fun test(b: Int): Int {
val a = if (b > 3) b * 5 - 6 else b * b
return a % 7
}
因此我们提出:
- 任何赋值语句默认都会创建常变量
- 任何对变量的修改,都需要使用
mut
关键字指定
3. 语言基本特性
- 静态语言,可使用 LLVM 作为编译器后端
- 支持类型推导、可空类型、宏指令
- 支持规则声明,由此允许混合使用不同版本的代码(
@@rules
指令) - 不使用指针,使用更安全的抽象(引用和委托)
- 支持类似于 OpenMP 的
#parallel
指令用于编译阶段转译为并行语句(需要import parallel
) - 支持原生的异步语法
async/await
,并且提供aio
作为工具库
原生类型:
类型 | 大小 (bit) |
---|---|
char | 8 |
uchar | 8 |
wchar | 16 |
int | 32 |
uint | 32 |
long | 64 |
ulong | 64 |
float | 32 |
double | 64 |
同时也提供带有数字后缀的版本:
类型 | 大小 (bit) |
---|---|
int8 | 8 |
int16 | 16 |
int32 | 32 |
int64 | 64 |
uint8 | 8 |
uint16 | 16 |
uint32 | 32 |
uint64 | 64 |
float32 | 32 |
float64 | 64 |
不区分原生类型和包装类型,原生类型可调用包装类型的方法是编译时行为。
其他常见类型:
str
array<>
list<>
hash_map<>
(也可能不加下划线)hash_set<>
ordered_map<>
ordered_set<>
命名原则:
- 内置类全部小写
- 和变量建议使用小写加下划线(更加简洁)
- 用户类、接口等使用大驼峰命名
- 常量使用大写下划线命名
我们把给变量指定类型称为类型注解,如 a: int = b + 5
,我们把 @
一个函数或者变量称为装饰器。这一点类似于 Python 但不同于 Java,@
注解将在编译时和运行时都发挥作用,减少或不使用反射。
4. 基本语法
- 类 C 语言风格(更加接近大多数人的编程方式)
- 每个语句单独一行,不使用分号,分号可让一行写多个语句
- 单行注释是
// ...
,多行注释是/* ... */
- 语句都是表达式,支持
if
、switch
等语句有返回值,没有三元表达式(类似于 Kotlin)a = if (b != 3) a + b else a + 2
- 使用
var
声明变量,默认行为是const
声明,如果赋值语句是对已经声明的变量赋值,则需要使用mut
来声明(也就是变量默认是常变量,类似于 Rust,大大减少错误,并且更加接近函数式编程)var a = 3 mut a = 5 b = 6 // 这是常变量,修改会报错
- 支持类型推导,多数情况下不需要指定类型
a = 3 b = a + 2 c = "hello world"
- 不使用指针,全面使用引用的方式(或在不安全的上下文的情况下提供指针支持,但是为了避免歧义,
*
必须紧跟类型)#begin unsafe var c: char* = null #end
- 支持宏,用于进行元编程和库的开发(增强语言的表达能力,可支持
#for
、#include
)#if Win32 || Unix // ... #endif
- 支持可空类型,空类型不能赋值到非空类型上
var a: str? = null mut a = "123"
- 支持匿名函数,使用
->
定义即可,可以赋值给一个变量(就像表达式一样),匿名函数和函数没有任何区别f = x: int -> x ** 2 + 3 * x - 5
- 大量的函数式编程支持,并且支持广泛的迭代器和生成器,支持
range
、map
、filter
、reduce
等函数// Lambda 写法,类型推导将在 Lambda 上使用,使得 x 不需要类型 a = map(x -> x ** 2, range(1, 100, 5)) // 此时 a 是一个迭代器 // 函数写法 a = (x -> x ** 2).map(range(1, 100, 5))
- 支持对象扩展和原生类型的方法扩展,不过这只是编译器行为,静态语言不会产生动态行为
int.to_mystr() -> { return format("[ {:3.6d} ]", this) } // 或者更简单 int.to_mystr() -> format("[ {:3.6d} ]", this)
- 支持装饰器,多数装饰器使用原生装饰器实现,用于编译时检查或编译时触发特定行为,少部分用于运行时行为
- 【试验】支持
...
运算符,用于原地解包def f(x: ...int) { print(...x) } a = [1, 2, 3] // a 是 list<int> 类型 b = arrayof(1, 2, 3) // b 是 array<int> 类型 f(...a) f(...b)
- 【试验】支持列表推导(类似于 Python)
b = [x ** 2 for x in range(1000)]
5. 语法详解和示例
if
语句:
if (a == 3) {
// ...
} else if (b == 5) {
// ...
} else {
// ...
}
for
语句的循环变量不需要使用 var
定义,并且不能被手动改变,仅仅在作用域内有效:
// 类似于 for (int i = 1; i < 100; ++i)
for (x in 1 .. 100) {
// ...
}
// 类似于 for (int i = 1; i < 100; i += 3)
for (x in 1 .. 100 .. 3) {
// ...
}
while
语句和 do ... while
语句也受到支持,类似于 C 语言。
switch
语句也受到支持,但是不需要写 break
也不需要 case
,含糖量极高:
// x: int
switch (x) {
1 -> {
}
2, 3 -> {
}
}
使用 def
定义一个函数:
def f(x: int) {
// print 是一个宏,默认情况能打印任何类型,因为对象默认继承了 to_str() 方法
print(x)
}
使用 class
定义一个类:
class A {
var _a: int?
var lateinit b
var _c: int = 3
init (x: int) {
mut b = x
}
}
init
用于创建初始化构造器,下划线开头的成员变量默认为私有的,除此之外默认为受保护的。
使用 interface
定义一个接口,不提供抽象类,接口和抽象类功能合并。
多态行为使用虚表实现,编译时扩展虚表行为使得语言更加动态(例如用于支持对象的混入和扩展)。
io
提供同步的 I/O 支持,异步工具 aio
提供异步版本的 I/O 支持(考虑到编译体积,如果不导入就不会被编译,同时提供静态库和动态库支持。如果不使用 libuv
来实现也是可能的,提供接口来允许第三方库使用,这种情况下就不需要 import aio
了)。
import aio
async def read_file() {
async with (
aio.open("f1.png", aio.R | aio.B) as f1,
aio.open("f2.txt", aio.R) as f2,
) {
// 同步读取
r1 = await f1.read(1024)
r2 = await f2.read(1024)
// 异步读取
r3, r4 = await aio.all(r1.read(1024), f2.read(1024))
}
}
aio.run(read_file())