分类 软件开发 下的文章

不需要昂贵的工具即可领略数据科学的力量,从这些开源工具起步即可。

无论你是一个具有数学或计算机科学背景的资深数据科学爱好者,还是一个其它领域的专家,数据科学提供的可能性都在你力所能及的范围内,而且你不需要昂贵的,高度专业化的企业级软件。本文中讨论的开源工具就是你入门时所需的全部内容。

Python,其机器学习和数据科学库(pandasKerasTensorFlowscikit-learnSciPyNumPy 等),以及大量可视化库(MatplotlibpyplotPlotly 等)对于初学者和专家来说都是优秀的自由及开源软件工具。它们易于学习,很受欢迎且受到社区支持,并拥有为数据科学而开发的最新技术和算法。它们是你在开始学习时可以获得的最佳工具集之一。

许多 Python 库都是建立在彼此之上的(称为依赖项),其基础是 NumPy 库。NumPy 专门为数据科学设计,经常被用于在其 ndarray 数据类型中存储数据集的相关部分。ndarray 是一种方便的数据类型,用于将关系表中的记录存储为 cvs 文件或其它任何格式,反之亦然。将 scikit 函数应用于多维数组时,它特别方便。SQL 非常适合查询数据库,但是对于执行复杂和资源密集型的数据科学操作,在 ndarray 中存储数据可以提高效率和速度(但请确保在处理大量数据集时有足够的 RAM)。当你使用 pandas 进行知识提取和分析时,pandas 中的 DataFrame 数据类型和 NumPy 中的 ndarray 之间的无缝转换分别为提取和计算密集型操作创建了一个强大的组合。

作为快速演示,让我们启动 Python shell 并在 pandas DataFrame 变量中加载来自巴尔的摩的犯罪统计数据的开放数据集,并查看加载的一部分 DataFrame:

>>>  import pandas as pd
>>>  crime_stats = pd.read_csv('BPD_Arrests.csv')
>>>  crime_stats.head()

我们现在可以在这个 pandas DataFrame 上执行大多数查询,就像我们可以在数据库中使用 SQL 一样。例如,要获取 Description 属性的所有唯一值,SQL 查询是:

$ SELECT unique(“Description”) from crime_stats;

利用 pandas DataFrame 编写相同的查询如下所示:

>>>  crime_stats['Description'].unique()
['COMMON   ASSAULT'   'LARCENY'   'ROBBERY   - STREET'   'AGG.   ASSAULT'
'LARCENY   FROM   AUTO'   'HOMICIDE'   'BURGLARY'   'AUTO   THEFT'
'ROBBERY   - RESIDENCE'   'ROBBERY   - COMMERCIAL'   'ROBBERY   - CARJACKING'
'ASSAULT   BY  THREAT'   'SHOOTING'   'RAPE'   'ARSON']

它返回的是一个 NumPy 数组(ndarray 类型):

>>>  type(crime_stats['Description'].unique())
<class   'numpy.ndarray'>

接下来让我们将这些数据输入神经网络,看看它能多准确地预测使用的武器类型,给出的数据包括犯罪事件,犯罪类型以及发生的地点:

>>>  from   sklearn.neural_network   import   MLPClassifier
>>>  import   numpy   as np
>>>
>>>  prediction   =  crime_stats[[‘Weapon’]]
>>>  predictors   =  crime_stats['CrimeTime',   ‘CrimeCode’,   ‘Neighborhood’]
>>>
>>>  nn_model   =  MLPClassifier(solver='lbfgs',   alpha=1e-5,   hidden_layer_sizes=(5,
2),   random_state=1)
>>>
>>>predict_weapon   =  nn_model.fit(prediction,   predictors)

现在学习模型准备就绪,我们可以执行一些测试来确定其质量和可靠性。对于初学者,让我们输入一个训练集数据(用于训练模型的原始数据集的一部分,不包括在创建模型中):

>>>  predict_weapon.predict(training_set_weapons)
array([4,   4,   4,   ..., 0,   4,   4])

如你所见,它返回一个列表,每个数字预测训练集中每个记录的武器。我们之所以看到的是数字而不是武器名称,是因为大多数分类算法都是用数字优化的。对于分类数据,有一些技术可以将属性转换为数字表示。在这种情况下,使用的技术是标签编码,使用 sklearn 预处理库中的 LabelEncoder 函数:preprocessing.LabelEncoder()。它能够对一个数据和其对应的数值表示来进行变换和逆变换。在这个例子中,我们可以使用 LabelEncoder()inverse_transform 函数来查看武器 0 和 4 是什么:

>>>  preprocessing.LabelEncoder().inverse_transform(encoded_weapons)
array(['HANDS',   'FIREARM',   'HANDS',   ..., 'FIREARM',   'FIREARM',   'FIREARM']

这很有趣,但为了了解这个模型的准确程度,我们将几个分数计算为百分比:

>>>  nn_model.score(X,   y)
0.81999999999999995

这表明我们的神经网络模型准确度约为 82%。这个结果似乎令人印象深刻,但用于不同的犯罪数据集时,检查其有效性非常重要。还有其它测试来做这个,如相关性、混淆、矩阵等。尽管我们的模型有很高的准确率,但它对于一般犯罪数据集并不是非常有用,因为这个特定数据集具有不成比例的行数,其列出 FIREARM 作为使用的武器。除非重新训练,否则我们的分类器最有可能预测 FIREARM,即使输入数据集有不同的分布。

在对数据进行分类之前清洗数据并删除异常值和畸形数据非常重要。预处理越好,我们的见解准确性就越高。此外,为模型或分类器提供过多数据(通常超过 90%)以获得更高的准确度是一个坏主意,因为它看起来准确但由于过度拟合而无效。

Jupyter notebooks 相对于命令行来说是一个很好的交互式替代品。虽然 CLI 对于大多数事情都很好,但是当你想要运行代码片段以生成可视化时,Jupyter 会很出色。它比终端更好地格式化数据。

这篇文章 列出了一些最好的机器学习免费资源,但是还有很多其它的指导和教程。根据你的兴趣和爱好,你还会发现许多开放数据集可供使用。作为起点,由 Kaggle 维护的数据集,以及在州政府网站上提供的数据集是极好的资源。


via: https://opensource.com/article/18/3/getting-started-data-science

作者:Payal Singh 译者:MjSeven 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

简介

在本实验中,你将为你的操作系统写内存管理方面的代码。内存管理由两部分组成。

第一部分是内核的物理内存分配器,内核通过它来分配内存,以及在不需要时释放所分配的内存。分配器以 page 为单位分配内存,每个页的大小为 4096 字节。你的任务是去维护那个数据结构,它负责记录物理页的分配和释放,以及每个分配的页有多少进程共享它。本实验中你将要写出分配和释放内存页的全套代码。

第二个部分是虚拟内存的管理,它负责由内核和用户软件使用的虚拟内存地址到物理内存地址之间的映射。当使用内存时,x86 架构的硬件是由内存管理单元(MMU)负责执行映射操作来查阅一组页表。接下来你将要修改 JOS,以根据我们提供的特定指令去设置 MMU 的页表。

预备知识

在本实验及后面的实验中,你将逐步构建你的内核。我们将会为你提供一些附加的资源。使用 Git 去获取这些资源、提交自实验 1 以来的改变(如有需要的话)、获取课程仓库的最新版本、以及在我们的实验 2 (origin/lab2)的基础上创建一个称为 lab2 的本地分支:

athena% cd ~/6.828/lab
athena% add git
athena% git pull
Already up-to-date.
athena% git checkout -b lab2 origin/lab2
Branch lab2 set up to track remote branch refs/remotes/origin/lab2.
Switched to a new branch "lab2"
athena%

上面的 git checkout -b 命令其实做了两件事情:首先它创建了一个本地分支 lab2,它跟踪给我们提供课程内容的远程分支 origin/lab2 ,第二件事情是,它改变你的 lab 目录的内容以反映 lab2 分支上存储的文件的变化。Git 允许你在已存在的两个分支之间使用 git checkout *branch-name* 命令去切换,但是在你切换到另一个分支之前,你应该去提交那个分支上你做的任何有意义的变更。

现在,你需要将你在 lab1 分支中的改变合并到 lab2 分支中,命令如下:

athena% git merge lab1
Merge made by recursive.
 kern/kdebug.c  |   11 +++++++++-- 
 kern/monitor.c |   19 +++++++++++++++++++
 lib/printfmt.c |    7 +++----
 3 files changed, 31 insertions(+), 6 deletions(-)
athena%

在一些案例中,Git 或许并不知道如何将你的更改与新的实验任务合并(例如,你在第二个实验任务中变更了一些代码的修改)。在那种情况下,你使用 git 命令去合并,它会告诉你哪个文件发生了冲突,你必须首先去解决冲突(通过编辑冲突的文件),然后使用 git commit -a 去重新提交文件。

实验 2 包含如下的新源代码,后面你将逐个了解它们:

  • inc/memlayout.h
  • kern/pmap.c
  • kern/pmap.h
  • kern/kclock.h
  • kern/kclock.c

memlayout.h 描述虚拟地址空间的布局,这个虚拟地址空间是通过修改 pmap.cmemlayout.hpmap.h 所定义的 PageInfo 数据结构来实现的,这个数据结构用于跟踪物理内存页面是否被释放。kclock.ckclock.h 维护 PC 上基于电池的时钟和 CMOS RAM 硬件,在此,BIOS 中记录了 PC 上安装的物理内存数量,以及其它的一些信息。在 pmap.c 中的代码需要去读取这个设备硬件,以算出在这个设备上安装了多少物理内存,但这部分代码已经为你完成了:你不需要知道 CMOS 硬件工作原理的细节。

特别需要注意的是 memlayout.hpmap.h,因为本实验需要你去使用和理解的大部分内容都包含在这两个文件中。你或许还需要去看看 inc/mmu.h 这个文件,因为它也包含了本实验中用到的许多定义。

开始本实验之前,记得去添加 exokernel 以获取 QEMU 的 6.828 版本。

实验过程

在你准备进行实验和写代码之前,先添加你的 answers-lab2.txt 文件到 Git 仓库,提交你的改变然后去运行 make handin

athena% git add answers-lab2.txt
athena% git commit -am "my answer to lab2"
[lab2 a823de9] my answer to lab2 4 files changed, 87 insertions(+), 10 deletions(-)
athena% make handin

正如前面所说的,我们将使用一个评级程序来分级你的解决方案,你可以在 lab 目录下运行 make grade,使用评级程序来测试你的内核。为了完成你的实验,你可以改变任何你需要的内核源代码和头文件。但毫无疑问的是,你不能以任何形式去改变或破坏评级代码。

第 1 部分:物理页面管理

操作系统必须跟踪物理内存页是否使用的状态。JOS 以“页”为最小粒度来管理 PC 的物理内存,以便于它使用 MMU 去映射和保护每个已分配的内存片段。

现在,你将要写内存的物理页分配器的代码。它将使用 struct PageInfo 对象的链表来保持对物理页的状态跟踪,每个对象都对应到一个物理内存页。在你能够编写剩下的虚拟内存实现代码之前,你需要先编写物理内存页面分配器,因为你的页表管理代码将需要去分配物理内存来存储页表。

练习 1

在文件 kern/pmap.c 中,你需要去实现以下函数的代码(或许要按给定的顺序来实现)。

  • boot_alloc()
  • mem_init()(只要能够调用 check_page_free_list() 即可)
  • page_init()
  • page_alloc()
  • page_free()

check_page_free_list()check_page_alloc() 可以测试你的物理内存页分配器。你将需要引导 JOS 然后去看一下 check_page_alloc() 是否报告成功即可。如果没有报告成功,修复你的代码直到成功为止。你可以添加你自己的 assert() 以帮助你去验证是否符合你的预期。

本实验以及所有的 6.828 实验中,将要求你做一些检测工作,以便于你搞清楚它们是否按你的预期来工作。这个任务不需要详细描述你添加到 JOS 中的代码的细节。查找 JOS 源代码中你需要去修改的那部分的注释;这些注释中经常包含有技术规范和提示信息。你也可能需要去查阅 JOS 和 Intel 的技术手册、以及你的 6.004 或 6.033 课程笔记的相关部分。

第 2 部分:虚拟内存

在你开始动手之前,需要先熟悉 x86 内存管理架构的保护模式:即分段和页面转换。

练习 2

如果你对 x86 的保护模式还不熟悉,可以查看 Intel 80386 参考手册的第 5 章和第 6 章。阅读这些章节(5.2 和 6.4)中关于页面转换和基于页面的保护。我们建议你也去了解关于段的章节;在虚拟内存和保护模式中,JOS 使用了分页、段转换、以及在 x86 上不能禁用的基于段的保护,因此你需要去理解这些基础知识。

虚拟地址、线性地址和物理地址

在 x86 的专用术语中,一个 虚拟地址 virtual address 是由一个段选择器和在段中的偏移量组成。一个 线性地址 linear address 是在页面转换之前、段转换之后得到的一个地址。一个 物理地址 physical address 是段和页面转换之后得到的最终地址,它最终将进入你的物理内存中的硬件总线。

一个 C 指针是虚拟地址的“偏移量”部分。在 boot/boot.S 中我们安装了一个 全局描述符表 Global Descriptor Table (GDT),它通过设置所有的段基址为 0,并且限制为 0xffffffff 来有效地禁用段转换。因此“段选择器”并不会生效,而线性地址总是等于虚拟地址的偏移量。在实验 3 中,为了设置权限级别,我们将与段有更多的交互。但是对于内存转换,我们将在整个 JOS 实验中忽略段,只专注于页转换。

回顾实验 1 中的第 3 部分,我们安装了一个简单的页表,这样内核就可以在 0xf0100000 链接的地址上运行,尽管它实际上是加载在 0x00100000 处的 ROM BIOS 的物理内存上。这个页表仅映射了 4MB 的内存。在实验中,你将要为 JOS 去设置虚拟内存布局,我们将从虚拟地址 0xf0000000 处开始扩展它,以映射物理内存的前 256MB,并映射许多其它区域的虚拟内存。

练习 3

虽然 GDB 能够通过虚拟地址访问 QEMU 的内存,它经常用于在配置虚拟内存期间检查物理内存。在实验工具指南中复习 QEMU 的监视器命令,尤其是 xp 命令,它可以让你去检查物理内存。要访问 QEMU 监视器,可以在终端中按 Ctrl-a c(相同的绑定返回到串行控制台)。

使用 QEMU 监视器的 xp 命令和 GDB 的 x 命令去检查相应的物理内存和虚拟内存,以确保你看到的是相同的数据。

我们的打过补丁的 QEMU 版本提供一个非常有用的 info pg 命令:它可以展示当前页表的一个具体描述,包括所有已映射的内存范围、权限、以及标志。原本的 QEMU 也提供一个 info mem 命令用于去展示一个概要信息,这个信息包含了已映射的虚拟内存范围和使用了什么权限。

在 CPU 上运行的代码,一旦处于保护模式(这是在 boot/boot.S 中所做的第一件事情)中,是没有办法去直接使用一个线性地址或物理地址的。所有的内存引用都被解释为虚拟地址,然后由 MMU 来转换,这意味着在 C 语言中的指针都是虚拟地址。

例如在物理内存分配器中,JOS 内存经常需要在不反向引用的情况下,去维护作为地址的一个很难懂的值或一个整数。有时它们是虚拟地址,而有时是物理地址。为便于在代码中证明,JOS 源文件中将它们区分为两种:类型 uintptr_t 表示一个难懂的虚拟地址,而类型 physaddr_trepresents 表示物理地址。这些类型其实不过是 32 位整数(uint32_t)的同义词,因此编译器不会阻止你将一个类型的数据指派为另一个类型!因为它们都是整数(而不是指针)类型,如果你想去反向引用它们,编译器将报错。

JOS 内核能够通过将它转换为指针类型的方式来反向引用一个 uintptr_t 类型。相反,内核不能反向引用一个物理地址,因为这是由 MMU 来转换所有的内存引用。如果你转换一个 physaddr_t 为一个指针类型,并反向引用它,你或许能够加载和存储最终结果地址(硬件将它解释为一个虚拟地址),但你并不会取得你想要的内存位置。

总结如下:

C 类型地址类型
T*虚拟
uintptr_t虚拟
physaddr_t物理

问题:

  1. 假设下面的 JOS 内核代码是正确的,那么变量 x 应该是什么类型?uintptr_t 还是 physaddr_t

JOS 内核有时需要去读取或修改它只知道其物理地址的内存。例如,添加一个映射到页表,可以要求分配物理内存去存储一个页目录,然后去初始化它们。然而,内核也和其它的软件一样,并不能跳过虚拟地址转换,内核并不能直接加载和存储物理地址。一个原因是 JOS 将重映射从虚拟地址 0xf0000000 处的物理地址 0 开始的所有的物理地址,以帮助内核去读取和写入它知道物理地址的内存。为转换一个物理地址为一个内核能够真正进行读写操作的虚拟地址,内核必须添加 0xf0000000 到物理地址以找到在重映射区域中相应的虚拟地址。你应该使用 KADDR(pa) 去做那个添加操作。

JOS 内核有时也需要能够通过给定的内核数据结构中存储的虚拟地址找到内存中的物理地址。内核全局变量和通过 boot_alloc() 分配的内存是在内核所加载的区域中,从 0xf0000000 处开始的这个所有物理内存映射的区域。因此,要转换这些区域中一个虚拟地址为物理地址时,内核能够只是简单地减去 0xf0000000 即可得到物理地址。你应该使用 PADDR(va) 去做那个减法操作。

引用计数

在以后的实验中,你将会经常遇到多个虚拟地址(或多个环境下的地址空间中)同时映射到相同的物理页面上。你将在 struct PageInfo 数据结构中的 pp_ref 字段来记录一个每个物理页面的引用计数器。如果一个物理页面的这个计数器为 0,表示这个页面已经被释放,因为它不再被使用了。一般情况下,这个计数器应该等于所有页表中物理页面出现在 UTOP 之下的次数(UTOP 之上的映射大都是在引导时由内核设置的,并且它从不会被释放,因此不需要引用计数器)。我们也使用它去跟踪放到页目录页的指针数量,反过来就是,页目录到页表页的引用数量。

使用 page_alloc 时要注意。它返回的页面引用计数总是为 0,因此,一旦对返回页做了一些操作(比如将它插入到页表),pp_ref 就应该增加。有时这需要通过其它函数(比如,page_instert)来处理,而有时这个函数是直接调用 page_alloc 来做的。

页表管理

现在,你将写一套管理页表的代码:去插入和删除线性地址到物理地址的映射表,并且在需要的时候去创建页表。

练习 4

在文件 kern/pmap.c 中,你必须去实现下列函数的代码。

  • pgdir\_walk()
  • bootmapregion()
  • page\_lookup()
  • page\_remove()
  • page\_insert()

check_page(),调用自 mem_init(),测试你的页表管理函数。在进行下一步流程之前你应该确保它成功运行。

第 3 部分:内核地址空间

JOS 分割处理器的 32 位线性地址空间为两部分:用户环境(进程),(我们将在实验 3 中开始加载和运行),它将控制其上的布局和低位部分的内容;而内核总是维护对高位部分的完全控制。分割线的定义是在 inc/memlayout.h 中通过符号 ULIM 来划分的,它为内核保留了大约 256MB 的虚拟地址空间。这就解释了为什么我们要在实验 1 中给内核这样的一个高位链接地址的原因:如是不这样做的话,内核的虚拟地址空间将没有足够的空间去同时映射到下面的用户空间中。

你可以在 inc/memlayout.h 中找到一个图表,它有助于你去理解 JOS 内存布局,这在本实验和后面的实验中都会用到。

权限和故障隔离

由于内核和用户的内存都存在于它们各自环境的地址空间中,因此我们需要在 x86 的页表中使用权限位去允许用户代码只能访问用户所属地址空间的部分。否则,用户代码中的 bug 可能会覆写内核数据,导致系统崩溃或者发生各种莫名其妙的的故障;用户代码也可能会偷窥其它环境的私有数据。

对于 ULIM 以上部分的内存,用户环境没有任何权限,只有内核才可以读取和写入这部分内存。对于 [UTOP,ULIM] 地址范围,内核和用户都有相同的权限:它们可以读取但不能写入这个地址范围。这个地址范围是用于向用户环境暴露某些只读的内核数据结构。最后,低于 UTOP 的地址空间是为用户环境所使用的;用户环境将为访问这些内核设置权限。

初始化内核地址空间

现在,你将去配置 UTOP 以上的地址空间:内核部分的地址空间。inc/memlayout.h 中展示了你将要使用的布局。我将使用函数去写相关的线性地址到物理地址的映射配置。

练习 5

完成调用 check_page() 之后在 mem_init() 中缺失的代码。

现在,你的代码应该通过了 check_kern_pgdir()check_page_installed_pgdir() 的检查。

问题:

​ 1、在这个时刻,页目录中的条目(行)是什么?它们映射的址址是什么?以及它们映射到哪里了?换句话说就是,尽可能多地填写这个表:

条目虚拟地址基址指向(逻辑上):
1023?物理内存顶部 4MB 的页表
1022??
.??
.??
.??
20x00800000?
10x00400000?
00x00000000[参见下一问题]

​ 2、(来自课程 3) 我们将内核和用户环境放在相同的地址空间中。为什么用户程序不能去读取和写入内核的内存?有什么特殊机制保护内核内存?

​ 3、这个操作系统能够支持的最大的物理内存数量是多少?为什么?

​ 4、如果我们真的拥有最大数量的物理内存,有多少空间的开销用于管理内存?这个开销可以减少吗?

​ 5、复习在 kern/entry.Skern/entrypgdir.c 中的页表设置。一旦我们打开分页,EIP 仍是一个很小的数字(稍大于 1MB)。在什么情况下,我们转而去运行在 KERNBASE 之上的一个 EIP?当我们启用分页并开始在 KERNBASE 之上运行一个 EIP 时,是什么让我们能够一个很低的 EIP 上持续运行?为什么这种转变是必需的?

地址空间布局的其它选择

在 JOS 中我们使用的地址空间布局并不是我们唯一的选择。一个操作系统可以在低位的线性地址上映射内核,而为用户进程保留线性地址的高位部分。然而,x86 内核一般并不采用这种方法,因为 x86 向后兼容模式之一(被称为“虚拟 8086 模式”)“不可改变地”在处理器使用线性地址空间的最下面部分,所以,如果内核被映射到这里是根本无法使用的。

虽然很困难,但是设计这样的内核是有这种可能的,即:不为处理器自身保留任何固定的线性地址或虚拟地址空间,而有效地允许用户级进程不受限制地使用整个 4GB 的虚拟地址空间 —— 同时还要在这些进程之间充分保护内核以及不同的进程之间相互受保护!

将内核的内存分配系统进行概括类推,以支持二次幂为单位的各种页大小,从 4KB 到一些你选择的合理的最大值。你务必要有一些方法,将较大的分配单位按需分割为一些较小的单位,以及在需要时,将多个较小的分配单位合并为一个较大的分配单位。想一想在这样的一个系统中可能会出现些什么样的问题。

这个实验做完了。确保你通过了所有的等级测试,并记得在 answers-lab2.txt 中写下你对上述问题的答案。提交你的改变(包括添加 answers-lab2.txt 文件),并在 lab 目录下输入 make handin 去提交你的实验。


via: https://sipb.mit.edu/iap/6.828/lab/lab2/

作者:Mit 译者:qhwdw 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

我们将会在本篇文章中看到从零开始实现的编译器,将简单的类 LISP 计算语言编译成 JavaScript。完整的源代码在 这里

我们将会:

  1. 自定义语言,并用它编写一个简单的程序
  2. 实现一个简单的解析器组合器
  3. 为该语言实现一个解析器
  4. 为该语言实现一个美观的打印器
  5. 为我们的用途定义 JavaScript 的一个子集
  6. 实现代码转译器,将代码转译成我们定义的 JavaScript 子集
  7. 把所有东西整合在一起

开始吧!

1、定义语言

Lisp 族语言最迷人的地方在于,它们的语法就是树状表示的,这就是这门语言很容易解析的原因。我们很快就能接触到它。但首先让我们把自己的语言定义好。关于我们语言的语法的范式(BNF)描述如下:

program ::= expr
expr ::= <integer> | <name> | ([<expr>])

基本上,我们可以在该语言的最顶层定义表达式并对其进行运算。表达式由一个整数(比如 5)、一个变量(比如 x)或者一个表达式列表(比如 (add x 1))组成。

整数对应它本身的值,变量对应它在当前环境中绑定的值,表达式列表对应一个函数调用,该列表的第一个参数是相应的函数,剩下的表达式是传递给这个函数的参数。

该语言中,我们保留一些内建的特殊形式,这样我们就能做一些更有意思的事情:

  • let 表达式使我们可以在它的 body 环境中引入新的变量。语法如下:
let ::= (let ([<letarg>]) <body>)
letargs ::= (<name> <expr>)
body ::= <expr>
  • lambda 表达式:也就是匿名函数定义。语法如下:
lambda ::= (lambda ([<name>]) <body>)

还有一些内建函数: addmulsubdivprint

让我们看看用我们这门语言编写的入门示例程序:

(let
  ((compose
    (lambda (f g)
      (lambda (x) (f (g x)))))
  (square
    (lambda (x) (mul x x)))
  (add1
    (lambda (x) (add x 1))))
  (print ((compose square add1) 5)))

这个程序定义了 3 个函数:composesquareadd1。然后将计算结果的值 ((compose square add1) 5) 输出出来。

我相信了解这门语言,这些信息就足够了。开始实现它吧。

在 Haskell 中,我们可以这样定义语言:

type Name = String

data Expr
  = ATOM Atom
  | LIST [Expr]
    deriving (Eq, Read, Show)

data Atom
  = Int Int
  | Symbol Name
    deriving (Eq, Read, Show)

我们可以解析用该语言用 Expr 定义的程序。而且,这里我们添加了新数据类型 EqReadShow 等实例用于测试和调试。你能够在 REPL 中使用这些数据类型,验证它们确实有用。

我们不在语法中定义 lambdalet 或其它的内建函数,原因在于,当前情况下我们没必要用到这些东西。这些函数仅仅是 LIST (表达式列表)的更加特殊的用例。所以我决定将它放到后面的部分。

一般来说你想要在抽象语法中定义这些特殊用例 —— 用于改进错误信息、禁用静态分析和优化等等,但在这里我们不会这样做,对我们来说这些已经足够了。

另一件你想做的事情可能是在语法中添加一些注释信息。比如定位:Expr 是来自哪个文件的,具体到这个文件的哪一行哪一列。你可以在后面的阶段中使用这一特性,打印出错误定位,即使它们不是处于解析阶段。

  • 练习 1:添加一个 Program 数据类型,可以按顺序包含多个 Expr
  • 练习 2:向语法树中添加一个定位注解。

2、实现一个简单的解析器组合库

我们要做的第一件事情是定义一个 嵌入式领域专用语言 Embedded Domain Specific Language (EDSL),我们会用它来定义我们的语言解析器。这常常被称为解析器组合库。我们做这件事完全是出于学习的目的,Haskell 里有很好的解析库,在实际构建软件或者进行实验时,你应该使用它们。megaparsec 就是这样的一个库。

首先我们来谈谈解析库的实现的思路。本质上,我们的解析器就是一个函数,接受一些输入,可能会读取输入的一些或全部内容,然后返回解析出来的值和无法解析的输入部分,或者在解析失败时抛出异常。我们把它写出来。

newtype Parser a
  = Parser (ParseString -> Either ParseError (a, ParseString))

data ParseString
  = ParseString Name (Int, Int) String

data ParseError
  = ParseError ParseString Error

type Error = String

这里我们定义了三个主要的新类型。

第一个,Parser a 是之前讨论的解析函数。

第二个,ParseString 是我们的输入或携带的状态。它有三个重要的部分:

  • Name: 这是源的名字
  • (Int, Int): 这是源的当前位置
  • String: 这是等待解析的字符串

第三个,ParseError 包含了解析器的当前状态和一个错误信息。

现在我们想让这个解析器更灵活,我们将会定义一些常用类型的实例。这些实例让我们能够将小巧的解析器和复杂的解析器结合在一起(因此它的名字叫做 “解析器组合器”)。

第一个是 Functor 实例。我们需要 Functor 实例,因为我们要能够对解析值应用函数从而使用不同的解析器。当我们定义自己语言的解析器时,我们将会看到关于它的示例。

instance Functor Parser where
  fmap f (Parser parser) =
    Parser (\str -> first f <$> parser str)

第二个是 Applicative 实例。该实例的常见用例是在多个解析器中实现一个纯函数。

instance Applicative Parser where
  pure x = Parser (\str -> Right (x, str))
  (Parser p1) <*> (Parser p2) =
    Parser $
      \str -> do
        (f, rest)  <- p1 str
        (x, rest') <- p2 rest
        pure (f x, rest')

(注意:我们还会实现一个 Monad 实例,这样我们才能使用符号)

第三个是 Alternative 实例。万一前面的解析器解析失败了,我们要能够提供一个备用的解析器。

instance Alternative Parser where
  empty = Parser (`throwErr` "Failed consuming input")
  (Parser p1) <|> (Parser p2) =
    Parser $
      \pstr -> case p1 pstr of
        Right result -> Right result
        Left  _      -> p2 pstr

第四个是 Monad 实例。这样我们就能链接解析器。

instance Monad Parser where
  (Parser p1) >>= f =
    Parser $
     \str -> case p1 str of
       Left err -> Left err
       Right (rs, rest) ->
         case f rs of
           Parser parser -> parser rest

接下来,让我们定义一种的方式,用于运行解析器和防止失败的助手函数:

runParser :: String -> String -> Parser a -> Either ParseError (a, ParseString)
runParser name str (Parser parser) = parser $ ParseString name (0,0) str

throwErr :: ParseString -> String -> Either ParseError a
throwErr ps@(ParseString name (row,col) _) errMsg =
  Left $ ParseError ps $ unlines
    [ "*** " ++ name ++ ": " ++ errMsg
    , "* On row " ++ show row ++ ", column " ++ show col ++ "."
    ]

现在我们将会开始实现组合器,这是 EDSL 的 API,也是它的核心。

首先,我们会定义 oneOf。如果输入列表中的字符后面还有字符的话,oneOf 将会成功,否则就会失败。

oneOf :: [Char] -> Parser Char
oneOf chars =
  Parser $ \case
    ps@(ParseString name (row, col) str) ->
      case str of
        []     -> throwErr ps "Cannot read character of empty string"
        (c:cs) ->
          if c `elem` chars
          then Right (c, ParseString name (row, col+1) cs)
          else throwErr ps $ unlines ["Unexpected character " ++ [c], "Expecting one of: " ++ show chars]

optional 将会抛出异常,停止解析器。失败时它仅仅会返回 Nothing

optional :: Parser a -> Parser (Maybe a)
optional (Parser parser) =
  Parser $
    \pstr -> case parser pstr of
      Left _ -> Right (Nothing, pstr)
      Right (x, rest) -> Right (Just x, rest)

many 将会试着重复运行解析器,直到失败。当它完成的时候,会返回成功运行的解析器列表。many1 做的事情是一样的,但解析失败时它至少会抛出一次异常。

many :: Parser a -> Parser [a]
many parser = go []
  where go cs = (parser >>= \c -> go (c:cs)) <|> pure (reverse cs)

many1 :: Parser a -> Parser [a]
many1 parser =
  (:) <$> parser <*> many parser

下面的这些解析器通过我们定义的组合器来实现一些特殊的解析器:

char :: Char -> Parser Char
char c = oneOf [c]

string :: String -> Parser String
string = traverse char

space :: Parser Char
space = oneOf " \n"

spaces :: Parser String
spaces = many space

spaces1 :: Parser String
spaces1 = many1 space

withSpaces :: Parser a -> Parser a
withSpaces parser =
  spaces *> parser <* spaces

parens :: Parser a -> Parser a
parens parser =
     (withSpaces $ char '(')
  *> withSpaces parser
  <* (spaces *> char ')')

sepBy :: Parser a -> Parser b -> Parser [b]
sepBy sep parser = do
  frst <- optional parser
  rest <- many (sep *> parser)
  pure $ maybe rest (:rest) frst

现在为该门语言定义解析器所需要的所有东西都有了。

  • 练习 :实现一个 EOF(end of file/input,即文件或输入终止符)解析器组合器。

3、为我们的语言实现解析器

我们会用自顶而下的方法定义解析器。

parseExpr :: Parser Expr
parseExpr = fmap ATOM parseAtom <|> fmap LIST parseList

parseList :: Parser [Expr]
parseList = parens $ sepBy spaces1 parseExpr

parseAtom :: Parser Atom
parseAtom = parseSymbol <|> parseInt

parseSymbol :: Parser Atom
parseSymbol = fmap Symbol parseName

注意到这四个函数是在我们这门语言中属于高阶描述。这解释了为什么 Haskell 执行解析工作这么棒。在定义完高级部分后,我们还需要定义低级别的 parseNameparseInt

我们能在这门语言中用什么字符作为名字呢?用小写的字母、数字和下划线吧,而且名字的第一个字符必须是字母。

parseName :: Parser Name
parseName = do
  c  <- oneOf ['a'..'z']
  cs <- many $ oneOf $ ['a'..'z'] ++ "0123456789" ++ "_"
  pure (c:cs)

整数是一系列数字,数字前面可能有负号 -

parseInt :: Parser Atom
parseInt = do
  sign <- optional $ char '-'
  num  <- many1 $ oneOf "0123456789"
  let result = read $ maybe num (:num) sign of
  pure $ Int result

最后,我们会定义用来运行解析器的函数,返回值可能是一个 Expr 或者是一条错误信息。

runExprParser :: Name -> String -> Either String Expr
runExprParser name str =
  case runParser name str (withSpaces parseExpr) of
    Left (ParseError _ errMsg) -> Left errMsg
    Right (result, _) -> Right result
  • 练习 1 :为第一节中定义的 Program 类型编写一个解析器
  • 练习 2 :用 Applicative 的形式重写 parseName
  • 练习 3 :parseInt 可能出现溢出情况,找到处理它的方法,不要用 read

4、为这门语言实现一个更好看的输出器

我们还想做一件事,将我们的程序以源代码的形式打印出来。这对完善错误信息很有用。

printExpr :: Expr -> String
printExpr = printExpr' False 0

printAtom :: Atom -> String
printAtom = \case
  Symbol s -> s
  Int i -> show i

printExpr' :: Bool -> Int -> Expr -> String
printExpr' doindent level = \case
  ATOM a -> indent (bool 0 level doindent) (printAtom a)
  LIST (e:es) ->
    indent (bool 0 level doindent) $
      concat
        [ "("
        , printExpr' False (level + 1) e
        , bool "\n" "" (null es)
        , intercalate "\n" $ map (printExpr' True (level + 1)) es
        , ")"
        ]

indent :: Int -> String -> String
indent tabs e = concat (replicate tabs "  ") ++ e
  • 练习 :为第一节中定义的 Program 类型编写一个美观的输出器

好,目前为止我们写了近 200 行代码,这些代码一般叫做编译器的前端。我们还要写大概 150 行代码,用来执行三个额外的任务:我们需要根据需求定义一个 JS 的子集,定义一个将我们的语言转译成这个子集的转译器,最后把所有东西整合在一起。开始吧。

5、根据需求定义 JavaScript 的子集

首先,我们要定义将要使用的 JavaScript 的子集:

data JSExpr
  = JSInt Int
  | JSSymbol Name
  | JSBinOp JSBinOp JSExpr JSExpr
  | JSLambda [Name] JSExpr
  | JSFunCall JSExpr [JSExpr]
  | JSReturn JSExpr
    deriving (Eq, Show, Read)

type JSBinOp = String

这个数据类型表示 JavaScript 表达式。我们有两个原子类型 JSIntJSSymbol,它们是由我们这个语言中的 Atom 转译来的,我们用 JSBinOp 来表示二元操作,比如 +*,用 JSLambda 来表示匿名函数,和我们语言中的 lambda expression(lambda 表达式) 一样,我们将会用 JSFunCall 来调用函数,用 let 来引入新名字,用 JSReturn 从函数中返回值,在 JavaScript 中是需要返回值的。

JSExpr 类型是对 JavaScript 表达式的 抽象表示。我们会把自己语言中表达式的抽象表示 Expr 转译成 JavaScript 表达式的抽象表示 JSExpr。但为了实现这个功能,我们需要实现 JSExpr ,并从这个抽象表示中生成 JavaScript 代码。我们将通过递归匹配 JSExpr 实现,将 JS 代码当作 String 来输出。这和我们在 printExpr 中做的基本上是一样的。我们还会追踪元素的作用域,这样我们才可以用合适的方式缩进生成的代码。

printJSOp :: JSBinOp -> String
printJSOp op = op

printJSExpr :: Bool -> Int -> JSExpr -> String
printJSExpr doindent tabs = \case
  JSInt    i     -> show i
  JSSymbol name  -> name
  JSLambda vars expr -> (if doindent then indent tabs else id) $ unlines
    ["function(" ++ intercalate ", " vars ++ ") {"
    ,indent (tabs+1) $ printJSExpr False (tabs+1) expr
    ] ++ indent tabs "}"
  JSBinOp  op e1 e2  -> "(" ++ printJSExpr False tabs e1 ++ " " ++ printJSOp op ++ " " ++ printJSExpr False tabs e2 ++ ")"
  JSFunCall f exprs  -> "(" ++ printJSExpr False tabs f ++ ")(" ++ intercalate ", " (fmap (printJSExpr False tabs) exprs) ++ ")"
  JSReturn expr      -> (if doindent then indent tabs else id) $ "return " ++ printJSExpr False tabs expr ++ ";"
  • 练习 1 :添加 JSProgram 类型,它可以包含多个 JSExpr ,然后创建一个叫做 printJSExprProgram 的函数来生成代码。
  • 练习 2 :添加 JSExpr 的新类型:JSIf,并为其生成代码。

6、实现到我们定义的 JavaScript 子集的代码转译器

我们快做完了。这一节将会创建函数,将 Expr 转译成 JSExpr

基本思想很简单,我们会将 ATOM 转译成 JSSymbol 或者 JSInt,然后会将 LIST 转译成一个函数调用或者转译的特例。

type TransError = String

translateToJS :: Expr -> Either TransError JSExpr
translateToJS = \case
  ATOM (Symbol s) -> pure $ JSSymbol s
  ATOM (Int i)    -> pure $ JSInt i
  LIST xs -> translateList xs

translateList :: [Expr] -> Either TransError JSExpr
translateList = \case
  []     -> Left "translating empty list"
  ATOM (Symbol s):xs
    | Just f <- lookup s builtins ->
      f xs
  f:xs ->
    JSFunCall <$> translateToJS f <*> traverse translateToJS xs

builtins 是一系列要转译的特例,就像 lambadalet。每一种情况都可以获得一系列参数,验证它是否合乎语法规范,然后将其转译成等效的 JSExpr

type Builtin  = [Expr] -> Either TransError JSExpr
type Builtins = [(Name, Builtin)]

builtins :: Builtins
builtins =
  [("lambda", transLambda)
  ,("let", transLet)
  ,("add", transBinOp "add" "+")
  ,("mul", transBinOp "mul" "*")
  ,("sub", transBinOp "sub" "-")
  ,("div", transBinOp "div" "/")
  ,("print", transPrint)
  ]

我们这种情况,会将内建的特殊形式当作特殊的、非第一类的进行对待,因此不可能将它们当作第一类函数。

我们会把 Lambda 表达式转译成一个匿名函数:

transLambda :: [Expr] -> Either TransError JSExpr
transLambda = \case
  [LIST vars, body] -> do
    vars' <- traverse fromSymbol vars
    JSLambda vars' <$> (JSReturn <$> translateToJS body)

  vars ->
    Left $ unlines
      ["Syntax error: unexpected arguments for lambda."
      ,"expecting 2 arguments, the first is the list of vars and the second is the body of the lambda."
      ,"In expression: " ++ show (LIST $ ATOM (Symbol "lambda") : vars)
      ]

fromSymbol :: Expr -> Either String Name
fromSymbol (ATOM (Symbol s)) = Right s
fromSymbol e = Left $ "cannot bind value to non symbol type: " ++ show e

我们会将 let 转译成带有相关名字参数的函数定义,然后带上参数调用函数,因此会在这一作用域中引入变量:

transLet :: [Expr] -> Either TransError JSExpr
transLet = \case
  [LIST binds, body] -> do
    (vars, vals) <- letParams binds
    vars' <- traverse fromSymbol vars
    JSFunCall . JSLambda vars' <$> (JSReturn <$> translateToJS body) <*> traverse translateToJS vals
   where
    letParams :: [Expr] -> Either Error ([Expr],[Expr])
    letParams = \case
      [] -> pure ([],[])
      LIST [x,y] : rest -> ((x:) *** (y:)) <$> letParams rest
      x : _ -> Left ("Unexpected argument in let list in expression:\n" ++ printExpr x)

  vars ->
    Left $ unlines
      ["Syntax error: unexpected arguments for let."
      ,"expecting 2 arguments, the first is the list of var/val pairs and the second is the let body."
      ,"In expression:\n" ++ printExpr (LIST $ ATOM (Symbol "let") : vars)
      ]

我们会将可以在多个参数之间执行的操作符转译成一系列二元操作符。比如:(add 1 2 3) 将会变成 1 + (2 + 3)

transBinOp :: Name -> Name -> [Expr] -> Either TransError JSExpr
transBinOp f _ []   = Left $ "Syntax error: '" ++ f ++ "' expected at least 1 argument, got: 0"
transBinOp _ _ [x]  = translateToJS x
transBinOp _ f list = foldl1 (JSBinOp f) <$> traverse translateToJS list

然后我们会将 print 转换成对 console.log 的调用。

transPrint :: [Expr] -> Either TransError JSExpr
transPrint [expr] = JSFunCall (JSSymbol "console.log") . (:[]) <$> translateToJS expr
transPrint xs     = Left $ "Syntax error. print expected 1 arguments, got: " ++ show (length xs)

注意,如果我们将这些代码当作 Expr 的特例进行解析,那我们就可能会跳过语法验证。

  • 练习 1 :将 Program 转译成 JSProgram
  • 练习 2 :为 if Expr Expr Expr 添加一个特例,并将它转译成你在上一次练习中实现的 JSIf 条件语句。

7、把所有东西整合到一起

最终,我们将会把所有东西整合到一起。我们会:

  1. 读取文件
  2. 将文件解析成 Expr
  3. 将文件转译成 JSExpr
  4. 将 JavaScript 代码发送到标准输出流

我们还会启用一些用于测试的标志位:

  • --e 将进行解析并打印出表达式的抽象表示(Expr
  • --pp 将进行解析,美化输出
  • --jse 将进行解析、转译、并打印出生成的 JS 表达式(JSExpr)的抽象表示
  • --ppc 将进行解析,美化输出并进行编译
main :: IO ()
main = getArgs >>= \case
  [file] ->
    printCompile =<< readFile file
  ["--e",file] ->
    either putStrLn print . runExprParser "--e" =<< readFile file
  ["--pp",file] ->
    either putStrLn (putStrLn . printExpr) . runExprParser "--pp" =<< readFile file
  ["--jse",file] ->
    either print (either putStrLn print . translateToJS) . runExprParser "--jse" =<< readFile file
  ["--ppc",file] ->
    either putStrLn (either putStrLn putStrLn) . fmap (compile . printExpr) . runExprParser "--ppc" =<< readFile file
  _ ->
    putStrLn $ unlines
      ["Usage: runghc Main.hs [ --e, --pp, --jse, --ppc ] <filename>"
      ,"--e     print the Expr"
      ,"--pp    pretty print Expr"
      ,"--jse   print the JSExpr"
      ,"--ppc   pretty print Expr and then compile"
      ]

printCompile :: String -> IO ()
printCompile = either putStrLn putStrLn . compile

compile :: String -> Either Error String
compile str = printJSExpr False 0 <$> (translateToJS =<< runExprParser "compile" str)

大功告成。将自己的语言编译到 JS 子集的编译器已经完成了。再说一次,你可以在 这里 看到完整的源文件。

用我们的编译器运行第一节的示例,产生的 JavaScript 代码如下:

$ runhaskell Lisp.hs example.lsp
(function(compose, square, add1) {
  return (console.log)(((compose)(square, add1))(5));
})(function(f, g) {
  return function(x) {
    return (f)((g)(x));
  };
}, function(x) {
  return (x * x);
}, function(x) {
  return (x + 1);
})

如果你在自己电脑上安装了 node.js,你可以用以下命令运行这段代码:

$ runhaskell Lisp.hs example.lsp | node -p
36
undefined
  • 最终练习 : 编译有多个表达式的程序而非仅编译一个表达式。

via: https://gilmi.me/blog/post/2016/10/14/lisp-to-js

作者:Gil Mizrahi 选题:oska874 译者:BriFuture 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

很多 Web 开发者都希望做出加载速度很快的网页。在移动设备浏览占比越来越大的背景下,使用响应式设计使得网站在小屏幕下看起来更漂亮只是其中一个方面。Browser Calories 可以展示网页的加载时间 —— 这不单单关系到用户,还会影响到通过加载速度来进行评级的搜索引擎。这个系列的文章介绍了如何使用 Fedora 提供的工具来给网页“瘦身”。

准备工作

在你开始缩减网页之前,你需要明确核心问题所在。为此,你可以使用 Browserdiet. 这是一个浏览器插件,适用于 Firefox、Opera、 Chrome 和其它浏览器。它会对打开的网页进行性能分析,这样你就可以知道应该从哪里入手来缩减网页。

然后,你需要一些用来处理的页面。下面的例子是针对 getferoda.org 的测试截图。一开始,它看起来非常简单,也符合响应式设计。

Browser Diet - getfedora.org 的评分

然而,BroserDiet 的网页分析表明,这个网页需要加载 1.8MB 的文件。所以,我们现在有活干了!

Web 优化

网页中包含 281 KB 的 JavaScript 文件、203 KB 的 CSS 文件,还有 1.2 MB 的图片。我们先从最严重的问题 —— 图片开始入手。为了解决问题,你需要的工具集有 GIMP、ImageMagick 和 optipng. 你可以使用如下命令轻松安装它们:

sudo dnf install gimp imagemagick optipng

比如,我们先拿到这个 6.4 KB 的文件

首先,使用 file 命令来获取这张图片的一些基本信息:

$ file cinnamon.png
cinnamon.png: PNG image data, 60 x 60, 8-bit/color RGBA, non-interlaced

这张只由白色和灰色构成的图片使用 8 位 / RGBA 模式来存储。这种方式并没有那么高效。

使用 GIMP,你可以为这张图片设置一个更合适的颜色模式。在 GIMP 中打开 cinnamon.png。然后,在“图片 > 模式”菜单中将其设置为“灰度模式”。将这张图片以 PNG 格式导出。导出时使用压缩因子 9,导出对话框中的其它配置均使用默认选项。

$ file cinnamon.png
cinnamon.png: PNG image data, 60 x 60, 8-bit gray+alpha, non-interlaced

输出显示,现在这个文件现在处于 8 位 / 灰阶 + aplha 模式。文件大小从 6.4 KB 缩小到了 2.8 KB. 这已经是原来大小的 43.75% 了。但是,我们能做的还有很多!

你可以使用 ImageMagick 工具来查看这张图片的更多信息。

$ identify cinnamon2.png
cinnamon.png PNG 60x60 60x60+0+0 8-bit Grayscale Gray 2831B 0.000u 0:00.000

它告诉你,这个文件的大小为 2831 字节。我们回到 GIMP,重新导出文件。在导出对话框中,取消存储时间戳和 alpha 通道色值,来让文件更小一点。现在文件输出显示:

$ identify cinnamon.png
cinnamon.png PNG 60x60 60x60+0+0 8-bit Grayscale Gray 2798B 0.000u 0:00.000

下面,用 optipng 来无损优化你的 PNG 图片。具有相似功能的工具有很多,包括 advdef(这是 advancecomp 的一部分),pngquantpngcrush

对你的文件运行 optipng。 注意,这个操作会覆盖你的原文件:

$ optipng -o7 cinnamon.png
** Processing: cinnamon.png
60x60 pixels, 2x8 bits/pixel, grayscale+alpha
Reducing image to 8 bits/pixel, grayscale
Input IDAT size = 2720 bytes
Input file size = 2812 bytes

Trying:
 zc = 9 zm = 8 zs = 0 f = 0 IDAT size = 1922
 zc = 9 zm = 8 zs = 1 f = 0 IDAT size = 1920

Selecting parameters:
 zc = 9 zm = 8 zs = 1 f = 0 IDAT size = 1920

Output IDAT size = 1920 bytes (800 bytes decrease)
Output file size = 2012 bytes (800 bytes = 28.45% decrease)

-o7 选项处理起来最慢,但最终效果最好。于是你又将文件缩小了 800 字节,现在它只有 2012 字节了。

要压缩文件夹下的所有 PNG,可以使用这个命令:

$ optipng -o7 -dir=<directory> *.png

-dir 选项用来指定输出文件夹。如果不加这个选项,optipng 会覆盖原文件。

选择正确的文件格式

当涉及到在互联网中使用的图片时,你可以选择:

JPG-LS 和 JPG 2000 没有得到广泛使用。只有一部分数码相机支持这些格式,所以我们可以忽略它们。aPNG 是动态的 PNG 格式,也没有广泛使用。

可以通过更改压缩率或者使用其它文件格式来节省下更多字节。我们无法在 GIMP 中应用第一种方法,因为现在的图片已经使用了最高的压缩率了。因为我们的图片中不再包含 aplha 通道,你可以使用 JPG 类型来替代 PNG。 现在,使用默认值:90% 质量 —— 你可以将它减小至 85%,但这样会导致可见的叠影。这样又省下一些字节:

$ identify cinnamon.jpg
cinnamon.jpg JPEG 60x60 60x60+0+0 8-bit sRGB 2676B 0.000u 0:00.000

只将这张图转成正确的色域,并使用 JPG 作为文件格式,就可以将它从 23 KB 缩小到 12.3 KB,减少了近 50%.

PNG vs JPG: 质量和压缩率

那么,剩下的文件我们要怎么办呢?除了 Fedora “风味”图标和四个特性图标之外,此方法适用于所有其他图片。我们能够处理的图片都有一个白色的背景。

PNG 和 JPG 的一个主要区别在于,JPG 没有 alpha 通道。所以,它没有透明度选项。如果你使用 JPG 并为它添加白色背景,你可以将文件从 40.7 KB 缩小至 28.3 KB.

现在又有了四个可以处理的图片:背景图。对于灰色背景,你可以再次使用灰阶模式。对更大的图片,我们就可以节省下更多的空间。它从 216.2 KB 缩小到了 51 KB —— 基本上只有原图的 25% 了。整体下来,你把这些图片从 481.1 KB 缩小到了 191.5 KB —— 只有一开始的 39.8%.

质量 vs 大小

PNG 和 JPG 的另外一个区别在于质量。PNG 是一种无损压缩光栅图形格式。但是 JPG 虽然使用压缩来缩小体积,可是这会影响到质量。不过,这并不意味着你不应该使用 JPG,只是你需要在文件大小和质量中找到一个平衡。

成就

这就是第一部分的结尾了。在使用上述技术后,得到的结果如下:

你将一开始 1.2 MB 的图片体积缩小到了 488.9 KB. 只需通过 optipng 进行优化,就可以达到之前体积的三分之一。这可能使得页面更快地加载。不过,要是使用蜗牛到超音速来对比,这个速度还没到达赛车的速度呢!

最后,你可以在 Google Insights 中查看结果,例如:

在移动端部分,这个页面的得分提升了 10 分,但它依然处于“中等”水平。对于桌面端,结果看起来完全不同,从 62/100 分提升至了 91/100 分,等级也达到了“好”的水平。如我们之前所说的,这个测试并不意味着我们的工作就做完了。通过参考这些分数可以让你朝着正确的方向前进。请记住,你正在为用户体验来进行优化,而不是搜索引擎。


via: https://fedoramagazine.org/design-faster-web-pages-part-1-image-compression/

作者:Sirko Kemter 选题:lujun9972 译者:StdioA 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

在过去的几个月里,我一直在使用 React 和 React-Native。我已经发布了两个作为产品的应用, Kiven Aa(React)和 Pollen Chat(React Native)。当我开始学习 React 时,我找了一些不仅仅是教我如何用 React 写应用的东西(一个博客,一个视频,一个课程,等等),我也想让它帮我做好面试准备。

我发现的大部分资料都集中在某一单一方面上。所以,这篇文章针对的是那些希望理论与实践完美结合的观众。我会告诉你一些理论,以便你了解幕后发生的事情,然后我会向你展示如何编写一些 React.js 代码。

如果你更喜欢视频形式,我在 YouTube 上传了整个课程,请去看看。

让我们开始……

React.js 是一个用于构建用户界面的 JavaScript 库

你可以构建各种单页应用程序。例如,你希望在用户界面上实时显示变化的聊天软件和电子商务门户。

一切都是组件

React 应用由组件组成,数量繁多且互相嵌套。你或许会问:”可什么是组件呢?“

组件是可重用的代码段,它定义了某些功能在 UI 上的外观和行为。 比如,按钮就是一个组件。

让我们看看下面的计算器,当你尝试计算 2 + 2 = 4 -1 = 3(简单的数学题)时,你会在 Google 上看到这个计算器。

红色标记表示组件

如上图所示,这个计算器有很多区域,比如展示窗口和数字键盘。所有这些都可以是许多单独的组件或一个巨大的组件。这取决于在 React 中分解和抽象出事物的程度。你为所有这些组件分别编写代码,然后合并这些组件到一个容器中,而这个容器又是一个 React 组件。这样你就可以创建可重用的组件,最终的应用将是一组协同工作的单独组件。

以下是一个你践行了以上原则并可以用 React 编写计算器的方法。

<Calculator>
  <DisplayWindow />
  <NumPad>
    <Key number={1}/>
    <Key number={2}/>
    .
    .
    .
    <Key number={9}/>
  </NumPad>
</Calculator>

没错!它看起来像HTML代码,然而并不是。我们将在后面的部分中详细探讨它。

设置我们的 Playground

这篇教程专注于 React 的基础部分。它没有偏向 Web 或 React Native(开发移动应用)。所以,我们会用一个在线编辑器,这样可以在学习 React 能做什么之前避免 web 或 native 的具体配置。

我已经为读者在 codepen.io 设置好了开发环境。只需点开该链接并且阅读 HTML 和 JavaScript 中的所有注释。

控制组件

我们已经了解到 React 应用是各种组件的集合,结构为嵌套树。因此,我们需要某种机制来将数据从一个组件传递到另一个组件。

进入 “props”

我们可以使用 props 对象将任意数据传递给我们的组件。 React 中的每个组件都会获取 props 对象。在学习如何使用 props 之前,让我们学习函数式组件。

a) 函数式组件

在 React 中,一个函数式组件通过 props 对象使用你传递给它的任意数据。它返回一个对象,该对象描述了 React 应渲染的 UI。函数式组件也称为无状态组件。

让我们编写第一个函数式组件。

function Hello(props) {
  return <div>{props.name}</div>
}

就这么简单。我们只是将 props 作为参数传递给了一个普通的 JavaScript 函数并且有返回值。嗯?返回了什么?那个 <div>{props.name}</div>。它是 JSX(JavaScript Extended)。我们将在后面的部分中详细了解它。

上面这个函数将在浏览器中渲染出以下 HTML。

<!-- If the "props" object is: {name: 'rajat'} -->
<div>
  rajat
</div>
阅读以下有关 JSX 的部分,这一部分解释了如何从我们的 JSX 代码中得到这段 HTML 。

如何在 React 应用中使用这个函数式组件? 很高兴你问了! 它就像下面这么简单。

<Hello name='rajat' age={26}/>

属性 name 在上面的代码中变成了 Hello 组件里的 props.name ,属性 age 变成了 props.age

记住! 你可以将一个 React 组件嵌套在其他 React 组件中。

让我们在 codepen playground 使用 Hello 组件。用我们的 Hello 组件替换 ReactDOM.render() 中的 div,并在底部窗口中查看更改。

function Hello(props) {
  return <div>{props.name}</div>
}

ReactDOM.render(<Hello name="rajat"/>, document.getElementById('root'));
但是如果你的组件有一些内部状态怎么办?例如,像下面的计数器组件一样,它有一个内部计数变量,它在 +- 键按下时发生变化。

具有内部状态的 React 组件

b) 基于类的组件

基于类的组件有一个额外属性 state ,你可以用它存放组件的私有数据。我们可以用 class 表示法重写我们的 Hello 。由于这些组件具有状态,因此这些组件也称为有状态组件。

class Counter extends React.Component {
  // this method should be present in your component
  render() {
    return (
      <div>
        {this.props.name}
      </div>
    );
  }
}

我们继承了 React 库的 React.Component 类以在 React 中创建基于类的组件。在这里了解更多有关 JavaScript 类的东西。

render() 方法必须存在于你的类中,因为 React 会查找此方法,用以了解它应在屏幕上渲染的 UI。为了使用这种内部状态,我们首先要在组件

要使用这种内部状态,我们首先必须按以下方式初始化组件类的构造函数中的状态对象。

class Counter extends React.Component {
  constructor() {
    super();
    
    // define the internal state of the component
    this.state = {name: 'rajat'}
  }
  
  render() {
    return (
      <div>
        {this.state.name}
      </div>
    );
  }
}

// Usage:
// In your react app: <Counter />

类似地,可以使用 this.props 对象在我们基于类的组件内访问 props

要设置 state,请使用 React.ComponentsetState()。 在本教程的最后一部分中,我们将看到一个这样的例子。

提示:永远不要在 render() 函数中调用 setState(),因为 setState 会导致组件重新渲染,这将导致无限循环。

基于类的组件具有可选属性 “state”。

除了 state 以外,基于类的组件有一些声明周期方法比如 componentWillMount()。你可以利用这些去做初始化 state这样的事, 可是那将超出这篇文章的范畴。

JSX

JSX 是 JavaScript Extended 的缩写,它是一种编写 React 组件的方法。使用 JSX,你可以在类 XML 标签中获得 JavaScript 的全部力量。

你把 JavaScript 表达式放在 {} 里。下面是一些有效的 JSX 例子。

 <button disabled={true}>Press me!</button>
 <button disabled={true}>Press me {3+1} times!</button>;
 <div className='container'><Hello /></div>

它的工作方式是你编写 JSX 来描述你的 UI 应该是什么样子。像 Babel 这样的转码器将这些代码转换为一堆 React.createElement() 调用。然后,React 库使用这些 React.createElement() 调用来构造 DOM 元素的树状结构。对于 React 的网页视图或 React Native 的 Native 视图,它将保存在内存中。

React 接着会计算它如何在展示给用户的 UI 的内存中有效地模仿这个树。此过程称为 reconciliation。完成计算后,React 会对屏幕上的真正 UI 进行更改。

React 如何将你的 JSX 转化为描述应用 UI 的树。

你可以使用 Babel 的在线 REPL 查看当你写一些 JSX 的时候,React 的真正输出。

使用Babel REPL 转换 JSX 为普通 JavaScript

由于 JSX 只是 React.createElement() 调用的语法糖,因此可以在没有 JSX 的情况下使用 React。

现在我们了解了所有的概念,所以我们已经准备好编写我们之前看到之前的 GIF 图中的计数器组件。

代码如下,我希望你已经知道了如何在我们的 playground 上渲染它。

class Counter extends React.Component {
  constructor(props) {
    super(props);
    
    this.state = {count: this.props.start || 0}
    
    // the following bindings are necessary to make `this` work in the callback
    this.inc = this.inc.bind(this);
    this.dec = this.dec.bind(this);
  }
  
  inc() {
    this.setState({
      count: this.state.count + 1
    });
  }
  
  dec() {
    this.setState({
      count: this.state.count - 1
    });
  }
  
  render() {
    return (
      <div>
        <button onClick={this.inc}>+</button>
        <button onClick={this.dec}>-</button>
        <div>{this.state.count}</div>
      </div>
    );
  }
}

以下是关于上述代码的一些重点。

  1. JSX 使用 驼峰命名 ,所以 button 的 属性是 onClick,不是我们在HTML中用的 onclick
  2. 绑定 this 是必要的,以便在回调时工作。 请参阅上面代码中的第8行和第9行。

最终的交互式代码位于此处

有了这个,我们已经到了 React 速成课程的结束。我希望我已经阐明了 React 如何工作,以及如何使用 React 来构建更大的应用程序,使用更小和可重用的组件。


via: https://medium.freecodecamp.org/rock-solid-react-js-foundations-a-beginners-guide-c45c93f5a923

作者:Rajat Saxena 译者:GraveAccent 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

你编写或维护过有意义的 bash 脚本吗?如果回答是,那么你可能希望它们以标准且健壮的方式接收命令行参数。Fedora 最近得到了一个很好的附加组件,它可以帮助你生成更好的脚本。不用担心,它不会花费你很多时间或精力。

为什么需要 Argbash?

Bash 是一种解释性的命令行语言,没有标准库。因此,如果你编写 bash 脚本并希望命令行界面符合 POSIXGNU CLI 标准,那么你一般只有两种选择:

  1. 直接编写为脚本量身定制的参数解析功能(可使用内置的 getopts)。
  2. 使用外部 bash 模块。

第一个选项看起来非常愚蠢,因为正确实现接口并非易事。但是,从 Stack OverflowBash Hackers wiki 的各种站点上,它却被认为是最佳选择。

第二个选项看起来更聪明,但使用模块有它自己的问题。最大的问题是你必须将其代码与脚本捆绑在一起。这可能意味着:

  • 要么,你将库作为单独的文件分发
  • 或者,在脚本的开头包含库代码

有两个文件而不是一个是愚蠢的;但采用一个文件的话,会让一堆上千行的复杂代码污染了你的脚本。

这是 Argbash 项目诞生的主要原因。Argbash 是一个代码生成器,它为你的脚本生成一个量身定制的解析库。与其他 bash 模块的通用代码不同,它生成你的脚本所需的最少代码。此外,如果你不需要 100% 符合那些 CLI 标准的话,你可以生成更简单的代码。

示例

分析

假设你要实现一个脚本,它可以在终端窗口中绘制条形图,你可以通过重复一个字符选定的次数来做到这一点。这意味着你需要从命令行获取以下信息:

  • 哪个字符是组成该行的元素。如果未指定,使用破折号 -。 在命令行上,这是个单值定位参数 character,其默认值为 -。(LCTT 译注:定位参数是指确定位置的参数,此处 character 需是命令行的第一个参数)
  • 直线的长度。如果未指定,会选择 80。 这是一个单值可选参数 length,默认值为 80
  • Verbose 模式(用于调试)。 这是一个布尔型参数 verbose,默认情况下关闭。

由于脚本的主体非常简单,因此本文主要关注从命令行获取用户的输入到合适的脚本变量。Argbash 生成的代码会将参数解析结果保存到 shell 变量 _arg_character_arg_length_arg_verbose 当中。

执行

接下来,你还需要 argbash-initargbash bash 脚本,它们是 argbash 包的一部分。因此,运行以下命令:

sudo dnf install argbash

然后,使用 argbash-init 来为 argbash 生成模板,它会生成可执行脚本。你需要三个参数:一个名为 character 的定位参数,一个可选的 length 参数以及一个可选的布尔 verbose。将这些传递给 argbash-init,然后将输出传递给 argbash : `argbash-init --pos character --opt length --opt-bool verbose script-template.sh
argbash script-template.sh -o script
./script`

看到帮助信息了吗?看起来该脚本不知道字符参数的默认选项。因此,看一下 Argbash API,然后通过编辑脚本的模板部分来解决问题:

# ...
# ARG_OPTIONAL_SINGLE([length],[l],[Length of the line],[80])
# ARG_OPTIONAL_BOOLEAN([verbose],[V],[Debug mode])
# ARG_POSITIONAL_SINGLE([character],[The element of the line],[-])
# ARG_HELP([The line drawer])
# ...

Argbash 非常智能,它试图让每个生成的脚本都成为自己的模板,这意味着你不需要存储源模版以供进一步使用,你也不要丢掉生成的 bash 脚本。现在,尝试重新生成如你所预期的下一个线条绘图脚本:

argbash script -o script
./script

如你所见,一切正常。剩下要做的唯一事情就是完成线条绘图功能。

结论

你可能会发现包含解析代码的部分很长,但考虑到它允许你以 ./script.sh x -Vl50 的方式调用,并且能像 ./script -V -l 50 x 一样工作。确实需要一些代码才能做到这一点。

但是,通过调用 argbash-init 并将参数 -mode 设置为 minimal,你可以平衡生成的代码复杂度和解析能力,而转向更简单的代码。这个选项将脚本的大小减少了大约 20 行,这相当于生成的解析代码大小减少了大约 25%。另一方面,full 模式使脚本更加智能。

如果你想要检查生成的代码,请给 argbash 提供参数 -commented,它会将注释放入解析代码中,从而揭示各个部分背后的意图。与其他参数解析库相比较,如 shflags, argsparsebash-modules/arguments,你将看到 Argbash 强大的简单性。如果出现了严重的错误,你需要快速修复解析功能中的一个故障,Argbash 也允许你这样做。

由于你很有可能是 Fedora 用户,因此你可以享受从官方仓库安装命令行 Argbash 的便利。不过,也有一个在线解析代码生成器服务可以使用。此外,如果你在服务器上使用 Docker 工作,你可以试试 Argbash Docker 镜像

这样你可以让你的脚本具有令用户满意的命令行界面。Argbash 随时为你提供帮助,你只需付出很少的努力。


via: https://fedoramagazine.org/improve-bash-scripts-argbash/

作者:Matěj Týč 译者:MjSeven 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出