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

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

 

闲谈MapReduce(1): Hadoop初探

1 缘起

MapReduce是这几年IT界很热的一个名词,从某种意义上讲,MapReduce引领了当代海量数据处理的趋势和潮流。与IT界的其他名词一样,MapReduce听起来也是个很玄乎的名词,MapReduce?Map?Reduce?如果你是一名初级Coder,那么你可能从某处知道,Map的翻译是映射,Reduce的翻译是归约。MapReduce就是映射归约,是一种数据并行处理的一种编程模型。如果很不幸,你不是一名光荣的Coder,只是中国网民大军中的五万万分之一1,那么没关系,MapReduce离你并不遥远,有一个名词你一定很熟悉,那就是——云计算。关于云计算,互联网上有一个经典笑话,“中国一留学生去美国打工的当过报童,不带计算器,习惯动作抬头望天时心算找零。顾客大为惊讶,纷纷掏出计算器验证,皆无误,也抬头望天,惊恐问:“云计算?”不过云计算的真正含义,用工程师的语言,准确地定义,应当是“超大规模的,可扩展的,低成本,但是高可靠性的服务器集群系统”2

从大的层面上来讲,云计算并不仅仅是MapReduce,MapReduce只是云计算的一个技术性开端,或者说,是互联网的发展需求推动技术的发展进步,最后由Google临门一脚创造出来的一种处理海量数据的并行编程模型。真正的云计算包含诸如IDC建设、海量存储、网络规划、商业模式、网络安全等等诸多技术的难题,还有很多被一些所谓的专家炒作起来的社会意义。不过本系列文章并不打算探讨这些过于宏大的主题,我本人也没有这样的水准和资格来说三到四。本系列文章的目的只有一个,那就是搞明白,MapReduce中的Map和Reduce,到底源自何处3

我第一次听说MapReduce这个名词,是在2010年9月,那时百度来浙大宣讲,顺带开了一个技术讲座,主讲人徐串曾是Google中国编程挑战赛冠军。当然,讲座内容我是听不懂的,随心记下来的几个技术名词,至今为止,还能记住的,只剩下MapReduce了。后来面试百度运维部,期间和面试官聊到我暑期实习在华数淘宝做的一个视频转码入库系统,面试官大概觉得这个系统有点分布式的意思,因此题目结束的时候给了我一个建议,就是让我学习下MapReduce。再后来,阴差阳错,我进入了百度的分布式运维小组学习Hadoop运维,学习MapReduce就成了一个身不由己的任务。

在百度的半年,我初步接触了大规模Hadoop集群(3000个节点的规模)的运维工作。但比较讽刺的是,运维Hadoop集群的我,自身并没有写过多少真正的MapReduce程序,对Hadoop的理解也浅尝辄止。而我也始终没有搞明白,好端端的一个程序,为什么要按照MapReduce这样别扭的模型重构运行?

大概是看了Paul Graham的《黑客与画家》后,我开始正式认真学习Lisp这门语言,至今已有小半年时间,这期间我利用业余时间,和最近一个月的休假时间,基本上看完了Paul Graham的《ANSI Common Lisp》《On Lisp》《SICP》太难,我穷尽脑力至今也只窥得前面三章,做了100道习题左右,不过即便如此,也令我受益匪浅4。半年Lisp的学习让我搞明白了MapReduce中,Map和Reduce的来龙去脉。这个问题我Google了很久,但是始终没有一个满意的答案,现如今,我自己找到了满意的答案。

2 抽象漏洞(兼谈数学和计算机工程)

在正式开始我们的探险之旅前,我们还面临这一个问题,那就是,这次探险的必要性在哪里?既然MapReduce已然为我们提供了成熟的理论编程模型,Hadoop生态圈也给我们提供了一整套成熟的工程实现,我们为什么还要纠缠这些学究般的历史问题?我的解答是, 一个学科的历史往往就是这个学科的本身 。无论是一门编程语言、编程范式、编程模型,或者IT业的任何一项新技术新名词,在学习的过程中,一定要搞明白:

  • 它解决了什么样的技术问题?当时的历史背景是什么?还有没有类似的(可能没有流行起来的)解决方案?
  • 它的引入带来了哪些新的问题?
  • 它的优点是什么?什么样的问题一定会手到禽来?
  • 它的不足是什么?什么样的问题是它解决不了的?

这就好比解一道很难数学题,如果光告诉你一个正确的数字,那么这个数字对你来说一点意义都没有;如果进一步,告诉你标准的解题过程,那么你可能会对这个题目有一个粗浅的认识;如果有那么一个白痴,不但告诉你正确的答案和解题过程,还把他的演算纸送给了你,并热切地给你讲解他在解题过程中碰到了哪些问题和陷阱,是怎样解决怎样解决问题怎样规规避这些陷阱的,那么你对这道题目的理解则会大大加深,日后再碰到同样类型的问题就会轻车熟路,手到拈来。 从某种意义上而言,一个完整的解题的探寻过程才是一个题目所具有的全部信息价值,扔掉这些而仅仅记住一个解题结果或者一个标准的解题证明,无疑是买椟还珠 5 。但是比较可悲的是,计算机是一个年轻的学科,所以专门讲述计算机历史的书籍并不像数学史书籍一般汗牛充栋。

从另一个角度上讲,计算机和数学的都是一座不断分层的抽象大厦。数学是从基本的整数,到数系的扩充,到微积分的诞生,到非欧几何等等宏大的诗篇;而计算机则是从最基本的与非门,到bit、byte、word,到汇编、c、高级语言,到设计模式、分布式等等现代工业的大厦和基础。“计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决6”,不过很不幸, 计算机的抽象和数学的抽象有一个本质的不同,那就是计算机的抽象是有漏洞的 ,这就是抽象漏洞法则:All non-trivial abstractions, to some degree, are leaky。在数学领域,我们从来不用担心说某个定理“因为空调故障的原因宕机了,或者因为内存吃紧运行特别缓慢,似乎是发生内存泄漏了”,等等诸如此类的各种各样令人头疼的问题。这条法则深刻的影响了计算机软件和数学定理的生产过程——在计算机中,我们不可能找到一个能困惑世间智者358年的谜7;在数学领域,我们也不可能找到一个耗费人类5000万人年人力的定理8如果说,基于抽象的数学,考验的是人类大脑思考深度的极限,那么同样是基于抽象,并且脱胎于数学的计算机工程,挑战的就是人类大脑思考广度的极限 ——软件工程中瓶瓶罐罐的细节太多了,所以大凡软件开发,走的都是兵团作战的策略;而数学就不一样,“10个产妇无法在一个月内生出孩子9”,数学领域中辉煌定理的产出,往往依靠少数几个,甚至单个天才数学家的苦苦思索和灵感涌现。

3 Let's Go

以上,废话了这么多,所要强调的无非就是,了解MapReduce的来龙去脉,对于写出更好的MapReduce程序,甚至将来拨开层层抽象,解决MapReduce的底层问题,乃至改善MapReduce,都是大有裨益的。不过,在正式踏上我们的探险旅程之前,我们需要检查一下手头的“装备”是否合格,很简单:

  • 你需要一台*nix系统,最好是GNU Linux,苹果也行,如果实在不方便,Windows下装个Cygwin也还能凑合;
  • 你需要有一定的编程经验,包括但不仅限于C、Java、Bash、Python,如果再对Lisp或者Scheme有一定了解就更好了(别急,本系列文章对Lisp做一个简要的介绍);
  • 你需要了解一些常见的*nix软件开发工具,包括但不限于Vim的使用、Ant和Make构建工具、Git和SVN版本控制软件;
  • 你需要对POSIX系统标准有一定了解,包括但不限于*nix的文件系统结构、用户属组、文件权限、管道等等。

Now, Let's Go!

4 Hadoop

行文至此,相信众位读者已经知晓了云计算的一些基础概念,最起码知道了所谓Google技术的三驾马车是什么,如果能看过Hadoop代码中WordCount的例子并能看懂的话,那你简直是太天才了。为了保证我们的探险顺利进行,我们需要一套开源的MapReduce平台实现来验证我们的学习成果,Hadoop是不二选择。关于Hadoop本身有太多太多的资料,因此我在这里就不再劳心劳力的copy别人的劳动成果了。推荐以下三本书,作为Hadoop的入门:

我们所要做的,就是在本机的*nix系统下,搭建一个demo的伪分布式运行的Hadoop平台。我采用的Hadoop版本是Hadoop 0.20,这个版本比较稳定,最新的Hadoop 1.0添加了很多新的特性,这些特性对于我们的探险并没有特别的作用,而且我也不甚了解。当然,本文的重点并不是Hadoop,所以我并不会带你去分析HDFS的源代码,告诉你如何打Patch(我也不会,嘿嘿)。本文的重点在于MapReduce的来龙去脉。

  • 首先本机*nix上存在jdk和ssh,并找到相应的$JAVA_HOME
  • 首先是建立本机用户到自身的ssh信任关系,步骤大致如下:
➜  ~  ssh-keygen 
Generating public/private rsa key pair.
Enter file in which to save the key (/home/lox/.ssh/id_rsa): 
/home/lox/.ssh/id_rsa already exists.
Overwrite (y/n)? y
Enter passphrase (empty for no passphrase): 
Enter same passphrase again: 
Your identification has been saved in /home/lox/.ssh/id_rsa.
Your public key has been saved in /home/lox/.ssh/id_rsa.pub.
The key fingerprint is:
19:3f:55:84:99:d2:1e:c6:42:d0:39:6f:3e:83:84:21 lox@lox-pad
The key's randomart image is:
+--[ RSA 2048]----+
|        .+.+ =o  |
|      E . * O.   |
|       ..o B..   |
|        .+..+    |
|        S.o+     |
|          ..+    |
|             o   |
+-----------------+
➜  ~  cp .ssh/id_rsa.pub .ssh/authorized_keys 
➜  ~  chmod 700 .ssh 
➜  ~  chmod 600 .ssh/authorized_keys 
➜  ~  ssh lox@localhost
  • 下载hadoop v0.20,解压缩到一个目录,我的目录结构如下,其中tmp/hadoop-data作为hdfs数据存放目录(包括伪分布式运行的namenode和datanode的数据),tmp/hadoop-v20作为$HADOOP_HOME
➜  ~  tree -L 1 tmp/hadoop-data tmp/hadoop-v20 
tmp/hadoop-data
tmp/hadoop-v20
├── bin
├── build.xml
├── CHANGES.txt
├── conf
├── conf.origin
├── conf.pseudo
├── conf.standalone
├── contrib
├── docs
├── hadoop-0.20.3-dev-ant.jar
├── hadoop-0.20.3-dev-core.jar
├── hadoop-0.20.3-dev-examples.jar
├── hadoop-0.20.3-dev-streaming.jar
├── hadoop-0.20.3-dev-test.jar
├── hadoop-0.20.3-dev-tools.jar
├── ivy
├── ivy.xml
├── lib
├── LICENSE.txt
├── logs
├── NOTICE.txt
├── README.txt
├── src
└── webapps
  • 修改hadoop的配置文件分别如下:
    • hadoop-env.sh,重点修改下$JAVA_HOME,指向SUN JDK或者OpenJDK的目录,Hadoop官方建议采用SUN(现在是Oracle啦)的JDK。
    • core-site.xml
    • hdfs-site.xml
    • mapred-site.xml
hadoop-env.sh

...
...

# The java implementation to use.  Required.
# export JAVA_HOME=/opt/java
export JAVA_HOME=/usr/lib/jvm/java-7-openjdk

...
...
core-site.xml

<?xml version="1.0"?>
<?xml-stylesheet type="text/xsl" href="configuration.xsl"?>

<!-- Put site-specific property overrides in this file. -->

<configuration>
    <property>
        <name>fs.default.name</name>
        <value>hdfs://localhost:9000</value>
    </property>
    <property>
        <name>fs.trash.interval</name>
        <value>1440</value>
    </property>
    <property>
        <name>hadoop.tmp.dir</name>
        <value>/home/lox/tmp/hadoop-data/tmp</value>
    </property>
</configuratione>
hdfs-site.xml

<?xml version="1.0"?>
<?xml-stylesheet type="text/xsl" href="configuration.xsl"?>

<!-- Put site-specific property overrides in this file. -->

<configuration>
    <property>
        <name>dfs.replication</name>
        <value>1</value>
    </property>
    <property>
        <name>dfs.name.dir</name>
        <value>/home/lox/tmp/hadoop-data/name</value>
        <final>true</final>
    </property>
    <property>
        <name>dfs.data.dir</name>
        <value>/home/lox/tmp/hadoop-data/data</value>
        <final>true</final>
    </property>
</configuration>

mapred-site.xml

<?xml version="1.0"?>
<?xml-stylesheet type="text/xsl" href="configuration.xsl"?>

<!-- Put site-specific property overrides in this file. -->

<configuration>
    <property>
        <name>mapred.job.tracker</name>
        <value>localhost:9001</value>
    </property>
    <property>
        <name>mapred.tasktracker.map.tasks.maximum</name>
        <value>5</value>
    </property>
    <property>
        <name>mapred.tasktracker.reduce.tasks.maximum</name>
        <value>5</value>
    </property>
    <property>
        <name>mapred.child.java.opts</name>
        <value>-Xmx512m</value>
    </property>
</configuration>
  • 启动hadoop,如果能用Hadoop FS Shell做一些常规的mkdir和ls操作,Hadoop搭建就算大功告成了:
➜  ~  hadoop namenode -format 
12/02/15 00:07:23 INFO namenode.NameNode: STARTUP_MSG: 
/************************************************************
STARTUP_MSG: Starting NameNode
STARTUP_MSG:   host = lox-pad/127.0.0.1
STARTUP_MSG:   args = [-format]
STARTUP_MSG:   version = 0.20.3-dev
STARTUP_MSG:   build = http://svn.apache.org/repos/asf/hadoop/common/tags/release-0.20.2 -r 916569; compiled by 'lox' on Wed Nov  9 23:40:01 CST 2011
************************************************************/
Re-format filesystem in /home/lox/tmp/hadoop-data/name ? (Y or N) y
Format aborted in /home/lox/tmp/hadoop-data/name
12/02/15 00:07:25 INFO namenode.NameNode: SHUTDOWN_MSG: 
/************************************************************
SHUTDOWN_MSG: Shutting down NameNode at lox-pad/127.0.0.1
************************************************************/
➜  ~  start-all.sh 
starting namenode, logging to /home/lox/tmp/hadoop-v20/bin/../logs/hadoop-lox-namenode-lox-pad.out
localhost: starting datanode, logging to /home/lox/tmp/hadoop-v20/bin/../logs/hadoop-lox-datanode-lox-pad.out
localhost: starting secondarynamenode, logging to /home/lox/tmp/hadoop-v20/bin/../logs/hadoop-lox-secondarynamenode-lox-pad.out
starting jobtracker, logging to /home/lox/tmp/hadoop-v20/bin/../logs/hadoop-lox-jobtracker-lox-pad.out
localhost: starting tasktracker, logging to /home/lox/tmp/hadoop-v20/bin/../logs/hadoop-lox-tasktracker-lox-pad.out
➜  ~  jps   
21061 JobTracker
20852 DataNode
21255 Jps
20977 SecondaryNameNode
20764 NameNode
21156 TaskTracker
➜  ~  hadoop fs -mkdir /tmp/this-is-a-test-dir
➜  ~  hadoop fs -ls /tmp
Found 1 items
drwxr-xr-x   - lox supergroup          0 2012-02-15 00:08 /tmp/this-is-a-test-dir
➜  ~  

好了。基础工作已经准备好,在接下来的旅程中,我会初步讲解一下Hadoop的基本概念和使用方法,进而转入Lisp(Scheme)函数式编程的美妙世界,带你逐本溯源,领略一下原生态的Map和Reduce到底是什么模样,并且会顺带谈到一些我在Lisp学习过程中领略到的别样风景,包括但不限于Java的反射、序列化等一些高级特性,XML、JSON的数据语言的特性特点等等。敬请期待!

--

Footnotes:

1 第29次中国互联网络发展状况统计报告显示,2012年初,中国网民共计5.13亿。

2 关于这个定义的出处可以参考弯曲评论上一篇非常好的关于云计算的科普文章“云里雾里云计算”,本文不打算探讨云计算的社会意义、产业变革、安全等过于宏大的主题(其实我对这些一点都不了解)。

3 MapReduce的第一篇论文"MapReduce: Simplified Data Processing on Large Clusters"曾写到:"Our abstraction is inspired by the map and reduce primitives present in Lisp and many other functional languages."可见,MapReduce的思想来自于古老的Lisp语言。

4 广告一下,在我有限的阅读经历中,《SICP》是我读过的计算机书籍中最棒的一本,没有之一。如果能认真做完这本书里面的356道题目,绝对会让你对编程本质的理解有一个脱胎换骨般的提高。这里有我个人的部分习题解答代码和学习笔记。

5 关于这一点,刘未鹏的《知其所以然》系列文章里有更好的解读,我就不再重复了。

6 参考程序员的自我修养

7 参考费马大定理 : 一个困惑了世间智者358年的谜

8《Unix编程艺术》的序言里的脚注:“从1969年到2003年,35年世间并不短。以这期间众多Unix站点数量的历史曲线来估算,人们在Unix系统的开发方面投入了约5000万人年”。

9 这原本是Brooks's Law的一种观点。

 

永不停歇

2009年的最后一天的下午,和妞去外婆家吃了顿大餐,特别记得那里的青豆泥和黄金鸡翅,还有3块钱的麻辣豆腐。之后元旦的那几天陪妞泡图书馆,妞准备考研,我准备学期末的十几门考试。10号左右考研结束,我回到玉泉,硬着头皮编数值分析的8道题目。月中,漫长考试正式拉开序幕。月末28号早7点,起床看《计算理论基础》,10:30-12:30战斗在计算理论的战场上。然后接下来吃饭填饱肚子,回到寝室收拾行李。两点多出发,四点左右到机场,五点多飞机起飞,八点多到福州,摩的到车站,晚九点多坐上去厦门的大巴,午夜到厦门,饿得发昏,吃了一碗热汤面,夜里一点左右,找到旅馆就寝。

厦门几日参观了厦大,南普陀,在鼓浪屿上闲庭信步,听了一场很感动的吉他演奏。最后一晚特地到鼓浪屿上的小旅馆住了一宿。次日早起赶往车站,9点大巴到福州。半路大巴抛锚,磨蹭下午三点多才到福州。我和妞匆匆感到火车站。两分钟内交换整理了各自行李,妞独自踏上回家的动车——妞的手机坏了,我的手机充电器也丢了。半小时后我也上了回家的车,福州到北京,24小时的硬座。

回家几日,家里闹了点小矛盾,心情十分不好,才初九就提早跑了出来。和妞汇合,正是南方赏梅佳节。杭州植物园、余杭超山,留下了我们匆匆的脚步。而后心情还是不太舒畅,没怎么去上课,白天就在寝室睡大觉,晚上起来折腾Gentoo。实在心情不好就去夜爬宝石山,山顶的清灯古寺,躺在石椅上看看星星也好——虽然杭州的天空本就没有几颗星星。磨磨蹭蹭到3月底,被导师抓到实验室,开始了一个月的苦力生涯。现在想想那一个月的生活,还是挺好玩的。整个组就像个生产软件的小作坊——没有版本控制、没有QA和bug控制、需求总是不断在变、没有统一的代码风格、参差不齐的编码水平,致使项目一再延期。4月初妞去深圳实习,也不知道要多久。我一个人每天早晨从玉泉的最北边走到最南边,午夜在借着月色星光,听着自己的脚步声从最南边走回最北边。含笑开得正盛,晚上出实验室的时候就随手摘一多,清香醒脑,然后装在口袋里。满身的“香蕉味”。含笑,多么示意的一个名字啊。还有云南黄馨,一片片的黄,招蜂引蝶,很温暖。

这一个月仍然是没有去上过几节课。月末的编译原理无奈的放弃掉了,人工智能水了一篇科普文、仗着开卷考试混了过去。月底来了两个初中的朋友,twt和ww。我做东,吃吃喝喝,逛了几个我再熟悉不过的地方——西湖、紫金港等。4月30日下午3点分批赶往火车站,无奈交通大堵塞,退了火车票,大巴赶往上海,与南京来的朋友fx汇合。晚8点左右,南京路,四个初中的铁哥们,拥抱之后,非常不理智的加入了南京路上赶往黄浦江边去看世博会开幕烟火的汹涌人流——事实证明这绝对是个错误的决定。烟火没看到,倒见识到了恐怕这辈子再也不会见到的人口密度,真正的比肩接踵——竟然还有车子在人流里面慢慢的蹭,你说你这司机不是找抽嘛。

两日世博匆匆茫茫。想比较10月100万/天的人流规模,5.1三日20万/天的人流对我们来说已经算是很仁慈了。可是逛了一天我还是感到厌倦。第二天下午我就下定决心以后再也不凑这种热闹了。幸好2日晚上妞来上海,我就找了个理由告别了哥们,提前结束了原本预计三天的世博之旅。3号回到杭州。4号开始着手数模比赛。14号晚上霸占了实验室的小会议室,我主刀文档,ly和lyf负责数据和资料支持,三人通力合作,早9点拿出了最终的论文。结果也没有让我们失望,校赛二等奖,拿到了国赛的资格——虽然国赛铩羽而归。数模完了就开始很功利地刷ZOJ,开始还是很认真的在做,有一周周一早晨进入实验室一直待到周四中午才会,就在不停的刷ZOJ。后来时间上还是来不及,加上题目难度增大,就开始耍一些小花招,比如有些题目回去看别人的代码,然后看懂后自己照着写写就提交等等。最后在月底总算刷够了题目,好歹能去参加选拔赛见识一下了——虽然我承认这样的急功近利很不光彩。我们对人对事常常会有双重标准。比如我们痛恨关系人情,轮到自己的时候却拼了命的去找关系、攀人情;我们痛恨钱的万能,但是却希望自己能有更多的钱。对人对己,一视同仁,这个真的好难做到。

6月初就在补各种课写各种实验报告了。妞在家帮妹妹准备中考、学车、做毕业设计。妞的毕业设计做的也非常匆忙,答辩前还在画图,半夜开始做答辩PPT,早七点半哭哭啼啼的弄醒我,说PPT软件崩溃,夜里做的都没了。上午要第一个答辩。我就安慰着说别慌别慌,跟老师申请下调整下顺序。总算过了这一关。我自己花了半周左右的时间写完了初步的简历。13日我生日,腐败的抽出了半天的时间在九溪逛了逛,中午就感到zjg参加ACM新手上路第二场选拔,还是铩羽而归。15号左右开始找实习。20号左右ACM新手上路第三场选拔,错过旅行者年度散伙饭。选拔赛比完,心里明白,自己所谓的ACM梦想彻底告吹,专心找实习。25号拿下华数淘宝的offer。然后就是漫长的考试。煎熬的日子。恰好又赶上妞的外婆过世,虽然我非常希望妞在身边,但这也是无奈的事情。只有那实验室前的合欢,花如雪,飘落一地。一个人,实验室,软件工程、算法设计、J2EE、嵌入式系统、计算机体系结构实验、计算机组成实验,一门又一门的考试,疲于奔命的我。那个时候真的感觉自己再也不想上学不想考试了。每天心里就想着一个字——“熬”。考试之前完全看不下去,就在youku上看鲁豫有约了。现在想想那段日子实在太痛苦了。

7月12日,妞去上海正式上班,我也需要入职实习,好巧。早六点送妞去火车站,9点公交上睡到淘宝总部,被告知华数淘宝已经搬家。又是一路奔波,总算找对了地方。整个暑假还是一个人。一个人,九点上班,磨磨蹭蹭地八点四十五起床,三分钟洗脸刷牙,五十准点从寝室出发,五十五到古荡,九点坐上公交,四站,十分下车,十二到公司,十五开早会。午十二点聚餐、一点小憩、三点去领餐票、六点吃饭、磨磨蹭蹭六点半出门回玉泉,掏出古董的Nokia 6670,拍下傍晚的天空晚霞,或者路上赌成一排的车,彩信给妞——“妞,我回去啦”。八点到寝,开电脑,调好小音箱,音乐飘荡,书香四溢,一个人静静的享受。十点会给妞打个电话,午夜问声好梦晚安。说不上特别快乐,但是十分享受这样的日子。

9月开学,第一周周末数模国赛,弃赛,铩羽而归,蹭了三天的伙食。15日以后,一年一度的校园招聘正式拉开序幕。请了两次小假,都是中午到三点这样的时段,参加了一次百度的小型交流会,参加了一次有道选拔机试。中秋前几日也是事情不断。跟Ouka说好了要一起去楠溪江徒步三天,怎奈走之前三天胃痛非常,下午在校医院挂了盐水后还是很痛,晚上就直接打车到了省立同德继续挂了盐水,总算好了点。胃痉挛还是很痛苦的,胃镜也是,想想就发抖。Ouka担心还能否去徒步领队,我逞强说肯定没问题。挂完盐水后十一点回到yq,去参观生仪学院的某个实验室,谈到夜里一点,被硬发了一个直博的Offer……老妈在家乡听说我挂了盐水,掉了眼泪,给妞打了电话;妞在上海也怕的要死,兴夜匆匆赶来,午夜到了玉泉毛主席像,一个人等我到夜里一点。好在同德的药很管用。我见到妞的时候已经基本不痛了,反倒比折腾了一程的妞还生龙活虎,妞就说“怎么感觉是你应该照顾我而不是我赶来照顾你呢”。我就说“你难得照顾我一次嘛”。

次日早起回到寝室,千里连音,帮在北京的zhzf同志写简历,三个小时通力合作到中午,总算完成,然后哦zhzf就直接打印简历去面试了。过了几天告诉我被录取了,说是简历起了大作用了。下午去zjg借了一些睡袋和炉头,晚饭后打算打车到火车站,赶往上海。怎奈月到中秋,人们都忙着过节,交通堵塞,打的困难,等了将近一个小时竟然还是打不到车,报废了一张火车票。无奈只能大巴去上海。午夜到上海南站,打车回到住处已经夜里一点多了。次日早起背着两个大包赶往上海南站,我悲剧的没有带公交卡,排队买地铁票花了好长时间,离火车出发一分钟的时候我和妞赶到了检票口。已经迟了。于是Ouka和yy先行一步。我和妞改签至下一斑。下午三点四人终于在温州汇合。紧接着就赶往永兴。三日旅程诸多不顺。天气下雨,徒步溯溪水位暴涨,危险性大大增高;阴天,原本期望的银河也就此泡汤;第三天返程,乡野司机悲剧的开到了温州老站而非动车站,又一次导致火车晚点,四张票改签退票,损失200大洋;去长途站做大巴,只有去上海的大巴而没有去杭州的……于是我一个人被抛弃,仗着身上仅剩下的100元钱买了张到杭州的硬座票。妞比较顺利,晚上九点多就回到住处了;Ouka和yy身上没钱了,错过了若干班车,夜里四点才安全的回到寝室;我呢,铺上防潮垫在火车站大厅地上极其不雅观地睡了三个小时。然后硬座8个小时到杭州,下了火车就直接去上班去了……

9月30日晚上下班收拾东西赶上了回家的火车,一夜拥挤无座,到下午两点下车,到承德,到家,夜里11点了。待几日,9日返上海看妞,10日返杭,13-15日接待zyy和lyh,然后就是各种血淋淋的笔试面试。24日一夜无座到北京,26日百度开放日活动,然后上海->杭州->考试->上海->杭州->北京->承德->家->北京->百度。

一路匆忙,永不停歇。1月中旬的时候我就想:还有两周,挺过去就好了,挺过去咱就出去玩一圈;4月的时候心里想:再挺6个月,什么都能搞定了;8月的时候想:这个时候即使什么都不干也有一个很好的保底Offer了;9月数模三天想:算了就算了吧,时间来不及了;中秋徒步最担心的是:能不能安全准时地赶回去;10月的某日接到百度的面试通知:离下午面试还有七个小时,出去吃饭发呆上厕所还有五个小时,赶紧看书!11月回到杭州:老师,这数电你就给点面子让我过了吧;12月:坏了,今天忘记写日报了,明天得九点之前起床,赶到公司,补上周报………………

总是那么忙;总是闲不下来;总是不懂得拒绝;总是给把自己和别人的期望值抬的特别高。

可不可以睡一天的懒觉晒一天的太阳;可不可以看一天的电影而心里没有浪费时间的愧疚感和负罪感;可不可以逃离各种email、SNS、IM一个人静静地过过有期待有等待的日子;可不可以不要对自己要求那么高,做一个观众,观望看台下激烈的百米赛跑?

我们还是没有勇气,我们停不下来。我们一路匆忙、永不停歇。在这无尽的匆忙中追逐着自己的欲望,却离自己的理想一点点远去。

真的是这样吗?朝九晚五、绩效业绩、技术职称,这些就是我们人生的全部意义?为什么有时候我会很邪恶的以为人生有80%的意义产生在20%的假期里面呢?

说说接下来的计划吧。

  • 学习:
    • hadoop相关,重点看完O'Reilly的《Hadoop: The Definitive Guide : MapReduce for the Cloud》,写读书笔记。
    • python,画一个学习脑图,总结下学习成果,重点研究下lxml、beautifulsoup、urllib模块。
    • c#,完成一张脑图,总结下c#和java的不同,批判的学习,完成大程序作业。
    • 软件架构,看看设计模式吧,过了考试。
    • 运维相关,不断学习不断总结。
    • CMS系统,调查下各种CMS系统,自己动手搭建一个,赶快给出一个小网站设计方案,拖了好久好久,太不应该了。
  • 计划中:
    • Qt,看完《C++ Programming with Qt 4》,计划好久的事情了。
    • Lisp,看完《ANSI Common Lisp》,计划好久的事情了。
    • 网络,看完《TCP/IP详解》一、三卷,计划好久的事情了。
    • 数据库,看完《数据库系统实现》,自己动手实现一个miniSQL,计划好久的事情了。
    • 摄影,看完《纽摄》,拍够十个卷,计划好久的事情了。
    • 文艺,先搞定《温柔的夜》,再去攻坚《三毛全集》,计划好久的事情了。
    • ……,计划好久的事情了。
  • 毕业:
    • 十门课
    • 毕业设计
    • 第二课堂
    • 散伙饭
    • 领到两证
    • 看看能否再抽出三周出去骑两千公里。不过貌似很难有这个时间了。悲剧。




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