行者无疆 始于足下 - 行走,思考,在路上

Knight Rush——关于编程语言学习的一些思考

1 引子的引子

金庸小说里的大侠们在行走江湖时,向来都是衣来伸手饭来张口,一不小心心情郁闷了还可以来个金盆洗手、笑傲江湖,就是从来不会考虑钱的问题。我不是大侠,生活也不是小说,“退隐闭关”半年啃了几十本书后,生活还是要继续。这几日满京城地找工作,各种笔试面试聊技术聊人生,也让我对自己的技术体系和职业规划做了一次全盘扫描,快哉快哉。

2 引子

前几日面试了一家专注云计算的创业公司易云捷讯,一面是公司创始人 从美国打来的越洋电话,聊什么我已经记不太清楚了,其中有一道题目是关于Fibonacci数列的,这个恰好问到了我的G点上,于是我就将我在《SICP》 上学到的什么快速乘法啊、尾递归啊、迭代和递归啊啥的和盘托出,大大地吹嘘了一番,总之感觉还不错,算是了给面试官的一个小惊喜,就这样胡乱通过了一面。一面之后邮件沟通被分配了一道编程题目,大意如下1

在一个国际象棋棋盘上(N*N),有一个棋子"马(Knight)",处在任意位置(x, y);马的走法是日字型,即其可以在一个方向上前进或后退两格,在另一方向上前进或后退一格。请编程找出马如何从位置(x, y)走到棋盘右下角(N, N)的位置的步骤。例如:假设棋盘大小为3*3,马处在(1,2)位置,马只需要走一步,即 (1,2)->(3,3)即可到达目的地。编程语言推荐的是Python,程序能输出一种可行的方法即可(无需找出所有方式),还有一些软件工程的要求诸如文档、注释、代码风格这些“无关痛痒”的东西。

我是第一次在应聘时碰到这种不限时间自由rush的题目,坦白的讲,我还是比较喜欢这种形式的题目的。纸上谈兵的笔试有点八股,ACM般的机试时间又太紧,面对面的聊天或者刺刀见红般的白板写代码算是比较好的了解一个人技术水平的手段,但是这三种方式的考核方式其实都和真实的工作流程相左;而这种rush类型的题目基本上可以反应一个人工作能力——你可以利用google、可以去查算法书籍、可以在有限的时间内再去学习一些技术,唯一要求的就是诚信。

我看到此题的第一想法就是暴力试探法,因为对于knight p(x, y)而言,p(x-1,y), p(x+1, y), p(x, y-1), p(x, y+1)四个方向上相邻的点都是可达的。按此思路只需要“步步为营,稳扎稳打”就可以到达“胜利的终点”p(N, N)。

显然,这种策略不是对方想要的结果。适逢夜深燥热、大脑混沌,且先睡去,待次日云淡风清,再做打算。果不其然,早起在“响应大自然呼唤”的时候,我想到了理论上的最佳解决方案,不但能给出一条可行的路径,而且这条路径保证是最短的。聪明的你或许已经想到,没错,这个题目本质上就是一道unweighted undirected shortest path的图论问题。转换的方法很简单:从左到右从上到下,将(N*N)的棋盘上N*N个point顺次编号为0, 1, … N*N-1,对于棋盘上的一个点p(x, y),按照knight的走法,其下一步到达的点最多有8个:p1(x-1, y-2), p2(x-1, y+2), p3(x+1, y-2), p4(x+1, y+2), p5(x-2, y-1), p6(x-2, y+1), p7(x+2, y-1), p8(x+2, y+1),所有这些点的集合构成了点p在unweighted undirected graph中的邻接表。接下来BFS算一下最短路径(由于是无权图,因此还是不劳烦Dijkstra大侠的高招了,简单的BFS就够了),再做一些简单的输入输出和错误处理就OK啦。那么最后的一块骨头就是Coding了。

我最后大概花了15个小时左右完成了C++/Lisp/Python三种不同语言的程序,三者的Coding时间比大概是5:2:3,这也是我第一次用不同的语言来写同样的程序,原本想起来不算太难的工作,写完后还是很有收获的2,勿在浮沙筑高台,Coding这种工匠手艺活,眼高手低是最要不得的。

文既至此,我就顺便谈谈语言学习的问题,高贤见笑后,如有时间,万望不吝赐教。

3 C++

按照我所了解的当代中国本科计算机教育,大多人工科学生学习的第一门编程语言应该是C,接下来如果还有需要的话,就是C++或者Java了。不幸的是,鄙人也是这样过来的,幸运的是,鄙人天资愚鲁,学了半年C++后知难而退,转战了实战主义的Shell Script和Python,这一年来闹中取静,为了探寻MapReduce算法模型的本原,啃了几本Lisp的书,回过头来,总算对C++的诸多繁杂特性有了一些全局性的认识,这种认识来自于跨语言的佐证和思考,而非来自于C++语言本身。事实上C++中的很多概念在C++中是无法学到通透的,比如:

  • C++11的lambda:你可以不知道Alonzo Church, 也可以不会Lambda calculus3, 但是如果你不知道什么叫Higher-order function4的话,你怎么好意思说,你会lambda?lambda仅仅是匿名函数那么简单吗?匿名函数有何用?匿名函数的递归问题怎么解决5?STL里的functor究竟是语言机制设计的经典还是为了弥补语言不足所贴的狗屁膏药?
  • STL:STL几乎就是C++经典库和设计的代名词,通过迭代器将算法和组件分离6,力求达到性能和代码重用的双重顶点。
    • 可惜你不知道的是,迭代器并不是一个高层次的抽象机制,迭代器的本质是一种迭代遍历操作,至于通过什么手段来迭代遍历,这些本不应该是使用者所应该关心的细节问题。所以对于下面的这段c++伪代码:
for (vector<int>::iterator itr = v.begin(); itr != v.end(); ++itr)
{
    do_something(*itr)
}
    • 其高阶抽象代码应该是:
for_each(v, do_something)
    • 稍微了解一点Lisp的读者都能想到如下的等价伪代码7
(mapcar #'do_something v)
    • 每次你写"v.begin(), v.end()"这样的代码时,你就不知不觉地降低了自己的抽象层次,使自己脱离问题域而转向去纠结于实现域,不要小看这种力量, 软件工程的一切欢乐和痛苦,只是聚沙成塔的两个极端而已。
    • 事实上STL本身包含很多函数式编程的思想,比如说 functor这种组件,其实质是将在c++中作为second-class的函数通过类封装的手段提升至first-class,如此一来,函数的核心操作摇身一变成为functor的时候,就可以像函数式语言里面的Higher-order function一样,可以用类成员变量来模拟实现闭包,可以被当做普通参数传递返回(这样就不用费力去写令很多新手语法不过关的函数指针了),甚至可以通过std::bind1st/std::bind2nd这种奇技淫巧实现一个蹩脚的线性代数级别的函数映射与变换。8
    • STL里面大量的算法都是基于迭代器的抽象而进行序列的批量化操作,同种算法多种容器的核心技术是 基于C++模板实现的静多态 ,"It is better to have 100 functions operate on one data structure than 10 functions on 10 data structures",从这个角度上来讲,STL算法和Lisp中针对sequence类型数据的各种函数(mapcar/remove/remove_if/member等)有异曲同工之妙。
    • 最后来八一八STL之父Alexander Stepanov,其实人家是莫斯科大学数学系毕业的高材生,所以STL背后有着很深的数学思想,"Elements of Programming"或许是解开这个谜题的钥匙。另外,Alexander是反对OOP的。
  • 泛型与模板:这大概是Modern C++中最重口味的话题了,也是很多C++初学者的噩梦。我认为C++模板足够强大,但同时也足够扭曲且非人道,布满了大大小小的地雷和陷阱。探究起来,C++模板之所以有那么多坑,其历史原因在于C++模板是一种被发现而非被发明的技术9。C++最初引入模板的动机非常简单,无非就是写一些通用的min/max函数和一些简单的泛型类,但是人们后来发现C++模板竟然是图灵完备的,这件事极大的刺激了C++程序员的神经,于是乎,一个又一个神乎其技的ad hoc的模板编程的奇技淫巧被挖掘出来,这些奇技淫巧分布在C++标准库的各个角落,而这些奇技淫巧本身也成了许多C++程序员绕不开躲不过的必修课。C++的学习就像练剑一样,练到一定境界总会碰到这样那样的瓶颈,这个时候很多人就会认为自己功力不够,或者是练得不够刻苦,于是乎找来一本又一本的“武功秘籍”10更加刻苦地练剑。殊不知,如果方向不对,再怎么努力刻苦也难免事倍功半。你可知道,繁杂的C++模板特性的背后,其本质到底是什么?
    • C++模板的本质在于用编程的手段显式地控制编译器的代码生成。没错,聪明的你已经想到,Lisp的macro做的也是同样的事情。但是不同于Lisp的macro,由于C++模板的先天不足和C++静态类型系统的限制,C++在语言层面上对模板编程的支持非常有限。荣耀先生有一篇非常精炼的PPT《C++模板元编程技术与应用》, 基本上概括了C++模板编程的核心机制和语言实现,我摘录了一些如下:
      • 模板元编程使用静态C++语言成分,编程风格类似于函数式编程,其中不可以使用变量、赋值语句和迭代结构等。
      • 在模板元编程中,主要操作整型(包括布尔类型、字符类型、整数类型)常量和类型。被操纵的实体也称为元数据(Metadata)。所有元数据均可作为模板参数。
      • 由于在模板元编程中不可以使用变量,我们只能使用typedef名字和整型常量。它们分别采用一个类型和整数值进行初始化,之后不能再赋予新的类型或数值。如果需要新的类型或数值,必须引入新的typedef名字或常量。
      • 编译期赋值通过整型常量初始化和typedef语句实现。例如:
        • enum { Result = Fib<N-1>::Result + Fib<N-2>::Result};
        • static const int Result = Fib<N-1>::Result + Fib<N-2>::Result;
      • 成员类型则通过typedef引入,例如:
        • typedef T1 Result;
      • 条件结构采用模板特化或条件操作符实现。如果需要从两个或更多种类型中选其一,可以使用模板特化,如前述的IfThenElse11
      • 静态C++代码使用递归而不是循环语句。递归的终结采用模板特化实现。如果没有充当终结条件的特化版,编译器将一直实例化下去,一直到达编译器的极限12
    • 而正是由于底层支撑性语言机制的匮乏,使得C++模板编程非常的冗长、丑陋,甚至有些扭曲乃至非人道13。我以为,用一门连IfThenElse都要靠Hack去实现的子语言去写高阶代码,和用汇编语言去写高级数据结构是差不多的。所以你去看STL的代码,看std::binary_function,你会发现大量的typedef做类型推导。可是你想过没有,类型推导真的是必须的吗?未必。这么多typedef完全是拜C++的静态类型系统所赐。我不是说静态类型不好,事实上关于静态类型和动态类型历来都是学术界和工业界乐此不疲的热门口水战。我想说明的是, 有时候你想要舞蹈的时候,要低头看看,你的脚上是否带着不必要的镣铐。 C++的静态类型系统对于泛型编程而言,就是这样的镣铐。
  • 引用、指针、const、static等:除了以上比较“重口味”的C++语言特性,C++里还有各种各样的语言小尾巴,而且这个尾巴一般都拉的特别长。当然,尾巴长的好处之一就是可以养活很多语言专家,什么effective啊、exceptional啊、faq啊啥的,在所有的编程语言中,C++这点绝对是独树一帜。其实每个语言特性的背后都有值得深究的知识, 没有任何事情是想当然的。14 const够简单了吧?可是你知道const pointer和pointer to const的区别吗?你知道什么时候用const引用传参什么时候返回const引用什么时候返回值吗?你知道const成员函数吗?你知道为什么会有初始化成员列表的存在吗?再来说说引用这个概念,其本质上就是一种受限指针加上编译器层面上的语法糖修饰,按理说不太难,但是什么时候传引用返回引用确是值得深究的好问题,搞清楚了这点,你就会搞明白C++中的copy constructor/copy assignment operator,Java中的Object.clone(),Python中的"is"、和Lisp中的eq/eql/equal。传引用/指针还是传值涉及到深刻的程序语言原理,并不是你想象的那么简单而已。
  • 以上谈了这么多,读者可能会问,既然C++如此繁杂,还要不要学习C++?学,当然要学,否则你怎么批判呢?怎么学?批判地学。要去学习语言机制的根源和本质而不要迷失在语言特性的森林里15
  • 最后,还是回到面试题上,还是放上鄙人的C++代码,也好和Lisp/Python版的程序做一个小对比:
#include <queue>
#include <limits>
#include <iostream>
#include <vector>
#include <map>
#include <functional>
#include <queue>
#include <list>
#include <cstdlib>
using namespace std;

struct vertex
{
    int index;                  /// the vertex index, also the vertex name
    vertex* prev;               /// the prev vertex node computed by bfs and bfs_shortest
    int dist;                   /// the distance to the start computed by bfs
                                /// and bfs_shortest
    vector<vertex*> adj;        /// the adjacency list for this vertex 

    vertex(int idx)
        : index(idx) {
        reset();
    }

    void reset() {
        prev = NULL;
        dist = numeric_limits<int>::max();
    }
};

class graph
{
public:
    graph() { }
    ~graph();
    void add_edge(int start, int end);
    void bfs(int start);
    void bfs_shortest(int start);
    list<int> get_path(int end) const;
    void print_graph() const;

protected:
    vertex* get_vertex(int idx);
    void reset_all();
    list<int> get_path(const vertex &end) const;

private:
    /// disable copy 
    graph(const graph &rhs);
    graph& operator=(const graph &rhs);

    typedef map<int, vertex*, less<int> > vmap;
    vmap vm;
};

graph::~graph() {
    for (vmap::iterator itr = vm.begin(); itr != vm.end(); ++itr)
    {
        delete (*itr).second;
    }
}

/** 
 * return a new vertex if not exists, else return the old vertex, using std::map
 * for vertex management
 *
 * @param idx vertex index
 *
 * @return a (new) vertex of index idx
 */
vertex* graph::get_vertex(int idx) {
    /// cout << "idx: " << idx << "\tvm.size(): " << vm.size() << endl;
    vmap::iterator itr = vm.find(idx);

    if (itr == vm.end())
    {
        vm[idx] = new vertex(idx);
        return vm[idx];
    }

    return itr->second;
}

/** 
 * clear all vertex state flags
 *
 */
void graph::reset_all() {
    for (vmap::iterator itr = vm.begin(); itr != vm.end(); ++itr)
    {
        (*itr).second->reset();
    }
}

/** 
 * add an edge(start --> end) to the graph
 *
 * @param start 
 * @param end 
 */
void graph::add_edge(int start, int end) {
    vertex *s = get_vertex(start);
    vertex *e = get_vertex(end);
    s->adj.push_back(e);
}

/** 
 * print the graph vertex by vertex(with adj list)
 *
 */
void graph::print_graph() const {
    for (vmap::const_iterator itr = vm.begin(); itr != vm.end(); ++itr)
    {
        cout << itr->first << ": ";
        for (vector<vertex*>::const_iterator vitr = itr->second->adj.begin();
             vitr != itr->second->adj.end();
             ++vitr)
        {
            cout << (*vitr)->index << " ";
        }
        cout << endl;
    }
}

/** 
 * traversal the graph breadth-first
 *
 * @param start the starting point of the bfs traversal
 */
void graph::bfs(int start) {
    if (vm.find(start) == vm.end())
    {
        cerr << "graph::bfs(): invalid point index " << start << endl;
        return;
    }

    vertex *s = vm[start];
    queue<vertex*> q;
    q.push(s);
    s->dist = -1;

    while (!q.empty()) {
        vertex *v = q.front();
        cout << v->index << " ";
        q.pop();

        for (int i = 0; i < v->adj.size(); ++i)
        {
            if (v->adj[i]->dist != -1)
            {
                q.push(v->adj[i]);
                v->adj[i]->dist = -1;
            }
        }
    }
}

/** 
 * the unweighted shortest path algorithm, using a std::queue instead of
 * priority_queue(which is used in dijkstra's algorithm)
 *
 * @param start 
 */
void graph::bfs_shortest(int start) {
    if (vm.find(start) == vm.end())
    {
        cerr << "graph::bfs_shortest(): invalid point index " << start << endl;
        return;
    }

    vertex *s = vm[start];

    queue<vertex*> q;
    q.push(s);
    s->dist = 0;

    while (!q.empty()) {
        vertex *v = q.front();
        q.pop();

        for (int i = 0; i < v->adj.size(); ++i)
        {
            vertex *w = v->adj[i];
            if (w->dist == numeric_limits<int>::max())
            {
                w->dist = v->dist + 1;
                w->prev = v;
                q.push(w);
            }
        }
    }
}

/** 
 * get the path from start to end
 *
 * @param end 
 *
 * @return a list of vertex which denotes the shortest path
 */
list<int> graph::get_path(int end) const {
    vmap::const_iterator itr = vm.find(end);

    if (itr == vm.end())
    {
        cerr << "graph::get_path(): invalid point index " << end << endl;
        return list<int>();
    }

    const vertex &w = *(*itr).second;

    if (w.dist == numeric_limits<int>::max())
    {
        cout << "vertex " << w.index << " is not reachable";
        return list<int>();
    }
    else {
        return get_path(w);
    }
}

/** 
 * the internal helper function for the public get_path function
 *
 * @param end 
 *
 * @return a list of vertex index
 */
list<int> graph::get_path(const vertex &end) const {
    list<int> l;
    const vertex *v = &end;

    while (v != NULL) {
        l.push_front(v->index);
        v = v->prev;
    }

    return l;
}

class chessboard {
private:
    struct point {
        int x;
        int y;

        point(int px, int pb)
            : x(px), y(pb) { }
    };

public:
    chessboard(int s);
    void solve_knight(int x, int y);

protected:
    bool is_valid(const point &p);
    point next_point(const point &p, int i);

private:
    graph board;
    int size;
};

/** 
 * constructor, build a underlying graph from a chessboard of size s
 *
 * @param s 
 */
chessboard::chessboard(int s)
    : size(s) {
    for (int i = 0; i < size; ++i)
    {
        for (int j = 0; j < size; ++j)
        {
            int start = i * size + j;
            point p(i, j);

            for (int k = 0; k < 8; ++k)
            {
                /// the next possible knight position 
                point np = next_point(p, k);

                if (is_valid(np))       
                {
                    int end = np.x * size + np.y;

                    /// add edges in both directions
                    board.add_edge(start, end);
                    board.add_edge(end, start);
                }
            }
        }
    }
}

/** 
 * find and print a path from (x, y) to (size, size)
 *
 * @param x 
 * @param y 
 */
void chessboard::solve_knight(int x, int y) {
    int start = (x-1) * size + (y-1);
    int end = size * size - 1;

    board.bfs_shortest(start);
    list<int> l = board.get_path(end);

    int count = 0;
    for (list<int>::const_iterator itr = l.begin(); itr != l.end(); ++itr)
    {
        cout << "(" << *itr/size + 1 << ", " << *itr%size + 1<< ")";
        if (count++ != l.size() - 1)
        {
            cout << " -> ";
        }
    }
    cout << endl;
}

/** 
 * whether or not the point is valid in the chessboard
 *
 * @param p 
 *
 * @return true for valid
 */
bool chessboard::is_valid(const point &p) {
    if (p.x < 0 || p.x >= size - 1 || p.y < 0 || p.y >= size - 1)
    {
        return false;
    }
    return true;
}

/** 
 * the next possible position, every has 8 next possible position, though not
 * all 8 position is valid
 *
 * @param p the original knight position 
 * @param i 
 *
 * @return 
 */
chessboard::point chessboard::next_point(const point &p, int i) {
    int knight[8][2] = {
        {2, 1}, {2, -1},
        {-2, 1}, {-2, -1},
        {1, 2}, {1, -2},
        {-1, 2}, {-1, -2}
    };

    return point(p.x + knight[i][0], p.y + knight[i][1]);
}

int main(int argc, char *argv[])
{
    if (argc != 4)
    {
        cerr << "Wrong arguments! Usage: knight.bin N x y" << endl;
        return -1;
    }

    int N = atoi(argv[1]);
    int x = atoi(argv[2]);
    int y = atoi(argv[3]);

    chessboard chess(N);

    chess.solve_knight(x, y);

    return 0;
}

4 Lisp

Lisp是一门阳春白雪的语言,前两天我去面试一个linux后端开发的职位,面试官看到我的简历还当面问我“Lisp是一个什么东西”……Lisp最广为人知的特点,大概就是——括号了吧。因此Lisp除了代表"List Processing", 还有一个别名"Lots of Irritating Superfluous Parentheses"。括号的背后其实是S-expression。诈看上去,S-expression (+ 1 2)比之于我们熟悉的"1 + 2"确实要晦涩一点,但是你要明白的是,我们之所以比较喜欢"1 + 2"这种形式的写法,那完全是我们小学教育的错16。想想高等数学吧,函数f(x, y, z),翻译成Lisp的S-expression就是(f x y z),但是如何翻译成"1 + 2"形式的语句呢?事实上在Lisp发明之初,确实有人指出说S-expression写起来特别的别扭,John McCarthy也曾经试图将S-expression转换成M-expression 的形式,可是后来人们发现S-expression所带来的好处远远超出其微末的学习成本,M-expression的计划也就无疾而终了。S-expression是Lisp程序员一切欢乐与痛苦的来源17

  • S-expression带给Lisp的第一个好处是语法的简单一致性。显而易见的例子就是Lisp中没有类似于C语言中的运算符优先表。
  • S-expression带给Lisp的第二个好处是Homoiconicity, 体现在Lisp中,就是"code is data"18
  • 基于"code is data", S-expreesion带给Lisp的第三个好处就是强大的macro。前面我们曾经讲到,“C++模板的本质在于用编程的手段显式地控制编译器的代码生成“,也就是所谓的元编程meta-programming。 我们还提到,C++在语言层面上对meta-programming的支持非常匮乏,因此才会有各种各样的workarounds(effective/exceptional中称为idioms或者techniques)。与C++模板不同,Lisp的macro可以调用几乎所有Lisp的语言机制。正式由于S-expression的存在,使得Lisp代码本身不经解析就是一颗完美的对编译器极度友好的抽象语法树当我们写Lisp macro的时候,我们其实是在和Lisp编译器交谈 ,我们告诉Lisp编译器,那些参数需要求值19,那些代码需要循环执行(do/dolist/dotimes),那些结构需要定义getter/setter(defstruct)等等。要知道,Lisp的老本行就是List Processing,而任何合法的Lisp的代码本身也一个List,用Lisp的能力来操作自身的代码,进行代码变换,这就是Lisp的macro。
    • 好学的读者可能会问,元编程到底有什么用?其实很简单,当你在写一句句C语言代码的时候,你就已经在用元编程了。广义上来讲,任何能够控制代码生成的编程方法都可以看作是元编程,元编程其实是编译器的主要工作职能。不明白?好吧。我们要从遥远的汇编时代讲起。没有C语言(高级语言)之前,人们在汇编语言的酱缸中浸淫。终于有一天,有那么几位智者大神跳出来,总结出说编程语言的控制结构无非就是顺序/选择/循环三种,于是就有了if,有了for/while,从此程序员就快乐写写if/for,抛弃了汇编,因为有一个叫做编译器的助手可以自动生成if/for的底层汇编代码。如果说编程是为了解决重复性的工作,那么元编程就是为了解决重复性的编程代码工作。
    • Lisp的macro所带来的元编程能力与其他语言相比,其最大的特点在于Lisp的macro元编程是可扩展的,也就是说,我们可以通过Lisp的macro写一些库,而这些库和语言本身的机制能够很好的融合在一起;其余的语言诸如C++/Java,其语言机制的扩展则需要进行漫长的标准化进程。

除了以上,Lisp还有一些非常独特的优点,使得这么古老的阳春白雪般的语言虽然尚未蓬勃,但注定不会消亡:

  • 快速反馈的交互式开发模型。是的,谈到Lisp开发就不能不谈到Emacs+Slime这套革命性的开发环境,没有Slime的Lisp,就像没有武器的战士一样。不同于C++的先构建再运行的开发模型,Lisp的开发模型是交互式的。你写了一个defun一个defstruct,不需要去main函数中写一段测试代码和print语句,然后编译运行看看结果是否符合预期;在Slime+Lisp的开发环境中,写了一个defun,C-c C-c即可编译完成,C-x C-e即可执行当前的一个表达式,快速的反馈和修改能够最大程度上保证你思维的连续性。关于这点可以参考我写的走进Lisp的世界——兼谈Emacs下Lisp的开发环境(上)
  • 强烈的数学味,更高层次的抽象,专注于what而不是how。
    • Paul GrahamThe Roots of Lisp中写到"It's worth understanding what McCarthy discovered, not just as a landmark in the history of computers, but as a model for what programming is tending to become in our own time. It seems to me that there have been two really clean, consistent models of programming so far: the C model and the Lisp model. These two seem points of high ground, with swampy lowlands between them. As computers have grown more powerful, the new languages being developed have been moving steadily toward the Lisp model. A popular recipe for new programming languages in the past 20 years has been to take the C model of computing and add to it, piecemeal, parts taken from the Lisp model, like runtime typing and garbage collection."
    • 按照我的理解,我们可以对中文“计算机”这个词语做一次咬文嚼字的分拆,Lisp代表着“计算”,而C语言则代表“机”。
    • Lisp具有强烈的数学色彩,Lisp程序中大量使用递归,深刻理解递归几乎是Lisp程序员的必备生存技能20。而C语言则终点关注底层机器模型,short/int/long/long long,不同数据类型的区分,榨干机器的内存空间;大量使用指针,将完整的冯诺依曼机器模型暴露给程序员,榨干机器的整体性能。
    • 在Lisp中更加强调what you want,而C中则更加倾向于给出长长的算法步骤和状态变换指令,专注于how to get it。
      • 比如求一个list的长度,在Lisp中,其核心代码就是(+ 1 (lenght (cdr lst)));而在C中,恐怕要设置int i = 0和各种计数器了21
  • 强类型的动态语言,一致的语法规则,关注实现域而非问题域,摒弃编程中的心智包袱。
    • C++是一门有心智包袱的语言22,在C++编程中,我们常常要考虑诸如是传值还是传引用、要不要进行运算符重载、深拷贝还是浅拷贝、堆内存还是栈内存等等这些实现域而非问题域的语言细节问题。我不是说关注实现域这点不好,只是不能太过,而C++的讨厌之处就在于,为了所谓一点点的性能提升,经常性地将苦命的码农们从问题域拉回实现域。
  • 万能的List,快速的原型构建能力。
    • 如何用可递归的List来一体化的表达常见的数据结构,这个问题比较深刻,后续我会再写一篇文章深入探讨下。

本节的最后,还是给出完整的Lisp程序,核心代码只有70行左右,大概是C++的三分之一左右。主题算法和代码来自于《ANSI Common Lisp》3.15节。顺带广告,《ANSI Common Lisp》是非常不错的Common Lisp书籍(用来入门的话还是比较难啃的),300页不到的篇幅里基本上覆盖了Common Lisp大部分的语言特性,并且有很多极具实用价值的小程序(最短路、行程压缩编码、二叉树、二分搜索、光线跟踪算法等等)。我认为此书之于Lisp,相当于K&R C之于C。

(defun point2index (x y n)
  "convert a coordinate point to an index"
  (+ (* x n) y))

(defun index2chess (index n)
  "convert an index back to a coordinate point"
  (floor index n))

(defun build-graph (n)
  "build a undirected unweighted graph according to the chess rules about
knight"
  ;; use lisp array to keep the vertex map
  (let ((vm (make-array (* n n) :initial-element nil)))
    ;;; define some auxiliary function 
    (defun is-valid (x y)
      "whether or not the point is valid in the chess board"
      (and (>= x 0) (< x n)
           (>= y 0) (< y n)))
    (defun all-adj-points (x y)
      "build the adjacency list for point (x, y)"
      (let ((adj-list))
        ;; return every possible next knight position as a list
        (dolist (next-step
                  '((2 . -1) (2 . 1)
                    (-2 . -1) (-2 . 1)
                    (1 . -2) (1 . 2)
                    (-1 . -2) (-1 . 2)))
          (let ((nx (+ x (car next-step)))
                (ny (+ y (cdr next-step))))
            (if (is-valid nx ny)
                ;; build the adjacency list
                (push (point2index nx ny n) adj-list))))
        adj-list))
    (dotimes (i n)
      (dotimes (j n)
        (setf (aref vm (point2index i j n)) (all-adj-points i j))))
   vm))

(defun shortest-path (start end graph)
  "one-source unweighted shortest-path algorithm using bfs method"
  (bfs end (list (list start)) graph))

(defun bfs (end queue graph)
  "the internal bfs routine to find shortest path"
  (if (null queue)
      nil
      (let* ((path (car queue))
             (node (car path)))
        (if (eql node end)
            (reverse path)
            (bfs end
                 ;; pop the queue and push some new path into the queue
                 (append (cdr queue)
                         (new-paths path node graph))
                 graph)))))

(defun new-paths (path node graph)
  "return the new-paths according to the node's adj list"
  (mapcar #'(lambda (n)
              (cons n path))
          (cdr (aref graph node))))

(defun solve-knight (n x y)
  "the main function to solve knight problem"
  (let ((path (shortest-path (point2index (- x 1) (- y 1) n)
                             (point2index (- n 1) (- n 1) n)
                             (build-graph n))))
    ;; print the start point first
    (multiple-value-bind (x1 y1)
        (index2point (car path) n)
      (format t "(~A, ~A)" (+ x1 1) (+ y1 1)))
    ;; print the path
    (mapcar #'(lambda (obj)
                (multiple-value-bind (px py)
                    (index2point obj n)
                  (format t " -> (~A, ~A)"
                          (+ px 1)
                          (+ py 1))))
     (cdr path))
    ;; return the path 
    path))


;;; some test
;; CL-USER> (SOLVE-KNIGHT 6 1 1)
;; (1, 1) -> (3, 2) -> (4, 4) -> (5, 6) -> (6, 4) -> (4, 5) -> (6, 6)
;; (0 13 21 29 33 22 35)
;; CL-USER> (SOLVE-KNIGHT 8 1 1)
;; (1, 1) -> (3, 2) -> (4, 4) -> (5, 6) -> (6, 8) -> (7, 6) -> (8, 8)
;; (0 17 27 37 47 53 63)
;; CL-USER> (SOLVE-KNIGHT 8 2 1)
;; (2, 1) -> (3, 3) -> (4, 5) -> (5, 7) -> (7, 6) -> (8, 8)
;; (8 18 28 38 53 63)

5 Python

坦白地说,我对Python的了解远不如C/C++,甚至不如Lisp,尽管我也用Python写过一些不大不小的原型程序,但是这些程序都没有触及到Python的语言核心。前两天写这个knight rush的程序,还要去翻书,熟悉下Python OOP编程的一些知识。我以为,Python是一门实用主义至上的语言,在保证实用主义的前提下,Python从诸多语言中吸收了很多特性,并一一做了精简(Python的OOP甚至没有private,而Python lambda对比Lisp算很一般),再加上简单至上的文化和缩进式的代码风格,构成了当今Python语言的主要面貌。

即便如此,我认为Python还是值得学习的。它既不像C/C++那样令人紧张、也不像Lisp那样阳春白雪,对比Shell Script,Python有自己的内建数据结构,能够在很大程度上替换Shell Script。其实和Python同级的语言还是有很多的,比如Perl,Ruby。Ruby我不了解,但是我对Perl/PHP/Shell这类遍布'$'符号的语言一向没什么好感,因为这类语言的可读性一般都很差。

其实关于Python本身,我已经没有太多想法可写,可能一方面我对Python的了解实在算不上深入,另一方面,Python本身也是不希望它的使用者过多关注于语言本身吧。对于Python,简单了解后拿过来直接用就好了,什么代码风格、缩进啊,那都是过去时的事情了。

作为对比,还是贴出Python版的Knight rush程序:

#!/usr/bin/env python2

import sys

class graph(object):
    """unweighted directed graph
    """

    def __init__(self):
        """set _vmap to and _vprev an empty python dict
        all vertex are represented by a simple index

        _vmap: {vertex x: x's adjacency list}
        _vprev: {vertex x: x's prev vertex computed by bfs routine}
        """
        self._vmap = {};
        self._vprev = {};

    def add_edge(self, start, end):
        """add an edge to the graph
        """
        if self._vmap.has_key(start):
            self._vmap[start].append(end)
        else:
            self._vmap[start] = [end]

    def bfs_shortest(self, start):
        """one-source shortest-path algorithm
        """
        queue = [start]

        self._vprev[start] = None

        while len(queue) != 0:
            v = queue[0]
            queue.pop(0)

            if self._vmap.has_key(v):
                v_adj = self._vmap[v]
            else:
                continue

            for nextv in v_adj:
                if self._vprev.has_key(nextv):# and self._vprev[nextv] is not None:
                    # nextv has already found its parent""
                    continue
                else:
                    queue.append(nextv)
                    self._vprev[nextv] = v

    def get_path(self, end):
        """return the shortest path as a python list
        """
        v = end;
        path = []
        while self._vprev.has_key(v) and self._vprev[v] is not None:
            path.insert(0, v)
            v = self._vprev[v]

        if self._vprev.has_key(v):
            path.insert(0, v)   # insert the start point to the path
        else:
            print "destination %d is not exist or unreachable" % v

        return path

class chessboard(object):
    """a chessboard of size n*n class
    """

    def __init__(self, n):
        """build the internal graph representation of the chessboard

        Arguments:
        - `n`: size of the chessboard
        """
        self._size = n
        self._board = graph()

        next_point = ((2, 1), (2, -1), \
                      (1, 2), (1, -2), \
                      (-2, 1), (-2, -1), \
                      (-1, 2), (-1, -2))

        for x in range(n):
            for y in range(n):
                start = self.point2index(x, y)
                for dx, dy in next_point:
                    nx = x + dx
                    ny = y + dy

                    if self.is_valid(nx, ny):
                        end = self.point2index(nx, ny)
                        self._board.add_edge(start, end)

    def is_valid(self, x, y):
        """whether or not point (x, y) is valid in the chessboard
        """
        return 0 <= x < self._size and 0 <= y < self._size

    def point2index(self, x, y):
        """convert a chessboard point to the internal graph vertex index 
        """
        return x * self._size + y

    def index2point(self, p):
        """convert the internal graph vertex index back to a chessboard point
        """
        return (p / self._size, p % self._size)

    def solve_knight(self, x, y):
        """just solve it
        """
        start = self.point2index(x, y)
        end = self.point2index(self._size - 1, self._size - 1)
        self._board.bfs_shortest(start)
        path = [self.index2point(x) for x in self._board.get_path(end)]
        return [(x + 1, y + 1) for x, y in path]

def main():
    """main routine
    """
    # g = graph()
    # g.add_edge(1, 2)
    # g.add_edge(1, 3)
    # g.add_edge(2, 3)
    # g.add_edge(3, 4)
    # g.bfs_shortest(1)
    # print g.get_path(4)

    if len(sys.argv) != 4:
        print """Wrong arguments! Usage: ./knight.py N x y
        """
        return -1

    N = int(sys.argv[1])
    x = int(sys.argv[2])
    y = int(sys.argv[3])

    chess = chessboard(N)

    print chess.solve_knight(x - 1, y - 1)

    return 0

if __name__ == "__main__":
    main()


# some test data
# $ ./knight.py 6 2 2
# [(2, 2), (4, 3), (6, 4), (4, 5), (6, 6)]
# $ ./knight.py 6 2 2
# [(2, 2), (4, 3), (6, 4), (4, 5), (6, 6)]
# $ ./knight.py 4 2 2
# [(2, 2), (4, 3), (2, 4), (3, 2), (4, 4)]
# $ ./knight.py 4 1 1
# [(1, 1), (3, 2), (4, 4)]
# $ ./knight.py 4 2 3
# [(2, 3), (4, 4)]
# $ ./knight.py 20 2 3
# [(2, 3), (4, 4), (6, 5), (8, 6), (10, 7), (12, 8), (14, 9), (16, 10), (18, 11), (20, 12), (19, 14), (20, 16), (19, 18), (20, 20)]
# $ 

6 总结

本文的初衷只是想针对此次面试做一个小的总结,但是写到一半发现面试题本身可写的内容不多,于是我就顺便写写我个人对C++/Lisp/Python的一些思考,而题目本身就“很悲剧地”成了本文的一个引子。写作终究不是一件容易的事情,将自己心中的想法转化成纸上清晰易懂的文字是一件耗时耗力的体力脑力并重的工作。整篇文章的写作大概耗时12个小时,但是写作的过程中也让我梳理了下自己的知识体系,如未鹏所言,“书写是为了更好的思考”。

罗嗦了这么多,其核心观点只有一个,那就是“ 要学会跳出语言的框架去学习语言 ”。站得高才能看得远, 只有跳出语言的框架,才能挣断语言给你的思维所上的枷锁,超越语言本身 ,看到更广阔的图景23

Footnotes:

1 由于面试前后并没有保密协议,加上本文内容主要是以我个人的一些技术思考为主,因此题 目内容本引述邮件。如果违反相关招聘规定,请不吝告知,谢谢。

2 按照ACM的标准,我这样的编码速度估计是死定了。

3 其实我也不会,相信我,我只是在吹牛而已 ^_^ 。

4 四个程序员的一天, 一篇非常生动的高 阶函数科普小品文。

5 关于lambda函数的递归问题涉及到非常深刻的计算理论问题,其入门文章可以参考未鹏写的 康托尔、哥德尔、图灵——永恒的金色对角线, 围绕此话题有一本奇书 《哥德尔·艾舍尔·巴赫——集异璧之大成》, 曾长期绝版,最近当当有售,大家抓紧机会。

6 没错,STL和OO的数据封装思想几乎是背道而驰的。

7 Python中有Lisp Comprehension和类似于Lisp的map/reduce/filter套装。

8 boost function提供了更好的function object支持。

9 刘未鹏:泛型编程:源起、 实现与意义

10 关于C++语言特性的书籍简直可以用浩如烟海来形容,参看 这里这里

11 关于这个IfThenElse模板,可以参考《C++ Templates》第15章的讲解与实现。

12 前文提到,C++模板代码是由C++编译器在编译期“解释执行”的,其冗长的编译时间、巨大的 编译资源以及内存资源需求,使得最新的GCC/Clang系列编译器对模板的递归层次支持也仅 有几千层而已。事实上编译时间的冗长也一直是C++模板被人诟病的地方之一。

13 看看《C++ Templates》Part 2吧,绝对 是顶级脑细胞杀手。

14 即便是int x = 3这样简单的一条赋值语句也不是你想象中的那么简单,看看 《SICP》 第三章吧。能够对变量直接赋 值是不同编程范式的一个主要区别,而这又从一个很重要的角度上决定了并行计算的本质困 难性。

15 怎么学?学习C++:实践者的 方法(Beta1)

16 就好比我们喜欢十进制而非二进制,完全是因为上帝赐予了我们十根手指。

17 你也可以说,指针是C程序员一切欢乐与痛苦的来源。其实我想说的是,每种编程语言的核 心关注点不同,在此至上,语言本身会围绕着这个核心点发展出自己的一套设计哲学,然后 根据这套哲学来指导语言本身的设计和发展。

18 "Data is just dumb code, and code is just smart data",关于"code is data"的话题 涉及到计算机科学里面很多深刻的论题。比如说c程序中的.text段和.data段, 冯诺依曼体系结构哈佛体系结构, 编译器代码 生成等等。 Code is data, and it always has been

19 学习Lisp带给你的一个思想革新就是,一个对象和这个对象的值是完全不同的东西(eval (quote x))。这是个很重要的概念,但这个概念在其余语言中往往是混为一体的,至少在语 法上是这样的。最简单的例子,比如c中的语句:x = x,等式的右边是x的值而不是x本身, 等式的左边是x本身而不是x的值,理解了这点,你就能理解C++中左值和右值的概念区别。 如果有机会,我会专门写篇文章,探讨下这个主题。

20 Lisp编程中的递归主要是数学归纳法的一种程式化转换, 《Common Lisp, A Gentle Introduction to Symbolic Computation》8.11节里面详细介绍了Lisp中递归的几种模式,清晰易懂, 强烈推荐。

21 这么简单的例子也许并不足以体现出Lisp和C这两种语言不同思维模式的区别,事实上如果 读者不去稍微深入地学习下Lisp的话,是很难体会到这种思维转变的。

22 具体可以参考孟岩先生的两篇文章, 用C设计,用C++编码Linux之父话糙理不糙

23 最后一个脚注(貌似我最近写文章脚注用得越来越多了,不知道这算是旁征博引还是逻辑不 清):侃侃那些美丽 的编程语言, ^_^ 。

 

走进Lisp的世界——兼谈Emacs下Lisp的开发环境(上)

1 磨刀不误砍柴工

”工欲善其行,必先利其器“,工具的强是无敌的。 而判断一个工具是否值得学习,需要理性的分析学习成本和收益。简单地讲,如果学习一个工具的时间远远超出你使用这个工具的时间,那么这个工具就是不值得你去学的。注意, 我并没有说这个工具本身不值得学,而是说它不值得你学 。同在互联网行业的人,你不可能建议UI设计师去学习Emacs/Vim,也不太可能去要求码农去学习Photoshop。

编程几乎是一种纯脑力劳动,更确切的说,编程是人脑的一种思维运作。 高效的编程必然伴随着顺畅的思维运作,任何使你在编程中感觉到思维运作受阻的东西,都是你需要不断改进的东西,包括但不限于编程语言、算法和编程工具。 我在前面的的文章《少即是多——兼谈对SNS的看法》 也曾经写到:“深入的思考是容不得别人打扰的,一旦中断,思考的大厦就会崩塌,重建的过程往往循环往复、困难重重。这就是为什么聪明人只想和更聪明,至少是和自己一样聪明的人一起工作的原因,资深的Hacker更是如此,他们才没有耐心告诉你Apache该怎么配置呢。”

简单的例子,作为码农,如果你不能流畅的阅读英文技术资料,那么你需要去提高下英文能力,考个TOFEL/GRE或许是个一劳永逸的办法;如果你的电脑配置不能顺畅的运行你需要用到的开发工具,那么攒钱换电脑或者增强配置吧, 时间就是金钱,将珍贵的时间耗费在等待软件启动的过程中,这是对生命的一种亵渎 ;如果你不能顺畅的实现各种基本数据结构和算法乃至于无法深入看一些技术书籍和自我学习深造,那么找本算法书吧,认认真真的将所有代码从头敲一遍,最好再做一些习题;如果你觉得C++的各种特性杂糅使你不堪重负,使得你在解决问题的时候不断地需要脱离问题本身而去关心实现细节的,那么你应该考虑下去学习几门新的编程语言,跳出语言的框架去寻找解决方案1

除了以上种种,最常见也最容易被广大码农忽略的,就是高效的文本编辑。很多人对此不以为然,鼓吹能力是最重要的,工具是次要的,甚至举出某某牛人用记事本编出某某牛软件的例子来自我麻醉,为自己的懒惰和不思进取提供理论支撑和YY的对象。后面我会说明,语言和工具对于编程工作而言起着至关重要的作用,如果将来有朝一日我去招人,我第一件事肯定问他是Emacs/Vim用得是否熟练,对于Emacs/Vim都没有听说过的人,我肯定是坚决不会要的。

为什么高效的文本编辑如此重要?因为对于码农而言,最重要的工作就是写代码, 而写代码本身可以看成是一种特化的文本编辑工作 ,因此找一款看着舒心、用着称心的编辑器是很有必要的。我不止一次的想起,大三时光,某个实验室的角落,某某同学滚着鼠标寻找码海里的某个函数定义。他的滚轮每滚一格,我的心就咯噔一下;每次他滚了一大段又往回滚的时候,我的心就咯噔咯噔跳个不停。何必呢?即使你不知道emacs/vim,不知道source insight这样的工具,但你至少应该会Edit->Find吧。《卓有成效的程序员》 里面有几条非常重要的原则:

  • Using the mouse less.
  • Prefer typing over mousing.
  • Typing is faster than navigation.

总结起来,这三条原则的核心要旨就是快,快速定位到你想要到的地方,随心所欲,不为外物所阻。毕竟,“天下武功,无坚不破,唯快不破“。只有快,才能让思维的高速列车略偏方向而迅速纠偏,不至于偏离车道,走向“车毁人亡”的不归路。具体说来,我们编程是为了解决问题(思维的高速列车),但是很多时候我们不得不花时间和精力去处理诸如代码缩进、括号配对等非常琐碎的工作(偏离方向),而这些琐碎工作不仅会严重影响我们思维的顺畅性,更有甚者,它有时候会让我们忘了我们最初需要解决的问题(“车毁人亡”了)。这里有一段yasnippet的demo视频, 是一个极佳的高效文本编辑的例子。比如我们写html代码,我们真正关心的问题应该是到底选用那个标签,至于这个标签该怎么缩进怎么配对怎么符合xhtml标准都不是也完全不应该是我们需要分心解决的问题;日常工作中诸如此类的例子还有很多很多。所有琐碎的小问题加起来,足以碎化你的思维,让你举步维艰。“没有NFS、Java和其他的技术还能活;但是如果没有Vi,简直没法活了”2,可见一个高效文本编辑器在优秀程序员心中的地位。

废话好像有点多,接下来主要谈谈Emacs下Lisp开发环境的配置,几天的折腾碰到了很多大大小小的issue,一并记录下来,希望后来者能够少走点弯路。

最后的最后,主流文本编辑器学习曲线, 博君一笑。

2 Emacs

  • ”Emacs是伪装成操作系统的编辑器3(emacs isn't a text editor but more of an operating system that incidentally happens to include a text editor.)“。
  • 没错,Emacs就是一个操作系统,只是这个操作系统本身缺少类似于Vim这样高效的编辑器4。如果你去看下Emacs和Elisp合起来将近2000页的手册,你就会发现,emacs这货真不是一个编辑器这么简单,实在是一个以编辑器为核心构建起来的操作系统,有自己的编程语言(elisp),API,API文档,window、frame……
  • 很多人说Emacs反Unix哲学的,因为Unix的哲学是提供简明的接口(这个接口主要是文本流),透过小工具的组合完成所有的工作。但Emacs似乎野心太大,妄图以一己之力完成从编程、上网、邮件、听音乐等所有的工作5。从某种意义上而言,这种说法是正确的。但是换一个角度,如果我们真的将Emacs看成一个类似于Unix这样的操作系统的话,那么Elisp之于Emacs就相当于C之于Unix;Emacs的各种插件扩展就相当于Unix下的各种小工具;Unix通过Shell管道将各种小工具粘合起来完成复杂的工作,而Emacs通过自己的Elisp环境将各种扩展插件整合起来,让他们完美合作,完成各种各样对编辑器而言几乎不可能的工作。从这个意义上而言,如果你真的把Emacs当做一种平台而不仅仅是一个编辑器的话,那么Unix的哲学和Emacs是不冲突的。因为Unix的哲学针对的是工具,而不是工具底层的平台,不是吗?
  • 可能你会问,Emacs到底能干些什么?Easy,除了Adobe能干的,Emacs都能干。具体而言,Emacs不擅长做图形图像视频音频方面的工作,这方面是Adobe CS系列软件的天下。而除了这些基于图形图像的工作, 其余 基于文本流的工作,Emacs都能干的不错 6
  • 如果你想看看Emacs到底长什么样,emacser.com一定会让你大开眼界。
  • Ruby语言的创始人松本行弘在LibrePlanet 2012 conference上讲述了"how emacs changed mylife"。
  • 当然,Emacs并不是完美的,体型巨大、启动速度慢、Elispe的性能、多线程支持还有统一的扩展管理,这些一直被人诟病。
  • 关于启动速度,最常见的优化方法有三种:
    • 将el文件编译成elc文件,
    • 将许多插件由load转换成autoload。
    • 在Emacs首次启动时开启M-x server-mode,然后以后启动Emacs只需要emacsclient即可。我还做了几个懒人专用的alias:
      • ecc='emacsclient -c'
      • ecd='emacs –daemon'
      • ect='emacsclient -nw'
      • emacs='LC_CTYPE=zh_CN.UTF-8 emacs'7
  • Emacs作为一个老牌自由软件,以无限的可扩展性作为核心竞争力,但直到近年来才出现了一些比较好的扩展管理工具,细节可以参考ELPA: Emacs Lisp Package ArchiveGNU Emacs的终极扩展管理工具。在此强烈推荐下el-get,结合eshell,让我在Emacs身上闻到了一丝Lisp Machine的味道。
    • eshell是可以直接调用Elisp函数的(这是我无意间发现的,惭愧),结合el-get,使得emacs扩展的安装可以像debian的apt-get一般简单。比如说,你可以通过如下的elisp代码“一键安装”auctex、auto-complete、cdlatex-mode、slime、yasnippet8
(let ((softs '(auctex auto-complete cdlatex-mode slime yasnippet)))
  (dolist (obj softs)
    (el-get-install obj)))

3 Slime

  • 学习计算机四年有余,用过的编程工具IDE环境没有上百也有一打,但从来没有任何一种编程环境,能够像Slime那样,让我印象深刻,彻底颠覆我的编程方法学和世界观。
  • 这种颠覆型的编程模型就是slime的交互式编程。
  • 多数人都已经对C/C++/Java这种编译型语言的构建模型见怪不怪了,对于C++ Template这种扭曲的所谓元编程模型和超长的编译时间也学会了忍耐,大不了去上个厕所、抽一颗烟,要么就去泡杯咖啡呗。可是很少有人去深入思考过,为什么我们要忍受冗长的编译过程?为什么我们只是随便更改几句代码,就要重新做一次完整的编译?如果你从来没有思考过这些问题,那么请尝试下Slime吧,或者python/ruby也好的,交互式的编程会颠覆你的编程理念。
  • Paul Graham在它的《Ansi Common Lisp》用这样一段话来描述Lisp中的编程模型:"In purely functional code, you can test each function as you write it. If it returns the values you expect, you can be confident that it is correct. The added confidence, in the aggregate, makes a huge difference. You have instant turnaround when you make changes anywhere in a program. And this instant turnaround enables a whole new style of programming, much as the telephone, as compared to letters, enabled a new style of communication"
    • 我认为这段话强调的关键之处在于"instant turnaround", 即快速的修改和反馈,更加生动和详细的描述可以参考Paul Graham的另一本Lisp广告书《黑客与画家》。
    • 想快速构建一个链表一棵树?没问题,在lisp中这些都可以用大一统的list来表示的。Alan J. Perlis在SICP的序言中曾写到:"It is better to have 100 functions operate on one data structure than 10 functions on 10 data structures"。如果你认真用C/C++/Java实现过链表和二叉树,你会发现两者的数据节点声明是一样的,都是一个data域和两个指针域。为什么会这样?很少有人深入想过这个问题。后续我会写文章,从Lisp的角度上探讨下这个问题。
    • 想快速测试某个函数的正确性和性能?没问题,开启slime然后C-c C-c即可,你再也不用像Java那样,先建立一个类、然后声明一个static function,最后在写JUnit测试,然后编译、运行(架屋叠床的设计9,OOP的风格也许并没有声称的那么美好)。Alan J. Perlis在SICP的序言中还写到:"Pascal is for building pyramids—imposing, breathtaking, static structures built by armies pushing heavy blocks into place. Lisp is for building organisms—imposing, breathtaking, dynamic structures built by squads fitting fluctuating myriads of simpler organisms into place."
  • 关于Slime配置,如果你直到什么叫load-path、major-mode、mode-hook这些elisp概念的话,还是比较容易的。要么就只能照抄网上配置碰运气了。Understanding SLIME (Using Emacs and Lisp Cooperatively)是一篇极好的Slime资源,Quick Intro to Live Programming with Overtone令人印象深刻,极为震撼。
  • python/ruby这类动态语言可以用Slime吗?这点我没有找到太好的资料,slime的contrib目录里面有一个ruby文件,但是我目前还不会ruby,所以没有做过尝试;google上搜到的一些资料说python由于语言本身的限制并不能采用Slime的编程模式10,不过要彻底理解这些,恐怕要涉及到对各种编程语言的深入探讨,目前的我功力有限,恳请高手不吝赐教。
    • 不过像python/ruby/octave这类语言,在Emacs里面开个文件同时开个解释器边写边测也是可以的,关键字:comint-mode

4 Common Lisp

  • 和c语言不同,Common Lisp的实现有很多11,我主要用的是SBCLCCL ,ArchLinux下的安装都比较简单,不再赘述。
  • Quicklisp 是推荐的Lisp库管理工具,Quicklisp之于Common Lisp相当于cpan之于Perl.
  • 在Emacs中装好Slime后(推荐用el-get),将下列代码放入SBCL的初始化文件.sbclrc或者CCL的初始化文件ccl-init.lisp中。启动SBCL或者CCL开启swank,然后在Emacs slime中用M-x slime-connect连接即可(swank可以是远程机器)。
;;; The following lines added by ql:add-to-init-file:
#-quicklisp
(let ((quicklisp-init (merge-pathnames "quicklisp/setup.lisp"
                                       (user-homedir-pathname))))
  (when (probe-file quicklisp-init)
    (load quicklisp-init)))

;;; swank for emacs slime to connect
(load "~/.emacs.d/el-get/slime/swank-loader.lisp")
(swank-loader:init)
(swank:create-server :port 4005 :dont-close t)
  • LispWorks公司为Common Lisp提供有一份非常详尽的HyperSpec 文档,在ArchLinux中,你可以通过AUR来安装(yaourt -S cl-hyperspec)。
  • Slime对HyperSpec提供了良好的支持:slime-hyperspec-lookup。配置好Emacs-w3m,就可以在Emacs通过w3m查询Common Lisp语言文档的,很方便。我的配置片段如下:
(add-to-list 'load-path "~/.emacs.d/el-get/emacs-w3m")
(require 'w3m-load)
(setq browse-url-browser-function 'w3m)

;; view common lisp hyperspec documentation
(global-set-key "\C-ch" 'slime-hyperspec-lookup)
(setq common-lisp-hyperspec-root "file:/usr/share/doc/HyperSpec/")
  • M-x slime-connect之后,几个常用的功能:
    • C-c C-c: slime-compile-defun,编译当前光标所在处的表达式
    • C-x C-e: slime-eval-last-expression,对last-expression进行求值
    • M-.: slime-edit-definition,这条命令可以看到Common Lisp中的各种语言结构诸如defun、and、progn的源码,代码取决于你所用的Lisp实现,非常强大,是深入理解Lisp底层的良师益友。
  • 绝大多数Lisp实现均支持trace函数,可以用来跟踪递归过程,形象化地展示递归的运行机理,是深入学习理解递归的良好工具。比如下面的SBCL的REPL中的代码展示:
CL-USER> (defun just-return (n) (if (zerop n) 0 (+ 1 (just-return (- n 1)))))

JUST-RETURN
CL-USER> (trace just-return)

(JUST-RETURN)
CL-USER> (just-return 5)
  0: (JUST-RETURN 5)
    1: (JUST-RETURN 4)
      2: (JUST-RETURN 3)
        3: (JUST-RETURN 2)
          4: (JUST-RETURN 1)
            5: (JUST-RETURN 0)
            5: JUST-RETURN returned 0
          4: JUST-RETURN returned 1
        3: JUST-RETURN returned 2
      2: JUST-RETURN returned 3
    1: JUST-RETURN returned 4
  0: JUST-RETURN returned 5
5
CL-USER> 
  • 书的话,伞哥的博客 已经给出了很好的建议,我再加一本Paul Graham的《Ansi Common Lisp》,一本一本的看吧。“LISP is worth learning for a different reason — the profound enlightenment experience you will have when you finally get it. That experience will make you a better programmer for the rest of your days, even if you never actually use LISP itself a lot”12

差不多了,今天就写到这里,从早到晚写了一天了,累坏了,再写下去我估计读者也坚持不下来了。信息量太大,因此临时决定将文章拆成上下两篇,下篇我会谈谈Scheme/Clojure这两种Lisp方言开发环境的建立,并顺手谈谈Emacs和Maxima的集成。虽然Maxima本身并不是Lisp,但是其基于Lisp实现的事实,也让其与Emacs的联姻充满了浪漫主义的色彩,最近在深入学习算法分析,常常用到Maxima和LaTeX,十分快乐。敬请期待。

--

Footnotes:

1The Joy of Clojure》有这样一段 话:”Writing code is often a constant struggle against distraction, and every time a language requires you to think about syntax, operator precedence, or inheritance hierarchies, it exacerbates the problem. “任何反紧凑的语言,其繁杂的 语言特性往往会使得人们在解决问题的过程中脱离问题本身而陷入语言细节的泥沼,要么是 像C++那样到处是坑到处是禁忌到处是编程规范,要么是像Java那样到处是架屋叠床的类。 问题域和实现域是我最近常常思考的问题,其深度超越于编程语言的范畴,后续我会再写文 章深入探讨下这个主题。

2 http://www.techcn.com.cn/index.php?doc-view-132647.html

3 "The only thing the emacs OS lacks is a really good editor",更多的八卦, 这里

4 坦白的讲,如果以击键次数为标准,单单比较文本编辑的效率,我认为Vim的效率确实比 Emacs强很多。考虑可扩展性的话,我认为Emacs的elisp还是要比Vim的vimscript强很多的。

5 Living in Emacs, 这篇Emacs之所以如此出名,完全在于它起了一个好名字,简明扼要的给出了这篇教程的终 极目标。

6 不擅长干并不代表不能干,比如 这里这里、 还有 这里

7 这个主要是解决Emacs中文输入法的问题,细节可以参考 解决ibus在gVim/Emacs下不能使用的问题、还有 输入法 环境变量的故事

8 鉴于天朝网络的奇葩性,如果某些扩展无法安装,无妨追查下是否是网络问题。解决方案关 键字:ssh && proxychains。

9 架屋叠床这么有创造力的词来源于 function/bind的救赎, “尽 管如此,Java还是沾染上了“面向类设计”的癌症,基础类库里就有很多架床叠屋的设计……”

10 Why there is no SLIME for Python (or Ruby or…)?

11 All Common Lisp Implementations,伞哥的博客有很多关于Lisp极有价值的文章,他对Lisp的执着和 不断学习的精神也让我很是景仰。

12 How to Become a Hacker

 

打造高效的工作环境(番外篇1): windows/linux钗黛双收

前两篇文章基本上都在对Windows进行各种吐槽,最近Win8消费者预览版的出现更是让我对我的吐槽充满了信心——微软已经堕落到需要靠五颜六色的砖块来吸引眼球的地步了。我虽然没有亲身体验过Metro UI,但是我依然坚持,Metro UI是个无比糟糕的设计,一个彻头彻尾的倒退。颠覆传统、回归本原?是回归到Windows 3.x时代吗?一眼望去,Metro UI似乎很绚烂,像焰火漫天的夜空,但是稍不留意,作为使用者的我们就很容易迷失在图标的海洋里面。在我的印象中,直角化的图案一直是设计里的禁忌之一,而为了弥补这个缺憾(或者说是对传统的颠覆),Metro采用了大量的颜色来填充一个又一个的砖块,使得这些砖块能够不至于“迷失自我”。

我认为一个良好的使用界面应该要有统一的风格,至少不能有太多的颜色,而不是像一个调色版一样令人眼花缭乱;圆角在舒适的用户体验中是不可缺少的,可惜苹果在前,*nix在后,微软在UI设计方面的创新已经没有太多可选的余地,只能走一个极端来玩一场赌博;前两年的Ribbon界面应该是Metro设计的一个铺垫,我认为Ribbon确实是个不错的创新,能够将大量的MS Office小白用户从繁杂的菜单中解放出来,但是Metro实在太过了;最苦的应该是Windows开发者,唉,三年一小改、五年一大改是微软的一贯风格。

不过这些对我都不重要,重要的是我相信,即便我对Windows吐槽了这么多,许多朋友还是小心翼翼,不敢投入Linux的怀抱。正常,因为Linux的门槛确实不低,加上一大堆的概念和思维的转变,再补上如百花从般绚烂多彩的发行版和如满天繁星般闪亮的各种大小bug瑕疵,足以令80%的初学者望而却步。而我又不想让我的文章太过小众(我在开篇中说过,这一系列的文章主要面对以Linux系统为工作环境的初级Coder,兼带高级电脑使用者)。因此,我总结了一些变通的方法,让你可以在你的Windows系统上无痛地尝尝Linux的鲜。

本文主要针对那些想要尝试下Linux但又怕损坏自己Windows的工作平台的同志。当然,本文的重点还是命令行环境的建立,因为熟练掌握命令行是高效使用Linux的必由之路。而至于图形界面,由于Windows Explorer的顽固存在,Windows下的GUI没什么可折腾的。除了虚拟机,似乎没有什么特别完美的办法能够在不切换系统的情况下体验Linux的图形界面。

本文主要介绍一些简单易行的方法手段,能够在Windows下建立一套相对完整的Linux命令行实验环境,为后面系列的文章奠定一个基础,并借此扩大下本系列文章的“打击面”。

1 Cygwin

  • What:通俗的讲,Cygwin是Windows上的一个模拟软件,最初是由Cygnus Solutions公司开发的。
    • Cygwin通过对Windows API的整合封装模拟出一套POSIX API。有了这套基础性的POSIX API(就是Linux C编程的什么fork/wait啊、signal啊、open/close啊这套东西),再加上一套合适的交叉编译器(就是大名鼎鼎的GCC了),就有了在Windows上模拟运行*nix软件的基础。
    • Cygwin本身也包含了很多常用的自由软件,包括核心的bash、coreutils、findutils等,配合Cygwin/X这个东西,甚至可以直接运行许多X Windows上的软件,包括Emacs,gvim等等。
  • How:1
  • Pros:傻瓜式安装;可以和host文件系统方便互通。
  • Cons:没有类似于apt-get这种杀手级的软件包管理工具,安装一些额外的软件颇费周折,而且官方软件库更新较慢。
  • 可用性:3星。
    • 很多以linux为推荐运行平台的软件也支持在Cygwin上开发测试,比如hadoop
  • 结论:如果只是看看Linux的命令行长什么样子,那么本文看到这里就可以了。

2 SUA

  • What:SUA是Windows的一个subsystem,提供一些基本的POSIX API,在此基础上提供一些简单的*nix程序,诸如vi、ksh、csh、ls、cat等。
  • How:@see 这里
  • Pros:SUA是微软自家的东西。
  • Cons:除了Pros之外都是缺点。
    • 软件稀少,版本低无法扩展是致命伤。我只用过一次就甩了。
  • 可用性:1星,玩具而已,未见日常使用
  • 结论:了解即可

3 虚拟机

  • What:虚拟机大概是最常用也最方便的方式了吧,虽然虚拟机本身的水非常深。我第一次接触虚拟机的概念还是在大一上,那个时候自己还在熟悉怎样更好更快地安装Windows操作系统,Virtual PC自然而然就成了我的好伙伴,而我也为能在Virtual PC下顺利安装一个MS DOS 7.1感到欣喜不已。后来接触到了VMware,它有两个显著的特性,一大特性是强,另一大特性就是卡。VirtualBox是我的最爱,轻量快速,乃居家旅行杀人越货必备良品。
  • How:准备10G硬盘空间就OK。
    • VirtualBox有一些非常给力的特性:
  • Pros:虚拟机能够给你100%原生的Linux观感和体验,而且使用过程中无毒无害,更不用担心将系统不小心搞挂,配合上VirtualBox的文件共享功能和全屏功能,再加上一个稍微给力点的电脑2,有时候能够达到以假乱真的效果
  • Cons:性能上还是有些损失的;除了VMware,其余虚拟机软件好像没有特别方便的方法支持bridge network,有时候很不方便。
  • 可用性:4星
  • 结论:VirtualBox乃居家旅行杀人越货必备良品。

4 Colinux

  • What:Cooperative Linux, abbreviated as coLinux, is software which allows Microsoft Windows and the Linux kernel to run simultaneously in parallel on the same machine. 简单而言,coLinux和VirtualBox这类虚拟软件最大的区别在于,coLinux运行的linux系统是何Windows宿主系统共享系统资源的3,因此其性能对比VirtualBox这类虚拟机软件要好很多。
  • How:coLinux本身的安装还是要非一番周折的。幸运的是,万能的社区提供了两个打包好的一键安装方案,那就是andLinuxTopologilinux。我只用过andLinux,推荐。
    • coLinux可以通过samba和windows系统共享文件。
    • 可以将coLinux做成随系统启动的一个服务,并且在coLinux里面开始sshd进行,之后用putty这类软件连接ssh,就可以全面享受linux命令行运指如飞的畅快了。
    • 如果你以前没有用过apt-get这个程序,这次不要错过,因为集中化的软件管理机制是linux(debian/ubuntu的apt-get)的杀手级特性,也是我的最爱。
  • Pros:除了Cons都是Pros。
  • Cons:None。
  • 可用性:5星。
  • 结论:这是我大三暑假在华数淘宝实习时跟一位高手偷师过来的,最爱,强烈推荐。

5 其余解决方案

除了以上谈到的,KDE for Windows是在Windows上体验KDE桌面环境的一种可行的方案,虽然其目前bug依然多多;如果你玩腻了以上所有,想装个真家伙,又怕手生一不小心误删重要文件,那么Ubuntu Wubi应该是一个不错的解决方案。如果你已经开始讨厌Ubuntu Wubi了,那么恭喜你,你已经成功地被我忽悠,进入*nix的精彩世界,just enjoy it。

--

Footnotes:

1 什么?你不会问我Cygwin要怎么安装吧?

2 不够2G内存的同志赶紧去花钱加内存吧,当然,有双屏更好。

3 coLinux运行的linux系统需要对内核进行特殊的修改。当然,关于这种修改本身和coLinux背后的原理已经远远超出了本文讨论的内容和本人的技术水平,还请高手不吝赐教。

少即是多——兼谈对SNS的看法

网络的普及,让知识的获取变得空前便利,也让噪音的弥漫变得防不胜防。 ——题记

1 SNS小议

我一直对SNS持踌躇的态度,这里所说的SNS包括但不限于人人、新浪微博、QQ、Facebook、Twitter等。事实上我也有一段时间曾经非常沉迷于校内网(人人网的前身),也会用Nokia 6670在http://m.xiaonei.com 上发表各种各样的生活琐事和所谓的人生感悟,然后强迫症般地不断刷新刷新,等待着盼望着翘首着“朋友”的回复,尽管这些“朋友”可能有30%完全没见过面,另30%仅仅是点头之交,再30%比较熟但谈不上铁,真正熟的知心朋友可能根本就不在乎你这些烂事——“擦,你个SB,别总在老子面前买萌了”。

大二,我生了一场不大不小的病,挂了七八门不小也不大的课,丢了一个爱的深沉的社团,弄得自己半年深居简出,沉迷网络,靠youku土豆,笑傲江湖勉强度日。都说时间是医治伤痛的良药,回过神来,我发现,在你最最痛苦的时候,哪怕是你最亲最铁的朋友,也不一定能够理解你的痛苦,更何况承担? 没有任何人能够对你的痛苦感同身受,更多的时候,我们需要独力承担,尽管有时独立难支,也要咬牙硬挺。 这之后的半年,我删除了自己所有的校内日志,并开始学习linux、开始补课、开始尝试着重新带一些户外活动,培养一下接班人——虽然名义上我深爱的社团由于种种原因挫骨扬灰、烟消云散了。那半年我过得充实、快乐,不但培养了几个户外的新人,新交了几个朋友,交接了自己的责任和工作,也开始学习linux,进入开源软件的精彩世界。我的大学,前两年围绕着户外,围绕着社团和社团的兄弟们运转,后两年围绕着开源和自由的学习精神运转,我经历过单车旅行路上风雨的洗礼,看过东极岛的日出,矮拉山的彩虹,挂了十几门课,但也学会了linux命令行下运指如飞键如舞蹈的畅快淋漓。

我渐渐的发现,校内上一方面有我很多不大不小的朋友,我想要去的地方和想拍的照片,另一方面这也充斥了各种诸如“萌翻了、美爆了、雷坏了、感动得一塌糊涂了”的所谓放松娱乐甚至是”不分享不是xx人“的垃圾信息。更可怕的是,我逐渐发现,一方面,我在这些网站上,浏览垃圾信息的时间大大超过了”关心朋友“的时间,另一方面,我也许并不需要特别关心绝大多数的朋友,即便是最铁的朋友,他今天吃过什么饭、去了哪些地方,这些也与我没啥大关系。最后的最后,我发现,大量的垃圾式的快餐化的娱乐分享、视频和照片会造成两个极其严重的后果:

  • 思考浅薄化。
    • 现在的我们,再也难以想象,在某个美好的阳光四溢的午后,合上笔记本,沏壶菊花茶,转一支钢笔,抽一页书签,在一本书上写写画画。更多的时候,我们是坐在书桌前,玩玩游戏、刷刷微博、浏览一篇又一篇的吐槽八卦日志。谈到这里,我不得不说,微博的存在使得思考浅薄化变得愈发严重1。我们没有生活在诗词曲赋的唐宋时代,这个事实使得140字很难表达出某种深刻的思想,更多的是一些日常生活中乌七八糟的文字碎片,这些文字碎片就像肯德基麦当劳里面的薯条一样,蘸上西红柿酱(不蘸也行)就可以吃,随便什么人都可以吃,吃这个东西也不需要什么锅碗瓢盆、烹饪调味。只是这些东西吃完了就是吃完了,没有营养,也没有回味。纵观社会化网络的发展,从Telnet、BBS,到个人Blog,再到微博,我们可以发现,人们想在网络上发表信息变得越来越容易,这也造就了这两年来微博的井喷以及进一步的信息爆炸2。可是无论网络怎么发展,信息怎样爆炸,在我看来,高质量的正面有益信息所占的百分比并没有提高,简单来讲,就是信噪比没有发生大的波动3。我认为Wikipedia是网络时代首屈一指的超高信噪比的网站,但是除了Wikipedia,我还能想起哪些网站,这些网站上的信息都是对我有益而无害的呢?我想不起来。人人不是,FaceBook不是,微博不是,QQ更不是。如果诸位高朋能够找到类似于Wikipedia这样的宝库,请不吝告诉我,我将不胜感激。
    • 思考浅薄化的间接后果就是你很难再去花几个小时连续的时间去阅读一本书了,你很难再去花几个小时连续的时间去想一道题的不同解法了。你也很难写出大段的具有逻辑性的精彩文章来,你所剩的只有只言片语,文字碎片。
  • 时间碎片化。
    • 如果你稍微懂点计算机操作系统原理,你就是知道Context Switch(上下文切换)这个名词,它说的是计算机CPU在切换不同进程时的一种开销。所以分时系统计算机的效率理论上永远也不会达到100%,因为肯定有一部分资源耗费在进程切换中了。说得更通俗一点,我们可以用初中生都知道的有用功和总功的概念来阐述这个观点。比如你想把一桶水从一楼提到十楼,当你耗费了三个包子的体力(这个是总功)终于把水提上来的时候,你会发现,如果不提桶,只提水,只需要耗费两个包子的体力(这个是有用功)就可以了。因为你真正目的是提水,而不是提桶。读者可能说,老兄你这不是废话吗,这么简单地道理还用你磨叽?想提水的话当然需要桶喽。别急。之所以打这个有点荒谬的比方,是因为从吃喝拉撒睡的角度上看,我们的人生就是如此。我们都知道,人的一天24小时(这个是总功),平均有8个小时(这个是无用功)是要耗在床上的(别想歪了哈,我说的是可是正常的睡觉),但是人生下来不是为了睡觉的。人的一辈子可能会干很多事情,比如读书写字画画演讲办企业下馆子,但每个人生下来都不是为了睡觉的。而睡觉又是必不可少的,正如你想提水,水桶是必不可少的一样。在此基础上,我们再来思考,互联网,尤其是SNS,到底怎样使我们的时间支离破碎?为什么大块的无人打扰的时间如此重要?
    • 有一个非常著名的理论,叫做一万小时天才理论。这个理论讲的是,一个人,如果想成为某一领域的专家权威,需要至少一万小时的刻苦专业训练,即便这个人是莫扎特、高斯一样的天才,一万小时也概莫能外。为什么你不是下一个马克·扎克伯格4?因为当人家已经编写了几万行代码考虑软件架构的时候,你还在骑着自行车绕着院子转圈;当人家已经在考虑大规模网站的可靠性可扩展性的时候,你还在纠结要不要去听明天早晨八点的课;当人家已经在考虑怎样透过自己的网站改变世界的时候,你还在犹豫着是要考研还是要找工作,找工作是要找外企呢还是体制内的呢。我们已经谈到,我们整个人生的三分之一要耗在睡眠上。而几乎可以肯定的是,对于一般人而言,剩余的16小时时间里,吃喝拉撒至少要占用3个小时的时间。剩下的13个小时,我们的大脑还要进行各种各样的Context Switch,效率肯定也是要大打折扣的。有一个理论讲的是,人同一时间内最多只能关注七件事情5深入的思考是容不得别人打扰的,一旦中断,思考的大厦就会崩塌,重建的过程往往循环往复、困难重重。 这就是为什么聪明人只想和更聪明,至少是和自己一样聪明的人一起工作的原因,资深的Hacker更是如此,他们才没有耐心告诉你Apache该怎么配置呢6

为什么深入的思考如此重要?因为人类的文明已经到了如此的境地,如果没有深入的思考,你就不可能识他人所不识、知他人所不知。大到一项科学理论的创立,小到一项发明的完成,想要影响世界,make a difference, 没有少则几年长则半生的苦苦思索,就想把人类已经发展到如此高度的文明再向前推进哪怕一小步,几乎是不可能的事情。那么,为什么大块的无人打扰的时间如此重要?因为没有大块的无人打扰的时间,就不会有深入的思考。 而微博、QQ、360以及最普通的桌面上搜狐、迅雷新闻首页弹出窗口的存在,弥漫在电脑LED上各种各样的名叫distraction的东西,会让你的思考深度曲线像正弦函数一般,摇摆不定,上下颤动7

以上讲了这么多,诸位读者可能会觉得我这个人太偏激了。毕竟”世界潮流,浩浩荡荡,顺之者昌,逆之者亡“,发端于第三次工业革命末端的互联网终将引领整个第三次工业革命的潮流,被我这么一说,倒好像成了吞噬人生毁灭梦想的洪水猛兽了。其实我不是这个意思,我热爱互联网,也热爱互联网行业,以及互联网行业的底层设施——开源自由的软件,和互联网行业的高尚品德——自由、分享的精神。任何事情都是一把双刃剑,网络如此,读书也是如此。而我采取的策略就是“取我所需,防我所恶“,核心精神就是本文的题目——“少即是多”。由此这四字箴言延伸而来,具体到生活中(不仅仅是对待互联网),就是:

  • 少见些人
  • 少说些话
  • 多读些书
  • 多做些事

2 少见些人

“她们都老了吧,她们在哪里呀,我们就这样,各自奔天涯” —— 朴树《那些花儿》

大学伊始,我非常幸运地加入了旅行者户外。这里有一群人,他们猥琐、腐败、自虐,他们行走、思考,他们始终在路上。他们是行者,而行者是无疆的。就好象命中注定一般,我庆幸来到浙大而没有选择去上交,庆幸提前半年过来上预科,碰巧就看到了这群人,碰巧就加入了这个组织,从此释放了深埋于我血液中骨子里十八年的流浪旅行的冲动。这之后的两年,靠着一辆单车,我几乎走遍了浙江省的各个城市,在中国的版图上画了几条长长的线圈。我热爱这里的人,热爱这里的坦诚相待,热爱这里的无拘无束。我感觉我找到了组织,沉迷其中,不可自拔8

这之后的两年,我接受了社团的工作,当上了社团的会长,带领着一群人山山水水并和学校团委保守派做不朽的抗争,见识到了比我早四年的学长和比我晚四年的学弟,见证了一个社团由巅峰到低谷到在我手中彻底除名毁灭最后又凤凰涅磐浴火永生的全过程,这期间当然免不了人员的去留摩擦,以及日久天长的隔阂和疙瘩。有的时候我常常分不清楚,我究竟是热爱这个组织多一点还是热爱这个组织里的人多一点,又或是,我两者都不爱,我只爱旅行,爱组织爱人只是因为爱屋及乌?

什么是真正的行者?行者最宝贵的精神是什么?行者仅仅是骑着单车去拉萨吗?仅仅是搭车去柏林吗?仅仅是十年不变的背包旅行吗?我无法回答,因为我至今也没有一个明晰的答案。所以我已经很久没有出去骑车了。因为在没有想明白这个问题之前,户外和骑车对于我来说,差不多只是重复劳动罢了。

到了大三,当我交接了手上的工作开始全身心地投入到计算机科学的学习之后,我开始越发明白一个道理:人与人之间的交往和感情是靠缘分的。没有什么特别的道理,有的人就能和你贴的很近,即便你们物理上远在天边;而有的人,即便是出去旅行睡一个帐篷,也难免会有隔阂。我曾经天真的幻想,大家一起去旅行是一种极好的交友的方式,因为热爱旅行的人一定是坦荡的、诚实的、热爱自然的、激情澎湃的。这不正是我欣赏的人吗?可是很奇怪,一场旅行过去,大家回到自己的生活轨道上,各自依旧。其实对于绝大多数人来讲,所谓旅行,不过是逃离烦恼、暂时放松,给自己打一针麻醉针的好方法而已。是我看得太重了。

这之后我一个人,对,就一个人,踏着一辆单车走了几千公理的路,抛洒了一路的汗水。有人问,为什么不找个伴?会不会感到孤单?也许吧。也许人生的基调就是孤独的,而你要独自习惯这种孤独。史铁生说,“没有什么能证明爱情,爱情是孤独的证明9”。

真正的朋友不需要保持频繁的联系,需要频繁的联系才能保持朋友关系的人,也许并不是真正的朋友。70%的社交(包括饭局)都是很无聊的10 所以,亲爱的朋友,如果你生日时没有收到我的礼物,并不代表我的心里没有记挂着你。下次我们再次见面的时候,我相信,亲切依旧,我会亲自为你下厨,做几个小菜,然后给你讲一讲我最近在做的事情、看的书籍、开发的自由软件(如果你感兴趣的话)。

缘起缘落,让我们顺其自然。云卷云舒,片刻的相聚并不能代表永恒,也许我们的友情会化作雨水,飘飘然的,润物于无声。原谅我好久没有和你打招呼,原谅我好久没有向你告知我的近况吧。我最近很好,但愿你也一样。

3 少说些话

"Talk is cheap. Show me the code." – Linus Torvalds

3月份的时候很幸运领到了WPS for Linux的邀请码,做了一些小事,也参与了一些论坛讨论。但是讨论的过程中,还是发现了一些令人忍俊不禁的帖子。比如有人建议金山出个操作系统、有人建议金山出一款输入法,更有甚者,还有人要求金山放弃QT,直接用Xlib编程,原因是他想要获得原生的界面效果;还有人要求金山出一款类似Office的VBA的中文扩展编程语言,注意,是中文编程语言。对于后面两位天外来客,我只能说,你们实在太高估我们地球人的能力了,仿佛软件中的QT就像积木一样,拔下来就可以换的。我劝你们还是多读读我们地球人的书,对我们地球人的能力有更深入的了解之后,再来说这说那。

所以我现在说话有些诚惶诚恐,因为我不知道,是否有朝一日,我的言论就像两位天外来客的言语一样,幼稚无知,却不自知。Talk is cheap。每个人都可以豪言壮语,但不是每个人都能信守承诺,坚持到底。所以要少说,多做,因为你不知道什么时候,你说错了话,却不自知。

我们还谈到,互联网的井喷式发展并没有改变互联网本身的信噪比,相反,我倒觉得互联网的发展是不断在降低互联网本身的信噪比,换言之,互联网上的噪声会越来越多,而真正有价值有营养的言论会越来越少。 如果把互联网比作海洋,那么现在的互联网,水面上水体里已经充满了各式各样大大小小的文字碎片和信息垃圾。 而这种趋势恰恰又是互联网繁荣发展必不可少的动力。因为互联网若想发展,就必须从阳春白雪的APRANET ——只给学校、教授和国防部用的网络,逐渐变成平等、开放、自由、信息获取和制造愈发方便的INTERNET。所以你会发现,从Telnet到BBS,从个人Blog到MicroBlog,我们制造信息的流程越来越简单,分享信息的方式越来越扁平,获取信息的手段也越来越迅捷。这极大地满足了劳苦大众唠叨猎奇和八卦的本性,使得原先在路灯下大叔旁棋盘边上的家长里短转移到了互联网上,特别是微博上。而事实上是,这些“碎碎念”般的文字碎片对你个人而言,不仅无用,而且有害。因为它会使你的思维和时间变得“碎碎念”化,这点我前面已经阐述过的。

我们没有办法改变互联网“碎碎念”化的这种趋势,但是一方面可以从自身做起,少给互联网制造一些垃圾信息(事实上我也会碎碎念,只不过我的主战场在豆瓣,看得人少,所以我也就不必担心会过多干扰他人的思维和生活);另一方面,可以想办法给互联网制造一些有营养的东西,恬不知耻的例子,比如这篇博客^_^ 。

4 多读些书

“求知欲是治疗无聊的良方,求知欲本身无药可治11。”

大二大三的时候,我曾经苦苦思索,人为了什么而活?最后得到的答案是两个字:快乐。具体说来,活着一是为了让自己快乐,二是为了给他人带去快乐。这几乎也可以推导出另一个重要的命题——人生下来就是要受苦的12。我们常常讲,人生之不如意,十有八九。不可选择的出身,无法追回的时间,聚散离别的亲友,独自一人的落寞,无可避免,无法选择。但我们这代人是幸运的,我们没有经历恐怖的文革,却享受了改革的成果。所以我相信,在这篇文章的众位读者里,95%的人都没有也不会有过饿肚子的感觉。 那么归结起来,我们活着就是吃饱了撑的,没事找事,反正得找点乐子,否则会无聊,会空虚,再之后就是碎碎念了^_^ 。

找乐子的方式各种各样,找到的乐子也不一而足。 乐子有深浅之分、长短之别。 读书所带来的乐趣,深邃而持久,远比饱餐一顿、高歌一曲更能满足人类的精神需求。可悲的是,人们已经不再阅读了,连乔布斯都这么说。有人说,使人毕业后拉开差距的,不是8个小时的工作时间,而是8小时外的业余时间。我承认这句话很有道理也很精辟,一针见血地指出了业余时间看书学习的重要性,但是我并不是100%赞同这种说法。因为在我看来,读书应该是很纯粹的活动,就是为了读书,完全不是为了什么拉开差距,更不要妄谈钱权地位影响力了(这可能是很多人对于差距的定义吧)。越是为了“拉开差距”而去读书的人,其往往会越走越偏,领会不到读书的真谛。

这或许也是当今中国教育的一大弊病和恶果吧。

5 多做些事

“用勇气去改变可以改变的事情,用胸怀去接受不能改变的事情,用智慧去分辨二者的不同。” ——李开复

我相信,如果李开复老师不是童年就移居美国,今天的创新工厂可能未必存在;如果陈士骏先生不是童年就移居了美国,也未必会有Youtube。有些东西是无法选择的,比如出身。一个农民的儿子和一个教授的儿子起点是不一样的;一个贵州山区的孩子和一个北京的孩子,出路也是不一样的。因为世界上本来就没有绝对的公平。

常有人讲,Your time is limited, you must follow your heart13。可是很多人连明白这个道理的机会都没有,一辈子就那么过去了。我很庆幸,在我二十岁出头,还不算太晚的时候,就已经明白"follow my heart(我随心动)"的这个道理了,所以我是个不循礼法、不懂屈服、特别能折腾的人。因为我明白,生命有限,如果我可以在有限的时间里做更多的事情,那么我就是在变相延长着我自己的生命。

我有一个观点,人生在于有目的地折腾。

我现在还有一个烦恼,就是始终无法克服起床困难综合症。

6 生命的维度

如果你看过Dimensions: A Walk Through Mathematics,你就会理解在艾舍儿的画作《爬虫》中的蜥蜴的困境,它生活在二维空间,因而几乎永远无法得到直观的三维认识。三维空间对二维动物的想象力而言,就如四维空间的相对论之于绝大多数人类的认知一样(我也不理解相对论),就是一个彻头彻尾的悲剧。

既然如此,为什么还要谈维度?什么是生命的维度?

我以为,人的生命是有维度的,读书是生命的一个维度,旅行是生命的另一个维度,写作画画也可以是生命的一个维度,搞科研发论文也可以是生命的一个维度。更多的维度需要由你自己来定义。之所以借用Dimensions的引用,是想说明一个观点,那就是人要勇于尝试。因为你永远无法预料,什么样的尝试会给你打来什么样的机遇、会给你的生命增加怎样的维度。生命的维度越高,人判断事情的本领也会越强,正如三维空间的人类比之于二维空间上的蜥蜴,可以看懂正立方体,但是比之于四维空间的人(假设有这样的生物),我们又几乎无法理解超立方体的存在了。但是你不理解,并不代表它就不存在。它一直在,只是你无法领略它的美。

旅行就是这样。在我上大学之前,我从来无法想象,一个人,可以盯着烈日、冒着风雨、背着行李、踏着单车冲上青藏高原,但后来我做到了,其实也远没有那么难。一旦你意识到外维空间的存在,你就会像影片中那只爬出二维空间走进三维空间的蜥蜴一样,领略高维空间的美,并看着自己的同伴在低维空间力徘徊迷茫,不知所措。旅行带给了我很多财富,它让我更淡定地面对惨淡的人生,并且去尝试在各个角度上寻找突破,不断地想办法给自己的生命拓展出新的维度。

7 求于至简,归于永恒

在所有的SNS网站以及所有的中国互联网公司中,我最喜欢的是豆瓣。它没有微博的喧闹,也没有校内的八卦无聊。它不跟风,却坚持自己的理念,做一家慢公司14,通过对产品和用户体验的绝对专注和持续改进,不断的改进用户体验,给用户创造价值。虽然这个过程难免一波三折,并且并不是所有人都能理解(绝大多数是因为狗日的中国网络审查制度),但是不可否认,豆瓣网的整体用户素质绝对是各大SNS中数一数二的。单凭这点,就足以让我抛弃所有其他的SNS,投身豆瓣了。更可贵的是,豆瓣是一个高信噪比的网站,我在上面的所得,包括高手的书评影评、一些颇有质量的博文订阅,以及对自己学习历程的记录,都让我受益匪浅。

顺便说一句,中国互联网公司的惯用手法就是耍流氓,不光对美帝耍,对国内用户也毫不客气。鉴别这类流氓网站有一个最简单的一个评判标准——看看能不能方便的注销账户。 以此标准,百度、新浪、人人都是流氓网站,操着为用户服务的口号,背地里却耍着请神容易送神难的手段,就是不让你注销、就是不让你离开、就是要不断地发邮件骚扰你、就是想让你“多回头看我两眼”、就是想让你的时间思考碎碎化。他娘的,如果不是有GFW的存在,我会用人人、百度?

少即是多,试着使自己的生活简单化、心灵单纯化,给自己留出点时间看看书、写写字,哪怕做做白日梦也好的。

只有求于至简,才能归于永恒。15

--

Footnotes:

1 我没有说微博不好,事实上我认为微博和BBS、博客一样,是网络信息源平民化过程中的一个自然而然的必要产物。

2 同时也造就了一批打着“云计算”、“云存储”、“大数据”的创业的和非创业的公司企业^_^ 。

3 《浅薄》, 我近期的想读书籍之一。

4 参看这篇 《Facebook效应》 的书评

5 未鹏的《暗时间》这本书里,有关于语言、思维、大脑、时间非常精彩的论述。

6 阮一峰翻译的的《软件随想录》 里面有一些非常精辟的关于聪明人、Hacker的论述。

7 如何避免这些分散注意力的东西,这是我写作这篇文章和《打造高效的工作环境》系列文章的主要动因之一。

8 沉溺其中、不可自拔是改变世界、成就梦想的必由之路。

9 感谢Wooooonderful告知我这句话。

10 70%…这个是俞敏洪老师微博上的观点,这里再次郑重推荐下俞敏洪老师的"创业传记", 会让你对人生、中国的人情社会,以及朋友关系有很深的思考。

11 豆瓣上的一句话,忘记出处了

12 这也是俞敏洪老师的观点。

13 乔布斯在斯坦福大学的演讲

14 参考豆瓣:“慢公司”

15 《计算机的心智——操作系统之哲学原理》

GAS 汇编-1:起步

百度离职后,随着递归式学习的深入,我的涉猎从Lisp/Scheme/SICP,到APUE,最近又转入到了汇编语言上。老实说,汇编这种文物级的东西,在一般的计算机编程中是绝难碰到的。但是计算机工程学到一定层次,瓶颈所在,就会发现,总是有那么几样东西,诸如汇编、C、算法、编译原理、体系结构、操作系统、网络数据库等,这些基础知识绕不过,躲不开,而能否跨越这些坎,从某种意义上决定了一个程序员能否从合格到优秀、从优秀到卓越。

最早接触汇编还是在大一上一门计算机硬件基础的导论课上,我原以为这课既然叫计算机硬件基础,讲的应该都是软驱硬盘主板鼠标等如何拆卸组装选购的科普杂事。要知道,那个时候的我刚刚学会在Windows XP控制面板里面装卸软件。给我们讲课的是一个老头,用的是自己编写的教材。老头最津津乐道的事情是在90年代中期自己花了400块钱买了一个32M的U盘,却因为U盘容量太大而束手无策,不知道该存些啥东西。这课开始几次讲得确实是硬盘、鼠标组成科普原理啥的,后来讲着讲着就拐到了16bit DOS下的debug汇编编程,mov ax, bx这种,结果可想而知,我们全班同学一下子就晕的不行,弃考了一小半,剩余勉强坚持到最后的,又挂了好几个。

大二下又上了一门汇编语言的通识课,仍旧是16bit DOS,仍旧是不得要领。直到大三,上了专业的汇编课,硬着头皮,在Linux+Dosemu+masm5.0的环境下编了一个300多行的软件模拟浮点数算数指令的汇编程序,这才对16bit汇编中的一些基本概念诸如段、寻址方式、中断等等, 有了一个初步的了解。不过说实话,大学里的汇编学习和实际脱节太远,多数的汇编教育还停留在16bit DOS环境下,用得教材以清华的那本《IBM》居多,做得实验也都是Window 98DOS子系统下步进电机、控制电灯明暗这种实验,一到实际编程,32bit和64bit的保护模式下编程,加上复杂的MMX指令,浮点数指令,就傻眼了。

本文介绍Linux平台下32bit的GAS汇编语言编程,GAS是GCC编译器的汇编器。之所以采用这套工具,主要的原因在于GCC工具链非常成熟,GAS配合Emacs、GDB,加上Shell命令行,和objdump、od等工具,对于我这个Linux fans和命令行控有莫大的吸引力,工作效率自不必说,谁用谁知道。

x86汇编编程有两种截然不同的语法风格,我们常见的是Intel语法风格,类似这种:

mov eax, 32

而GAS汇编采用的是AT&T的语法风格:

movl $32, %eax

讲GAS汇编的书籍并不多,以下两本都是好书:

我粗略的看完了第一本,第二本正在进行中,这样的学习已经让我对C语言中很多高级的概念,诸如指针、编译连接过程、内存布局和分布有了比较深入的理解。

不过对于计算机的学习,光说不练是远远不够的。书本上看懂并不代表就能快速写出准确、实用、高效的程序来。我学习一门计算机语言,往往都会去找一本评价较好的书籍,把书上所有的示例代码看懂,自己亲自敲一遍。这不,刚刚起步,就碰到了两个小问题,记录下来,作为GAS汇编学习的起点。

首先是Emacs编辑器的问题。Emacs本身对汇编语言编辑功能的支持远比不上对其余高级编程语言的支持。一个内建的ASM Mode,乃是ESR的大作,粗略看了下,两百多行的代码,功能有限,要命的是还有个小bug,那就是默认的注释符号为';',而不是gas汇编接受的'#'。恰好这个月花费了大量精力研习了emacs manual和elisp manual,因此不自量力写了两个小函数,专门用于gas汇编语言的注释,(代码很初级,elisp高手轻拍):

(defun gas-comment-region (start end)
  "comment region for AT&T syntax assembly language
The default comment-char for gas is ';', we need '#' instead"
  (interactive "r")
  (setq end (copy-marker end t))
  (save-excursion
    (goto-char start)
    (while (< (point) end)
      (beginning-of-line)
      (insert "# ")
      (next-line))
    (goto-char end)))

(defun gas-uncomment-region (start end)
  "uncomment region for AT&T syntax assembly language
the inversion of gas-comment-region"
  (interactive "r")
  (setq end (copy-marker end t))
  (save-excursion
    (goto-char start)
    (while (< (point) end)
      (beginning-of-line)
      (if (equal (char-after) ?#)
          (delete-char 1))
      (next-line))
    (goto-char end)))

希望随着学习emacs和elisp的深入,自己能够写出一个稍微好一点的gas汇编的major-mode(顺便,前两天给emacs的包管理工具el-get提供了一rcp被接受了,这是我认真学习计算机以来第一次为开源项目做出点实际的贡献,开心)。

第二个问题是64位下写32位汇编程序的问题。这其中涉及到两个问题。其一是32位汇编和64位汇编的内在区别问题,包括语法层面上的,和机器层面上的,这个我还没有发言权,这里有一篇文档,详述了x86下64位汇编和32汇编的区别。而由于我上面提到的两本好书中,采用的都是32位的汇编,而32位向64位的迁移并不是无缝的,外加上我现在对64位汇编了解有限,因此如何在64位平台下编写、汇编、连接32位汇编,就是我们要解决的第二个问题。

要解决这个问题,就要对c程序的整个编译流程有一个稍微详细点的了解,光靠编译器点击一个按钮自动编译,是解决不了这个问题的。这个我不再具体展开,《程序员的自我修养》是这方面难得的一本好书,我粗略看过一遍,但一直不敢在豆瓣打上“看过”的标签,因为没有对计算机体系结构、汇编语言和C语言的深刻了解,是不可能深刻理解并“看过”此书的。

下面给出一个示例程序,来自于《Professional Assembly Language》

        .section .data
output:
        .asciz "The processor Vendor ID is '%s'\n"

        .section .bss
        .lcomm buffer, 12

        .section .text
        .globl _start
_start:
        movl $0, %eax
        cpuid
        movl $buffer, %edi
        movl %ebx, (%edi)
        movl %edx, 4(%edi)
        movl %ecx, 8(%edi)

        pushl $buffer
        pushl $output
        call printf

        addl $8, %esp

        movl $4, %eax
        movl $1, %ebx
        movl $output, %ecx
        movl $33, %edx
        int $0x80

        pushl $1
        call exit

这个程序很简单,主要是调用cpuid指令得到CPU本身的一些信息,然后调用C标准库中的函数打印出来,之后利用linux系统调用write打印出一个字符串,最后再次调用C标准库中的exit函数,状态码为1。我们来验证下此程序的编译、连接和运行过程,主要的汇编、连接指令是:

  • sudo pacman -S gcc-multilib binutils-multilib gcc-libs-multilib lib32-glibc
    • 安装必须的32位编译器和运行库
  • as -g -o cpuid2.o cpuid2.s –32
    • –32: 生成32位的.o文件
    • -g: 生成gdb调试信息,便于程序的调试
  • ld –dynamic-linker /lib/ld-linux.so.2 cpuid2.o -o cpuid2 -m elf_i386 -L/usr/lib32 -lc
    • –dynamic-linker /lib/ld-linux.so.2: 采用动态连接
    • -m elf_i386: 生成32位的程序
    • -L:讲lib32-glibc的库加入库搜索路径
    • -lc: 连接标准c语言库,printf必须
% uname -m 
x86_64
% as -g -o cpuid2.o cpuid2.s --32
% file cpuid2.o
cpuid2.o: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped
% ld --dynamic-linker /lib/ld-linux.so.2  cpuid2.o -o cpuid2 -m elf_i386 -L/usr/lib32 -lc
% file cpuid2
cpuid2: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), not stripped
% ./cpuid2 
The processor Vendor ID is 'GenuineIntel'
The processor Vendor ID is '%s'
% echo $?
1
% ls -l
total 24
drwxr-xr-x  2 lox users 4096 Mar 24 12:07 .
drwxr-xr-x 19 lox users 4096 Feb 29 20:08 ..
-rwxr-xr-x  1 lox users 2675 Mar 24 12:07 cpuid2
-rw-r--r--  1 lox users 1464 Mar 24 12:07 cpuid2.o
-rw-r--r--  1 lox users  414 Mar 23 18:18 cpuid2.s
-rw-r--r--  1 lox users  348 Mar 23 10:46 cpuid.s
% 

OK,就这么多,至于Makefile、gas程序调试等话题,容我后续再叙。




Host by is-Programmer.com | Power by Chito 1.3.3 beta | © 2007 LinuxGem | Design by Matthew "Agent Spork" McGee