make 的命令行都是调用操作系统的 shell ,这样会带来移植性问题。就连基本的文件操作都需要定义很多环境变量来处理 cp / copy cat / type 之类的差异。不同操作系统的命令行转义规则也各不相同,如果命令行参数中出现特殊字符,要想在不同的 shell 下都正确转义难度极大。
算术运算
比如我想要从 1 循环到 10,声明 frame_1.png 依赖于 frame_1.tga,frame_2.png 依赖于 frame_2.tga,frame_3.png 依赖于 frame_3.tga …… frame_10.png 依赖于 frame_10.tga。这样的事情在 C 里面就是 for (i = 0; i < 10; ++i) 即可。在 make 中, for 倒是可以用递归来模拟,但 make 里面除非求助于 shell 就没有办法能做 ++i 这个事情。
子模块依赖关系
这个是 make 的死穴。如果一个项目中有多个模块,模块 a 依赖于模块 b 的输出文件 libb.a,那应该怎么写依赖关系呢?一般有两种做法
不同的模块使用独立的 Makefile ,a 模块依赖于 libb.a,如果 libb.a 不存在,就调用 b 模块的 make 来创建 libb.a,大概代码如下:
这样做的问题在于如果模块 b 中的某个源文件修改了,在 a 模块下 make 时, libb.a 不能自动重新编译,因而 a 模块也不会重新编译
用 include 之类的办法把所有模块的 Makefile 统一起来,这是目前我们做的方式。缺点在于:
编写子模块的 Makefile 时需要知道别的模块的信息,破坏了 Makefile 的局部性
为了解决这个问题,云风想的办法是自己定义了一套语法,在子模块 make 时,通通调用根一级的 Makefile ,只是把子模块的信息通过环境变量传进去。但这样带来了另一个缺点,即自定义的语法有学习成本,和直接的 make 语法相比,维护起来要稍微困难一些。
即使只构建一个子模块,也需要生成整个项目的依赖图
尤其当文件很多的时候,生成整个依赖图会比较慢。这个问题在 bjam 中也存在。因为我们现在的做法就相当于用 make 实现了一套 bjam ,所以我们也会面临 bjam 的一些问题。
当一个项目构建过程比较复杂的时候,可以考虑用通用语言来管理构建过程。对于通用语言来说,处理字符串转义、算术运算、循环遍历等事情易如反掌,无 需使用 make 那些神奇而又晦涩的神技。而跨平台的通用语言的文件系统库往往也比 shell 有更好的移植性。至于子模块依赖问题,对于支持 closure 、 coroutine 的语言来说,很容易就能实现依赖关系的惰性计算,只需要生成整个依赖图中于构建目标相关的那部分,速度就快多了。
我在做的一个项目的构建过程就十分复杂,需要把一些配置文件从一种格式转成另一种格式,然后再嵌入主程序中;用到的美术素材也需要转格式。这些过程 需要用到好几种转换工具,有一些转换工具就直接要用 lua 编写。我原先是用 GNU Make 来做这件事情的,把各种神奇的特性都用上了,最后碰到空格文件名的时候,还是放弃了,决心自己用 lua 实现一套构建系统,我把它叫做 lua-make
make 函数的第一个参数指最多允许多少个并行任务的意思,这里传 10,就相当于GNU Make 的 -j 10
make.filetarget 是说这是一个文件目标,而不是一个伪目标(即 GNU Make 的 .PHONY)。文件目标和伪目标相比,要增加判断文件修改时间、自动创建父级目录等功能。在我实现的构建系统中,一个目标默认是没有这些功能的,如果需 要这些功能就得写 make.filetarget {......}
int foo(int f) { return f * 3; } int i = 5; immutable x = 3 * 4; // 正确,12 immutable y = i + 1; // 错误,无法在编译时求值 immutable z = foo(2) + 1; // 正确,foo(2)能在编译时求得值——7
非静态的局部immutable数据的初始化表达式是在运行时计算的:
int foo(int f) { immutable x = f + 1; // 运行时求值 x = 3; // 错误,x不可改变 }
由于immutable数据具有可传递性,由immutable数据引用的数据也是不变的:
immutablechar[] s = "foo"; s[0] = 'a'; // 错误,s指向一块不可改变的数据 s = "bar"; // 错误,s不可改变
def foo(): Int @cps[Int,Int] = { // could leave out return type shift { k: (Int=>Int) => k(7) } + 1 } reset(2 * foo()) // result: 16
以下是另一示例,示范了如何在基于回调的API使用Continuation.
import java.util.{Timer,TimerTask}
val timer = new Timer()
def sleep(delay: Int) = shift { k: (Unit => Unit) => timer.schedule(new TimerTask { def run() = k() // in a real program, we'd execute k on a thread pool }, delay) }
println("look, Ma ...") sleep(1000) println(" no threads!")
for key, value in pairs(t) do print(key, value) end
有没有办法像unpack一样,对枚举器进行unpack,返回多个返回值呢?
这也可以用递归实现:
local function _iterator_unpack_i(i, f, s, var1, ...) if var1 == nil then return else return select(i, var1, ...), _iterator_unpack_i(i, f, s,f(s, var1)) end end local function iterator_unpack_i(i, f, s, var) return _iterator_unpack_i(i, f, s, f(s, var)) end
local function _iterator_unpack_f(formatstring, f, s, var1, ...) if var1 == nil then return else return format(formatstring, var1, ...), _iterator_unpack_f(formatstring, f, s,f(s, var1)) end end local function iterator_unpack_f(formatstring, f, s, var) return _iterator_unpack_f(formatstring, f, s, f(s, var)) end
local function keys(t) return iterator_unpack_i(1, pairs(t)) end
local function values(t) return iterator_unpack_i(2, pairs(t)) end