Practical Examples
Inspect one language lane at a time so line-level text and code deltas stay readable.
Diff Lane
English
0 modified sections0 code block delta0 anchor delta
Diff Lane
中文
2 modified sections0 code block delta0 anchor delta
modified快速幂的计算text+1 line
v1.0.5
Section Text
1
通过一个简单的例子展示使用宏进行编译期求值,生成优化代码的应用。在计算幂 `n ^ e` 时,如果 `e` 是一个(比较大的)整数,可以通过重复取平方(而不是迭代相乘)的方式加速计算。这个算法可以直接使用 while 循环实现,例如:2
3
<!-- run -->4
5
6
然而,这个实现需要每次对 `e` 的值进行分析,在循环和条件判断中多次对 `ve` 进行判断和更新。此外,实现只支持 `n` 的类型为`Int64`的情况,如果要支持其他类型的 `n`,还要处理如何表达 `result = 1` 的问题。如果预先知道 `e` 的具体值,可以将这个代码写的更简单。例如,如果知道 `e` 的值为 10,可以展开整个循环如下:7
8
<!-- run -->9
10
11
当然,手动编写这些代码非常繁琐,希望在给定 `e` 的值之后,自动将这些代码生成出来。宏可以做到这一点。使用案例如下:12
13
14
这个宏展开的代码是(根据 `.macrocall` 文件):15
16
17
下面是宏 `@power` 的实现。18
19
<!-- verify -macro13 -->20
<!-- cfg="--compile-macro" -->21
22
23
这段代码的解释如下:24
25
- 首先,确认输入的属性 `attrib` 是一个整数字面量,否则通过 `diagReport` 报错。将这个字面量解析为整数 `n`。26
- 设 `result` 为当前积累的输出代码,首先添加 `var _power_vn` 的声明。这里为了避免变量名冲突,使用不易造成冲突的名字 `_power_vn`。27
- 下面进入 while 循环,布尔变量 `flag` 表示 `var _power_result` 是否已经被初始化。其余的代码结构和之前展示的 `power` 函数的实现类似,但区别是使用 while 循环和 if 判断在编译时决定生成的代码是什么,而不是在运行时做这些判断。然后生成由 `_power_result *= _power_vn` 和 `_power_vn *= _power_vn` 适当组合的代码。28
- 最后添加返回 `_power_result` 的代码,并将这段代码作为宏的输出值返回。29
30
将这段代码放到 `macros/power.cj` 文件中,并在 `main.cj` 添加如下测试:31
32
<!-- verify -macro13 -->33
<!-- cfg="--debug-macro" -->34
35
36
输出结果为:37
38
<!-- verify -macro13 -->Code 1 · cangjie
1
func power(n: Int64, e: Int64) {2
var result = 13
var vn = n4
var ve = e5
while (ve > 0) {6
if (ve % 2 == 1) {7
result *= vn8
}9
ve /= 210
if (ve > 0) {11
vn *= vn12
}13
}14
result15
}Code 2 · cangjie
1
func power_10(n: Int64) {2
var vn = n3
vn *= vn // vn = n ^ 24
var result = vn // result = n ^ 25
vn *= vn // vn = n ^ 46
vn *= vn // vn = n ^ 87
result *= vn // result = n ^ 108
result9
}Code 3 · cangjie
1
public func power_10(n: Int64) {2
@power[10](n)3
}Code 4 · cangjie
1
public func power_10(n: Int64) {2
/* ===== Emitted by MacroCall @power in main.cj:20:5 ===== */3
/* 20.1 */var _power_vn = n4
/* 20.2 */_power_vn *= _power_vn5
/* 20.3 */var _power_result = _power_vn6
/* 20.4 */_power_vn *= _power_vn7
/* 20.5 */_power_vn *= _power_vn8
/* 20.6 */_power_result *= _power_vn9
/* 20.7 */_power_result10
/* ===== End of the Emit ===== */11
}Code 5 · cangjie
1
macro package define2
3
import std.ast.*4
import std.convert.*5
6
public macro power(attrib: Tokens, input: Tokens) {7
let attribExpr = parseExpr(attrib)8
if (let Some(litExpr) <- (attribExpr as LitConstExpr)) {9
let lit = litExpr.literal10
if (lit.kind != TokenKind.INTEGER_LITERAL) {11
diagReport(DiagReportLevel.ERROR, attrib,12
"Attribute must be integer literal",13
"Expected integer literal")14
}15
var n = Int64.parse(lit.value)16
var result = quote(var _power_vn = $(input)17
)18
var flag = false19
while (n > 0) {20
if (n % 2 == 1) {21
if (!flag) {22
result += quote(var _power_result = _power_vn23
)24
flag = true25
} else {26
result += quote(_power_result *= _power_vn27
)28
}29
}30
n /= 231
if (n > 0) {32
result += quote(_power_vn *= _power_vn33
)34
}35
}36
result += quote(_power_result)37
return result38
} else {39
diagReport(DiagReportLevel.ERROR, attrib,40
"Attribute must be integer literal",41
"Expected integer literal")42
}43
return input44
}Code 6 · cangjie
1
import define.*2
3
public func power_10(n: Int64) {4
@power[10](n)5
}6
7
main() {8
let a = 39
println(power_10(a))10
}Code 7 · text
1
59049v1.1.0
Section Text
1
通过一个简单的例子展示使用宏进行编译期求值,生成优化代码的应用。在计算幂 `n ^ e` 时,如果 `e` 是一个(比较大的)整数,可以通过重复取平方(而不是迭代相乘)的方式加速计算。这个算法可以直接使用 while 循环实现,例如:2
3
<!-- run -->4
5
6
然而,这个实现需要每次对 `e` 的值进行分析,在循环和条件判断中多次对 `ve` 进行判断和更新。此外,实现只支持 `n` 的类型为`Int64`的情况,如果要支持其他类型的 `n`,还要处理如何表达 `result = 1` 的问题。如果预先知道 `e` 的具体值,可以将这个代码写的更简单。例如,如果知道 `e` 的值为 10,可以展开整个循环如下:7
8
<!-- run -->9
10
11
当然,手动编写这些代码非常繁琐,希望在给定 `e` 的值之后,自动将这些代码生成出来。宏可以做到这一点。使用案例如下:12
13
<!-- code_no_check -->14
15
16
这个宏展开的代码是(根据 `.macrocall` 文件):17
18
<!-- code_no_check -->19
20
21
下面是宏 `@power` 的实现。22
23
<!-- verify -macro13 -->24
<!-- cfg="--compile-macro" -->25
26
27
这段代码的解释如下:28
29
- 首先,确认输入的属性 `attrib` 是一个整数字面量,否则通过 `diagReport` 报错。将这个字面量解析为整数 `n`。30
- 设 `result` 为当前积累的输出代码,首先添加 `var _power_vn` 的声明。这里为了避免变量名冲突,使用不易造成冲突的名字 `_power_vn`。31
- 下面进入 while 循环,布尔变量 `flag` 表示 `var _power_result` 是否已经被初始化。其余的代码结构和之前展示的 `power` 函数的实现类似,但区别是使用 while 循环和 if 判断在编译时决定生成的代码是什么,而不是在运行时做这些判断。然后生成由 `_power_result *= _power_vn` 和 `_power_vn *= _power_vn` 适当组合的代码。32
- 最后添加返回 `_power_result` 的代码,并将这段代码作为宏的输出值返回。33
34
将这段代码放到 `macros/power.cj` 文件中,并在 `main.cj` 添加如下测试:35
36
<!-- verify -macro13 -->37
<!-- cfg="--debug-macro" -->38
39
40
输出结果为:41
42
<!-- verify -macro13 -->Code 1 · cangjie
1
func power(n: Int64, e: Int64) {2
var result = 13
var vn = n4
var ve = e5
while (ve > 0) {6
if (ve % 2 == 1) {7
result *= vn8
}9
ve /= 210
if (ve > 0) {11
vn *= vn12
}13
}14
result15
}Code 2 · cangjie
1
func power_10(n: Int64) {2
var vn = n3
vn *= vn // vn = n ^ 24
var result = vn // result = n ^ 25
vn *= vn // vn = n ^ 46
vn *= vn // vn = n ^ 87
result *= vn // result = n ^ 108
result9
}Code 3 · cangjie
1
public func power_10(n: Int64) {2
@power[10](n)3
}Code 4 · cangjie
1
public func power_10(n: Int64) {2
/* ===== Emitted by MacroCall @power in main.cj:20:5 ===== */3
/* 20.1 */var _power_vn = n4
/* 20.2 */_power_vn *= _power_vn5
/* 20.3 */var _power_result = _power_vn6
/* 20.4 */_power_vn *= _power_vn7
/* 20.5 */_power_vn *= _power_vn8
/* 20.6 */_power_result *= _power_vn9
/* 20.7 */_power_result10
/* ===== End of the Emit ===== */11
}Code 5 · cangjie
1
macro package define2
3
import std.ast.*4
import std.convert.*5
6
public macro power(attrib: Tokens, input: Tokens) {7
let attribExpr = parseExpr(attrib)8
if (let Some(litExpr) <- (attribExpr as LitConstExpr)) {9
let lit = litExpr.literal10
if (lit.kind != TokenKind.INTEGER_LITERAL) {11
diagReport(DiagReportLevel.ERROR, attrib,12
"Attribute must be integer literal",13
"Expected integer literal")14
}15
var n = Int64.parse(lit.value)16
var result = quote(var _power_vn = $(input)17
)18
var flag = false19
while (n > 0) {20
if (n % 2 == 1) {21
if (!flag) {22
result += quote(var _power_result = _power_vn23
)24
flag = true25
} else {26
result += quote(_power_result *= _power_vn27
)28
}29
}30
n /= 231
if (n > 0) {32
result += quote(_power_vn *= _power_vn33
)34
}35
}36
result += quote(_power_result)37
return result38
} else {39
diagReport(DiagReportLevel.ERROR, attrib,40
"Attribute must be integer literal",41
"Expected integer literal")42
}43
return input44
}Code 6 · cangjie
1
import define.*2
3
public func power_10(n: Int64) {4
@power[10](n)5
}6
7
main() {8
let a = 39
println(power_10(a))10
}Code 7 · text
1
59049modifiedMemoize 宏text+1 line
v1.0.5
Section Text
1
Memoize(记忆化)是动态规划算法的常用手段。它将已经计算过的子问题的结果存储起来,当同一个子问题再次出现时,可以直接查询表来获取结果,从而避免重复的计算,提高算法的效率。2
3
通常 Memoize 的使用需要开发者手动实现存储和提取的功能。通过宏,可以自动化这一过程。宏的效果如下:4
5
6
在以上代码中,`fib` 函数采用简单的递归方式实现。如果没有 `@Memoize[true]` 标注,这个函数的运行时间将随着 `n` 指数增长。例如,如果在前面的代码中去掉 `@Memoize[true]` 这一行,或者把 `true` 改为 `false`,则 `main` 函数的运行结果为:7
8
9
恢复 `@Memoize[true]`,运行结果为:10
11
12
相同的答案和大幅缩短的计算时间表明,`@Memoize` 的使用确实实现了记忆化。13
14
为了理解 `@Memoize` 的原理,展示对以上 `fib` 函数进行宏展开的结果(来自 `.macrocall` 文件,但是为了提高可读性整理了格式)。15
16
<!-- run -->17
18
19
上述代码的执行流程如下:20
21
- 首先,定义 `memoizeFibMap` 为一个从 `Int64` 到 `Int64` 的哈希表,这里第一个 `Int64` 对应 `fib` 的唯一参数的类型,第二个 `Int64` 对应 `fib` 返回值的类型。22
- 其次,在函数体中,检查入参是否在 `memoizeFibMap` 中,如果是则立即反馈哈希表中存储的值。否则,使用 `fib` 原来的函数体得到计算结果。这里使用了(不带参数的)匿名函数使 `fib` 的函数体不需要任何改变,并且能够处理任何从 `fib` 函数退出的方式(包括中间的 return,返回最后一个表达式等)。23
- 最后,把计算结果存储到 `memoizeFibMap` 中,然后将计算结果返回。24
25
有了这样一个“模板”之后,下面宏的实现就不难理解了。完整的代码如下。26
27
<!-- compile -->28
<!-- cfg="--compile-macro" -->29
30
31
首先,对属性和输入做合法性检查。属性必须是布尔字面量,如果为 `false` 则直接返回输入。否则,检查输入必须能够解析为函数声明(`FuncDecl`),并且必须包含正好一个参数。下面,产生哈希表的变量,取不容易造成冲突的变量名。最后,通过 `quote`模板生成返回的代码,其中用到哈希表的变量名,以及唯一参数的名称、类型和输入函数的返回类型。Code 1 · cangjie
1
@Memoize[true]2
func fib(n: Int64): Int64 {3
if (n == 0 || n == 1) {4
return n5
}6
return fib(n - 1) + fib(n - 2)7
}8
9
main() {10
let start = DateTime.now()11
let f35 = fib(35)12
let end = DateTime.now()13
println("fib(35): ${f35}")14
println("execution time: ${(end - start).toMicroseconds()} us")15
}Code 2 · text
1
fib(35): 92274652
execution time: 199500 usCode 3 · text
1
fib(35): 92274652
execution time: 78 usCode 4 · cangjie
1
import std.collection.*2
3
var memoizeFibMap = HashMap<Int64, Int64>()4
5
func fib(n: Int64): Int64 {6
if (memoizeFibMap.contains(n)) {7
return memoizeFibMap.get(n).getOrThrow()8
}9
10
let memoizeEvalResult = { =>11
if (n == 0 || n == 1) {12
return n13
}14
15
return fib(n - 1) + fib(n - 2)16
}()17
memoizeFibMap.add(n, memoizeEvalResult)18
return memoizeEvalResult19
}Code 5 · cangjie
1
macro package define2
3
import std.ast.*4
5
public macro Memoize(attrib: Tokens, input: Tokens) {6
if (attrib.size != 1 || attrib[0].kind != TokenKind.BOOL_LITERAL) {7
diagReport(DiagReportLevel.ERROR, attrib,8
"Attribute must be a boolean literal (true or false)",9
"Expected boolean literal (true or false) here")10
}11
12
let memoized = (attrib[0].value == "true")13
if (!memoized) {14
return input15
}16
17
let fd = FuncDecl(input)18
if (fd.funcParams.size != 1) {19
diagReport(DiagReportLevel.ERROR, fd.lParen + fd.funcParams.toTokens() + fd.rParen,20
"Input function to memoize should take exactly one argument",21
"Expect only one argument here")22
}23
24
let memoMap = Token(TokenKind.IDENTIFIER, "_memoize_" + fd.identifier.value + "_map")25
let arg1 = fd.funcParams[0]26
27
return quote(28
var $(memoMap) = HashMap<$(arg1.paramType), $(fd.declType)>()29
30
func $(fd.identifier)($(arg1)): $(fd.declType) {31
if ($(memoMap).contains($(arg1.identifier))) {32
return $(memoMap).get($(arg1.identifier)).getOrThrow()33
}34
35
let memoizeEvalResult = { => $(fd.block.nodes) }()36
$(memoMap).add($(arg1.identifier), memoizeEvalResult)37
return memoizeEvalResult38
}39
)40
}v1.1.0
Section Text
1
Memoize(记忆化)是动态规划算法的常用手段。它将已经计算过的子问题的结果存储起来,当同一个子问题再次出现时,可以直接查询表来获取结果,从而避免重复的计算,提高算法的效率。2
3
通常 Memoize 的使用需要开发者手动实现存储和提取的功能。通过宏,可以自动化这一过程。宏的效果如下:4
5
<!-- code_no_check -->6
7
8
在以上代码中,`fib` 函数采用简单的递归方式实现。如果没有 `@Memoize[true]` 标注,这个函数的运行时间将随着 `n` 指数增长。例如,如果在前面的代码中去掉 `@Memoize[true]` 这一行,或者把 `true` 改为 `false`,则 `main` 函数的运行结果为:9
10
11
恢复 `@Memoize[true]`,运行结果为:12
13
14
相同的答案和大幅缩短的计算时间表明,`@Memoize` 的使用确实实现了记忆化。15
16
为了理解 `@Memoize` 的原理,展示对以上 `fib` 函数进行宏展开的结果(来自 `.macrocall` 文件,但是为了提高可读性整理了格式)。17
18
<!-- run -->19
20
21
上述代码的执行流程如下:22
23
- 首先,定义 `memoizeFibMap` 为一个从 `Int64` 到 `Int64` 的哈希表,这里第一个 `Int64` 对应 `fib` 的唯一参数的类型,第二个 `Int64` 对应 `fib` 返回值的类型。24
- 其次,在函数体中,检查入参是否在 `memoizeFibMap` 中,如果是则立即反馈哈希表中存储的值。否则,使用 `fib` 原来的函数体得到计算结果。这里使用了(不带参数的)匿名函数使 `fib` 的函数体不需要任何改变,并且能够处理任何从 `fib` 函数退出的方式(包括中间的 return,返回最后一个表达式等)。25
- 最后,把计算结果存储到 `memoizeFibMap` 中,然后将计算结果返回。26
27
有了这样一个“模板”之后,下面宏的实现就不难理解了。完整的代码如下。28
29
<!-- compile -->30
<!-- cfg="--compile-macro" -->31
32
33
首先,对属性和输入做合法性检查。属性必须是布尔字面量,如果为 `false` 则直接返回输入。否则,检查输入必须能够解析为函数声明(`FuncDecl`),并且必须包含正好一个参数。下面,产生哈希表的变量,取不容易造成冲突的变量名。最后,通过 `quote`模板生成返回的代码,其中用到哈希表的变量名,以及唯一参数的名称、类型和输入函数的返回类型。Code 1 · cangjie
1
@Memoize[true]2
func fib(n: Int64): Int64 {3
if (n == 0 || n == 1) {4
return n5
}6
return fib(n - 1) + fib(n - 2)7
}8
9
main() {10
let start = DateTime.now()11
let f35 = fib(35)12
let end = DateTime.now()13
println("fib(35): ${f35}")14
println("execution time: ${(end - start).toMicroseconds()} us")15
}Code 2 · text
1
fib(35): 92274652
execution time: 199500 usCode 3 · text
1
fib(35): 92274652
execution time: 78 usCode 4 · cangjie
1
import std.collection.*2
3
var memoizeFibMap = HashMap<Int64, Int64>()4
5
func fib(n: Int64): Int64 {6
if (memoizeFibMap.contains(n)) {7
return memoizeFibMap.get(n).getOrThrow()8
}9
10
let memoizeEvalResult = { =>11
if (n == 0 || n == 1) {12
return n13
}14
15
return fib(n - 1) + fib(n - 2)16
}()17
memoizeFibMap.add(n, memoizeEvalResult)18
return memoizeEvalResult19
}Code 5 · cangjie
1
macro package define2
3
import std.ast.*4
5
public macro Memoize(attrib: Tokens, input: Tokens) {6
if (attrib.size != 1 || attrib[0].kind != TokenKind.BOOL_LITERAL) {7
diagReport(DiagReportLevel.ERROR, attrib,8
"Attribute must be a boolean literal (true or false)",9
"Expected boolean literal (true or false) here")10
}11
12
let memoized = (attrib[0].value == "true")13
if (!memoized) {14
return input15
}16
17
let fd = FuncDecl(input)18
if (fd.funcParams.size != 1) {19
diagReport(DiagReportLevel.ERROR, fd.lParen + fd.funcParams.toTokens() + fd.rParen,20
"Input function to memoize should take exactly one argument",21
"Expect only one argument here")22
}23
24
let memoMap = Token(TokenKind.IDENTIFIER, "_memoize_" + fd.identifier.value + "_map")25
let arg1 = fd.funcParams[0]26
27
return quote(28
var $(memoMap) = HashMap<$(arg1.paramType), $(fd.declType)>()29
30
func $(fd.identifier)($(arg1)): $(fd.declType) {31
if ($(memoMap).contains($(arg1.identifier))) {32
return $(memoMap).get($(arg1.identifier)).getOrThrow()33
}34
35
let memoizeEvalResult = { => $(fd.block.nodes) }()36
$(memoMap).add($(arg1.identifier), memoizeEvalResult)37
return memoizeEvalResult38
}39
)40
}