Rhyme——在程序语言设计上的一次尝试

Rhyme——在程序语言设计上的一次尝试

Rhyme 是我设想中的一门新语言,它现在还没有一个可用的编译器实现,也仍有许多内容处于设计阶段。Rhyme 的初步设想是编译成 JavaScript 实现,这可能被称为 RhymeScript,这也许会在未来几年被实现。我试图将一些我认为很有趣的新想法加入到这一实验性语言中。当然,目前这一想法仅停留于自娱自乐阶段,很多东西非常不严谨,所以也不用太过严肃。

我这里不会详细描述 Rhyme 的每一个部分,只是简单提出几点我个人认为很有趣(当然不一定实用的设计),写在这里自娱自乐一下。

示例

为了直观起见,先贴一点示例(自然是跑不起来的,因为目前这门语言只存在于想象中):

核心设计思路

  • Rhyme 是静态强类型、渐进类型与结构化类型的,这和 TypeScript 很相似
  • Rhyme 试图尽可能使得编程像是英文写作,并提供强大的元编程能力最大程度地实现“自然语言编程”,这使得 DSL 的设计变得非常简单,这和 Ruby 是类似的;同理,为了尽可能使得代码易读,Rhyme 也会使用andor这些关键字而非&&||
  • Rhyme 试图尽可能减少程序员的打字负担。因此 Rhyme 会较常使用简写,例如function被简写成fnString被简写成StrInteger被简写成Int
  • 在 Rhyme 中空格的地位被极端强调,所有的二元运算符两边必须加上空格,因此1+1会报错,1+ 1也会报错,只有1 + 1不会。同理,赋值运算符=、乘方运算符^等使用时两边也必须加上空格。除此之外,一元运算符中除了负号必须不带空格使用(例如- 1会报错,而-1不会),其他所有一元运算符必须带上空格使用
  • 承接上一条,Rhyme 对空格的重视不仅限于运算符。例如函数定义时fn关键字与后面的括号之间必须带空格,比如fn() {}会报错,而fn () {}不会。泛型必须和fn后的括号结合,比如fn<T> () {}会报错,而fn <T>()不会。
  • 对空格的重视使得 Rhyme 获得以下好处:① 代码风格统一;② 编译速度快,因为这使得编译器不用再考虑很多二义性问题;③ 变量命名自由,这使得变量命名可以带-作为连字符而不再被二义性所困扰。
  • Rhyme 底层采用原型继承,这使得元编程变得更加灵活。不过 Rhyme 同时也提供 class 语法糖将底层的原型继承模式隐藏了起来,而且有额外的优化(这甚至更胜于 JavaScript ES6 加入的 class 语法糖)。一般来说,当用户不需要使用元编程时,他就不需要了解有关底层原型继承机制的任何知识。
  • Rhyme 会在编译期进行大量的性能优化,例如单类型的 list 会有额外的性能优化。
  • Rhyme 原生对科学计算有很好的支持,标量(Scaler)、向量(Vector)、矩阵(Matrix)及张量(Tensor)这几个数据类型是内置的。同时也对向量化有很好的支持。
  • 考虑到多数程序员并不经常使用位运算符,但位运算符实质上又诡异地占据了许多语言中那些最宝贵的运算符,因此 Rhyme 将取消位运算符。当然,位运算本身仍是被保留的,如果确实需要大量使用位运算符,例如嵌入式开发,可以通过其他方式重新引入位运算符。
  • Rhyme 的所有语句按理来说都不需要在末尾加上分号,不过这仍是可选的。Rhyme 会按照换行符尽可能解析代码直到其不能解析为止,类似 JavaScript,不过当一行以([+等符号开头时,Rhyme 会强制将其视作新一行代码,这是与 JavaScript 不同的。
  • Rhyme 没有new关键字,所有的类都可以直接创建实例,Rhyme 选择相信程序员。
  • Rhyme 拥抱函数式编程,鼓励尽可能使用foreach/map/filter/reduce等函数而非使用 for 循环。同时 Rhyme 不提供类似fn fnName () {}的函数定义方式,只提供const/let fnName = fn () {}的函数定义方式(或是const/let fnName = () => {})。甚至类定义也是如此,只提供类似const/let className = class () {}的定义方式。自然,只有这类函数/类定义方式的 Rhyme 不存在函数/类作用域提升。

好了,枯燥的文字到此结束了,下面谈谈一些存在于我设想中的有趣设计。

运算符

在 Rhyme 中,运算符的定义被极大扩展。运算符可以是一元或二元的,一元运算符只能是前置运算符。用户自定义运算符的优先级永远是最低的,这可以防止一些意料之外的问题。所有运算符调用时都必须在两边加上空格,只有取负运算符-是例外,它必须不加空格调用。

多说无益,直接上代码。由于 Rhyme 尽量以“自然语言编程”的设计哲学,因此 Rhyme 中应当不会加入管道运算符|>,但通过自定义运算符,用户可以轻松实现它:

1
2
3
4
5
6
7
IKWID {
Object.|> = op <T, U>(self, f: T => U) -> U {
return f(self)
}
}

1 |> x => x + 1 |> x => x ^ 2 // => 4

在上面的代码中,IKWID是”I Know What I’m Doing”的缩写。当用户尝试进行危险操作,例如直接修改内置类时,需要将代码放在IKWID块中。

在 Ruby 中,2.days.ago是吸引人入坑的一大特色,在 Rhyme 中甚至可以更好地实现它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
IKWID {
Number.days = op <T, E>(self, rhs: Symbol) -> Either<T, E> => {
if rhs == :ago {
return datetime.now() - self * 24 * 60 * 60 * 1000
} else if rhs == :later {
return datetime.now() + self * 24 * 60 * 60 * 1000
}
throw TypeError('operator "days" must be called with "ago" or "later"')
}
}

// 假设当前时间为2022-03-19 01:15:52
const t = 2 days ago
print(t) // => 2022-03-17 01:15:52

上述代码将”days”定义为了一个在 num 类型上使用的运算符,它只接受一个值为”ago”或”later”的 Symbol 作为右值,返回 n 天前/后的时间。

可以看到,运算符某种意义上可以视作接收一个或两个参数的函数的另一种形式。因此,普通的函数如果接收一个或两个参数,也可以通过这种方式调用,只需加上一个装饰器,例如:

1
2
3
4
const add = @operator (lhs: num, rhs: num) => lhs + rhs

add(1, 2) // => 3
1 add 2 // => 3

匿名函数

虽然在 Rhyme 中无法直接通过fn fnName () {}定义函数,但可以在const/let fnName = fn () {}中加上可选的函数名,例如const/let fnName = fn fnName () {},这主要是为了方便递归。不过通常来说,不必为函数标上函数名,而且即使不标函数名 Rhyme 也会对报错信息进行优化。

Rhyme 中定义匿名函数的形式多种多样。上面已经演示过的fn和箭头函数就不展示了,这里展示一下通过it关键字定义的匿名函数。显然,该灵感来自于 Clojure。

先看下面这段代码:

1
[1, 2, 3, 4, 5] filter #{it % 2 == 0} // => [2, 4]

显然,可以看到filter应当已经被定义在列表类型上的运算符,其右值接收一个函数作为参数,而这里的#{it % 2 == 0}就是这个被接收的函数。

一个由#{}包裹的代码块会被视为一个匿名函数,而it就表示该函数接收的参数。当仅接收一个参数时,it很自然地就表示那个唯一的参数;当接收多个参数时,it表示一个包含多个参数的元组,可以通过it[0]it[1]等调用这些参数。

有时,包裹 it 匿名函数的#{}可以省略,例如:

1
[1, 2, 3, 4, 5].filter(it % 2 == 0) // => [2, 4]

仅当其作为函数参数时能够省略#{}。此外省略形式的匿名函数不能包含逗号,逗号将截断表达式并将其视作两个参数。不过,如div(a, b) == 0这样的代码不会有问题。

管道

Rhyme 不提供|>作为管道操作符,因为这不符合“自然语言编程”的设计思路。但 Rhyme 以其他方式提供管道操作。

在 Rhyme 中,管道操作通过.then 方法实现。该方法存在于 Object 对象上:

1
2
3
4
5
6
7
const x = 1
const result = x
.then(it + 1)
.then(it * 2)
.then(it ^ 2)
x // => 1
result // => 8

值得注意的是,这里的.then()是同步的,它只是用于表示管道操作的方法,与 JavaScript 中的 Promise 没有任何关系。

当然,自定义一个全局的|>运算符也没什么问题:

1
2
3
IKWID {
Object.|> = op <T, U>(self, f: T => U) -> U => f(self)
}

Rhyme 并不禁止为内置对象添加属性,这意味着程序员可以直接更改内置 API。然而,当进行类似的修改时,Rhyme 的编译器会发出警告,以确保你确实知道自己在做什么,因此上面使用了@suppress-warning装饰器来抑制警告。

然后,你就可以轻松地使用管道运算符了:

1
2
3
4
const x = 1
const result = x |> #{ it + 1 } |> #{ it * 1 } |> #{ it ^ 2 }
x // => 1
result // => 8

当然,由于这不太符合 Rhyme 的设计哲学,因此建议使用.then()方法,而不是自定义的管道运算符。这里仅做演示用。

空格

在 Rhyme 中,空格是有意义的,并且无论什么情况下都不可省略。

1
2
let a = 10
let b =10 // !SyntaxError

空格以外的空白字符是可以省略的,例如换行。Rhyme 在编译时会智能为行末加上;,参考 JavaScript。然而,不同于 JavaScript 的是,当下一行以()[]及运算符开头时,Rhyme 会直接在上一行的末尾加上;

Rhyme 对空格的严格限制远不仅限于此。有些限制似乎毫无必要,例如要求在fn关键字后加上空格,这甚至与多数人的习惯相悖。然而严格限制空格带来很多好处,例如更好的编译效率、更自由的变量命名(例如可使用-连字符),这也能强制所有 Rhyme 代码维持相对统一的代码规范。

模式匹配

在 Rhyme 中,模式匹配使用 switch…case…语句实现。需要注意的是,Rhyme 中不存在fallthrough语法,因此不需要写 break。

1
2
3
4
5
6
7
8
9
10
11
match letter {
'a' | 'e' | 'i' | 'o' | 'u' -> {
println('Vowel')
}
'y' | 'w' -> {
println('Sometimes Vowel')
}
default -> {
println('consonant')
}
}

面向对象

与许多语言一样,Rhyme 同样使用 class 关键字进行类定义。通过 class 关键字定义的类名不得以小写字母开头,若用下划线开头,则在若干下划线之后的第一个非下划线字符不得是小写字母或数字。不过,Rhyme 并不限制其他 Unicode 字符,例如使用中文命名类名,这将没有大小写字母的限制。

值得注意的是,Rhyme 直接使用方法的第一个参数区分实例方法、类方法以及静态方法。若第一个参数为self,则认为该方法为实例方法;若第一个参数为cls,则认为该方法为类方法;否则认为该方法为静态方法。因此,在 Rhyme 中selfcls为保留字。

1
2
3
4
5
const Point = class (restricted x: num, restricted y: num) {}

const point1 = Point() // Error
const point1 = Point(1, 2)
const point2 = Point(3, 4)

Rhyme 提供抽象类和特性(trait)。

1
2
3
const Person = abs class (...) {
// 不能实例化
}
1
2
3
const Human = trait {
// ...
}

同许多语言一样,类支持继承(且支持多重继承,使用线性化实现)。

1
2
3
const Teacher = class (...) < Person, Human {
// 继承
}

Rhyme 强制规定继承时,trait 必须放在 class 之后,否则无法通过编译。

另外,实际上由于 Rhyme 使用鸭子类型,因此显式继承 trait 是可选的,但仍推荐这么做。当声明继承某个 trait 的类被发现未实现 trait 中规定的某些方法时,将编译失败。

类本质上仍是对象,因此支持动态修改。

1
2
3
4
5
6
7
Point.distanceTo = (self, point: Point) -> num => {
return ((self.x - point.x) ^ 2 + (self.y - point.y) ^ 2) ^ 0.5
} // 需要注意的是,如果没有template,创建的是静态方法(类方法),而非实例方法

point1.distanceTo(point2) // => 2.8284271247461903

Point.__options__.extensible = false // 当然也支持禁止这一特性

在默认情况下,一个通过 class 创建的类是支持动态修改的,然而这可能造成原型链污染。如果对此感到厌恶,可以在配置文件里将其关掉。

下面演示 Range 类的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const Range = class (
start: num,
end: num,
step: num = 1
) {
def current = start - step

ovld init = (self, end: num) => {
self.init(0, end, 1)
}

def __iter__ = self => {
return self
}

def __next__ = self => {
self.current += self.step
if self.current >= self.end {
raise IterationEndError()
}
return self.current
}

def range = (cls, start: num, end: num, step: num = 1) => {
return cls(start, end, step)
}

ovld range = (cls, end: num) => {
return cls(end)
}
}

在类定义中只能包含defovld(重载)和ovrd(重写)语句,def可以用来定义属性或方法,它们本质上没什么不同。此外,Rhyme 使用不同前缀区分私有(_)、公有(默认)和受保护(#)。

Rhyme 并不认为继承有什么坏处,甚至也不禁止多重继承,因为这是一种很直接的思维方式。继承产生问题的根源在于滥用继承的人身上,而不在于继承本身。


Rhyme——在程序语言设计上的一次尝试
https://snowfly-t.github.io/2022/05/29/Rhyme——在程序语言设计上的一次尝试/
作者
Snowflyt
发布于
2022年5月29日
许可协议