Red语言:向编程复杂性反击

你是否曾对软件编程栈的混乱感到过困惑?错综复杂和臃肿的解决方案无处不在,但无论是在当代软件的构建还是维护环节,很少有架构师和程序员能真正意识到由此带来的成本。Red的存在正是为了反击这种复杂性,这是它最主要的设计目的。没错,在现代软件世界中,“简单”的工具和简单的解决方案仍是可以企及的。这是个所有人都竭力掖藏的行业秘密(这可不是什么好事儿),但自1997年以来,解决方案其实已由Rebol编程语言给出。Red继承自Rebol谱系,并尝试将Rebol原先的使用范围加以大大拓展。Red是个雄心壮志的项目,它想要成为首个全栈编程语言。以下是其主要特性和设计目标:

  • 范式无关,默认提供函数式/命令式/符号式范式
  • 支持基于原型的对象
  • 同像语言(Red语言是其自身的元语言)
  • 既能以静态方式,也能以JIT方式编译为本地代码
  • 强力支持并发和并行(通过Actor和并行聚集)
  • 通过内置Red/System DSL提供底层系统编程能力
  • 提供高级脚本特性和REPL控制台支持
  • 高度可嵌入(​​像Lua一样,或者更好一些)
  • 低内存占用,支持垃圾回收
  • 低磁盘空间占用(<1MB)
  • 生成单个的命令行可执行文件
  • 零安装、零配置
  • 独立的跨平台工具链
  • 除运行的操作系统外,概无其他依赖

Red语言,正如Rebol一样,依靠独特的途径来编程,与目前主流语言的主张有所不同。以下是JSON发明者Douglas Crockford对Rebol语言发表的评论:

Rebol是一种更具现代性的语言,但也有些与Lisp语言非常相似的思想蕴涵在内,主要体现在它也是完全建立在先进行数据表示、尔后将其作为程序执行起来这样的基础之上。但它拥有更丰富的语法材料。Rebol是一种才华横溢的语言,它本该更普及一些,现在却并未如此,这可引为憾事。

Red和Rebol的设计目标是要尽可能地提供表达能力,同时保持源代码的高可读性,事实上它们的源代码很接近自然语言。2013年的一项将程序设计语言按表达能力排序的研究项目中,Rebol语言名列第三,仅次于Augeas和Puppet这两种DSL(http://goo.gl/qyPXx)。这有力地表明,在所有通用型程序设计语言中,Rebol是表达能力最强的一个。Red和Rebol都遵守务实设计原则,没有哪部分是拍脑袋的决定,或是“为了不同而不同”。每个语法或语义背后,都有明确的设计理由。

Red语言初探

Red和Rebol如何在企及如此水平的效率的同时,又保持“简单”呢?根本的原因之一,是因为这两者都既是一种数据格式,又是一门程序设计语言。此特性继承自Lisp,特别是s-表达式概念。Lisp中也建立了Red和Rebol所依赖的元编程模型。它们都是同像语言,所有的代码都表示为数据。值存储在块中(使用方括号记法:[……]),即普通列表,这和Lisp的记法一样,但它们不要求函数调用的那对括号,这就使得代码看上去更像自然语言。函数在默认情况下采用前缀记法,然而,Red和Rebol也支持针对数学和布尔运算符采用中缀表示法,这为代码阅读提供了便利。采用这种纯元编程进行程序设计,反射、热补丁、甚至即时编码这些语言特性都能得到内建支持,不需要学习任何新API,也无须特别的库支持,只使用基本函数进行数据操作即可。以下是使用Red语言REPL的示例:

 

$ red
red>> code: ["hello"]
== ["hello"]

red>> insert code 'print
== ["hello"]

red>> code
== [print "hello"]

red>> do code
hello

red>> code/2: 123
== 123

red>> code
== [print 123]

red>> append code [+ 1]
== [print 123 + 1]

red>> do code
124

red>> length? code
== 4

red>> type? code/1
== word!

red>> foreach value code [probe type? value]
word!
integer!
word!
integer!

如君所见,该语言的基本构件如下:

 

  1. 值:它们代表着数字、时间、字符串、文件名和URL等,如123 45.6 10:20:30.456 192.168.1.0 “Hello” http://red-lang.org。
  2. 字:它们可以被用作“变量”(但不是所有的都可以),如print block? string! map-each ?? + =。
  3. 块:由值、字和块以任何顺序组合而成,如[1 2 3] [123 “abc” print 10:20]。

这些就是用来表示数据、构建表达式、定义函数、创建对象、以及构造更复杂数据类型的基本构件。符号都是一等(first-class)值,也是一种更自然也更有效的字符串替代品,这对于很多用例都适用,尤其是涉及查找操作时就更能说明问题,因为符号可以以O(1)的效率比较,而字符串则要求O(n)。符号不区分大小写,所以print、Print和PRINT是等同的,人类语言不也是这样吗?

Rebol或Red程序源代码通常都是UTF-8输入字符串,它们都会被LOAD化处理。LOAD是原生的核心功能,能够将任何字符串变换成包含在一个块中的内存二进制格式。块将值存储至128比特的相邻单元内。值主要分为标量值(其尺寸固定)或序列值(用户可以向其中添加/删除数据)。数字、日期、元组和值对是标量值的,而值块、字符串、URL、路径、文件和标签是序列值的示例。标量值通常可以置入单个存储单元,而序列值则需要额外内存。

因此,块是带有垃圾回收的内存管理器进行保留和回收的主要分配单位。Rebol采用经典的stop-the-world标记和清理垃圾回收算法,而Red则依赖于stop-the-thread分代压缩垃圾回收算法(尚未完全实现),交替进行部分或全部遍扫。Red将扩充其垃圾回收算法,在未来实现增量回收,以使得它可以用来开发实时应用,例如60-FPS的街机游戏。

一旦源代码被加载入内存,变成一个值块,它就只是纯粹的数据。默认地,在遍扫源代码,使之变换成Red或Rebol二进制文件时,就会对值进行计算。如果是用Rebol语言,加载的块会被解释执行;如果是用Red语言,它们就将被编译成本地代码,但目的都是为了完成计算。然而,值块以何种方式进行解释,则是依赖于语境的。解释方式有如下几种:

  1. 函数式:由某个函数来取参计算
  2. 作为一种特殊语法:即所谓方言(指DSL)
  3. 通过Parse方言语句:匹配(matching)、回溯(backtrack),以及生成(production)
  4. ad-hoc代码:你想如何处理,由自己决定

默认的(非纯的)、以函数式方式解释的语言(即我们通常所谓的“代码”),是一种面向表达式的语言,它带有很简单的语义规则:

  1. 没有关键字
  2. 带有称为“改进”的可选扩充的定参函数
  3. 单一或多值的函数参数和返回值
  4. 表达式由左到右求值
  5. 中缀运算符优先级高于前缀函数调用

因此,浮在语言上方的函数层可以很容易地被任何自定义计算体取代(例如,撰写一个类似于Prolog的解释器将非常简单)。这使得Red和Rebol具备了极好的可延展性,十分容易适应任何你可能的需要。这种灵活性是DSL得以成为一种自然的方式解决一些计算任务,通过提供特定领域的微语言对于给定的任务实现优化的基础。Red和Rebol凭借这种力量,在核心语言和标准库中广泛地使用了嵌入式DSL:

  1. VID:视图接口方言(View Interface Dialect)是用于建立GUI的DSL
  2. Draw:进行2D绘图的DSL
  3. Parse DSL:一种类BNF语法解析DSL
  4. Security Dialect:一种在运行时用于控制安全沙箱特性的DSL
  5. 函数规格:函数原型亦使用DSL来描述

使得嵌入式DSL的创建容易、便捷的因素有:

  1. Parse DSL:一种强大的TDPL,并带递归和回溯支持
  2. 可将Red或Rebol代码直接内嵌于规则解析的能力
  3. 丰富的数据类型:已支持数十种常用数据的字面量符号,所以没有必要建立特殊规则来解析之

下面是一些在Rebol2中的GUI DSL的示例:

 按一个大小为100×100的红色按钮,以打开一个窗口:

 

>> view layout [button "Hello" red 100x100]

 

 显示会触发一个动作的按钮:

 

>> view layout [button "Hi" [print "Hello!"]]

 

 显示按钮,打印一个字段的内容:

 

>> view layout [
in: field 200
button "Print" [print in/text]
]

 

 Red语言中的一个Parse DSL示例:

 

  red>> digit: charset "0123456789"
  red>> parse "hello 888 world" [
  some [copy n some digit | skip]
  ]
  red>> n
  == "888"

 

与Rebol相比,Red带来了什么?

由于想要解决Rebol语言的一些不足之处,Red语言应运而生。这些不足包括:

  1.  性能不够高(接近Ruby/ PHP)
  2.  缺乏底层编程能力
  3.  不能利用多核,并发支持有限
  4.  缺少对移动操作系统的支持(在2013年才对Android提供有限的支持)
  5.  不开源(开源时限仅至2012年12月,且仅限于Rebol第3版)

Red在解决以上大部分问题时都采用了创新设计:嵌入一个低层次的程序设计语言,它可以直接编译为本地代码,即Red/System。它采用Red语法(值仍为块的形式),但与C类似,语义级别更低。它是静态类型语言,并仅提供少数几种数据类型:integer!、float!、float32!、byte!、logic!、pointer!、struct!,以及function!。它算支持指针算术,这赋予了它近于C的力量。标准库也非常简约,所以纯Red/System编译后的代码尺寸非常小,下面这个程序:

 

Red/System [
    Title: "Hello World app"
]
print "Hello World!"

 

编译后通常小于10KB。

Red程序被编译成Red/System代码,并与Red标准库链接,而Red标准库则大部分采用Red/System,少部分用Red自身写就。该库目前未压缩体积约为200KB。我们的目标是在发行1.0版时,将其控制在500KB左右。

Red与Red/System紧密集成,有数种方法在Red语言中调用或内嵌Red/System代码。其中最有用的一种是“常规”函数类型,它允许定义一个Red函数,其函数体为纯Red/System代码。Red语言会为你自动完成传递参数和返回值的装箱/拆箱操作。有了Red/System的帮助,Red语言便可以解决从硬件到DSL的任何抽象层次,从而成为真正的首个全栈编程语言。

因此,Red依靠Red/System工具链来生成可执行文件。该编译器和链接器目前支持的目标平台包括:

  1. CPU:IA-32、ARMv5和有限的AVR-8支持(Atmel328P)
  2. 文件格式:PE、ELF、Mach-o、.dll、so、APK,以及Windows驱动程序
  3. 计划支持的其他格式:
  4. 后端:IA-64、ARMv7、asm.js、JVM、CLR、Dalvik,以及LLVM
  5. 文件格式:.lib、.a、.ipa,各种驱动程序格式,war(Web归档)

除此之外,Red语言还依靠桥接技术,以访问像Java那样的虚拟机,尤其是提供对Android的支持。事实是,目前已可通过Red/Java桥,采用JNI技术来从Red程序远程控制Java和Android API,并获取事件返回。2014年晚些时候,我们还计划推出Red/Obj-C桥,以访问iOS和Cocoa API。

尽管后端和文件格式的组合可能性繁多,但确定选用何种目标平台的Red配置文件却短小精悍。这就使得交叉编译变得非常简单,例如:

 从Linux或Mac平台上生成一个Windows平台的可执行文件:

 

$ red -t Windows hello.red

 

 从Windows平台上生成一个Raspberry Pi平台的可执行文件:

 

$ red -t Linux-ARM hello.red

 

以下是关于这些目标平台的定义:

 

Windows [
    OS: 'Windows
    format:     'PE
    target: 'IA-32
    type:   'exe
    sub-system: 'GUI
    ]

Linux-ARM [
    OS: 'Linux
    format: 'ELF
    target: 'ARM
    type:   'exe
    base-address: 32768 ; 8000h
    dynamic-linker: "/lib/ld-linux.so.3"
    ]

 

Red语言的开发工作仍然任重道远。目前它的工具链部分使用Rebol2完成自举(指Red语言和Red/System编译器+链接器)。由于需支持JIT编译动态生成的Red/System代码,Red语言必须有自承载能力,所以所有工具链需要在1.0版之后用Red语言重写。新的工具链将能生成真正包含全部特征的Red语言,并同时为两种编译器提供优化层,生成更小更快的代码。目前,Red/System的性能实测下来大约比C慢4~6倍,这对于未经优化的本地代码而言已经不错了。

性能还会随着即将到来的并发支持而得到提升。今年晚些时候,Red语言会提供一个完全异步I/O接口,以及M:N线程模型(M个轻量级线程,分发到N个OS线程中去)。根据目前的计划,更高级别的抽象会使用Actor模型,这是为了实现简单而高效的共享状态同步。从用户的角度看,Actor也只是个一等的数据类型,和使用任何其他对象都差不多,很少或者根本没有额外的语法负担。然而,考虑到Go语言的goroutine模型的拥趸之多,我们仍在斟酌各种选项以决定使用Red语言的最佳模式。

Red语言是一个开源项目,采用BSD许可证。作为语言的作者,我全职工作于此业已三年。项目资金来自用户和支持者捐款。自2011年首次发布以来,得到的支持已然令我难以置信。开发者对于Red语言可提供的一切感到惊喜,尤其是那些了解Rebol语言能力的用户。Red语言对于他们中的大多数人,包括我自己,都是梦寐以求的工具。Red语言再次将乐趣带回了程序设计,将复杂性拒之门外,让程序员们再次感觉控制在握,就像那曾经经历的8位机时代。

从中国开始

由于Red语言将具备强大的Andr​​oid支持,又格外适合新入行的程序员,我们希望它能够被世界上最大的Andr​​oid市场——中国所接受。在中国,程序员数量巨大,而最有才华的Red语言贡献者中,就有一位是来中国上海的程序员谢晴天。

你可以从http://www.red-lang.org/p/download.html下载Red二进制文件。可执行文件只有半兆字节,包含了整个工具链,其中包括Red语言和Red/System编译器,一个带有可选控制台的解释器,还有目前支持30余种数据类型的整个标准库。你也可以尝试虽然老旧一些,但比较完整的Rebol2解释器,尤其是在测试GUI DSL时。