如何刻意练习 – 中秋读书随笔

中秋假期在家中收拾之前没有看完的书籍,翻到万维钢老师写的《学习究竟是什么》,其中第一章就写到刻意练习的内容,读起来非常深刻,而且通过有理有据的论述,让我对刻意练习有了一个系统的了解,因此也写次小文作为一个总结,也希望分享给大家,试更多的人受益。

刻意练习和一万小时定律

在很多个文章中都提到的一万小时定律,说的是为了成为某个领域的专家需要一万小时的练习,这一点深入人心。但是,我们需要关注的是怎么去使用这个一万小时,如果仅仅就是漫无目的的练习一万小时,那是绝对的浪费时间。因此,需要我们进行有针对性的刻意练习,通过有目的的练习和科学的方法才能真正的达到从底层到高层的升级。

只在学习区练习

我们把需要练习的内容分为舒适区、学习区和恐慌区。舒适区是我们非常擅长的部分,而恐慌区就是我们打不到的区域,在这两个区学习毫无用处,不回对技能的提升有什么建设的意见。而我们应该在学习区进行学习,而且在学会了一个内容之后,迅速跳跃到下一个学习区,因为之前的学习区已经变成了舒适区了。

掌握套路

刻意练习的主要工作就是做基础训练。而基础的学习就是为了掌握所谓的套路,这个很容易理解,我们从小到大做的很多练习题做的很多实验,都是为了掌握基本的套路,而当这些套路成为我们的一部分时,就会为更高的学习阶段打下基础。掌握套路的基本方法就是进行大量的重复训练,并且训练必须就有高度的针对性。

随时获得反馈

从刻意练习的角度,如果我们通过自我的不断重复的学习,应该能够有很快的提高,但是为什么这么多年来,好像自学的成长确实很慢。这里面主要的原因就处在反馈机制上。为了能有提高一定要有反馈机制,而且老师的作用就在督促学生进行学习,其次才是传授知识,因为在当今社会获取知识的是非常容易的,但是获得反馈确实很难,这也是学习了有没有效果的关键因素所在吧。

刻意练习并不好玩

为了对某个技能进行提升,在刻意练习的过程中,是对人性的一种反作用力。我们必须找一个安静的环境好好的学习,并且需要搭上大量的时间和精力才能起作用。学习一个知识的最好办法是找到该领域的著作然后找个安静的地方反复的读,并且自己做好笔记做一些习题,这样可能会有很好的效果,这个过程需要有坚强的意志力支撑。

为了能够完成目标,在很长的时间内,必须合理的利用好时间,如果决定太累,还是做好一个奖励的机制,帮我我们前行。在前行的过程中不断的获得反馈,以防止我们走太多的弯路。这样才能真正的实现刻意练习的目的。

元宇宙-原来不简单

引言

最近工作比较忙碌,重新开始了数字孪生项目,很多年前数字孪生作为一个新名词出现在工业互联领域,经过几年的发展,很多二维的展示界面被三维的界面所代替,很多现代化的工作场景也都进行了虚拟的建模。在工业互联网领域,3D 的模型已经成为了很多项目追求的方向。在生活领域,元宇宙这个名词也走入人们的视野。在所有的场景中,首先要搭建一个虚拟的世界,从一个工程师的角度,构建数字孪生的工具链是开启项目的第一步,因此近期与团队重新评估目前的工具链,搭建数字虚拟世界。本文主要通过简单的例子去全面了解这个过程,给有相关需求的小伙伴一个参考。

元宇宙、数组孪生是个有前途的领域

本篇文章主要通过建立一个 3D 模型,然后在网页中做出呈现。这是最为简单的一个流程验证过程。

3D 模型的构建

在模型构建的环节,市面上有大量的工具可以完成相关工作。如果模型不是很复杂,可以试用 iPad 构建。我这次用的是 iPad Pro 中 Shapr3D,这个工具非常容易上手,配合 Apple Pencil 可以非常方便的构建基础的模型。我这里做一个简单的模型,然后将其输出成一个 stl 格式的模型,用来后期进行渲染和处理。模型的效果图如下,这里仅作一个例子,真实的项目比这复杂的多。

对于大型的项目可以试用 solidworks 进行相关的工作,想成为专业人士还得深入学习。

这个部分输出一个 STL 格式的模型

通过 Blender 渲染场景

得到一个模型后,可以试用 Blender 进行相关场景的渲染。Blender 是一个开源软件,作为没有那么高要求的使用场景,可以非常方便的进行相关的场景操作。

1、新建一个 Blender 项目,然后清空相关实体。

2、导入之前导出的 STL 格式模型,根据需求进行型相关内容的渲染。

3、将这个场景进行导出。

通过这些操作可以将场景导出 fbx 格式,方便后面的相关程序的构建。

使用 Unity3D 进行编程

在构建 3D 游戏中,使用 Unity3d 引擎能很好的支持相关的功能开发,无论是元宇宙还是数字孪生都可以用这个引擎进行构建,Unity3d 需要有时间进行专门的学习,可以构建元宇宙场景进行相关的交互,使用 C#和 js 进行相关的开发工作,因为这次是验证工具链,主要就是将上一个环节输出的 fbx 格式文件,导入 Unity3D,然后通过设置输出效果将工程输出为 webgl 的平台。

这个环节主要工作是设置 webgl 的参数,在 Unity 的 webgl 设置中需要将压缩格式设置成为 disable,这样才能保证后面输出的内容能够在开发中正常显示。

设置好以后直接进行 build,会生成一个文件夹,内部就是可以运行这个模型的 html 文件。

这个时候,如果直接打开这个 index,会被浏览器拦截,无法查看,解决方案看使用时 Vscode 实时查看。

VS CODE 调试

使用 vs code 打开 Unity 生成的工程项目,可以在 code 中加载 Live server 插件。

在 VS code 中查看 index 文件,然后右键选择 Open With Live Server,就可以在浏览器中查看相关的结果了。

总结

在构建数字孪生的过程中,有很多工具链的选择,这篇文章主要是介绍一个构建三维数字孪生工厂的一种解决方案,由于收到保密项目的设置,简单做了描述,虽然过程比较简单,但是整体的过程大致如此。

希望这个文章对兴趣的读者有所帮助。

攻克编译器

编译器作为软件由代码至可执行文件的中间过程,是软件研发非常重要的技术。在软件开发人员中,仅有极少数的人能够真正的了解相关技术。最近非常偶然看到一本关于编译器技术的数据,非常适合进行相关技术的学习,相关的数据如下:

本系列的文章将对本书进行相关的解读和翻译,对相关的技术在我可以理解的情况下进行说明,对于有兴趣了解编译器的工程师,将会有很大的帮助。

前言

我真的很兴奋我们将一起踏上这段旅程。这是一本关于为编程语言实现解释器的书。这也是一本关于如何设计一种值得实现的语言的书。这是我刚开始接触编程语言时希望拥有的书,也是我近十年来一直在脑海中构思的书。

本书中,我们将逐步介绍两个完整的解释器,以实现功能齐全的编程语言。我假设这是您第一次涉足语言领域,因此我将介绍构建一个完整、可用、快速的语言所需的每个概念和代码。

为了在一本书中塞入两个完整的实现,而且避免这变成一个门槛,本文在理论上比其他文章更轻松。在我们构建系统的每一部分时,我将介绍它背后的历史和概念。我尽力让你熟悉这些术语,这样如果你发现自己参加了一个充满 PL(编程语言)研究人员的鸡尾酒会,也能融入其中。

我们主要会费尽脑汁来让这个语言运转起来。 这并不是说理论不重要。 能够准确而正式地推理语法和语义是研究语言时的一项重要技能。 但是,就个人而言,我通过实践学习效果最好。 我很难通过充满抽象概念的段落真正理解它们。 但是,如果我编写了一些代码、运行并调试代码,那么我就理解了。

这就是我给你设定目标。 我希望你能直观了解到真正的编程语言是如何生活和呼吸的。 我希望,当你以后再读其他理论性更强的书时,这些概念会牢牢地印在你的脑海里,附着在这个有形的基础上。

1.1 为什么要学习这些内容?

每本编译器书籍的介绍似乎都有这一章节。 我不知道为什么编程语言会引起这种观念存在性的怀疑。 我不认为鸟类学书籍会担心证明鸟类的存在。 他们假设读者喜欢鸟类,并开始教授内容。

但是编程语言有点不同。 我认为,对于我们中的任何人来说创建一种广泛成功的通用编程语言的可能性确实很小。 世界上广泛使用的编程语言的设计者一辆大众巴士就能装得下,甚至都不需要加装额外的露营车。 如果加入这个精英群体是学习语言的唯一原因,那将很难证明其合理性。 幸运的是,事实并非如此。

1.1.1 小型语言无处不在

对于每一种成功的通用语言,对应着都有一千种成功的小众语言。 我们曾经称它们为“小众语言”,但术语泛滥的今天产生了“特定领域语言”的名称。 这些是为特定任务量身定制的 混合语言,如应用程序脚本语言、模板引擎、标记格式和配置文件。

几乎每个大型软件项目都需要一些这样的语言。 如果可以,最好重用现有的,而不是实现自己的。 如果你考虑到文档、调试器、编辑器支持、语法高亮和所有其他可能存在的问题,自己实现就变成很艰巨的任务了。

但是,当前没有库符合现有需求时,仍然很有可能会发现自己需要开发解析器或其他工具。 即使你重用了一些现有的实现,最终也不可避免地需要调试和维护它们,并在它们内部进行探索。

1.1.2 语言是很好的练习方式

长跑运动员有时会将重物绑在脚踝上,或在空气稀薄的高海拔地区进行训练。 随后当他们卸下这些负担时,轻巧的四肢和富含氧气的空气带来了相对轻松的舒适度,使他们能够跑得更远更快。

实现一种语言是对编程技能的真正考验。 代码复杂且性能至关重要。 您必须掌握递归、动态数组、树、图形和哈希表。 您可能至少在日常编程中使用哈希表,但您真的了解它们吗? 好吧,等我们从头开始完成自己的作品之后,我保证你会的理解的。

虽然我打算向您展示解释器并不像想象的那样令人生畏,但实现一个解释器仍然是一个挑战。 坚持下去,你会成为一个更强大的程序员,并且在你日常工作中也能更加聪明的使用数据结构和算法。

1.1.3 另一个原因

最后一个原因我很难承认,因为它是我内心的理由。自从我小时候学会编程以来,我就觉得语言有一些神奇的东西。当我第一次编写 BASIC 程序时,我无法想象 BASIC 本身是如何制作出来的。

后来,当我的大学朋友谈论他们的编译器课程时,他们脸上敬畏和恐惧的表情,足以让我相信语言黑客是另一种人,—某种被授予访问通向神秘艺术特权的巫师。

这是一个令人着迷的场景,但它也有阴暗的一面。我不觉得自己像个巫师,所以我觉得我缺乏加入这个团体所需的与生俱来品质。虽然自从我在学校笔记本上涂鸦些关键词后,就对语言着迷,但我花了几十年的时间才鼓起勇气尝试真正学习它们。那种“神奇”的品质,那种排他性的感觉,把我排除在外。

当我终于开始拼凑我自己的小编译器时,我很快就意识到,根本没有魔法。它只是代码,而掌握语言的人也只是人。

有一些技术你在语言之外不经常遇到,有些部分有点困难。但并不比克服的其他障碍更困难。我希望,如果你对编程语言感到恐惧,这本书可以帮助你克服这种恐惧,也许我会让你比以前更勇敢一点。

而且,谁知道呢,也许你会创造下一个伟大的编程语言。毕竟总有人这样做。

1.2 本书的组织方式

本书分为三个部分。你现在正在阅读第一部分。这部的几章可以让你掌握大方向,教你一些语言黑客使用的术语,并向你介绍我们要实现的语言 Lox。

其他两部分中则分别构建了一个完整的 Lox 解释器。在这些部分中,每个章节的结构都相同。每一章选择一个语言功能点,向您介绍其背后的概念,并逐步完成实现。

我花了很多时间试错,设法将两个解释器按章节组织,每一章建立在前面的章节之上,但不需要后面章节的内容。从第一章开始,您将拥有一个可以运行和使用的程序。随着每一章的深入,它的功能越来越丰富,直到你最终拥有一门完整的语言。

使用Gradio与Nginx构建互联网可访问服务

最近在使用Gradio进行一些demo的编写,需要提供一个外网的服务。找了网上很多的内容,发现这篇文章说的很清楚,我也根据这篇文章结合我的实践进行了修改。

文章原文:https://blog.fat.plus/archives/272

0. 前言

Gradio包本身可以设定share=True启动参数,让Gradio官方帮我们创建一个公网链接。
但是这个方法网络很慢,而且链接24小时就生效了。
所以最好还是通过nginx在自己的云服务器上部署。
这是成品:ai.fat.plus

1. Gradio安装和python代码

首先创建venv虚拟环境
python -m venv venv
启动venv环境
source ./venv/bin/activate

pip安装Gradio依赖包
pip install gradio

创建一个demo.py文件,就用官方的演示代码好了:

import gradio as gr

def greet(name):
    return "Hello " + name + "!"

demo = gr.Interface(fn=greet, inputs="text", outputs="text")

demo.launch()

可以先启动py文件尝试一下效果:
python app.py
按默认设定,它会启动一个127.0.0.1:7860的web服务

但目前他只能监听本地端口的,需要通过nginx反向代理到这个端口,以提供公网服务。

2. nginx反向代理配置

如果还没安装nginx的话,建议使用Oneinstack脚本一次性安装LNPM环境。
并可以通过它的vhost.sh脚本创建nginx配置文件,很方便。

nginx的配置文件例子如下,
关键是proxy_pass http://127.0.0.1:17860;那几个location块,
将外网访问ai.fat.plus的请求全部将代理到127.0.0.1:7860。

注:这里我用的网址的是ai.fat.plus,并且配置了ssl证书。
这方面的教程可以参考别的文章……

server {
  listen 80;
  listen [::]:80;
  listen 443 ssl http2;
  listen [::]:443 ssl http2;
  ssl_certificate /usr/local/nginx/conf/ssl/ai.fat.plus.crt;
  ssl_certificate_key /usr/local/nginx/conf/ssl/ai.fat.plus.key;
  ssl_protocols TLSv1.2 TLSv1.3;
  ssl_ecdh_curve X25519:prime256v1:secp384r1:secp521r1;
  ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256;
  ssl_conf_command Ciphersuites TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256;
  ssl_conf_command Options PrioritizeChaCha;
  ssl_prefer_server_ciphers on;
  ssl_session_timeout 10m;
  ssl_session_cache shared:SSL:10m;
  ssl_buffer_size 2k;
  add_header Strict-Transport-Security max-age=15768000;
  ssl_stapling on;
  ssl_stapling_verify on;
  server_name ai.fat.plus;
  access_log /data/wwwlogs/ai.fat.plus_nginx.log combined;
  index index.html index.htm index.php;
  root /data/wwwroot/Gradio;
  if ($ssl_protocol = "") { return 301 https://$host$request_uri; }

   location / {
      proxy_connect_timeout 60;
      proxy_read_timeout 60;
      proxy_send_timeout 60;
      proxy_intercept_errors off;
      proxy_http_version 1.1;
      proxy_set_header        Host            $http_host;
      proxy_set_header        X-Real-IP            $remote_addr;
      proxy_set_header        X-Forwarded-For            $proxy_add_x_forwarded_for;
      proxy_set_header        X-Forwarded-Proto            $scheme;
      proxy_pass http://127.0.0.1:7860;

  }
  #好像Gradio的ws协议是通过/queue路径的,所以这里加上
  location /queue {
      proxy_pass http://127.0.0.1:7860;
      proxy_http_version 1.1;
      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection Upgrade;
  }

  location ~ /(\.user\.ini|\.ht|\.git|\.svn|\.project|LICENSE|README\.md) {
    deny all;
  }
  location /.well-known {
    allow all;
  }

3. 配置systemctl后台服务

为了python demo.py进程在后台持续运行,把它配置到后台服务并开机启动。

先用root权限:
sudo -i
创建配置文件:
vi /etc/systemd/system/gradio.service
配置文件实例如下:

[Unit]
Description=for Gradio
After=network.target

[Service]
#这里填写你安装ubuntu的用户和用户组
User=ubuntu
Group=ubuntu

#这里填写demo.py文件的位置
WorkingDirectory=/data/wwwroot/Gradio
#/data/wwwroot/Gradio/venv/bin/python是venv虚拟环境python的
#/data/wwwroot/Gradio/demo.py是demo文件的位置
ExecStart=/data/wwwroot/Gradio/venv/bin/python /data/wwwroot/Gradio/app.py

[Install]
WantedBy=multi-user.target

完成配置文件后,启动服务:
systemctl start gradio.service
确认是否启动成功:
systemctl status gradio.service

正常的话,把服务设定成开机启动
systemctl enable gradio.service

4.完成

正常的话,打开ai.fat.plus就可以访问啦!

CentOS7 离线安装python3.7.12

最近工作需要离线安装python3,发现这个文章写的不错,转至blog保存。

来源地址:CentOS7 离线安装python3.7.12 – Liao’s blog (laobaiblog.top)

Python3下载地址:官网地址

蓝奏云下载地址:Python-3.7.12及依赖包

一、环境

  1. 系统:CentOS 7
  2. 用户:ROOT
  3. Python版本:python3.7.12

二、安装

  1. 登录到centOS机器,新建文件夹
mkdir -p /usr/local/python3

BashCopy

  1. 将安装包和依赖包里的文件上传至/usr/local/python3
  2. 进入目录
cd /usr/local/python3/

BashCopy

  1. 安装依赖
# 如遇到安装失败,可以添加强制安装参数 --nodeps --force
例: rpm -ivh zlib-devel-1.2.7-17.el7.x86_64.rpm --nodeps --force
#安装依赖包 zlib-devel
rpm -ivh zlib-devel-1.2.7-17.el7.x86_64.rpm
#安装依赖包 bzip2-devel
rpm -ivh bzip2-devel-1.0.6-13.el7.x86_64.rpm
#安装依赖包 epel-release
rpm -ivh epel-release-7-11.noarch.rpm
#安装依赖包 ncurses-devel
rpm -ivh ncurses-devel-5.9-13.20130511.el7.x86_64.rpm
# 安装依赖包 gcc(一定要按照以下的顺序进行安装)
rpm -ivh mpfr-3.1.1-4.el7.x86_64.rpm
rpm -ivh libmpc-1.0.1-3.el7.x86_64.rpm
rpm -ivh kernel-headers-3.10.0-514.el7.x86_64.rpm
rpm -ivh glibc-headers-2.17-157.el7.x86_64.rpm
rpm -ivh glibc-devel-2.17-157.el7.x86_64.rpm
rpm -ivh cpp-4.8.5-11.el7.x86_64.rpm
rpm -ivh gcc-4.8.5-11.el7.x86_64.rpm
# 安装依赖包 gcc-c+
rpm -ivh libstdc++-4.8.5-11.el7.x86_64.rpm
rpm -ivh libstdc++-devel-4.8.5-11.el7.x86_64.rpm
rpm -ivh gcc-c++-4.8.5-11.el7.x86_64.rpm

BashCopy

  1. 进行解压
tar -xzvf Python-3.7.12.tgz

BashCopy

  1. 进入解压路径
cd /usr/local/python3/Python-3.7.12

BashCopy

  1. 设置安装路径
./configure --prefix=/usr/local/python3.7 --enable-shared
make && make install
ln -s /usr/local/python3.7/bin/python3 /usr/bin/python3
echo "/usr/local/python3.7/lib" > /etc/ld.so.conf.d/python3.7.conf
ldconfig

BashCopy

  1. 检查Python3.7.12是否安装成功
python3 -V

BashCopy

显示如下图,说明成功;

  1. 删除安装包以及依赖包
rm -rf  /usr/local/python3

BashCopy

三、安装Pip3

  1. python 3.7.12中自带了pip,所以只需要建立链接就行
ln -s /usr/local/python3.7/bin/pip3.7 /usr/bin/pip3

BashCopy

  1. 检查pip是否安装成功
pip3 -V

BashCopy

攻克编译器技术(2)

通过上两篇文章的反馈,感觉逐字句的翻译,读者很难读下去。从这篇文章开始,我会用简练的语言,对这本书进行拆解,方便读者快速的掌握知识脉络。

编译器全景图

对于一个编译器的主要任务是将我们写的源代码翻译成一个机器可以执行的代码,当然,在做这个过程中也可能会编译成中间代码,或者是字节码。每一种编码都有其特殊的目的和意义。在下面这个全景图中展示了这些路径。我们将循着这些路径完成我们的编译器之旅。

扫描(语义分析)

第一个需要进行的就是将我们写的源代码进行语义分析。例如我们有如下一个源码:

编译器首先按照某种规则将源码一段一段的读进来,然后将一句话分成不同的 token,在这个语句中有一些没有用的词,比如空格以及后面的分号。经过扫描器,我们得到了这样的内容。

我们自己做一个编程语言,这次我们自己尝试做一个脚本语言,主要是对源代码做解释执行。如果我们使用 Java 开发我们的编译器,我们应该第一步进行扫描。

package com.craftinginterpreters.lox;
import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;import java.nio.charset.Charset;import java.nio.file.Files;import java.nio.file.Paths;import java.util.List;
public class Lox {  public static void main(String[] args) throws IOException {    if (args.length > 1) {      System.out.println("Usage: jlox [script]");      System.exit(64);    } else if (args.length == 1) {      runFile(args[0]);    } else {      runPrompt();    }  }}
private static void runFile(String path) throws IOException {    byte[] bytes = Files.readAllBytes(Paths.get(path));    run(new String(bytes, Charset.defaultCharset())); }private static void runPrompt() throws IOException {    InputStreamReader input = new InputStreamReader(System.in);    BufferedReader reader = new BufferedReader(input);
    for (;;) {      System.out.print("> ");      String line = reader.readLine();      if (line == null) break;      run(line);    }  }
package com.craftinginterpreters.lox;
import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;import java.nio.charset.Charset;import java.nio.file.Files;import java.nio.file.Paths;import java.util.List;
public class Lox { public static void main(String[] args) throws IOException { if (args.length > 1) { System.out.println("Usage: jlox [script]"); System.exit(64); } else if (args.length == 1) { runFile(args[0]); } else { runPrompt(); } }}
private static void runFile(String path) throws IOException { byte[] bytes = Files.readAllBytes(Paths.get(path)); run(new String(bytes, Charset.defaultCharset())); }private static void runPrompt() throws IOException { InputStreamReader input = new InputStreamReader(System.in); BufferedReader reader = new BufferedReader(input);
for (;;) { System.out.print("> "); String line = reader.readLine(); if (line == null) break; run(line); } }

复制代码

在这段代码中,首先将源码读进来,然后进行句子的分析。首先要将代码编程一个一个的词,然后分析每句话是否有语法错误。这里可以根据我们自己要求进行检查。具体实现,暂时不做过多的解释。

private static void run(String source) {    Scanner scanner = new Scanner(source);    List<Token> tokens = scanner.scanTokens();
// For now, just print the tokens. for (Token token : tokens) { System.out.println(token); } }

复制代码

如果我们有个语句是这样的:var l = “hello”;

这个语句中 var 是关键字。l 是变量名,hello 是给变量的值。对于这样一个简单的语句我们应该如何区分呢,首先我们应该有一个表,用来记录我们自己设计的编程语言中的一些内容

package com.craftinginterpreters.lox;
enum TokenType { // Single-character tokens. LEFT_PAREN, RIGHT_PAREN, LEFT_BRACE, RIGHT_BRACE, COMMA, DOT, MINUS, PLUS, SEMICOLON, SLASH, STAR,
// One or two character tokens. BANG, BANG_EQUAL, EQUAL, EQUAL_EQUAL, GREATER, GREATER_EQUAL, LESS, LESS_EQUAL,
// Literals. IDENTIFIER, STRING, NUMBER,
// Keywords. AND, CLASS, ELSE, FALSE, FUN, FOR, IF, NIL, OR, PRINT, RETURN, SUPER, THIS, TRUE, VAR, WHILE,
EOF}

复制代码

对于 var 他是属于 keywords,这样在后 main 的过程中,我们将会进行单独的处理。 = 这是一个二元的操作符。通过这样的区分,我们在后面的分析过程中将会对每一个语句进行合适的切割,实现一个树形的结构。

以上是一个最简单的语句的大致分析过程,下一个部分,将会试图解释一个长的语句是如何解析的。

攻克编译器(1)

1.2.1 代码

本书是关于制作解释器的,所以这本书包含真实的代码。所需的每一行代码都包括在内,每个代码片段都会告诉您需要将不断增长的实现代码插入哪个位置。

许多其他语言书籍和语言实现使用诸如 Lex 和 Yacc 之类的工具,即所谓的编译器-编译器,它们会根据某些更高级别的(语法)描述自动生成一些源文件。这样的工具各有利弊,而且各方都有强烈的主张——有些人可能会说是信仰。

我们避免在这里使用这些工具。我想确保没有魔法和困惑藏在黑暗角落,所以我们将手写所有代码。正如您将看到的,它并不像听起来那么糟糕,这意味着您将真正理解每一行代码以及两个解释器的工作方式。

一本书与“现实世界”有不同的限制,因此这里的编码风格可能并不是可维护生产软件的最佳方式。我在省略私有变量或声明全局变量方面显得有点无所谓,请理解我这样做是为了让代码更容易阅读。书籍页面没有你的IDE那么宽,每个字符的空间都是很珍贵的。

此外,代码没有太多注释。那是因为每一部分代码前都用一些简介的文字进行解释,当你写一本书来配合你的程序时,你也可以省略注释。否则,您可能应该比我多使用更多注释符号(//)。

虽然这本书包含每一行代码实现,并说明每行代码的含义,但它没有描述编译和运行解释器所需的机制。我假设您可以在您选择的 IDE 中将一个 makefile 或一个项目放在一起,以便让代码运行。这些说明很快就会过时,我希望这本书能像 XO 白兰地一样历久弥新,而不是自家酿的酒一样容易过期。

1.2.2 代码片段

这本书实际上包含了实现所需的每一行代码,因此这些代码片段非常准确。 另外,即使缺少主要功能,我也会尝试让程序保持在可运行状态,因此有时我们会添加临时代码,这些代码会在以后的代码片段中被替换。

一个完整的代码片段如下所示:

您可以在其中添加新代码。 它可能在上方或下方有一些淡出的行,用来显示它在相关代码中的位置。如果该简介说“replace x lines”,则在浅色的行之间存在一些现有代码需要删除并替换为新的代码段。

1.2.3 旁白

旁白包含传记简介、历史背景、对相关主题的引用以及其他领域的探索建议。您不需要了解就可以理解本书后面的部分,所以如果你愿意,你可以跳过它们。 我不加评判,但可能会有点难过。

1.2.4 挑战

每章结尾都有一些练习题。 与教科书习题集不同,这些习题集往往会复习您已经学习的材料,这些习题集旨在帮助您学习比本章更加丰富的内容。 他们引导您离开文章引导路径并自行探索。 它们会让你研究其他语言,弄清楚如何实现功能,或者让你走出舒适区。

克服挑战,您将获得更广泛的认知,可能还会遇到一些挫折。 如果您想留在旅游巴士舒适区内,也可以跳过它们。 这是你的书。

1.2.5 设计说明

大多数“编程语言”书籍都是严格意义上的编程语言实现书籍。他们很少讨论如何设计正在实现的语言。实现之所以有趣,是因为它的定义如此精确。程序员似乎更喜欢黑与白、1 和 0 这样的内容。

就个人而言,我认为世界只需要一些 FORTRAN 77 的实现。在某些时候,您会发现自己正在设计一种新语言,一旦开始这样做,表达式中更舒适、更人性化的一面就变得至关重要。诸如哪些功能易于学习,创新和熟悉程度需要取得平衡,哪种语法更易读以及面向哪些用户。

所有这些都对新语言的成功有深远的影响。我希望你的语言能够取得成功,所以在某些章节中,我以“设计笔记”结尾,这是一篇关于编程语言人文某些方面的讨论。我不是这方面的专家,我不知道是否有人真的精通这些内容。所以把这些思考的食材和调料一起烹饪,这应该使它们变得更美味,值得深思,这是我的主要目标。

1.3 第一个解释器

我们将用 Java 编写我们的第一个解释器 jlox。关注的重点是概念。我们将编写最简单、最干净的代码来正确实现语言的语义。这将使我们熟悉基本技术,并磨练我们正确理解语言应表现形式。

Java 是一种很好的语言。它足够高级,我们不会被繁琐的实现细节所淹没,代码仍然非常明确。与脚本语言不同,隐藏在引擎盖下的机器往往不太复杂,并且您可以使用静态类型来查看正在使用的数据结构。

我特别选择了 Java,因为它是一种面向对象的语言。这种编程范式在 90 年代席卷了编程世界,现在是数百万程序员的主要思维方式。很有可能你已经习惯于将代码组织成类和方法,所以我们会让你保持在这个舒适区。

虽然学术性语言专家有时看不起面向对象的语言,但现实中它们广泛用于语言工作中。 GCC 和 LLVM 是用 C++ 编写的,大多数 JavaScript 虚拟机也是如此。面向对象的语言无处不在,一种语言的工具和编译器通常是用同一种语言编写的。

最后,Java 非常受欢迎。这意味着你很有可能已经知道它,所以你要学习的东西就更少了。如果您对 Java 不太熟悉,也不用担心。我尽量只使用它的一个最小子集。我使用 Java 7 中的菱形运算符使代码变得更简洁,但就“高级”功能而言,仅此而已。如果您了解另一种面向对象的语言,例如 C# 或 C++,您可以应对自如。

在第二部分结束时,我们将有一个简单易读的计时器实现。它的运行速度不是很快,但它是正确运行的,因为只能利用在 Java 虚拟机自身运行时基础上构建的实现。我们想了解 Java 本身是如何实现这些东西的。

1.4 第二个解释器

所以在下一部分,我们从头开始,但这次是使用C语言。C 是理解实现如何真正工作的完美语言,它深入到内存中的字节和流经 CPU 的代码。

我们使用 C 的一个重要原因是我可以向你展示 C 特别擅长的东西,但这确实意味着你需要对它非常熟悉。你不必是丹尼斯·里奇的转世,但你也不应该被指针吓到。

如果您还没有非常熟悉C语言,请找一本关于 C 的介绍性书籍并仔细阅读,然后在完成后返回这里。作为回报,从本书中您将成为一个优秀的 C 程序员。可以想象有多少语言实现是用 C 编写的,例如:Lua、CPython 和 Ruby 的 MRI 等等。

在我们的 C 解释器 clox 中,我们不得不自己实现 Java 免费提供给我们的东西。我们将编写自己的动态数组和哈希表。我们将决定对象在内存中的表示方式,并构建一个垃圾收集器来回收它们。

我们的 Java 实现专注于正确性。既然我们已经做到了,我们将转向提高运行速度。我们的 C 解释器将包含一个编译器,该编译器将 Lox 转换为有效的字节码表示(别担心,我很快就会讲解明白这意味着什么),然后它会执行。这与 Lua、Python、Ruby、PHP 和许多其他成功语言的实现所使用的技术相同。

我们甚至会尝试进行基准测试和优化。 到最后,我们将拥有一个强大、准确、快速的语言解释器,能够跟其他专业水准的实现媲美。 对于一本书和几千行代码来说还不错。

挑战

1. 在我编写和出版这本书中的系统中至少使用了六种特定领域的语言。 是哪些?

2.用Java编写和运行一个“hello world!”的程序。 设置您需要的 makefile 或 IDE 项目以使其正常工作。 如果您有调试器,请熟悉它并在程序运行时对代码进行单步调试。

3. 对 C 做同样的操作。为了练习指针,定义一个在堆中分配字符串的双向链表。 编写函数在其中插入、查找和删除项目。 测试编写的函数。