如何快速熟悉一門語言
首先請原諒我這個標題起的有點大,只是希望你能從我的經歷中得到啟發。
我看到不少沒有編程背景的人也收藏了這篇文章,我會把太學術的地方注釋一下。
編程一直是我的愛好,我最喜歡的是和圖形圖像有關的部分,但是過去幾年里我學的一直是和電子有關的內容。我最初上手編程是通過數學軟件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) 的強大威力?,F在幾乎所有的以表為單位的語言都開始支持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,或是長期為開源項目作貢獻的程序員,他們的口才和文筆都不差。
那么如何查找錯誤呢?在代碼得到錯誤結果,或是沒有正常的運行,或者沒有通過編譯的時候,有一個地方已經不符合你的預期,使它出現了錯誤。你可以從最后一個符合預期的結果開始查找,或是沿著最終的錯誤向后回溯。當然最好的就是你能為每一個關鍵的步驟都提供詳細的信息,以便你之后的修改?,F在大部分的單元測試框架都鼓勵人,甚至要求人這么做。這不是很高端很酷炫的技術,而是充分考慮到人本身思維的各種缺陷。