首先请原谅我这个标题起的有点大,只是希望你能从我的经历中得到启发。
我看到不少没有编程背景的人也收藏了这篇文章,我会把太学术的地方注释一下。
编程一直是我的爱好,我最喜欢的是和图形图像有关的部分,但是过去几年里我学的一直是和电子有关的内容。我最初上手编程是通过数学软件Mathematica,它本身也是一个强大的函数式编程语言,于是顺手学习了Haskell/Scheme/Prolog等FP语言。后来修了一门分布式系统的课程,为了设计分布式系统我学习了Erlang,因为也是函数式编程,且语法很多借鉴了Prolog,所以使用了很长时间。
FP指functional programming,是以函数作为基础的语言,与之相对应的是imperative language,指令式语言。指令式的语言的程序逐条执行指令,每个指令都会影响到某些存储单元的数据,或者是设备。函数式语言则相对抽象,用操作(函数)本身代替了被操作的对象,且函数本身还能用别的函数来描述,是一个很自洽的体系。Haskell和Scheme都是经典的FP语言。
在本科后半和研究生期间,有三年时间在研究FPGA,所以一直在使用Verilog和VHDL。它们是描述电路逻辑的语言,也就是说这些代码最终会对应到一个芯片的电路。但是与Erlang有很多非常相似的地方,很可能是因为它们都在描述并发系统。所以那个时候我就有了一个概念,语言是受限于所描述的系统的。
FPGA 叫做Field Programmable Gate Array (现场可编程门阵列)。我们都知道集成电路的信号都是0和1,也就是高低点位,而这0和1又能控制其他的电路通断,这就是数字电路的基本原理。我们知道内存里面存的也都是0和1,于是就有很天才的人想到了能不能用一个内存条作为开关,来控制一大堆电路的通断呢?这个东西后来实现了,一个叫Ross Freeman的人最终实现了商用的FPGA,并且创建了Xilinx公司,建立了一个大产业。(然而Ross Freeman在实现了FPGA没多久后就因癌症去世了,没有看到他发起的大产业,这是题外话。)
简单来说,一个FPGA就相当于一个结构可以不断变化的电路,很像人的神经网络。目前FPGA的使用途径主要集中在金融领域的高频交易,但是Google已经构建起一个FPGA farm,进行深度学习,分析Google的数据中心中存储的所有数据(还有Youtube的视频),在不同的数据之间寻找隐含的关联。如果有一天Google搜索能完全理解自然语言,那一定是FPGA的功劳。
研究生后半和博士的前两年,我在做无线传感器网络,无线传感器节点使用了一个神奇的叫做Contiki的操作系统。这个系统的特点是用了protothread,就是介于process和thread之间的概念,scheduler切换进程时只会记住当前的指令指针,而不会保存context,于是所有的内容必须得存储在全局变量中。这么做的原因是protothread实现很简单,且无线传感器用的处理器内存只有2K。
无线传感器网络基本就是我们常说的物联网,每个节点的主要构成是一个小的无线收发芯片和一个小处理器。由于功耗限制,这些芯片的性能都不能太高,所以能够在这些芯片上设计一个操作系统(还是多任务的)实在是心灵手巧的人才做得到。(当然,现在随便拿出来一个处理器,速度都比当年开发UNIX的PDP-11快出好多)
博士的最后一年在做软件无线电,就是通过USRP+GNURadio来实现OFDM通信,并且找到当前WiFi(802.11a/g/n)中可以改进的地方。但是回国之后就不再做硬件了,还是回到了最钟爱的图形图像。但是图形和图像本身也是两个极为不同的领域,一般来说图形(计算机图形学)指的是如何显示图像,而图像(计算机视觉)指的是如何识别和处理图像,二者只有在很小的领域拥有交集。目前做的两个创业项目分别和WebGL及二维码识别有关。
在过去几年中做了这么多项目,跨度都非常大。由衷感觉到知识是无穷无尽的,懂得多不如学的快,而最关键的是你能很快地找到切入点,并且迅速建立自信。
0. 为什么要快速熟悉一门语言/框架
你已经见过网上有很多人在为哪种语言或那种框架而争论,在这之前你也听说过中国人要不要学英语这种问题。语言自始至终都有两种功能,第一种是从实用主义出发的功能,就是与人交际,你每掌握一门新的语言,就能理解使用这门语言的人或者物件,并进一步掌控。如果你能讲VHDL,你就能控制FPGA,如果你会讲CUDA或OpenCL,你就能控制GPU,如果你会讲kext脚本,你就能控制苹果各种产品的设备,如果你能自如地使用英文搜索,你积累关于计算机方面的知识的速度会比你同龄同资历的程序员快很多。
第二种功能是从情感出发的功能,就是使用同一种语言的人在寻求彼此认同。当你在外漂泊多年遇到老乡时的感觉,或是突然回到家乡发现大家讲话你全能听得懂,甚至包括一些微妙的用其他语言难以表达的事物或情境的时候,那感觉会很不一样。程序员容易在自己使用的语言当中找到归属感,并且通过自己熟悉的语言来建立自信。这样会使人骄傲,并且阻断继续学习的道路。
语言本身没有任何好坏,一种新的语言的诞生总是和它使用的环境有关。如果和使用环境无关的语言,即使本身的设计再优雅也不会流行,如Haskell及Lisp一干dialects,反之亦然,如JavaScript。你要学一门语言或框架,是为了做一些东西,是为了和一同使用这门语言的人共事,所以任何时候都不要评价语言或者框架。毕竟这一切都是人创造的,而人总是有局限的。最终要达到的目的是为了对机器的掌控,与人的交流,而不是自我陶醉。
1. 搭建调试平台
在看任何书之前,先去按文档把自己机器上的调试环境搭好。你再懂得这门语言的历史,再懂的这门语言的使用场景,甚至把语言的关键字API全都记得滚瓜烂熟也没用,这就和你认为背单词对学好英语有贡献一样可笑。成功,或者成熟掌握一项技能很少有能够量化的指标,如果硬要找一个指标来衡量你离成功或熟练有多近,那最接近的是你在实践中失败过多少次。
2. 研究最基本的数据结构
几乎在所有语言中最基本的数据结构是形如Array/List一类带有标记的顺序结构,我们姑且称之为『表』。树/图/Dictionary/矩阵都可看作表的延伸。在数据库中有大量对字典的增删改查(CRUD)操作,允许你往表里堆不同的东西,结构也可以非常多样化。然而在图形和图像中,则含有大量矩阵操作,操作的对象是非常整齐的数据。
在写代码的时候有一个通过实践总结出的原则,很少出现在教科书内,就是代码块之间(代码块是我临时想到的词,因为在不同paradigm里语言的单位是不同的,在Java中是类,在Python中是对象,在JS中是prototype,在Erlang中是process等等)传递的信息应当是尽可能简单和通用的数据结构,而最好不是很复杂的对象。就是说一个代码块收到了这样的信息,经过一些操作变成了结构复杂,容量也很大的信息。那么这个代码块应当把这个信息简化到尽可能接近简单通用的数据结构,才算作是完成了一件事。如果这个代码块返回了一坨巨大的数据,而另一个代码块接收的也是一坨巨大的数据,那么这个代码块的功能划分是有问题的。
这是为什么UNIX选择以文本文件作为基础的信息组织单位,并且所有的文件都允许以文本文件的方式来处理,并且明确地提出一个程序应该能够输出文本,所以你能够看到UNIX内部的程序都会输出stderr,使你通过dmesg就可以查看它们的足迹。由于以文本文件为基础,UNIX对正则表达式异常重视(grep, sed, awk, perl, ...)也就不足为奇了。这些个小部件统称coreutils,掌握这些小部件才算能理解*x系 (当然包括OS X) 的强大威力。现在几乎所有的以表为单位的语言都开始支持map/reduce/filter等操作,并很自然地引入lambda function,也正是因为它们极大地拓展了表结构的表达能力。
在JavaScript中,Array是基础的数据结构,但是和JSON兼容的Object也是,Array和Object可以无差别替换,但是在JS引擎中,Array和Object的表达是完全不同的,通常Array是被优化过的。如果Array的index不是从0开始连续递增的整数(比如中间有一个元素是undefined),那么它存储时就会被退化为Object,查找也要以Object匹配key的方式进行,这样就会慢很多。
在OpenCV中,由于大部分的操作都和图像有关,所以cv::Mat就成了基础的数据结构,矩阵的各种操作,包括转置,求逆,合并与拆分,SVD等等。OpenCV对这个基础数据结构进行了很多优化,使得运行时的I/O开销最小。譬如现在有对Intel处理器的TBB支持,TBB将程序的并发性显式地表达了出来,当然这部分机制又被OpenCV封装了起来,比如进行RANSAC操作时(要在一堆点中拟合出一条直线或者椭圆)需要在一个很大的点集中寻找,不断进行SVD解方程求模型的系数,有了TBB,就可以使SVD操作并发执行,大大提高性能。所以你首先要做的便是去熟悉各种与Mat有关的操作。
GNURadio是一个软件无线电框架,它需要一个无线收发机进行直接上下变频,CPU通过程序对代码进行编码和解码操作,这些事以前都是由硬件电路完成的,现在全都变成CPU指令,对于延迟就变得非常敏感。在GNURadio中有一套类似TBB的机制,将操作变为不同的模块,譬如FFT和IFFT,功率谱密度等等,而在这些模块之间的接口则不光定义了数据类型,还包括最大和最小允许写入的数据速率。
不同领域的语言相差很大,语言背后的核心概念相差可能更大,在3D图形领域以矩阵操作为主,在某些语境下矩阵不是个很方便的表达方法,于是有了欧拉角和四元数(Quaternion),这些notation更简明,但是也更不易理解。在2D图像领域有些很神奇的算法,譬如Hough找直线找椭圆,RANSAC拟合平面,SIFT/FAST来找图像的特征,在无线通信领域这一切又不一样。但是无论哪个领域,都总是有一套最基础的和领域之外的人交流的途径,这就是基础的数据结构。
3. 调试
之前提到从基础的数据结构开始,是因为对基础数据结构的操作结果最容易预料到。当代码开始变得复杂时,编译器或者运行环境就有可能无法很明确地告诉你错在哪里,有的时候是不报错,有的时候没有报对错(这个更误导)。
编译器或运行环境的反馈是最佳的理解计算机的信息,但是因为种种原因这信息可能不够全面。如果仅仅依靠这些信息会浪费你大量的时间,因此你需要设法让你的代码向你提供更多的信息。在写C代码的时代,我会用大量的printf来帮助我,主要有以下的功能:
- 打印运行结果
- 打印if-else的控制流语句中代码流到了哪个岔路口
- 打印循环是在什么条件下终止的
之所以出现bug原因无非有三种:
- 你对你要做的事情不够了解
- 你对你使用的语言的语法特性不够了解
- 你对你使用的函数/库/框架不够了解
所以出现了bug之后,你首先要确定你的bug大概来自上述问题中的哪一种,当然很有可能的就是这几种都包含。在这种情况下错误会十分难查找。我们先考虑第二种,对于语法特性如果还不够了解,请回到基础的数据结构那一关,做各种各样简单的示例,在那一关解决掉所有的语法问题。如果是第三种,那么你需要认真阅读文档和源码。
如果你对你做的事情不够了解,这涉及到一个很微妙很有趣的事情。我来告诉你,人类编程的能力最终是由他驾驭母语的能力决定的。如果一个人无法和另一个人讲清楚一件事情要如何做,那么他自然无法向计算机描述清楚一件事情。所以走上编程道路的理科生,数理思维再突出,当年拉下的语文课也会成为日后的短板。相反,如果你观察美国各种技术发布会的presentation,或是长期为开源项目作贡献的程序员,他们的口才和文笔都不差。
那么如何查找错误呢?在代码得到错误结果,或是没有正常的运行,或者没有通过编译的时候,有一个地方已经不符合你的预期,使它出现了错误。你可以从最后一个符合预期的结果开始查找,或是沿着最终的错误向后回溯。当然最好的就是你能为每一个关键的步骤都提供详细的信息,以便你之后的修改。现在大部分的单元测试框架都鼓励人,甚至要求人这么做。这不是很高端很酷炫的技术,而是充分考虑到人本身思维的各种缺陷。