#import "mod.typ": *

#show: book.page.with(title: "块与表达式")

纵览Typst的编译流程，其大致分为4个阶段，解析、求值、排版和导出。

// todo: 介绍Typst的多种概念

// Source Code
// Value
// Type
// Content

// todo: 简化下面的的图片
#import "../figures.typ": figure-typst-arch
#align(center + horizon, figure-typst-arch())

// ，层层有缓存

为了方便排版，Typst首先使用了一个函数“解析和评估”你的代码。有趣地是，我们之前已经学过了这个函数。事实上，它就是#typst-func("eval")。

#code(```typ
#repr(eval("#[一段内容]", mode: "markup"))
```)

流程图展现了编译阶段间的关系，也包含了本节「块」与「表达式」两个概念之间的关系。

- #typst-func("eval")输入：在文件解析阶段，*代码字符串*被解析成一个语法结构，即「表达式」。古人云，世界是一个巨大的表达式。作为世界的一部分，Typst文档本身也是一个巨大的表达式。事实上，它就是我们在上一章提及的「内容块」。文档的本身是一个内容块，其由一个个标记串联形成。

- #typst-func("eval")输出：在内容排版阶段，排版引擎事实不作任何计算。用TeX黑话来说，文档被“解析和评估”完了之后，就成为了一个个「材料」（material）。排版引擎将材料。

在求值阶段。「表达式」被计算成一个方便排版引擎操作的值，即「材料」。一般来说，我们所谓的表达式是诸如`1+1`的算式，而对其求值则是做算数。

#code(```typ
#eval("1+1")
```)

显然，如果意图是让排版引擎输出计算结果，让排版引擎直接排版2要比排版“1+1”更简单。

但是对于整个文档，要如何理解对内容块的求值？这就引入了「可折叠」的值（Foldable）的概念。「可折叠」成为块作为表达式的基础。

#pro-tip[
  Typst借鉴了Rust，遵从「面向表达式编程」（expression-oriented programming）的哲学。它将所有的语句都根据可折叠规则（见后文）设计为表达式。
  + 如果一个语句能产生值，那么该语句的结果是按*控制流*顺序所产生所有值的折叠。
  + 否则，如果一个语句不能产生值，那么该语句的结果是```typc none```。
  + 特别地，任意类型 $T$ 的值 $v$ 与```typc none```折叠仍然是值本身。
  $ forall v in T union {"none"}, op("fold")_T (v, "none") = v $
]

== 表达式 <scripting-expression>

Typst中绝大部分语法结构都可作表达式，可以说学习完了所有表达式，则学会了Typst所有语法。当然，其吸取了一定历史经验，仅有少量的语句不是表达式。

#pro-tip[
  `show`语句和`set`语句不是表达式。
]

根据大的分类，可用的表达式可以分为5类（它们并非严格术语，而是为了方便教学而分类），他们是：
+ 代数运算表达式。
+ 逻辑比较表达式。
+ 逻辑运算表达式。
+ 赋值表达式。
+ 项。

其中第五项又是一个大类。我们上一章所学的所有常量、变量以及变量的应用都是项。块表达式也是项，将在本节后半部分详细介绍。

== 代数运算表达式 <grammar-arith-exp>

Typst支持对数字的算数运算，其中浮点运算遵守IEEE-754标准。整数和浮点数之间可以混合运算：

#code(```typ
//加     减        乘      除
#(1 + 2, 1.0 - 2, 1 * 2, 1 / 2 + 1)
```)

从上可以看出，整数和整数运算尽可能*保持*整数，但是整数和浮点数运算则结果变为浮点数。除法运算的结果是浮点数。算数之间遵守四则运算规则。

#pro-tip[
  更高级的运算，例如数字的位运算和数值计算隐藏在类型的方法和`calc`标准库中：

  #code(```typ
  #(0o755.bit-and(0o644)), // 8进制位运算
  #(calc.pow(9, 4)) // 9的4次方
  ```)

  请参考《》。
]

除了数字运算，字符串、数组等还支持加法和乘法运算。它们的加法实际上是连接操作，它们的乘法则是重复元素的累加。

#code(```typ
//乘         加
#("1"  * 2, "4"  + "5", ) \
#((1,) * 2, (4,) + (5,),)
```)

字典只支持加法操作。若有重复键值对，则右侧列表的键值对会覆盖左侧列表的键值对。

#code(```typ
#((a: 1) + (b: 2),
  (a: 1) + (b: 3, a: 2) + (a: 4 , c: 5))
```)

== 逻辑比较表达式 <grammar-logical-cmp-exp>

有三大简单比较关系：

#code(```typ
//大于    小于   等于
#(1 > 2, 1 < 2, 1 == 2)
```)

基于此，可以延申出三个方便脚本编辑的比较关系。

#code(```typ
//大于或等于 小于或等于 不等于
#(1 >= 2.0, 1 <= 1,  1 != 2)
```)

从数学角度，真正基本的关系另有其系，其中“小于或等于”是数学中的偏序关系，“等于”是数学中的等价关系。这两个关系可以衍生出其他四种关系，但是不太好理解。举例来讲，大于其实就是“小于等于”的否定；而小于则是“小于等于”但是不能“等于”。

注意：不推荐将整数与浮点数相互比较。具体请参考上一节所提及的浮点数陷阱。

字符串、数组和字典之间也是可以比较的，理解他们则必须从偏序关系和等价关系入手。

三种项的等价关系比较容易理解，说两字符串/数组/字典相等，则是在说二者有一样多的子项，且每个子项都一一相等。

#code(```typ
#((1, 1) == (1, 1)),
#((a: 1, c: (1, )) == (a: 1, c: (1, )))
```)

字典之间没有偏序关系，便只剩字符串和数组。偏序关系则需要指定一种排序规则。如果两字符串/数组不相等，则首先考虑它们的长度关系，长度小的一方是更小的：

#code(```typ
#("1"   <= "23"),
#((1, ) <= (2, 3, ))
```)

否则从前往后依次比较每一个子项，直到找到*第一个*不相等的子项，进而确定顺序。见下三例：

#code(```typ
#("1" <= "2", "113" <= "121", "2" <= "12")
```)

对于单个字符，我们依照字典序比较，其中容易理解地是数字字符顺序按字面递增。对于前两者，我们可以看到`"1"`比`"2"`小；进由此，故"113"比"121"小；尽管如此，优先判断长度关系，故"2"比"12"小。

从两种基础比较关系，其他四种比较关系也能被良好地定义，并在脚本中使用：

#code(```typ
#("1" > "2", "1" != "2",
  "1" < "2", "1" >= "2")
```)

== 逻辑运算表达式 <grammar-logical-calc-exp>

布尔值之间可以做“且”、“或”和“非”三种逻辑运算，并产生布尔类型的表达式：

#code(```typ
#(not false), #(false or false), #(true and false)
```)

真值表如下：

#{
  set align(center)
  table(
    columns: (33pt * 0.6,) * 2 + (33pt,) * 3,
    stroke: 0.5pt,
    $p$,
    $q$,
    $not p$,
    $p or q$,
    $p and q$,
    $0$,
    $0$,
    $1$,
    $0$,
    $0$,
    $0$,
    $1$,
    $1$,
    $1$,
    $0$,
    $1$,
    $0$,
    $0$,
    $1$,
    $0$,
    $1$,
    $1$,
    $0$,
    $1$,
    $1$,
  )
}

逻辑运算使用起来很简单，建议入门的同学找一些专题阅读，例如#link("https://zhuanlan.zhihu.com/p/82986019")[数理逻辑（1）——命题逻辑的基本概念]。但一旦涉及到对复杂事物的逻辑讨论，你就可能陷入了知识的海洋。关于逻辑运算已经形成一门学科，如有兴趣建议后续找一些书籍阅读，例如#link("https://www.xuetangx.com/course/THU12011001060/19316572")[逻辑学概论]。

本书自然不负责教你逻辑学。

== 赋值表达式 <grammar-assign-exp>

变量可以被赋予一个表达式的值。事实上，`let`表达式后的语法结构就是赋值表达式。

#code(```typ
#let a = 1; #a,
#(a = 10); #a
```)

除此之外，还有先加（减、乘或除）后赋值的变形。所有这些赋值语句都产生`none`值而非返回变量的值。

#code(```typ
#let a = 1; #a,
#repr(a += 2), #a, #repr(a -= 2), #a, #repr(a *= 2), #a, #repr(a /= 2), #a
```)

== 代码块 <grammar-code-block>

在Typst中，代码块和内容块是等同的。与「代码块」

- 代码块：按顺序包含一系列语句，内部为#term("code mode")。
- 内容块：按顺序包含一系列内容，内部为#term("markup mode")。

内容块（标记模式）内部没有语句的概念，一个个内容或元素按顺序排列。但你可以通过#mark("#")将解释器的「解释模式」从「标记模式」*临时*改为「脚本模式」。当执行完脚本后，将脚本结果转换成内容，并放置在「井号」处。

相比，代码块内部则有语句概念。每个语句可以是换行分隔，也可以是#mark(";")分隔。

#code(```typ
#{
  "a"
  "b"
} \ // 与下表达式等同：
#{ "a"; "b" }
```)

// 整数转浮点数：<grammar-int-to-float>

// #code(```typ
// #float(1), #(type(float(1)))
// ```)

// 布尔值转整数：<grammar-bool-to-int>

// #code(```typ
// #int(false), #(type(int(false))) \
// #int(true), #(type(int(true)))
// ```)

// 浮点数转整数：<grammar-float-to-int>

// #code(```typ
// #int(1), #(type(int(1)))
// ```)

// 数字转字符串：<grammar-num-to-str>

// #code(```typ
// #repr(str(1)), #(type(str(1)))
// #repr(str(.5)), #(type(str(.5)))
// ```)

// 布尔值转字符串：<grammar-bool-to-str>

// #code(```typ
// #repr(false), #(type(repr(false)))
// ```)

// 数字转布尔值：<grammar-int-to-bool>

// #code(```typ
// #let to-bool(x) = x != 0
// #repr(to-bool(0)), #(type(to-bool(0))) \
// #repr(to-bool(1)), #(type(to-bool(1)))
// ```)

// 表达式从感性地理解上就是检验执行一段代码是否对应产生一个值。精确来说，表达式是以下对象的集合：
// + 前述的各种「字面量」是表达式：
//   #code(```typ
//   // 这些是表达式
//   #none, #true, #1, #.2, #"s"
//   ```)
// + 「变量」是表达式：
//   #code(```typ
//   #let a = 1;
//   // 这是表达式
//   #a
//   ```)
// + 「括号表达式」（parenthesized expression）是表达式：
//   #code(```typ
//   // 这些是表达式
//   #(0), #(1+2), #((((((((1))))))))
//   ```)
// + 「函数调用」是表达式：
//   #code(```typ
//   #let add(a, b) = a + b
//   #let a = 1;
//   // 这是表达式
//   #add(a, 2)
//   ```)
// + 特别地，Typst中的「代码块」和「内容块」是表达式：
//   #code(```typ
//   // 这些是表达式
//   #repr({ 1 }), #repr([ 1 ]),
//   ```)
// + 特别地，Typst中的「if语句」、「for语句」和「while语句」等都是表达式：
//   #code(```typ
//   // 这些是表达式
//   #repr(if false [啊？]),
//   #repr(for _ in range(0) {}),
//   #repr(while false {}),
//   #repr(let _ = 1),
//   ```)
//   这些将在后文中介绍。
// + 情形1至情形7的所有对象作为项，任意项之间的「运算」也是表达式。

// 其中，情形1至情形3被称为「初始表达式」（primary expression），它们就像是表达式的种子，由其他操作继续组合生成更多表达式。

// 情形4至情形6本身都是经典意义上的「语句」（statement），它们由一个或多个子表达式组成，形成一个有新含义的表达式。在Typst中它们都被赋予了「值」的语义，因此它们也都是表达式。我们将会在后续文章中继续学习。

// 本节主要讲解情形7。由于情形1至情形6都可以作为情形7的项，不失一般性，我们仍然可以仅以「字面量」作为项讲解所有情形7的情况。

== 「可折叠」的值（Foldable）

先来看代码块。代码块其实就是一个脚本。既然是脚本，Typst就可以按照语句顺序依次执行「语句」。

#pro-tip[
  准确地来说，按照控制流顺序。
]

Typst按控制流顺序执行代码，将所有结果*折叠*成一个值。所谓折叠，就是将所有数值“连接”在一起。这样讲还是太抽象了，来看一些具体的例子。

=== 字符串折叠

Typst实际上不限制代码块的每个语句将会产生什么结果，只要是结果之间可以*折叠*即可。

我们说字符串是可以折叠的：

#code(```typ
#{"Hello"; " "; "World"}
```)

实际上折叠操作基本就是#mark("+")操作。那么字符串的折叠就是在做字符串连接操作：

#code(```typ
#("Hello" + " " + "World")
```)

再看一个例子：

#code(```typ
#{
  let hello = "Hello";
  let space = " ";
  let world = "World";
  hello; space; world;
  let destroy = ", Destroy"
  destroy; space; world; "."
}
```)

如何理解将「变量声明」与表达式混写？

回忆前文。对了，「变量声明」表达式的结果为```typc none```。
#code(```typ
#type(let hello = "Hello")
```)

并且还有一个重点是，字符串与`none`相加是字符串本身，`none`加`none`还是`none`：

#code(```typ
#("Hello" + none), #(none + "Hello"), #repr(none + none)
```)

现在可以重新体会这句话了：Typst按控制流顺序执行代码，将所有结果*折叠*成一个值。对于上例，每句话的执行结果分别是：

```typc
#{
  none; // let hello = "Hello";
  none; // let space = " ";
  none; // let world = "World";
  "Hello"; " "; "World"; // hello; space; world;
  none; // let destroy = ", Destroy"
  ", Destroy"; " "; "Wrold"; "." // destroy; space; world; "."
}
```

将结果收集并“折叠”，得到结果：

#code(```typc
#(none + none + none + "Hello" + " " + "World" + none + ", Destroy" + " " + "Wrold" + ".")
```)

#pro-tip[
  还有其他可以折叠的值，例如，数组与字典也是可以折叠的：

  #code(```typ
  #for i in range(1, 5) { (i, i * 10) }
  ```)

  #code(```typ
  #for i in range(1, 5) { let d = (:); d.insert(str(i), i * 10); d }
  ```)
]

=== 其他基本类型的情况

那么为什么说折叠操作基本就是#mark("+")操作。那么就是说有的“#mark("+")操作”并非是折叠操作。

布尔值、整数和浮点数都不能相互折叠：

```typ
// 不能编译
#{ false; true }; #{ 1; 2 }; #{ 1.; 2. }
```

那么是否说布尔值、整数和浮点数都不能折叠呢。答案又是否认的，它们都可以与```typc none```折叠（把下面的加号看成折叠操作）：

#code(```typ
#(1 + none)
```)

所以你可以保证一个代码块中只有一个「语句」产生布尔值、整数或浮点数结果，这样的代码块就又是能编译的了。让我们利用`let _ = `来实现这一点：

#code(```typ
#{ let _ = 1; true },
#{ let _ = false; 2. }
```)

回忆之前所讲的特殊规则：#term("placeholder")用作标识符的作用是“忽略不必要的语句结果”。

=== 内容折叠

Typst脚本的核心重点就在本段。

内容也可以作为代码块的语句结果，这时候内容块的结果是每个语句内容的“折叠”。

#code(```typ
#{
  [= 生活在Content树上]
  [现代社会以海德格尔的一句“一切实践传统都已经瓦解完了”为嚆矢。]
  [滥觞于家庭与社会传统的期望正失去它们的借鉴意义。]
  [但面对看似无垠的未来天空，我想循卡尔维诺“树上的男爵”的生活好过过早地振翮。]
}
```)

是不是感觉很熟悉？实际上内容块就是上述代码块的“糖”。所谓糖就是同一事物更方便书写的语法。上述代码块与下述内容块等价：

```typ
#[
= 生活在Content树上
现代社会以海德格尔的一句“一切实践传统都已经瓦解完了”为嚆矢。滥觞于家庭与社会传统的期望正失去它们的借鉴意义。但面对看似无垠的未来天空，我想循卡尔维诺“树上的男爵”的生活好过过早地振翮。
]
```

由于Typst默认以「标记模式」开始解释你的文档，这又与省略`#[]`的写法等价：

```typ
= 生活在Content树上
现代社会以海德格尔的一句“一切实践传统都已经瓦解完了”为嚆矢。滥觞于家庭与社会传统的期望正失去它们的借鉴意义。但面对看似无垠的未来天空，我想循卡尔维诺“树上的男爵”的生活好过过早地振翮。
```

#pro-tip[
  实际上有区别，由于多两个换行，前后各多一个Space Element。
]

// == Hello World程序

// 有的时候，我们想要访问字面量、变量与函数中存储的“信息”。例如，给定一个字符串```typc "Hello World"```，我们想要截取其中的第二个单词。

// 单词`World`就在那里，但仅凭我们有限的脚本知识，却没有方法得到它。这是因为字符串本身是一个整体，虽然它具备单词信息，我们却缺乏了*访问*信息的方法。

// Typst为我们提供了「成员」和「方法」两种概念访问这些信息。使用「方法」，可以使用以下脚本完成目标：

// #code(```typ
// #"Hello World".split(" ").at(1)
// ```)

// 为了方便讲解，我们改写出6行脚本。除了第二行，每一行都输出一段内容：

// #code(```typ
// #let x = "<NAME>"; #x \
// #let split = str.split
// #split(x, " ") \
// #str.split(x, " ") \
// #x.split(" ") \
// #x.split(" ").at(1)
// ```)

// 从```typ #x.split(" ").at(1)```的输出可以看出，这一行帮助我们实现了“截取其中的第二个单词”的目标。我们虽然隐隐约约能揣测出其中的意思：

// ```typ
// #(       x .split(" ")           .at(1)          )
// // 将字符串 根据字符串拆分  取出其中的第2个单词（字符串）
// ```

// 但至少我们对#mark(".")仍是一无所知。

// 本节我们就来讲解Typst中较为高级的脚本语法。这些脚本语法与大部分编程语言的语法相同，但是我们假设你并不知道这些语法。

== `none`类型和`if`语句 <grammar-if>

默认情况下，在逻辑上，Typst按照顺序执行执行你的代码，即先执行前面的语句，再执行后面的语句。开发者如果想要控制程序执行的流程，就必须使用流程控制的语法结构，主要是条件执行和循环执行。

`if`语句用于条件判断，满足条件时，就执行指定的语句。

```typ
#if expression { then-block } else { else-block }
#if expression { then-block }
```

上面式子中，表达式`expression`为真（值为布尔值`true`）时，就执行`then-block`代码块，否则执行`else-block`代码块。特别地，`else`可以省略。

如下所示：

#code(```typ
#if (1 < 2) { "确实" } else { "啊？" }
```)

因为`1 < 2`表达式为真，所以脚本仅执行了`then-block`代码块，于是最后文档的内容为“确实”。

`if`语句还可以无限串联下去，你可以自行类比推理更长的`if`语句的语义：<grammar-if-if>

```typ
#if expression { .. } else if expression { .. } else { .. }
#if expression { .. } else if expression { .. }
#if expression { .. } else if expression { .. } else if ..
```
如果只写了`then`代码块，而没写`else`代码块，但偏偏表达式不为真，最终脚本会报错吗？请看：

#code(```typ
#repr(if (1 > 2) { "啊？" })
```)

当`if`表达式没写`else`代码块而条件为假时，结果为`none`。“none”在中文里意思是“无”，表示“什么都没有”。同时再次强调`none`在「可折叠」的值中很重要的一个性质：`none`在折叠过程中被忽略。

见下程序，其根据数组所包含的值输出特定字符串：

#code(```typ
#let 查成分(成分数组) = {
  "是个"
  if "A" in 成分数组 or "C" in 成分数组 or "G" in 成分数组 { "萌萌" }
  if "T" in 成分数组 { "工具" }
  "人"
}

#查成分(()) \
#查成分(("A","T",)) \
```)

由于`if`也是表达式，你可以直接将`if`作为函数体，例如fibnacci函数的递归可以非常简单：

#code(```typ
#let fib(n) = if n <= 1 { n } else {
  fib(n - 1) + fib(n - 2)
}
#fib(46)
```)

== `while`语句 <grammar-while>

// if condition {..}
// if condition [..]
// if condition [..] else {..}
// if condition [..] else if condition {..} else [..]

`while`语句用于循环结构，满足条件时，不断执行循环体。

```typ
#while expression { cont-block }
```

上面代码中，如果表达式`expression`为真，就会执行`cont-block`代码块，然后再次判断`expression`是否为假；如果`expression`为假就跳出循环，不再执行循环体。

#code(```typ
#let i = 0;
#while i < 10 { (i * 2, ); i += 1 }
```)

上面代码中，循环体会执行`10`次，每次将`i`增加`1`，直到等于`10`才退出循环。

== `for`语句 <grammar-for>

`for`语句也是常用的循环结构，它迭代访问某个对象的每一项。

```typ
#for X in A { cont-block }
```

上面代码中，对于`A`的每一项，都执行`cont-block`代码块。在执行`cont-block`时，项的内容是`X`。例如以下代码做了与之前循环相同的事情：

#code(```typ
#for i in range(10) { (i * 2, ) }
```)

其中`range(10)`创建了一个内容为`(0, 1, 2, .., 9)`一共10个值的数组。

== 使用内容块替代代码块

所有可以使用代码块的地方都可以使用内容块作为替代。

#code(```typ
#for i in range(4) [阿巴]......
```)

== 使用`for`遍历字典

与数组相同，同理所有字典也都可以使用`for`遍历。此时，在执行`cont-block`时，Typst将每个键值对以数组的形式交给你。键值对数组的第0项是键，键值对数组的第1项是对应的值。

#code(```typ
#let cat = (neko-mimi: 2, "utterance": "喵喵喵", attribute: [kawaii\~])
#for i in cat {
  [猫猫的 #i.at(0) 是 #i.at(1)\ ]
}
```)

你可以同时使用「解构赋值」让代码变得更容易阅读：<grammar-for-destruct>

```typ
#let cat = (neko-mimi: 2, "utterance": "喵喵喵", attribute: [kawaii\~])
#for (特色, 这个) in cat [猫猫的 #特色 是 #这个\ ]
```

== `break`语句和`continue`语句 <grammar-break>

无论是`while`还是`for`，都可以使用`break`跳出循环，或`continue`直接进入下一次执行。

基于以下`for`循环，我们探索`break`和`continue`语句的作用。

#code(```typ
#for i in range(10) { (i, ) }
```)

在第一次执行时，如果我们直接使用`break`跳出循环，但是在break之前就已经产生了一些值，那么`for`的结果是`break`前的那些值的「折叠」。

#code(```typ
#for i in range(10) { (i, ); (i + 1926, ); break }
```)

特别地，如果我们直接使用`break`跳出循环，那么`for`的结果是*`none`*。

#code(```typ
#for i in range(10) { break }
```)

在`break`之后的那些值将会被忽略：

#code(```typ
#for i in range(10) { break; (i, ); (i + 1926, ); }
```)

以下代码将收集迭代的所有结果，直到`i >= 5`：
#code(```typ
#for i in range(10) {
  if i >= 5 { break }
  (i, )
}
```)

// #for 方位 in ("东", "南", "西", "北", "中", "间", "东北", "西北", "东南", "西南") [鱼戏莲叶#方位，]

`continue`有相似的规则，便不再赘述。我们举一个例子，以下程序输出在`range(10)`中不是偶数的数字：<grammar-continue>

#code(```typ
#let 是偶数(i) = calc.even(i)
#for i in range(10) {
  if 是偶数(i) { continue }
  (i, )
}
```)

事实上`break`语句和`continue`语句还可以在参数列表中使用，但本书非常不推荐这些写法，因此也不多做介绍：

#code(```typ
#let add(a, b, c) = a + b + c
#while true { add(1, break, 2) }
```)

== 控制函数返回值

你可以通过多种方法控制函数返回值。

=== 占位符 <grammar-placeholder>

早在上节我们就学习过了占位符，这在编写函数体表达式的时候尤为有用。你可以通过占位符忽略不需要的函数返回值。

以下函数获取数组的倒数第二个元素：

#code(```typ
#let last-two(t) = {
  let _ = t.pop()
  t.pop()
}
#last-two((1, 2, 3, 4))
```)

=== `return`语句 <grammar-return>

你可以通过`return`语句忽略表达式其余*所有语句*的结果，而使用`return`语句返回特定的值。

以下函数获取数组的倒数第二个元素：

#code(```typ
#let last-two(t) = {
  t.pop()
  return t.pop()
}
#last-two((1, 2, 3, 4))
```)
