乐者为王

Do one thing, and do it well.

逻辑题-谁养鱼

  1. 在一条街上,有5座房子,喷了5种颜色。
  2. 每座房子里住着不同国籍的人。
  3. 每个人喝不同的饮料,抽不同品牌的香烟,养不同的宠物。

问题是:谁养鱼?

提示:

  1. 英国人住红色房子。
  2. 瑞典人养狗。
  3. 丹麦人喝茶。
  4. 绿色房子在白色房子左面。
  5. 绿色房子主人喝咖啡。
  6. 抽Pall Mall香烟的人养鸟。
  7. 黄色房子主人抽Dunhill香烟。
  8. 住在中间房子的人喝牛奶。
  9. 挪威人住第一间房子。
  10. 抽Blends香烟的人住在养猫的人隔壁。
  11. 养马的人住抽Dunhill香烟的人隔壁。
  12. 抽Blue Master的人喝啤酒。
  13. 德国人抽Prince香烟。
  14. 挪威人住蓝色房子隔壁。
  15. 抽Blends香烟的人有一个喝水的邻居。

在回答上述问题前先画一个6行5列的表格,从上到下的每一行分别代表房子的顺序(A表示左边第一间房子)、哪国人、房子颜色、饮料、香烟、宠物。下表是问题的初始状态:

A B C D E
? ? ? ? ?
? ? ? ? ?
? ? ? ? ?
? ? ? ? ?
? ? ? ? ?

根据提示8、9和14可以得到:

A B C D E
挪威人 ? ? ? ?
? 蓝色 ? ? ?
? ? 牛奶 ? ?
? ? ? ? ?
? ? ? ? ?

由提示4和5可以判定房子D的颜色是绿色,房子主人喝咖啡,房子E的颜色是白色。如下表所示:

A B C D E
挪威人 ? ? ? ?
? 蓝色 ? 绿色 白色
? ? 牛奶 咖啡 ?
? ? ? ? ?
? ? ? ? ?

结合提示1、7和11可以知道房子C是红色的,住的是英国人,房子A是黄色的。挪威人抽Dunhill香烟。住房子B的人养马。如下表所示:

A B C D E
挪威人 ? 英国人 ? ?
黄色 蓝色 红色 绿色 白色
? ? 牛奶 咖啡 ?
Dunhill ? ? ? ?
? ? ? ?

依据上表可知,挪威人喝的饮料是水、茶或者啤酒。结合提示3和12可以断定挪威人喝的是水。如下表所示:

A B C D E
挪威人 ? 英国人 ? ?
黄色 蓝色 红色 绿色 白色
? 牛奶 咖啡 ?
Dunhill ? ? ? ?
? ? ? ?

通过提示15可以得出住房子B的人抽Blends香烟。如下表所示:

A B C D E
挪威人 ? 英国人 ? ?
黄色 蓝色 红色 绿色 白色
? 牛奶 咖啡 ?
Dunhill Blends ? ? ?
? ? ? ?

结合提示12可以推断住房子E的人抽Blue Master香烟、喝啤酒。住房子B的人喝茶。如下表所示:

A B C D E
挪威人 ? 英国人 ? ?
黄色 蓝色 红色 绿色 白色
牛奶 咖啡 啤酒
Dunhill Blends ? ? Blue Master
? ? ? ?

由提示3、13得到房子B住的是丹麦人。房子D住的是德国人,抽Prince香烟。如下表所示:

A B C D E
挪威人 丹麦人 英国人 德国人 ?
黄色 蓝色 红色 绿色 白色
牛奶 咖啡 啤酒
Dunhill Blends ? Prince Blue Master
? ? ? ?

再由提示6和10确定住房子C的人抽Pall Mall香烟、养鸟。挪威人养猫。如下表所示:

A B C D E
挪威人 丹麦人 英国人 德国人 ?
黄色 蓝色 红色 绿色 白色
牛奶 咖啡 啤酒
Dunhill Blends Pall Mall Prince Blue Master
? ?

最后结合提示2推断得到房子E住的是瑞典人,养狗。如下表所示:

A B C D E
挪威人 丹麦人 英国人 德国人 瑞典人
黄色 蓝色 红色 绿色 白色
牛奶 咖啡 啤酒
Dunhill Blends Pall Mall Prince Blue Master
?

现在,结果已经出来了:德国人养鱼。

开发者被灯光蒙蔽了双眼

英文原文:http://programmingzen.com/2008/12/30/developers-are-blinded-by-the-light/

Blinded by the light,
revved up like a deuce,
another runner in the night
— Bruce Springsteen

人类在计算几率方面是异常地差。我们有限的经验强烈地影响着我们对事件的可能性的认知。例如,我们往往极大地高估由恐怖袭击、意外枪支走火或者飓风引起死亡的几率,并且极大地低估像坠落、溺水或者流感死亡的原因。其原因是媒体经常提醒我们恐怖主义、飓风的危险或者关于孩子们被意外射杀的瞬间故事。你很少发现关于一个人溺水、坠落或者由于流感死亡的故事在国家新闻频道上被报道。新闻报道有一种倾向是耸人听闻,为的是引起人们的注意和钩住大量的观众,因此当谈到估计什么可能/不可能发生时它们促成人们的偏见。

同样的,在电视和报纸上过度曝光心花怒放的彩票中奖者举起他们超大的支票往往歪曲人们对通过购买一注彩票胜利的可能性的认知。对这个问题持一个严谨和客观的态度将会很快揭露中奖的几率比它们表面上看起来的要差得多。[1]

我注意到这种情况也正在开发/创业的世界里发生。这是一波新的淘金热。太多的开发者正在试图建立下一个大的社交网络,成为下一个Facebook(或YouTube),聚集数以百万计的人群,希望被大公司以一笔数量荒谬的钱收购。媒体喜欢这类故事。

因此,正在试图构建下一个Facebook的开发者类似于彩票买家。他们中的一些人会成功和获胜,但大部分人会惨遭失败。我们真正需要多少社交网络?广告支撑的模式适合某些设法吸引庞大的人群同时保持其费用最低(例如PlentyOfFish)或者被收购(例如YouTube,它也在花Google的钱)的幸运的公司。在这个过程中其他人都是在烧钱以及浪费VC的钱和诚意。

我担心很多开发者被灯光蒙蔽了双眼。他们对获得成功的真实几率的认知受到了媒体连续报道的百万——如果不是10亿——美元收购和成功故事的扭曲。而且有些VC鼓励这种行为,希望能在他们的投资上看到高回报。毕竟这些都是非常富有的人,他们对小规模的成功不感兴趣。

除了明显的浪费时间和资源之外,我认为许多开发者为了追求极不可能的结果放弃了极好的机会。用一个传统商业计划挣1,000万的可能性和按YouTube的方式挣10亿的可能性的比例,与这些金额能负担得起你的不同生活质量不成正比。如果你破产了,有3万美元的信用卡债务,或者你是中产阶级,你会发现1,000万美元可以提高的生活质量总是远远超过从1,000万到10亿能提高的。而且重要的是,我们要认识到瞄准尽管更小,但更有可能的结果不会以任何方式阻止你以后的“伟大梦想”,一旦你的第一次(或者第一次成功的)创业已经取得了成功。

你愿意参加20次有1次胜利机会的100万美元抽奖,还是50,000,000次有1次机会的5亿美元抽奖?理性的人会选择第1个,然而在今天,大部分创业公司都倾向于选择第2个。他们这么做是因为他们极大地高估了他们以第2个抽奖成功的几率。

创建一个产品并让人们为它付费。不要拿VC的钱,除非你真的不考虑依靠自己的力量启动你的公司。软件世界的主要优点之一是在开始的时候极少量的资本需要。如果你想做Web应用,可以使用软件即服务(SaaS)模型,让你的用户为你提供的软件和服务付费。你将会有更加少的受众,更少的可伸缩性问题和费用,以及有更多的收入和更大的盈利机会。Joel Spolsky(和他那华丽的办公空间)挣得数百万收入是因为他的公司在出售一套Web版的bug追踪器。你知道有多少免费的bug追踪器?在这个市场上存在多少竞争对手?我确信有许多。然而,虽然Joel的人气毫无疑问地帮助到了他的公司,但此案例仍然展示了一个企业如何通过构建一个更好的产品而成功。

就像David Heinemeier Hansson提及的,有无数不受关注的公司在像那样挣钱。[2]如果你把视线从聚光灯上移开,你将看到许多公司在它们所做的事情上面非常成功,尽管它们不出名或者没有制造新闻头条。它们中的有些公司实际上努力不去吸引太多的关注到它们的成功上(经常用数百万美元来衡量),以便防止竞争对手的涌现。

不管你的名字是不是家喻户晓,你甚至不必创建Web应用才能非常成功。你可能会想到为智能手机包括iPhone开发移动应用。但是良好的老式桌面应用让各种各样的软件公司继续发展。这就是为什么“你不能再用商业桌面软件挣钱,或者桌面应用都死了”的扭曲的认知太荒谬的原因。作为开发者/微型ISV/创业公司,你用良好设计的桌面软件挣钱的机会远远高于构建任何一款YouTube、Flickr或者Facebook复制品的机会。

要了解我们的认知是怎么被扭曲的,你只需要和那些公开分享它们软件销售统计的公司交谈。你将会被用相对普通的软件挣到钱的数额震惊。Balsamiq制作了一款UI草图应用卖79美元。作者成功挣到了10万美元收入在前5个月,大部分是通过销售应用的桌面版本。在这个产业里他当然远非最大的赢家之一。我提到这个不过是因为它表明那是一个非常不错的想法,很好执行,当你让你的用户付费时能很快带来收入。如果你认为在5个月里10万美元很少,那我来问你有多少免费网站达成一个类似的每月收入净额。如果你正在寻找更大的收入,了解下Omni Graffle,它给Omni Group赚取了数百万美元,或者把你的目光放到B2B应用上(在那个市场里某些应用卖上千美元一份)。

当许多开发者被灯光蒙蔽了双眼的时候,有创业想法的智者正在建立真正的软件业务。我请你走出来做同样的事情。

脚注

[1] 我在这里总结的概念被Dan Gilbert在这个TED演讲里更详细地说明了。

[2] David Heinemeier Hansson在他的一篇帖子中持类似的观点,该帖给了本文以灵感。

哪些书不要读

计算机

《像程序员一样思考》

点评:这是一本教你学会如何解决编程问题的书。但实际上只有第1、第7和第8章值得一读。

经济学

《郎咸平说:我们的日子为什么这么难》

点评:提出的观点都是当前大家关心的问题,可惜他只是财务和公司治理专家,不是经济学专家,所提的解决方案或说的话也只是让大家心中的郁闷可以有机会发泄下,所以郎咸平的其它关于经济学的书籍你就可以不要看了。如果你想学公司财务方面的东西,那么他的论文(注意不是书,是发表过的学术论文)是一定要读的。延伸阅读是东方出版社搞的,而非郎咸平,推荐的也都是该出版社的书。

《货币战争》

点评:又是一本博大众眼球的书。可是一个连美联储是私有的都要当作秘密大声来讲的作者你就知道他是什么水平了,这些可是人家美联储官方网站上都黑纸白纸写出来的。其它什么断章取义、胡乱拼凑的就不要讲了,我读了一部分就看不下去,把它扔角落里去了。以前还写过一篇博文来讲这本书是如何混淆读者视听的,只是可惜没有完成。

《魔鬼经济学》

点评:引用郭凯的话“虽然书里的每个故事都是基于作者的实证研究,可是他的那些实证研究没有一个不存在致命缺陷,他所有的结论事后都被更仔细的研究推翻了。”

《13个严重的经济学谬误》

点评:这本书有点专业,且翻译的极为糟糕,好些语句和段落不甚通顺,所以部分内容读起来有种似是而非的感觉。翻译后的书名加上“经济学的真相”,完全是想用耸人听闻的标题吸引读者的注意。该书最值得看的是《前言》和《一份经济学研究方法的备忘录》,尤其是后者,可以让你了解一些经济学观点是怎么得出的。谬误1整篇文章几乎看不出是在证明命题的错误;谬误3中的印度可以用中国替代。作者的观点看似有理其实偏颇,看过郭凯的《王二的经济学故事》中关于汇率的文章就可以知道,印度出口商品换成美元结余,只劳动不消费,相当于给美国白打工;印度坚持高估美元,虽然可以保持它在与美国贸易中的顺差地位,但这种强制性的高估实际上是在用贸易补贴美国。谬误5虽然讲的有道理,但实在想不明白作者是如何得到租金限制法令的几个特征的!谬误7中房屋的建筑成本是不包含土地价格的,这种违背常识的假设会增加理解的困难。而且,设定房屋价格后再去谈论削减建筑成本不会影响房价,怎么看怎么觉得奇怪,虽然我同意他关于削减建筑成本不会降低房价的观点。谬误9的第4段翻译的极其马虎,明显有两个人翻译的痕迹,错字漏字导致边际成本傻傻的说不清。

《一切皆有价》

点评:书名标题很诱人,容易引起读者进一步阅读的欲望,可惜内容只是大量数据和案例的罗列,毫无作者自己的观点。

书法

《书法有法》

点评:全书可用“转笔”二字一言蔽之。但疑惑的是,既然她这么推崇转笔,并且通过私相授受的方式从长辈手中得以传承这个笼罩着神秘、高深莫测、技巧高难、妙不可言的笔法的不传之秘,为什么她在书写时却几乎不用呢?而且文中对古人论述书法的言论的解释多有牵强附会之处。

贫富差距精简版

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

2016年1月

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

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

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

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

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

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

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

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

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

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

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

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

  • sequence 序列

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

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

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

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

1
2 9 10 3 1 2 3

第1个数字表示匹配后续两个数字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

要达成这个目标,以下语法中的关键是一个被称为语义谓词的布尔值操作:{$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

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

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

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

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

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

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

垃圾流量检测方法

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

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

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

使用过滤器屏蔽垃圾流量

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

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

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

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

  • action 操作
  • clause 子句

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

但有时候我们需要额外的控制权和灵活性。为了达到这个目的,我们可以直接在语法中嵌入代码片段(这些嵌入的代码片段被称为操作)。这些操作会被注入到由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
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,你会看到rows.txt文件的第2列内容被输出;如果换成3,那么rows.txt文件的第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先生的图上:

我觉得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篇博文,才使得它在软件开发社区内流行开来。

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

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

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

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

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

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

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

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

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

语法分析树Listener

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

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

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

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

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

语法分析树Visitor

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

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

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

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

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

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

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

ANTLR 4权威参考读书笔记(3)中的计算器是以解释的方式执行的,现在我们想要把它转换成以编译的方式执行。编译执行和解释执行相比,需要依赖于特定的目标机器。在这里我们假设有一台这样的机器,它用堆栈进行运算,支持如下表所示的几种指令:

指令 说明 操作数个数 用途
LDV Load Variable 1 变量入栈
LDC Load Constant 1 常量入栈
STR Store Value 1 栈顶一个元素存入指定变量
ADD Add 0 栈顶两个元素出栈,求和后入栈
SUB Subtract 0 栈顶两个元素出栈,求差后入栈
MUL Multiply 0 栈顶两个元素出栈,求积后入栈
DIV Divide 0 栈顶两个元素出栈,求商后入栈
RET Return 0 栈顶一个元素出栈,计算结束

做这个最简单的方法是使用ANTLR的语法分析树Listener机制实现DirectiveListener类,然后它通过监听来自树遍历器触发的事件,输出对应的机器指令。

Listener机制的优势是我们不必要自己去做任何树遍历,甚至我们不必要知道遍历语法分析树的运行时如何调用我们的方法,我们只要知道我们的DirectiveListener类得到通知,在与语法规则匹配的短语开始和结束时。这种方法减少了我们学习ANTLR必须要花费的时间,让我们回到我们所熟悉的编程语言领域。

这里不需要创建新的语法规则,还是继续沿用前文Calc.g所包含的语法,标签也要保留:

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
27
28
29
30
31
32
grammar Calc;

prog
    : stat+
    ;

stat
    : expr                   # printExpr
    | ID '=' expr            # assign
    ;

expr
    : expr op=(MUL|DIV) expr # MulDiv
    | expr op=(ADD|SUB) expr # AddSub
    | INT                    # int
    | ID                     # id
    | '(' expr ')'           # parens
    ;

MUL : '*' ;

DIV : '/' ;

ADD : '+' ;

SUB : '-' ;

ID  : [a-zA-Z]+ ;

INT : [0-9]+ ;

WS  : [ \t\r\n]+ -> skip ;    // toss out whitespace

然后,我们可以运行ANTLR工具:

1
antlr Calc.g

它会生成后缀名为tokens和java的六个文件:

1
2
Calc.tokens         CaclLexer.java          CalcParser.java
CalcLexer.tokens    CalcBaseListener.java   CalcListener.java

正如这里我们看到的,ANTLR会为我们自动生成Listener基础设施。其中CalcListener是语法和Listener对象之间的关键接口,描述我们可以实现的回调方法:

1
2
3
4
5
6
public interface CalcListener extends ParseTreeListener {
  void enterProg(CalcParser.ProgContext ctx);
  void exitProg(CalcParser.ProgContext ctx);
  void enterPrintExpr(CalcParser.PrintExprContext ctx);
    ...
}

CalcBaseListener则是ANTLR生成的一组空的默认实现。ANTLR内建的树遍历器会去触发在Listener中像enterProg()和exitProg()这样的一串回调方法,如同它对语法分析树执行了一次深度优先遍历。为响应树遍历器触发的事件,我们的DirectiveListener需要继承CalcBaseListener并实现一些方法。我们不需要实现全部的接口方法,我们也不需要去覆写每个enter和exit方法,我们只需要去覆写那些我们感兴趣的回调方法。

在本例中,我们需要通过覆写6个方法对6个事件——当树遍历器exit那些有标签的选项时触发——作出响应。我们的基本策略是当这些事件发生时打印出已转换的指令。以下是完整的实现代码:

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class DirectiveListener extends CalcBaseListener {

    @Override
    public void exitPrintExpr(CalcParser.PrintExprContext ctx) {
        System.out.println("RET\n");
    }

    @Override
    public void exitAssign(CalcParser.AssignContext ctx) {
        String id = ctx.ID().getText();
        System.out.println("STR " + id);
    }

    @Override
    public void exitMulDiv(CalcParser.MulDivContext ctx) {
        if (ctx.op.getType() == CalcParser.MUL) {
            System.out.println("MUL");
        } else {
            System.out.println("DIV");
        }
    }

    @Override
    public void exitAddSub(CalcParser.AddSubContext ctx) {
        if (ctx.op.getType() == CalcParser.ADD) {
            System.out.println("ADD");
        } else {
            System.out.println("SUB");
        }
    }

    @Override
    public void exitId(CalcParser.IdContext ctx) {
        System.out.println("LDV " + ctx.ID().getText());
    }

    @Override
    public void exitInt(CalcParser.IntContext ctx) {
        System.out.println("LDC " + ctx.INT().getText());
    }
}

为了让它运行起来,余下我们唯一需要做的事是创建一个主程序去调用它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Calc {

    public static void main(String[] args) throws Exception {
        InputStream is = args.length > 0 ? new FileInputStream(args[0]) : System.in;

        ANTLRInputStream input = new ANTLRInputStream(is);
        CalcLexer lexer = new CalcLexer(input);
        CommonTokenStream tokens = new CommonTokenStream(lexer);
        CalcParser parser = new CalcParser(tokens);
        ParseTree tree = parser.prog();

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

        // print LISP-style tree
        System.out.println(tree.toStringTree(parser));
    }
}

这个程序和前文Calc.java中的代码极度相似,区别只在12-13行。这两行代码负责创建树遍历器,然后让树遍历器去遍历那颗从语法分析器返回的语法分析树,当树遍历器遍历时,它就会触发调用到我们的DirectiveListener中实现的方法。此外,通过传入一个不同的Listener实现我们能简单地生成完全不同的输出。Listener机制有效地隔离了语法和语言应用,使语法可以被其它应用再次使用。

现在一切完备,让我们尝试着去编译和运行它吧!下面是完整的命令序列:

1
2
compile *.java
run Calc calc.txt

编译的输出结果如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
LDC 19
RET

LDC 5
STR a
LDC 6
STR b
LDV a
LDV b
LDC 2
MUL
ADD
RET

LDC 1
LDC 2
ADD
LDC 3
MUL
RET

代码下载:https://github.com/dohkoos/antlr4-calculator