谈谈分支与合并

branch
对于开发人员来说,经常会碰到这样的难题:某一个特性A正在开发之中,突然有了新的需求特性B,或者有紧急的bug C需要修改。如果B或C的重要性排在A之后,那么没有问题,等A开发完上线之后再来开发B或者C就可以了。但是通常情况是B和C的重要性要高于A。这种情况下,已经开发了一半的A特性的代码该何去何从?

branch_simple
代码目录拷贝

最简单的办法是把当前A特性的代码拷贝一份出来,放到别的目录下面,然后再修改原来的代码以支持B特性或者解决C bug,等这个版本改完后再把之前拷贝出来的A特性代码合并回去。但是拷贝出来的这份代码是不受版本控制的,中间的任何改动都没有记录,与使用版本控制的思想背道而驰。另外合并代码是个非常关键的操作,如果只有一个分支,手工合并问题也不大。但如果分支一多,就容易犯错了。

另一种办法就是使用版本控制软件自身的分支功能。

SVN branch

SVN的分支功能比较鸡肋,与上面的第一种办法相比好不到哪里去。使用svn的代码库下面会有trunk和branch这两个目录,trunk下的代码是主干版本,branch下则是分支版本。创建一个新分支就是在branch下面拷贝一份代码而已。与上面的方法相比,唯一的一点进步就是每次改动可以提交到代码库了。但是这种方式仍然非常的笨重,每次都要把代码拷贝一份到branch目录下。
branch_svn
在分支的处理上,SVN确实比较差。可能在设计之初就没有考虑到多分支并行的情况。所以SVN正确的使用方式就是线性的提交。更多的讨论可以参考stackoverflow上的这个回答:So why did Subversion merges suck?

Git branch

接下来再看看Git的分支是如何工作的。Git中的分支与svn的分支从概念上有着天壤之别。svn中的分支只是代码的拷贝,git中的分支本质上是指向某次commit的指针。如下图中一共有master, feature_b, bug_c三个分支,分别指向不同的提交。
branch_git

这样设计的好处是分支的操作灵活了很多,创建新分支只要增加一个指针就完成了。比起复制整个代码库快了很多。另外git切换分支的操作是在当前工作目录进行的,不需要像SVN那样单独建一个branch目录。切换分支非常方便。

Git与SVN一个关键的区别在于SVN提交历史的数据结构是树形结构,一个节点只能有一个父节点。而Git是有向无环图(DAG),一个节点可以有多个父节点。

数据结构差异
数据结构差异

三方合并

DAG这样的数据结构让Git可以追踪每一个分支的来源,所以Git在合并分支时可以实现自动的三方合并,而SVN只能是两方合并(或者需要人工介入的三方合并)。

假设有一个函数,分支1在函数开始加了一句log,分支2在函数末尾加了一句log。如果使用两方合并,是无法自动完成合并的。因为它并不知道要合并成什么样子,是应该保留第一行的log,还是保留最后一行?这种情况就会需要人工介入来解决冲突。如果类似的改动很多,那么合并的效率是非常低下的,而且容易出错。

branch_2ways

Git在做分支合并操作时,会根据提交历史的DAG计算出两个分支最近的公共祖先,然后根据公共祖先和两个分支做一次三方合并。这种合并方式大大减少了人工介入,减轻了合并的工作量,保证了合并分支的准确性。
branch_3ways

Git与SVN协作

Git在分支处理上有诸多优点,但很多时候整个团队采用的版本控制是SVN,这种情况下有没有办法享受到Git的各种优点呢?办法总是有的,只不过会有所牺牲,无法做到跟原生使用Git一样。

一种办法是使用git svn命令。官方文档里面是这么描述的:

Git 最为重要的特性之一是名为 git svn 的 Subversion 双向桥接工具。该工具把 Git 变成了 Subversion 服务的客户端,从而让你在本地享受到 Git 所有的功能,而后直接向 Subversion 服务器推送内容,仿佛在本地使用了 Subversion 客户端。也就是说,在其他人忍受古董的同时,你可以在本地享受分支合并,使暂存区域,衍合以及 单项挑拣等等。这是个让 Git 偷偷潜入合作开发环境的好东西,在帮助你的开发同伴们提高效率的同时,它还能帮你劝说团队让整个项目框架转向对 Git 的支持。这个 Subversion 之桥是通向分布式版本控制系统(DVCS, Distributed VCS )世界的神奇隧道。

非常让人激动是不是?好像是一个非常完美的解决方案。不过文档后面又写道:

习惯了 Git 的工作流程以后,你可能会创建一些特性分支,完成相关的开发工作,然后合并他们。如果要用 git svn 向 Subversion 推送内容,那么最好是每次用衍合来并入一个单一分支,而不是直接合并。使用衍合的原因是 Subversion 只有一个线性的历史而不像 Git 那样处理合并,所以 git svn 在把快照转换为 Subversion 的 commit 时只能包含第一个祖先。

git svn推荐的工作流程是使用衍合来进行分支的合并。我不太喜欢这种方法,最重要的原因就是通过衍合这种方式会修改提交历史,将本来是有向无环图的结构变成了线性的,那就跟svn没什么两样了。

除了git svn命令,还有一种方式。建立一个git repo,然后将svn当前最新版本的代码拷贝到git repo中。之后的开发工作都在git中进行,想拉分支就拉分支,想推送到远程仓库就推送。一个特性开发完成之后,把特性分支合并进master分支。然后把git中的master分支看作是SVN主干分支的镜像,每次git的master分支有进展时,把代码同步提交到SVN即可。

branch_co

这种方式的缺点是需要手工保持两个仓库的主分支同步,但换来了极大的便利性。你可以在这个Git repo里面随意施展你的技能,只要不把master分支玩坏就可以了。比如可以推送到远程仓库,建立多人协作开发的流程,这在之前的方法里面是无法做到的。

我喜欢用SourceTree作为Git客户端,图形界面优雅,支持多平台。最迷人的特性就是图形化的提交历史,每个分支是怎么来的,最后合并到哪里,这些信息都一目了然。
git-graph

Referrence

  1. Three-Way Merging: A Look Under the Hood
  2. Git 分支 - 何谓分支
  3. Git 与其他系统 - Git 与 Subversion

布隆过滤器(Bloom Filter)简介

如果在系统中需要判断某个数据是否存在于集合中,一种可能的办法是把每个数据都存在容器中,然后再来查询,例如使用std::map。但有些情况下我们只关心数据存在与否,而并不关心数据本身。这种方法就会造成空间的浪费,影响系统整体的性能。在这种场景下,布隆过滤器则是一种很高效的办法。

布隆过滤器是一种非常节省空间的概率性数据结构。它可以非常快速地告诉你两个结果,“数据有可能存在于集合中”或者“数据一定不存在于集合中”。但布隆过滤器不是万能的,“Everything comes at a price”,为了获得查找的高性能,必须付出一些代价。很明显,布隆过滤器的结果并不那么精准。所以布隆过滤器只适用于那些可以容忍False Positive,但坚决不容忍False Negative的场景。比如,HBase中就使用了bloom filter来优化系统对硬盘的读取[1]

布隆过滤器的数据结构就是一个m位的bit数组。另外有k个不同的hash函数,其中每个函数会将输入的内容映射到bit数组的其中一位上。剩下的事情就很简单了,如果是新插入一条数据,只要把内容传给k个hash函数,得到k个位置,然后将数组中对应位置为1。如果是查询某个数据是否存在,则把数据内容做k个hash,得到对应的bit位,如果bit数组中这些位上的值有一个为0,那么这条数据肯定不存在。如果全部为1,则说明有可能已经存在。

Bloom_filter1

上面是布隆过滤器的示意图,可以看到这个布隆过滤器的bit数组有18位,另外有3个hash函数。

至于是否在实际系统中使用,还是要看对应的场景需求。参考链接中也有实际应用场景的例子[2]


参考

  1. Quora - How are bloom filters used in HBase?
  2. Bloom filter usage
  3. Bloom Filters by Example
  4. What is the advantage to using bloom filters?

swift closure capture list

最近用swift写app,发现很多特性都很有意思,并且很实用,比如closure, Optional类型等等。closure可以看做是objc中的block,c++和python中的lambda,熟悉函数式编程的同学可以很快上手。

在进行异步的网络调用时,需要把closure作为参数传递给执行网络请求的函数。比如NSURLSessionDataTask,需要把网络请求执行完毕之后的逻辑写在completionHandler里面。

很多时候我们需要在UIViewContronller里面来执行网络请求,请求完成后再更新ui上的数据,那么这里的closure中肯定会访问到self.someProperty或者self.someMethod()。这样closure本身就有一个对self的强引用(strong referrence)。另外,closure在swift中跟class一样,也是引用类型(referrence type)。这样一来closure和self就互相引用,产生strong referrence cycle。closure和self都无法被deallocate,造成资源的浪费。

为了解决这个问题,就要用到closure的捕获列表。语法如下:

捕获列表中可以使用两种类型:unowned和weak。因为一开始没有弄清楚这两种的区别,直接在代码中使用了unowned self,造成app在运行过程中出现“attempted to retain deallocated object”错误。

这段代码中第29行可以看到,在closure中访问self时并没有判断self是否还存在。所以当网络请求执行完再更新ui的时候就会出现“attempted to retain deallocated object”。unowned和weak的一个区别在于weak self是optional类型。这样一来就可以利用optional chaining来做一个保护机制。正确的实现代码如下:

这里的29行使用了swift中的if let结构,如果photoView是有值的,则继续执行后面的逻辑,否则会走到else分支中。这样便很好的解决了上面的问题。

参考:

  1. StackOverflow - Shall we always use [unowned self] inside closure in Swift?
  2. Automatic Reference Counting
  3. Swift Optionals: When to use if let, when ? and !, when as? and as

机器学习入门

2015年年初,因为工作接触到了机器学习(Machine Learning)。最近一直在学习,算是一只脚跨进了机器学习的大门。与之前积累的技能比较,机器学习更偏向于学术,学习难度比偏向于工程方面的技术大的多。要想掌握机器学习,线性代数,概率与统计,积分这些都是必不可少的。学生时代因为年少无知,看不到这些课程存在的意义,所以根本不放在心上,现在后悔已经来不及了。没办法,“出来混迟早都要还的”,只能看公开课重新拾起来了。这也算对自己的一个挑战,希望可以在新的领域有所突破。

什么是机器学习?

Machine learning is actually a software method. It's a way to generate software. So, it uses statistics but it's fundamentally... it's almost like a compiler. You use data to produce programs.

通过最近的学习,已经有了对机器学习的简单认识。机器学习就是利用一些算法为已有的数据建立模型,得出数据的规律,然后根据新的输入数据做出预测或者决策。举几个例子:根据房产交易的数据(房屋面积、所处地段、是否学位房、成交价格等等),估算出另一套房子大概的售价;根据最近几年某只股票的走势和其间相关的事件预测未来走势;根据肿瘤细胞的特征评估是良性还是恶性的。最贴近生活的几个例子:根据你喜欢的音乐为你推荐新的音乐(网易云音乐做的最好);微博里面根据你的关注为你推荐新的用户;社交平台根据某一用户所有的行为数据预测什么广告最容易获得他的点击(qq,微信)。看到这些应用,相信你也会像我一样深深的被它迷住。如果看到这些你还没有心动,可以再看看这篇文章:Data Scientist: The Sexiest Job of the 21st Century

资源

近期积累了不少机器学习相关的教程和资料,整理出来发在这里。其实关于机器学习的资料有太多太多,以至于一开始的时候都出现了选择恐惧症,不知道该从什么地方开始。好在后来能够静下心来从最简单的开始。

公开课
1. 强烈推荐Coursera上的Machine Learning。这是机器学习的入门课程,作者是Andrew Ng,也是coursera的创始人。如果连这门课都没看过,实在是不好意思跟别人说自己懂机器学习。别看这门课简单,想从头到尾认真完成真不是件简单的事情,不仅仅是把视频看完,而且要把所有习题和编程题做完。我从2月份开始,一直看到现在(5.23),还剩下最后两章没有看完。但是已经超过了课程的deadline,拿不到课程certificate了。Andrew Ng讲得深入浅出,只要能仔细完成,机器学习就算是入门了。

2. 统计学的课程也是必须的。这里比较推荐Statistical Learning。Statistical Learning的作者是大名鼎鼎的The Elements of Statistical Learning(ESL)这本书的作者。ESL的难度比较大,不太适合入门,所以作者又写了另外一本面向初学者的An Introduction to Statistical Learning(ISL)。ISL可以作为课程的配套书籍来看,事半功倍。更重要的是ISL是免费的。

Coursera的ML公开课看的差不多之后,可以写点代码实际操练起来了。scikit-learn是一个很重要的机器学习库。本文题图就是sklearn官方给出的如何选择合适的机器学习算法的roadmap。sklearn的学习我推荐An Introduction to scikit-learn: Machine Learning in Python这篇教程,里面给出了如何用sklearn实现常用机器学习的算法。

其他还有些非常重要的资源:
1. Datatau:大数据界的Hacker News
2. Reddit的Machine Learning频道
3. Quora的Mechine Learning话题,另外推荐关注William Chen

也许用不了多少时间,奇点就会到来。所以能多了解点机器学习的事儿也是好事,加油!

2014,我看风景

去年拍了不少照片,挑了几张自己比较满意的放在这里,就当是对2014年的纪念。大多数都是出自RX100,只有一张是用手机拍的。RX100是台非常优秀的相机,尤其对我这种不懂摄影的菜鸟来说。虽然我也很口水那些高端单反和镜头,但我不希望成为一个只关注设备而忘记摄影本身的“摄影爱好者”。对于我来说,RX100已经足够了,只要它不坏掉,我愿意用一辈子(其实就是没钱的另一种说法)。

RX100胜在小巧,随身携带方便。当然我没玩过单反,这点纯属人云亦云和猜测,没有做过对比。某某大师曾经说过,一个牛逼的摄影师应该做到无论何时何地都随身带着相机,以便任何时刻看到美景(或者美女)都能端起相机就“啪啪啪”。我不够牛逼,所以我要朝着牛逼的方向努力。每天带着单反显然不太现实,但带着RX100就很轻松了。总之,这台小相机满足了我的所有需求,还带来不少惊喜。

20140517

2014-05-17,早晨爬莲花山,小雨。下山之后刚好雨停,出现了短暂的晴天。对面两个很有设计感的建筑物,左边是深圳图书馆,右边是深圳音乐厅。


20140524

2014-05-24,保利剧院


20140624

2014-06-24,腾讯大厦22楼。每天都要面对的窗口,虽然不起眼,但总会有不寻常的风景出现。很庆幸我能坐在这个位置写代码。


20140710

2014-07-10,同上


20140807

2014-08-07,同上


20140831

2014-08-31,红山地铁站。龙华线是港铁运营,地铁站完全是香港的风格,最显著的应该是那些马赛克。


20140831

2014-08-31,南方科大


20140913

2014-09-13,阳光棕榈园阳台


20141019

2014-10-19,半岛城邦的灯塔,半岛城邦是深圳蛇口的豪宅,小区环境确实好,这才是真正的海景房。


20141122

2014-11-22,阳光棕榈园楼顶。秋季很难遇到这么美丽的天空,刚好还有一架飞机飞过。


20141227

2014-12-27,深圳机场T3航站楼。用RX100来拍真的有点为难,没有广角很难表现出T3的气势。只能黑白处理了。


希望明年可以拍更多更好的照片。

objective-c protected instance variable

最近在做斯坦福公开课cs193p的课后练习Machismo,其中用到了类的继承。

// GameViewController.h
@interface GameViewController : UIViewController
@property (nonatomic) Game *game;
@property (strong, nonatomic) IBOutletCollection(UIButton) NSArray *cardButtons;
@property (weak, nonatomic) IBOutlet UILabel *scoreLabel;
@property (weak, nonatomic) IBOutlet UILabel *messageLabel;
@end

// CardGameViewController.h
@interface CardGameViewController : GameViewController
@property (nonatomic) CardMatchingGame *game;
@end

CardGameViewControllerGameViewController的一个子类。因为在子类中需要初始化@property (nonatomic) CardMatchingGame *game,所以就要在子类中访问基类中的instance variable,也就是_game。如果这样尝试的话,编译器会给出一个错误。因为_game是基类的private instance variable,子类是无法访问的。那如何才能将这个instance variable变为protected?只要将_game在interface中声明即可。

// GameViewController.h
@interface GameViewController : UIViewController {
    Game *_game;
}
@property (nonatomic) Game *game;
@property (strong, nonatomic) IBOutletCollection(UIButton) NSArray *cardButtons;
@property (weak, nonatomic) IBOutlet UILabel *scoreLabel;
@property (weak, nonatomic) IBOutlet UILabel *messageLabel;
@end

在interface中声明的变量,默认都是@protected。如果想定义为其他类型,可以使用@public@private等关键字。这点和C++还是类似的。更多详细内容请参阅下面的资料。

参考

  1. How are declared private ivars different from synthesized ivars?
  2. Are synthesized instance variables generated as private instead of protected?
  3. Objective-C Access To Instance Variables
  4. Private properties, methods and ivars in Objective-C
  5. Objective-C Tuesdays: instance variables

redis sds数据结构的指针技巧

redis中没有直接使用char *,而是使用自定义的sds结构来表示字符串。sds的定义如下:

typedef char *sds;
struct sdshdr {
    int len;
    int free;
    char buf[];
};

sdshdr中的len代表buf中字符串的长度(不包括结尾的''),free代表buf剩余可用的长度。这种结构与char *相比有什么优点,请参考Redis设计与实现

上面看到sds这种类型就是char *,那sds和sdshdr有什么关系?先来看生成sds的函数:

sds sdsnewlen(const void *init, size_t initlen) {
    struct sdshdr *sh;

    if (init) {
        sh = zmalloc(sizeof(struct sdshdr)+initlen+1);
    } else {
        sh = zcalloc(sizeof(struct sdshdr)+initlen+1);
    }
    if (sh == NULL) return NULL;
    sh->len = initlen;
    sh->free = 0;
    if (initlen && init)
        memcpy(sh->buf, init, initlen);
    sh->buf[initlen] = '\0';
    return (char*)sh->buf;
}

每当生成sds的时候,会先生成sdshdr,然后返回sdshdr->buf。原来sds就是sdshdr->buf,sdshdr的意思应该是“sds header”。sds相关的函数大多以sds作为参数的类型,而不是sdshdr,当需要用到sdshdr的时候,redis使用了一个技巧,以便快速的通过sds得到sdshdr。以sdslen这个函数为例:

static inline size_t sdslen(const sds s) {
    struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
    return sh->len;
}

通过将sds的地址减去sizeof(struct sdshdr),便得到了sdshdr的地址。redis的文档里面已经解释了这个技巧。假如我们通过sdsnewlen("redis", 5)生成了一个sds,其sdshdr的地址为sh,那么像下图所示:

-----------
|5|0|redis|
-----------
^   ^
sh  sh->buf

sh = sh->buf - sizeof(struct sdshdr)。看到这里也许会有疑问,sizeof(strcut sdshdr)不应该是sizeof(int) + sizeof(int) + sizeof(char *)吗?C99标准中有如下规定:

As a special case, the last element of a structure with more than one named member may have an incomplete array type; this is called a flexible array member. In most situations, the flexible array member is ignored. In particular, the size of the structure is as if the flexible array member were omitted except that it may have more trailing padding than the omission would imply.

However, when a . (or ->) operator has a left operand that is (a pointer to) a structure with a flexible array member and the right operand names that member, it behaves as if that member were replaced with the longest array (with the same element type) that would not make the structure larger than the object being accessed; the offset of the array shall remain that of the flexible array member, even if this would differ from that of the replacement array. If this array would have no elements, it behaves as if it had one element but the behavior is undefined if any attempt is made to access that element or to generate a pointer one past it.

sdshdr最后一个成员是未完整定义的数组类型,这种结构体的定义方式使buf成为可变数组成员。内核中也有flexible array member的应用:

 struct bts_action {
         u16 type;
         u16 size;
         u8 data[0];
 } __attribute__ ((packed));

struct bts_action *var = kmalloc(sizeof(*var) + extra, GFP_KERNEL);

这样就减少了kmalloc的调用次数。这就是通过将buf的地址减去sizeof(struct sdshdr)可以得到sdshdr地址的原因。

参考

  1. Flexible array members in C - bad?
  2. Size of a struct with two void pointers is 4?
  3. Hacking Strings

django+tornado

Django的部署方式有很多种:WSGI,FastCGI等等,当然最主流的还是WSGI。比如官方文档中推荐的django+apache+mod_wsgi, django+gunicorn,django+uWSGI等等。那么什么是WSGI?

其实这个问题也困扰了我很久。每当我想研究一下WSGI到底是何方神圣的时候,google给出的答案就是pep 3333,不过这篇像论文一样的pep 3333比较晦涩难懂,一直都没搞明白。直到后来发现了这篇文章。和其他的文章不一样,文章没有开头就讲WSGI是什么,反而开始就说WSGI不是什么:

What WSGI is not: a server, a python module, a framework, an API or any kind of software. What it is: an interface specification by which server and application communicate.

看到这里心中无比的敞亮,原来WSGI就是一种标准:application和server之间交互的标准。application就是通常所说的framework,server则接受客户端的请求并将请求发给application处理,然后将application的应答返回给客户端。现在应该明白,之前说的apache+mod_wsgi, gunicorn, uWSGI都是扮演WSGI Server的角色,只要兼容WSGI的server都可以用来和任意的python web framework搭配。

今天的主角是Tornado。它包含两部分:一个framework和一个server。由于tornado使用了异步非阻塞的IO模型,所以特别适用于长轮询的场景,性能比较强大。今天主要用tornado的http server来部署django应用。

tornado的wsgi模块包含了两个类WSGIApplication和WSGIContainer。WSGIApplication是把tornado作为framework,用其他的WSGI server来驱动。WSGIContainer则是我们需要的,把其他web framework的应用封装一下,以便在tornado server上运行。
继续阅读django+tornado

使用el-get管理emacs包

el-get是emacs的一个包管理器。你可以把它当作是emacs的synaptic、yum或者pacman,功能强大,用起来也十分方便。通过它你可以安装和管理各种elisp的包。对于我来说,el-get最大的功能在于,当你有多台机器时,仅仅需要一个.emacs文件就可以保证多个机器上的elisp包一致,而不是每个机器都需要手动管理。想想看,没有el-get的话,当你重装系统之后需要花多少时间来安装各种elisp包吧。有了el-get之后,只要一个.emacs文件,当你第一次打开emacs的时候,各种包已经开始默默的下载和安装了,完全不用你操心。这是一件多么美好的事啊。

el-get对软件包的描述放在recipe文件中,包括安装方式、url、安装完之后的操作,以及软件包的基本配置。也就是说当你用el-get安装完某个包之后,自己的.emacs文件基本上不用修改,新的包就可以用了。el-get支持多种安装方式,可以直接从github clone,也可以从指定的url下载回来然后解压,另外还支持ELPA,cvs,bzr,apt-get,pacman等等。最强大的地方在于你可以自己定义recipe,如果默认的recipe文件中没有你需要的包,那么你完全可以自己写一个。如果你在github上面fork了el-get,可以给作者提交pull request,请求将自己定义的recipe加入默认的recipe中。这样如果下次有别人需要用到这个包,你的recipe就派上用场了。

安装el-get非常简单,只要打开你的emacs,切换到scratch buffer,复制下面这段代码,然后在最后一个括号后面C-j就可以了。

(url-retrieve
 "https://raw.github.com/dimitri/el-get/master/el-get-install.el"
 (lambda (s)
   (let (el-get-master-branch)
     (goto-char (point-max))
     (eval-print-last-sexp))))

安装好el-get后,配置文件里面加入下面的内容

(setq
 el-get-sources
 '((:name asciidoc
          :type elpa
          :after (progn
                   (autoload 'doc-mode "doc-mode" nil t)
                   (add-to-list 'auto-mode-alist '("\.adoc$" . doc-mode))
                   (add-hook 'doc-mode-hook '(progn
                                               (turn-on-auto-fill)
                                               (require 'asciidoc)))))

   (:name buffer-move   ; have to add your own keys
           :after (progn
                    (global-set-key (kbd "<C-S-up>") 'buf-move-up)
                    (global-set-key (kbd "<C-S-down>") 'buf-move-down)
                    (global-set-key (kbd "<C-S-left>") 'buf-move-left)
                    (global-set-key (kbd "<C-S-right>") 'buf-move-right)))

   (:name smex  ; a better (ido like) M-x
          :after (progn
                   (setq smex-save-file "~/.emacs.d/.smex-items")
                   (global-set-key (kbd "M-x") 'smex)
                   (global-set-key (kbd "M-X") 'smex-major-mode-commands)))

   (:name lisppaste        :type elpa)))

(setq my-packages
      (append
       '(cssh el-get switch-window escreen vkill xcscope color-theme color-theme-railscasts color-theme-tomorrow yasnippet python-mode python-pep8 pymacs rope ropemacs ropemode pylookup haskell-mode anything helm emacs-w3m auto-complete browse-kill-ring sr-speedbar session popup markdown-mode xml-rpc-el)
       (mapcar 'el-get-source-name el-get-sources)))

(el-get 'sync my-packages)

上面的el-get-sources变量是自定义的recipe,my-packages则是需要安装的包。只要把包的名字写在my-packages里面,下次启动emacs的时候el-get就会自动安装。

使用el-get就是这么简单。但是在使用的过程中发现el-get还有些不完美的地方。比如用el-get安装color-theme这个插件的时候,总会提示下载的color-theme.tar.gz无法解压。经过一番调查发现原来是el-get的http-method有问题。el-get下载的color-theme.tar.gz总会比用wget下载的少几个字节。http-method的实现是先用url-retrieve把文件拉到当前的buffer中,再用write-file将当前buffer的内容写入文件。问题就出在write-file这里。write-file默认会对buffer进行一些编码的转换,结果导致丢掉了几个字节。修改的方法也很简单,只要将write-file替换为write-region即可。发现这个bug之后在github上给作者提交了pull request,已经被合并了。之后再有人用el-get就不会碰到这个问题了。

学习elisp

对我来说,Emacs已经成为日常用的最多的编辑器了,vi则只有在ssh的时候才会用到。不过emacs可是很需要调教的,如果没有好好配置,基本上处于不可用的状态。关于目前使用的emacs配置,改天会再详细写出来。今天只是分享一个自己写的函数。

我很庆幸,现在的我能够沉下心来去研究elisp这么神奇的语言。虽然elisp的用途很狭隘,但它绝对会对提升你的生产力产生巨大的帮助。emacs我用了很久,大概跟我用linux的时间一样长。但是从2012年开始我才算真正入了emacs的门。以前只是在网上搜罗各种配置文件,安装各种插件。但是配置却看不懂,做不到自己定制,只是简单的依葫芦画瓢。配置不适用也不知道该如何下手解决。

随着使用的深入,现在慢慢学会了如何解决遇到的emacs问题。搞定了以前一直很想解决的bug。有些bug,其实是emacs的某些feature没有配置好。比如之前python-mode中有时出现C-x C-f会卡死的情况,最后发现原来是网上广泛流传的python-mode配置中(setq ido-use-filename-at-point 'guess)这一句,导致打开文件的时候会调用python-mode-path函数。只要把这句注释掉即可。再比如python-mode中M-b的跳转总是不正确,在其他的major mode中M-b总是会把“_”当作单词的分隔符,但python-mode中却不会。这点实际使用中会造成很大的困扰,因为不仅和其他的major mode行为不一致,即便在python-mode中M-b和M-f的行为也不一致。后来到python-mode的项目主页上准备提交这个bug,结果发现已经有人提过了,原因是“_”在python-mode的syntax table中是属于word类而不是symbol类。现在python-mode中已经有了一个自定义的变量py-underscore-word-syntax-p,只要把此变量的值设为nil即可。

解决了前面的几个bug之后,开始想自定义几个函数已满足自己的需求。以前用惯了vim的yy复制一行的功能,现在emacs里面没有,非常的不适应,这个操作如果自己做,需要按太多次键盘。先要C-a跳到行首,再C-spc设置标记,再C-e跳到行尾。于是开始翻阅elisp的文档。发现elisp的文档写的真是太人性化了,目录编排尤其合理,即便是我这种从来没用过的人也可以对着目录找到自己所需要的函数或者变量。下面是复制一行的函数

;;copy line
(defun copy-line ()
  (interactive)
  (kill-ring-save (save-excursion
                    (back-to-indentation)
                    (point))
                  (line-end-position))
  (message "line copied"))
(global-set-key "C-cC-y" 'copy-line)

实现起来很简单,kill-ring-save的描述是“Save the region as if killed, but don't kill it”,即我们通常理解的“复制”。这个函数会接受两个参数,待复制区域的起始和结束位置。save-excursion的作用是再执行完函数参数内的操作后,把光标移动到执行之前的位置。save-excursion这段的作用是获得行的起始位置,略去前面的缩进。line-end-position很明显就是获得行的结束位置。这样就实现了复制一行的功能,再把这个函数绑定一个快捷键C-c C-y即可。

现在终于体会到了emacs的强大。emacs的各种插件还不足以让他成为神器,最强大的地方在与用户可以很容易的自己定制所需要的功能,而不用依赖于别人开发的插件。难怪有人说emacs是伪装成编辑器的操作系统了。