乐者为王

Do one thing, and do it well.

ANTLR 4权威参考读书笔记(11)

为制作语言应用,我们必须为每个输入短语或子短语执行一些适当的代码,那样做最简单的方法是操作由语法分析器自动创建的语法分析树。

早些时候我们已经学习了词法分析器处理字符和把记号传递给语法分析器,然后语法分析器分析语法和创建语法分析树的相关知识。对应的ANTLR类分别是CharStream、Lexer、Token、Parser和ParseTree。连接词法分析器和语法分析器的管道被称为TokenStream。下图说明了这些类型的对象如何连接到内存中其它的对象。

basic-data-structure

这些ANTLR数据结构共享尽可能多的数据以便节省内存的需要。上图显示在语法分析树中的叶子(记号)节点含有在记号流中记号的点。记号记录开始和结束字符在CharStream中的索引,而不是复制子串。这里没有与空格字符有关的记号,因为我们假设我们的词法分析器扔掉了空格。

下图显示的是ParseTree的子类RuleNode和TerminalNode以及它们所对应的子树根节点和叶子节点。RuleNode包含有方法如getChild()和getParent()等,但RuleNode并不专属于特定语法所有。为了能更好地访问在特定节点中的元素,ANTLR为每个规则生成一个RuleNode的子类。下图显示了赋值语句例子的子树根节点的特定类,它们是StatContext、AssignContext和ExprContext:

parse-tree-node

它们记录了通过规则对短语识别的每件事情,所以被称为上下文对象。每个上下文对象都知道被识别短语的开始和结束记号以及提供对所有短语的元素的访问。例如,AssignContext提供方法ID()和expr()去访问标志符节点和表达式子树。

给出了具体类型的描述后,我们可以手工编写代码去执行树的深度优先遍历。当我们发现和完成节点时我们可以执行任何我们想要的动作。典型的动作是诸如计算结果,更新数据结构,或者生成输出。相比每次为每个应用编写同样的树遍历样板代码,使用ANTLR自动生成的树遍历机制更方便、更容易。

ANTLR 4权威参考读书笔记(10)

一个模棱两可的短语或句子通常是指它有不止一种解释。换句话说,短语或句子能匹配不止一种语法结构。要解释或转换一个短语,程序必须要能唯一地确认它的含义,这意味着我们必须提供无歧义的语法,以便生成的语法分析器能用明确的一个方法匹配每个输入短语。

在这里,让我们展示一些有歧义的语法以便让二义性的概念更具体。如果你以后在构建语法时陷入二义性,你可以参考本节的内容。

一些明显有歧义的语法:

1
2
3
4
5
6
7
assign
    : ID '=' expr    // 匹配一个赋值语句,例如f()
    | ID '=' expr    // 前面选项的精确复制
    ;

expr
    : INT ;

大多数时候二义性是不明显的,就像下面的语法,它能通过规则stat的两个选项匹配函数调用:

1
2
3
4
5
6
7
8
9
stat
    : expr          // 表达式语句
    | ID '(' ')'    // 函数调用语句
    ;

expr
    : ID '(' ')'
    | INT
    ;

这里是输入f()的两个解释,从规则stat开始:

fn-parse-tree

左边的语法分析树显示f()匹配规则expr。右边的语法分析树显示f()匹配规则stat的第二个选项。

因为大部分语言它们的语法都被设计成无歧义的,有歧义的语法类似于编程缺陷,所以我们需要识别语法并为每个输入短语提交单一选择给语法分析器。如果语法分析器发现一个有歧义的短语,它必须选一个可行的选项。ANTLR通过选择涉及决定的第一个选项解决二义性。在本例中,语法分析器将选择与左边的语法分析树有关的f()的解释。

二义性可以发生在词法分析器中也能发生在语法分析器中,但ANTLR可以自动地解决它们。ANTLR通过使输入字符串和语法中第一个指定的词法规则匹配来解决词法二义性。为了明白这是如何工作的,让我们看看对大部分编程语言都很普遍的二义性:在关键字和标志符规则中的二义性。关键字begin(后面有个非字母)也是标志符,至少词法上,因此词法分析器可以匹配b-e-g-i-n到两者中的任何一个规则。

1
2
BEGIN : 'begin' ;    // 匹配b-e-g-i-n序列,即把二义性解析为BEGIN
ID    : [a-z]+ ;     // 匹配一个或多个任意小写字母

注意,词法分析器会试着为每个记号尽可能匹配最长的字符串,这意味着输入beginner将仅仅匹配规则ID。词法分析器不会把beginner匹配成BEGIN然后ID匹配输入ner。

有时候语言的语法就明显有歧义,没有任何的语法重组能改变这个事实。例如,算术表达式的自然语法可以用两种方式解释像1+2*3这样的输入,要么从左到右执行运算符,要么像大部分语言那样按优先级顺序。

C语言展示了另一种二义性,但我们可以使用上下文信息比如标志符如何被定义来解决它。考虑代码片段i*j;。在语法上,它看起来像是一个表达式,但它的含义或者语义依赖i是类型名还是变量。如果i是类型名,那么这个片段不是表达式,而是一个声明为指向类型i的指针变量j。

ANTLR 4权威参考读书笔记(9)

  • predication 断定
  • predict 预判
  • lookahead 预读

ANTLR工具根据语法规则,例如我们刚才看到的assign,生成递归下降语法分析器。递归下降语法分析器只是递归方法的一个集合,每个规则一个方法。下降这个术语指的是分析从语法分析树的根开始向着叶子进行(记号)。我们首先调用的规则,即stat符号,成为语法分析树的根。那也就意味着对ANTLR 4权威参考读书笔记(8)中的语法分析树来说需要调用方法stat()。这类分析更通用的术语是自顶向下分析:递归下降语法分析器仅仅是自顶向下语法分析器实现的一种。

要了解递归下降语法分析器是什么样子,可以看看下面ANTLR为规则assign生成的方法(稍微做过整理):

1
2
3
4
5
6
// assign : ID '=' expr ;
void assign() {    // 根据规则assign生成的方法
    match(ID);     // 比较ID和当前输入符号然后消费
    match('=');
    expr();        // 通过调用expr()匹配表达式
}

递归下降语法分析器最酷的部分是通过跟踪调用方法stat()、assign()和expr()绘制出的调用关系图反映了内部的语法分析树节点。match()的调用对应语法分析树叶子。为了在一个手工构建的语法分析器中手动构建一颗语法分析树,我们需要在每个规则方法的开始处插入“添加新子树根”操作,以及给match()一个“添加新叶子节点”操作。

方法assign()只是检查确保所有必要的记号存在且以正确的顺序。当语法分析器进入assign()时,它不必在多个选项之间进行选择。选项是规则定义右边的选择之一。例如,调用assign的stat规则可能有其它类型的语句。

1
2
3
4
5
6
7
/** 匹配起始于当前输入位置的任何语句 */
stat
    : assign    // 第一个选项('|'是选项分隔符)
    | ifstat    // 第二个选项
    | whilestat
    ...
    ;

stat的分析规则看起来像一条switch语句:

1
2
3
4
5
6
7
8
9
void stat() {
    switch ( «current input token» ) {
        CASE ID : assign(); break;
        CASE IF : ifstat(); break;    // IF是关键字'if'的记号类型
        CASE WHILE : whilestat(); break;
        ...
        default : «raise no viable alternative exception»
    }
}

方法stat()必须通过检查下一个输入记号作出分析决定或断定。分析决定预判哪一个选项将会成功。在本例中,当看到WHILE关键字时会预判是规则stat的第三个选项。规则方法stat()然后就会调用whilestat()。你以前可能听说过术语预读记号,那只是下一个输入记号。预读记号可以是语法分析器在匹配和消费输入记号之前嗅探的任何记号。

有时候,语法分析器需要一些预读记号去预判哪个选项会成功。它甚至必须考虑从当前位置直到文件结尾的所有的记号。ANTLR默默地为你处理所有的这些事情,但是对决策过程有个基本的了解是有帮助的,可以让调试生成的语法分析器更容易。

为更好地理解分析决定,想象有一个单一入口和单一出口的迷宫,有单词写在地板上。每个沿着从入口到出口路径的单词序列表示一个句子。迷宫的结构与定义一门语言的语法规则类似。为测试一个句子在一门语言中的成员身份,我们在穿越迷宫时把句子的单词和沿着地板的单词作比较。如果通过句子的单词我们能到达出口,那么句子就是有效的。

为了通过迷宫,我们必须在每个岔口选择一条有效路径,正如我们必须在语法分析器中选择选项一样。通过把我们句子中下一个单词(们)和沿着来自每个岔口的每条路径上可见的单词比较,我们做出决定该走哪条路。我们能从岔口看到的单词与预读记号类似。当每条路径以唯一的单词开始时决定是相当容易的。在规则stat中,每个选项从唯一的记号开始,因此stat()可以通过查看第一个预读记号识别选项。

当单词从一个岔口重叠部分开始每条路径时,语法分析器需要继续往前看,扫描可以识别选项的单词。ANTLR可以根据需要为每个分析决定自动上下调节预读数量。如果预读的结果是多条同样的到出口的路径,那说明当前的输入短语有多种解释,这会导致二义性。

贫富差距精简版

英文原文:http://paulgraham.com/sim.html

2016年1月

正如经常发生的那样——当说一些有争议的话题时,已经有我刚写的一篇贫富差距的文章的一些非常新奇的解释。我想这可能有助于澄清问题,如果我试着写一个简简单单、没有误解的版本。

很多人谈论贫富差距。几乎所有人都说贫富差距增大是坏的,贫富差距缩小是好的。

但是贫富差距本身并非坏事。它有多方面的原因。很多是坏的,但有些是好的。

例如,高入狱率和税收漏洞是增大贫富差距的不良因素。

但是创业同样地增大贫富差距。创业成功的创始人最终会得到值很多钱的股票。

而且不像高入狱率和税收漏洞,创业整体上是好的。

既然贫富差距本身并非坏事,我们就不应该攻击它。相反,我们应该攻击那些造成贫富差距的不良因素。

例如,我们应该攻击贫穷,而不是攻击贫富差距。

攻击贫富差距是双重错误。它会损害好的和坏的原因。但更糟的是,这是一个攻击坏的原因的无效方式。

除非我们直接攻击坏的贫富差距原因,否则我们不能做好解决它们的工作。

但是,如果我们解决了所有坏的贫富差距原因,我们依然将增加贫富差距的水平,因为正在增长的技术力量。

ANTLR 4权威参考读书笔记(8)

  • sentence 句子

一门语言由有效的句子组成,一个句子由短语组成,一个短语由子短语和词汇符号组成。要实现一门语言,我们必须构建一个能读取句子以及对发现的短语和输入符号作出适当反应的应用。

这样的应用必须能够识别特定语言的所有有效的句子、短语和子短语。识别一个短语意味着我们能够确定短语的各种组件并能指出它与其它短语的区别。例如,我们把输入sp=100识别为赋值语句,这就意味着我们知道sp是赋值目标以及100是要存储的值。识别赋值语句sp=100也意味着应用认为它是明显不同于,比如说,a+b语句的。在识别后,应用将执行适当的操作,例如performAssignment("sp", 100)或者translateAssignment("sp", 100)。

识别语言的程序被称为语法分析器。语法指代控制语言成员的规则,每条规则都表示一个短语的结构。为了更容易地实现识别语言的程序,通常我们会把识别语言的语法分析拆解成两个相似但不同的任务或阶段。

把字符组成单词或符号(记号)的过程被称为词法分析或简单标记化。我们把标记输入的程序称为词法分析器。词法分析器能把相关的记号组成记号类型,例如INT(整数)、ID(标志符)、FLOAT(浮点数)等。当语法分析器只关心类型的时候,词法分析器会把词汇符号组成类型,而不是单独的符号。记号至少包含两块信息:记号类型(确定词法结构)和匹配记号的文本。

第二阶段是真正的语法分析器,它使用这些记号去识别句子结构,在本例中是赋值语句。默认情况下,ANTLR生成的语法分析器会构建一个称为语法分析树或语法树的数据结构,它记录语法分析器如何识别输入句子的结构和它的组件短语。下图阐明了语言识别器的基本数据流:

basic-data-flow

语法分析树的内部节点是分组和确认它们子节点的短语名字。根节点是最抽象的短语名字,在本例中是stat(“statement”的缩写)。语法分析树的叶子节点永远是输入记号。

通过生成语法分析树,语法分析器给应用的其余部分提供了方便的数据结构,它们含有关于语法分析器如何把符号组成短语的完整信息。树是非常容易处理的,并且也能被程序员很好的理解。更好的是,语法分析器能自动地生成语法分析树。

通过操作语法分析树,需要识别相同语言的多个应用能复用同一个语法分析器。当然,你也可以选择直接在语法中嵌入特定应用的代码片段,这是语法分析器生成器传统的做法。ANTLR v4仍然允许这样做,但是语法分析树有助于更简洁更解耦的设计。

语法分析树对于需要多次树遍历的转换也是非常有用的,因为在计算依赖关系的阶段通常会需要前一个阶段的信息。相比于在每个阶段都要准备输入字符,我们只需要遍历语法分析树多次,更具有效率。

我们使用一套规则指定短语:语法分析树子树根节点对应于语法规则名。这里的语法规则对应于上图中assign子树的第一层:

1
assign : ID '=' expr ;    // 匹配赋值语句像"sp = 100"

明白ANTLR如何把这些规则转换为人类可读的语法分析代码是使用和调试语法的基础,可以让我们更深入地挖掘语法分析是如何工作的。

ANTLR 4权威参考读书笔记(7)

ANTLR 4权威参考读书笔记(6)中的这些动作仅仅是提取和打印被语法分析器匹配的值,它们并没有改变语法分析器本身。

实际上,动作还可以影响语法分析器如何识别输入短语。这类特殊的动作被称为语义谓词。下面我们会用一个简单的例子来展示语义谓词的强大能力:动态地打开和关闭语法的某个部分。

使用语义谓词改变语法分析

有一个读入整数序列的语法,它的玄机是由输入的部分指定有多少个整数组合在一起,所以我们必须等到运行时才能知道有多少个整数被匹配。这里是示例输入文件idata.txt的内容:

1
2 9 10 3 1 2 3

第一个数字表示匹配后续两个数字9和10;紧跟着10的数字3表示匹配接下来的三个数字。我们的目的是设计一个语法IData.g,把9和10组合在一起,把1、2和3组合在一起。在语法上执行下面的命令后显示的语法分析树能够清楚地标识出整数的分组,就像下图显示的那样:

1
2
3
antlr -no-listener IData.g
compile *.java
grun IData file -gui idata.txt

idata-parse-tree

要达成这个目标,以下语法中的关键是一个被称为语义谓词的布尔值操作:{$i <= $n}?。当谓词计算结果为true时,语法分析器匹配整数直到超过规则参数n要求的数量;当计算结果为false时,谓词让相关的选项从生成的语法分析器中“消失”。 在这个案例中,值为false的谓词让(...)*循环从规则里终止并返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
grammar IData;

file : group+ ;

group: INT sequence[$INT.int] ;

sequence[int n]
locals [int i=1]
     : ( {$i <= $n}? INT {$i++;} )*    // match n integers
     ;

INT  : [0-9]+ ;  // match integers
WS   : [ \t\n\r]+ -> skip ;    // toss out all whitespace

被语法分析器使用的规则sequence的内部语法表示看起来就像下图这样:

idata-rule-sequence

虚线表明谓词可以剪断那条路径,只给语法分析器留下一个选择:退出的路径。

虽然大部分时间我们不需要这样的微管理,但它至少让我们知道我们有这样的武器可以处理病理分析问题。

Google Analytics中如何检测并防止垃圾流量

使用Google Analytics网站分析工具对博客进行数据统计。在经过一段时间的数据收集后,发现总是会有大量的垃圾流量存在。这里的垃圾流量,指的是对网站毫无作用且会影响网站数据报表质量的流量。通常Google Analytics中的垃圾流量可以分为以下两大类:

  • 一类被称为ghost referral,这些流量事实上从来没有来过你的网站,也不会出现在你网站服务器的日志中,但你可以在数据报表中发现它们,它们影响了Google Analytics中的数据;
  • 另一类是爬虫流量,包括搜索引擎爬虫流量和非搜索引擎爬虫流量,这些流量会影响Google Analytics中各渠道流量占比及会话次数、跳出率、停留时间等关键指标。

垃圾流量检测方法

打开报告 -> 受众群体 -> 技术 -> 广告网络 -> 主机名,统计报表如下图所示:

report-with-spam

可以看到,只有181个会话的主机名是我的博客域名,即真实来到我博客的流量,也就是说有超过一半的流量属于垃圾流量。并且这些垃圾流量基本都出现了不同程度的数据异常,如新会话百分比为0%、新用户为0、跳出率为100%、平均会话时长为00:00:00。这些垃圾流量的主机名与博客域名无关,说明是第一类垃圾流量。出现这类数据的原因可能是:

  • 别的网站使用了和你网站相同的媒体资源ID,这种情况一般来说不可能,除非恶意为之;
  • 有人使用Google Analytics中的Measurement Protocol做机器生成的访问流量,而你的媒体资源ID不幸躺枪。

使用过滤器屏蔽垃圾流量

打开“管理”页面,在博客帐号的“所有过滤器”下添加新的过滤条件,使用预定义或自定义均可,基本配置如下所示:

exclude-spam-filter

然后把“可选择的数据视图”中的选项添加到“选定的数据视图”中,保存即可。

过段时间后再回来查看报表,就会发现垃圾流量消失的干干净净了:

report-without-spam

ANTLR 4权威参考读书笔记(6)

  • action 动作
  • clause 子句

监听器和访问者机制是极好的。大多数时候,我们可以用监听器或访问者来构建语言应用,它们让特定应用的代码置身于语法之外,使语法容易被阅读。

但有时候我们需要额外的控制权和灵活性。为了达到这个目的,我们可以直接在语法中嵌入代码片段(这些嵌入的代码片段被称为动作)。这些动作会被注入到由ANTLR工具生成的分析器代码中。这些被注入的代码在分析期间执行,并且能像其它任意代码片段一样收集信息或生成输出。结合语义谓词,我们甚至可以在运行时让我们语法的某部分消失!例如,我们可能想打开或关闭Java语法中的enum关键字,分析语言的不同版本。没有语义谓词,我们就需要两个不同版本的语法。

下面我们将实现一个简单的程序,读入数据行,然后打印出在特定列中找到的值。

在语法中嵌入任意的动作

如果我们不想付出构建语法分析树的开销,或者想要在分析期间动态地计算值或把东西打印出来,那么可以通过在语法中嵌入任意代码来实现。它是比较困难的,因为我们必须明白在语法分析器上的动作的影响,以及在哪里放置这些动作。

为了解释嵌入在语法中的动作,让我们先来看下文件rows.txt中的数据:

1
2
3
parrt   Terence Parr    101
tombu   Tom Burns       020
bke     Kevin Edgar     008

这些列是由TAB分隔的,每行以一个换行符结束。匹配这种类型的输入在语法上还是相当简单的。下面是此语法文件Rows.g的内容:

1
2
file : (row NL)+ ;    // NL is newline token: '\r'? '\n'
row  : STUFF+ ;

我们需要创建一个构造器以便我们能传递我们想要的列号(从1开始计数),所以我们需要在规则中添加一些动作来做这个事情:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
grammar Rows;

@parser::members {    // add members to generated RowsParser
    int col;

    public RowsParser(TokenStream input, int col) {    // custom constructor
        this(input);
        this.col = col;
    }
}

file: (row NL)+ ;

row
locals [int i=0]
    : ( STUFF
        {
            $i++;
            if ($i == col) System.out.println($STUFF.text);
        }
      )+
    ;

TAB  :  '\t' -> skip ;    // match but don't pass to the parser
NL   :  '\r'? '\n' ;      // match and pass to the parser
STUFF:  ~[\t\r\n]+ ;      // match any chars except tab, newline

在上述语法中,动作是被花括号括起来的代码片段;members动作的代码将会被注入到生成的语法分析器类中的成员区;在规则row中的动作访问的$i是由locals子句定义的局部变量,该动作也用$STUFF.text获取最近匹配的STUFF记号的文本内容。STUFF词法规则匹配任何非TAB或换行的字符,这意味着在列中可以有空格字符。

现在,是时候去思考如何使用定制的构造器传递一个列号给语法分析器,并且告诉语法分析器不要构建语法分析树了:

1
2
3
4
5
6
7
8
9
10
11
12
public class Rows {

    public static void main(String[] args) throws Exception {
        ANTLRInputStream input = new ANTLRInputStream(System.in);
        RowsLexer lexer = new RowsLexer(input);
        CommonTokenStream tokens = new CommonTokenStream(lexer);
        int col = Integer.valueOf(args[0]);
        RowsParser parser = new RowsParser(tokens, col);    // pass column number!
        parser.setBuildParseTree(false);    // don't waste time bulding a tree
        parser.file();
    }
}

现在,让我们核实下我们的语法分析器能否正确匹配一些示例输入:

1
2
3
antlr -no-listener Rows.g  # don't need the listener
compile *.java
run Rows 1 < rows.txt

这时你会看到rows.txt文件的第1列内容被输出:

1
2
3
parrt
tombu
bke

如果将上面命令中的1换成2,你会看到文件的第2列内容被输出;如果换成3,那么第3列内容将会被输出。

一夜成名:这需要好几年时间

英文原文:http://blog.codinghorror.com/overnight-success-it-takes-years/

Gmail的原首席开发者Paul Buchheit说过Gmail的成功花了很长时间

我们在2001年8月开始开发Gmail。很长一段时间里,几乎每个人都不喜欢它。有些人使用它因为搜索,但他们有着无尽地抱怨。很多人认为我们应该杀死这个项目,又或者重启它作为一个有本地客户端软件的企业产品,而不是这个疯狂的JavaScript东西。甚至在2004年4月1日当我们到达发布它的那个点时——在开始开发它两年半之后——Google内部的很多人都在预测它的死亡。他们觉得这个产品太怪异了,并且没有人想去更换电子邮件服务。我被告知我们永远不会得到100万用户。

但在我们发布后,除了因为各种原因讨厌它的人,反响出人意外地好。尽管如此,它还是经常被描述为“小众产品(niche)”和“不会被硅谷之外的人使用”。

现在,在我们开始开发Gmail差不多七年半后,我看到一篇文章叙述Gmail如何在去年增长40%,相比Yahoo的2%和Hotmail的-7%。

Paul已经离开Google,现在在从事他自己的创业公司FriendFeed(译者注:FriendFeed已于2015年4月9日关闭)。许多业内人士对待FriendFeed不太友善。Stowe Boyd甚至竟然称FriendFeed就是个失败(译者注:Stowe Boyd评论FriendFeed的文章已经被删除了)。Paul从容应对批评:

创建一个重要的新产品通常需要时间。FriendFeed需要继续追求创新,就像Gmail六年以前做的那样。FriendFeed显示了很好的前景,但它仍然是一个“在制品”。

我的预期是很大的成功需要好几年时间,没有许多反例(除了YouTube,但现在它其实还没有到达挣成堆钱的那个点)。Facebook成长非常快,但它此时已经五岁了。Larry和Sergey 开始开发Google在1996年——当我开始在那里是1999年,几乎没人听说过它。

一夜成名的观念非常具有误导性,而且相当有害。如果你开始新的东西,那会是一次长途旅行。没有借口去行动缓慢。相反,你必须行动的非常快,否则你将不会到达,因为它是一次长途旅行!这也是为什么节俭是重要的——你不想饿死在半山腰上

Stowe Boyd用一张Twitter和FriendFeed的流量对比图说明他关于FriendFeed的观点。这里请允许我把我自己的数据也加到Boyd先生的图上:

three-traffic-comparison

我觉得Paul的态度令人耳目一新,因为对于我们的创业公司Stack Overflow我也采用同样的态度。我没有期望或甚至渴望一夜成名。我计划的是花上几年的时间去打磨,持续地、稳步地提升。

这项商业计划和我的职业生涯发展计划没有太多区别:成功需要好几年时间。当我说年的时候,我是认真的!不是在说像“更聪明地工作,而不是更努力地工作”那样的陈词滥调。我是在说真正的日历年。你知道的,12个月的,365天的那种。你必须花上你生命的多年时间孜孜不倦地钻研这些东西,每天醒来后一遍又一遍地做它。每天练习和收集反馈去不断变得更好。有时它可能是不愉快的,甚至偶尔是很无趣的,但它是必需的。

这几乎不是唯一的或有趣的建议。Peter Norvig的经典用十年自学编程也谈到过这个话题,而且讲得比我更好。

研究人员发现在任何领域都需要大约10年时间才能培养出专业技能,包括国际象棋、音乐作曲、电报操作、绘画、钢琴演奏、游泳、网球、以及神经心理学和拓扑学的研究。关键是刻意(deliberative)练习:不仅仅是一次又一次地做它,而是用略微超出你当前能力的任务来挑战自己,尝试它,在做时和做后分析你的表现,并且纠正所有错误。然后重复。再重复。

似乎没有真正的捷径:即使是莫扎特,4岁的音乐天才,在他开始创作世界级音乐前也花了超过13年。甲壳虫乐队似乎以一连串的冠军歌曲(a string of #1 hits)横空出世,并且在1964年出现在《埃德·沙利文秀》。但其实自1957年以来他们就已经在利物浦和汉堡的小俱乐部里演出了,虽然他们在早期有广泛的吸引力,但他们最最成功的《Sgt. Pepper's Lonely Hearts Club Band》发布在1967年。

老实说,我期待着有一天醒来,从现在起的2年或3年之后,做着和今天我在做的完全相同的事:为Stack Overflow编写代码,增加另一个微小的改进或有用的功能。很明显我们想要成功。但在某种程度上,成功是无关紧要的,因为这个过程本身是令人满意的。每天醒来做你喜欢的事情——甚至更好的是,周围社区的人也喜欢它——这本身就是一种奖赏。尽管有着成吨的工作要做。

博客也不例外。我经常给有抱负的博客作者这个很重要的建议:如果你开始你的博客,在六个月内别指望有人来读它。如果你这样做,我可以保证你将会非常失望。 可是,如果你能坚持发布计划并且每周写1篇或2篇高质量的博文一整年……然后,也只有到那个时候,你才可以看到稀稀落落的读者。我开始这个博客于2004年,花了整整3年的时间,每周写3到5篇博文,才使得它在软件开发社区内流行开来。

我非常期望在这个博客上一直写,以一种形式或另一种,用我的余生。它是我是谁的一部分。至于那种戏剧性的成名方式,我不抱有任何幻想:归根结底,我只是在网上写博客的那个人

pearls-before-swine

那样挺好的对我来说。我从来没有说过我是聪明的。

不管你最终获得多少读者,或页面浏览量,或任何我们这周正在度量的高分排行榜 ,请记住,你正在做的事情是值得去做的,因为——嗯——你正在做的事情是值得去做的。

如果你一直这样坚持下去,谁知道会发生什么?很有可能某天你醒过来,发现自己一夜成名了。

ANTLR 4权威参考读书笔记(5)

ANTLR 4权威参考读书笔记(3)以及ANTLR 4权威参考读书笔记(4)中我们分别用访问者和监听器机制实现了计算器的解释执行和编译执行。但并没有给出这两种机制的太多细节,这次就来详细地讲讲。

ANTLR在它的运行库中为两种树遍历机制提供支持。默认情况下,ANTLR生成一个语法分析树监听器接口,在其中定义了回调方法,用于响应被内建的树遍历器触发的事件。

在监听器和访问者机制之间最大的不同是:监听器方法被ANTLR提供的遍历器对象调用;而访问者方法必须显式的调用visit方法遍历它们的子节点,在一个节点的子节点上如果忘记调用visit方法就意味着那些子树没有得到访问。

让我们首先从监听器开始。在我们了解监听器之后,我们也将看到ANTLR如何生成遵循访问者设计模式的树遍历器。

语法分析树监听器

在Calc.java中有这样两行代码:

1
2
ParseTreeWalker walker = new ParseTreeWalker();
walker.walk(new DirectiveListener(), tree);

类ParseTreeWalker是ANTLR运行时提供的用于遍历语法分析树和触发监听器中回调方法的树遍历器。ANTLR工具根据Calc.g中的语法自动生成ParseTreeListener接口的子接口CalcListener和默认实现CalcBaseListener,其中含有针对语法中每个规则的enter和exit方法。DirectiveListener是我们编写的继承自CalcBaseListener的包含特定应用代码的实现,把它传递给树遍历器后,树遍历器在遍历语法分析树时就会触发DirectiveListener中的回调方法。

calc-listener-hierarchy

下图左边的语法分析树显示ParseTreeWalker执行了一次深度优先遍历,由粗虚线表示,箭头方向代表遍历方向。右边显示的是语法分析树的完整调用序列,它们由ParseTreeWalker触发调用。当树遍历器遇到规则assign的节点时,它触发enterAssign()并且给它传递AssignContext这个语法分析树节点的上下文。在树遍历器访问完assign节点的所有子节点后,它触发exitAssign()。

listener-call-sequence

监听器机制的强大之处在于所有都是自动的。我们不必要写语法分析树遍历器,而且我们的监听器方法也不必要显式地访问它们的子节点。

语法分析树访问者

有些情况下,我们实际想要控制的是遍历本身,在那里我们可以显式地调用visit方法去访问子树节点。参数-visitor告诉ANTLR工具从相应语法生成访问者接口和默认实现,其中含有针对语法中每个规则的visit方法。

下图是我们熟悉的访问者模式操作在语法分析树上。左边部分的粗虚线表示语法分析树的深度优先遍历,箭头方向代表遍历方向。右边部分指明访问者中的方法调用序列。

visitor-call-sequence

下面是Calc.java中的两行代码:

1
2
3
EvalVisitor eval = new EvalVisitor();
// To start walking the parse tree
eval.visit(tree);

我们首先初始化自制的树遍历器EvalVisitor,然后调用visit()去访问整棵语法分析树。ANTLR运行时提供的访问者支持代码会在看到根节点时调用visitProg()。在那里,visitProg()会把子树作为参数调用visit方法继续遍历,如此等等。

calc-visitor-hierarchy

ANTLR自动生成的访问者接口和默认实现可以让我们为访问者方法编写自己的实现,让我们避免必须覆写接口中的每个方法,仅仅聚焦在我们感兴趣的方法上。这种方式减少了我们学习ANTLR必须要花费的时间,让我们回到我们所熟悉的编程语言领域。