<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet href="/pretty-feed-v3.xsl" type="text/xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Luyu Huang&#39;s Blog</title>
  <icon>https://luyuhuang.tech/img/avatar.png</icon>
  <subtitle>Stay hungry. Stay foolish.</subtitle>
  <link href="https://luyuhuang.tech/feed.xml" rel="self"/>
  <link href="https://luyuhuang.tech/"/>
  <updated>2026-01-28T15:07:36.691Z</updated>
  <id>https://luyuhuang.tech/</id>
  
  <author>
    <name>Luyu Huang</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>程序员是什么阶级</title>
    <link href="https://luyuhuang.tech/2026/01/28/programmer-class.html"/>
    <id>https://luyuhuang.tech/2026/01/28/programmer-class.html</id>
    <published>2026-01-27T16:00:00.000Z</published>
    <updated>2026-01-28T15:07:36.691Z</updated>
    
    <content type="html"><![CDATA[<p>我最近在读《毛泽东选集》。毛主席教导我们说，要将理论与实践相结合。那么就我所从事的行业，根据我观察到的事实，结合我学到的理论知识，分析一下程序员的阶级性。</p>
<p>传统制造业需要厂房和机器这样的生产资料，这些生产资料一般的工人不可能拥有。工人要生产，就必须被企业雇佣。
而软件开发却不同，基本上只需要一台普普通通的电脑，几千块钱，几乎人人都能拥有。也就是说，大多数情况下，一个程序员独立拥有开发软件的能力。事实上，早年也存在不少由一个或几个程序员开发的独立软件。那么从这个角度看，程序员应该是类似于小手工业者的小资产阶级。</p>
<p>但是我们身处一个资本主义高度发达的世界。十几年前智能手机的普及、几十年前个人电脑的普及，造就了一个巨大的软件消费市场。面对这个潜力巨大、尚未开发的市场，金融资本虎视眈眈。他们绝不会让独立软件开发者从两三人的小团队开始，通过挣得的利润慢慢扩大规模、完成原始积累。他们会大量砸钱，帮助创业者孵化项目，给创业公司估值，给它们投资，帮助它们快速扩大规模，为的是获得它们的股权，从而获得这个行业的利润——也就是产业工人的剩余价值。若不这样做，这个新兴市场的利润就与他们无关了。这样一来，软件行业迅速地产业化、规模化，诞生一大批颇具规模的软件公司。</p>
<p>具有一定规模的软件公司能够凭借规模效应，快速抢占市场。这样一来，独立开发者开发的产品难以同企业中几十人甚至几百人的团队开发的产品竞争。
如果某个品类尚无产品存在，拥有庞大资金的企业一定会迅速组建团队，以最快的速度研发出产品，以便快速抢占市场。如果独立开发者的创新产品开创了一个新品类，在市场上取得了一定成绩，那么企业也会立刻组建团队跟进，拆解、模仿、改进独立开发者的产品，推出自己的产品与之竞争。在大多数情况下，企业的产品会打败独立开发者的产品；或者企业直接出资收购独立开发者的团队。越大的企业，在竞争中往往越有优势。于是在短短二十多年的时间内，市场已经被各大企业瓜分完毕，独立开发者基本没有生存空间。</p>
<p>对于软件企业来说，要开发一款有竞争力的产品，需要组建一支几十人甚至几百人的团队，开发两三年甚至四五年，然后才能发行并盈利。这期间需要承担这支团队所有人的工资；且软件发行需要一大笔宣发费用。而且软件发行后不一定能盈利，这需要企业有一定的风险承担能力。所以，软件企业通常需要庞大的资本，这其中主要是用于购买劳动力的可变资本，而不是电脑、服务器这样的固定资本。</p>
<p>普通程序员虽然拥有开发软件的生产资料，但是基本上没什么用，只能被企业雇佣。这就像在工业时代，小手工业者，如木匠、裁缝，基本上没有独立生产的可能。他们要谋生，只能进厂打工，成为工人阶级。</p>
<p>在过去十几年的移动互联网浪潮中，有一部分程序员创业成功，赚了很多钱，实现阶级跃升，成为资产阶级。他们当中一小部分人当了老板；但是更多的人，虽然创业成功，但没能一直当老板。因为这是一个资本主义高度发达的世界，如果你的创业团队真的有潜力，金融资本会想方设法收购你的公司，反正他们有的是钱。如果你不想被收购，反而有倒闭的风险——可能有接受了投资的竞争对手开始迅速扩大规模，挤压你的市场。事实上很多创业者本质上就是炒作概念，讲故事，提高估值，然后卖掉套现离场。这些人能赚到一大笔钱，然后把钱投入房地产和金融市场，希望由此实现“财富自由”。但房价会下跌，投资可能会亏钱。如果投资赚的钱不够花，还得打工贴补一部分，就只是小资产阶级。如果投资不能稳定赚钱，那么也随时可能从资产阶级跌落。</p>
<p>在市场开发之初，垄断尚未形成，行业还存在大量创业机会。一旦创业成功，就能赚大钱，很多人都想找机会去创业。我称之为“创业机会主义”。再加上行业的快速发展与技术人才供应不足的矛盾，优秀程序员有一定的稀缺性。为了留住人才，让他们不要去创业单干，企业愿意同他们分享一部分利润。因此成功项目的程序员，往往能分到可观的奖金。这样一来，他们便具有小资产阶级的性质，因为分享了一部分利润。</p>
<p>但是事情正在起变化。随着产业的发展，垄断正在逐渐形成，市场逐渐被各大企业瓜分，创业机会逐渐减少；再加上产业发展放缓，人才不再稀缺，程序员的奖金分红也会逐渐减少。这样一来，程序员的小资产阶级性质会逐渐消失，最终成为单纯出卖劳动力的无产阶级。</p>
<p>AI 的出现带来了更加不确定的变化。一方面来说，AI
会带来新的市场和新的创业机会，相关领域的从业人员可能因此暂时成为小资产阶级。但在另一方面，AI
正在威胁程序员的岗位。让 AI
参与编程工作，会极大提高开发效率，产业的劳动密集型的现状可能因此改变，就像流水线工厂代替旧的劳动密集型工坊。由于训练
AI 需要高性能 GPU，这可能是一般工人无法拥有的。因此在可预见的未来，高阶
AI
可能像流水线机器一样，成为资本集团才能拥有的生产资料。如果是这样的话，普通程序员绝无可能保住小资产阶级的地位。</p>
<p>总结一下。程序员本是类似于小手工业者的小资产阶级。但是随着产业的发展，独立开发者难以与企业竞争，只能被企业雇佣。但成为工人的程序员并非完全是无产阶级，因为市场开发之初的“创业机会主义”和由此带来的分红，很多程序员都有小资产阶级的性质。然而随着市场逐渐被垄断集团瓜分，程序员的小资产阶级性质会逐渐消失，最终成为无产阶级。创业成功、当上老板的程序员自然是资产阶级。被收购、套现离场的创业者的阶级性质取决于资产状况，但是他们的阶级地位并不稳固。AI
的出现带来新的变化：新的市场会带来新的机会；但高阶 AI
可能成为普通人无法拥有的生产资料，普通程序员可能因此沦为真正的无产阶级。</p>
]]></content>
    
    
    <summary type="html">我最近在读《毛泽东选集》。毛主席教导我们说，要将理论与实践相结合。那么就我所从事的行业，根据我观察到的事实，结合我学到的理论知识，分析一下程序员的阶级性。
传统制造业需要厂房和机器这样的生产资料，这些生产资料一般的工人不可能拥有。工人要生产，就必须被企业雇佣。
而软件开发却不同，基本上只需要一台普普</summary>
    
    
    
    
    <category term="essays" scheme="https://luyuhuang.tech/tags/essays/"/>
    
  </entry>
  
  <entry>
    <title>组了一台 NAS</title>
    <link href="https://luyuhuang.tech/2026/01/08/diy-nas.html"/>
    <id>https://luyuhuang.tech/2026/01/08/diy-nas.html</id>
    <published>2026-01-07T16:00:00.000Z</published>
    <updated>2026-01-08T15:58:42.635Z</updated>
    
    <content type="html"><![CDATA[<p>我最近组了一台 NAS，用于同步手机上的相册、自建云盘存储文件、同步
Obsidian 的笔记、存储和管理影音资源、搭建 Git
托管代码等。这篇文章记录我是怎么做的。</p>
<p><img src="/assets/images/diy-nas_1.jpg" width="400" ></p>
<h2 id="硬件">硬件</h2>
<p>主板选用云星 CS246，一款专门面向 NAS 的 ITX 小主板。它有 LGA 1151 CPU
插槽、2 个 2.5G 网卡（可加钱升级成 4 个）、8 个 SATA 3.0 接口、两个 NVMe
插槽，很适合用来做 NAS。花费￥429。</p>
<p>CPU 买了个 i3 9100T 二手散片。4 核 4 线程，带核显，可以硬解码，做 NAS
基本够用；TDP 35w，比较省电。花费￥135。</p>
<p>内存条选用七彩虹 DDR4 2666 8GB。说实话 8G
有点小，但是考虑到我的腾讯云服务器内存才 1G，苦日子过惯了，8G
凑合着也能用。花费￥119。</p>
<p>固态硬盘也是七彩虹的，CN600 128GB。Linux 系统盘 64G
都绰绰有余了，不过现在能买到的 NVMe 硬盘最小也有 128G。花费￥115。</p>
<p>机箱选的是疾风知 N52 NAS 机箱。它有 5
个盘位，硬盘仓支持安装风扇给硬盘散热。选它是因为我觉得它比较好看，而且相对便宜。花费￥229。</p>
<p>电源选了个 TT TRM SFX 350W。花费￥159。</p>
<p>散热器选了个铜芯铝鳍片的下压式散热器，花费￥27。硬盘仓风扇买的是利民的
12CM 无光静音风扇￥17。</p>
<p>总共花费￥1230。算下来，似乎并不是很划算😂。但自组 NAS 图的就是 DIY
的乐趣，为的是自由定制的权力，便不计较这个了。</p>
<h2 id="系统">系统</h2>
<p>系统安装的是 Ubuntu 24.04.3 LTS。我理解的 NAS 就是一台 Linux
服务器，需要什么服务就在上面装就好了，这样最能自由定制，因此没有选择飞牛之类的
NAS 专用系统。主板装好后，插入安装 U
盘试下能否点亮。成功点亮后，顺便也把系统装好了。</p>
<p><img src="/assets/images/diy-nas_2.png" width="400" ></p>
<h2 id="硬盘">硬盘</h2>
<p>我本想用两块硬盘组一个 RAID 1，但是现在机械硬盘涨价严重，就只买一块 4
TB
的西数红盘。反正一块硬盘一时半会儿坏不了，等后面硬盘降价了（希望），再组成
RAID 1。</p>
<p>为了后续扩展成 RAID 1，我把这块 4 TB 的硬盘组成一个单盘的 RAID
1。首先使用 parted 创建分区，执行 <code>sudo parted /dev/sda</code> 进入
parted 交互界面，然后依次执行</p>
<pre><code class="hljs txt">(parted) mklabel gpt                # 创建分区表<br>(parted) mkpart primary 0% 100%     # 创建主分区<br>(parted) print                      # 打印查看下分区表<br>(parted) set 1 raid on              # 标记为 raid 分区<br></code></pre>
<p>然后使用 mdadm 创建 raid：</p>
<pre><code class="hljs sh">sudo mdadm --verbose --create /dev/md0 --level=1 --raid-devices=1 --force /dev/sda1<br>sudo mkfs.ext4 /dev/md0<br></code></pre>
<p>然后挂载到 <code>/data</code>：</p>
<pre><code class="hljs sh">sudo <span class="hljs-built_in">mkdir</span> /data<br>sudo mount /dev/md0 /data<br><br><span class="hljs-comment"># 挂载持久化</span><br>sudo blkid | grep /dev/md0 <span class="hljs-comment"># 查看一下 UUID</span><br><span class="hljs-built_in">echo</span> UUID=ae65b749-4e1e-4209-ae57-15ec83578c5c /data ext4 defaults 0 0 | sudo <span class="hljs-built_in">tee</span> -a /etc/fstab <span class="hljs-comment"># 配置写入 /etc/fstab</span><br></code></pre>
<p>Mdadm 的配置最好也持久化一下：</p>
<pre><code class="hljs sh">sudo mdadm --detail --scan | sudo <span class="hljs-built_in">tee</span> -a /etc/mdadm/mdadm.conf<br>sudo update-initramfs -u<br></code></pre>
<p>以后如果要加硬盘，可以执行以下命令即可：</p>
<pre><code class="hljs sh">sudo mdadm --grow /dev/md0 --raid-devices=2 --add /dev/sdb1<br></code></pre>
<h2 id="网络环境">网络环境</h2>
<p>我对网络环境有几点要求。第一，不能只在内网访问，在外面也要能访问。第二，要用域名访问，用不同的域名区分不同的服务，而不是用
IP +
端口。第三，同一个服务在内网和外网的访问地址是一样的，这样回到家后不需要修改任何配置。</p>
<h3 id="tailscale-实现内网穿透">Tailscale 实现内网穿透</h3>
<p>为了实现内网穿透，我的方案是使用 <a href="https://tailscale.com/">Tailscale</a>。Tailscale 是一个基于
WireGuard 实现的 VPN，通过 UDP
打洞实现内网穿透。如果打洞成功，则可以直连通信，无需经由中继服务器；通信速度取决于带宽，非常方便。</p>
<p>Tailscale 非常容易使用。在机器上安装好 Tailscale 后，执行
<code>tailscale up</code>，访问链接登录账号，即可将当前主机加入这个账号下的
Tailscale 网络 (Tailnet)。</p>
<pre><code class="hljs awk">$ sudo tailscale up<br><br>To authenticate, visit:<br><br>        https:<span class="hljs-regexp">//</span>login.tailscale.com<span class="hljs-regexp">/a/</span>******<br></code></pre>
<p>同时手机上也安装 Tailscale 客户端，登录账号后，也可以加入我的
tailnet。这样手机和 NAS 便可以随时通信。</p>
<p><img src="/assets/images/diy-nas_3.jpg" width="300" ></p>
<p>NAT 打洞也不是总能成功。根据 Tailscale 官方文档，如果通信双方都在
Hard NAT 中，则不能实现直连。就需要通过中继服务器（称为
DERP），速度很慢。</p>
<table>

<thead>
<tr class="header">
<th><strong>Client A NAT Type</strong></th>
<th><strong>Client B NAT Type</strong></th>
<th><strong>Expected Connection</strong></th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>No NAT</td>
<td>No NAT</td>
<td>Direct</td>
</tr>
<tr class="even">
<td>No NAT</td>
<td>Easy NAT</td>
<td>Direct</td>
</tr>
<tr class="odd">
<td>No NAT</td>
<td>Hard NAT</td>
<td>Direct</td>
</tr>
<tr class="even">
<td>Easy NAT</td>
<td>Easy NAT</td>
<td>Direct</td>
</tr>
<tr class="odd">
<td>Easy NAT</td>
<td>Hard NAT</td>
<td>Relayed</td>
</tr>
<tr class="even">
<td>Hard NAT</td>
<td>Hard NAT</td>
<td>Relayed</td>
</tr>
</tbody>
</table>
<p>好在现在宽带基本上都提供 ipv6
公网地址。我在光猫上开启允许内网设备使用 ipv6 通信，让 NAS 拥有公网 ipv6
地址。这样，使用 4G/5G 网络能 100% 直连；在外面连的 WIFI 如果支持
ipv6，也能直连。</p>
<h3 id="配置-dns-服务器">配置 DNS 服务器</h3>
<p>我的目标是在家里连上家里的 WiFi，或者在外连上
VPN，都可以用同样的域名访问 NAS 上的服务。这首先要在 NAS 上部署一个 DNS
服务器。我使用的是 <a href="https://coredns.io/">CoreDNS</a>，它的配置非常简单：</p>
<pre><code class="hljs stylus">.:<span class="hljs-number">53</span> &#123;<br>  bind <span class="hljs-number">192.168</span>.<span class="hljs-number">1.2</span><br>  hosts &#123;<br>    <span class="hljs-number">192.168</span>.<span class="hljs-number">1.2</span> nas<span class="hljs-selector-class">.luyuhuang</span><span class="hljs-selector-class">.tech</span><br>    <span class="hljs-number">192.168</span>.<span class="hljs-number">1.2</span> immich<span class="hljs-selector-class">.luyuhuang</span><span class="hljs-selector-class">.tech</span><br><br>    ttl <span class="hljs-number">60</span><br>    reload <span class="hljs-number">1</span>m<br>    fallthrough<br>  &#125;<br><br>  forward . /etc/resolv<span class="hljs-selector-class">.conf</span><br>  cache <span class="hljs-number">120</span><br>  reload <span class="hljs-number">6s</span><br>  errors<br>&#125;<br></code></pre>
<p>这样的配置让 CoreDNS 监听 <code>192.168.1.2:58</code>，在
<code>hosts</code> 中配置域名解析规则。其他域名默认 forward 到系统 DNS
解析。在路由器中将 NAS 的 IP 地址固定为
<code>192.168.1.2</code>，并将首选 DNS 服务器设置为
<code>192.168.1.2</code>。这样任何通过我家路由器上网的设备都可以通过域名访问
NAS 上的服务。</p>
<p>为了让连上 VPN 的设备也能用同样的域名访问 NAS，我们在 Tailscale
的控制台设置一个 Split DNS，地址设置为 NAS 的虚拟 IP；并指定后缀为
<code>luyuhuang.tech</code> 的域名请求 NAS 上的 DNS 服务器。</p>
<p><img src="/assets/images/diy-nas_4.png" ></p>
<p>由于 NAS 的虚拟 IP 和物理局域网的 IP 不同，所以还要让 CoreDNS
监听虚拟 IP，解析结果也要返回虚拟 IP。</p>
<pre><code class="hljs stylus">.:<span class="hljs-number">53</span> &#123;<br>  bind <span class="hljs-number">100.64</span>.<span class="hljs-number">0.3</span><br>  hosts &#123;<br>    <span class="hljs-number">100.64</span>.<span class="hljs-number">0.3</span> nas<span class="hljs-selector-class">.luyuhuang</span><span class="hljs-selector-class">.tech</span><br>    <span class="hljs-number">100.64</span>.<span class="hljs-number">0.3</span> immich<span class="hljs-selector-class">.luyuhuang</span><span class="hljs-selector-class">.tech</span><br><br>    ttl <span class="hljs-number">60</span><br>    reload <span class="hljs-number">1</span>m<br>    fallthrough<br>  &#125;<br><br>  forward . /etc/resolv<span class="hljs-selector-class">.conf</span><br>  cache <span class="hljs-number">120</span><br>  reload <span class="hljs-number">6s</span><br>  errors<br>&#125;<br></code></pre>
<p>这样，例如同样是访问
<code>nas.luyuhuang.tech</code>，内网设备解析得到
<code>192.168.1.2</code>，连上 VPN 的设备则得到虚拟 IP
<code>100.64.0.3</code>。它们都可以用同样的域名访问 NAS。</p>
<h3 id="配置-nginx-和-https">配置 Nginx 和 HTTPS</h3>
<p>我是用 Nginx 作为所有服务的反向代理，这样可以统一使用 80 或 443
端口，但使用不同的域名区分不同的服务。为了简单我直接使用 Docker 版的
Nginx，简单写个 <code>docker-compose.yaml</code>
即可。因为要反向代理到各种服务，网络模式使用 host 模式。</p>
<pre><code class="hljs yaml"><span class="hljs-attr">services:</span><br>  <span class="hljs-attr">nginx:</span><br>    <span class="hljs-attr">image:</span> <span class="hljs-string">nginx</span><br>    <span class="hljs-attr">container_name:</span> <span class="hljs-string">nginx</span><br>    <span class="hljs-attr">network_mode:</span> <span class="hljs-string">host</span><br>    <span class="hljs-attr">volumes:</span><br>      <span class="hljs-bullet">-</span> <span class="hljs-string">./config:/etc/nginx</span><br>      <span class="hljs-bullet">-</span> <span class="hljs-string">./cert:/cert</span><br>    <span class="hljs-attr">restart:</span> <span class="hljs-string">unless-stopped</span><br></code></pre>
<p>我的域名 <code>luyuhuang.tech</code> 申请了 Let’s Encrypt
的证书，因此可以直接使用这个证书部署 HTTPS 服务。简单写个脚本同步证书到
NAS 上，这样也不用担心证书过期。</p>
<p>这样服务配置起来就很简单了。例如我的 Homepage 服务（下文会介绍）监听
<code>http://127.0.0.1:4000</code>，Nginx 这边就直接反代过去：</p>
<pre><code class="hljs nginx"><span class="hljs-section">server</span> &#123;<br>    <span class="hljs-attribute">listen</span>       <span class="hljs-number">80</span>;<br>    <span class="hljs-attribute">server_name</span>  nas.luyuhuang.tech;<br><br>    <span class="hljs-section">location</span> / &#123;<br>        <span class="hljs-attribute">return</span> <span class="hljs-number">301</span> https://nas.luyuhuang.tech<span class="hljs-variable">$request_uri</span>;<br>    &#125;<br>&#125;<br><br><span class="hljs-section">server</span> &#123;<br>    <span class="hljs-attribute">listen</span>       <span class="hljs-number">443</span> ssl;<br>    <span class="hljs-attribute">server_name</span>  nas.luyuhuang.tech;<br><br>    <span class="hljs-attribute">include</span> /etc/nginx/ssl.conf;<br><br>    <span class="hljs-section">location</span> / &#123;<br>        <span class="hljs-attribute">proxy_pass</span> http://127.0.0.1:4000;<br>        <span class="hljs-attribute">proxy_set_header</span> Host nas.luyuhuang.tech;<br>    &#125;<br>&#125;<br></code></pre>
<h2 id="安装各种服务">安装各种服务</h2>
<p>这里介绍几个我的 NAS 上安装的服务。这些服务都是用 Docker
安装的，安装步骤都很简单，这里就不介绍了。</p>
<h3 id="immich">Immich</h3>
<p><a href="https://immich.app/">Immich</a> 是一个自建相册服务。它自带
Android 和 iOS 手机客户端，可以将手机的相册同步到 NAS 上。实际上我这次搞
NAS 的导火索就是我的小米云相册的容量满了。它支持 AI
人脸识别、以文搜图的功能（需要 GPU
支持）；支持地图足迹功能。我用下来体验还是很不错的。</p>
<p><img src="/assets/images/diy-nas_5.png" ></p>
<h3 id="cloudreve">Cloudreve</h3>
<p><a href="https://cloudreve.org/">Cloudreve</a>
是自建网盘服务。它的网页端功能很不错，大多数格式的文件，无论是图片、音乐、视频，还是文档、压缩包，都可以直接在网页端打开。它支持
WebDAV，我用来作为笔记软件的同步服务；支持离线下载，我用来下载影音资源。它还支持各种云服务的对象存储，不过我暂时还没用到。缺点是没有
Android 客户端，iOS 客户端则需收费。</p>
<p><img src="/assets/images/diy-nas_6.png" ></p>
<h3 id="qbittorrent">qBittorrent</h3>
<p><a href="https://www.qbittorrent.org/">qBittorrent</a>
不用介绍了吧，著名的 Bittorrent 下载器。它可以与 Cloudreve 联动，作为
Cloudreve 的离线下载器。在 Cloudreve 中创建离线下载任务，调用
qBittorrent 下载，下载完成自动存入 Cloudreve 网盘。</p>
<p><img src="/assets/images/diy-nas_7.png" ></p>
<h3 id="jellyfin">Jellyfin</h3>
<p><a href="https://jellyfin.org/">Jellyfin</a>
是一个影视资源库。它将影视资源整理成影片和剧集，并从影视数据库中拉取海报、标题、介绍等元数据，方便观看。它能保存观看进度，并支持服务器解码（硬解码需要
GPU 支持），方便在各种设备中观看。除了网页端和 PC
客户端，它还支持手机客户端和 TV 客户端。</p>
<p><img src="/assets/images/diy-nas_8.png" ></p>
<p>我让 Jellyfin 通过 WebDAV 挂载
Cloudreve，使用其中的影视资源。Jellyfin 本身不支持远程挂载，我使用 <a href="https://rclone.org/docker/">Rclone 的 Docker 插件</a>，让 Docker
将 WebDAV 链接挂载成 Jellyfin 所在容器的 volume。</p>
<pre><code class="hljs yaml"><span class="hljs-attr">volumes:</span><br>  <span class="hljs-attr">cloudreve:</span><br>    <span class="hljs-attr">driver:</span> <span class="hljs-string">rclone</span><br>    <span class="hljs-attr">driver_opts:</span><br>      <span class="hljs-attr">remote:</span> <span class="hljs-string">&#x27;cloudreve:&#x27;</span><br>      <span class="hljs-attr">read_only:</span> <span class="hljs-string">&#x27;true&#x27;</span><br>      <span class="hljs-attr">dir_cache_time:</span> <span class="hljs-string">&#x27;1s&#x27;</span><br></code></pre>
<h3 id="homepage">Homepage</h3>
<p>最后我还部署了 <a href="https://gethomepage.dev/">Homepage</a>，将
NAS
上所有的服务都集中到一个主页，顺便还能显示机器的基本状态、各个服务的运行状态。</p>
<p><img src="/assets/images/diy-nas_9.png" ></p>
]]></content>
    
    
    <summary type="html">我最近组了一台 NAS，用于同步手机上的相册、自建云盘存储文件、同步
Obsidian 的笔记、存储和管理影音资源、搭建 Git
托管代码等。这篇文章记录我是怎么做的。

硬件
主板选用云星 CS246，一款专门面向 NAS 的 ITX 小主板。它有 LGA 1151 CPU
插槽、2 个 2.5G</summary>
    
    
    
    
    <category term="practice" scheme="https://luyuhuang.tech/tags/practice/"/>
    
  </entry>
  
  <entry>
    <title>新一代排版系统 Typst 介绍</title>
    <link href="https://luyuhuang.tech/2025/07/27/typst.html"/>
    <id>https://luyuhuang.tech/2025/07/27/typst.html</id>
    <published>2025-07-26T16:00:00.000Z</published>
    <updated>2025-07-27T15:02:21.237Z</updated>
    
    <content type="html"><![CDATA[<p>一直以来文字排版都是一项复杂的工作。计算机出现不久后，人们就尝试用计算机取代铅字处理排版工作。现在计算机上的排版工具有很多。Microsoft
Office Word
可能是使用最广泛的排版工具。它容易上手，功能丰富，能够满足绝大多数办公场景。缺点是文件格式私有，价格昂贵；面对一些复杂排版需求（如公式排版）力不从心。随着
web 技术的发展，HTML + CSS 也可以作为排版工具；Markdown 这种可以编译成
HTML 的简易标记语言广泛应用于网络文字排版（本站的文章都是用 Markdown
写的）。但是 web
技术主要服务于网页设计，缺失换行、分页算法，也难以应对复杂排版。由著名计算机科学家、图灵奖获得者
Donald E. Knuth 发明的 <span class="math inline"><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.488ex;" xmlns="http://www.w3.org/2000/svg" width="4.294ex" height="2.033ex" role="img" focusable="false" viewBox="0 -683 1898 898.5"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="TeXAtom" data-mjx-texclass="ORD"><g data-mml-node="mi"><path data-c="54" d="M36 443Q37 448 46 558T55 671V677H666V671Q667 666 676 556T685 443V437H645V443Q645 445 642 478T631 544T610 593Q593 614 555 625Q534 630 478 630H451H443Q417 630 414 618Q413 616 413 339V63Q420 53 439 50T528 46H558V0H545L361 3Q186 1 177 0H164V46H194Q264 46 283 49T309 63V339V550Q309 620 304 625T271 630H244H224Q154 630 119 601Q101 585 93 554T81 486T76 443V437H36V443Z"></path></g><g data-mml-node="mspace" transform="translate(722,0)"></g><g data-mml-node="mpadded" transform="translate(582,0)"><g transform="translate(0,-215.5)"><g data-mml-node="TeXAtom" data-mjx-texclass="ORD"><g data-mml-node="mi"><path data-c="45" d="M128 619Q121 626 117 628T101 631T58 634H25V680H597V676Q599 670 611 560T625 444V440H585V444Q584 447 582 465Q578 500 570 526T553 571T528 601T498 619T457 629T411 633T353 634Q266 634 251 633T233 622Q233 622 233 621Q232 619 232 497V376H286Q359 378 377 385Q413 401 416 469Q416 471 416 473V493H456V213H416V233Q415 268 408 288T383 317T349 328T297 330Q290 330 286 330H232V196V114Q232 57 237 52Q243 47 289 47H340H391Q428 47 452 50T505 62T552 92T584 146Q594 172 599 200T607 247T612 270V273H652V270Q651 267 632 137T610 3V0H25V46H58Q100 47 109 49T128 61V619Z"></path></g></g></g></g><g data-mml-node="mspace" transform="translate(1263,0)"></g><g data-mml-node="mi" transform="translate(1148,0)"><path data-c="58" d="M270 0Q252 3 141 3Q46 3 31 0H23V46H40Q129 50 161 88Q165 94 244 216T324 339Q324 341 235 480T143 622Q133 631 119 634T57 637H37V683H46Q64 680 172 680Q297 680 318 683H329V637H324Q307 637 286 632T263 621Q263 618 322 525T384 431Q385 431 437 511T489 593Q490 595 490 599Q490 611 477 622T436 637H428V683H437Q455 680 566 680Q661 680 676 683H684V637H667Q585 634 551 599Q548 596 478 491Q412 388 412 387Q412 385 514 225T620 62Q628 53 642 50T695 46H726V0H717Q699 3 591 3Q466 3 445 0H434V46H440Q454 46 476 51T499 64Q499 67 463 124T390 238L353 295L350 292Q348 290 343 283T331 265T312 236T286 195Q219 88 218 84Q218 70 234 59T272 46H280V0H270Z"></path></g></g></g></g></svg></mjx-container></span> 及其衍生品 <span class="math inline"><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.488ex;" xmlns="http://www.w3.org/2000/svg" width="5.788ex" height="2.108ex" role="img" focusable="false" viewBox="0 -716.3 2558.3 931.8"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="TeXAtom" data-mjx-texclass="ORD"><g data-mml-node="mi"><path data-c="4C" d="M128 622Q121 629 117 631T101 634T58 637H25V683H36Q48 680 182 680Q324 680 348 683H360V637H333Q273 637 258 635T233 622L232 342V129Q232 57 237 52Q243 47 313 47Q384 47 410 53Q470 70 498 110T536 221Q536 226 537 238T540 261T542 272T562 273H582V268Q580 265 568 137T554 5V0H25V46H58Q100 47 109 49T128 61V622Z"></path></g><g data-mml-node="mspace" transform="translate(625,0)"></g><g data-mml-node="mpadded" transform="translate(300,0)"><g transform="translate(0,210)"><g data-mml-node="TeXAtom" data-mjx-texclass="ORD"><g data-mml-node="mstyle" transform="scale(0.707)"><g data-mml-node="TeXAtom" data-mjx-texclass="ORD"><g data-mml-node="mi"><path data-c="41" d="M255 0Q240 3 140 3Q48 3 39 0H32V46H47Q119 49 139 88Q140 91 192 245T295 553T348 708Q351 716 366 716H376Q396 715 400 709Q402 707 508 390L617 67Q624 54 636 51T687 46H717V0H708Q699 3 581 3Q458 3 437 0H427V46H440Q510 46 510 64Q510 66 486 138L462 209H229L209 150Q189 91 189 85Q189 72 209 59T259 46H264V0H255ZM447 255L345 557L244 256Q244 255 345 255H447Z"></path></g></g></g></g></g></g><g data-mml-node="mspace" transform="translate(830.3,0)"></g><g data-mml-node="mi" transform="translate(660.3,0)"><path data-c="54" d="M36 443Q37 448 46 558T55 671V677H666V671Q667 666 676 556T685 443V437H645V443Q645 445 642 478T631 544T610 593Q593 614 555 625Q534 630 478 630H451H443Q417 630 414 618Q413 616 413 339V63Q420 53 439 50T528 46H558V0H545L361 3Q186 1 177 0H164V46H194Q264 46 283 49T309 63V339V550Q309 620 304 625T271 630H244H224Q154 630 119 601Q101 585 93 554T81 486T76 443V437H36V443Z"></path></g><g data-mml-node="mspace" transform="translate(1382.3,0)"></g><g data-mml-node="mpadded" transform="translate(1242.3,0)"><g transform="translate(0,-215.5)"><g data-mml-node="TeXAtom" data-mjx-texclass="ORD"><g data-mml-node="mi"><path data-c="45" d="M128 619Q121 626 117 628T101 631T58 634H25V680H597V676Q599 670 611 560T625 444V440H585V444Q584 447 582 465Q578 500 570 526T553 571T528 601T498 619T457 629T411 633T353 634Q266 634 251 633T233 622Q233 622 233 621Q232 619 232 497V376H286Q359 378 377 385Q413 401 416 469Q416 471 416 473V493H456V213H416V233Q415 268 408 288T383 317T349 328T297 330Q290 330 286 330H232V196V114Q232 57 237 52Q243 47 289 47H340H391Q428 47 452 50T505 62T552 92T584 146Q594 172 599 200T607 247T612 270V273H652V270Q651 267 632 137T610 3V0H25V46H58Q100 47 109 49T128 61V619Z"></path></g></g></g></g><g data-mml-node="mspace" transform="translate(1923.3,0)"></g><g data-mml-node="mi" transform="translate(1808.3,0)"><path data-c="58" d="M270 0Q252 3 141 3Q46 3 31 0H23V46H40Q129 50 161 88Q165 94 244 216T324 339Q324 341 235 480T143 622Q133 631 119 634T57 637H37V683H46Q64 680 172 680Q297 680 318 683H329V637H324Q307 637 286 632T263 621Q263 618 322 525T384 431Q385 431 437 511T489 593Q490 595 490 599Q490 611 477 622T436 637H428V683H437Q455 680 566 680Q661 680 676 683H684V637H667Q585 634 551 599Q548 596 478 491Q412 388 412 387Q412 385 514 225T620 62Q628 53 642 50T695 46H726V0H717Q699 3 591 3Q466 3 445 0H434V46H440Q454 46 476 51T499 64Q499 67 463 124T390 238L353 295L350 292Q348 290 343 283T331 265T312 236T286 195Q219 88 218 84Q218 70 234 59T272 46H280V0H270Z"></path></g></g></g></g></svg></mjx-container></span>
因其强大的功能、美观的排版效果、优秀的换行分页算法、出色的公式排版能力、灵活的可定制性，一直以来是排版系统的黄金标准。但是由于年代久远，上手难度高、历史包袱众多。那么有没有什么功能强大、容易上手、开源免费的排版系统推荐一下呢？有的兄弟，有的。今天我要推荐的
<a href="https://typst.app/">Typst</a> 就是这样一款优秀的排版工具。</p>
<h2 id="overview">Overview</h2>
<p>来自柏林工业大学的计算机研究生 Martin Haug 和 Laurenz Mädje 不满足于
<span class="math inline"><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.488ex;" xmlns="http://www.w3.org/2000/svg" width="4.294ex" height="2.033ex" role="img" focusable="false" viewBox="0 -683 1898 898.5"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="TeXAtom" data-mjx-texclass="ORD"><g data-mml-node="mi"><path data-c="54" d="M36 443Q37 448 46 558T55 671V677H666V671Q667 666 676 556T685 443V437H645V443Q645 445 642 478T631 544T610 593Q593 614 555 625Q534 630 478 630H451H443Q417 630 414 618Q413 616 413 339V63Q420 53 439 50T528 46H558V0H545L361 3Q186 1 177 0H164V46H194Q264 46 283 49T309 63V339V550Q309 620 304 625T271 630H244H224Q154 630 119 601Q101 585 93 554T81 486T76 443V437H36V443Z"></path></g><g data-mml-node="mspace" transform="translate(722,0)"></g><g data-mml-node="mpadded" transform="translate(582,0)"><g transform="translate(0,-215.5)"><g data-mml-node="TeXAtom" data-mjx-texclass="ORD"><g data-mml-node="mi"><path data-c="45" d="M128 619Q121 626 117 628T101 631T58 634H25V680H597V676Q599 670 611 560T625 444V440H585V444Q584 447 582 465Q578 500 570 526T553 571T528 601T498 619T457 629T411 633T353 634Q266 634 251 633T233 622Q233 622 233 621Q232 619 232 497V376H286Q359 378 377 385Q413 401 416 469Q416 471 416 473V493H456V213H416V233Q415 268 408 288T383 317T349 328T297 330Q290 330 286 330H232V196V114Q232 57 237 52Q243 47 289 47H340H391Q428 47 452 50T505 62T552 92T584 146Q594 172 599 200T607 247T612 270V273H652V270Q651 267 632 137T610 3V0H25V46H58Q100 47 109 49T128 61V619Z"></path></g></g></g></g><g data-mml-node="mspace" transform="translate(1263,0)"></g><g data-mml-node="mi" transform="translate(1148,0)"><path data-c="58" d="M270 0Q252 3 141 3Q46 3 31 0H23V46H40Q129 50 161 88Q165 94 244 216T324 339Q324 341 235 480T143 622Q133 631 119 634T57 637H37V683H46Q64 680 172 680Q297 680 318 683H329V637H324Q307 637 286 632T263 621Q263 618 322 525T384 431Q385 431 437 511T489 593Q490 595 490 599Q490 611 477 622T436 637H428V683H437Q455 680 566 680Q661 680 676 683H684V637H667Q585 634 551 599Q548 596 478 491Q412 388 412 387Q412 385 514 225T620 62Q628 53 642 50T695 46H726V0H717Q699 3 591 3Q466 3 445 0H434V46H440Q454 46 476 51T499 64Q499 67 463 124T390 238L353 295L350 292Q348 290 343 283T331 265T312 236T286 195Q219 88 218 84Q218 70 234 59T272 46H280V0H270Z"></path></g></g></g></g></svg></mjx-container></span>
的繁琐和臃肿，决定开发一款新的排版工具，这便是 Typst。Typst
功能强大、效果出色，使用却非常简单，容易上手。Typst
是文本文件格式，后缀通常是 <code>.typ</code>。</p>
<pre><code class="hljs typst">= 这是标题<br><br>== 这是二级标题<br><br>为了展示Typst上手有多么简单，我们直接展示一段Typst代码。Typst的标记语法类似于Markdown：这是行内代码 `inline code`，这是*加粗*，这是斜体_italic_，这是公式 $e^(pi i) + 1 = 0$。<br><br>用空行表示分段。这是无序列表：<br><br>- 无序列表<br>- 用横杠 `-` 标记无序列表<br>  - 多层列表<br>  - 缩进表示多层列表<br><br>这是有序列表<br><br>+ 有序列表<br>+ 用加号 `+` 标记有序列表<br>  - 同样支持多层<br><br>```c<br>int main() {<br>  printf("三个反引号表示代码块，支持代码高亮\n");<br>  return 0;<br>}<br>```<br><br>Typst还是一门强大的编程语言。可以调用函数实现各种样式：#text(size: 15pt, fill: red)[调用`text`函数插入自定义样式的文本。]还有#highlight(fill: yellow)[`highlight`高亮]、#underline(stroke: 1.5pt + red)[`underline`下划线]、#strike[`strike`删除线]等。实际上Typst的标记语法大多数都是函数的简便写法。例如#strong[`strong`加粗]、斜体#emph[italic]<br><br>#heading(depth: 3)[这是三级标题]<br><br>等价于 `=== 这是三级标题`。通过自定义函数，可以实现复杂的自定义效果。<br><br>#let myStyle(content) = { // 定义函数<br>  let styled = text(size: 1.3em, fill: yellow, stroke: 0.3pt + red, font: "KaiTi", content)<br>  return underline(stroke: 1.2pt + blue, styled)<br>}<br><br>这就是#myStyle[调用自定义函数的效果]。<br></code></pre>
<p><img src="/assets/images/typst_1.svg" class="shadow"></p>
<h2 id="安装">安装</h2>
<p>Typst 的安装非常简单。如果你用 vscode，那么最简单的方法就是安装 <a href="https://marketplace.visualstudio.com/items?itemName=myriad-dreamin.tinymist">Tinymist</a>
插件。它是一个非常棒的 Typst 写作环境，能实现实时预览。</p>
<p><img src="/assets/images/typst_2.png"></p>
<p>也可以在<a href="https://github.com/typst/typst/releases">官网下载</a>下载软件本体。Typst
只有一个可执行文件，执行 <code>typst compile</code> 即可将 Typst
源文件编译成 pdf。</p>
<pre><code class="hljs sh">$ typst compile test.typ<br></code></pre>
<h2 id="语法">语法</h2>
<p>Typst
有三种模式：<strong>标记模式</strong>、<strong>数学模式</strong>和<strong>代码模式</strong>。默认为标记模式，使用类似
Markdown
的语法写作文本。数学模式用于编排数学公式；代码模式则用于实现各种可编程功能。这三种模式之间可以互相切换：</p>
<ul>
<li>使用 <code>#</code> 号切换到代码模式。<code>#</code>
后紧跟代码，直到整个语句结束都是代码模式。如果有歧义，可使用分号
<code>;</code> 标记语句结束。</li>
<li>使用 <code>[...]</code> 切换到标记模式。例如前面看到的
<code>#strong[`strong`加粗]</code>，方括号内便可使用标记语法；这个语句将标记文本传入
<code>strong</code> 函数获得加粗效果。</li>
<li>使用 <code>$...$</code> 切换到数学模式。</li>
</ul>
<h3 id="标记模式">标记模式</h3>
<p>如上面看到的，标记模式的语法与 Markdown
相似。基本上每个标记语法有对应的函数，后面我们介绍函数的用法。</p>
<table>

<thead>
<tr class="header">
<th style="text-align: left;">语法</th>
<th style="text-align: left;">含义</th>
<th style="text-align: left;">元素函数</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td style="text-align: left;"><code>= 标题</code></td>
<td style="text-align: left;">等号的数量表示标题的层级</td>
<td style="text-align: left;"><code>heading</code></td>
</tr>
<tr class="even">
<td style="text-align: left;"><code>*加粗*</code></td>
<td style="text-align: left;">加粗字体</td>
<td style="text-align: left;"><code>strong</code></td>
</tr>
<tr class="odd">
<td style="text-align: left;"><code>_强调_</code></td>
<td style="text-align: left;">斜体强调。中文字体一般没有斜体，所以一般不生效。</td>
<td style="text-align: left;"><code>emph</code></td>
</tr>
<tr class="even">
<td style="text-align: left;"><code>- 无序列表</code></td>
<td style="text-align: left;">无序列表</td>
<td style="text-align: left;"><code>list</code></td>
</tr>
<tr class="odd">
<td style="text-align: left;"><code>+ 有序列表</code></td>
<td style="text-align: left;">有序列表</td>
<td style="text-align: left;"><code>enum</code></td>
</tr>
<tr class="even">
<td style="text-align: left;">空行</td>
<td style="text-align: left;">分段</td>
<td style="text-align: left;"><code>parbreak</code></td>
</tr>
<tr class="odd">
<td style="text-align: left;"><code>`code`</code></td>
<td style="text-align: left;">代码（是否为行内代码取决于是否分行写）。使用三个反引号可支持高亮：<code>```c return 0;```</code></td>
<td style="text-align: left;"><code>raw</code></td>
</tr>
<tr class="even">
<td style="text-align: left;"><code>$y=k x + b$</code></td>
<td style="text-align: left;">数学公式（是否为行内公式取决于是否分行写）</td>
<td style="text-align: left;">数学模式不是函数</td>
</tr>
</tbody>
</table>
<p>更多语法见<a href="https://typst.app/docs/reference/syntax/#markup">官方文档</a>。</p>
<h3 id="数学模式">数学模式</h3>
<p>Typst 的数学语法不同于 LaTeX，但比它简单。Typst
中单个字母表示它本身；但多个字母表示特殊值或函数，类似于 LaTeX
省略反斜杠。如果要表示多个字母本身，就需要加双引号。</p>
<p><img src="/assets/images/typst_3.svg"></p>
<p>与 LaTeX 用花括号 <code>{}</code> 不同，Typst
中函数参数放在小括号里面，不同的参数用逗号 <code>,</code>
分隔。上下标的用法与 LaTeX 一致，<code>^</code> 表示上标，<code>_</code>
表示下标。此外 Typst 的数学公式有很多简便用法。例如 <span class="math inline"><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.486ex;" xmlns="http://www.w3.org/2000/svg" width="1.76ex" height="2.106ex" role="img" focusable="false" viewBox="0 -716 778 931"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mo"><path data-c="2260" d="M166 -215T159 -215T147 -212T141 -204T139 -197Q139 -190 144 -183L306 133H70Q56 140 56 153Q56 168 72 173H327L406 327H72Q56 332 56 347Q56 360 70 367H426Q597 702 602 707Q605 716 618 716Q625 716 630 712T636 703T638 696Q638 692 471 367H707Q722 359 722 347Q722 336 708 328L451 327L371 173H708Q722 163 722 153Q722 140 707 133H351Q175 -210 170 -212Q166 -215 159 -215Z"></path></g></g></g></svg></mjx-container></span> 可以写作 <code>!=</code>，<span class="math inline"><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.312ex;" xmlns="http://www.w3.org/2000/svg" width="1.76ex" height="1.751ex" role="img" focusable="false" viewBox="0 -636 778 774"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mo"><path data-c="2264" d="M674 636Q682 636 688 630T694 615T687 601Q686 600 417 472L151 346L399 228Q687 92 691 87Q694 81 694 76Q694 58 676 56H670L382 192Q92 329 90 331Q83 336 83 348Q84 359 96 365Q104 369 382 500T665 634Q669 636 674 636ZM84 -118Q84 -108 99 -98H678Q694 -104 694 -118Q694 -130 679 -138H98Q84 -131 84 -118Z"></path></g></g></g></svg></mjx-container></span> 可以写作 <code>&lt;=</code>，分数
<code>\frac{a}{b}</code> 可以用斜杠 <code>/</code> 等。</p>
<p><img src="/assets/images/typst_4.svg"></p>
<p>详细用法见 <a href="https://typst.app/docs/reference/math/" class="uri">https://typst.app/docs/reference/math/</a></p>
<h3 id="代码模式">代码模式</h3>
<p>Typst 是完善的编程语言，有很多通用编程语言的特性。</p>
<pre><code class="hljs typst">#let factorial(x) = { // let 定义函数<br>  let i = 1; // let 定义变量<br>  let ans = 1;<br>  while i &lt;= x { // while 循环<br>    ans *= i;<br>    i += 1;<br>  }<br>  return ans; // 返回结果<br>}<br><br>#let a = 10; // 定义变量<br>#a;的阶乘等于#factorial(a) // 调用函数<br></code></pre>
<p><img src="/assets/images/typst_5.svg" class="shadow"></p>
<p>下面展示了一些常用语法：</p>
<table>

<thead>
<tr class="header">
<th style="text-align: left;">语法</th>
<th style="text-align: left;">含义</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td style="text-align: left;"><code>let a = 1</code></td>
<td style="text-align: left;">定义变量</td>
</tr>
<tr class="even">
<td style="text-align: left;"><code>let f(x, y) = { return x * y; }</code></td>
<td style="text-align: left;">定义函数</td>
</tr>
<tr class="odd">
<td style="text-align: left;"><code>let f(x, y) = x + y</code></td>
<td style="text-align: left;">定义函数。如果没有 <code>return</code>
语句，函数的返回值等于函数体所有表达式的拼接</td>
</tr>
<tr class="even">
<td style="text-align: left;"><code>let f(x: 0, y: 0) = x + y</code></td>
<td style="text-align: left;">带<strong>命名参数 (named
argument)</strong>的函数定义。命名参数自带默认值，调用时是可选的</td>
</tr>
<tr class="odd">
<td style="text-align: left;"><code>(x, y) =&gt; x + y</code></td>
<td style="text-align: left;">匿名函数表达式</td>
</tr>
<tr class="even">
<td style="text-align: left;"><code>42</code>, <code>0xff</code></td>
<td style="text-align: left;">整数</td>
</tr>
<tr class="odd">
<td style="text-align: left;"><code>3.14</code>, <code>1e10</code></td>
<td style="text-align: left;">浮点数</td>
</tr>
<tr class="even">
<td style="text-align: left;"><code>"hello"</code></td>
<td style="text-align: left;">字符串</td>
</tr>
<tr class="odd">
<td style="text-align: left;"><code>10pt</code>, <code>1.5em</code></td>
<td style="text-align: left;">长度</td>
</tr>
<tr class="even">
<td style="text-align: left;"><code>90deg</code>, <code>1rad</code></td>
<td style="text-align: left;">角度</td>
</tr>
<tr class="odd">
<td style="text-align: left;"><code>50%</code></td>
<td style="text-align: left;">比例</td>
</tr>
<tr class="even">
<td style="text-align: left;"><code>1fr</code></td>
<td style="text-align: left;">分数</td>
</tr>
<tr class="odd">
<td style="text-align: left;"><code>(1, 2, 3)</code></td>
<td style="text-align: left;">数组</td>
</tr>
<tr class="even">
<td style="text-align: left;"><code>(a: 1, b: "ok")</code></td>
<td style="text-align: left;">字典</td>
</tr>
<tr class="odd">
<td style="text-align: left;"><code>a = 1</code></td>
<td style="text-align: left;">赋值</td>
</tr>
<tr class="even">
<td style="text-align: left;"><code>-a</code>, <code>a + b</code></td>
<td style="text-align: left;">一元运算符和二元运算符</td>
</tr>
<tr class="odd">
<td style="text-align: left;"><code>f(a, b)</code></td>
<td style="text-align: left;">调用函数</td>
</tr>
<tr class="even">
<td style="text-align: left;"><code>enum([a], [b])</code></td>
<td style="text-align: left;">调用 enum
函数，传入两个类型为<strong>标记内容 (content)</strong>
的参数（<code>[...]</code> 切换到标记模式）</td>
</tr>
<tr class="odd">
<td style="text-align: left;"><code>enum(start: 2, [a], [b])</code></td>
<td style="text-align: left;">带命名参数的函数调用</td>
</tr>
<tr class="even">
<td style="text-align: left;"><code>enum(start: 2)[a][b]</code></td>
<td style="text-align: left;">语法糖，与上面的写法等价。标记内容参数可以放在括号外面</td>
</tr>
<tr class="odd">
<td style="text-align: left;"><code>x.y</code></td>
<td style="text-align: left;">成员访问</td>
</tr>
<tr class="even">
<td style="text-align: left;"><code>x.flatten()</code></td>
<td style="text-align: left;">方法调用</td>
</tr>
</tbody>
</table>
<p>完整的语法见 <a href="https://typst.app/docs/reference/scripting/" class="uri">https://typst.app/docs/reference/scripting/</a> 和 <a href="https://typst.app/docs/reference/foundations/" class="uri">https://typst.app/docs/reference/foundations/</a></p>
<p>Typst
提供了很多用于实现各种样式的函数，例如文字样式、段落、图表、表格、列表等等，称为<strong>元素函数
(element function)</strong>。Typst
的标记语法基本上都是元素函数的简便写法。例如用于创建各种样式的文本的
<code>text</code> 函数，它有很多参数。下面列举了它的一小部分参数：</p>
<pre><code class="hljs typst">text(<br>  font: str | array,<br>  weight: int | str,<br>  size: length,<br>  fill: color,<br>  stroke: none | length | color,<br>  tracking: length,<br>  spacing: relative,<br>  lang: str,<br>  str,<br>  content,<br>) -&gt; content<br></code></pre>
<ul>
<li><code>font</code> 字体</li>
<li><code>weight</code> 字重。从细到粗分别是 <code>"thin"</code>,
<code>"extralight"</code>, <code>"light"</code>, <code>"regular"</code>,
<code>"medium"</code>, <code>"semibold"</code>, <code>"bold"</code>,
<code>"extrabold"</code>, <code>"black"</code></li>
<li><code>size</code> 字体大小。如 <code>10pt</code>,
<code>1.5em</code></li>
<li><code>fill</code> 填充色。如 <code>red</code>,
<code>rgb("#eb27ba")</code></li>
<li><code>stroke</code> 描边，可以是长度 + 颜色。如
<code>0.3pt + red</code> 表示 0.3 像素红色描边</li>
<li><code>tracking</code> 字母间距</li>
<li><code>spacing</code> 单词间距</li>
<li><code>lang</code> 语言</li>
<li><code>str</code> 字符串文本</li>
<li><code>content</code> 也可传入标记内容</li>
</ul>
<p>利用 <code>text</code> 函数我们就能生成各种各样的文本了。</p>
<p><img src="/assets/images/typst_6.svg"></p>
<p>完整的元素函数可参考 <a href="https://typst.app/docs/reference/model/" class="uri">https://typst.app/docs/reference/model/</a>。下面列出了一些例子：</p>
<p><img src="/assets/images/typst_7.svg"></p>
<h2 id="定制样式">定制样式</h2>
<p>如果每次使用自定义样式时都要显式调用元素函数，未免有些太麻烦了。Typst
提供了两种语法用于定制样式：set 规则和 show 规则。</p>
<h3 id="set-规则">Set 规则</h3>
<p>Set
规则很容易理解：在文档中显示任何元素都实际上是调用元素函数的结果，那么
set 语句就是用于设置元素函数某些参数的默认值。例如
<code>#set text(size: 20pt)</code>，那么所有文字的大小都会变成 20
点。</p>
<p><img src="/assets/images/typst_8.svg"></p>
<p>Set 规则作用范围是当前 block。所谓 block 就是内容块
<code>[...]</code> 或代码块 <code>{...}</code>。例如</p>
<pre><code class="hljs typst">#set text(fill: red) // 全局生效<br>#[<br>  #set text(size: 20pt) // 当前 block 生效<br>  红色 20 点文字<br>]<br>红色默认大小文字<br><br>#let bold(content) = {<br>  set text(weight: "bold") // 当前函数的 block 生效<br>  content<br>}<br><br>#bold[加粗字体]<br>正常字体<br></code></pre>
<p>Set 规则非常实用。有些元素函数，例如 <code>par</code>（段落）,
<code>page</code>（页面）,
<code>document</code>（文档），我们很少直接调用它们，而是将它们应用 set
规则，设置它们的样式。</p>
<h3 id="show-规则">Show 规则</h3>
<p>Show 规则的一种写法是关键词 <code>show</code> + 选择器 +
<code>:</code> + set 语句，表示将所有满足选择器的元素应用指定的 set
规则。最常用的选择器就是元素函数，例如下面的语句表示将所有的标题文字设置为海军蓝：</p>
<pre><code class="hljs typst">#show heading: set text(fill: navy)<br></code></pre>
<p>另一种写法是 <code>show</code> + 选择器 + <code>:</code> +
函数，表示将所有满足选择器的元素传入指定函数。例如下面的语句表示将所有的超链接的样式设置为带下划线的蓝色文字：</p>
<pre><code class="hljs moonscript">#show <span class="hljs-name">link</span>: <span class="hljs-function"><span class="hljs-params">(a)</span> =&gt;</span> underline(text(<span class="hljs-name">fill</span>: blue, a))<br></code></pre>
<p>选择器除了是元素函数外，还可以是以下几种：</p>
<ul>
<li>字符串：<code>show "Text": ...</code>
所有匹配到指定字符串的内容应用指定样式。这在某些场景非常实用。 <img src="/assets/images/typst_9.svg"></li>
<li>正则表达式：<code>show regex("\w+"): ...</code>
所有匹配到指定正则表达式的内容应用指定样式。</li>
<li>指定参数的元素函数：<code>show heading.where(level: 1): ...</code>
元素函数支持 <code>.where</code>
方法，返回一个选择器，只选择指定参数的元素。这个例子将所有 1
级标题应用指定样式</li>
<li>所有内容 <code>show: ...</code> 所有内容应用指定样式。如果
<code>:</code>
后面是函数，就会把整篇文档传入函数，这在模板的使用中非常常用。</li>
</ul>
<p>详细用法见 <a href="https://typst.app/docs/reference/styling/" class="uri">https://typst.app/docs/reference/styling/</a></p>
<h2 id="实战使用-typst-制作简历">实战：使用 Typst 制作简历</h2>
<p>Typst 很适合制作技术简历。在 Typst Universe 中，有很多<a href="https://typst.app/universe/search/?category=cv">简历模板</a>。我们从中挑选一个，例如
<a href="https://typst.app/universe/package/basic-resume">basic
resume</a>。我们执行</p>
<pre><code class="hljs apache"><span class="hljs-attribute">typst</span> init @preview/basic-resume:<span class="hljs-number">0</span>.<span class="hljs-number">2</span>.<span class="hljs-number">8</span><br></code></pre>
<p>初始化一个 typst 工程。使用 vscode 打开
<code>basic-resume/main.typ</code> 便可以开始编辑了。</p>
<p><img src="/assets/images/typst_10.png"></p>
<p>首先使用 <code>import</code> 语句引入 basic-resume 模块的内容。这里的
<code>*</code> 表示引入模块中的所有符号。</p>
<pre><code class="hljs typst">#import "@preview/basic-resume:0.2.8": *<br></code></pre>
<p>接着用 <code>let</code> 定义一些可能会复用的变量。接下来是最关键的
show 语句：</p>
<pre><code class="hljs typst">#show: resume.with(<br>  author: name,<br>  location: location,<br>  email: email,<br>  ...<br>)<br></code></pre>
<p><code>resume</code> 是 basic-resume
模块中定义的一个函数。我们可以看到它的定义：</p>
<pre><code class="hljs typst">#let resume(<br>  author: "",<br>  location: "",<br>  email: "",<br>  ...<br>  body,<br>) = {<br>  ...<br>}<br></code></pre>
<p>它本质上是一个有很多命名参数和一个普通参数 <code>body</code>
的函数，将内容 <code>body</code> 转换成一篇简历并返回。而
<code>with</code>
实际上是函数对象的一个方法，它返回一个预应用了给定参数的新函数。例如函数
<code>let f(a, b) = a + b</code>，<code>f.with(1)</code> 就等价于
<code>(b) =&gt; f(1, b)</code>。那么这里 <code>resume.with(...)</code>
就得到一个各种命名参数设置好了的新函数。这里的 show
语句会将整个文档作为参数传入这个新函数，我们就能得到一篇简历了。</p>
<p>接下来就是简历正文，也就是被传入 <code>resume</code>
函数的内容。其中的各种语法我们已经基本上已经介绍过了，这里无非是调用模板中定义的函数插入各种内容。例如
<code>#edu(...)</code> 插入教育经历、<code>#work(...)</code>
插入工作经历、<code>#project(...)</code> 插入项目经历等。</p>
<p>编辑完成后，点击 “Export PDF”，或者手动执行
<code>typst compile main.typ</code> 就可以得到 PDF 格式的简历了。</p>
<h2 id="最后">最后</h2>
<p>过去我总觉得各种排版系统都不是特别好：Word
排版效果一般，且不便版本控制；Markdown 排版能力弱；LaTeX
古老且使用复杂，编译缓慢。直到发现了
Typst，使用过后立刻就喜欢上了。本文只是简单推荐，而不是详细的教程。如果要深入学习，可参考
Typst 官方文档 <a href="https://typst.app/docs" class="uri">https://typst.app/docs</a>，或者直接咨询 AI。</p>
]]></content>
    
    
    <summary type="html">一直以来文字排版都是一项复杂的工作。计算机出现不久后，人们就尝试用计算机取代铅字处理排版工作。现在计算机上的排版工具有很多。Microsoft
Office Word
可能是使用最广泛的排版工具。它容易上手，功能丰富，能够满足绝大多数办公场景。缺点是文件格式私有，价格昂贵；面对一些复杂排版需求（如公</summary>
    
    
    
    
    <category term="tools" scheme="https://luyuhuang.tech/tags/tools/"/>
    
  </entry>
  
  <entry>
    <title>Clang 编译安装指南</title>
    <link href="https://luyuhuang.tech/2025/03/30/clang.html"/>
    <id>https://luyuhuang.tech/2025/03/30/clang.html</id>
    <published>2025-03-29T16:00:00.000Z</published>
    <updated>2025-03-30T12:43:09.608Z</updated>
    
    <content type="html"><![CDATA[<p>Clang 是一个基于 LLVM 的 C/C++ 编译器，与 GCC 相比有一些优势</p>
<ul>
<li>编译速度比 GCC 快</li>
<li>内存占用更小</li>
<li>编译报错信息更友好</li>
<li>工具链丰富：ASan, clangd, clang-tidy, clang-doc 等</li>
</ul>
<p>如果 Linux
发行版比较新，可以直接用包管理器安装；但如果发行版比较老旧，就只能编译安装了。本文介绍一些编译安装
clang 的方法。</p>
<h2 id="直接编译安装">直接编译安装</h2>
<p>Clang 是 LLVM 项目的一部分。LLVM
是一个编译器基础设施，它定义一种中间代码
(IR)，并且能够将这种中间代码编译成各个平台 (x86, ARM, …) 的机器码。这在
LLVM 中称为<em>编译器后端</em>。而 clang 则是 C/C++
的<em>编译器前端</em>，负责将 C/C++ 代码编译成 LLVM
中间代码。因此要编译安装 clang，我们就要编译 LLVM。</p>
<p>我们进入 LLVM 官网的下载页面 <a href="https://releases.llvm.org/"
class="uri">https://releases.llvm.org/</a> 下载 LLVM 源码。LLVM 10.0.0
之后提供整合包 <ruby>llvm-project<rt>LLVM 全家桶</rt></ruby> 下载，包含
clang 在内的各种 LLVM 组件。以最新的 20.1.0 为例，我们直接下载 LLVM
20.1.0 并解压</p>
<pre><code class="hljs bash">curl -LO https://github.com/llvm/llvm-project/releases/download/llvmorg-20.1.0/llvm-project-20.1.0.src.tar.xz<br>unxz llvm-project-20.1.0.src.tar.xz<br>tar -xf llvm-project-20.1.0.src.tar<br></code></pre>
<p>LLVM 使用 CMake 构建，要求版本至少为 3.20.0。进入 llvm-project
目录，接着创建 build 目录，然后执行 cmake</p>
<pre><code class="hljs bash"><span class="hljs-built_in">cd</span> llvm-project-20.1.0.src<br><span class="hljs-built_in">mkdir</span> build &amp;&amp; <span class="hljs-built_in">cd</span> build<br>cmake ../llvm -DCMAKE_BUILD_TYPE=Release \<br>              -DLLVM_ENABLE_PROJECTS=<span class="hljs-string">&#x27;clang&#x27;</span> \<br>              -DLLVM_ENABLE_RUNTIMES=<span class="hljs-string">&#x27;compiler-rt;libcxx;libcxxabi;libunwind&#x27;</span><br></code></pre>
<p>CMake 这一步可以传入各种参数，这里介绍一些常用的参数</p>
<ul>
<li><code>CMAKE_BUILD_TYPE</code> 构建类型，可以为 <code>Debug</code>,
<code>Release</code>, <code>RelWithDebInfo</code>, 或
<code>MinSizeRel</code>。默认为 <code>Debug</code>，一般设置为
<code>Release</code>。</li>
<li><code>DLLVM_ENABLE_PROJECTS</code> 启用的组件。llvm-project
中有很多组件，除了 <code>clang</code>，常用的还有
<ul>
<li><code>clang-tools-extra</code> 额外工具集。包含 clangd, clang-tidy,
clang-doc, clang-include-fixer 等。</li>
<li><code>lldb</code> 调试器。Clang 编译的程序用 gdb
调试可能会遇到各种问题，最好用 lldb 调试。</li>
<li><code>lld</code> 链接器，可替代 ld。</li>
</ul>
此外还有 <code>bolt</code>, <code>polly</code>, <code>libclc</code>
等组件，具体可参见 LLVM 文档。多个组件之间用分号 <code>;</code>
分隔。</li>
<li><code>DLLVM_ENABLE_RUNTIMES</code> 启用的运行时组件。常用的组件有
<ul>
<li><code>compiler-rt</code> 编译器运行时。如果要用 Sanitizer
系列工具（如 ASan），则必选。</li>
<li><code>libcxx</code> 和 <code>libcxxabi</code> 是 LLVM 的 C++
标准库实现。Clang 默认将程序链接到系统的 libstdc++，但如果要链接到 LLVM
的标准库 libc++，这两个必选</li>
<li><code>libunwind</code> 实现堆栈展开的库。如果要链接到
libc++，则必选。</li>
</ul>
此外还有 <code>libc</code>, <code>llvm-libgcc</code>,
<code>offload</code> 等组件，具体可参见 LLVM 文档。多个组件之间用分号
<code>;</code> 分隔。</li>
<li><code>CMAKE_INSTALL_PREFIX</code> 安装路径前缀，默认为
<code>/usr/local</code>。</li>
<li><code>CMAKE_C_COMPILER</code> 和 <code>CMAKE_CXX_COMPILER</code>
分别指定 C 和 C++ 的编译器，默认为 <code>gcc</code> 和
<code>g++</code>。后面我们会用到这两个参数。</li>
<li><code>LLVM_ENABLE_LIBCXX</code> 是否链接到 libc++。默认链接到
libstdc++。后面我们会用到这个参数。</li>
</ul>
<p>如果 LLVM 的各个依赖项都没有问题、这一步成功后，便可执行
<code>make</code> 开始构建</p>
<pre><code class="hljs bash">make -j8 <span class="hljs-comment"># 根据机器情况调整线程数 </span><br></code></pre>
<p>如果一切顺利，执行 <code>sudo make install</code>
即可完成安装。安装前可以执行 <code>make check-clang</code> 执行 clang
的测试用例，确认没有问题。</p>
<h2 id="老旧发行版编译">老旧发行版编译</h2>
<p>要成功构建 LLVM 20.1.0，GCC 版本至少为 7.4。然而在一些老旧发行版（如
CentOS 7）中，GCC
版本并不能满足要求。为此我们需要先用系统的老版编译器编译一个新版编译器，再用新版编译器编译
LLVM。</p>
<pre><code class="hljs bash"><span class="hljs-comment"># 下载 gcc-9.1.0</span><br>curl -LO https://ftp.gnu.org/gnu/gcc/gcc-9.1.0/gcc-9.1.0.tar.xz<br>unxz gcc-9.1.0.tar.xz<br>tar -xf gcc-9.1.0.tar<br><br><span class="hljs-built_in">cd</span> gcc-9.1.0<br><br><span class="hljs-comment"># 安装依赖</span><br>./contrib/download_prerequisites<br>./configure --prefix=<span class="hljs-variable">$&#123;HOME&#125;</span>/toolchains <span class="hljs-comment"># 安装到 ~/toolchains</span><br>make -j8<br>make install<br></code></pre>
<p>这里我们编译的 gcc-9.1.0 是用于构建 LLVM
的“临时”编译器，我们不把它安装到系统目录
(<code>/usr/local/</code>)，而是安装到 <code>~/toolchains</code>。GCC
是系统编译器，不可随意升级，否则可能导致系统其它软件出现兼容性问题。</p>
<p>接着我们用刚刚编译的 gcc-9.1.0 构建 LLVM。</p>
<pre><code class="hljs bash"><span class="hljs-built_in">cd</span> llvm-project-20.1.0.src<br><span class="hljs-built_in">mkdir</span> build &amp;&amp; <span class="hljs-built_in">cd</span> build<br>cmake ../llvm -DCMAKE_C_COMPILER=<span class="hljs-variable">$&#123;HOME&#125;</span>/toolchains/bin/gcc \<br>              -DCMAKE_CXX_COMPILER=<span class="hljs-variable">$&#123;HOME&#125;</span>/toolchains/bin/g++ \<br>              -DCMAKE_BUILD_TYPE=Release \<br>              -DLLVM_ENABLE_PROJECTS=<span class="hljs-string">&#x27;clang&#x27;</span> \<br>              -DLLVM_ENABLE_RUNTIMES=<span class="hljs-string">&#x27;compiler-rt;libcxx;libcxxabi;libunwind&#x27;</span><br><br>LD_LIBRARY_PATH=<span class="hljs-variable">$&#123;HOME&#125;</span>/toolchains/lib:<span class="hljs-variable">$&#123;HOME&#125;</span>/toolchains/lib64 make -j8<br></code></pre>
<p>这里我们用 <code>-DCMAKE_C_COMPILER</code> 和
<code>-DCMAKE_CXX_COMPILER</code> 指定 C/C++ 编译器的完整路径，也就是
gcc-9.1.0 的安装路径 <code>~/toolchains/</code> 下的
<code>bin/gcc</code> 和 <code>bin/g++</code>。LLVM
构建过程中会执行编译出来的工具，这些工具都依赖于 gcc-9.1.0 的 C++
运行库。因此我们要用环境变量 <code>LD_LIBRARY_PATH</code>
指定动态库路径，确保它们能正常运行。</p>
<p>因为这样构建的 LLVM 工具链都依赖于 gcc-9.1.0 的运行库，我们要设置好
<code>LD_LIBRARY_PATH</code> 才能正常运行它们。</p>
<pre><code class="hljs bash">$ bin/clang --version <span class="hljs-comment"># 直接运行通常会出现 libstdc++ 不兼容的报错</span><br>bin/clang: /lib64/libstdc++.so.6: version `GLIBCXX_3.4.26<span class="hljs-string">&#x27; not found (required by bin/clang)</span><br><span class="hljs-string"></span><br><span class="hljs-string">$ LD_LIBRARY_PATH=$&#123;HOME&#125;/toolchains/lib:$&#123;HOME&#125;/toolchains/lib64 bin/clang --version # 需要指定 gcc-9.1.0 的动态库路径</span><br><span class="hljs-string">clang version 20.1.0</span><br><span class="hljs-string">Target: x86_64-unknown-linux-gnu</span><br><span class="hljs-string">Thread model: posix</span><br><span class="hljs-string">InstalledDir: /home/luyuhuang/llvm-project-20.1.0.src/build/bin</span><br><span class="hljs-string"></span><br><span class="hljs-string">$ LD_LIBRARY_PATH=$&#123;HOME&#125;/toolchains/lib:$&#123;HOME&#125;/toolchains/lib64 ldd bin/clang</span><br><span class="hljs-string">        linux-vdso.so.1 =&gt;  (0x00007fffe95c9000)</span><br><span class="hljs-string">        libpthread.so.0 =&gt; /lib64/libpthread.so.0 (0x00007f470cbbb000)</span><br><span class="hljs-string">        librt.so.1 =&gt; /lib64/librt.so.1 (0x00007f470c9b3000)</span><br><span class="hljs-string">        libdl.so.2 =&gt; /lib64/libdl.so.2 (0x00007f470c7af000)</span><br><span class="hljs-string">        libz.so.1 =&gt; /lib64/libz.so.1 (0x00007f470c599000)</span><br><span class="hljs-string">        libstdc++.so.6 =&gt; /home/luyuhuang/toolchains/lib64/libstdc++.so.6 (0x00007f470c1c0000)</span><br><span class="hljs-string">        libm.so.6 =&gt; /lib64/libm.so.6 (0x00007f470bebe000)</span><br><span class="hljs-string">        libgcc_s.so.1 =&gt; /home/luyuhuang/toolchains/lib64/libgcc_s.so.1 (0x00007f470bca6000)</span><br><span class="hljs-string">        libc.so.6 =&gt; /lib64/libc.so.6 (0x00007f470b8e2000)</span><br><span class="hljs-string">        /lib64/ld-linux-x86-64.so.2 (0x00007f470cdd7000)</span><br></code></pre>
<h2 id="bootstrap">Bootstrap</h2>
<p>前面使用 gcc-9.1.0 编译的 LLVM 工具链虽然可以运行，但是却依赖于
gcc-9.1.0 的运行库。由于系统的 C++ 运行库不能随意升级，我们不便将
gcc-9.1.0 的运行库安装到系统中。这里比较合适的做法是让 clang 做一次
bootstrap（自举），用 gcc-9.1.0 编译的 clang 构建 LLVM，并链接到 LLVM 的
C++ 运行库（也就是 libc++）。</p>
<pre><code class="hljs bash"><span class="hljs-built_in">cd</span> llvm-project-20.1.0.src<br><span class="hljs-built_in">mkdir</span> build1 &amp;&amp; <span class="hljs-built_in">cd</span> build1 <span class="hljs-comment"># 创建一个新的构建目录</span><br>cmake ../llvm -DCMAKE_C_COMPILER= $(<span class="hljs-built_in">realpath</span> ../build)/bin/clang \      <span class="hljs-comment"># 使用 ../build 目录下，用 gcc-9.1.0 编译的 clang 构建</span><br>              -DCMAKE_CXX_COMPILER= $(<span class="hljs-built_in">realpath</span> ../build)/bin/clang++ \<br>              -DLLVM_ENABLE_LIBCXX=ON \                                 <span class="hljs-comment"># 使用 LLVM 的 libc++</span><br>              -DCMAKE_BUILD_TYPE=Release \<br>              -DLLVM_ENABLE_PROJECTS=<span class="hljs-string">&#x27;clang&#x27;</span> \<br>              -DLLVM_ENABLE_RUNTIMES=<span class="hljs-string">&#x27;compiler-rt;libcxx;libcxxabi;libunwind&#x27;</span><br><br>LD_LIBRARY_PATH=<span class="hljs-variable">$&#123;HOME&#125;</span>/toolchains/lib:<span class="hljs-variable">$&#123;HOME&#125;</span>/toolchains/lib64:$(<span class="hljs-built_in">realpath</span> ../build)/lib/x86_64-unknown-linux-gnu make -j8<br><br></code></pre>
<p>这里我们则是将编译器路径设置成前面用 gcc-9.1.0 编译的 clang
的路径。同时设置 <code>-DLLVM_ENABLE_LIBCXX=ON</code> 链接到 LLVM 的
libc++。最后注意环境变量 <code>LD_LIBRARY_PATH</code> 除了需要指定
gcc-9.1.0 的运行库路径之外，还需要指定前面 gcc-9.1.0 编译的 LLVM
的运行库路径。</p>
<p>构建完成后，执行 <code>sudo make install</code> 安装即可。由于 LLVM
20.1.0 的 C++ 运行库位于
<code>lib/x86_64-unknown-linux-gnu/</code>（更老版本的 LLVM 则直接在
<code>lib/</code> 里），我们通常需要再配置下动态库搜索路径。</p>
<pre><code class="hljs bash"><span class="hljs-built_in">echo</span> /usr/local/lib/x86_64-unknown-linux-gnu &gt;&gt; /etc/ld.so.conf<br>ldconfig<br></code></pre>
<p>这样安装的 clang 就能正常运行了。</p>
<pre><code class="hljs bash">$ clang --version<br>clang version 20.1.0<br>Target: x86_64-unknown-linux-gnu<br>Thread model: posix<br>InstalledDir: /usr/local/bin<br><br>$ ldd /usr/local/bin/clang<br>        linux-vdso.so.1 =&gt;  (0x00007ffc6d57c000)<br>        libpthread.so.0 =&gt; /lib64/libpthread.so.0 (0x00007fca001ae000)<br>        librt.so.1 =&gt; /lib64/librt.so.1 (0x00007fc9fffa6000)<br>        libdl.so.2 =&gt; /lib64/libdl.so.2 (0x00007fc9ffda2000)<br>        libm.so.6 =&gt; /lib64/libm.so.6 (0x00007fc9ffaa0000)<br>        libz.so.1 =&gt; /lib64/libz.so.1 (0x00007fc9ff88a000)<br>        libc++.so.1 =&gt; /usr/local/lib/x86_64-unknown-linux-gnu/libc++.so.1 (0x00007fc9ff583000)<br>        libc++abi.so.1 =&gt; /usr/local/lib/x86_64-unknown-linux-gnu/libc++abi.so.1 (0x00007fc9ff33b000)<br>        libunwind.so.1 =&gt; /usr/local/lib/x86_64-unknown-linux-gnu/libunwind.so.1 (0x00007fc9ff12e000)<br>        libgcc_s.so.1 =&gt; /lib64/libgcc_s.so.1 (0x00007fc9fef18000)<br>        libc.so.6 =&gt; /lib64/libc.so.6 (0x00007fc9feb54000)<br>        /lib64/ld-linux-x86-64.so.2 (0x00007fca0ad52000)<br>        libatomic.so.1 =&gt; /lib64/libatomic.so.1 (0x00007fc9fe94c000)<br></code></pre>
<hr>
<p><strong>参考资料:</strong></p>
<ul>
<li><a href="https://llvm.org/docs/CMake.html"
class="uri">https://llvm.org/docs/CMake.html</a></li>
<li><a href="https://llvm.org/docs/GettingStarted.html"
class="uri">https://llvm.org/docs/GettingStarted.html</a></li>
</ul>
]]></content>
    
    
    <summary type="html">Clang 是一个基于 LLVM 的 C/C++ 编译器，与 GCC 相比有一些优势

编译速度比 GCC 快
内存占用更小
编译报错信息更友好
工具链丰富：ASan, clangd, clang-tidy, clang-doc 等

如果 Linux
发行版比较新，可以直接用包管理器安装；但如果发</summary>
    
    
    
    
    <category term="linux" scheme="https://luyuhuang.tech/tags/linux/"/>
    
    <category term="c/c++" scheme="https://luyuhuang.tech/tags/c-c/"/>
    
  </entry>
  
  <entry>
    <title>使用 Address Sanitizer 排查内存越界</title>
    <link href="https://luyuhuang.tech/2025/02/16/asan.html"/>
    <id>https://luyuhuang.tech/2025/02/16/asan.html</id>
    <published>2025-02-15T16:00:00.000Z</published>
    <updated>2025-02-16T13:05:21.067Z</updated>
    
    <content type="html"><![CDATA[<p>在 C++
开发中，内存越界是很头痛的问题。这类问题往往非常隐蔽，难以排查，且难以复现。为了排查这类问题，我们可以使用
Address Sanitizer 这个工具。</p>
<p>Address Sanitizer (aka ASan) 是 Google
开发的一款内存错误排查工具，帮助开发这定位各种内存错误，目前已经集成在主流工具链中。LLVM
3.1 和 GCC 4.8 以上的版本均支持 ASan。本文介绍 ASan
的使用方法和其基本原理。</p>
<h2 id="如何使用">如何使用</h2>
<p>在支持 ASan 的编译器加上编译参数 <code>-fsanitize=address</code>
即可开启 ASan。例如我们有代码 <code>test.c</code>：</p>
<pre><code class="hljs c"><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&lt;stdlib.h&gt;</span></span><br><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&lt;stdio.h&gt;</span></span><br><br><span class="hljs-type">int</span> <span class="hljs-title function_">main</span><span class="hljs-params">()</span> &#123;<br>    <span class="hljs-type">int</span> *p = <span class="hljs-built_in">malloc</span>(<span class="hljs-keyword">sizeof</span>(<span class="hljs-type">int</span>));<br>    p[<span class="hljs-number">1</span>] = <span class="hljs-number">42</span>;<br>    <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;<br>&#125;<br></code></pre>
<p>虽然存在内存越界写入，但是使用常规方法编译后，是能够“正常”运行的：</p>
<pre><code class="hljs shell"><span class="hljs-meta prompt_">$ </span><span class="language-bash">gcc -o <span class="hljs-built_in">test</span> test.c</span><br><span class="hljs-meta prompt_">$ </span><span class="language-bash">./test</span><br>42<br></code></pre>
<p>加上编译参数 <code>-fsanitize=address</code> 启用 ASan
后，运行便会触发 ASan 报错：</p>
<pre><code class="hljs shell"><span class="hljs-meta prompt_">$ </span><span class="language-bash">gcc -o <span class="hljs-built_in">test</span> test.c -fsanitize=address -g</span><br><span class="hljs-meta prompt_">$ </span><span class="language-bash">./test</span><br>=================================================================<br>==65982==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x602000000014 at pc 0x5600b2a24202 bp 0x7ffda82b5c70 sp 0x7ffda82b5c60<br>WRITE of size 4 at 0x602000000014 thread T0<br>    #0 0x5600b2a24201 in main (/home/luyuhuang/test+0x1201)<br>    #1 0x7f7eee24c082 in __libc_start_main ../csu/libc-start.c:308<br>    #2 0x5600b2a240ed in _start (/home/luyuhuang/test+0x10ed)<br><br>0x602000000014 is located 0 bytes to the right of 4-byte region [0x602000000010,0x602000000014)<br>allocated by thread T0 here:<br>    #0 0x7f7eee527808 in __interceptor_malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cc:144<br>    #1 0x5600b2a241be in main (/home/luyuhuang/test+0x11be)<br>    #2 0x7f7eee24c082 in __libc_start_main ../csu/libc-start.c:308<br>...<br></code></pre>
<p>ASan 告诉我们程序触发了一个堆越界 (heap-buffer-overflow)
的报错，尝试在地址 0x602000000014 写入 4
字节数据。随后打印出出错位置的堆栈。第 10 行 ASan
还告诉我们越界的内存是在哪个位置被分配的，随后打印出分配位置的堆栈。</p>
<h2 id="常用参数">常用参数</h2>
<p>ASan
的参数有<em>编译参数</em>和<em>运行时参数</em>两种。常用的编译参数有：</p>
<ul>
<li><code>-fsanitize=address</code> 启用 ASan</li>
<li><code>-fno-omit-frame-pointer</code> 获得更好的堆栈信息</li>
<li>ASan 专属的参数，GCC 使用 <code>--param FLAG=VAL</code> 传入，LLVM
使用 <code>-mllvm -FLAG=VAL</code> 传入：
<ul>
<li><code>asan-stack</code> 是否检测栈内存错误，默认启用。GCC 使用
<code>--param asan-stack=0</code>、LLVM 使用
<code>-mllvm -asan-stack=0</code> 关闭栈内存错误检测。</li>
<li><code>asan-global</code>
是否检测全局变量内存错误，默认启用。同理使用
<code>--param asan-global=0</code> 或 <code>-mllvm -asan-global=0</code>
可关闭。</li>
</ul></li>
</ul>
<p>运行时参数通过环境变量 <code>ASAN_OPTIONS</code>
设置，每个参数的格式为 <code>FLAG=VAL</code>，参数之间用冒号
<code>:</code> 分隔。例如：</p>
<pre><code class="hljs elixir"><span class="hljs-variable">$ </span><span class="hljs-title class_">ASAN_OPTIONS</span>=handle_segv=<span class="hljs-number">0</span><span class="hljs-symbol">:disable_coredump=</span><span class="hljs-number">0</span> ./test<br></code></pre>
<p>常用的运行时参数有：</p>
<ul>
<li><code>log_path</code>: ASan 报错默认输出在
stderr。使用这个参数可以指定报错输出的路径。</li>
<li><code>abort_on_error</code>: 报错时默认使用 <code>_exit</code>
结束进程。指定 <code>abort_on_error=1</code> 则使用 <code>abort</code>
结束进程。</li>
<li><code>disable_coredump</code>: Asan 默认会禁用 coredump。指定
<code>disable_coredump=0</code> 启用 coredump。</li>
<li><code>detect_leaks</code>: 是否启用内存泄漏检测，默认启用。ASan
还包含 LSan (Leak Sanitizer) 内存泄露检测模块。</li>
<li><code>handle_*</code>: 信号控制选项。ASan
默认会注册一些信号处理函数，参数置 0 表示让 ASan
不注册相应的信号信号处理器，置 1 则注册信号信号处理器，置 2
则注册信号处理器并禁止用户修改。
<ul>
<li><code>handle_segv</code>: SIGSEGV</li>
<li><code>handle_sigbus</code>: SIGBUS</li>
<li><code>handle_abort</code>: SIGABRT</li>
<li><code>handle_sigill</code>: SIGILL</li>
<li><code>handle_sigfpe</code>: SIGFPE</li>
</ul></li>
</ul>
<p>因为 ASan 默认会注册 SIGSEGV
的信号处理器，所以当程序发生段错误时，会触发 ASan 的报错而不是直接
coredump。要想让程序像往常一样产生 coredump，可以指定参数
<code>handle_segv=0</code> 不注册信号处理器，和
<code>disable_coredump=0</code> 启用 coredump。</p>
<p>有些函数可能会做一些比较 hack 操作，又想绕过 Asan
的越界检测。这可以通过声明属性
<code>__attribute__((no_sanitize_address))</code> 实现。例如</p>
<pre><code class="hljs c">__attribute__((no_sanitize_address))<br><span class="hljs-type">size_t</span> <span class="hljs-title function_">chunk_size</span><span class="hljs-params">(<span class="hljs-type">void</span> *p)</span> &#123;<br>    <span class="hljs-keyword">return</span> *((<span class="hljs-type">size_t</span>*)p - <span class="hljs-number">4</span>);<br>&#125;<br></code></pre>
<p>这样即使 <code>chunk_size</code> 访问越界，ASan 也不会报错。</p>
<p>更多参数可参考<a href="https://github.com/google/sanitizers/wiki/AddressSanitizerFlags">官方文档</a>。</p>
<h2 id="原理简介">原理简介</h2>
<p>ASan
需要检测的是应用程序是否访问已向操作系统申请、但未分配给应用程序的内存，也就是下图中红色的部分。至于图中白色的部分，也就是未向操作系统申请的内存，是不需要检测的（一旦访问就会触发段错误）。</p>
<p><img src="/assets/images/asan_1.png" width="230" ></p>
<p>ASan 会 hook 标准内存分配函数（malloc、free
等），所有未被分配和已释放的区域都会标记为红区。所有内存的访问都会被检查，如果访问了红区的内存，asan
会立刻报错。例如，原本简单的内存访问</p>
<pre><code class="hljs c">*address = ...;  <span class="hljs-comment">// or: ... = *address;</span><br></code></pre>
<p>在启用 ASan 后，会生成类似如下的代码：</p>
<pre><code class="hljs c"><span class="hljs-keyword">if</span> (IsPoisoned(address)) &#123;<br>  ReportError(address, kAccessSize, kIsWrite);<br>&#125;<br>*address = ...;  <span class="hljs-comment">// or: ... = *address;</span><br></code></pre>
<p>为了标记内存是否为红区，ASan 将每 8 字节内存映射成 1 字节的 shadow
内存，在 shadow 内存中标记这 8 字节内存的使用情况。64 位系统中，地址 p
对应的 shallow 内存的地址为
<code>(p &gt;&gt; 3) + 0x7fff8000</code>。</p>
<p><img src="/assets/images/asan_2.png" width="280" ></p>
<p>因为 malloc 分配的内存必然是 8 字节对齐的。这样的话只有 9
种情况：</p>
<ul>
<li>这 8 字节内存都不是红区。此时 shadow 内存值为 0</li>
<li>这 8 字节内存都是红区。此时 shadow 内存值为负数</li>
<li>前 k 字节不在红区，剩下的 8 - k 字节在红区。此时 shadow 内存值为
k</li>
</ul>
<p>例如 <code>malloc(11)</code> 分配 11
字节内存的情况，如下图所示。第一个 8 字节都不在红区，对应的 shadow
内存值为 0；第二个 8 字节前 3 字节不在红区，后 5 字节在红区，对应的
shadow 内存值为 3。第三个 8 字节都在红区，对应的 shadow
内存值为负数。</p>
<p><img src="/assets/images/asan_3.png" ></p>
<p>这样，整个地址空间被分为 5 个部分。HighMem 对应 HighShadow，LowMem
对应 LowShadow。如果一个地址对应的 shallow 内存在 ShadowGap
区域，则这个地址不可访问。因为 64
位机器的虚拟内存地址空间很大，这样划分后地址仍然很够用。</p>
<table>
<thead>
<tr class="header">
<th style="text-align: left;">地址区间</th>
<th style="text-align: left;">区域</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td style="text-align: left;">[0x10007fff8000, 0x7fffffffffff]</td>
<td style="text-align: left;">HighMem</td>
</tr>
<tr class="even">
<td style="text-align: left;">[0x02008fff7000, 0x10007fff7fff]</td>
<td style="text-align: left;">HighShadow</td>
</tr>
<tr class="odd">
<td style="text-align: left;">[0x00008fff7000, 0x02008fff6fff]</td>
<td style="text-align: left;">ShadowGap</td>
</tr>
<tr class="even">
<td style="text-align: left;">[0x00007fff8000, 0x00008fff6fff]</td>
<td style="text-align: left;">LowShadow</td>
</tr>
<tr class="odd">
<td style="text-align: left;">[0x000000000000, 0x00007fff7fff]</td>
<td style="text-align: left;">LowMem</td>
</tr>
</tbody>
</table>
<p>这样，每当程序要访问内存时，ASan 都会做如下检查：</p>
<pre><code class="hljs c"><span class="hljs-type">char</span> *pShadow = ((<span class="hljs-type">intptr_t</span>)address &gt;&gt; <span class="hljs-number">3</span>) + <span class="hljs-number">0x7fff8000</span>;  <span class="hljs-comment">// 计算得到 shadow 内存地址</span><br><span class="hljs-keyword">if</span> (*pShadow) &#123;     <span class="hljs-comment">// 如果 shadow 内存不为 0，做进一步检查</span><br>    <span class="hljs-type">int</span> last = ((<span class="hljs-type">intptr_t</span>)address &amp; <span class="hljs-number">7</span>) + kAccessSize - <span class="hljs-number">1</span>;   <span class="hljs-comment">// address % 8 + kAccessSize - 1 计算这次访问的最后一个字节</span><br>    <span class="hljs-keyword">if</span> (last &gt;= *pShadow) &#123; <span class="hljs-comment">// 如果 last &gt;= shadow 则报错</span><br>        ReportError(address, kAccessSize, kIsWrite);<br>    &#125;<br>&#125;<br>*address = ...;  <span class="hljs-comment">// or: ... = *address;</span><br></code></pre>
<p>假设某 8 字节内存后 3 字节在红区，程序要从第 4
字节开始访问两字节，如下图所示。那么有 address % 8 = 4，last = 4 + 2 - 1
= 5 &gt;= shadow，因此这次访问是越界访问，ASan 就会报错。</p>
<p><img src="/assets/images/asan_4.png" ></p>
<p>了解了 ASan 原理之后就能更好地理解 ASan 的报错信息。ASan
报错时会打印出报错位置的 shadow 内存情况：</p>
<pre><code class="hljs txt">SUMMARY: AddressSanitizer: heap-buffer-overflow (/home/luyuhuang/test+0x1201) in main<br>Shadow bytes around the buggy address:<br>  0x0c047fff7fb0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00<br>  0x0c047fff7fc0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00<br>  0x0c047fff7fd0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00<br>  0x0c047fff7fe0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00<br>  0x0c047fff7ff0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00<br>=&gt;0x0c047fff8000: fa fa[04]fa fa fa fa fa fa fa fa fa fa fa fa fa<br>  0x0c047fff8010: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa<br>  0x0c047fff8020: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa<br>  0x0c047fff8030: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa<br>  0x0c047fff8040: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa<br>  0x0c047fff8050: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa<br></code></pre>
<p>程序试图在 0x602000000014 写入 4 字节数据。 0x602000000014 对应的
shadow 内存地址为 (0x602000000014 &gt;&gt; 3) + 0x7fff8000 =
0x0c047fff8002，也就是上面 <code>[04]</code> 的位置。0x602000000014 % 8
= 4，从这 8 字节的第 4 字节开始访问四字节。而这 8
字节后四字节都在红区，因此访问越界。</p>
<hr>
<p><strong>参考资料:</strong> <a href="https://github.com/google/sanitizers/wiki/AddressSanitizer"
class="uri">https://github.com/google/sanitizers/wiki/AddressSanitizer</a></p>
]]></content>
    
    
    <summary type="html">在 C++
开发中，内存越界是很头痛的问题。这类问题往往非常隐蔽，难以排查，且难以复现。为了排查这类问题，我们可以使用
Address Sanitizer 这个工具。
Address Sanitizer (aka ASan) 是 Google
开发的一款内存错误排查工具，帮助开发这定位各种内存错误，</summary>
    
    
    
    
    <category term="linux" scheme="https://luyuhuang.tech/tags/linux/"/>
    
    <category term="tools" scheme="https://luyuhuang.tech/tags/tools/"/>
    
    <category term="c/c++" scheme="https://luyuhuang.tech/tags/c-c/"/>
    
  </entry>
  
  <entry>
    <title>用 C 语言实现协程</title>
    <link href="https://luyuhuang.tech/2024/06/14/c-coroutine.html"/>
    <id>https://luyuhuang.tech/2024/06/14/c-coroutine.html</id>
    <published>2024-06-13T16:00:00.000Z</published>
    <updated>2024-06-13T16:16:37.752Z</updated>
    
    <content type="html"><![CDATA[<p>协程与线程（进程）<sup id="fnref:1" class="footnote-ref"><a href="#fn:1" rel="footnote"><span class="hint--top hint--rounded"
aria-label="对于 Linux 内核而言，线程和进程是同一个东西">[1]</span></a></sup>都是模拟多<em>任务（routine）</em>并发执行的软件实现。操作系统线程在一个
CPU
线程中模拟并发执行多个任务，而<em>协程（coroutine）</em>在一个操作系统线程中模拟并发执行多个任务。
因此协程也被称为“用户态线程”、“轻量级线程”。之所以说“模拟并发执行”，是因为任务实际并没有同时运行，而是通过来回切换实现的。</p>
<p><img src="/assets/images/c-coroutine_1.svg" ></p>
<p>线程切换由操作系统负责，而协程切换通常由程序员直接控制。程序员通过
resume/yield 操作控制协程切换。resume 操作唤醒一个指定协程；yield
操作挂起当前协程，切换回唤醒它的协程。如果你用过 Lua
的协程，就会很熟悉这套流程。不过 Lua 是基于 <em>Lua
虚拟机（LVM）</em>的脚本语言，它只需要 LVM
中执行“虚拟的”上下文切换。本文介绍如何用 C 语言（和一点点汇编）实现一个
native 协程，执行真正的上下文切换。这个实现非常简单，总共不到 200
行代码。我参考了 <a href="https://github.com/tencent/libco">libco</a>
的实现。本文的完整代码见 <a href="https://github.com/luyuhuang/toy-coroutine">toy-coroutine</a>。</p>
<h2 id="上下文切换">上下文切换</h2>
<p>所谓的上下文，就是一段程序之前做了什么，接下来要做什么，以及做事情过程的中间产物。例如我们有函数
<code>f</code>。<code>f</code>
需要知道下一个指令是什么才能接着往下执行，便是“接下来要做什么”。<code>f</code>
函数还需要知道之前是谁调用了它，以便把结果返回给调用者，便是“之前做了什么”。在
<code>f</code>
函数执行过程中，局部变量要存好（不能被写坏），接下来的指令才能正确执行。这便是“过程的中间产物”。</p>
<p>在 x86-64 下，“之前做了什么” 存储在栈里。函数调用会执行
<code>call</code>
指令，把当前函数的下一个指令的地址压入栈顶，然后再跳转到被调用函数。被调用函数返回时执行
<code>ret</code>
指令，从栈顶取出调用者的返回点地址，然后跳转到返回点。因此栈上存有所有前序调用者的返回点地址。</p>
<p>函数的局部变量通常储存在 16
个通用寄存器中，如果寄存器不够用，就存在栈里（只要在函数返回前将它们弹出，让栈顶是返回点地址即可）。函数调用的参数也是局部变量，存在约定的
6 个通用寄存器里。如果不够用，也存在栈里。</p>
<p>至于“接下来要做什么”，其实也在栈里。上下文切换不过是调用一个函数，调用者在调用它之前已经把下一个指令的地址压栈了。当上下文切换函数返回，<code>ret</code>
指令自然会跳转到接下来要执行的指令。所以上下文就是 16 个通用寄存器 +
栈。</p>
<p>所有的协程共享同一个 CPU，也就共享同样的 16
个通用寄存器。如果我们要把 A 协程切换成 B 协程，就要把当前 16
个通用寄存器的值存在 A 协程的数据结构里；然后再从 B 协程的数据结构里取出
B
协程的寄存器的值，写回通用寄存器中。我们还要处理栈。不过栈与寄存器不同，x86-64
规定 <code>%rsp</code>
寄存器（也是通用寄存器之一）存的值便是栈顶的地址。不同的协程不必共享栈，它们可以分配各自的栈，上下文切换时将
<code>%rsp</code> 指向各自的栈顶即可。</p>
<p>实际上我们不必存储全部的 16
个通用寄存器，它们有些是<em>暂存寄存器（Scratch
Registers）</em>，是允许被写坏的。这些寄存器的值可能在执行一次函数调用后就变了（被被调用函数写坏的）。编译器也不会在暂存寄存器里存储函数调用后还要用的值。参考
libco 的实现，我们存储 13 个寄存器：</p>
<pre><code class="hljs c"><span class="hljs-class"><span class="hljs-keyword">enum</span> &#123;</span><br>    CO_R15 = <span class="hljs-number">0</span>,<br>    CO_R14,<br>    CO_R13,<br>    CO_R12,<br>    CO_R9,<br>    CO_R8,<br>    CO_RBP,<br>    CO_RDI,<br>    CO_RSI,<br>    CO_RDX,<br>    CO_RCX,<br>    CO_RBX,<br>    CO_RSP,<br>&#125;;<br><br><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">co_context</span> &#123;</span><br>    <span class="hljs-type">void</span> *regs[<span class="hljs-number">13</span>];<br>&#125;;<br></code></pre>
<p>有些寄存器有特殊的用途。这里我们只需要知道这三个：</p>
<ul>
<li><code>%rsp</code>: 栈寄存器，指向栈顶。</li>
<li><code>%rdi</code>, <code>%rsi</code>:
第一参数寄存器和第二参数寄存器，调用函数前将第一个参数存在
<code>%rdi</code> 里，第二个存在 <code>%rsi</code> 里（剩下的四个依次是
<code>%rdx</code>, <code>%rcx</code>, <code>%r8</code>,
<code>%r9</code>, 不过这里我们用不上），然后执行 <code>call</code>
指令。</li>
</ul>
<p>接着我们定义一个函数做上下文切换，把当前通用寄存器的值保存在
<code>curr</code> 中，再把 <code>next</code>
中保存的寄存器的值写回各个通用寄存器。</p>
<pre><code class="hljs c"><span class="hljs-keyword">extern</span> <span class="hljs-type">void</span> <span class="hljs-title function_">co_ctx_swap</span><span class="hljs-params">(<span class="hljs-keyword">struct</span> co_context *curr, <span class="hljs-keyword">struct</span> co_context *next)</span>;<br></code></pre>
<p>Emm，这个函数没法用 C
语言实现，我们得用到一点点汇编了。其实非常简单，我们只需要用
<code>movq</code> 指令存取寄存器。代码如下：</p>
<pre><code class="hljs asm">.globl co_ctx_swap<br><br>co_ctx_swap:<br>    movq %rsp, 96(%rdi)<br>    movq %rbx, 88(%rdi)<br>    movq %rcx, 80(%rdi)<br>    movq %rdx, 72(%rdi)<br>    movq %rsi, 64(%rdi)<br>    movq %rdi, 56(%rdi)<br>    movq %rbp, 48(%rdi)<br>    movq %r8, 40(%rdi)<br>    movq %r9, 32(%rdi)<br>    movq %r12, 24(%rdi)<br>    movq %r13, 16(%rdi)<br>    movq %r14, 8(%rdi)<br>    movq %r15, (%rdi)<br><br>    movq (%rsi), %r15<br>    movq 8(%rsi), %r14<br>    movq 16(%rsi), %r13<br>    movq 24(%rsi), %r12<br>    movq 32(%rsi), %r9<br>    movq 40(%rsi), %r8<br>    movq 48(%rsi), %rbp<br>    movq 56(%rsi), %rdi<br>    movq 72(%rsi), %rdx<br>    movq 80(%rsi), %rcx<br>    movq 88(%rsi), %rbx<br>    movq 96(%rsi), %rsp<br>    movq 64(%rsi), %rsi<br><br>    ret<br></code></pre>
<p>不懂汇编没关系（其实我也不是很懂），只需要知道 <code>movq</code>
指令将第一个操作数的值复制到第二个操作数中。<code>%</code>
开头的标识符为寄存器。<code>%rsp</code>
这样不带括号的，表示存取寄存器的值。<code>(%rdi)</code>
这种带括号的，表示去内存里存取地址为 <code>%rdi</code>
的数据。如果括号前面有数字几，就表示这个地址要加几。<code>movq</code>
存取数据的长度为 8 字节，寄存器的长度也是 8 字节。</p>
<p>还记得前面说过，<code>%rdi</code> 是第一个参数，<code>%rsi</code>
是第二个参数吗？所以 <code>%rdi</code> 就是
<code>struct co_context *curr</code>。<code>96(%rdi)</code> 就是
<code>curr-&gt;regs[12]</code>，<code>88(%rdi)</code> 就是
<code>curr-&gt;regs[11]</code>，……，<code>(%rdi)</code> 就是
<code>curr-&gt;regs[0]</code>。上半部分把 13 个通用寄存器的值全部存到了
<code>curr</code> 里。同理 <code>%rsi</code> 就是
<code>struct co_context *next</code>。<code>(%rsi)</code> 就是
<code>next-&gt;regs[0]</code>、<code>8(%rsi)</code> 就是
<code>next-&gt;regs[1]</code>，依次类推。于是下半部分把
<code>next</code> 中保存的寄存器的值写回寄存器中。最后执行
<code>ret</code> 指令返回。</p>
<p>注意 29 行写入 <code>%rsp</code> 的值就是上次挂起时第 4
行保存的值，这个值我们原封未动，也没有做任何栈操作。因此最后
<code>ret</code> 返回时，栈顶就是 <code>co_ctx_swap</code>
的调用者设置的返回点地址。一个协程调用 <code>co_ctx_swap</code>
将自己挂起，便陷入沉睡。当 <code>co_ctx_swap</code>
返回之时，便是其它协程调用 <code>co_ctx_swap</code>
将它唤醒之时。此时寄存器被还原、栈被还原、也回到了返回点。它便知道自己之前做了什么、接下来要做什么、中间产物是怎样的。</p>
<h2 id="协程的初始化">协程的初始化</h2>
<p><code>struct co_context</code>
仅存储协程的上下文。我们还需要维护给协程分配的栈空间、记录入口函数地址等。我们定义
<code>struct coroutine</code> 表示协程对象。</p>
<pre><code class="hljs c"><span class="hljs-keyword">typedef</span> <span class="hljs-title function_">void</span> <span class="hljs-params">(*start_coroutine)</span><span class="hljs-params">()</span>;<br><br><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">coroutine</span> &#123;</span><br>    <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">co_context</span> <span class="hljs-title">ctx</span>;</span><br>    <span class="hljs-type">char</span> *<span class="hljs-built_in">stack</span>;<br>    <span class="hljs-type">size_t</span> stack_size;<br>    start_coroutine start;<br>&#125;;<br><br><span class="hljs-keyword">struct</span> coroutine *<span class="hljs-title function_">co_new</span><span class="hljs-params">(start_coroutine start, <span class="hljs-type">size_t</span> stack_size)</span> &#123;<br>    <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">coroutine</span> *<span class="hljs-title">co</span> =</span> <span class="hljs-built_in">malloc</span>(<span class="hljs-keyword">sizeof</span>(<span class="hljs-keyword">struct</span> coroutine));<br>    <span class="hljs-built_in">memset</span>(&amp;co-&gt;ctx, <span class="hljs-number">0</span>, <span class="hljs-keyword">sizeof</span>(co-&gt;ctx));<br>    <span class="hljs-keyword">if</span> (stack_size) &#123;<br>        co-&gt;<span class="hljs-built_in">stack</span> = <span class="hljs-built_in">malloc</span>(stack_size);<br>    &#125; <span class="hljs-keyword">else</span> &#123;<br>        co-&gt;<span class="hljs-built_in">stack</span> = <span class="hljs-literal">NULL</span>;<br>    &#125;<br>    co-&gt;stack_size = stack_size;<br>    co-&gt;start = start;<br><br>    <span class="hljs-keyword">return</span> co;<br>&#125;<br><br><span class="hljs-type">void</span> <span class="hljs-title function_">co_free</span><span class="hljs-params">(<span class="hljs-keyword">struct</span> coroutine *co)</span> &#123;<br>    <span class="hljs-built_in">free</span>(co-&gt;<span class="hljs-built_in">stack</span>);<br>    <span class="hljs-built_in">free</span>(co);<br>&#125;<br></code></pre>
<p><code>co_new</code> 创建一个新协程，接受两个参数：<code>start</code>
协程入口函数指针，和 <code>stack_size</code> 栈大小；这类似于
<code>pthread_create</code>。<code>co_new</code>
分配协程的栈空间并设置好各个字段。</p>
<p>要把主线程切换到我们新创建的协程，这里有两个问题。一是主线程并不是一个协程，新协程跟谁交换上下文呢？二是新创建的协程的上下文是空的（19
行），切换过去肯定跑不起来。</p>
<p>第一个问题很简单：创建一个就行。因为主线程已经跑起来了，要切换到新协程，主线程只需要一个“容器”把它的上下文装进去。直接执行
<code>main = co_new(NULL, 0)</code> 创建主协程，调用
<code>co_ctx_swap(&amp;main-&gt;ctx, &amp;new-&gt;ctx)</code>
便可切换到新协程。此时主线（协）程的上下文保存在 <code>main</code>
中，当新协程反向调用
<code>co_ctx_swap(&amp;new-&gt;ctx, &amp;main-&gt;ctx)</code>，便又切换回主协程了。</p>
<p>为了解决第二个问题，我们需要对新协程初始化。<code>co_ctx_swap</code>
将新协程的上下文复制到 CPU 后，执行 <code>ret</code>
返回栈顶记录的地址。因此我们要将栈顶置为协程入口函数的地址，这样在
<code>co_ctx_swap</code> 返回后便跳转到协程入口函数了。</p>
<pre><code class="hljs c"><span class="hljs-type">void</span> <span class="hljs-title function_">co_ctx_make</span><span class="hljs-params">(<span class="hljs-keyword">struct</span> coroutine *co)</span> &#123;<br>    <span class="hljs-type">char</span> *sp = co-&gt;<span class="hljs-built_in">stack</span> + co-&gt;stack_size - <span class="hljs-keyword">sizeof</span>(<span class="hljs-type">void</span>*);<br>    sp = (<span class="hljs-type">char</span>*)((<span class="hljs-type">intptr_t</span>)sp &amp; <span class="hljs-number">-16LL</span>);<br>    *(<span class="hljs-type">void</span>**)sp = (<span class="hljs-type">void</span>*)co-&gt;start;<br>    co-&gt;ctx.regs[CO_RSP] = sp;<br>&#125;<br></code></pre>
<p>因为 x86 的栈是从高地址向低地址增长的，初始栈为空，所以栈顶应该指向
<code>co-&gt;stack</code> 的最末尾。又因为 x86 的栈必须 16
字节对齐，所以执行 <code>(intptr_t)sp &amp; -16LL</code>（-16 低 4 位为
0，其它都为 1）得到栈顶地址。然后将栈顶置为
<code>co-&gt;start</code>，也就是入口函数的地址。最后我们把保存的 rsp
寄存器的值设置为栈顶地址，这个值会在 <code>co_ctx_swap</code>
被复制到寄存器 <code>%rsp</code> 中。</p>
<p>现在我们的协程已经可以跑起来了。写一个简单的例子试试：</p>
<pre><code class="hljs c"><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">coroutine</span> *<span class="hljs-title">main_co</span>, *<span class="hljs-title">new_co</span>;</span><br><br><span class="hljs-type">void</span> <span class="hljs-title function_">foo</span><span class="hljs-params">()</span> &#123;<br>    <span class="hljs-built_in">printf</span>(<span class="hljs-string">&quot;here is the new coroutine\n&quot;</span>);<br>    co_ctx_swap(&amp;new_co-&gt;ctx, &amp;main_co-&gt;ctx);<br>    <span class="hljs-built_in">printf</span>(<span class="hljs-string">&quot;new coroutine resumed\n&quot;</span>);<br>    co_ctx_swap(&amp;new_co-&gt;ctx, &amp;main_co-&gt;ctx);<br>&#125;<br><br><span class="hljs-type">int</span> <span class="hljs-title function_">main</span><span class="hljs-params">()</span> &#123;<br>    main_co = co_new(<span class="hljs-literal">NULL</span>, <span class="hljs-number">0</span>);<br>    new_co = co_new(foo, <span class="hljs-number">1024</span> * <span class="hljs-number">1024</span>);<br>    co_ctx_make(new_co);<br><br>    co_ctx_swap(&amp;main_co-&gt;ctx, &amp;new_co-&gt;ctx);<br>    <span class="hljs-built_in">printf</span>(<span class="hljs-string">&quot;main coroutine here\n&quot;</span>);<br>    co_ctx_swap(&amp;main_co-&gt;ctx, &amp;new_co-&gt;ctx);<br>    <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;<br>&#125;<br></code></pre>
<p>把上面所有的 C 代码复制到文件 <code>co.c</code>，汇编代码存为
<code>co.S</code>，然后执行 <code>gcc -o co co.c co.S</code>
编译，运行试试！</p>
<h2 id="协程的管理">协程的管理</h2>
<p>现在的协程虽然可以跑，但是使用起来很不方便，需要手动交换上下文，也容易出错。我们需要实现
resume/yield 操作。resume
操作唤醒指定协程，也就是当前协程与指定协程交换。yield
挂起当前协程，将当前协程与上次唤醒它的协程交换。因此我们需要记录当前运行的协程；而对于每个协程，要保存唤醒它的协程的指针。</p>
<p><img src="/assets/images/c-coroutine_2.svg" ></p>
<p>协程切换要遵循这几条规则：</p>
<ul>
<li>主协程不能执行 yield 操作。这是显而易见的，因为它没有唤醒者。</li>
<li>不能 resume 一个正在运行的协程。</li>
<li>如果一个协程通过 resume 操作进入挂起状态，则不能由 resume
操作唤醒。例如，上图所示的协程 B 在 resume 协程 C 后，只能被协程 C 的
yield 操作唤醒。如果允许其它协程通过 resume
操作唤醒它，则协程切换会陷入混乱。</li>
<li>除主协程外的协程结束时需要执行 yield
操作，之后进入死亡状态。死亡状态的协程不能被 resume 操作唤醒。</li>
</ul>
<p>基于此，我们给协程定义五个状态：</p>
<pre><code class="hljs c"><span class="hljs-class"><span class="hljs-keyword">enum</span> &#123;</span><br>    CO_STATUS_INIT,         <span class="hljs-comment">// 初始状态</span><br>    CO_STATUS_PENDING,      <span class="hljs-comment">// 执行 yield 操作进入的挂起状态</span><br>    CO_STATUS_NORMAL,       <span class="hljs-comment">// 执行 resume 操作进入的挂起状态</span><br>    CO_STATUS_RUNNING,      <span class="hljs-comment">// 运行状态</span><br>    CO_STATUS_DEAD,         <span class="hljs-comment">// 死亡状态</span><br>&#125;;<br></code></pre>
<p>我们使用全局变量 <code>g_curr_co</code>
记录当前协程。每个协程还要记录当前状态和唤醒自己的协程。</p>
<pre><code class="hljs c"><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">coroutine</span> &#123;</span><br>    <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">co_context</span> <span class="hljs-title">ctx</span>;</span><br>    <span class="hljs-type">char</span> *<span class="hljs-built_in">stack</span>;<br>    <span class="hljs-type">size_t</span> stack_size;<br>    <span class="hljs-type">int</span> status;                 <span class="hljs-comment">// 协程状态</span><br>    <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">coroutine</span> *<span class="hljs-title">prev</span>;</span>     <span class="hljs-comment">// 唤醒者</span><br>    start_coroutine start;<br>&#125;;<br><br><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">coroutine</span> *<span class="hljs-title">g_curr_co</span> =</span> <span class="hljs-literal">NULL</span>;     <span class="hljs-comment">// 当前协程</span><br><br><span class="hljs-keyword">struct</span> coroutine *<span class="hljs-title function_">co_new</span><span class="hljs-params">(start_coroutine start, <span class="hljs-type">size_t</span> stack_size)</span> &#123;<br>    <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">coroutine</span> *<span class="hljs-title">co</span> =</span> <span class="hljs-built_in">malloc</span>(<span class="hljs-keyword">sizeof</span>(<span class="hljs-keyword">struct</span> coroutine));<br>    ...<br>    co-&gt;status = CO_STATUS_INIT;<br>    co-&gt;prev = <span class="hljs-literal">NULL</span>;<br>    <span class="hljs-keyword">return</span> co;<br>&#125;<br><br><span class="hljs-type">void</span> <span class="hljs-title function_">check_init</span><span class="hljs-params">()</span> &#123;<br>    <span class="hljs-keyword">if</span> (!g_curr_co) &#123;   <span class="hljs-comment">// 初始化主协程</span><br>        g_curr_co = co_new(<span class="hljs-literal">NULL</span>, <span class="hljs-number">0</span>);<br>        g_curr_co-&gt;status = CO_STATUS_RUNNING;  <span class="hljs-comment">// 主协程状态初始为 RUNNING</span><br>    &#125;<br>&#125;<br></code></pre>
<p>接着实现 resume 操作和 yield
操作。根据上面描述的思路，实现起来很容易。</p>
<pre><code class="hljs c"><span class="hljs-type">int</span> <span class="hljs-title function_">co_resume</span><span class="hljs-params">(<span class="hljs-keyword">struct</span> coroutine *next)</span> &#123;<br>    check_init();<br><br>    <span class="hljs-keyword">switch</span> (next-&gt;status) &#123;<br>        <span class="hljs-keyword">case</span> CO_STATUS_INIT:        <span class="hljs-comment">// 初始状态，需要执行 co_ctx_make 初始化</span><br>            co_ctx_make(next);<br>        <span class="hljs-keyword">case</span> CO_STATUS_PENDING:     <span class="hljs-comment">// 只有处于 INIT 和 PENDING 状态的协程可以被 resume 唤醒</span><br>            <span class="hljs-keyword">break</span>;<br>        <span class="hljs-keyword">default</span>:<br>            <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;<br>    &#125;<br><br>    <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">coroutine</span> *<span class="hljs-title">curr</span> =</span> g_curr_co;<br>    g_curr_co = next;                   <span class="hljs-comment">// g_curr_co 指向新协程</span><br>    next-&gt;prev = curr;                  <span class="hljs-comment">// 设置新协程的唤醒者为当前协程</span><br>    curr-&gt;status = CO_STATUS_NORMAL;    <span class="hljs-comment">// 当前协程进入 NORMAL 状态</span><br>    next-&gt;status = CO_STATUS_RUNNING;   <span class="hljs-comment">// 新协程进入 RUNNING 状态</span><br>    co_ctx_swap(&amp;curr-&gt;ctx, &amp;next-&gt;ctx);<br><br>    <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;<br>&#125;<br><br><span class="hljs-type">int</span> <span class="hljs-title function_">co_yield</span><span class="hljs-params">()</span> &#123;<br>    check_init();<br><br>    <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">coroutine</span> *<span class="hljs-title">curr</span> =</span> g_curr_co;<br>    <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">coroutine</span> *<span class="hljs-title">prev</span> =</span> curr-&gt;prev;<br><br>    <span class="hljs-keyword">if</span> (!prev) &#123;    <span class="hljs-comment">// 没有唤醒者，不能执行 yield 操作</span><br>        <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;<br>    &#125;<br><br>    g_curr_co = prev;       <span class="hljs-comment">// g_curr_co 指向当前协程的唤醒者</span><br>    <span class="hljs-keyword">if</span> (curr-&gt;status != CO_STATUS_DEAD) &#123;<br>        curr-&gt;status = CO_STATUS_PENDING;   <span class="hljs-comment">// 当前协程进入 PENDING 状态</span><br>    &#125;<br>    prev-&gt;status = CO_STATUS_RUNNING;       <span class="hljs-comment">// 唤醒者进入 RUNNING 状态</span><br>    co_ctx_swap(&amp;curr-&gt;ctx, &amp;prev-&gt;ctx);<br><br>    <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;<br>&#125;<br></code></pre>
<p>除主协程外的协程结束运行时一定要执行 yield
操作将自己切出，否则它不知道该返回到哪儿。为了不让使用者手动执行这个操作，我们将协程入口函数封装一层。</p>
<pre><code class="hljs c"><span class="hljs-type">void</span> <span class="hljs-title function_">co_entrance</span><span class="hljs-params">(<span class="hljs-keyword">struct</span> coroutine *co)</span> &#123;<br>    co-&gt;start(); <span class="hljs-comment">// 执行入口函数</span><br>    co-&gt;status = CO_STATUS_DEAD;<br>    co_yield(); <span class="hljs-comment">// 已经置为 DEAD 状态了，切出后不会有人唤醒它了。这里 co_yield 永远不会返回</span><br>    <span class="hljs-comment">// 不会走到这里来</span><br>&#125;<br><br><span class="hljs-type">void</span> <span class="hljs-title function_">co_ctx_make</span><span class="hljs-params">(<span class="hljs-keyword">struct</span> coroutine *co)</span> &#123;<br>    <span class="hljs-type">char</span> *sp = co-&gt;<span class="hljs-built_in">stack</span> + co-&gt;stack_size - <span class="hljs-keyword">sizeof</span>(<span class="hljs-type">void</span>*);<br>    sp = (<span class="hljs-type">char</span>*)((<span class="hljs-type">intptr_t</span>)sp &amp; <span class="hljs-number">-16LL</span>);<br>    *(<span class="hljs-type">void</span>**)sp = (<span class="hljs-type">void</span>*)co_entrance;   <span class="hljs-comment">// 设置入口地址为 co_entrance</span><br>    co-&gt;ctx.regs[CO_RSP] = sp;<br>    co-&gt;ctx.regs[CO_RDI] = co; <span class="hljs-comment">// rdi 为第一参数寄存器，将它的值置为 co，这样 co_entrance 就能拿到它的参数了</span><br>&#125;<br></code></pre>
<p>这样我们的协程用起来就更方便了:</p>
<pre><code class="hljs c"><span class="hljs-type">void</span> <span class="hljs-title function_">foo</span><span class="hljs-params">()</span> &#123;<br>    <span class="hljs-built_in">printf</span>(<span class="hljs-string">&quot;here is the new coroutine\n&quot;</span>);<br>    co_yield();<br>    <span class="hljs-built_in">printf</span>(<span class="hljs-string">&quot;new coroutine resumed\n&quot;</span>);<br>&#125;<br><br><span class="hljs-type">int</span> <span class="hljs-title function_">main</span><span class="hljs-params">()</span> &#123;<br>    <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">coroutine</span> *<span class="hljs-title">co</span> =</span> co_new(foo, <span class="hljs-number">1024</span> * <span class="hljs-number">1024</span>);<br>    co_resume(co);<br>    <span class="hljs-built_in">printf</span>(<span class="hljs-string">&quot;main coroutine here\n&quot;</span>);<br>    co_resume(co);<br>    <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;<br>&#125;<br></code></pre>
<h2 id="传递参数">传递参数</h2>
<p>resume/yield 可以用于传递参数。运行上面的例子，我们发现
<code>co_yield</code> 返回之时便是其它协程调用 <code>co_resume</code>
之时；而 <code>co_resume</code> 返回之时又是其它协程调用
<code>co_yield</code> 之时。因此 resume 操作接受参数，传递给 yield
返回；yield 操作接受参数，传递给 resume
返回。这样方便在协程之间传递数据。</p>
<p>我们在 <code>struct coroutine</code> 中新增一个 <code>data</code>
字段用于传递参数。协程切换时，如果要给目标协程传递参数，就对目标协程的
<code>data</code> 字段赋值。协程切换后，就能从 <code>data</code>
字段中取出上一个协程传递的参数。</p>
<pre><code class="hljs c"><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">coroutine</span> &#123;</span><br>    ...<br>    <span class="hljs-type">void</span> *data;     <span class="hljs-comment">// 参数</span><br>&#125;;<br><br><span class="hljs-type">int</span> <span class="hljs-title function_">co_resume</span><span class="hljs-params">(<span class="hljs-keyword">struct</span> coroutine *next, <span class="hljs-type">void</span> *param, <span class="hljs-type">void</span> **result)</span> &#123;<br>    ...<br><br>    next-&gt;data = param;         <span class="hljs-comment">// 切换到 next 协程，给 next 协程的参数</span><br>    co_ctx_swap(&amp;curr-&gt;ctx, &amp;next-&gt;ctx);<br>    <span class="hljs-keyword">if</span> (result) &#123;<br>        *result = curr-&gt;data;<br>    &#125;<br>    <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;<br>&#125;<br><br><span class="hljs-type">int</span> <span class="hljs-title function_">co_yield</span><span class="hljs-params">(<span class="hljs-type">void</span> *result, <span class="hljs-type">void</span> **param)</span> &#123;<br>    ...<br><br>    prev-&gt;data = result;        <span class="hljs-comment">// 切回 prev 协程，给 prev 协程的结果</span><br>    co_ctx_swap(&amp;curr-&gt;ctx, &amp;prev-&gt;ctx);<br>    <span class="hljs-keyword">if</span> (param) &#123;<br>        *param = curr-&gt;data;    <span class="hljs-comment">// 其它协程唤醒它时给它的参数</span><br>    &#125;<br>    <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;<br>&#125;<br></code></pre>
<p>我们重新定义协程入口函数，让它接受参数和返回值。第一次 resume
的参数传给入口函数；入口函数的返回值在最后一次 yield 时传出去。</p>
<pre><code class="hljs c"><span class="hljs-keyword">typedef</span> <span class="hljs-type">void</span> *(*start_coroutine)(<span class="hljs-type">void</span> *);<br><br><span class="hljs-type">static</span> <span class="hljs-type">void</span> <span class="hljs-title function_">co_entrance</span><span class="hljs-params">(<span class="hljs-keyword">struct</span> coroutine *co)</span> &#123;<br>    <span class="hljs-type">void</span> *result = co-&gt;start(co-&gt;data);<br>    co-&gt;status = CO_STATUS_DEAD;<br>    co_yield(result, <span class="hljs-literal">NULL</span>); <span class="hljs-comment">// 协程的最后一次 yield 操作，将入口函数的返回值传出去</span><br>&#125;<br></code></pre>
<h2 id="例子">例子</h2>
<p>现在，我们的协程库已经完全实现了。我们可以写一些例子测试一下。比如说我们可以创建一个源源不断生成以
n 开头的自然数的协程：</p>
<pre><code class="hljs c"><span class="hljs-type">void</span> *<span class="hljs-title function_">number</span><span class="hljs-params">(<span class="hljs-type">void</span> *param)</span> &#123;<br>    <span class="hljs-type">intptr_t</span> i = (<span class="hljs-type">intptr_t</span>)param;<br>    co_yield(<span class="hljs-literal">NULL</span>, <span class="hljs-literal">NULL</span>);   <span class="hljs-comment">// 初始化后立刻 yield</span><br>    <span class="hljs-keyword">while</span> (<span class="hljs-number">1</span>) &#123;<br>        co_yield((<span class="hljs-type">void</span>*)i, <span class="hljs-literal">NULL</span>);<br>        ++i;<br>    &#125;<br>&#125;<br><br><span class="hljs-type">int</span> <span class="hljs-title function_">main</span><span class="hljs-params">()</span> &#123;<br>    <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">coroutine</span> *<span class="hljs-title">num</span> =</span> co_new(number, <span class="hljs-number">1024</span> * <span class="hljs-number">1024</span>);<br>    co_resume(num, (<span class="hljs-type">void</span>*)<span class="hljs-number">0</span>, <span class="hljs-literal">NULL</span>); <span class="hljs-comment">// 初始化为以 0 开头的自然数流</span><br>    <span class="hljs-keyword">for</span> (<span class="hljs-type">int</span> i = <span class="hljs-number">0</span>; i &lt; <span class="hljs-number">10</span>; ++i) &#123;<br>        <span class="hljs-type">intptr_t</span> n;<br>        co_resume(num, <span class="hljs-literal">NULL</span>, (<span class="hljs-type">void</span>**)&amp;n);<br>        <span class="hljs-built_in">printf</span>(<span class="hljs-string">&quot;%ld &quot;</span>, n);<br>    &#125;<br>    co_free(num);<br>    <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;<br>&#125;<br></code></pre>
<p>运行结果就是</p>
<pre><code class="hljs txt">0 1 2 3 4 5 6 7 8 9<br></code></pre>
<p>这个协程就是一个无限流。我们还可以写一个将两个无限流逐项相加的协程：</p>
<pre><code class="hljs c"><span class="hljs-type">void</span> *<span class="hljs-title function_">add</span><span class="hljs-params">(<span class="hljs-type">void</span> *param)</span> &#123;<br>    <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">coroutine</span> **<span class="hljs-title">cov</span> =</span> param, *co0 = cov[<span class="hljs-number">0</span>], *co1 = cov[<span class="hljs-number">1</span>]; <span class="hljs-comment">// cov 指向前序协程的栈，这里要立刻将其中的数据取出来</span><br>    co_yield(<span class="hljs-literal">NULL</span>, <span class="hljs-literal">NULL</span>);   <span class="hljs-comment">// 同样，初始化后立刻 yield</span><br>    <span class="hljs-keyword">while</span> (<span class="hljs-number">1</span>) &#123;<br>        <span class="hljs-type">intptr_t</span> a, b;<br>        co_resume(co0, <span class="hljs-literal">NULL</span>, (<span class="hljs-type">void</span>**)&amp;a);<br>        co_resume(co1, <span class="hljs-literal">NULL</span>, (<span class="hljs-type">void</span>**)&amp;b);<br>        co_yield((<span class="hljs-type">void</span>*)(a + b), <span class="hljs-literal">NULL</span>);<br>    &#125;<br>&#125;<br></code></pre>
<p>然后将 0 开头的自然数流与 1 开头的自然数流逐项相加，得到奇数无限流（0
+ 1 = 1, 1 + 2 = 3, 2 + 3 = 5, …）</p>
<pre><code class="hljs c"><span class="hljs-type">int</span> <span class="hljs-title function_">main</span><span class="hljs-params">()</span> &#123;<br>    <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">coroutine</span> *<span class="hljs-title">num0</span> =</span> co_new(number, <span class="hljs-number">1024</span> * <span class="hljs-number">1024</span>);<br>    <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">coroutine</span> *<span class="hljs-title">num1</span> =</span> co_new(number, <span class="hljs-number">1024</span> * <span class="hljs-number">1024</span>);<br>    <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">coroutine</span> *<span class="hljs-title">co_add</span> =</span> co_new(add, <span class="hljs-number">1024</span> * <span class="hljs-number">1024</span>);<br>    <br>    co_resume(num0, (<span class="hljs-type">void</span>*)<span class="hljs-number">0</span>, <span class="hljs-literal">NULL</span>); <span class="hljs-comment">// 以 0 开头的自然数流</span><br>    co_resume(num1, (<span class="hljs-type">void</span>*)<span class="hljs-number">1</span>, <span class="hljs-literal">NULL</span>); <span class="hljs-comment">// 以 1 开头的自然数流</span><br>    <br>    <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">coroutine</span> *<span class="hljs-title">cov</span>[] =</span> &#123;num0, num1&#125;;<br>    co_resume(co_add, cov, <span class="hljs-literal">NULL</span>);   <span class="hljs-comment">// 初始化 add 协程</span><br><br>    <span class="hljs-keyword">for</span> (<span class="hljs-type">int</span> i = <span class="hljs-number">0</span>; i &lt; <span class="hljs-number">10</span>; ++i) &#123;<br>        <span class="hljs-type">intptr_t</span> s;<br>        co_resume(co_add, <span class="hljs-literal">NULL</span>, (<span class="hljs-type">void</span>**)&amp;s);<br>        <span class="hljs-built_in">printf</span>(<span class="hljs-string">&quot;%ld &quot;</span>, s);<br>    &#125;<br><br>    co_free(num0), co_free(num1), co_free(co_add);<br>    <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;<br>&#125;<br></code></pre>
<p>运行结果就是</p>
<pre><code class="hljs txt">1 3 5 7 9 11 13 15 17 19<br></code></pre>
<p>当然还有更好玩的。我们可以实现一个斐波那契数列生成器。斐波那契数列可以自我定义：令
f(i) 是以第 i 项开头的斐波那契数列，f(a) + f(b)
表示将两个数列逐项相加，那么如下所示，f(2) = f(0) + f(1)。</p>
<pre><code class="hljs txt">    0  1  1  2  3  5<br>+   1  1  2  3  5  8<br>-----------------------<br>    1  2  3  5  8  13<br></code></pre>
<p>所以我们可以这样做</p>
<pre><code class="hljs c"><span class="hljs-type">void</span> *<span class="hljs-title function_">fib</span><span class="hljs-params">(<span class="hljs-type">void</span> *param)</span> &#123;<br>    co_yield((<span class="hljs-type">void</span>*)<span class="hljs-number">0</span>, <span class="hljs-literal">NULL</span>);   <span class="hljs-comment">// 第 0 项</span><br>    co_yield((<span class="hljs-type">void</span>*)<span class="hljs-number">1</span>, <span class="hljs-literal">NULL</span>);   <span class="hljs-comment">// 第 1 项</span><br>    <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">coroutine</span> *<span class="hljs-title">f0</span> =</span> co_new(fib, <span class="hljs-number">1024</span> * <span class="hljs-number">1024</span>);<br>    <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">coroutine</span> *<span class="hljs-title">f1</span> =</span> co_new(fib, <span class="hljs-number">1024</span> * <span class="hljs-number">1024</span>);<br>    co_resume(f1, <span class="hljs-literal">NULL</span>, <span class="hljs-literal">NULL</span>); <span class="hljs-comment">// f1 先走一步，让它成为以第 1 项开头的斐波那契数列</span><br><br>    <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">coroutine</span> *<span class="hljs-title">co_add</span> =</span> co_new(add, <span class="hljs-number">1024</span> * <span class="hljs-number">1024</span>);<br>    <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">coroutine</span> *<span class="hljs-title">cov</span>[] =</span> &#123;f0, f1&#125;;<br>    co_resume(co_add, cov, <span class="hljs-literal">NULL</span>); <span class="hljs-comment">// 将 f0 与 f1 逐项相加</span><br>    <span class="hljs-keyword">while</span> (<span class="hljs-number">1</span>) &#123;<br>        <span class="hljs-type">intptr_t</span> s;<br>        co_resume(co_add, <span class="hljs-literal">NULL</span>, (<span class="hljs-type">void</span>**)&amp;s);<br>        co_yield((<span class="hljs-type">void</span>*)s, <span class="hljs-literal">NULL</span>);<br>    &#125;<br>&#125;<br><br><span class="hljs-type">int</span> <span class="hljs-title function_">main</span><span class="hljs-params">()</span> &#123;<br>    <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">coroutine</span> *<span class="hljs-title">f</span> =</span> co_new(fib, <span class="hljs-number">1024</span> * <span class="hljs-number">1024</span>);<br>    <span class="hljs-keyword">for</span> (<span class="hljs-type">int</span> i = <span class="hljs-number">0</span>; i &lt; <span class="hljs-number">10</span>; ++i) &#123;<br>        <span class="hljs-type">intptr_t</span> s;<br>        co_resume(f, <span class="hljs-literal">NULL</span>, (<span class="hljs-type">void</span>**)&amp;s);<br>        <span class="hljs-built_in">printf</span>(<span class="hljs-string">&quot;%ld &quot;</span>, s);<br>    &#125;<br>&#125;<br></code></pre>
<p>运行结果便是斐波那契数列：</p>
<pre><code class="hljs txt">0 1 1 2 3 5 8 13 21 34<br></code></pre>
<p>不过这种写法会创建大量协程，性能很低。仅供演示<span style="background-color: var(--post-text-color)"><del>（炫技）</del></span>。</p>
<hr>
<section class="footnotes">
<div class="footnote-list">
<ol>
<li>
<span id="fn:1" class="footnote-text"><span>对于 Linux
内核而言，线程和进程是同一个东西
<a href="#fnref:1" rev="footnote" class="footnote-backref">
↩︎</a></span></span>
</li>
</ol>
</div>
</section>
]]></content>
    
    
    <summary type="html">协程与线程（进程）[1]都是模拟多任务（routine）并发执行的软件实现。操作系统线程在一个
CPU
线程中模拟并发执行多个任务，而协程（coroutine）在一个操作系统线程中模拟并发执行多个任务。
因此协程也被称为“用户态线程”、“轻量级线程”。之所以说“模拟并发执行”，是因为任务实际并没有同</summary>
    
    
    
    
    <category term="featured" scheme="https://luyuhuang.tech/tags/featured/"/>
    
    <category term="c/c++" scheme="https://luyuhuang.tech/tags/c-c/"/>
    
    <category term="assembly" scheme="https://luyuhuang.tech/tags/assembly/"/>
    
  </entry>
  
  <entry>
    <title>理想国</title>
    <link href="https://luyuhuang.tech/2024/06/08/republic.html"/>
    <id>https://luyuhuang.tech/2024/06/08/republic.html</id>
    <published>2024-06-07T16:00:00.000Z</published>
    <updated>2025-06-26T03:59:29.937Z</updated>
    
    <content type="html"><![CDATA[<p>我最近读完了《理想国》。这本书为古希腊哲学先贤柏拉图所著，是哲学经典之作、奠基之作，内容晦涩深奥，比较难懂。本人才疏学浅，以我的贫乏的哲学素养，难以完全领会其中深邃的思想。然而读完之后，仍然对我的价值观造成了不小的冲击。这里斗胆分享两点我的体会。</p>
<h2 id="什么是正义">什么是正义</h2>
<p>中国有句古话，叫做成王败寇。胜者为正义，败者为邪恶。蒙古铁蹄南下，击败南宋，占据中原，成为正统王朝，自然是正义。元末朱元璋起兵驱逐鞑虏，恢复中华，自然也是正义。但如果朱元璋失败了会怎样？自然是和其它千千万万失败农民起义一样，被列为乱臣贼子，成为邪恶的化身。正义与邪恶似乎是相对的概念，胜利者定义的价值即为正义。“邪不压正”不是规律，而是结果：正是因为甲战胜了乙，甲便是“正”；如果“邪”压住了“正”，“邪”就变成了正。</p>
<p>我曾经一度相信这种观点。然而柏拉图全然否定了这个观点。因为如果把胜利者定义为正义，反对胜利者定义为邪恶，我们可以作出这样的推导：</p>
<p>胜利者是正义的，所以胜利者制定的法律也是正义的。因此遵纪守法是正义的，违法犯罪是不正义的。</p>
<p>那么按照现行法律，谋杀是不正义的、抢劫是不正义的、盗窃是不正义的、贪污是不正义的、诈骗是不正义的。</p>
<p>与这些行为相关的品质也是不正义的。所以伤害他人是不正义的、损人利己是不正义的、贪婪是不正义的、奸诈狡猾是不正义的。</p>
<p>我们说一个贪婪、损人利己、奸诈狡猾的人是坏人，他会做坏事。个人的力量有限，他能做的坏事相对较小。我们试图让一群这样的坏人聚在一起，看看他们能不能做一件更大的坏事，甚至是最大的坏事：把正义——当前的胜利者——推翻？</p>
<p>然而他们会让我们失望的：一群贪婪、损人利己、奸诈狡猾的人，无论如何不能团结起来。越是不正义的个人，越不能团结成不正义的群体。因此不正义的人终究无法完成任何伟业。书中原话是这么说的：</p>
<blockquote>
<p>我们看到正义的人的确更聪明能干更好，而不正义的人根本不能合作。当我们说不正义者可以有坚强一致的行动，我们实在说得有点不对头。因为他们要是绝对违反正义，结果非内讧不可。他们残害敌人，而不至于自相残杀，还是因为他们之间多少还有点正义。就凭这么一点儿正义，才使他们做事好歹有点成果；而他们之间的不正义对他们的作恶也有相当的妨碍。因为绝对不正义的真正坏人，也就绝对做不出任何事情来。</p>
</blockquote>
<p>因此我们发现，即使是黑帮，也要讲究道义，要求遵守纪律。他们能在一定程度上团结在一起，正是因为他们还有一些正义。蒙古能灭南宋，因为蒙古有一定的正义；而南宋，却冤杀岳飞，昏庸腐败，致使生灵涂炭，怎能说它没有不正义。而元朝末年亦是“人心离叛，天下兵起，使我中国之民，死者肝脑涂地，生者骨肉不相保”，也是它的不正义导致了灭亡。</p>
<p>所以无论怎么改朝换代，无论谁是胜利者，法律永远禁止谋杀、抢劫、盗窃、贪污和诈骗。所以邪不压正，是因为邪本来就不压正，这是规律，不是结果。</p>
<p>社会上流行这样的观点：好人和坏人是相对的，只是立场不同；小孩子才讲对错，大人只看利弊。但这种善恶相对的观点很危险。如果认为正义与邪恶是相对的概念，就不会相信真正的良善是存在的。人就可能会走邪路，成为自私自利、损人利己、为达目的不择手段的人。</p>
<p>为了探寻什么是正义，柏拉图构想了一个理想的城邦，一个“理想国”。在这个城邦里，不同的人分工合作，每个人在国家里执行一种最适合他天性的职务。工匠制作工具，农民种田，皮匠做鞋，以及“爱智慧的人<sup id="fnref:1" class="footnote-ref"><a href="#fn:1" rel="footnote"><span class="hint--top hint--rounded" aria-label="哲学家 philosopher 字面意思为“爱智慧的人”。philo- 爱，sophia 智慧。">[1]</span></a></sup>”担任领导者。因为柏拉图认为只有智慧和理性才能让这个城邦在各种情况下做出最正确的决策。在智慧和理性的领导下，这个城邦训练勇敢的护卫者，用音乐和体操教化人民。这样的城邦是智慧的、理性的、勇敢的、节制的。一个这样的城邦便是正义的城邦。</p>
<p>柏拉图将人与城邦类比。一个城邦有形形色色的人，一个人内心也有不同的部分。一个人内心有受<strong>理性</strong>控制的部分，也有受<strong>欲望</strong>控制的非理性的部分，也有受<strong>激情</strong>控制的部分。柏拉图认为在这三部分中，理性的部分应该担任“领导者”，就像理性的人在应当在城邦担任领导者一样。</p>
<blockquote>
<p>理智既然是智慧的，是为整个心灵的利益而谋划的，还不应该由它起领导作用吗？激情不应该服从它和协助它吗？</p>
</blockquote>
<blockquote>
<p>正义的人不许可自己灵魂里的各个部分相互干涉，起别的部分的作用。他应当安排好真正自己的事情，首先达到自己主宰自己，自身内秩序井然，对自己友善。</p>
</blockquote>
<blockquote>
<p>不正义应该就是三种部分之间的争斗不和、相互间管闲事和相互干涉，灵魂的一个部分起而反对整个灵魂，企图在内部取得领导地位——它天生就不应该领导的而是应该像奴隶一样为统治部分服务的，——不是吗？我觉得我们要说的正是这种东西。</p>
</blockquote>
<p>我们常说“做自己的主人”，柏拉图说人怎么才能做自己的主人呢？因为如果说一个人是自己的主人，那他同时也是自己的奴隶。他认为这句话的意思是，一个人内心理性的部分要做非理性部分的主人。理性会让人追求智慧，会在必要时抑制欲望与激情；在他需要战斗时，又会释放激情。这样，这个人便是智慧的、理性的、勇敢的、节制的。这样的人便是正义的人。</p>
<h2 id="什么是快乐">什么是快乐</h2>
<p>我曾经认为，人的快乐不取决于人拥有多少物质，而取决于拥有的物质的变化。例如，假设你在路上捡到一千块钱，你会快乐，但仅限于得到这些钱的这一刻。你不会因为资产增加了一千元而一直开心。再比如年收入只有
10 万的时候会想，要是我一年能赚 20 万就好了。然而当收入真的变为 20
万时，他会发现快乐只存在于收入变化的这一小段时间，之后便开始追求更高的收入了。痛苦便与之相反：当人失去物质时，会感受到痛苦，但痛苦也仅存在于失去的这一刻。我甚至提出了一个“快乐公式”：函数
<span class="math inline"><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.566ex;" xmlns="http://www.w3.org/2000/svg" width="3.821ex" height="2.262ex" role="img" focusable="false" viewBox="0 -750 1689 1000"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D453" d="M118 -162Q120 -162 124 -164T135 -167T147 -168Q160 -168 171 -155T187 -126Q197 -99 221 27T267 267T289 382V385H242Q195 385 192 387Q188 390 188 397L195 425Q197 430 203 430T250 431Q298 431 298 432Q298 434 307 482T319 540Q356 705 465 705Q502 703 526 683T550 630Q550 594 529 578T487 561Q443 561 443 603Q443 622 454 636T478 657L487 662Q471 668 457 668Q445 668 434 658T419 630Q412 601 403 552T387 469T380 433Q380 431 435 431Q480 431 487 430T498 424Q499 420 496 407T491 391Q489 386 482 386T428 385H372L349 263Q301 15 282 -47Q255 -132 212 -173Q175 -205 139 -205Q107 -205 81 -186T55 -132Q55 -95 76 -78T118 -61Q162 -61 162 -103Q162 -122 151 -136T127 -157L118 -162Z"></path></g><g data-mml-node="mo" transform="translate(550,0)"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z"></path></g><g data-mml-node="mi" transform="translate(939,0)"><path data-c="1D461" d="M26 385Q19 392 19 395Q19 399 22 411T27 425Q29 430 36 430T87 431H140L159 511Q162 522 166 540T173 566T179 586T187 603T197 615T211 624T229 626Q247 625 254 615T261 596Q261 589 252 549T232 470L222 433Q222 431 272 431H323Q330 424 330 420Q330 398 317 385H210L174 240Q135 80 135 68Q135 26 162 26Q197 26 230 60T283 144Q285 150 288 151T303 153H307Q322 153 322 145Q322 142 319 133Q314 117 301 95T267 48T216 6T155 -11Q125 -11 98 4T59 56Q57 64 57 83V101L92 241Q127 382 128 383Q128 385 77 385H26Z"></path></g><g data-mml-node="mo" transform="translate(1300,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z"></path></g></g></g></svg></mjx-container></span> 表示 <span class="math inline"><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.025ex;" xmlns="http://www.w3.org/2000/svg" width="0.817ex" height="1.441ex" role="img" focusable="false" viewBox="0 -626 361 637"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D461" d="M26 385Q19 392 19 395Q19 399 22 411T27 425Q29 430 36 430T87 431H140L159 511Q162 522 166 540T173 566T179 586T187 603T197 615T211 624T229 626Q247 625 254 615T261 596Q261 589 252 549T232 470L222 433Q222 431 272 431H323Q330 424 330 420Q330 398 317 385H210L174 240Q135 80 135 68Q135 26 162 26Q197 26 230 60T283 144Q285 150 288 151T303 153H307Q322 153 322 145Q322 142 319 133Q314 117 301 95T267 48T216 6T155 -11Q125 -11 98 4T59 56Q57 64 57 83V101L92 241Q127 382 128 383Q128 385 77 385H26Z"></path></g></g></g></svg></mjx-container></span> 时刻人拥有的物质，那么 <span class="math inline"><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.025ex;" xmlns="http://www.w3.org/2000/svg" width="0.817ex" height="1.441ex" role="img" focusable="false" viewBox="0 -626 361 637"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D461" d="M26 385Q19 392 19 395Q19 399 22 411T27 425Q29 430 36 430T87 431H140L159 511Q162 522 166 540T173 566T179 586T187 603T197 615T211 624T229 626Q247 625 254 615T261 596Q261 589 252 549T232 470L222 433Q222 431 272 431H323Q330 424 330 420Q330 398 317 385H210L174 240Q135 80 135 68Q135 26 162 26Q197 26 230 60T283 144Q285 150 288 151T303 153H307Q322 153 322 145Q322 142 319 133Q314 117 301 95T267 48T216 6T155 -11Q125 -11 98 4T59 56Q57 64 57 83V101L92 241Q127 382 128 383Q128 385 77 385H26Z"></path></g></g></g></svg></mjx-container></span> 时刻的快乐便是函数 <span class="math inline"><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.464ex;" xmlns="http://www.w3.org/2000/svg" width="1.244ex" height="2.059ex" role="img" focusable="false" viewBox="0 -705 550 910"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D453" d="M118 -162Q120 -162 124 -164T135 -167T147 -168Q160 -168 171 -155T187 -126Q197 -99 221 27T267 267T289 382V385H242Q195 385 192 387Q188 390 188 397L195 425Q197 430 203 430T250 431Q298 431 298 432Q298 434 307 482T319 540Q356 705 465 705Q502 703 526 683T550 630Q550 594 529 578T487 561Q443 561 443 603Q443 622 454 636T478 657L487 662Q471 668 457 668Q445 668 434 658T419 630Q412 601 403 552T387 469T380 433Q380 431 435 431Q480 431 487 430T498 424Q499 420 496 407T491 391Q489 386 482 386T428 385H372L349 263Q301 15 282 -47Q255 -132 212 -173Q175 -205 139 -205Q107 -205 81 -186T55 -132Q55 -95 76 -78T118 -61Q162 -61 162 -103Q162 -122 151 -136T127 -157L118 -162Z"></path></g></g></g></svg></mjx-container></span> 的导数 <span class="math inline"><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.566ex;" xmlns="http://www.w3.org/2000/svg" width="4.569ex" height="2.283ex" role="img" focusable="false" viewBox="0 -759 2019.5 1009"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msup"><g data-mml-node="mi"><path data-c="1D453" d="M118 -162Q120 -162 124 -164T135 -167T147 -168Q160 -168 171 -155T187 -126Q197 -99 221 27T267 267T289 382V385H242Q195 385 192 387Q188 390 188 397L195 425Q197 430 203 430T250 431Q298 431 298 432Q298 434 307 482T319 540Q356 705 465 705Q502 703 526 683T550 630Q550 594 529 578T487 561Q443 561 443 603Q443 622 454 636T478 657L487 662Q471 668 457 668Q445 668 434 658T419 630Q412 601 403 552T387 469T380 433Q380 431 435 431Q480 431 487 430T498 424Q499 420 496 407T491 391Q489 386 482 386T428 385H372L349 263Q301 15 282 -47Q255 -132 212 -173Q175 -205 139 -205Q107 -205 81 -186T55 -132Q55 -95 76 -78T118 -61Q162 -61 162 -103Q162 -122 151 -136T127 -157L118 -162Z"></path></g><g data-mml-node="mo" transform="translate(636,363) scale(0.707)"><path data-c="2032" d="M79 43Q73 43 52 49T30 61Q30 68 85 293T146 528Q161 560 198 560Q218 560 240 545T262 501Q262 496 260 486Q259 479 173 263T84 45T79 43Z"></path></g></g><g data-mml-node="mo" transform="translate(880.5,0)"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z"></path></g><g data-mml-node="mi" transform="translate(1269.5,0)"><path data-c="1D461" d="M26 385Q19 392 19 395Q19 399 22 411T27 425Q29 430 36 430T87 431H140L159 511Q162 522 166 540T173 566T179 586T187 603T197 615T211 624T229 626Q247 625 254 615T261 596Q261 589 252 549T232 470L222 433Q222 431 272 431H323Q330 424 330 420Q330 398 317 385H210L174 240Q135 80 135 68Q135 26 162 26Q197 26 230 60T283 144Q285 150 288 151T303 153H307Q322 153 322 145Q322 142 319 133Q314 117 301 95T267 48T216 6T155 -11Q125 -11 98 4T59 56Q57 64 57 83V101L92 241Q127 382 128 383Q128 385 77 385H26Z"></path></g><g data-mml-node="mo" transform="translate(1630.5,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z"></path></g></g></g></svg></mjx-container></span>。如果 <span class="math inline"><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.566ex;" xmlns="http://www.w3.org/2000/svg" width="8.717ex" height="2.283ex" role="img" focusable="false" viewBox="0 -759 3853 1009"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msup"><g data-mml-node="mi"><path data-c="1D453" d="M118 -162Q120 -162 124 -164T135 -167T147 -168Q160 -168 171 -155T187 -126Q197 -99 221 27T267 267T289 382V385H242Q195 385 192 387Q188 390 188 397L195 425Q197 430 203 430T250 431Q298 431 298 432Q298 434 307 482T319 540Q356 705 465 705Q502 703 526 683T550 630Q550 594 529 578T487 561Q443 561 443 603Q443 622 454 636T478 657L487 662Q471 668 457 668Q445 668 434 658T419 630Q412 601 403 552T387 469T380 433Q380 431 435 431Q480 431 487 430T498 424Q499 420 496 407T491 391Q489 386 482 386T428 385H372L349 263Q301 15 282 -47Q255 -132 212 -173Q175 -205 139 -205Q107 -205 81 -186T55 -132Q55 -95 76 -78T118 -61Q162 -61 162 -103Q162 -122 151 -136T127 -157L118 -162Z"></path></g><g data-mml-node="mo" transform="translate(636,363) scale(0.707)"><path data-c="2032" d="M79 43Q73 43 52 49T30 61Q30 68 85 293T146 528Q161 560 198 560Q218 560 240 545T262 501Q262 496 260 486Q259 479 173 263T84 45T79 43Z"></path></g></g><g data-mml-node="mo" transform="translate(880.5,0)"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z"></path></g><g data-mml-node="mi" transform="translate(1269.5,0)"><path data-c="1D461" d="M26 385Q19 392 19 395Q19 399 22 411T27 425Q29 430 36 430T87 431H140L159 511Q162 522 166 540T173 566T179 586T187 603T197 615T211 624T229 626Q247 625 254 615T261 596Q261 589 252 549T232 470L222 433Q222 431 272 431H323Q330 424 330 420Q330 398 317 385H210L174 240Q135 80 135 68Q135 26 162 26Q197 26 230 60T283 144Q285 150 288 151T303 153H307Q322 153 322 145Q322 142 319 133Q314 117 301 95T267 48T216 6T155 -11Q125 -11 98 4T59 56Q57 64 57 83V101L92 241Q127 382 128 383Q128 385 77 385H26Z"></path></g><g data-mml-node="mo" transform="translate(1630.5,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z"></path></g><g data-mml-node="mo" transform="translate(2297.2,0)"><path data-c="3E" d="M84 520Q84 528 88 533T96 539L99 540Q106 540 253 471T544 334L687 265Q694 260 694 250T687 235Q685 233 395 96L107 -40H101Q83 -38 83 -20Q83 -19 83 -17Q82 -10 98 -1Q117 9 248 71Q326 108 378 132L626 250L378 368Q90 504 86 509Q84 513 84 520Z"></path></g><g data-mml-node="mn" transform="translate(3353,0)"><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z"></path></g></g></g></svg></mjx-container></span>
则人是快乐的，反之则是痛苦的。也就是说快乐和痛苦是对比出来的。</p>
<p>然而柏拉图全然否定了这个观点。柏拉图认为世界上有快乐，也有痛苦；还有一种介于快乐和痛苦之间的平静的状态。把快乐、平静、痛苦比喻为上、中、下三级。但人在受到痛苦时会把摆脱痛苦称为快乐，然而摆脱痛苦实际是中间的平静的状态，并不是什么享受。例如生病的人会说，没有什么比健康更快乐的了。然而他们在生病之前并不曾觉得那是最大的快乐。同样，当一个人正在享乐，让他突然停止享乐，进入平静的状态，也是痛苦的。然而平静怎么可以既是快乐又是痛苦呢？这便推导出矛盾了。因此柏拉图认为这种对比出来的快乐不是真的快乐，只是快乐的影像。</p>
<blockquote>
<p>和痛苦对比的快乐以及和快乐对比的痛苦都是平静，不是真实的快乐和痛苦，而只是似乎快乐或痛苦。这些快乐的影像和真正的快乐毫无关系，都只是一种欺骗。</p>
</blockquote>
<p>柏拉图认为，通过肉体上的享受得到的快乐，大多数属于“快乐的影像”，是某种意义上的脱离痛苦。例如人饥饿时吃食物会觉得无比的美味，这种快乐实际上是脱离痛苦。更进一步地，因<strong>欲望</strong>的满足而得到的快乐，大多属于“快乐的影像”。人对某事物有了欲望，求而不得，感到痛苦。欲望越强，越求而不得，就越痛苦，得到它时就越“快乐”。然而这种“快乐”某种意义上是痛苦的脱离，只是“快乐的影像”，不是真正的快乐。</p>
<p>柏拉图认为有一种真正的快乐，得到它之前不会感到痛苦，出现的时候能感受到强烈的快乐；停止之后也不留下痛苦。例如当你坚持学习，头脑变得越来越充实的时候；领悟了某个真理时那种”朝闻道，夕死可矣”的满足感。我还记得当时理解了
Y-Combinator，学会了用 CPS 变换实现 <code>call/cc</code>
的时候的那种兴奋感，真的能让人快乐好几天。柏拉图认为，用<strong>理性</strong>的部分追求智慧、美德得到的是<strong>实在的东西</strong>，而肉体上的享受是<strong>不实在的东西</strong>。让实在的东西填充内心才能得到可靠的真实的快乐。</p>
<blockquote>
<p>那么，没有经验过真实的人，他们对快乐、痛苦及这两者之中间状态的看法应该是不正确的，正如他们对许多别的事物的看法不正确那样。因此，当他们遭遇到痛苦时，他们就会认为自己处于痛苦之中，认为他们的痛苦是真实的。他们会深信，从痛苦转变到中间状态就能带来满足和快乐。而事实上，由于没有经验过真正的快乐，他们是错误地对比了痛苦和无痛苦。正如一个从未见过白色的人把灰色和黑色相比那样。</p>
</blockquote>
<blockquote>
<p>因此，那些没有智慧和美德经验的人，只知聚在一起寻欢作乐，终身往返于我们所比喻的中下两级之间，从未再向上攀登看见和到达真正的最高一级境界，或为任何实在所满足，或体验到过任何可靠的纯粹的快乐。他们头向下眼睛看着宴席，就像牲畜俯首牧场只知吃草，雌雄交配一样。须知，<strong>他们想用这些不实在的东西满足心灵的那个不实在的无法满足的部分是完全徒劳的</strong>。由于不能满足，他们还像牲畜用犄角和蹄爪互相踢打顶撞一样地用铁的武器互相残杀。</p>
</blockquote>
<p>柏拉图称肉体上的享受为<strong>不实在的东西</strong>，认为内心的欲望是<strong>不实在的无法满足的部分</strong>。现代科学在一定程度上支持柏拉图的观点。肉体上的享受得到的快乐来源于多巴胺。多巴胺能给人带来快乐，然而代价是当多巴胺消退时，人会感觉到空虚和痛苦，需要更大剂量的多巴胺才能弥补这些痛苦。于是人对多巴胺的追求永远不会满足，这个过程最终会变成一种折磨。而人在节制、自律的时候会分泌内啡肽，它给人带来的快乐是缓慢持续的；当它消退时也不会感到痛苦。自律和节制能给人带来更高级的快乐。这也是罗翔所说的，低级的快乐来自放纵，高级的快乐来自克制。</p>
<p>我过去提出的那个“快乐公式”只适用于通过多巴胺获取的快乐。也就是柏拉图所说的，往返于中下两级之间，从平静和痛苦之间感受快乐的影像。</p>
<p>最后柏拉图认为正义的人是真正快乐的，因为他们是<strong>理性</strong>的、节制的。他们会追求智慧，追求真理，这其中得到的快乐要胜与满足欲望得到的快乐。一个极端不正义的人<strong>欲望</strong>会无限膨胀，理性成为欲望的奴隶，他永远无法满足，给他再多的物质也得不到快乐。</p>
<section class="footnotes">
<div class="footnote-list">
<ol>
<li>
<span id="fn:1" class="footnote-text"><span>哲学家 philosopher
字面意思为“爱智慧的人”。philo- 爱，sophia 智慧。
<a href="#fnref:1" rev="footnote" class="footnote-backref">
↩︎</a></span></span>
</li>
</ol>
</div>
</section>
]]></content>
    
    
    <summary type="html">我最近读完了《理想国》。这本书为古希腊哲学先贤柏拉图所著，是哲学经典之作、奠基之作，内容晦涩深奥，比较难懂。本人才疏学浅，以我的贫乏的哲学素养，难以完全领会其中深邃的思想。然而读完之后，仍然对我的价值观造成了不小的冲击。这里斗胆分享两点我的体会。
什么是正义
中国有句古话，叫做成王败寇。胜者为正义，</summary>
    
    
    
    
    <category term="reading" scheme="https://luyuhuang.tech/tags/reading/"/>
    
    <category term="philosophy" scheme="https://luyuhuang.tech/tags/philosophy/"/>
    
  </entry>
  
  <entry>
    <title>AppImage: 一次打包，到处运行</title>
    <link href="https://luyuhuang.tech/2024/04/19/appimage.html"/>
    <id>https://luyuhuang.tech/2024/04/19/appimage.html</id>
    <published>2024-04-18T16:00:00.000Z</published>
    <updated>2024-04-18T16:13:08.087Z</updated>
    
    <content type="html"><![CDATA[<p>我们知道，不同于 Windows 将软件的所有文件安装在一个目录，一个 Linux
软件的不同部分会被安装在不同路径。例如，可执行文件安装在
<code>/usr/bin</code> 下；库文件安装在 <code>/usr/lib</code>
下；文档、脚本等资源文件通常安装在 <code>/usr/share</code>
下等。这是因为 Linux
认为软件包之间会相互依赖，不同的软件可能依赖于同一个库，那么这个库就只应该存在一份。例如
curl, ssh 和 nginx 都依赖于 libcrypt.so 这个共享库，其为 openssl
的一部分。当我们使用 <code>apt</code> 安装 nginx 时，先会检测 openssl
是否已经安装。如果没有，就先安装 openssl；否则直接安装 nginx。</p>
<p>这样做的好处是可以节省磁盘：所有软件依赖的相同的库只存在一份。因此安装一个
Linux 系统通常只需要几 G 的磁盘空间，而 Windows 通常需要几十
G。同时可以节省内存，因为共享库的加载方式是
mmap，同一个共享库在内存中也只有一份。</p>
<p>但是这么做是有代价的。假设 A, B 软件都依赖于库 L，那么 A 和 B
就只能依赖于同一个版本的库
L。一台机器上有这么多软件，意味着整个依赖网络让他们相互钳制，版本号被限制，不能随意升级。一个发行版会确定各种软件包的版本（确定主次版本号，补丁号通常不做限制），组成软件库，确保它们相互兼容，没有依赖冲突。也就是说
<code>apt</code> 安装的软件版本由当前 Ubuntu 版本决定的。这也是为什么
Linux 发行版通常每年都要发布一个新版本，否则软件库会落后于时代。</p>
<p>如果你在用一个较老的发行版，想安装一些新软件，通常需要自己编译。自己编译的软件通常安装在
<code>/usr/local/*</code> 下，与 <code>/usr/*</code>
区分开。但是我公司用的开发环境的发行版太老了，g++ 版本 4.8，只支持 C++
11，无法编译要求支持 C++ 17 的新软件。更糟糕的是这个发行版的 glibc
版本也非常老，新软件即使在新系统中编译出来，也无法在这个老系统上运行。而
gcc 工具链（包括 glibc）是操作系统的一部分，不能随意升级。</p>
<p>要是能像 Windows 一样将软件的依赖的各种 DLL 都打包到一起就好了！Linux
有类似的解决方案，AppImage 就是其中一种。它可以将软件打包成一个二进制
AppImage 文件，这是一个标准的 ELF 可执行文件。用户下载 AppImage
文件后，直接 <code>chmod +x</code> 后就可以直接运行，非常方便。对于
AppImage 来说，一个软件就是一个可执行文件。</p>
<pre><code class="hljs sh"><span class="hljs-built_in">chmod</span> +x app.AppImage<br>./app.AppImage<br></code></pre>
<h2 id="原理">原理</h2>
<p>AppImage 的原理是将软件和相关依赖归档成一个磁盘镜像，打包在 AppImage
文件里。这个归档的目录称为 AppDir，它的结构大概是这样的：</p>
<pre><code class="hljs stata">AppDir<br>├── AppRun<br>├── icon.svg<br>├── <span class="hljs-keyword">app</span>.desktop<br>└── usr<br>    ├── bin<br>    │   └── <span class="hljs-keyword">app</span><br>    ├── lib<br>    │   └── x86_64-linux-gnu<br>    │       ├── ld-2.31.<span class="hljs-keyword">so</span><br>    │       ├── libm.<span class="hljs-keyword">so</span>.6<br>    │       ├── libpthread.<span class="hljs-keyword">so</span>.0<br>    │       └── libc.<span class="hljs-keyword">so</span>.6<br>    └── share<br>        └── icons<br>            └── icon.svg<br></code></pre>
<p>运行 AppImage 文件时，其中的磁盘镜像会被挂载到
<code>/tmp/.mount_XXX.XXXXX</code> 上，然后执行其中的 AppRun。AppRun
可以是一个脚本，也可以是一个二进制，它负责做一些前序工作，设置各种环境变量（如
<code>LD_LIBRARY_PATH</code>），然后启动目标程序。</p>
<h2 id="hello-appimage">Hello, AppImage</h2>
<p>接下来我们动手制作一个 AppImage。我们有一个 C 程序
<code>hello.c</code></p>
<pre><code class="hljs c"><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&lt;stdio.h&gt;</span></span><br><br><span class="hljs-type">int</span> <span class="hljs-title function_">main</span><span class="hljs-params">()</span> &#123;<br>    <span class="hljs-built_in">printf</span>(<span class="hljs-string">&quot;Hello Appimage\n&quot;</span>);<br>    <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;<br>&#125;<br></code></pre>
<p>然后编译它 <code>gcc -o hello hello.c</code>。接着我们创建一个
<code>AppDir</code> 目录，将 <code>hello</code> 放到
<code>AppDir/usr/bin/</code> 中。</p>
<pre><code class="hljs sh">$ <span class="hljs-built_in">mkdir</span> -p AppDir/usr/bin<br>$ <span class="hljs-built_in">cp</span> hello AppDir/usr/bin/<br>$ tree Appdir<br>AppDir<br>└── usr<br>    └── bin<br>        └── hello<br></code></pre>
<p>接着我们要将程序依赖的共享库也打包进去。我们用 <code>ldd</code> 查看
<code>hello</code> 依赖的共享库：</p>
<pre><code class="hljs sh">$ ldd hello<br>        linux-vdso.so.1 (0x00007ffe66f8f000)<br>        libc.so.6 =&gt; /lib/x86_64-linux-gnu/libc.so.6 (0x00007f849544b000)<br>        /lib64/ld-linux-x86-64.so.2 (0x00007f8495650000)<br></code></pre>
<p><code>hello</code> 很简单，只依赖 libc。链接器
<code>/lib64/ld-linux-x86-64.so.2</code>
为程序加载各种共享库，是程序的解释器
(interpreter)，也需要打包进去。我们把这两个 .so 文件复制到 AppDir
的对应目录：</p>
<pre><code class="hljs txt">AppDir<br>├── lib<br>│   └── x86_64-linux-gnu<br>│       └── libc.so.6<br>├── lib64<br>│   └── ld-linux-x86-64.so.2<br>└── usr<br>    └── bin<br>        └── hello<br></code></pre>
<p>接下来我们创建 <code>AppRun</code> 脚本。这个脚本先设置
<code>LD_LIBRARY_PATH</code> 环境变量，然后用 AppDir 中的链接器加载运行
<code>hello</code> 程序：</p>
<pre><code class="hljs sh"><span class="hljs-meta">#!/usr/bin/sh</span><br><br><span class="hljs-built_in">export</span> LD_LIBRARY_PATH=<span class="hljs-variable">$&#123;APPDIR&#125;</span>/lib/x86_64-linux-gnu<br><span class="hljs-variable">$&#123;APPDIR&#125;</span>/lib64/ld-linux-x86-64.so.2 <span class="hljs-variable">$&#123;APPDIR&#125;</span>/usr/bin/hello<br></code></pre>
<p>AppImage 运行时环境变量 <code>APPDIR</code> 便是 AppDir
挂载的路径（<code>/tmp/.mount_XXX.XXXXX</code>），我们可以直接在脚本中引用它。最后我们需要一个
desktop 文件配置一些元数据，还要准备一个图标文件：</p>
<pre><code class="hljs ini"><span class="hljs-section">[Desktop Entry]</span><br><span class="hljs-attr">Name</span>=hello<br><span class="hljs-attr">Exec</span>=hello<br><span class="hljs-attr">Icon</span>=hello<br><span class="hljs-attr">Type</span>=Application<br><span class="hljs-attr">Categories</span>=Utility<span class="hljs-comment">;</span><br></code></pre>
<p>最终 AppDir 的目录结构是这样的：</p>
<pre><code class="hljs txt">AppDir<br>├── AppRun<br>├── hello.desktop<br>├── hello.svg<br>├── lib<br>│   └── x86_64-linux-gnu<br>│       └── libc.so.6<br>├── lib64<br>│   └── ld-linux-x86-64.so.2<br>└── usr<br>    └── bin<br>        └── hello<br></code></pre>
<p>要将 AppDir 打包成可执行文件，需要用到的工具是 appimagetool，可以到
<a href="https://github.com/AppImage/AppImageKit/releases/latest">Github</a>
下载。appimagetool 本身也是个 AppImage，下载后即可运行。执行
<code>appimagetool AppDir</code> 便可将 AppDir 打包成一个
AppImage。运行它</p>
<pre><code class="hljs sh">$ ./hello-x86_64.AppImage<br>Hello Appimage<br></code></pre>
<p>因为它打包了程序所需的所有依赖，所以理论上它可以在任意一个同架构（这里是
X86_64）的 Linux 系统上运行，无论这个系统的 libc
版本是多少。你也可以修改这个程序，让它引用一些较新的 libc
里才有的函数（如 <code>gettid</code>, glibc 2.30 被加入），打包成
AppImage 后再发给一个老系统（如 CentOS 7），看看它能不能正常运行。</p>
<h2 id="使用-appimage-builder">使用 appimage-builder</h2>
<p>上面例子中的程序很简单，只依赖一个
libc。而实际情况下程序通常依赖很多共享库，这些共享库有可能又依赖更多其它的共享库。手动找出来非常麻烦，我们可以使用工具。appimage-builder
就是一个很方便的工具。它的原理是运行目标程序，分析它访问了哪些共享库；然后使用包管理器（如
apt）获取依赖，并制作成 AppDir。此外它还提供了一个功能强大的
AppRun，支持路径映射，通过 hook 程序的文件访问函数，将指定路径映射到
AppDir 中。</p>
<p>appimage-builder 是一个 Python 工具，可以使用 pip 安装：</p>
<pre><code class="hljs sh">pip install appimage-builder<br></code></pre>
<p>要用 appimage-builder 制作 AppImage，我们首先需要准备一个“基础版”的
AppDir，包含软件的可执行文件和一些相关依赖。通常那些
<code>make install</code> 复制到 <code>/usr/local/</code>
下的文件就是基础 AppDir 应当包含的文件。上面例子的基础 AppDir
结构如下:</p>
<pre><code class="hljs txt">AppDir<br>└── usr<br>    └── bin<br>        └── hello<br></code></pre>
<p>appimage-builder 基于一个 yaml 配置文件制作 AppImage，称为
recipe。我们不必手动创建 recipe，可以用
<code>appimage-builder --generate</code>
命令生成，然后再根据需要修改。generate
命令是一个向导程序，会询问这个应用的基本信息。</p>
<pre><code class="hljs sh">$ appimage-builder --generate<br>INFO:Generator:Searching AppDir<br>? ID [Eg: com.example.app]: tech.luyuhuang.hello<br>? Application Name: hello<br>? Icon: hello<br>? Executable path: usr/bin/hello<br>? Arguments [Default: <span class="hljs-variable">$@</span>]: <span class="hljs-variable">$@</span><br>? Version [Eg: 1.0.0]: latest<br>? Update Information [Default: guess]: guess<br>? Architecture: x86_64<br>INFO:AppRuntimeAnalyser:/usr/bin/strace -f -E LD_LIBRARY_PATH= -e trace=openat --status=successful AppDir/usr/bin/hello<br></code></pre>
<p>接着 appimage-builder 会用 <code>strace</code>
运行目标程序，分析它打开了哪些共享库文件；然后用包管理工具分析共享库属于哪个软件包。结束后就会生成
recipe 文件 <code>AppImageBuilder.yml</code>。它的结构如下：</p>
<pre><code class="hljs yaml"><span class="hljs-attr">version:</span> <span class="hljs-number">1</span><br><span class="hljs-attr">AppDir:</span><br>  <span class="hljs-attr">path:</span> <span class="hljs-string">/path/to/AppDir</span><br>  <span class="hljs-attr">app_info:</span> <span class="hljs-comment"># 应用基础信息</span><br>    <span class="hljs-attr">id:</span> <span class="hljs-string">tech.luyuhuang.hello</span><br>    <span class="hljs-attr">name:</span> <span class="hljs-string">hello</span><br>    <span class="hljs-attr">icon:</span> <span class="hljs-string">hello</span><br>    <span class="hljs-attr">version:</span> <span class="hljs-string">latest</span><br>    <span class="hljs-attr">exec:</span> <span class="hljs-string">usr/bin/hello</span><br>    <span class="hljs-attr">exec_args:</span> <span class="hljs-string">$@</span><br>  <span class="hljs-attr">apt:</span><br>    <span class="hljs-attr">arch:</span><br>    <span class="hljs-bullet">-</span> <span class="hljs-string">amd64</span><br>    <span class="hljs-attr">allow_unauthenticated:</span> <span class="hljs-literal">true</span><br>    <span class="hljs-attr">sources:</span> <span class="hljs-comment"># 用到的软件源</span><br>    <span class="hljs-bullet">-</span> <span class="hljs-attr">sourceline:</span> <span class="hljs-string">deb</span> <span class="hljs-string">http://archive.ubuntu.com/ubuntu/</span> <span class="hljs-string">focal</span> <span class="hljs-string">main</span> <span class="hljs-string">restricted</span><br>    <span class="hljs-bullet">-</span> <span class="hljs-attr">sourceline:</span> <span class="hljs-string">deb</span> <span class="hljs-string">http://archive.ubuntu.com/ubuntu/</span> <span class="hljs-string">focal-updates</span> <span class="hljs-string">main</span> <span class="hljs-string">restricted</span><br>    <span class="hljs-bullet">-</span> <span class="hljs-string">...</span><br>    <span class="hljs-attr">include:</span> <span class="hljs-comment"># 用到的软件包</span><br>    <span class="hljs-bullet">-</span> <span class="hljs-string">libc6:amd64</span><br>  <span class="hljs-attr">files:</span><br>    <span class="hljs-attr">include:</span> [] <span class="hljs-comment"># 额外需要包含到 AppDir 的文件</span><br>    <span class="hljs-attr">exclude:</span> <span class="hljs-comment"># 需要排除的文件</span><br>    <span class="hljs-bullet">-</span> <span class="hljs-string">usr/share/man</span><br>    <span class="hljs-bullet">-</span> <span class="hljs-string">usr/share/doc/*/README.*</span><br>    <span class="hljs-bullet">-</span> <span class="hljs-string">...</span><br>  <span class="hljs-attr">test:</span> <span class="hljs-comment"># 测试配置</span><br>    <span class="hljs-attr">fedora-30:</span><br>      <span class="hljs-attr">image:</span> <span class="hljs-string">appimagecrafters/tests-env:fedora-30</span><br>      <span class="hljs-attr">command:</span> <span class="hljs-string">./AppRun</span><br>    <span class="hljs-attr">debian-stable:</span><br>      <span class="hljs-string">...</span><br><span class="hljs-attr">AppImage:</span><br>  <span class="hljs-attr">arch:</span> <span class="hljs-string">x86_64</span><br>  <span class="hljs-attr">update-information:</span> <span class="hljs-string">guess</span><br></code></pre>
<p>我们通常需要关注这些配置：</p>
<ul>
<li><code>AppDir.apt</code> 软件包相关信息，由 generate
命令探测出，通常不用自行修改。<code>include</code>
为要用到的软件包，<code>sources</code> 是这些软件包相关的软件源。</li>
<li><code>AppDir.files</code> 控制要包含哪些文件。支持使用 <a href="https://docs.python.org/3.6/library/glob.html#module-glob">Glob
表达式</a>（如 <code>*</code> 和 <code>**</code> 通配符）匹配文件路径。
<ul>
<li><code>include</code> 为需要包含到 AppDir
的文件的绝对路径列表，这些文件会被复制到 AppDir 中的对应位置。例如
<code>/usr/bin/bash</code> 对应 <code>$APPDIR/usr/bin/bash</code>。</li>
<li><code>exclude</code> 则为需要在 AppDir
中排除的文件路径列表，路径相对于 AppDir。</li>
</ul></li>
<li><code>AppDir.test</code> 为测试环境。appimage-builder
会拉取其中指定的 Docker 镜像，并在其中测试 AppDir。</li>
</ul>
<p>除这些自动生成的配置外，还有很实用的运行时配置。</p>
<pre><code class="hljs yaml"><span class="hljs-attr">AppDir:</span><br>  <span class="hljs-attr">runtime:</span><br>    <span class="hljs-attr">env:</span><br>      <span class="hljs-attr">LD_PRELOAD:</span> <span class="hljs-string">&#x27;$&#123;APPDIR&#125;/usr/lib/libjemalloc.so&#x27;</span><br>    <span class="hljs-attr">path_mappings:</span><br>        <span class="hljs-bullet">-</span> <span class="hljs-string">/bin/bash:$APPDIR/bin/bash</span><br></code></pre>
<ul>
<li><code>env</code> 指定运行时的环境变量。appimage-builder 自带的
AppRun 程序还支持一些特殊的环境变量
<ul>
<li><code>APPDIR_EXEC_ARGS</code> 程序的命令行参数，默认为
<code>$@</code>，即原样透传传给 AppRun 的参数。</li>
<li><code>APPDIR_LIBRARY_PATH</code> 共享库搜索路径，效果等同于
<code>LD_LIBRARY_PATH</code>。</li>
</ul></li>
<li><code>path_mappings</code> 设置路径映射。支持将一个绝对路径映射到
AppDir 中的路径，格式为 <code>源路径:目标路径</code>。例如
<code>/bin/bash:$APPDIR/bin/bash</code>，每当程序访问
<code>/bin/bash</code> 都会实际访问 AppDir 中的
<code>bin/bash</code>。</li>
</ul>
<p>准备好 recipe 文件后执行
<code>appimage-builder --recipe AppImageBuilder.yml</code> 即可生成
AppImage。也可以加上 <code>--skip-tests</code> 跳过测试。</p>
<h2 id="实战制作-ccls-的-appimage">实战：制作 ccls 的 AppImage</h2>
<p>ccls 是一个 C++ 的 language server。我想在公司的开发环境用上
ccls，但是 ccls
依赖的工具链和运行时环境都比较新，无法直接在公司的开发环境上编译、运行。因此我准备在
Ubuntu 20.04 下编译 ccls 并制作成
AppImage，让这个老系统也能用上新软件。</p>
<p>执行如下命令构建 ccls:</p>
<pre><code class="hljs sh">sudo apt-get install clang libclang-10-dev <span class="hljs-comment"># 安装依赖</span><br>git <span class="hljs-built_in">clone</span> --depth=1 --branch=0.20220729 --recursive https://github.com/MaskRay/ccls <span class="hljs-comment"># 获取 ccls, 版本 0.20220729</span><br><span class="hljs-built_in">cd</span> ccls<br>cmake -H. -BRelease -DCMAKE_BUILD_TYPE=Release \<br>                    -DCMAKE_PREFIX_PATH=/usr/lib/llvm-10 \<br>                    -DLLVM_INCLUDE_DIR=/usr/lib/llvm-10/include \<br>                    -DLLVM_BUILD_INCLUDE_DIR=/usr/include/llvm-10/ \<br>                    -DCMAKE_INSTALL_PREFIX=/usr <span class="hljs-comment"># 设置 prefix 为 /usr</span><br><span class="hljs-built_in">cd</span> Release<br>make -j8<br>make install DESTDIR=AppDir <span class="hljs-comment"># 安装到 ./AppDir</span><br></code></pre>
<p>这样我们就有了基础 AppDir:</p>
<pre><code class="hljs txt">AppDir<br>└── usr<br>    └── bin<br>        └── ccls<br></code></pre>
<p>接着我们执行 <code>appimage-builder --generate</code> 生成
recipe:</p>
<pre><code class="hljs sh">$ appimage-builder --generate<br>INFO:Generator:Searching AppDir<br>? ID [Eg: com.example.app]: com.github.MaskRay.ccls<br>? Application Name: ccls<br>? Icon: ccls<br>? Executable path relative to AppDir [usr/bin/app]: usr/bin/ccls<br>? Arguments [Default: <span class="hljs-variable">$@</span>]: <span class="hljs-variable">$@</span><br>? Version [Eg: 1.0.0]: latest<br>? Update Information [Default: guess]: guess<br>? Architecture: x86_64<br></code></pre>
<p>根据 ccls 的文档（和我的测试结果），ccls 运行时要访问 clang 的 lib
目录。我的 clang 是用 apt 安装的，路径在
<code>/usr/lib/llvm-10/lib/clang/10.0.0</code>。我们需要把这个路径打包进
AppDir，并且将其映射到 AppDir 内。我们修改
<code>AppImageBuilder.yml</code>:</p>
<pre><code class="hljs yaml"><span class="hljs-attr">AppDir:</span><br>  <span class="hljs-attr">files:</span><br>    <span class="hljs-attr">include:</span><br>    <span class="hljs-bullet">-</span> <span class="hljs-string">/usr/lib/llvm-10/lib/clang/10.0.0/**</span> <span class="hljs-comment"># 把这个路径下的全部文件包含进去</span><br>  <span class="hljs-attr">runtime:</span><br>    <span class="hljs-attr">path_mappings:</span><br>      <span class="hljs-bullet">-</span> <span class="hljs-string">/usr/lib/llvm-10/lib/clang/10.0.0:$APPDIR/usr/lib/llvm-10/lib/clang/10.0.0</span> <span class="hljs-comment"># 映射到 AppDir 内</span><br></code></pre>
<p>然后我们还要创建个图标。虽然是命令行程序，但是 AppImage
要求每个应用都要有个图标，所以没办法。这里我们就 touch
一个空文件就好：</p>
<pre><code class="hljs sh"><span class="hljs-built_in">mkdir</span> -p AppDir/usr/share/icons<br><span class="hljs-built_in">touch</span> AppDir/usr/share/icons/ccls.svg<br></code></pre>
<p>最后执行 <code>appimage-builder --recipe AppImageBuilder.yml</code>
生成 AppImage。大功告成！</p>
<pre><code class="hljs sh">$ ./ccls-latest-x86_64.AppImage --version<br>ccls version 0.20220729-0-g7445891<br>clang version 10.0.0-4ubuntu1<br></code></pre>
<h2 id="总结">总结</h2>
<p>Linux
的软件管理方式虽然节省了磁盘和内存空间，但是也增加了软件安装的难度。导致
Linux
的软件要么进入发行版使用包管理器安装；要么发布源码，编译安装。前者虽然安装方便，但是版本受限，不能随意升级；后者需要准备开发环境，安装较为麻烦。当编译依赖的工具链不满足要求时，软件安装会变得很棘手。</p>
<p>针对这个问题，Linux 有几种解决方案，例如 snap、容器，以及本文介绍的
AppImage
等。它们的解决思路其实差不多，都是将软件与其依赖一起打成包发布。它们各有优劣，对于
AppImage 来说，优点就是使用方便，用户不需要安装任何环境，下载 AppImage
即可执行；缺点是依赖于 AppRun 的前序处理，兼容性可能不如 snap
和容器。个人感觉 Linux 桌面系统要想推广，软件安装还是要走 Windows 和
macOS 这种形态，即打包软件依赖，降低安装门槛，提高兼容性。</p>
<p><strong>参考资料：</strong></p>
<ul>
<li><a href="https://docs.appimage.org/index.html"
class="uri">https://docs.appimage.org/index.html</a></li>
<li><a href="https://appimage-builder.readthedocs.io/en/latest/"
class="uri">https://appimage-builder.readthedocs.io/en/latest/</a></li>
</ul>
]]></content>
    
    
    <summary type="html">我们知道，不同于 Windows 将软件的所有文件安装在一个目录，一个 Linux
软件的不同部分会被安装在不同路径。例如，可执行文件安装在
/usr/bin 下；库文件安装在 /usr/lib
下；文档、脚本等资源文件通常安装在 /usr/share
下等。这是因为 Linux
认为软件包之间会相</summary>
    
    
    
    
    <category term="linux" scheme="https://luyuhuang.tech/tags/linux/"/>
    
    <category term="tools" scheme="https://luyuhuang.tech/tags/tools/"/>
    
  </entry>
  
  <entry>
    <title>2023 Annual Review</title>
    <link href="https://luyuhuang.tech/2024/01/01/2023-annual-review.html"/>
    <id>https://luyuhuang.tech/2024/01/01/2023-annual-review.html</id>
    <published>2023-12-31T16:00:00.000Z</published>
    <updated>2024-01-01T10:49:09.926Z</updated>
    
    <content type="html"><![CDATA[<p>In 2023 I spent most of my time on work, learning <span style="background-color: var(--post-text-color)">and dating</span>.
Compared with the last year, devoted less time on this blog and
community. It might be a pretext but to be honest, writing a blog post
always consumes a lot of my energy, especially when there was a lot of
overtime this year. Anyway, I should write more posts in the new year,
and I’ll try to write some non-technical posts (well, partially because
they’re easy to write (and read)).</p>
<p>Let’s talk a little about professional skills over here. In the past,
I focused most of my efforts on coding skill, rather than engineering
skills, or let’s say, the business skills. This is because I love the
hacker spirit, and am fascinated by intricate program structures and
algorithms. But your boss just need one who solve problems, them don’t
care about computer science. To solve a problem, you have to consider
many things other than the computer - namely, the people around you. And
that’s exactly what software engineering does - to use some engineering
methods to prevent or eliminate mistakes made by humans. So when I focus
on hacking, I must keep an open mind on other skills, especially those
methodologies for problem-solving.</p>
<h2 id="goals">Goals</h2>
<ul class="task-list">
<li><input type="checkbox" checked="" >Keep up learning English</li>
<li><input type="checkbox" >Read <em>Understanding the Linux
Kernel</em> (I’m not sure if I can finish it)</li>
<li><input type="checkbox" checked="" >Keep up daily LeetCode
exercises</li>
<li><input type="checkbox" >Keep up writing blogs, basically one post a
month</li>
<li><input type="checkbox" checked="" >Read some non-technical
books</li>
<li><input type="checkbox" checked="" >Keep up exercises</li>
</ul>
<h2 id="learning">Learning</h2>
<p>I finished learning <em>Structure and Interpretation of Computer
Programs (SICP)</em>, an amazing book. Its content comprised of
functional programming, layering of program and data structure, OOP,
infinite streams, the metacircular evaluator, lazy evaluation,
compilation principle, etc. I have to say, <em>SICP</em> opened the gate
of computer science for me. Before then, I don’t really comprehend the
essential of computer science.</p>
<figure>
<img src="/assets/images/2023-annual-review-2.jpg" width="350"
alt="sicp" >
<figcaption aria-hidden="true">sicp</figcaption>
</figure>
<h2 id="leetcode">LeetCode</h2>
<p>I kept on doing LeetCode like the past few years. The grid looks not
bad.</p>
<figure>
<img src="/assets/images/2023-annual-review-1.png" alt="leetcode" >
<figcaption aria-hidden="true">leetcode</figcaption>
</figure>
<p>Now I’ve solved 1182 problems. Last year it’s 912, so I solved 270
problems in 2023.</p>
<h2 id="language">Language</h2>
<p>I kept learning English in 2023, as I did in the past few years.
After continuously learning vocabulary on the APP <em>baicizhan</em> for
2024 days, I found it might not be a very effective way to memorize new
words for me at the moment. Therefore, In November, I started learning
<em>Merriam-Webster’s Vocabulary Builder</em>. This book organizes words
by their roots, besides telling you how to use a word, its history, and
related knowledge.</p>
<figure>
<img src="/assets/images/2023-annual-review-3.jpg" width="250"
alt="webster" >
<figcaption aria-hidden="true">webster</figcaption>
</figure>
<p>I also read another amazing vocabulary builder, <em>Word Power Made
Easy</em>. To me, this book like an introduction to the etymology and at
the same time teaches you how to memorize over 3,000 words and continue
building your vocabulary.</p>
<figure>
<img src="/assets/images/2023-annual-review-4.jpg" width="250"
alt="webster" >
<figcaption aria-hidden="true">webster</figcaption>
</figure>
<p>In addition, I kept leaning Japanese on Duolingo as I did in
2022.</p>
<figure>
<img src="/assets/images/2023-annual-review-5.jpg" width="400"
alt="webster" >
<figcaption aria-hidden="true">webster</figcaption>
</figure>
<h2 id="creating">Creating</h2>
<p>I only wrote 6 posts on the blog.</p>
<p>I created a repo <a href="https://github.com/luyuhuang/nvim">luyuhuang/nvim</a>, but it’s
just for personal configuration, should not be considered as a
contribution. I submitted some pull requests and issues to the community
and some of have been accepted.</p>
<h2 id="something-happy">Something Happy</h2>
<p>Hiking and seeing the skyline of the city at the summit is quite
happy, especially with the one you love.</p>
<figure>
<img src="/assets/images/2023-annual-review-6.jpg" width="700"
alt="webster" >
<figcaption aria-hidden="true">webster</figcaption>
</figure>
<h2 id="finally">Finally</h2>
<p>At the end of a year, I always feel time flies by quickly. But after
I wrote the annual review, I found that the time of a year is quite
long, because you can literally do many things in a year and grow quite
a bit in certain aspects (say, I found my English writing skill is much
better than the last year). So in the new year, keep learning and
growing, and I believe we’ll get a better result. Happy New Year to
everybody.</p>
]]></content>
    
    
    <summary type="html">In 2023 I spent most of my time on work, learning and dating.
Compared with the last year, devoted less time on this blog and
community. It might be a</summary>
    
    
    
    
    <category term="essays" scheme="https://luyuhuang.tech/tags/essays/"/>
    
    <category term="english" scheme="https://luyuhuang.tech/tags/english/"/>
    
  </entry>
  
  <entry>
    <title>Scheme 语言</title>
    <link href="https://luyuhuang.tech/2023/07/09/scheme-lang.html"/>
    <id>https://luyuhuang.tech/2023/07/09/scheme-lang.html</id>
    <published>2023-07-08T16:00:00.000Z</published>
    <updated>2025-06-26T03:59:29.937Z</updated>
    
    <content type="html"><![CDATA[<p>我最近在读 <em>SICP (Structure and Interpretation of Computer
Programs)</em>，中文译名是《计算机程序的构造与解释》，感觉受益匪浅。我打算开个坑，总结分享一些我学到的内容。<em>SICP</em>
综合性非常强，内容包括函数式编程、数据结构的分层与抽象、面向对象、无限流、元循环解释器、惰性求值、非确定性编程、逻辑编程、汇编语言与机器、编译原理等等。我只能选取一个主题抛砖引玉，这个系列文章的主题是
continuation，主要内容可能包括：</p>
<ul>
<li>Scheme 语言</li>
<li>Scheme 元循环解释器</li>
<li>神奇的 <code>call/cc</code></li>
<li>通过 CPS 解释器实现 <code>call/cc</code></li>
<li>通过 CPS 变换（也就是传说中的<a href="https://www.zhihu.com/question/20822815">“王垠 40
行代码”</a>）实现 <code>call/cc</code></li>
<li>…</li>
</ul>
<p>我最近已经在读最后一章了，待读完本书后再看情况更新一些内容。这些内容的基础是
Scheme 语言，我们从介绍 Scheme 语言开始。本文介绍的 Scheme
语言主要目的是让不了解 Scheme
的同学看完之后能看得懂后面几篇文章，因此不会涉及到一些很细节的内容。（特别细节的内容我也不懂，<em>SICP</em>
也没有很深入介绍）如果要深入了解，可以阅读相关的文档。</p>
<h2 id="scheme-的特性">1 Scheme 的特性</h2>
<p>Scheme 是一种 Lisp 的方言。而 Lisp
是世界上第二古老的语言（第一古老的是
Fortran），有着众多的方言。这些方言有着一个共同的特性——基于 <strong>S
表达式 (S-expressions)</strong>。</p>
<p>S 表达式可以是<strong>原子表达式 (atom)</strong>
或者<strong>列表</strong>。原子表达式可以是数字，如 <code>1</code>,
<code>42</code>, <code>3.14</code>；可以是字符串，如
<code>"hello"</code>；可以是布尔值，如 <code>#t</code>,
<code>#f</code>；也可以直接是符号，如 <code>a</code>, <code>if</code>,
<code>add</code>。而列表则是将若干个 S
表达式放在一对括号里，用空格隔开：</p>
<pre><code class="hljs scheme">(<span class="hljs-name">&lt;s-exp1&gt;</span> &lt;s-exp2&gt; &lt;s-exp3&gt; ...)<br></code></pre>
<p>下面给出了一些 S 表达式的例子：</p>
<pre><code class="hljs scheme"><span class="hljs-number">100</span><br><span class="hljs-number">100.13</span><br><span class="hljs-string">"Hello world"</span><br>(<span class="hljs-name">add</span> <span class="hljs-number">1</span> <span class="hljs-number">2</span>)<br>(<span class="hljs-name"><span class="hljs-built_in">display</span></span> <span class="hljs-string">"Hello world"</span>)<br>(<span class="hljs-name"><span class="hljs-built_in">list</span></span> (<span class="hljs-name"><span class="hljs-built_in">list</span></span> <span class="hljs-number">1</span> <span class="hljs-number">2</span>) (<span class="hljs-name"><span class="hljs-built_in">list</span></span> <span class="hljs-string">"a"</span> <span class="hljs-string">"b"</span>))<br></code></pre>
<p>前三个 S 表达式都是原子表达式。<code>(add 1 2)</code> 是一个长度为 3
的列表，3 个元素分别是符号 <code>add</code>、数字 1 和数字
2。<code>(display "Hello world")</code> 是一个长度为 2
的列表，第一个元素是符号 <code>display</code>，第二个元素是字符串
<code>"Hello world"</code>。<code>(list (list 1 2) (list "a" "b"))</code>
是一个长度为 3 的列表，三个元素分别是符号 <code>list</code>、列表
<code>(list 1 2)</code>、列表 <code>(list "a" "b")</code>。</p>
<p>Scheme 全部是由 S 表达式组成的。在 Scheme
中，复合表达式的第一个元素作为表达式的类型，剩余的元素则作为表达式的参数。</p>
<figure>
<img src="/assets/images/scheme-lang_1.png" alt="s-exp">
<figcaption aria-hidden="true">s-exp</figcaption>
</figure>
<p>表达式类型决定这个表达式的语义和参数的含义。例如 <code>if</code>
表达式规定有三个参数，第一个参数为条件，第二个参数为条件为真时执行的表达式，第三个参数为条件为假时执行的表达式。由于
S 表达式可以任意嵌套，因此利用它就可以构造出任意复杂的代码。下面就是一段
Scheme 代码的例子：</p>
<pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">queens</span> board-size)<br>  (<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">queen-cols</span> k)<br>    (<span class="hljs-name"><span class="hljs-built_in">if</span></span> (<span class="hljs-name"><span class="hljs-built_in">=</span></span> k <span class="hljs-number">0</span>)<br>      (<span class="hljs-name"><span class="hljs-built_in">list</span></span> '())<br>      (<span class="hljs-name">filter</span><br>        (<span class="hljs-name"><span class="hljs-built_in">lambda</span></span> (positions) (<span class="hljs-name">safe?</span> positions))<br>        (<span class="hljs-name">flatmap</span><br>          (<span class="hljs-name"><span class="hljs-built_in">lambda</span></span> (rest-of-queens)<br>            (<span class="hljs-name"><span class="hljs-built_in">map</span></span> (<span class="hljs-name"><span class="hljs-built_in">lambda</span></span> (new-row)<br>                   (<span class="hljs-name">adjoin-position</span> new-row k rest-of-queens))<br>                 (<span class="hljs-name">enumerate-interval</span> <span class="hljs-number">1</span> board-size)))<br>          (<span class="hljs-name">queen-cols</span> (<span class="hljs-name"><span class="hljs-built_in">-</span></span> k <span class="hljs-number">1</span>))))))<br>  (<span class="hljs-name">queen-cols</span> board-size))<br></code></pre>
<p>可以看到 S
表达式层层嵌套，形成了一个树状结构，这其实就是语法树。也就是说这个语言实际是把语法树明确的写出来。后面我们能看到这种做法的好处：代码可以直接表示为数据结构，代码极其容易解析、编译。</p>
<h2 id="编程环境">2 编程环境</h2>
<p>Scheme 作为 Lisp 的一种方言，它本身又有很多方言，例如 Chez Scheme,
MIT Scheme, Racket 等。我们使用的环境是
Racket，它功能强大，易于使用。我们可以到它的<a href="https://racket-lang.org/">官网</a>下载最新版本。Racket 自带一个
IDE，叫 DrRacket，我们可以使用它学习编写 Scheme。</p>
<p>打开 DrRacket，就可以开始 Scheme
编程了。程序的第一行需要声明所使用的语言
<code>#lang racket</code>。编辑好了后点击 “Run” 便可执行代码。</p>
<figure>
<img src="/assets/images/scheme-lang_2.png" alt="s-exp">
<figcaption aria-hidden="true">s-exp</figcaption>
</figure>
<p>有些同学可能不习惯这种全是括号的语言，阅读代码需要数括号，十分麻烦。但如果代码做好缩进与对齐，之间的嵌套关系是一目了然的。我们可以让参数另起一行，相对类型缩进两个空格：</p>
<pre><code class="hljs scheme">(<span class="hljs-name">type</span><br>  arg1<br>  arg2<br>  ...)<br></code></pre>
<p>或者第一个参数与类型同行，后续参数与第一个参数对齐：</p>
<pre><code class="hljs scheme">(<span class="hljs-name">type</span> arg1<br>      arg2<br>      ...)<br></code></pre>
<p>如果第一个参数比较特殊，也可以让第一个参数与类型同行，剩余的参数另起一行，并缩进两个空格</p>
<pre><code class="hljs scheme">(<span class="hljs-name">type</span> special-arg1<br>  arg2<br>  arg3<br>  ...)<br></code></pre>
<p>基本上就这三种缩进风格。使用 DrRacket
可以自动缩进；阅读代码时一般不需要关心括号，直接看代码缩进即可，就像
Python 一样。</p>
<h2 id="基础表达式">3 基础表达式</h2>
<p>一个高级语言一定具备这三个要素：</p>
<ol type="1">
<li><strong>原子表达式 (primitive
expressions)</strong>：语言提供的最简单、最基础的元素。</li>
<li><strong>组合方法 (means of
combination)</strong>：将原子表达式组合成复合元素的方法。</li>
<li><strong>抽象方法 (means of
abstraction)</strong>：给复合元素命名，从而将其作为一个整体操作。</li>
</ol>
<p>我们说汇编语言不是高级语言，因为它有非常弱的组合能力和抽象能力。例如
<code>add $42 %eax</code> 可以表示 <code>eax + 42</code>，但是要想表示
<code>(eax + 42) * 3</code>
就得写两条指令了，因为这个语言根本没有嵌套组合的能力。至于抽象能力，汇编中的函数（准确来说应该是
subroutine）更像是个 goto。而 Scheme
是非常高级的语言，因为它有非常强的组合能力和抽象能力，稍后我们可以看到。</p>
<h3 id="原子表达式">3.1 原子表达式</h3>
<p>原子表达式有这么几种：</p>
<ul>
<li>数字。可以是整数 <code>10</code>、<code>-12</code>；浮点数
<code>3.14</code>；有理数
<code>1/2</code>、<code>-3/5</code>，形式是两个由 <code>/</code>
分隔的整数，注意中间不能有空格，因为这是一个原子。</li>
<li>字符串。由双引号标识，如 <code>"Hello world"</code>。</li>
<li>布尔。有两种，<code>#t</code> 和 <code>#f</code>。</li>
<li>符号。也就是所谓的“变量”，或者说标识符。例如 <code>pi</code>，值为
<code>3.141592653589793</code>；<code>sqrt</code>，为一个内建函数。不同于很多语言，Scheme
的符号不局限于字母、数字和下划线，例如
<code>reset!</code>、<code>*important*</code>、<code>+</code>、<code>1st-item</code>
都是有效的符号。</li>
</ul>
<h3 id="复合表达式">3.2 复合表达式</h3>
<p>Scheme 中的复合表达式有两种，特殊形 (special form) 和函数调用。Scheme
函数调用的语法是 <code>(function arg1 arg2 ...)</code>，让 S
表达式的第一个元素为函数，剩余元素为函数参数。例如下面的几个表达式都是函数调用：</p>
<pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">sqrt</span></span> <span class="hljs-number">2</span>)<br>(<span class="hljs-name"><span class="hljs-built_in">+</span></span> <span class="hljs-number">1</span> <span class="hljs-number">2</span>)<br>(<span class="hljs-name"><span class="hljs-built_in">*</span></span> (<span class="hljs-name"><span class="hljs-built_in">+</span></span> <span class="hljs-number">1</span> <span class="hljs-number">2</span>) (<span class="hljs-name"><span class="hljs-built_in">+</span></span> <span class="hljs-number">3</span> <span class="hljs-number">4</span>))<br></code></pre>
<p>这里的 <code>sqrt</code>，<code>+</code>，<code>*</code>
都是函数名，分别执行平方根、加法和乘法操作。与其他大部分语言不同，Scheme
没有运算符，加减乘除运算、比较运算等都是函数。</p>
<p>对于初学者来说可能有些奇怪，但这种语法有很大的好处。首先表达式关系明确无歧义，程序员不需要记忆运算符优先级、是左结合还是右结合，且程序容易解析编译。使用方式统一，不会像
C 语言一样，乘法运算是 <code>a * b</code>，指数运算却是
<code>pow(a, b)</code>。不需要 C++
那样复杂的运算符重载规则，直接定义一个名为
<code>+</code>、<code>*</code> 的函数即可。</p>
<p>下面给出了一些常用函数和调用方式：</p>
<pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">+</span></span> <span class="hljs-number">1</span> <span class="hljs-number">1</span>) <span class="hljs-comment">;; 加法</span><br>(<span class="hljs-name"><span class="hljs-built_in">-</span></span> <span class="hljs-number">1</span> <span class="hljs-number">1</span>) <span class="hljs-comment">;; 减法</span><br>(<span class="hljs-name"><span class="hljs-built_in">*</span></span> <span class="hljs-number">2</span> <span class="hljs-number">3</span>) <span class="hljs-comment">;; 乘法</span><br>(<span class="hljs-name"><span class="hljs-built_in">/</span></span> <span class="hljs-number">3</span> <span class="hljs-number">2</span>) <span class="hljs-comment">;; 除法。整数触发会返回有理数，这个例子返回 3/2</span><br>(<span class="hljs-name"><span class="hljs-built_in">=</span></span> <span class="hljs-number">2</span> <span class="hljs-number">2</span>) <span class="hljs-comment">;; 判断两个数字是否相等</span><br>(<span class="hljs-name"><span class="hljs-built_in">&lt;</span></span> <span class="hljs-number">2</span> <span class="hljs-number">3</span>) <span class="hljs-comment">;; 第一个参数是否小于第二个参数。类似的还有 &gt;, &lt;= &gt;=</span><br>(<span class="hljs-name"><span class="hljs-built_in">eq?</span></span> a b) <span class="hljs-comment">;; 判断两个对象是否相同，可以理解成比较地址</span><br>(<span class="hljs-name"><span class="hljs-built_in">remainder</span></span> <span class="hljs-number">3</span> <span class="hljs-number">2</span>) <span class="hljs-comment">;; 求余数。这个例子返回 1</span><br>(<span class="hljs-name"><span class="hljs-built_in">sqrt</span></span> <span class="hljs-number">2</span>) <span class="hljs-comment">;; 开根号</span><br>(<span class="hljs-name"><span class="hljs-built_in">display</span></span> <span class="hljs-string">"Hello world"</span>) <span class="hljs-comment">;; 打印到标准输出</span><br>(<span class="hljs-name"><span class="hljs-built_in">newline</span></span>) <span class="hljs-comment">;; 打印换行符</span><br></code></pre>
<p>分号 <code>;</code> 在 Scheme 中用作单行注释。</p>
<p>看到这里，你可能会以为表达式 <code>(if (&gt; a b) a b)</code>
也是调用了一个 <code>if</code>
函数。但，实际上不是。对函数求值时，会先依次对各个参数求值，然后再调用函数。而对于
<code>if</code> 来说，当 <code>(&gt; a b)</code> 为真时，只应该对
<code>a</code> 求值，不应该对 <code>b</code> 求值。反之，只应该对
<code>b</code> 求值。因此 <code>if</code>
不能是函数，应该是一个特殊形。</p>
<p>S
表达式就像是语法树的表示，而特殊形就是一种特定的语法，它定义这个语法有哪些子节点，含义分别是什么。下面给出了一些常用的特殊形和使用方式。</p>
<pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">if</span></span> predicate consequence alternative) <span class="hljs-comment">;; 如果 predicate 为真返回 consequence, 否则返回 alternative</span><br><br><span class="hljs-comment">;; 方括号 [] 与圆括号 () 等价，可交错使用方括号和圆括号提升可读性。</span><br>(<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [<span class="hljs-name">predicate1</span> consequence1] <span class="hljs-comment">;; 依次判断: 如果 predicate1 为真返回 consequence1</span><br>      [<span class="hljs-name">predicate2</span> consequence2] <span class="hljs-comment">;; 如果 predicate2 为真返回 consequence2</span><br>      ...<br>      [<span class="hljs-name"><span class="hljs-built_in">else</span></span> alternative]) <span class="hljs-comment">;; 如果所有的条件都不成立，则返回 alternative</span><br><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> var val) <span class="hljs-comment">;; 定义变量 var 的值为 val</span><br><br>(<span class="hljs-name"><span class="hljs-built_in">and</span></span> exp1 exp2 ...) <span class="hljs-comment">;; 逻辑与，遵循短路原则（所以必须是特殊形）</span><br>(<span class="hljs-name"><span class="hljs-built_in">or</span></span> exp1 exp2 ...) <span class="hljs-comment">;; 逻辑或，遵循短路原则</span><br><span class="hljs-comment">;; 逻辑非是一个函数 (not exp)</span><br><br>(<span class="hljs-name"><span class="hljs-built_in">begin</span></span> exp1 exp2 ...) <span class="hljs-comment">;; 按顺序依次对 exp1, exp2, ... 求值，整个表达式的值为最后一个表达式的值。类似于 C 中的逗号运算符。</span><br><br>(<span class="hljs-name"><span class="hljs-built_in">lambda</span></span> (arg1 arg2 ...) body ...) <span class="hljs-comment">;; 构造一个函数，第 4 节详细介绍</span><br></code></pre>
<h2 id="定义函数">4 定义函数</h2>
<p><code>lambda</code> 特殊形创建一个函数，形式为
<code>(lambda (arg1 arg2 ...) body ...)</code>。其中
<code>(arg1 arg2 ...)</code> 为参数列表，剩下的 <code>body ...</code>
为函数体，可由多个表达式组成，函数的返回值为最后一个表达式的值。我们通常结合
<code>define</code> 定义函数，下面给出了一个例子</p>
<pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> gcd<br>  (<span class="hljs-name"><span class="hljs-built_in">lambda</span></span> (a b)<br>    (<span class="hljs-name"><span class="hljs-built_in">if</span></span> (<span class="hljs-name"><span class="hljs-built_in">=</span></span> b <span class="hljs-number">0</span>)<br>        a<br>        (<span class="hljs-name"><span class="hljs-built_in">gcd</span></span> b (<span class="hljs-name"><span class="hljs-built_in">remainder</span></span> a b)))))<br></code></pre>
<p>这个函数实现欧几里得算法，求两个整数 <code>a</code> 和 <code>b</code>
的最大公约数。函数参数列表是 <code>(a b)</code>，函数体只有一个
<code>if</code> 表达式。<code>if</code> 表达式检查 <code>b</code> 是否为
0，如果 <code>b</code> 为 0 则返回 <code>a</code>，否则递归调用自身
<code>(gcd b (remainder a b))</code>。现在我们就可以调用
<code>gcd</code> 了：</p>
<pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">gcd</span></span> <span class="hljs-number">10</span> <span class="hljs-number">12</span>) <span class="hljs-comment">;; 2</span><br>(<span class="hljs-name"><span class="hljs-built_in">gcd</span></span> <span class="hljs-number">7</span> <span class="hljs-number">11</span>) <span class="hljs-comment">;; 1</span><br></code></pre>
<p>由于我们经常使用 <code>define</code> 和 <code>lambda</code>
定义函数，我们有一种简便的写法
<code>(define (fname args ...) body ...)</code> 等价于
<code>(define fname (lambda (args ...) body ...))</code>。因此
<code>gcd</code> 还可写成这样</p>
<pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name"><span class="hljs-built_in">gcd</span></span> a b)<br>  (<span class="hljs-name"><span class="hljs-built_in">if</span></span> (<span class="hljs-name"><span class="hljs-built_in">=</span></span> b <span class="hljs-number">0</span>)<br>      a<br>      (<span class="hljs-name"><span class="hljs-built_in">gcd</span></span> b (<span class="hljs-name"><span class="hljs-built_in">remainder</span></span> a b)))))<br></code></pre>
<h3 id="环境">4.1 环境</h3>
<p>函数可以嵌套定义。例如定义函数 <code>prime?</code>
判断一个数是否是质数，我们寻找能整除它的大于 1
的整数。如果找不到能整除它的整数，则它是一个质数</p>
<pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">prime?</span> n)<br>  (<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">iter</span> i)<br>    (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name"><span class="hljs-built_in">&gt;</span></span> (<span class="hljs-name"><span class="hljs-built_in">*</span></span> i i) n) <span class="hljs-literal">#t</span>]<br>          [(<span class="hljs-name"><span class="hljs-built_in">=</span></span> (<span class="hljs-name"><span class="hljs-built_in">remainder</span></span> n i) <span class="hljs-number">0</span>) <span class="hljs-literal">#f</span>]<br>          [<span class="hljs-name"><span class="hljs-built_in">else</span></span> (<span class="hljs-name">iter</span> (<span class="hljs-name"><span class="hljs-built_in">+</span></span> i <span class="hljs-number">1</span>))]))<br>  (<span class="hljs-name">iter</span> <span class="hljs-number">2</span>))<br></code></pre>
<p><code>prime?</code> 所在的环境称为全局环境，<code>iter</code>
所在的环境为 <code>prime?</code> 的内部环境。<code>define</code>
执行的时候，会在它所处的环境中增加一个变量。当函数被调用时，会创建一个新环境，这个新环境继承函数定义时所在的环境；而函数的参数就在新环境中实例化。对表达式求值，会先在当前环境寻找变量的值，如果找不到则在上层环境寻找，依次类推。因此要考察一个函数的行为，必须考虑两个要素：这个函数的代码，和这个函数所在的环境。这两要素有时合在一起称为“闭包”。</p>
<figure>
<img src="/assets/images/scheme-lang_3.png" alt="env">
<figcaption aria-hidden="true">env</figcaption>
</figure>
<p>当我们在全局环境中执行 <code>(prime? 11)</code>
时，会有这么几步：</p>
<ul>
<li>在全局环境中找到 <code>prime?</code>
变量，发现它是一个函数，调用它。</li>
<li>读取闭包的 Env 字段，发现这个这个函数的环境是全局环境
G，因此创建一个继承 G 的新环境，记作 E1。</li>
<li>在 E1 中实例化参数，有 <code>n: 11</code>。</li>
<li>开始执行 <code>prime?</code> 的代码。</li>
<li>执行 <code>(define (iter i) ...)</code>，在 E1 中添加变量
<code>iter</code>。<code>iter</code> 是一个函数，所在的环境指向
E1。</li>
<li>执行 <code>(iter 2)</code>，在 E1 中找到
<code>iter</code>，发现它是一个函数，调用它。</li>
<li>发现这个函数的环境是 E1，因此创建一个继承 E1 的新环境，记作
E2。</li>
<li>在 E2 中实例化参数，有 <code>i: 2</code>。</li>
<li>开始执行 <code>iter</code> 的代码。</li>
<li>执行到 <code>(&gt; (* i i) n)</code>：
<ul>
<li>在 E2 找查找变量 <code>*</code>，找不到；然后再 E1
中找，还是找不到；最后在 G 中找到 <code>*</code> 是个内建函数。</li>
<li>在 E2 中查找变量 <code>i</code>，找到 <code>i: 2</code>。</li>
<li>在 E2 中查找变量 <code>n</code>，找不到；然后在 E1 中找，找到
<code>n: 11</code></li>
<li>…</li>
</ul></li>
<li>…</li>
<li>执行 <code>(iter (+ i 1))</code>，可在 E2 中找到
<code>i: 2</code>，在 E1 中找到 <code>iter</code>。调用
<code>iter</code>。</li>
<li>发现 <code>iter</code> 所在的环境是 E1，因此创建一个继承 E1 的新环境
E3。</li>
<li>在 E3 中实例化参数，有 <code>i: 3</code>。</li>
<li>开始执行 <code>iter</code> 的代码，以此类推。</li>
</ul>
<p>这便是 Scheme
环境的运作机制。下一篇文章我们会实现这个机制，从而实现一个 Scheme
解释器。</p>
<p>Scheme
的函数是一等公民，我们可以将函数当作参数传递，也可以当成返回值返回。当函数被传递时，它所在的环境也将被传递。例如</p>
<pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">f</span> x)<br>  (<span class="hljs-name"><span class="hljs-built_in">lambda</span></span> () x))<br><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> n (<span class="hljs-name">f</span> <span class="hljs-number">10</span>))<br>(<span class="hljs-name">n</span>) <span class="hljs-comment">;; 10</span><br></code></pre>
<p>函数 <code>f</code> 返回一个函数，这个函数便保存了调用 <code>f</code>
时创建的环境。因此我们可以通过这个函数获取到调用 <code>f</code>
时传的值。后面我们可以看到这个机制有趣的应用。</p>
<h3 id="let-与-let">4.2 <code>let</code> 与 <code>let*</code></h3>
<p>当我们需要中间变量时，例如计算 <span class="math inline"><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.566ex;" xmlns="http://www.w3.org/2000/svg" width="24.156ex" height="2.452ex" role="img" focusable="false" viewBox="0 -833.9 10677 1083.9"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mn"><path data-c="35" d="M164 157Q164 133 148 117T109 101H102Q148 22 224 22Q294 22 326 82Q345 115 345 210Q345 313 318 349Q292 382 260 382H254Q176 382 136 314Q132 307 129 306T114 304Q97 304 95 310Q93 314 93 485V614Q93 664 98 664Q100 666 102 666Q103 666 123 658T178 642T253 634Q324 634 389 662Q397 666 402 666Q410 666 410 648V635Q328 538 205 538Q174 538 149 544L139 546V374Q158 388 169 396T205 412T256 420Q337 420 393 355T449 201Q449 109 385 44T229 -22Q148 -22 99 32T50 154Q50 178 61 192T84 210T107 214Q132 214 148 197T164 157Z"></path></g><g data-mml-node="mo" transform="translate(500,0)"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z"></path></g><g data-mml-node="mn" transform="translate(889,0)"><path data-c="33" d="M127 463Q100 463 85 480T69 524Q69 579 117 622T233 665Q268 665 277 664Q351 652 390 611T430 522Q430 470 396 421T302 350L299 348Q299 347 308 345T337 336T375 315Q457 262 457 175Q457 96 395 37T238 -22Q158 -22 100 21T42 130Q42 158 60 175T105 193Q133 193 151 175T169 130Q169 119 166 110T159 94T148 82T136 74T126 70T118 67L114 66Q165 21 238 21Q293 21 321 74Q338 107 338 175V195Q338 290 274 322Q259 328 213 329L171 330L168 332Q166 335 166 348Q166 366 174 366Q202 366 232 371Q266 376 294 413T322 525V533Q322 590 287 612Q265 626 240 626Q208 626 181 615T143 592T132 580H135Q138 579 143 578T153 573T165 566T175 555T183 540T186 520Q186 498 172 481T127 463Z"></path></g><g data-mml-node="msup" transform="translate(1389,0)"><g data-mml-node="mi"><path data-c="1D465" d="M52 289Q59 331 106 386T222 442Q257 442 286 424T329 379Q371 442 430 442Q467 442 494 420T522 361Q522 332 508 314T481 292T458 288Q439 288 427 299T415 328Q415 374 465 391Q454 404 425 404Q412 404 406 402Q368 386 350 336Q290 115 290 78Q290 50 306 38T341 26Q378 26 414 59T463 140Q466 150 469 151T485 153H489Q504 153 504 145Q504 144 502 134Q486 77 440 33T333 -11Q263 -11 227 52Q186 -10 133 -10H127Q78 -10 57 16T35 71Q35 103 54 123T99 143Q142 143 142 101Q142 81 130 66T107 46T94 41L91 40Q91 39 97 36T113 29T132 26Q168 26 194 71Q203 87 217 139T245 247T261 313Q266 340 266 352Q266 380 251 392T217 404Q177 404 142 372T93 290Q91 281 88 280T72 278H58Q52 284 52 289Z"></path></g><g data-mml-node="mn" transform="translate(605,363) scale(0.707)"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z"></path></g></g><g data-mml-node="mo" transform="translate(2619.8,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z"></path></g><g data-mml-node="mn" transform="translate(3620,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z"></path></g><g data-mml-node="msup" transform="translate(4120,0)"><g data-mml-node="mo"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z"></path></g><g data-mml-node="mn" transform="translate(422,363) scale(0.707)"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z"></path></g></g><g data-mml-node="mo" transform="translate(5167.8,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z"></path></g><g data-mml-node="mn" transform="translate(6168,0)"><path data-c="34" d="M462 0Q444 3 333 3Q217 3 199 0H190V46H221Q241 46 248 46T265 48T279 53T286 61Q287 63 287 115V165H28V211L179 442Q332 674 334 675Q336 677 355 677H373L379 671V211H471V165H379V114Q379 73 379 66T385 54Q393 47 442 46H471V0H462ZM293 211V545L74 212L183 211H293Z"></path></g><g data-mml-node="mo" transform="translate(6668,0)"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z"></path></g><g data-mml-node="mn" transform="translate(7057,0)"><path data-c="33" d="M127 463Q100 463 85 480T69 524Q69 579 117 622T233 665Q268 665 277 664Q351 652 390 611T430 522Q430 470 396 421T302 350L299 348Q299 347 308 345T337 336T375 315Q457 262 457 175Q457 96 395 37T238 -22Q158 -22 100 21T42 130Q42 158 60 175T105 193Q133 193 151 175T169 130Q169 119 166 110T159 94T148 82T136 74T126 70T118 67L114 66Q165 21 238 21Q293 21 321 74Q338 107 338 175V195Q338 290 274 322Q259 328 213 329L171 330L168 332Q166 335 166 348Q166 366 174 366Q202 366 232 371Q266 376 294 413T322 525V533Q322 590 287 612Q265 626 240 626Q208 626 181 615T143 592T132 580H135Q138 579 143 578T153 573T165 566T175 555T183 540T186 520Q186 498 172 481T127 463Z"></path></g><g data-mml-node="msup" transform="translate(7557,0)"><g data-mml-node="mi"><path data-c="1D465" d="M52 289Q59 331 106 386T222 442Q257 442 286 424T329 379Q371 442 430 442Q467 442 494 420T522 361Q522 332 508 314T481 292T458 288Q439 288 427 299T415 328Q415 374 465 391Q454 404 425 404Q412 404 406 402Q368 386 350 336Q290 115 290 78Q290 50 306 38T341 26Q378 26 414 59T463 140Q466 150 469 151T485 153H489Q504 153 504 145Q504 144 502 134Q486 77 440 33T333 -11Q263 -11 227 52Q186 -10 133 -10H127Q78 -10 57 16T35 71Q35 103 54 123T99 143Q142 143 142 101Q142 81 130 66T107 46T94 41L91 40Q91 39 97 36T113 29T132 26Q168 26 194 71Q203 87 217 139T245 247T261 313Q266 340 266 352Q266 380 251 392T217 404Q177 404 142 372T93 290Q91 281 88 280T72 278H58Q52 284 52 289Z"></path></g><g data-mml-node="mn" transform="translate(605,363) scale(0.707)"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z"></path></g></g><g data-mml-node="mo" transform="translate(8787.8,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z"></path></g><g data-mml-node="mn" transform="translate(9788,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z"></path></g><g data-mml-node="mo" transform="translate(10288,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z"></path></g></g></g></svg></mjx-container></span>，为了避免重复计算，我们需要一个中间变量 <span class="math inline"><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.186ex;" xmlns="http://www.w3.org/2000/svg" width="11.144ex" height="2.072ex" role="img" focusable="false" viewBox="0 -833.9 4925.6 915.9"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D461" d="M26 385Q19 392 19 395Q19 399 22 411T27 425Q29 430 36 430T87 431H140L159 511Q162 522 166 540T173 566T179 586T187 603T197 615T211 624T229 626Q247 625 254 615T261 596Q261 589 252 549T232 470L222 433Q222 431 272 431H323Q330 424 330 420Q330 398 317 385H210L174 240Q135 80 135 68Q135 26 162 26Q197 26 230 60T283 144Q285 150 288 151T303 153H307Q322 153 322 145Q322 142 319 133Q314 117 301 95T267 48T216 6T155 -11Q125 -11 98 4T59 56Q57 64 57 83V101L92 241Q127 382 128 383Q128 385 77 385H26Z"></path></g><g data-mml-node="mo" transform="translate(638.8,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z"></path></g><g data-mml-node="mn" transform="translate(1694.6,0)"><path data-c="33" d="M127 463Q100 463 85 480T69 524Q69 579 117 622T233 665Q268 665 277 664Q351 652 390 611T430 522Q430 470 396 421T302 350L299 348Q299 347 308 345T337 336T375 315Q457 262 457 175Q457 96 395 37T238 -22Q158 -22 100 21T42 130Q42 158 60 175T105 193Q133 193 151 175T169 130Q169 119 166 110T159 94T148 82T136 74T126 70T118 67L114 66Q165 21 238 21Q293 21 321 74Q338 107 338 175V195Q338 290 274 322Q259 328 213 329L171 330L168 332Q166 335 166 348Q166 366 174 366Q202 366 232 371Q266 376 294 413T322 525V533Q322 590 287 612Q265 626 240 626Q208 626 181 615T143 592T132 580H135Q138 579 143 578T153 573T165 566T175 555T183 540T186 520Q186 498 172 481T127 463Z"></path></g><g data-mml-node="msup" transform="translate(2194.6,0)"><g data-mml-node="mi"><path data-c="1D465" d="M52 289Q59 331 106 386T222 442Q257 442 286 424T329 379Q371 442 430 442Q467 442 494 420T522 361Q522 332 508 314T481 292T458 288Q439 288 427 299T415 328Q415 374 465 391Q454 404 425 404Q412 404 406 402Q368 386 350 336Q290 115 290 78Q290 50 306 38T341 26Q378 26 414 59T463 140Q466 150 469 151T485 153H489Q504 153 504 145Q504 144 502 134Q486 77 440 33T333 -11Q263 -11 227 52Q186 -10 133 -10H127Q78 -10 57 16T35 71Q35 103 54 123T99 143Q142 143 142 101Q142 81 130 66T107 46T94 41L91 40Q91 39 97 36T113 29T132 26Q168 26 194 71Q203 87 217 139T245 247T261 313Q266 340 266 352Q266 380 251 392T217 404Q177 404 142 372T93 290Q91 281 88 280T72 278H58Q52 284 52 289Z"></path></g><g data-mml-node="mn" transform="translate(605,363) scale(0.707)"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z"></path></g></g><g data-mml-node="mo" transform="translate(3425.3,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z"></path></g><g data-mml-node="mn" transform="translate(4425.6,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z"></path></g></g></g></svg></mjx-container></span>。这个使用我们会使用
<code>let</code> 特殊形。</p>
<pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">let</span></span> ([<span class="hljs-name">t</span> (<span class="hljs-name"><span class="hljs-built_in">+</span></span> (<span class="hljs-name"><span class="hljs-built_in">*</span></span> <span class="hljs-number">3</span> x x) <span class="hljs-number">1</span>)])<br>  (<span class="hljs-name"><span class="hljs-built_in">+</span></span> (<span class="hljs-name"><span class="hljs-built_in">*</span></span> <span class="hljs-number">5</span> t t) (<span class="hljs-name"><span class="hljs-built_in">*</span></span> <span class="hljs-number">4</span> t)))<br></code></pre>
<p><code>let</code> 的语法格式如下：</p>
<pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">let</span></span> ([<span class="hljs-name">var1</span> val1] <span class="hljs-comment">;; 定义若干个变量</span><br>      [<span class="hljs-name">var2</span> val2]<br>      ...)<br>  body            <span class="hljs-comment">;; 可在 body 中使用这些变量</span><br>  ...)<br><span class="hljs-comment">;; let 外不能使用这些变量</span><br></code></pre>
<p>它其实是个语法糖，等价于使用 <code>lambda</code>
创建一个函数，然后立刻调用它：</p>
<pre><code class="hljs scheme">((<span class="hljs-name"><span class="hljs-built_in">lambda</span></span> (var1 var2 ...)<br>    body<br>    ...)<br>  val1 val2 ...)<br></code></pre>
<p><code>let</code>
有一个缺陷，就是定义后面的变量的值时不能引用前面的变量，也就是说
<code>(let ([a 1] [b (+ a 1)]) b)</code> 是非法的。于是我们有
<code>let*</code>：</p>
<pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">let*</span></span> ([<span class="hljs-name">var1</span> val1]<br>       [<span class="hljs-name">var2</span> val2] <span class="hljs-comment">;; val2 可以引用 var1</span><br>       ...)<br>  body<br>  ...)<br></code></pre>
<p>它也是一个语法糖，等价于</p>
<pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">let</span></span> ([<span class="hljs-name">var1</span> val1])<br>  (<span class="hljs-name"><span class="hljs-built_in">let</span></span> ([<span class="hljs-name">var2</span> val2])<br>    (<span class="hljs-name"><span class="hljs-built_in">let</span></span> ...<br>      body<br>      ...))<br></code></pre>
<p><code>let*</code> 通过嵌套 <code>let</code>
实现，因此允许引用前面的变量。</p>
<h2 id="数据结构">5 数据结构</h2>
<p>前面介绍了代码的组合和抽象，这一节介绍数据结构。这一系列文章只会用到非常简单的数据结构。</p>
<h3 id="有序对和列表">5.1 有序对和列表</h3>
<p>为了构造复合结构，我们用 <code>cons</code> 构造<strong>有序对
(pair)</strong>。<code>car</code>
获取有序对的第一个元素，<code>cdr</code> 获取有序对的第二个元素。</p>
<pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> p (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> <span class="hljs-number">1</span> <span class="hljs-number">2</span>))<br>(<span class="hljs-name"><span class="hljs-built_in">car</span></span> p) <span class="hljs-comment">;; 1</span><br>(<span class="hljs-name"><span class="hljs-built_in">cdr</span></span> p) <span class="hljs-comment">;; 2</span><br></code></pre>
<p>有序对可以任意嵌套，如
<code>(cons (cons 1 2) (cons 3 4))</code>。因为可以任意嵌套，所以理论上仅靠有序对就可以构造出任意复杂的数据结构。如果将有序对依次连接，就得到了一个链式列表：</p>
<pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">cons</span></span> <span class="hljs-number">1</span> (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> <span class="hljs-number">2</span> (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> <span class="hljs-number">3</span> (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> <span class="hljs-number">4</span> '()))))<br></code></pre>
<p>每个有序对的第一个元素 (car) 存储当前节点的值，第二个元素 (cdr)
指向下一个节点。最后一个元素的 cdr 为 <code>'()</code>，表示
NIL，链表的结尾。使用 <code>list</code> 函数可以快速创建一个列表：</p>
<pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">list</span></span> <span class="hljs-number">1</span> <span class="hljs-number">2</span> <span class="hljs-number">3</span> <span class="hljs-number">4</span>) <span class="hljs-comment">;; 等价于 (cons 1 (cons 2 (cons 3 (cons 4 '()))))</span><br></code></pre>
<p>这样对于列表来说，<code>car</code>
用于获取列表的第一个元素，<code>cdr</code> 用于获取列表剩余的元素，而
<code>cons</code> 在列表头部插入一个元素。</p>
<pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> items (<span class="hljs-name"><span class="hljs-built_in">list</span></span> <span class="hljs-number">1</span> <span class="hljs-number">2</span> <span class="hljs-number">3</span> <span class="hljs-number">4</span>))<br><br>(<span class="hljs-name"><span class="hljs-built_in">car</span></span> items) <span class="hljs-comment">;; 1</span><br>(<span class="hljs-name"><span class="hljs-built_in">cdr</span></span> items) <span class="hljs-comment">;; '(2 3 4)</span><br>(<span class="hljs-name"><span class="hljs-built_in">cons</span></span> <span class="hljs-number">0</span> items) <span class="hljs-comment">;; '(0 1 2 3 4)</span><br><br>(<span class="hljs-name"><span class="hljs-built_in">car</span></span> (<span class="hljs-name"><span class="hljs-built_in">cdr</span></span> items)) <span class="hljs-comment">;; 2</span><br>(<span class="hljs-name"><span class="hljs-built_in">car</span></span> (<span class="hljs-name"><span class="hljs-built_in">cdr</span></span> (<span class="hljs-name"><span class="hljs-built_in">cdr</span></span> items))) <span class="hljs-comment">;; 3</span><br>(<span class="hljs-name"><span class="hljs-built_in">cdr</span></span> (<span class="hljs-name"><span class="hljs-built_in">cdr</span></span> (<span class="hljs-name"><span class="hljs-built_in">cdr</span></span> items))) <span class="hljs-comment">;; '(4)</span><br>(<span class="hljs-name"><span class="hljs-built_in">cdr</span></span> (<span class="hljs-name"><span class="hljs-built_in">cdr</span></span> (<span class="hljs-name"><span class="hljs-built_in">cdr</span></span> (<span class="hljs-name"><span class="hljs-built_in">cdr</span></span> items)))) <span class="hljs-comment">;; '()</span><br></code></pre>
<h3 id="quote">5.2 Quote</h3>
<p>你可能会好奇 <code>'()</code> 和 <code>'(2 3 4)</code> 中的单引号
<code>'</code> 是什么意思。回想一下第 1 节，S
表达式可以是原子表达式或<em>列表</em>。是的，这里的说的<em>列表</em>与
<code>list</code> 函数创建的列表是一个东西。也就是说，S 表达式</p>
<pre><code class="hljs scheme">(<span class="hljs-name">1</span> <span class="hljs-number">2</span> <span class="hljs-number">3</span> <span class="hljs-number">4</span>)<br></code></pre>
<p>本身就是一个列表。但是这个表达式会被 Scheme 解释成调用函数
<code>1</code>，传入参数 2, 3, 4。为了表示列表本身，我们用
<code>quote</code> 特殊形。<code>quote</code> 接受一个 S
表达式作为参数，不对这个表达式求值，而是直接返回它。下面是一些使用例子。</p>
<pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">quote</span></span> (<span class="hljs-name">1</span> <span class="hljs-number">2</span> <span class="hljs-number">3</span> <span class="hljs-number">4</span>)) <span class="hljs-comment">;; 等价于 (list 1 2 3 4)</span><br>(<span class="hljs-name"><span class="hljs-built_in">quote</span></span> a) <span class="hljs-comment">;; 不认为 a 是一个变量，而是直接返回符号 a 本身</span><br>(<span class="hljs-name"><span class="hljs-built_in">quote</span></span> (<span class="hljs-name"><span class="hljs-built_in">+</span></span> a b)) <span class="hljs-comment">;; 返回一个长度为 3 的列表，三个元素分别是符号 +, 符号 a, 符号 b</span><br>(<span class="hljs-name"><span class="hljs-built_in">quote</span></span> (<span class="hljs-name"><span class="hljs-built_in">lambda</span></span> (x) (<span class="hljs-name"><span class="hljs-built_in">*</span></span> x x))) <span class="hljs-comment">;; 任何代码都可以放到 quote 里</span><br></code></pre>
<p>由于 quote 十分常用，因此我们有一种简化形式。在任意 S
表达式前加上单引号 <code>'</code> 表示对这个 S 表达式 quote。</p>
<pre><code class="hljs scheme"><span class="hljs-symbol">'a</span> <span class="hljs-comment">;; 等价于 (quote a)</span><br>'() <span class="hljs-comment">;; 等价于 (quote ())，表示一个空列表，也称为 NIL</span><br>'(<span class="hljs-number">1</span> <span class="hljs-number">2</span> <span class="hljs-number">3</span> <span class="hljs-number">4</span>) <span class="hljs-comment">;; 等价于 (list 1 2 3 4)</span><br>'(lambda (x) (* x x)) <span class="hljs-comment">;; 任何代码前面都可以加上单引号，表示代码本身</span><br></code></pre>
<p>这有这非常重要的意义——意味着代码可以当作数据解析。这是其它非 Lisp
系语言不具备的能力。我们会在下一篇文章中大量使用它，这里我们先看一些简单的例子：</p>
<pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> code '(lambda (x) (* x x)))<br><br>(<span class="hljs-name"><span class="hljs-built_in">car</span></span> code) <span class="hljs-comment">;; 'lambda</span><br>(<span class="hljs-name"><span class="hljs-built_in">cadr</span></span> code) <span class="hljs-comment">;; '(x)</span><br>(<span class="hljs-name">caddr</span> code) <span class="hljs-comment">;; '(* x x)</span><br></code></pre>
<p>这里的 <code>cadr</code> 和 <code>caddr</code>
是快捷函数。<code>(cadr x)</code> 等价于
<code>(car (cdr x))</code>，<code>(caddr x)</code> 等价于
<code>(car (cdr (cdr x)))</code>。这种命名也很容易记忆：中间的
<code>a</code> 和 <code>d</code> 分布表示依次调用 <code>car</code> 和
<code>cdr</code>。</p>
<p>我们知道列表由有序对构成。S
表达式使用括号表示列表，那么对于有序对这种更基础的元素，它如何表示呢？我们可以试验下：</p>
<pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">cons</span></span> <span class="hljs-number">1</span> <span class="hljs-number">2</span>) <span class="hljs-comment">;; '(1 . 2)</span><br></code></pre>
<p>如果括号里的两个元素用 <code>.</code>
隔开，则表示这是一个有序对。但如果有序对的第二个元素被括号包裹，则会省略掉
<code>.</code> 和第二个元素的括号：</p>
<pre><code class="hljs scheme">'(<span class="hljs-number">1</span> . (<span class="hljs-number">2</span> . <span class="hljs-number">3</span>)) <span class="hljs-comment">;; '(1 2 . 3)</span><br>'((<span class="hljs-number">1</span> . <span class="hljs-number">2</span>) . (<span class="hljs-number">3</span> . <span class="hljs-number">4</span>)) <span class="hljs-comment">;; '((1 . 2) 3 . 4)</span><br>'(<span class="hljs-number">1</span> . (<span class="hljs-number">2</span> . (<span class="hljs-number">3</span> . (<span class="hljs-number">4</span> . ())))) <span class="hljs-comment">;; '(1 2 3 4)</span><br></code></pre>
<p>因此 <code>(cons 1 (cons 2 (cons 3 (cons 4 '()))))</code> 的结果是
<code>'(1 2 3 4)</code>，看上去像是个列表了。这种语法的好处是，既能体现列表是由有序对构成的（可以显式写成
<code>(+ . (2 . (3 . ())))</code>），又能让列表看上去很舒服（一般写作
<code>(+ 2 3)</code>）。</p>
<h3 id="quasiquote-与-unquote">5.3 Quasiquote 与 unquote</h3>
<p>Scheme
还提供了一对方便我们构造特定列表的特殊形：<code>quasiquote</code> 与
<code>unquote</code>。它们同样接受一个 S
表达式作为参数。<code>(quasiquote exp)</code> 可简写为
<code>`exp</code>，<code>(unquote exp)</code> 可简写为
<code>,exp</code>。与 <code>quote</code> 类似，<code>quasiquote</code>
也原样返回 S 表达式，但会对其中 <code>unquote</code> 的部分求值。</p>
<pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> a <span class="hljs-number">10</span>)<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> b <span class="hljs-number">20</span>)<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> c '(x y))<br><br>`(<span class="hljs-number">1</span> <span class="hljs-number">2</span> ,a ,b) <span class="hljs-comment">;; '(1 2 10 20)</span><br>`(,a . ,b) <span class="hljs-comment">;; '(10 . 20)</span><br>`(<span class="hljs-number">1</span> ,c <span class="hljs-number">2</span> <span class="hljs-number">3</span>) <span class="hljs-comment">;; '(1 (x y) 2 3)</span><br>`(<span class="hljs-number">1</span> ,(* a b) <span class="hljs-number">2</span>) <span class="hljs-comment">;; '(1 200 2)</span><br></code></pre>
<p>还有一个类似的语法是
<code>unquote-splicing</code>，接受一个列表作为参数，<code>(unquote-splicing list)</code>
简写为 <code>,@list</code>。它会对列表求值并展开：</p>
<pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> items (<span class="hljs-name"><span class="hljs-built_in">list</span></span> <span class="hljs-number">10</span> <span class="hljs-number">20</span> <span class="hljs-number">30</span>))<br><br>`(<span class="hljs-number">1</span> <span class="hljs-number">2</span> ,@items <span class="hljs-number">3</span>) <span class="hljs-comment">;; '(1 2 10 20 30 3)</span><br>`(,@(list <span class="hljs-number">1</span> <span class="hljs-number">2</span>) <span class="hljs-number">3</span> <span class="hljs-number">4</span>) <span class="hljs-comment">;; '(1 2 3 4)</span><br>`(,@'(<span class="hljs-number">1</span>) <span class="hljs-number">2</span> <span class="hljs-number">3</span>) <span class="hljs-comment">;; '(1 2 3)</span><br></code></pre>
<h3 id="常用函数">5.4 常用函数</h3>
<p>这里介绍一些操作列表的常用函数。</p>
<p><code>pair?</code> 判断是否是有序对</p>
<pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">pair?</span></span> <span class="hljs-number">1</span>) <span class="hljs-comment">;; #f</span><br>(<span class="hljs-name"><span class="hljs-built_in">pair?</span></span> '()) <span class="hljs-comment">;; #f，NIL 不是有序对</span><br>(<span class="hljs-name"><span class="hljs-built_in">pair?</span></span> (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> <span class="hljs-number">1</span> <span class="hljs-number">2</span>)) <span class="hljs-comment">;; #t</span><br>(<span class="hljs-name"><span class="hljs-built_in">pair?</span></span> (<span class="hljs-name"><span class="hljs-built_in">list</span></span> <span class="hljs-number">1</span> <span class="hljs-number">2</span> <span class="hljs-number">3</span>)) <span class="hljs-comment">;; #t，列表也是有序对组成的</span><br></code></pre>
<p><code>list?</code> 判断是否是列表</p>
<pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">list?</span></span> '(<span class="hljs-number">1</span> <span class="hljs-number">2</span> <span class="hljs-number">3</span>)) <span class="hljs-comment">;; #t</span><br>(<span class="hljs-name"><span class="hljs-built_in">list?</span></span> '()) <span class="hljs-comment">;; #t，NIL 就是空列表</span><br>(<span class="hljs-name"><span class="hljs-built_in">list?</span></span> '(<span class="hljs-number">1</span> <span class="hljs-number">2</span> . <span class="hljs-number">3</span>)) <span class="hljs-comment">;; #f，不以 NIL 结尾，不是列表</span><br></code></pre>
<p><code>symbol?</code> 判断是否是符号</p>
<pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">symbol?</span></span> <span class="hljs-number">12</span>) <span class="hljs-comment">;; #f，数字不是符号</span><br>(<span class="hljs-name"><span class="hljs-built_in">symbol?</span></span> <span class="hljs-symbol">'abc</span>) <span class="hljs-comment">;; #t</span><br>(<span class="hljs-name"><span class="hljs-built_in">symbol?</span></span> (<span class="hljs-name"><span class="hljs-built_in">quote</span></span> abc)) <span class="hljs-comment">;; #t</span><br>(<span class="hljs-name"><span class="hljs-built_in">symbol?</span></span> pi) <span class="hljs-comment">;; #f，pi 的值是 3.14159，不是符号</span><br>(<span class="hljs-name"><span class="hljs-built_in">symbol?</span></span> <span class="hljs-symbol">'pi</span>) <span class="hljs-comment">;; #t</span><br>(<span class="hljs-name"><span class="hljs-built_in">symbol?</span></span> '(<span class="hljs-number">1</span> <span class="hljs-number">2</span> <span class="hljs-number">3</span>)) <span class="hljs-comment">;; #f，列表不是符号</span><br></code></pre>
<p><code>null?</code> 判断列表是否为空。</p>
<pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">null?</span></span> '()) <span class="hljs-comment">;; #t</span><br>(<span class="hljs-name"><span class="hljs-built_in">null?</span></span> (<span class="hljs-name"><span class="hljs-built_in">list</span></span>)) <span class="hljs-comment">;; #t</span><br>(<span class="hljs-name"><span class="hljs-built_in">null?</span></span> (<span class="hljs-name"><span class="hljs-built_in">list</span></span> <span class="hljs-number">1</span> <span class="hljs-number">2</span>)) <span class="hljs-comment">;; #f</span><br>(<span class="hljs-name"><span class="hljs-built_in">null?</span></span> (<span class="hljs-name">cddr</span> '(<span class="hljs-number">1</span> <span class="hljs-number">2</span>))) <span class="hljs-comment">;; #t</span><br></code></pre>
<p><code>memq</code> 在列表中找到 car 等于给定值的有序对</p>
<pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">memq</span></span> <span class="hljs-number">2</span> '(<span class="hljs-number">1</span> <span class="hljs-number">2</span> <span class="hljs-number">3</span>)) <span class="hljs-comment">;; '(2 3)</span><br>(<span class="hljs-name"><span class="hljs-built_in">memq</span></span> <span class="hljs-number">3</span> '(<span class="hljs-number">1</span> <span class="hljs-number">2</span> <span class="hljs-number">3</span>)) <span class="hljs-comment">;; '(3)</span><br>(<span class="hljs-name"><span class="hljs-built_in">memq</span></span> <span class="hljs-number">4</span> '(<span class="hljs-number">1</span> <span class="hljs-number">2</span> <span class="hljs-number">3</span>)) <span class="hljs-comment">;; #f，返回 false 表示不存在</span><br></code></pre>
<p><code>assoc</code> 假设列表的元素都是有序对，找到有序对的 car
等于给定值的元素</p>
<pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">assoc</span></span> <span class="hljs-number">2</span> '((<span class="hljs-number">1</span> a) (<span class="hljs-number">2</span> b) (<span class="hljs-number">3</span> c))) <span class="hljs-comment">;; '(2 b)</span><br>(<span class="hljs-name"><span class="hljs-built_in">assoc</span></span> <span class="hljs-number">3</span> '((<span class="hljs-number">1</span> . a) (<span class="hljs-number">2</span> . b) (<span class="hljs-number">3</span> . c))) <span class="hljs-comment">;; '(3 . c)</span><br>(<span class="hljs-name"><span class="hljs-built_in">assoc</span></span> <span class="hljs-number">4</span> '((<span class="hljs-number">1</span> a) (<span class="hljs-number">2</span> b) (<span class="hljs-number">3</span> c))) <span class="hljs-comment">;; #f</span><br></code></pre>
<p><code>append</code> 连接两个列表</p>
<pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">append</span></span> '(<span class="hljs-number">1</span> <span class="hljs-number">2</span> <span class="hljs-number">3</span>) '(<span class="hljs-number">4</span> <span class="hljs-number">5</span>)) <span class="hljs-comment">;; '(1 2 3 4 5)</span><br>(<span class="hljs-name"><span class="hljs-built_in">append</span></span> '(<span class="hljs-number">1</span> <span class="hljs-number">2</span> <span class="hljs-number">3</span>) '()) <span class="hljs-comment">;; '(1 2 3)</span><br></code></pre>
<h2 id="函数式编程">6 函数式编程</h2>
<p>Scheme
倡导函数式编程，除了函数是一等公民外，还有一点就是“非必要不赋值”。到现在为止，我们还没有介绍赋值语句。对于命令式编程来说，不使用赋值语句连个有限
while 循环都写不出来。但是在函数式编程中，我们会熟练使用各种递归。</p>
<pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">sum</span> items)<br>  (<span class="hljs-name"><span class="hljs-built_in">if</span></span> (<span class="hljs-name"><span class="hljs-built_in">null?</span></span> items)<br>      <span class="hljs-number">0</span><br>      (<span class="hljs-name"><span class="hljs-built_in">+</span></span> (<span class="hljs-name"><span class="hljs-built_in">car</span></span> items)<br>         (<span class="hljs-name">sum</span> (<span class="hljs-name"><span class="hljs-built_in">cdr</span></span> items)))))<br><br>(<span class="hljs-name">sum</span> '(<span class="hljs-number">1</span> <span class="hljs-number">2</span> <span class="hljs-number">3</span> <span class="hljs-number">4</span>)) <span class="hljs-comment">;; 10</span><br></code></pre>
<p>虽然无法通过赋值改变变量，但是我们在可以调用函数时改变参数的值。有人可能会说递归性能差，因为需要消耗栈空间。确实，上面的代码在调用
<code>(sum (cdr items))</code> 之前需要将 <code>(car items)</code>
的值压栈，以便 <code>sum</code>
返回后计算两者之和。但是我们只需要稍微修改一下写法：</p>
<pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">sum</span> items)<br>  (<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">iter</span> i s)<br>    (<span class="hljs-name"><span class="hljs-built_in">if</span></span> (<span class="hljs-name"><span class="hljs-built_in">null?</span></span> i)<br>        s<br>        (<span class="hljs-name">iter</span> (<span class="hljs-name"><span class="hljs-built_in">cdr</span></span> i) (<span class="hljs-name"><span class="hljs-built_in">+</span></span> s (<span class="hljs-name"><span class="hljs-built_in">car</span></span> i)))))<br>  (<span class="hljs-name">iter</span> items <span class="hljs-number">0</span>))<br></code></pre>
<p>我们发现递归调用 <code>(iter (cdr i) (+ s (car i)))</code>
的返回值直接作为原函数 <code>(iter i s)</code>
的返回值，因此调用之前不需要压栈。这被称为尾递归。尾递归本质就是迭代，因为递归调用
<code>iter</code> 的过程就是不断迭代更新变量 <code>i</code> 和
<code>s</code> 的过程。</p>
<h3 id="accumulate">6.1 Accumulate</h3>
<p>刚才我们定义了一个函数求所有元素之和。那么如果要求所有元素之积呢？我们可以定义一个
<code>product</code> 函数</p>
<pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">product</span> items)<br>  (<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">iter</span> i p)<br>    (<span class="hljs-name"><span class="hljs-built_in">if</span></span> (<span class="hljs-name"><span class="hljs-built_in">null?</span></span> i)<br>        p<br>        (<span class="hljs-name">iter</span> (<span class="hljs-name"><span class="hljs-built_in">cdr</span></span> i) (<span class="hljs-name"><span class="hljs-built_in">*</span></span> p (<span class="hljs-name"><span class="hljs-built_in">car</span></span> i)))))<br>  (<span class="hljs-name">iter</span> items <span class="hljs-number">1</span>))<br></code></pre>
<p>我们发现这个函数跟 <code>sum</code>
几乎一样。这两个函数都是给定一个<em>初始值</em>，依次与列表中的元素执行某个<em>操作</em>，然后依次迭代；只是初始值（一个是
0 另一个是 1）和操作（一个是 <code>+</code> 另一个是
<code>*</code>）不同。在 Scheme 中，函数可以当作值传递，而
<code>+</code> 和 <code>*</code>
都是函数。因此我们可以定义一个通用的函数，将初始值和操作作为参数传递进去：</p>
<pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">accumulate</span> op init items)<br>  (<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">iter</span> i res)<br>    (<span class="hljs-name"><span class="hljs-built_in">if</span></span> (<span class="hljs-name"><span class="hljs-built_in">null?</span></span> i)<br>        res<br>        (<span class="hljs-name">iter</span> (<span class="hljs-name"><span class="hljs-built_in">cdr</span></span> i) (<span class="hljs-name">op</span> res (<span class="hljs-name"><span class="hljs-built_in">car</span></span> i)))))<br>  (<span class="hljs-name">iter</span> items init))<br><br>(<span class="hljs-name">accumulate</span> + <span class="hljs-number">0</span> '(<span class="hljs-number">1</span> <span class="hljs-number">2</span> <span class="hljs-number">3</span> <span class="hljs-number">4</span>)) <span class="hljs-comment">;; 10</span><br>(<span class="hljs-name">accumulate</span> * <span class="hljs-number">1</span> '(<span class="hljs-number">1</span> <span class="hljs-number">2</span> <span class="hljs-number">3</span> <span class="hljs-number">4</span>)) <span class="hljs-comment">;; 24</span><br>(<span class="hljs-name">accumulate</span> append '() '((<span class="hljs-number">1</span> <span class="hljs-number">2</span>) (<span class="hljs-number">3</span> <span class="hljs-number">4</span> <span class="hljs-number">5</span>) (a b c d))) <span class="hljs-comment">;; '(1 2 3 4 5 a b c d)</span><br></code></pre>
<h3 id="map">6.2 Map</h3>
<p>与之类似的函数是 <code>map</code>。<code>map</code>
将列表中的每个元素通过一个给定的函数映射成新值</p>
<pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">map</span></span> - '(<span class="hljs-number">1</span> <span class="hljs-number">2</span> <span class="hljs-number">3</span> <span class="hljs-number">4</span> <span class="hljs-number">5</span>)) <span class="hljs-comment">;; '(-1 -2 -3 -4 -5)</span><br>(<span class="hljs-name"><span class="hljs-built_in">map</span></span> (<span class="hljs-name"><span class="hljs-built_in">lambda</span></span> (x) (<span class="hljs-name"><span class="hljs-built_in">*</span></span> x x)) '(<span class="hljs-number">1</span> <span class="hljs-number">2</span> <span class="hljs-number">3</span> <span class="hljs-number">4</span> <span class="hljs-number">5</span>)) <span class="hljs-comment">;; '(1 4 9 16 25)</span><br></code></pre>
<p><code>map</code> 还支持传多个列表，如
<code>(map proc list1 list2 ...)</code>。这些列表的长度要相等，并且列表的数量等于传入函数的参数数量。<code>list1</code>
的元素作为第一个参数传给 <code>proc</code>，<code>list2</code>
的元素作为第二个元素传给 <code>proc</code>，以此类推。</p>
<pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">map</span></span> + '(<span class="hljs-number">1</span> <span class="hljs-number">2</span> <span class="hljs-number">3</span>) '(<span class="hljs-number">10</span> <span class="hljs-number">20</span> <span class="hljs-number">30</span>)) <span class="hljs-comment">;; '(11 22 33)</span><br>(<span class="hljs-name"><span class="hljs-built_in">map</span></span> list '(<span class="hljs-number">1</span> <span class="hljs-number">2</span> <span class="hljs-number">3</span>) '(a b c) '(x y z)) <span class="hljs-comment">;; '((1 a x) (2 b y) (3 c z))</span><br></code></pre>
<p>如何实现 <code>map</code> 呢？Scheme
支持定义可变参数的函数。我们可以定义
<code>(define (map proc . lists))</code>，这种情况下 <code>lists</code>
便是一个包含剩余参数的列表。因为 <code>(map proc list1 list2)</code>
也可以写作 <code>(map proc . (list1 list2))</code>（见 5.2
节），因此不难理解这种写法。</p>
<p>反过来如果有 n 个参数存储在一个列表中，可以用 <code>apply</code>
将它们传给一个指定函数：</p>
<pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">apply</span></span> + '(<span class="hljs-number">1</span> <span class="hljs-number">2</span>)) <span class="hljs-comment">;; 3</span><br>(<span class="hljs-name"><span class="hljs-built_in">apply</span></span> * '(<span class="hljs-number">2</span> <span class="hljs-number">3</span> <span class="hljs-number">4</span>)) <span class="hljs-comment">;; 24</span><br></code></pre>
<p>这样我们可以实现 <code>map</code> 函数：</p>
<pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name"><span class="hljs-built_in">map</span></span> proc . lists)<br>  (<span class="hljs-name"><span class="hljs-built_in">if</span></span> (<span class="hljs-name"><span class="hljs-built_in">null?</span></span> (<span class="hljs-name"><span class="hljs-built_in">car</span></span> lists))<br>      '()<br>      (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> (<span class="hljs-name"><span class="hljs-built_in">apply</span></span> proc (<span class="hljs-name"><span class="hljs-built_in">map</span></span> car lists))<br>            (<span class="hljs-name"><span class="hljs-built_in">apply</span></span> map (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> proc (<span class="hljs-name"><span class="hljs-built_in">map</span></span> cdr lists))))))<br></code></pre>
<h3 id="filter">Filter</h3>
<p>从列表中过滤出符合要求的函数，可以用
<code>filter</code>。它接受一个返回布尔值的函数和一个列表作为参数，例如</p>
<pre><code class="hljs scheme">(<span class="hljs-name">filter</span> odd? '(<span class="hljs-number">1</span> <span class="hljs-number">2</span> <span class="hljs-number">3</span> <span class="hljs-number">4</span> <span class="hljs-number">5</span> <span class="hljs-number">6</span>)) <span class="hljs-comment">;; '(1 3 5)</span><br>(<span class="hljs-name">filter</span> even? '(<span class="hljs-number">1</span> <span class="hljs-number">2</span> <span class="hljs-number">3</span> <span class="hljs-number">4</span> <span class="hljs-number">5</span> <span class="hljs-number">6</span>)) <span class="hljs-comment">;; '(2 4 6)</span><br></code></pre>
<p>我们同样可以实现 <code>filter</code>：</p>
<pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">filter</span> proc items)<br>  (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name"><span class="hljs-built_in">null?</span></span> items) '()]<br>        [(<span class="hljs-name">proc</span> (<span class="hljs-name"><span class="hljs-built_in">car</span></span> items))<br>         (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> (<span class="hljs-name"><span class="hljs-built_in">car</span></span> items) (<span class="hljs-name">filter</span> proc (<span class="hljs-name"><span class="hljs-built_in">cdr</span></span> items)))]<br>        [<span class="hljs-name"><span class="hljs-built_in">else</span></span><br>         (<span class="hljs-name">filter</span> proc (<span class="hljs-name"><span class="hljs-built_in">cdr</span></span> items))]))<br></code></pre>
<blockquote>
<p>思考题：你能把 <code>map</code> 和 <code>filter</code>
改成迭代（尾递归）的形式吗？</p>
</blockquote>
<h2 id="赋值">7 赋值</h2>
<p>虽然函数式编程不鼓励使用赋值，但是很多场景完全不使用赋值会非常不方便，并且有些场景适当地使用赋值可以提升代码性能，简化一些实现。Scheme
使用 <code>set!</code> 特殊形执行赋值，使用格式是
<code>(set! var val)</code>。<code>set!</code> 先对 <code>val</code>
表达式求值，然后将值赋值给 <code>var</code>。例如：</p>
<pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> a <span class="hljs-number">1</span>)<br>(<span class="hljs-name"><span class="hljs-built_in">set!</span></span> a (<span class="hljs-name"><span class="hljs-built_in">+</span></span> a <span class="hljs-number">1</span>)) <span class="hljs-comment">;; a = 2</span><br>(<span class="hljs-name"><span class="hljs-built_in">set!</span></span> a (<span class="hljs-name"><span class="hljs-built_in">*</span></span> a <span class="hljs-number">2</span>)) <span class="hljs-comment">;; a = 4</span><br>(<span class="hljs-name"><span class="hljs-built_in">set!</span></span> a (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> a (<span class="hljs-name"><span class="hljs-built_in">+</span></span> a <span class="hljs-number">1</span>))) <span class="hljs-comment">;; a = '(4 . 5)</span><br></code></pre>
<p>引入赋值会给系统增加很多不确定性。对于不使用赋值的函数，传入确定的参数必然得到确定的值，就像数学函数一样。而一旦引入赋值，就不一定了。可以看下面的例子：</p>
<pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">make-account</span> n)<br>  (<span class="hljs-name"><span class="hljs-built_in">lambda</span></span> (d)<br>    (<span class="hljs-name"><span class="hljs-built_in">set!</span></span> n (<span class="hljs-name"><span class="hljs-built_in">+</span></span> n d))<br>    n))<br><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> account (<span class="hljs-name">make-account</span> <span class="hljs-number">0</span>))<br><br>(<span class="hljs-name">account</span> <span class="hljs-number">10</span>) <span class="hljs-comment">;; 10</span><br>(<span class="hljs-name">account</span> <span class="hljs-number">10</span>) <span class="hljs-comment">;; 20</span><br>(<span class="hljs-name">account</span> <span class="hljs-number">-5</span>) <span class="hljs-comment">;; 15</span><br></code></pre>
<p>这里 <code>(account 10)</code>
调用了两次，传入相同的参数但是返回不同的值。4.1
节提到，当我们把函数当作值传递时，它所在的环境也会随之传递。因此我们可以把函数当作数据结构使用。上面的
<code>account</code> 是一个函数，也可以认为是一个数据。</p>
<p>Racket
中的有序对一旦构造好就不能修改。我们可以利用函数实现一个可修改的有序对：</p>
<pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">mcons</span> <span class="hljs-number">1</span>st <span class="hljs-number">2</span>nd)<br>  (<span class="hljs-name"><span class="hljs-built_in">let</span></span> ([<span class="hljs-name">set-mcar!</span> (<span class="hljs-name"><span class="hljs-built_in">lambda</span></span> (v) (<span class="hljs-name"><span class="hljs-built_in">set!</span></span> <span class="hljs-number">1</span>st v))]<br>        [<span class="hljs-name">set-mcdr!</span> (<span class="hljs-name"><span class="hljs-built_in">lambda</span></span> (v) (<span class="hljs-name"><span class="hljs-built_in">set!</span></span> <span class="hljs-number">2</span>nd v))])<br>    (<span class="hljs-name"><span class="hljs-built_in">lambda</span></span> (m)<br>      (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name"><span class="hljs-built_in">eq?</span></span> m <span class="hljs-symbol">'mcar</span>) <span class="hljs-number">1</span>st]<br>            [(<span class="hljs-name"><span class="hljs-built_in">eq?</span></span> m <span class="hljs-symbol">'mcdr</span>) <span class="hljs-number">2</span>nd]<br>            [(<span class="hljs-name"><span class="hljs-built_in">eq?</span></span> m <span class="hljs-symbol">'set-mcar!</span>) set-mcar!]<br>            [(<span class="hljs-name"><span class="hljs-built_in">eq?</span></span> m <span class="hljs-symbol">'set-mcdr!</span>) set-mcdr!]))))<br><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">mcar</span> mpair) (<span class="hljs-name">mpair</span> <span class="hljs-symbol">'mcar</span>))<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">mcdr</span> mpair) (<span class="hljs-name">mpair</span> <span class="hljs-symbol">'mcdr</span>))<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">set-mcar!</span> mpair val) ((<span class="hljs-name">mpair</span> <span class="hljs-symbol">'set-mcar!</span>) val))<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">set-mcdr!</span> mpair val) ((<span class="hljs-name">mpair</span> <span class="hljs-symbol">'set-mcdr!</span>) val))<br></code></pre>
<p>上面的例子可以认为是 Scheme 中的“面向对象编程”。<code>mcons</code>
返回的函数可视为对象，它通过传入的参数决定执行的操作，因此这种写法又被称为消息传递模式
(massage passing style)。<code>mcons</code> 可以称为构造器
(constructor)，调用构造器的返回值获取“成员”，<code>(mpair 'mcar)</code>
这样的表达式就类似于 Java 中的 <code>mpair.mcar</code>。下面的代码展示了
<code>mcons</code> 的一些用法。</p>
<pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> p (<span class="hljs-name">mcons</span> <span class="hljs-number">1</span> <span class="hljs-number">2</span>))<br>(<span class="hljs-name">mcar</span> p) <span class="hljs-comment">;; 1</span><br>(<span class="hljs-name">mcdr</span> p) <span class="hljs-comment">;; 2</span><br>(<span class="hljs-name">set-mcar!</span> p <span class="hljs-number">10</span>)<br>(<span class="hljs-name">set-mcdr!</span> p <span class="hljs-number">20</span>)<br>(<span class="hljs-name">mcar</span> p) <span class="hljs-comment">;; 10</span><br>(<span class="hljs-name">mcdr</span> p) <span class="hljs-comment">;; 20</span><br></code></pre>
<h2 id="总结">8 总结</h2>
<p>这篇文章介绍 Scheme 的一些基础内容，包括 S
表达式的构造、常用特殊形的用法、函数的调用与定义、对环境的理解、有序对与列表、常用函数的使用、赋值操作等内容。这些内容足以写出很多
Scheme 程序了。下一篇文章我们将用 Scheme 实现一个 Scheme
的解释器，实现本文介绍的绝大部分语言特性。</p>
<hr>
<p><strong>参考资料：</strong></p>
<ul>
<li>[1] <em>Structure and Interpretation of Computer Programs</em>,
Harold Abelson, The MIT Press</li>
<li>[2] <em>The Little Schemer</em>, Daniel P. Friedman, The MIT
Press</li>
</ul>
]]></content>
    
    
    <summary type="html">我最近在读 SICP (Structure and Interpretation of Computer
Programs)，中文译名是《计算机程序的构造与解释》，感觉受益匪浅。我打算开个坑，总结分享一些我学到的内容。SICP
综合性非常强，内容包括函数式编程、数据结构的分层与抽象、面向对象、无限流</summary>
    
    
    
    
    <category term="featured" scheme="https://luyuhuang.tech/tags/featured/"/>
    
    <category term="comupter-science" scheme="https://luyuhuang.tech/tags/comupter-science/"/>
    
  </entry>
  
</feed>
