lcomplete's Blog

一个带着极客精神正努力成为黑客的程序员

七周七语言之使用prolog解决爱因斯坦斑马难题

| Comments

目前商业上广泛使用的编程语言多是命令式或函数式的编程语言,这些语言在某些方面具有很高的相似度,比如 python 和 ruby 在很多地方是相通的,学会了一门,再学另一门便能够事半功倍,很多语言都是如此,然而今天要介绍的这门语言,却跟主流编程语言截然不同,它就是prolog——一门逻辑编程语言。

prolog 是 Programming in Logic 的缩写,它被广泛应用在人工智能、自然语言等研究领域,使用它来解决逻辑难题完全不在话下,今天我们将使用它来解决著名的爱因斯坦逻辑难题(斑马难题),首先让我们来认识一下 prolog 的语法。

prolog 基本语法

prolog 中有3种基本元素:事实、规则和查询。事实和规则用于描述数据,查询用于获取问题的答案。
我们可以这样定义事实:

1
2
3
4
5
human(lucy).
human(lili).
father(lucy,david).
sister(lucy,lili).
sister(lili,lucy).

这段代码表示 lucy 和 lili 是人类,且她们是姐妹,david 是 lucy 的父亲,继续定义规则:

1
2
daughter(Father,A,B) :-
    father(A,Father), sister(A,B)

这段规则表示对于变量A、B,如果Father变量是A的父亲,且A、B是姐妹,则A、B是Father对象的女儿。
注意,在 prolog 中一个词若以小写开头,那么它是一个固定值,若以大写字母开头,则是一个变量。
将这些事实和规则放在一个文件里面,在命令行下打开 prolog ,对这个文件进行编译,即可提出查询,比如 daughter(david,A,_),prolog 将会求出A可能的取值并输出到控制台,最后的下划线是一个占位符,不会进行求值。

在 prolog 中还可以使用递归完成列表和数学等运算,这部分是很强大的,但这里不打算讲,因为有了上面的基础知识后,我们就可以利用它来解决逻辑问题了,下面就让我们来解决“斑马难题”吧。

爱因斯坦逻辑难题

题目:5个不同国家且工作各不相同的人分别住在一条街上的5所房子里,每所房子的颜色不同,每个人都有自己养的不同宠物,喜欢喝不同的饮料。根据以下提示,你能告诉我哪所房子里的人养斑马,哪所房子里的人喜欢喝矿泉水吗?

  1. 英国人住在红色的房子里
  2. 西班牙人养了一条狗
  3. 日本人是一个油漆工
  4. 意大利人喜欢喝茶
  5. 挪威人住在左边的第一个房子里
  6. 绿房子在白房子的右边
  7. 摄影师养了一只蜗牛
  8. 外交官住在黄房子里
  9. 中间那个房子的人喜欢喝牛奶
  10. 喜欢喝咖啡的人住在绿房子里
  11. 挪威人住在蓝色的房子旁边
  12. 小提琴家喜欢喝橘子汁
  13. 养狐狸的人所住的房子与医生的房子相邻
  14. 养马的人所住的房子与外交官的房子相邻

这道题的解题关键在于,要以一种清晰的方式将每个房子相关的属性(国家、颜色、工作、宠物、饮料、编号)列出来,前面5个提示中包含了5个国家,那么可以利用这一点画出一个表格,每一行表示一个国家,每一列表示房子的一种属性。一步步根据提示得到一些推论,将结果填入表格,答案便渐渐清晰起来,使用这种人工方式推理的结果如下图所示: 爱因斯坦逻辑难题结果

虽然我们知道了解题的关键,但这个问题仍然需要经过很多步的推导才能得出结果,如果使用 prolog 那得到这个问题的答案就简单多了,只需要定义好事实和规则,然后向 prolog 提出问题,逻辑引擎就会为我们查出结果来。

下面是解决这个问题的 prolog 代码。

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
42
43
44
45
house(A,[A,_,_,_,_]).
house(A,[_,A,_,_,_]).
house(A,[_,_,A,_,_]).
house(A,[_,_,_,A,_]).
house(A,[_,_,_,_,A]).

right(A,B,[A,B,_,_,_]).
right(A,B,[_,A,B,_,_]).
right(A,B,[_,_,A,B,_]).
right(A,B,[_,_,_,A,B]).

middle(A,[_,_,A,_,_]).

first(A,[A,_,_,_,_]).

neighbor(A,B,[A,B,_,_,_]).
neighbor(A,B,[_,A,B,_,_]).
neighbor(A,B,[_,_,A,B,_]).
neighbor(A,B,[_,_,_,A,B]).
neighbor(A,B,[B,A,_,_,_]).
neighbor(A,B,[_,B,A,_,_]).
neighbor(A,B,[_,_,B,A,_]).
neighbor(A,B,[_,_,_,B,A]).

attr(Country,Pet,Color,Drink,Work).

all_houses(Houses) :-
    house(attr(britsh,_,red,_,_), Houses),
    house(attr(spain,dog,_,_,_), Houses),
    house(attr(japan,_,_,_,painter), Houses),
    house(attr(italy,_,_,tea,_), Houses),
    house(attr(norway,_,_,_,_), Houses),
    first(attr(norway,_,_,_,_), Houses),
    right(attr(_,_,white,_,_), attr(_,_,green,_,_), Houses),
    house(attr(_,snail,_,_,photographer), Houses),
    house(attr(_,_,yellow,_,diplomat), Houses),
    middle(attr(_,_,_,milk,_), Houses),
    house(attr(_,_,green,cafe,_), Houses),
    neighbor(attr(norway,_,_,_,_), attr(_,_,blue,_,_), Houses),
    house(attr(_,_,_,orange,violinst), Houses),
    neighbor(attr(_,fox,_,_,_), attr(_,_,_,_,doctor), Houses),
    neighbor(attr(_,horse,_,_,_), attr(_,_,_,_,diplomat), Houses),

    house(attr(_,zebra,_,_,_), Houses),
    house(attr(_,_,_,water,_), Houses).

在事实部分,将房子看做一个整体,描述了房子在5所房子中、房子的左右关系、中间的房子处于什么位置、第一所房子处于什么位置、房子间的相邻关系以及每所房子拥有哪些属性。
规则部分包含了对题目中提示的描述和最终问题的描述,这些定义是为了告诉逻辑引擎,在求值时必须满足这些条件。
最终的查询为 all_houses(A) ,prolog 逻辑引擎将会查找出满足结果的房子数组,注意每所房子由它的属性组成,这样最后得到的结果为:

[attr(norway, fox, yellow, water, diplomat),
attr(italy, horse, blue, tea, doctor),
attr(britsh, snail, red, milk, photographer),
attr(spain, dog, white, orange, violinst),
attr(japan, zebra, green, cafe, painter)] .

我读《程序员的职业素养》

| Comments

Bob 大叔的《代码整洁之道》一书,曾经让我受益匪浅,这本书从编码上很小的细节着手,讲解如何编写出高质量的代码。最近我又拜读了 Bob 大叔去年出的一本新书《程序员的职业素养》,这本书未深入讲解编程的知识细节,而是从程序员工作的角度描述哪些行为是不专业的、哪些行为可能会导致项目延期、哪些行为会导致工作效率低下等等事情,其中也有很多非常好的专业建议。

提升职业素养,使自己成为一个专业的程序员,这并非一件简单的事情,技术能力、沟通技巧、工作态度等等都构成了评价一个员工专业性的因素,缺少任何一个可能都会令我们显得不够专业,如果能把书中的理论知识持之以恒地应用到工作之中,可以说对我们的职业生涯将大有裨益。

我对书中的一些关键点做了一些摘要,以便自己可以经常回过头来看一看,思考自己是否有将理论和实践结合起来以及怎样才能做的更好。培根在《论读书》中说过,有的书可以请人代读,然后看他的笔记摘要就行了,但这只应限于不太重要的书,否则一本书将像被蒸馏过的水,变得淡而无味了。我下面列举的摘要,就像是蒸馏过的水一样,虽然读起来没什么味道,但是一些观点确实是引人深思的,希望对大家有所帮助。

第一章 - 专业主义

  1. 专业主义意味着担当责任
  2. 不行损害之事
    不破坏软件功能,失误率不可能等于零,但专业的程序员有责任让它无线接近零;让QA找不出任何问题,发送明知有缺陷的代码,这种做法是极其不专业的,记住那些你没把握的代码都可以看做是有某种缺陷的,QA不是抓虫机器。自动化QA——单元测试和验收测试;不要破坏结构,无情重构和坚持代码设计原则。
  3. 职业道德
    工作时间用来解决雇主的问题,而不是你自己的问题;了解工作领域;坚持学习。
  4. 练习——软件开发技能和效率也可以通过训练来提高,可以坚持重复做一些简单的练习,这种练习可称作“卡塔”,卡塔的形式是一个有待解决的简单编程问题,做卡塔的目的不是找出解决方法,而是训练你的手指和大脑。
  5. 合作
    一起编程、一起练习、一起设计、一起计划。
  6. 辅导——教学相长。
  7. 了解业务领域
  8. 与雇主保持一致,站在雇主的角度来思考。
  9. 谦逊
    对自己的能力充满自信,并因此勇于承担有把握的风险;清楚自己的自负。

第二章 - 说“不”

“能就是能,不能就是不能。不要说试试看。”

  1. 对抗角色
    掌握沟通技巧,杜绝说“试试看”,交流应该保持明确性,当可能存在风险时,保持自己的立场。
  2. 高风险时刻
    最需要说“不”的是那些高风险的关键时刻,把最明确的信息传递给上级。
  3. 要有团队精神
    真正为团队努力的人,会根据自己最好的能力状况,明确说明哪些是做得到的事,哪些是做不到的事。
  4. 说“是”的成本
    急于完成,必然要做出某些牺牲,比如代码质量。

其他:有时候,提供太多细节,只会招致更多的微观管理

第三章 - 说“是”

  1. 承诺用语
    口头上说。心里认真。付诸行动。
  2. 识别缺乏承诺的征兆
    话语中包含“需要/应当”、“希望/但愿”、“让我们”等词语时,很可能是缺乏承诺的征兆
  3. 真正的承诺听起来是怎样的
    关键在于,你对自己将会做的某件事做了清晰的事实陈述,而且还明确说明了完成期限/时间。
  4. 信守承诺将使你成为一名严谨负责的开发人员
  5. 专业人士给出肯定回答时,会使用承诺用语,以确保各方能明白无误地理解承诺内容。

第四章 - 编码

  1. 做好准备
    代码要能正常工作、解决问题、跟现有系统结合、具有高的可读性。
  2. 确保将睡眠、健康和生活方式调整到最佳状况,这样才能在工作中全力以赴,产生优秀的代码。
  3. 流态区
    这是一种编码时会进入的一种意识高度专注但思维视野却会收拢到狭窄的状态,在这种状态下能够进行非常高效的代码编写,但它也有不利的一面,那就是思维视野收拢,此时可能无法估计到代码的全局层面。
  4. 中断、阻塞
    中断会打断思维,而阻塞是一种无法思考的状态,结对编程可以从某些方面表面这些问题,同伴能在被中断之后恢复之前被打断的思维,同伴也能在你思维阻塞的时候重新激活你的思维。
  5. 创造性输入
    广泛地阅读,学习各种各样的知识,能激活人的创造力和增加很多灵光一闪的机会。
  6. 保持节奏
    要适当分配精力,不要一直困在解决问题的状态中,暂时从问题中脱离出来,有助于大脑以不同且更具创造性的方式搜寻解决方案。
  7. 进度延迟
    根据目标定期衡量进度,使用三个考虑到多种因素的期限:乐观预估、标称预估、悲观预估。
  8. 无法完成任务时,不要轻易让步,不要让其他人抱有期望,不要盲目冲刺,若要加班,一定要提供说明加班失败的后备方案。
  9. 定义“完成”
    完成不是往源代码管理系统中提交已经写完的代码,团队应该对“完成”有一个清晰的定义,一般来讲,只有当代码完全通过验收测试,开发任务才算完成。

第五章 - 测试驱动开发

  1. TDD的三项法则
    编好失败的单元测试之前,不编写产品代码;
    一个单元测试失败,就不再编写单元测试代码;
    产品代码刚好让失败的单元测试通过,不要多写。
  2. TDD的优势
    确定性——通过测试能让你更加确信代码能正常工作 降低缺陷注入率
  3. 敢于重构的勇气
    拥有一套值得信赖的测试,便可完全打消对修改代码的恐惧。
  4. 文档
    遵循TDD的三项法则,所编写的每个单元测试都是一个示例,它们是最好的底层文档。
  5. 设计
    为了编写测试,必须找出函数跟其他函数解耦的方法; 事后写的测试只是一种防守,而现行编写的测试则是进攻,事后编写测试的作者已经受制于已有代码。
  6. TDD的局限
    即使有全面的测试,也有可能产生糟糕的代码,因为写出的测试代码可能就很糟糕,所以应当把测试代码当做产品代码一样认真对待。

第六章 - 练习

一句话总结:任何事情要想做得快,都离不开练习,重复编码过程,要求你迅速做出决定,熟练之后大脑可以放心思考更高层次的问题。

第七章 - 验收测试

  1. 验收测试定义为业务方与开发方合作编写的测试,目的在于确定需求已经完成。
  2. 自动化
    验收测试应当自动进行,这可以节省不必要的成本。
  3. 持续集成
    务必确保在持续集成系统中,单元测试和验收测试每天都能运行。 保持持续集成系统的正常运行是非常重要的,集成一旦失败,应当立即处理。

第八章 - 测试策略

  1. 业务人员编写针对正常路径的测试(happy-pathtest),QA则编写针对极端情况(corner)、边界状态(boundary)和异常路径(unhappy-path)的测试。
  2. 自动化测试金字塔
    使用测试驱动开发来产生单元测试;
    验收测试定义系统需求;
    持续集成保证质量稳步上升;
    系统测试确保正确的系统构造;
    人工探索式测试确保在人工操作下表现良好,同时富有创造性地找出尽可能多的“古怪之处”。

第九章 - 时间管理

  1. 关于会议的真理:会议是必须的;会议浪费了大量的时间。
  2. 谨慎选择应当参加哪些会议,必要时礼貌拒绝。
  3. 避免消耗注意力点数
  4. 避免优先级错乱

第十章 - 预估

  1. 不同的人对预估有不同的看法
    很多时候,业务方觉得预估就是承诺,开发方认为预估就是猜测,两者相差迥异。
  2. 预估是一种猜测,它不是定数,预估的结果是一种概率分布。

第十一章 - 压力

  1. 在压力下保持冷静的最好方式,便是规避会导致压力的处境,例如不要轻易承诺。
  2. “快速且脏乱”的抛弃型代码,是自相矛盾的说法,脏乱只会导致缓慢,让系统、代码和设计尽可能整洁,就可以避免压力。
  3. 危机中的纪律
    如果你遵守的纪律原则是工作的最佳方式,那么即使处于危机之中,也应当坚决秉持这些纪律原则。
    如果你在危机中放弃TDD,那说明你并不真正相信TDD是有帮助的。
  4. 应对压力
    不要惊慌失措;保持沟通;依靠纪律原则;寻求帮助。

第十二章 - 协作

  1. 程序员与人
    我们并非是因为喜欢和人们在一起工作才选择做程序员。整个群体的平均状况便是如此,我们,程序员们,还是最享受面无表情的沉思,沉浸于问题思考中。
  2. 程序员与程序员
    不正常的团体最糟糕的症状是,每个程序员在自己的代码周边筑起一道高墙,拒绝让其他程序员接触到这些代码。这是招致灾难的“最佳秘诀”。
  3. 结对
    结对工作是分享知识的最好途径,也是复查代码的最好方式,系统中不应该包含未经其他程序员复查过的代码。

也许我们不是因为通过编程可以和人互相协作才选择从事这项工作,但真不走运,编程就意味着与人合作。

第十三章 - 团队与项目

  1. 成员克服个体差异,默契配合,彼此信任,才能形成真正有凝聚力的团队,当然这需要一个“发酵”的过程。
  2. 专业的开发组织会把项目分配给已形成凝聚力的团队,而不会围绕着项目来组建团队。

《代码整洁之道》与 Python 之禅

| Comments

Bob 大叔在《代码整洁之道》一书的前言打趣着说,当你写的代码在经受代码审查时,如果审查者愤怒的吼道“What the fuck is this shit?”或者“Dude, What the fuck?”等言辞激烈的词语时,那说明你写的是 Bad Code;如果审查者只是漫不经心的吐出几个“What the fuck?”,那说明你写的是 Good Code。这就是衡量代码质量的唯一标准——每分钟骂出“What the fuck?”的频率。

想写出整洁的代码很难,有一部分原因在于糟糕的代码太容易编写。想快点完成任务时,考虑不周全时,忽略安全时,随意命名时,参数过多时,嵌套太深时,未及时更改注释时,违反法则时,重复你自己时等等情形,我们有太多的机会来制造糟糕的代码。只有严肃对待自己的代码,了解哪些事情会使我们的代码变味,才有可能写出整洁的代码。

写代码和写文章在某种程度上有相似之处,好的文章一定有好的可读性,写代码也一样,只有优美干净的代码才能具有良好的可读性。编写具有可读性的代码不光是保持有意义的命名就行,如果你想成为一名更好的程序员,写代码时你需要注意的有很多,比如:

  1. 规范本地变量的位置
  2. 使函数尽量短小
  3. 调用者尽可能放在被调用者上面
  4. 保持代码拥有良好的格式
  5. 编写只做一件事的函数
  6. 函数参数不要超过三个
  7. 暴露时序耦合
  8. 使用异常代替返回错误码

除此之外,你还须牢记众多设计原则,如:

  1. 开放封闭原则(OCP)
  2. 迪米特法则
  3. 依赖倒置原则(DIP)
  4. 单一职责原则(SRP)
  5. 里氏替换原则(LSP)
  6. 不要重复(DRY)
  7. 你不会需要它(YAGNI)

当然仅有这些是不够的,这不是骑自行车,学写整洁代码得花许多功夫,必须不断实践,从失败中提取代码的坏味道并从中得到启发。

编写整洁代码,你需要牢记并遵守很多东西,但这并不是循规蹈矩和刻板,而是对简单之美、代码之美的追求。代码整洁之道,是编写优秀代码的一种方法,其核心是尽力使代码保持简单——Keep It Simple, Stupid。判断一个人写的代码的好坏,不是看它的代码写的有多复杂,而是看他有没有把复杂的事物抽象出来并用简单的方式去描述它,此外这个人对代码的态度也至关重要,大多数时候我们并不能从一开始就把代码写的很完美,当我们需要快速做出一个原型,或者一开始代码看起来不错,但新的需求使现有的设计无法满足,如果不对设计进行改动的话,那么代码就会变的丑陋,如果你热爱自己正在做的事情,崇尚代码之美,那么你就会有足够的动力去重构它、完善它,而不是破坏结构使代码腐烂。

保持简单、追求简单,我想这就是编码之中的禅,一种回归本真的境界。这种禅意在 Python 的设计哲学中体现的淋漓尽致,在 Python 解释器中输入“import this”,便会出现经典的 Python 之禅。

  • Beautiful is better than ugly.
    优美胜于丑陋。
  • Explicit is better than implicit.
    显式胜于隐式。
  • Simple is better than complex.
    简单胜于复杂。
  • Complex is better than complicated.
    复杂胜于难懂。
  • Flat is better than nested.
    扁平胜于嵌套。
  • Sparse is better than dense.
    分散胜于密集。
  • Readability counts.
    可读性应当被重视。
  • Special cases aren’t special enough to break the rules. Although practicality beats purity.
    尽管实用性会打败纯粹性,特例也不能凌驾于规则之上。
  • Errors should never pass silently. Unless explicitly silenced.
    除非明确地使其沉默,错误永远不应该默默地溜走。
  • In the face of ambiguity, refuse the temptation to guess.
    面对不明确的定义,拒绝猜测的诱惑。
  • There should be one– and preferably only one –obvious way to do it.
    用一种方法,最好只有一种方法来做一件事。
  • Although that way way not be obvious at first unless you’re Dutch.
    虽然一开始这种方法并不是显而易见的,但谁叫你不是Python之父呢。
  • Now is better than never. Although never is often better than right now.
    做比不做好,但立马去做有时还不如不做。
  • If the implementation is hard to explain, it’s a bad idea.
    如果实现很难说明,那它是个坏想法。
  • If the implementation is easy to explain, it may be a good idea.
    如果实现容易解释,那它有可能是个好想法。
  • Namespaces are one honking great idea – let’s do more of those!
    命名空间是个绝妙的想法,让我们多多地使用它们吧!

道着重于方法,禅着重于态度,让我们把这两者相结合,做一个有追求的程序员,为成为软件匠人而奋斗吧。

七周七语言之用Io编写领域特定语言

| Comments

Io 语言是在2002年创造出来的,虽然距今已经有11个年头了,但是对于一门编程语言来说,它还只能算是一门年轻的语言。Io 并不是主流编程语言,没有什么名气,就连名字取的也并不适合推广,io 有太多其他的含义了,用google直接搜索 io 的话,很难找到关于这门编程语言的资料,英文推荐搜索“io language”,中文则建议搜索“io 编程语言”。

Io 的优缺点

Io 是个小众语言,文档不足,这是其中的一个缺点,但是这门语言吸收了许多语言的特点,具有很高的学习价值。Io 的设计借鉴了 SmallTalk、Self、NewtonScript、Act1、LISP和 Lua 的一些特点,它是一门基于原型式设计的编程语言,具有面向对象特性,语法概念极少,它没有关键字,所以能够在很短的时间内熟悉语法,Io 还有出色的并发库,利用它能够以简单直观的方式编写并发程序。

Io 有很少的语法规则和语言特性,这似乎在某种程度上束缚了它的能力,但是使用 Io 可以很容易地扩展这门语言自身的语法,我们可以自定义运算符或改变运算符的行为,也可以使用元编程在任何时候改变任何对象的行为。正是这些高扩展性和可定制性使得 Io 在创造领域特定语言(Domain Specific Language,简称DSL)方面具有非常强大的能力,我们可以利用它创造出自己喜欢的语法,编写出能够编写程序的程序,学习 Io 颇具启发性,怎样利用 Io 打造出能够提高编程效率的 DSL 更是一个值得深入研究的课题。

什么是领域特定语言?

Martin Flower 在《领域特定语言》一书中给出的定义是:针对某一特定语言,具有受限表达性的一种计算机程序设计语言。DSL 不是图灵完备的,所以它的表达能力有限;由于它是领域特定的,它的出现往往是为了消除业务领域的复杂性,所以 DSL 一般是整洁的、易于使用且接近自然语言的。我们平时在编码的过程中其实会接触到很多领域特定语言,比如 CSS、HTML、XML、JSON、正则表达式、XAML,他们都是针对某一领域的,“求专不求全”,CSS用于定义Web页面样式;HTML用于定义Web界面元素;XML、JSON用于描述数据,这些数据可以作为纯数据,也可作为某种行为的配置。

DSL 从实现角度分为内部DSL和外部DSL,用XML来描述某种行为就是一种常见的外部DSL的形式,利用编程语言自带的语法结构定义出来的 DSL 称为内部DSL,比如时下流行的流畅接口(Fluent Interface)编码方式就是一种内部DSL。上面说到 Io 编程语言可以自定义运算符且具有元编程特性,因此 Io 可以方便的创造出内部DSL,下面我们就通过几个示例来看一看 Io 在这方面的能力。

使用 Io 创造内部DSL

先看一个比较简单的例子,可以说在所有的编程语言中都有列表和字典(一些语言中也称为映射)这两种数据结构,在 io 中也不例外,io 中创建列表对象可以使用 list 函数,比如 foo := list(1,2) ,这很方便,然而创建一个字典的写法却比较繁琐,你需要 clone 一个 Map 对象,然后依次调用 atPut 方法向其中添加键值对,作为一个常用的数据结构,这样写不仅会影响效率而且会使代码变的臃肿,纵观其他编程语言,大部分都有相应的字典初始化语法糖,常用的写法为 {“key”:”value”} 结构,下面我们就在 io 中来创造这种语法糖。

dict.io
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
OperatorTable addAssignOperator(":", "atPutNumber")

Map atPutNumber := method(
  self atPut(
    call evalArgAt(0) asMutable removePrefix("\"") removeSuffix("\""),
    call evalArgAt(1)
  )
)

curlyBrackets := method(
  data := Map clone
  call message arguments foreach(arg,
    data doMessage(arg))
  data
)

squareBrackets := method(
    arr := list()
    call message arguments foreach(arg,
        arr push(call sender doMessage(arg))
    )
    arr
)

doFile("dict_sample.io")

先简单说明一下上面这段代码,OperatorTable 中保存了可用的操作符号表,我们可以使用 addAssignOperator 方法向其中添加用于赋值的操作符号,并将其映射到第二个参数所指定的方法中,第一行代码的意思为将 : 符号映射到 atPutNumber 方法中,当在代码中碰到 : 符号时,将调用 atPutNumber 方法;curlyBracketssquareBrackets 为 io 中的特殊变量,将方法赋值给它们相当于改变 {} 和 [] 符号的行为,在我们的代码中,指定了当碰到 { 符号时初始化 Map 对象,接着循环处理参数,这里的参数是用 , 符号分段的,data doMessage 方法相当于以data为上下文执行参数中的代码,参数代码中若碰到 : 符号则会调用 atPutNumber 方法,将符号左边的参数当作键,右边的参数当作值存入data中,最后返回data。

注意,最后一行的 doFile 方法是执行另一文件中的代码的意思,为什么这个例子要分成两个文件呢?主要是因为虽然我们在操作符号表里面添加了一个符号,但是在这个文件的执行上下文中,是不会起效果的,只有调用 doString 或 doFile 才会起效,所以将创建字典的代码放在了下面这个文件中。

dict_sample.io
1
2
3
4
5
6
7
dict := {
  "name": "lcomplete",
  "array": [1, "str"],
  "nested": {
    "hello" : "io"
  }
}

很简洁吧,而且还支持嵌套定义哦。

通过第一个例子,相信各位已经能看出 io 在创造内部dsl上的灵活性了,接下来我们看一个更强大的例子,使用 io 让编写html变的更有禅意,何谓禅意,其实是受到 Sublime 上的 ZenCode 插件的启发,使用 ZenCode 语法可以快速的编写 html 片段。这次我们先看一下使用 dsl 的方式。

enhance_html.io
1
zencode(Html html>head+body>div!content$id@class*3)

不难发现,这个语法已经跟宿主语言几乎没有任何关系,是一门彻底的内部dsl,上面这段代码生成的 html 如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<html>
  <head>
  </head>
  <body>
    <div id="id" class="class">
      content
    </div>
    <div id="id" class="class">
      content
    </div>
    <div id="id" class="class">
      content
    </div>
  </body>
</html>

很强大吧,一行代码就可以生成出这么多的 html,现在我们再看看这个 dsl 是如何定义出来的。

zencode.io
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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
forward := method(
    call message name
)

root := nil

Html := Object clone do(
    OperatorTable addOperator("@", 15)
    OperatorTable addOperator("$", 15)
    OperatorTable addOperator(">", 15)
    OperatorTable addOperator("+", 15)
    OperatorTable addOperator("*", 15)
    OperatorTable addOperator("!", 15)

    @ := method(classname,
        attrs append(" class=\"#{classname}\"" interpolate)
        self
    )

    $ := method(id,
        attrs append(" id=\"#{id}\"" interpolate)
        self
    )

    > := method(tag,
        html := Html clone
        html tag := tag
        html parent := self
        self childs append(html)
        html
    )

    + := method(tag,
        html := nil
        if(self parent,
            html = Html clone
            html tag := tag
            self parent childs append(html)
        )
        html
    )

    * := method(count,
        self count := count asNumber
        self
    )

    ! := method(content,
        self content := content
        self
    )

    forward := method(
        name := call message name
        html := Html clone
        html tag := name
        root = html
        html
    )

    init := method(
        self childs := list()
        self attrs := list()
        self count := 1
        self parent := nil
        self content := nil
    )
)

Html flush := method(
    root render("")
)

Html render := method(indent,
    self count repeat(
        writeln(indent,"<",self tag,self attrs join(""),">")
        childIndent := indent .. "  "
        if(self content,
            writeln(childIndent,self content)
        )
        self childs foreach(index,arg,
            arg render(childIndent)
        )
        writeln(indent,"</",self tag,">")
    )
)


zencode := method(html,
    html flush
)

doFile("enhance_html.io")

有几处需要说明一下,forward 是一个特殊方法,相当于 ruby 中的 method_missing ,当调用的方法不存在时,系统将会把调用转向到 forward 方法,这就给了我们很大的灵活性,我们无需定义 html、head 等方法,这些未定义的方法会自动转向到 forward 中处理;注意,一共有两个 forward 方法,分别应用在 Object 和 Html 对象上,只有 Html html 会返回 Html 对象,其他的未定义方法会转向到 Object 的 forward 方法上,这里直接将方法名作为字符串返回,新添加的 @、$ 等操作符,将会把接受到的字符串进行处理,最后再返回一个 Html 对象以进行链式调用。

总结

在 Io 中可以轻松地定义操作符、实现动态方法或更改所有对象的行为,这些能力是强大的,也是危险的,使用之前需要仔细衡量。当然,如果只是用它来做某一件事情,比如用来编写上面的 ZenCode 时,那么可以大胆发挥自己的想象力,在它的基础上构建一门属于自己的语言。

在windows下通过cygwin使用octopress

| Comments

在windows下使用git和octopress还真是个麻烦事,本文记录了在windows下通过cygwin安装git和使用octopress的过程,希望对大家有所帮助。

准备工作

  1. 下载 cygwin
  2. 下载 ruby 1.9.3 注:需要使用RubyInstall的方式进行安装,可在该下载页面可找到1.9.3的版本,Octopress依赖ruby1.9.3版本,其他版本可能会出现兼容性问题。
  3. 下载 DevKit

安装cygwin

安装cygwin时,默认是不安装git的,因此需要在安装时选中git,如下图所示: cygwin中安装git 另,在使用github时,需要提供ssh key,所以在安装时也需选中openssh包。

安装ruby和DevKit

安装ruby时,记得选中“Add Ruby executables to your PATH”,将ruby的执行路径添加到环境变量中,安装完后接着安装DevKit。
注意,安装DevKit这一步是必需的,因为若不安装的话,后面安装其他依赖时会出错,首先解压下载下来的DevKit包,在解压的文件夹下打开命令行执行以下命令即可完成安装。

1
2
ruby dk.rb init
ruby dk.rb install

获取octopress并安装相关依赖

首先从github上获取octopress源代码,不知道如何使用github的可以参考 Windows 下使用Git管理Github项目 这篇文章。 若没有自己的分支可clone主干版本。

1
2
$ git clone https://github.com/imathis/octopress.git octopress
cd octopress

在cygwin中使用gem等命令时,会出现找不到路径的错误,此时可以使用gem.bat命令,但每次写.bat着实是件麻烦事,可通过在.bash_profile中设置别名来解决这个问题。

1
2
3
alias gem=gem.bat
alias irb=irb.bat
alias rake=rake.bat

安装其他依赖

1
2
3
gem install fast-stemmer
gem install bundler
bundle install

fast-stemmer是执行bundle install时所需的一个依赖项,若不安装的话则bundle install命令执行到大半时会失败,这个时候需要根据提示安装其他依赖项,再重复这个步骤,若出错再重复,在天朝这么坑爹的网络环境下,有时又会出现无法下载的情况,真是好不折腾。若使用goagent,在无法下载时,可设置shell通过代理上网,命令如下。

1
export http_proxy=127.0.0.1:8087

使用octopress

安装octopress主题

1
rake install

在本地进行预览

1
rake preview

在windows下使用octopress,会有一些中文编码方面的问题,还需要做一些设置。
在.bashrc中添加下面的键值对

1
2
LANG=zh_CN.UTF-8 
LC_ALL=zh_CN.UTF-8

在ruby安装路径下找到lib\ruby\gems\1.9.1\gems\jekyll-0.12.0\lib\jekyll\convertible.rb这个文件,查找File.read,将该行代码修改为

1
self.content = File.read(File.join(base, name), :encoding => 'utf-8')

还有其他2个rb文件需要采用同样的方式进行修改,当在命令执行出错时可查看trace,并找到相关的文件进行修改。

关于如何使用octopress在github上搭建blog,你可以查看 使用Octopress + Github管理blog 这篇文章。

总结

在windows下使用为linux而生的git和为hacker而生的octopress,确实没有在ubuntu下那么方便,不过还好这些工具都是开源的,在碰到问题的时候,可以自行修改代码来解决,这确实体现了开源的优越性。昨天也在mac下搭建了octopress的blog编写环境,由于我的mac系统版本自带的ruby是1.8.7版本,需要通过rvm来安装1.9.3版本,安装的时候也折腾了一番,对比一下,发现还是在ubuntu下使用octopress最为便捷。