仓颉编程语言速览

本文对华为开发的全新编程语言进行了简要介绍。该语言在前一年仅限部分用户使用,而如今其官方网站已提供适用于WindowsLinuxDarwin系统的下载包。

https://cangjie-lang.cn/en/download

我们将简要介绍下载、运行编译器,并浏览各种语言特性,将其与其他流行工业语言进行比较。如果你有使用JavaGo(以及在一定程度上C++)的经验,你会觉得它有些熟悉——实际上,该语言并未试图通过一些令人惊叹的创新来吸引用户,似乎其目标是为JavaC#程序员提供一种易于迁移的原生编译语言。

曾有传言称该语言将使用象形文字,或专为人工智能应用设计——但本文中你将看不到这些内容。代码使用典型的英语关键字编写,若存在人工智能集成,也并非在语言层面实现。

元素周期表

安装与首次尝试

该网站提供了下载包(我选择了 Linux——后端程序员最常见的系统)——但这里还有一些有用链接: 文档实验环境——在讨论该语言时我会参考前者,并通过本地安装或实验环境来探索和演示语法等内容。

下载后,我只需将包解压到合适的文件夹(例如~/utils/cangjie),并在其中找到bin子文件夹,其中包含cjc可执行文件——显然是编译器。让我们创建一个名为test.cj的文件,其中包含如下简单程序:

图1:仓颉编程语言速览

然后尝试编译它(即使没有进一步的设置),例如 ~/utils/cangjie/bin/cjc test.cj——它运行正常,并生成多个文件,其中包括名为 main 的可执行文件。但它无法运行:

$ ./main

./main: error while loading shared libraries: libcangjie-runtime.so:
    cannot open shared object file: No such file or directory

因此我们显然需要设置一些环境变量。幸运的是,捆绑文件的根目录下有一个脚本 envsetup.sh

$ source ~/utils/cangjie/envsetup.sh

该命令将修改 PATH(以便无需指定路径即可访问 cjc)和 LD_LIBRARY_PATH(以便可执行文件能找到动态链接库)。现在好多了:

$ ./main 

Nihao, I'm a fine language :)

关于库文件——通过运行 cjc --help 可以看到有使用静态库进行编译的选项,但我尚未成功调用这些选项来创建完全独立的可执行文件。


基本语法与功能

后续讨论的内容若不涉及编译,可在上述“实验区”中尝试。抱歉使用代码截图,但由于目前尚无适用于仓颉输入法的代码高亮工具,因此将使用图片展示短代码片段(较长片段则以文本形式呈现)。

标识符

非常常见——标识符以字母开头,可包含数字。下划线也被支持。字母应按 Unicode 含义理解,因此国家字符也有效。此外,标识符可被反引号(\…“)包围,这允许使用保留字作为标识符(可能不是非常有用的功能)。

变量声明

使用关键字 letvar,两者的区别在于前者创建不可变变量(在给定作用域内)。这有助于避免错误(在并发编程中也可能有所帮助)。变量的类型通常会被推断,但也可以显式指定。这里还有 const,它看起来与 let 类似,但表示方式和可能的优化不同。

var a = "thirteen"
let b: Int64
let c: Array<String> = ["one", "two", "three"]

函数定义

如果你通过上面的示例认为函数声明不需要任何关键字,那是不完全正确的。只有 main 函数可以不带关键字声明,其他函数必须在名称前使用 func。让我们看看一个示例:

图2:仓颉编程语言速览

从这里我们可以学到几点:

  • 命令行参数以字符串数组的形式传递给 main,但该参数是可选的
  • 数组访问使用典型的方括号(arr[i]),且从零开始计数
  • println 函数仅接受一个参数
  • 但字符串可以像往常一样使用加号进行连接
  • main 的返回类型可以省略或指定为 Unit(不是 uint!);或者可以指定为 Int64 以返回退出代码
  • 允许对函数进行前向引用(例如,它们可以在代码中稍后声明,而不是在提及时声明)

基本类型

此处 basic 指的是属于语言核心而非“最简单”的类型。这些类型分为几类:

  • 整数 – 包括带符号和无符号整数,位数为 8163264,还包括“原生”类型,其大小取决于当前硬件架构
  • 浮点数 – 令人惊讶的是,除了常见的 Float32Float64 之外,还包含了较短的 Float16 类型。我惊讶地发现它被纳入了 IEEE 754 标准,尽管我对其实用性存疑,因为其精度较低(仅有 2-3 位)。
  • 逻辑类型(Bool)、字符类型(Rune)和字符串类型(String)——同样非常典型,其中字符串类型是不可变的(这也相当常见),并且支持类似于 PHP 的变量插值语法(例如 “I am ${person.age} years old”)

对这些类型的操作也非常典型,你可以主要应用你的 C++GoJava 经验。不过可能存在一些细微差别(例如 ++ 仅以后缀形式存在)。

数组

数组使用类型名称 Array 和类型参数(用尖括号括起)进行声明,例如 Array<String>,但类型参数也可省略。其他方面则与常见数组一致,数组具有固定大小。

图3:仓颉编程语言速览

这里可以声明为 let words: Array<String> = [“...”, “...”],或者使用 let words = Array<String>([“...”, “...”]),但我们使用“语法糖”来简化。如果编译器能从上下文(包括尖括号)推断出类型参数,则可省略该参数,即 let words = Array([...]) 同样有效。

范围

专门为“值序列”(即起始值、结束值和步长组合)定义内置类型可能看似多余,但它们可作为 Iterable 使用,例如在 for 循环中。

图4:仓颉编程语言速览

实际上,这是语言中唯一的 for 循环形式。不存在像 CJava 中那种三部分组成的“经典”形式。因此,这里的 1..10 不仅仅是另一种“语法糖”,而是 Range<Int64> 类型的表达式。Python 程序员可能会觉得更熟悉。

元组

PythonC# 类似,该语言包含“元组”——不可变的值对、三元组等,可能包含不同类型的值。语法非常熟悉,它们可以随意打包和解包:

图5:仓颉编程语言速览

我认为这非常方便——JavaGo没有元组,有时这确实令人烦恼。

Unit和Nothing类型

这里有两种非常有趣的类型。第一种是Unit,它只有一个值(比Bool少一个)——用空括号()表示。如你所见,它用于替代 void。第二个类型则少一个值,即完全没有值。它是 breakcontinue 等表达式的类型。这是一个较为“技术性”的类型,无法直接赋值。同时 Unit 可以赋值和比较,尽管这似乎没什么用处。

枚举

可定义自定义值集的类型。并非必要,但使用方便。它们拥有专用的“匹配”表达式和语法以简化使用。一个重要案例将在后续提及。

空值与指针

不存在 null 或“指针”(除“外部”语言互操作模块外)。替代 null 的是一种名为 Option 的枚举类型,它可以包含 Some<...> 值或 None。这种概念在“函数式”语言中常见,例如 ScalaHaskell

我个人觉得处理它们不太方便,但据说这能更好地防范空指针错误或类似问题。然而,由于程序员的惰性,这种保护机制并不总能如预期般生效。

表达式

大多数编程语言将“语句”与“表达式”区分开来。例如,println(“Hi, people!”)是一个语句,而括号内的字符串则是表达式。语句可以嵌套,语句中也可以包含表达式。实际上,println本身也是一个表达式,其值被计算为对函数的引用。

然而 Cangjie 试图简化这一问题,其中一切都是表达式。这带来了一些有用的后果,例如条件运算符也是表达式,可以作为“三元运算符”使用:

println(if (true) { 5 } else { 8 })

同时,for 循环的结果是类型为 Unit 的值——为什么不是 Nothing?我在此处未看到有用的逻辑。

循环

我们已经看到了 for-in 循环——这是唯一一种可以遍历实现 Iterable 接口的一切的循环,包括范围、数组或集合,我们将在后续讨论中提及。这里有一个 where 子句的额外功能,类似于 Python 中的“列表推导式”——它允许跳过 for 循环中的某些迭代:

for (i in 0..8 where i % 2 == 1)

当然,这个示例也可以通过范围本身的步长来表达,例如:

for (i in 1..8:2)

该语言还提供了遵循经典 C 及其衍生语言模式的 while 循环和 do-while 循环。


关于性能

现在我们已经了解了一些基本语法并能编写小型程序,让我们来检查编译后程序的性能。

我们知道,解释型语言(如PHPPython)通常比“原生编译”的语言慢5..20倍,其中C/C++是最快的之一(因为它不需要处理自动内存管理)。Go作为另一种原生编译语言的例子,速度仅略慢,而Java则处于中间位置 – 它被编译为由虚拟机执行的字节码,即不是“原生代码”,但具有“即时编译”功能以在运行时加速代码,通常仅比C2-3倍,尽管这可能因应用程序而异(主要是由于内存管理,而非纯指令执行速度)。

Cangjie 语言似乎是原生编译的,因此我们将尝试在专用仓库 Languages Benchmark 中实现一些我用其他语言编写的程序。

第一个程序基于柯拉茨猜想——这一未解决的数学问题,其中我们从给定的初始值构建数列,这些数列(据称)最终都会收敛到1,但经过的迭代次数难以预测。我们计算所有小于等于 maxN 的数字的序列长度——因此程序只是一个双重嵌套循环,使用标量变量。无需内存管理,因此主要涉及纯指令执行。

import std.os.*;
import std.convert.*;

main() {
  var maxn = Int.parse(if (let Some(v) <- getEnv("MAXN")) { v } else { "10" })
  var sum = 0
  for (i in 1..(maxn+1)) {
    sum += collatz(i)
  }
  println("sum=${sum}")
}

func collatz(n: Int): Int {
  var cnt = 0
  var res = n
  while (res > 1) {
    res = if (res % 2 > 0) {
      res * 3 + 1
    } else {
      res / 2
    }
    cnt++
  }
  return cnt
}

在此示例中,我们还尝试使用“标准库”中的部分函数——如上文所示的“imports”——包括Int.parse(...)getEnv(...)函数。这些函数将允许我们通过环境变量传递maxN。此外,你还可以看到我对 getEnv(...) 返回的 Option<String> 值的笨拙处理方式。其他一些语言会选择在变量未设置时直接返回空字符串,或者提供额外的默认值参数(我认为这是最好的做法)。

执行 C 语言的等效代码(来自上述仓库)在 maxN3mln 时耗时约 0.73 秒,而 Cangjie 耗时约 1.03 秒,与 Go 的结果非常接近。

需要注意的是,我们需要使用 -O2 优化开关进行编译,否则运行速度会慢得多。

第二个问题只是生成一个大小为 maxN 的素数列表,通过遍历所有整数并使用“试除法”测试,直到找到足够数量的素数。让这个源代码以图像形式呈现以保留高亮显示。

图6:仓颉编程语言速览

结果令人惊喜——Cangjie版本仅需2秒,而Go版本需4秒。当然,这主要与数组实现有关(Cangjie版本使用ArrayList集合)。我们未将C语言纳入比较,因其默认不支持自动扩容的列表/数组。


面向对象编程

我们仅简要提及这一点,因为Cangjie中面向对象编程(OOP)的实现与Java非常接近,特别是:

  • 继承机制存在,但仅支持单继承(而C++Python支持多继承,Go则采用聚合)
  • 接口由给定类显式声明(与 Go 不同,Go 中只要类具有合适的方法即可实现接口)
  • 四种访问修饰符(与 Go 不同,Go 中仅区分公共和私有两种级别,通过标识符的首字母区分)
  • 抽象类和最终类定义,但语法略有不同

还有一些差异,让我们来看看。

结构体与类

除了 class 定义外,还有 struct 定义——它们也可以包含成员函数和字段,但无法被继承。关于它们还有一些更微妙的特性(如字段的可变性等),我们现在不做讨论。很难说它们是否真的必要……除了与 C++ 的相似性。

运算符重载(重新定义)

C++ 类似,可以定义通过运算符调用的成员函数——即对于类 Vector,我们可以定义 +- 等运算符。对于通道,则可以定义类似“管道”的运算符,如 >> 等。当然这并非必要,但有时会很方便。

扩展

这一特性类似于 Go 中的面向对象实现——我们可以向现有类添加方法,但仅限于给定包的范围。以下示例演示了向预定义类型 String 添加方法 printSize

图7:仓颉编程语言速览


函数式编程

在语言层面,函数式编程的主要特性是定义匿名函数的语法,特别是以 lambda-expressions 的形式,以及能够将函数作为参数传递、赋值等。

其他用于以“函数式”风格处理数据的功能,主要以函数的形式实现于std.collection包中。这与Java 8中的实现有些类似,但采用了带有“柯里化”的特定语法。例如,map函数本身并不直接接受要映射的集合。相反,它接受一个转换函数作为参数——而结果才是真正的“映射函数”——该函数可以进一步应用于集合本身:

图8:仓颉编程语言速览

当然会输出 [3, 3, 5, 4, 4]。你看到,在 map 函数之后,是“无括号”形式的传递 lambda——即带 => 箭头的花括号。这将生成映射函数,然后它会被带括号的 arr 参数调用。结果是一个 Iterator,可以通过调用 collectX 函数之一将其转换为合适的集合。虽然它能正常工作,但与 Python 相比可能显得有些笨拙。


异常

它们存在于语言中(与 Go 不同)。但没有“强制风格”的异常(我认为这仅存在于Java中)。trycatchfinally关键字以及try-with-resource形式——这些对Java开发者来说都非常熟悉,因此我们无需在此过多赘述。它们存在,这是关键点 🙂


集合

对于 Java 开发者来说,这一部分看起来非常熟悉。std.collection 包为我们提供了 ArrayListLinkedList,以及 HashMapHashSetTreeMap。当然,后者在相等性和排序方面有一些细微差别,但这些内容你可以在相应的 API 文档中轻松找到。

另一个有用的包是 std.collection.concurrent,它提供了 BlockingQueueArrayBlockingQueueConcurrentHashMapNonBlockingQueue——后者类似于 Go 中的“通道”,但没有强制性的大小限制。

尽管前述的 Array 类型在语法上看起来相似,但它实际上是截然不同的“内置”类型,不应与其他类型混淆。总体而言,在处理“基本”或“内置”类型时,该语言的风格更趋于“统一”,类似于 C# 而非 Java


多线程与并发

实现了典型的“抢占式”模型,因此所有同时运行的代码片段都可能获得一部分处理器时间用于执行。对程序员而言,这看起来像典型的线程模型,线程在语言本身中实现并映射到操作系统线程,因此一个操作系统线程可以执行多个语言线程。这使得线程之间的“上下文切换”速度更快,并对线程的执行时间控制更精准。

这种方法与Go中的方法非常相似——只需将“线程”替换为“goroutine”,并将关键字go替换为spawn

图9:仓颉编程语言速览

我们记得,带“箭头”的波浪括号是lambda表达式的语法,即函数定义——而不是专门用于创建线程的语法。

对于并发和同步,语言提供了熟悉的原子操作(可安全地从多个线程读取和修改的数据类型)、互斥锁以及上述提到的并发集合类型。这里还有一个 ThreadLocal 类——类似于 Java 中的便捷功能——用于存储与线程绑定的值。关键字 synchronized 是使用互斥锁的方式,与 Java 类似,但每个对象不需要“监视器”。


反射、注解和宏

该语言在“元编程”方面功能丰富,允许你在运行时或编译时检查或修改代码结构本身。我们目前不会深入探讨这些功能,因为它们通常在大型项目或特定框架中才会变得有趣。附带一提——同时遇到反射和宏的情况较为罕见。

支持一种类似于 Go 语言构建标签的条件编译机制,但这些标签可置于任何声明之上,形式如下:

@When[os == "Linux"]

但灵活性(且易用性)远不及 C/C++ 中的 #ifdefs


基本输入输出

本段仅提及当前“开发指南”中对应部分包含三页空白 🙂

不过已提供 std.iostd.fs 甚至 std.socket 包的 API 描述,因此仍可使用这些功能。


工具

现代编程语言通常在缺乏实用工具时显得“不完整”。Cangjie 默认提供以下工具:

  • 包管理器 cjpm
  • 调试器 cjdb
  • 性能分析器 cjprof
  • 源代码格式化工具 cjfmt
  • 测试覆盖率计算工具 cjcov

此外,还提供了适用于 VSCode 集成开发环境(IDE)的插件。我未使用该 IDE,因此未尝试过该插件。对于我使用的 vim,至少存在由某些爱好者提供的语法高亮工具,并且似乎还有适用于 Intellij Idea 的插件。然而,似乎语言服务器尚未准备就绪,这在一定程度上限制了这些第三方工具的功能。以下是 vim 中该“着色器”显示代码的示例:

图10:仓颉编程语言速览


结论

Cangjie 语言似乎旨在取代企业开发中的 Java,因为如今 Java 通过编译为“运行于任何环境”的字节码,在某些场景下显得有些过度设计,而所有服务器后端软件通常在非常特定的环境中运行(常在 Docker 和 Kubernetes 中),因此直接进行原生编译可能更优。另一方面,该语言在功能上比JavaGo更为丰富。

另一方面,该语言并未引入一些独特的“杀手级特性”(如Rust的“无垃圾回收”内存管理或Haskell的严格类型系统)——没有特定的“创新”。这或许是好事,因为此类创新往往会导致陡峭的学习曲线和语言的过早消亡 🙂

本文文字及图片出自 Cangjie Programming Language overview

你也许感兴趣的:

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注