Javascript的鋼鐵戰衣:CoffeeScript

海興發表於2013-02-27

東風不與周郎便,銅雀春深鎖二喬。

我猜,所有程式設計師都寫過Javascript程式碼,但很少有人關心她從何而來。這是一個傍過大款兒,借過東風,被無數人用過,卻輕易被忽視、被白眼、被粗暴對待的pretty old girl。她的確三觀模糊,原則性不強,靈活的讓人髮指,但世事艱難,為了名字中的Java,她也付出了很多代價。好在她飽經滄桑的容顏之下,仍有一顆金子般的心。

當然,她的怪脾氣也折磨過很多人, 好在Jeremy Ashkenas來了,發現了她文藝小清新的本質,精心為她量身設計出了一套鋼鐵戰衣:CoffeeScript。既增強了她的能力,又掩蓋了她的缺陷,所以,現在她是值得每一位程式設計師認真瞭解,仔細思考的好妹紙。哦,你可以瞭解她的內心世界和古怪脾氣,但和她相處的時候,不要太靠近,也不要太深入,讓她穿著CoffeeScript這樣的衣服必是極好的。

交待一下
看了很多文章,還買了本電子書,最後還是覺得 官網上那種方式最容易建立概念,和生成的javascript對比,沒有那麼多細枝末節和古怪的例子。ps,雖然官網上沒說,但其實在windows上安裝也很容易

本來不想再寫,但看到CoffeeScript從1.5.0開始支援文藝範兒的程式設計方式,只要把檔案命名為.litcoff,就能像寫Markdown文件那樣寫程式碼,或者說,把程式碼嵌在文件裡面,這個style打動了我。Smooth CoffeeScript — Interactive Edition就是這樣神奇的電子書,點選書裡的程式碼就可以修改,馬上能看到效果。這種體驗對我來說是第一次,很有愛。

簡介

只是簡介

程式設計的藝術

原文

個人電腦剛出現的時候,大多都配備了簡單的程式語言,一般是BASIC的某種變體。 與電腦的互動主要仰仗這種語言,因此,初期的電腦使用者,不管願意與否,對這種 語言都略知一二。現在電腦已經非常普遍了,大多數使用者只要用滑鼠在螢幕上點來點去 就能滿足自己的需要。可對於我們這些天生的技術迷來說,離了程式設計彷彿就是把電腦 關進了籠子。

拜網際網路所賜,現在所有電腦上都裝著web瀏覽器,因此也有Javascript程式設計環境。 這個環境可以很容易地為CoffeeScript所用。可本著不用技術細節給使用者添堵的精神, 這個環境被藏起來了,但只要有個web頁面,就能進入這個環境,並以它為平臺學習程式設計。

○•○

子曰:“不憤不啟,不悱不發。舉一隅不以三隅反,則不復也。” 選自《論語·第七章·述而篇》

我們不光要講解CoffeeScript,還要介紹程式設計的基本原則。程式設計,說實在的,很難。 一般來說,程式設計的宗旨是保持簡單,清晰。但在這些基本原則之上構建起來的程式 會變得越來越複雜,形成其固有的規律和複雜性。因此,程式設計很難呈現出簡單或可預測的 狀態。該領域的奠基人之一,Donald Knuth說,這是一門藝術

要想撈到乾貨,光看不行。你還得保持清醒的頭腦,努力做好練習,有股 不搞明白絕不罷休的勁頭。

○•○

程式設計師跟宇宙的締造者一樣。程式就是他們建立的虛擬宇宙, 可以變得無限複雜.
Joseph Weizenbaum, Computer Power and Human Reason

計算機程式是個複雜的多面體。它是程式設計師敲的一段文字,是指揮電腦工作的直接命令,是存在 記憶體裡的資料,還控制著其所在記憶體的動作。在我們熟知的東西里很難找到跟程式類似的 東西,不過勉強可以把它比作一臺機器。機械錶的齒輪巧妙地契合在一起,如果表匠好,這塊 表能精確地走好多年。程式的各個元素也是這樣組合在一起,如果程式設計師知道自己在幹啥,程式 也能平穩執行。

電腦就是執行這些非物質機器的宿主。電腦本身只能傻乎乎地做一些簡單的動作,但它們做這些 事情的速度快得讓人眼花繚亂的。程式能把很多簡單的動作組合起來,完成非常複雜的任務。

程式設計是個讓人著迷的遊戲。一個程式就是一座思想之塔。搭建它不用花一文錢,也沒重量,敲敲 鍵盤它就能長。如果我們忘乎所以,程式的規模和複雜度很快就會失去控制,連建立者都會被搞 糊塗。這就是程式設計最主要的問題。所以現如今很多軟體都會崩潰,出錯,搞砸了。

能正常運轉的程式很美。程式設計的藝術就是控制複雜度的技巧。優秀的程式是被馴服的猛獸, 能在複雜中呈現簡單。

○•○

現在,很多程式設計師認為只在程式中使用一小部分大家所熟知的技術可以控制這種複雜性。他們給 正規的程式設定了嚴格的規則,而其中的狂熱分子會聲討那些膽敢打破這些規則的“壞”程式設計師。

要把程式設計變成簡單直接和可預測的東西,要給所有奇美的程式加上各種限制。這對豐富多彩的 程式設計是有多大敵意啊!程式設計技術的迷人之處就在於其廣袤的多樣性,有很多尚未探索過的蠻荒之地。

其中肯定有各種陷阱和圈套,引誘著經驗不足的程式設計師犯下各種恐怖的錯誤,但因此而固步自封, 因噎廢食的程式設計師肯定會忘掉自己的樂趣,失去程式設計的樂趣(只好變成經理)。正確的做法是在 程式設計時小心謹慎,充分發揮你的聰明才智。隨著不斷的學習,你總會遇到新的挑戰,進入新的領域。

衡量程式的終極標準是它做的對不對。效率,清晰度,大小雖然也都重要,但如何平衡他們, 每個程式設計師必須自己做出判斷。經驗法則固然有用,但違反它們也未嘗不可。

程式演化簡史

最初,計算機剛剛誕生時,根本沒有程式語言。程式是下面這種東西:

00110001 00000000 00000000 00110001 00000001 00000001
00110011 00000001 00000010 01010001 00001011 00000010
00100010 00000010 00001000 01000011 00000001 00000000
01000001 00000001 00000001 00010000 00000010 00000000
01100010 00000000 00000000

這段程式從1加到10,並輸出結果(1 + 2 + … + 10 = 55)。它可以在非常簡單的計算機 上執行。要給早期計算機程式設計,必須把一大組開關撥到正確的位置上,或者在硬紙帶上打孔, 輸入到計算機裡。你可以想象得出來,這個過程是多麼的繁瑣和容易出錯。即便寫個簡單 的程式,也需要極大的聰明才智和嚴明的紀律,複雜的就別想了,沒戲。

當然,手工輸入這些晦澀的位模式(上面這種1和0一般都這麼叫)的確給程式設計師一種自己是 神奇魔法師的良好感覺。從工作的滿意度來講,的確是值得的。

程式的每一行都有一條指令,翻成漢語就是:

1 把數值 0 存在記憶體的位置 0 中
2 把數值 1 存在記憶體的位置 1 中
3 把記憶體位置 1 中的值存在位置 2 中
4 將位置 2 中的值減去 11 
5 如果記憶體位置 2 中的值是 0,轉而執行指令 9
6 將記憶體位置 1 的值加到 位置 0 上
7 給記憶體位置 1 的值加 1
8 轉而執行指令 3
9 輸出記憶體位置 0 中的值

儘管這比那段二進位制程式碼的可讀性更強,但還是不太好。用名字代替指令和記憶體位置的數值 可能會更好:

Set 'total' to 0
Set 'count' to 1
[loop]
Set 'compare' to 'count'
Subtract 11 from 'compare'
If 'compare' is zero, continue at [end]
Add 'count' to 'total'
Add 1 to 'count'
Continue at [loop]
[end]
Output 'total'

這就不難看出來這程式是怎麼回事兒了。頭兩行給兩個記憶體位置設定了初始值:total 用來儲存程式的執行結果,count用來跟蹤我們目前要加的數值。用compare那一行 應該是最奇怪的。它的作用是程式用來判斷count是否等於11,以便決定是否應該停下 來。因為機器太原始,所以它只能判斷數值是否為0,然後根據判斷結果決定(跳轉)。 所以它用標籤為compare 的記憶體位置計算 count - 11的值,並以此值為依據做 出決定。接下來的兩行將count的值加到結果上,只要還不是11,就給count加1。 下面是同一程式的CoffeeScript實現:

total = 0
count = 1
while count <= 10
  total += count
  count += 1
total

這段程式碼又有幾處改善。其中最重要的是我們再不用關心程式如何向前後跳轉。魔咒 while會幫我們處理好這些事情。只要滿足條件: count <= 10,即count小於 等於10,它就繼續執行。很明顯,已經沒必要建立一個臨時值並判斷它是否等於0了。這是 一個無腦的小細節,而變成語言的力量就在於它能幫我們處理那些無腦的小細節。

在CoffeeScript中,還可以用更短的程式碼表示相同的意思:

total = 0
total += count for count in [1..10]
total

關鍵字 forin 遍歷 1 到 10 之間的數值 [1..10],依次將其中的值賦給 count。然後將其加到total上。

最後,如果恰好有個便捷的sum操作,能像數學符號 ∑ 那樣計算 一個集合數值的和值,程式碼還可以更簡潔:

sum [1..10]
具體實現
# Summation using a reduce function
sum = (v) -> _.reduce v, ((a,b) -> a+b), 0
sum [1..10]

還有一種可能,這個資料型別恰好有個函式。陣列上有個sum函式,可以給出陣列 元素的和值。

[1..10].sum()
具體實現
# Summation using reduce attached to Array
Array::sum = -> @reduce ((a,b) -> a+b), 0
show [1..10].sum()
delete Array::sum

之所以講這個故事,是想告訴你,同樣的程式,其表示方式可長可斷,可以讓你看懂, 也可以讓你看不懂。我們這個程式的第一版非常難懂,而最後一個幾乎就是普通英文: show110 這寫數值的 sum (在後續章節中,你會看到如何構建 sum這樣的東西)。

好的程式語言能讓程式設計師以更簡明的方式表示自己的想法。可以把無聊的細節隱藏起來, 提供便捷的構件(比如 while結構),而且一般還能讓程式設計師新增自己的構件(比如 sum操作)。

不羈的Javascript

目前來看,Javascript主要用來在網際網路的頁面上做各種或聰明,或恐怖的事情。也在 很多應用和作業系統上 充當指令碼語言。

特別值得一提的是服務端Javascript(SSJS),web應用的服務端也是用Javascript寫 的,所以整個應用都可以用一種程式語言搞定。CoffeeScript生成標準的Javascript 程式碼,所以能用標準Javascript的環境就可以接受CoffeeScript。也就是說瀏覽器端 和服務端都能用CoffeeScript寫。

CoffeeScript是新丁,能不能在通用程式開發中流行開來還需要時間的檢驗。但如果 你對程式設計有興趣,CoffeeScript絕對是值得學習的語言。也許你以後沒太多機會寫web 程式,但書中這些讓你絞盡腦汁的程式最終會留下來陪伴著你,縈繞在你身邊,影響 你用其它語言寫的程式。

有些人可能會說Javascript*劣跡斑斑*。而他們說的基本都是真的。我第一次接觸 Javascript後,很快就開始鄙視她了。幾乎我敲什麼她都能接受,但她的解釋結果和我 最初的意圖卻完全是兩碼事。雖然我知道有很多事情要做,但我卻完全搞不清楚自己 在做什麼,她也不給個提示,可真正困擾我的是:Javascript簡直太隨意了,怎麼搞 都行。

Javascript之所以這樣,是因為設計者希望新人也能很快上手。可實際上,結果卻是讓你 很難找到程式中出現的問題,也別指望語言系統會給你指出來。

然而,靈活終究是個優點。相對比較嚴格死板的語言來說,她有更多的發揮空間,可以 實現些新技術,還可以用來克服自身的某些缺點。經過恰當的學習,以及一段時間的用心 相處之後,我發現自己真的開始喜歡她了。CoffeeScript對Javascript中很多令人 迷惑和臃腫的地方進行了修復,同是保留了她內在的靈活性和美好的一面。是好上加好 的語言。

○•○

本書的大多數章節中都有大量的程式碼。按我的經驗,讀寫程式碼是學習程式設計時的一門重要 功課。希望你能珍惜這些例子,不要走馬觀花一帶而過,要用心閱讀,務必弄懂。一開始 你可能會覺得很慢,很糊塗,但你很快就能掌握訣竅。另,對待練習也要如此。在你真正 寫出能用的程式碼之前,別假裝自己已經明白了。不裝也不會死。

因為web的工作機制,總能看到別人在自己的頁面上寫的Javascript程式。這對於學習某些 前端效果是如何實現的很有幫助。但是,大多數web程式設計師都不夠‘職業’,或者考慮到由於Javascript 程式設計是如此的無聊,他們可能從來沒認真學過,所以你能找到的很多程式碼質地都不怎麼樣。 跟著醜陋或錯誤的程式碼學,學到的只能是醜陋和迷糊,所以要慎重選擇自己學習的物件。對了,在 github這樣的開源服務網站上也能找到些CoffeeScript專案。

CoffeeScript基礎

if !懂Javascript then 看這裡看這裡看這裡 else 貴賓,這邊請!

Coffeescript基礎:零基礎篇

boring,僅摘錄部分內容供參考,詳細內容請參見原文:Smooth-CoffeeScript

值與變數

在計算機的世界裡只有資料,沒有不是資料的東西。儘管所有資料本質上都只是 一個位序列,從根本上來說差不多,但每塊資料都有自己的職責。在CoffeeScript 系統中,這些資料一般都可以整齊地分為稱作值的東西。每個值都 有型別,決定了它所承擔的職責。CoffeeScript中有6種基本的值型別:數字,字串, 布林型,物件,函式和undefined值。

建立值很方便,只要給它個名字就行。不用收集素材,也不用花錢,只要給它起個名字, 你就能擁有它。當然,它們也不是憑空出現的。所有值都必須有自己的空間,並且如果你要 同時使用大量的值,很可能會把記憶體耗光。但只有需要它們同時出現時才有這個問題,一旦 你不再需要,這個值就會被廢棄掉,只留下一些bits。這些bits還可以回收再利用,分配給 新的值。

你應該把變數想象成觸手,而不是盒子。變數不是把值裝在裡面,而是抓在它 - 兩個變數可能指向同一個值。程式只能訪問那些它還抓著的值。當它需要記住什麼的 時候,它會長出一個觸手來抓住這個東西,或者用已有的觸手抓住一個新的值:

要記住2gua還欠你多少錢,可以用變數...然後,每次2gua還錢給你,都可以減掉還的錢數, 然後給這個變數一個新值:

2gua = 140
2gua = 2gua - 35

程式碼塊的體形

先看一塊程式碼:

line = ''
counter = 0
while counter < 10
  line = line + '#'
  show line
  counter = counter + 1

你應該注意到了,某些語句前面加了空格。這是有說法的:縮排多少決定了某行 語句屬於哪塊程式碼。縮排是為了讓程式碼更清晰。因為新的程式碼塊可能要在其它程式碼塊 裡開啟,所以如果沒有縮排,可能很難看出來這些程式碼塊 在哪裡開始,又在哪裡結束。如果每行程式碼都按規定縮排,各程式碼塊就能形成各自的 身體曲線,很直觀。我喜歡用2個空格縮排,但個人喜好不同,你也可以把程式碼弄得更凹凸。 另外,如果一行太長,可以在行尾放個 \,然後在下一行繼續。

控制流

本質就是兩樣,iffor,其它都是各種改進,自己看去吧


假如生活欺騙了你,
不要悲傷,不要心急!
憂鬱的日子裡需要鎮靜:
相信吧,快樂的日子將會來臨。


CoffeeScript基礎 for 熟悉Javascript的人

原文:Rocking Out With CoffeeScript

CoffeeScript的背景都交待過了,閒言碎語就不講了。先來段程式碼:

index = (list, target) ->
  [low, high] = [0, list.length]
  while low < high
    mid = (low + high) >> 1
    val = list[mid]
    return mid if val is target
    if val < target then low = mid + 1 else high = mid
  return -1

這是個二分查詢的CoffeeScript實現,看不懂沒關係,先感受下語法。再看下這段程式碼的編譯結果,也就是用它生成的Javascript:

var index;
index = function(list, target) {
  var high, low, mid, val, _ref;
  _ref = [0, list.length], low = _ref[0], high = _ref[1];
  while (low < high) {
    mid = (low + high) >> 1;
    val = list[mid];
    if (val === target) {
      return mid;
    }
    if (val < target) {
      low = mid + 1;
    } else {
      high = mid;
    }
  }
  return -1;
};

目測兩者的體檢確實有1/3強的差距。感受完了嗎?好,正文開始,先來說說CoffeeScript的長短。

長處與短處

簡單列幾條,是不是真的請用後面的程式碼自行判斷。

長處

  • Python風格的縮排分塊法
  • Ruby風格的輕量化語法
  • 精簡的函式宣告
  • 支援JSLint
  • 基於類的繼承

當然,他的優點不限於此,還有很多,等著你去發掘。

短處

  • 學起來有點難
  • 部署起來也有點難,還得先編譯
  • 你還是得了解Javascript,除錯起來效率才能比較高

怎麼安裝前面已經說過了,如果你覺得還差個編輯器,可以用Sublime Text 2。不過記得 裝上 Package Control ,然後再裝 CoffeeScript-Sublime-Plugin ,coffeescript 的編輯工具就準備好了。

空格也是乾糧

在很多語言中,空格都是無足輕重的東西。可在Coffeescript中,它很重要。而且它也不負重託,出色完成了程式碼簡化的任務。Pythoner請去打醬油吧,後面沒啥可看的了,謝謝!

首先,一行程式碼結束的時候,不用寫分號。換行就代表一句話結束了,又不是自然語言,搞什麼標點符號。 這樣是不是挺好?

numbers = [0, 1, 2, 3]
name = "NetTuts+"

這樣呢?

var name, numbers;
numbers = [0, 1, 2, 3];
name = "NetTuts+";

有必要嗎?那麼多;,有幾個有用的?即便有,不能克服一下嗎?不能從眾嗎?

還有,大括號其實也沒必要。寫那麼多大括號,甚至還得肉眼去匹配,累不累?用Coffeescript吧,用空格形成縮排就能標記程式碼塊的開始和結束。

Coffeescript不要求你輸入不必要的大括弧和小括弧 。括弧 有必要的時候還可以用 括弧結束

自己比較吧,Coffeescript:

if chasedByCylons
 runForYourLife()

編譯結果:

if (chasedByCylons) {
  runForYourLife();
}

如果你的手指頭還不由自主地敲 ;(),以及 {},先過渡一下,慢慢它會習慣的。

俏皮的別名

CoffeeScript 給Javascript中的一些操作符和關鍵字起了別名,以便讓程式碼看起來更直觀,易懂。先看幾個比較操作符:

  • is 對應 ===
  • isnt 對應 !==
  • ==!= 分別編譯成 ===!==

馬上來段程式碼看一下:

if pant is onFire
 lookForWater()
if game isnt good
 badMouth();

哦,還是手賤,不由自主地敲了個;。它會被編譯成:

if (pant === onFire) {
  lookForWater();
}
if (game !== good) {
  badMouth();
}

是不是很容易看懂?好了,接著看邏輯操作符的別名。

  • and&&
  • or||
  • not!

延續上一個例子:

if pant is onFire and not aDream
 lookForWater()
if game isnt good or haughtyDevs
 badMouth()

這是編譯結果:

if (pant === onFire && !aDream) {
  lookForWater();
}
if (game !== good || haughtyDevs) {
  badMouth();
}

條件判斷

前面的程式碼中已經見過 if/else 了,用法和javascript一樣,只是去掉了大小括號。

if tired and bored
 sleep()
else
 jog()

coffeescript裡還有些用起來比較方便的變體。先看下三元操作符 :

activity = if sunday then isChilling else isWorking
//JS  
activity = sunday ? isChilling : isWorking;

還有相當於if notunless

keepRunning() unless tired  
keepWorking unless focus is extremelyLow

這是編譯結果:

if (!tired) {
  keepRunning();
}
if (focus !== extremelyLow) {
  keepWorking;
}

迴圈

先來個墊場,說說switch。coffeescript不用case,改成when了,也不需要break,最後是個else。這種寫法倒是比較貼近最初設計switch的初衷,本來就是else if的簡化版。

switch time
 when 6.00 
  wakeUp()
  jotDownList()
 when 9.00 then startWorking()
 when 13.00 then eat()
 when 23.00
  finishUpWork()
  sleep()
 else doNothing()

編譯結果就不貼了,馬上進入正題。

基本迴圈

愛因斯坦:“用同樣的方式做同樣的事,卻希望得到不同的結果,這是瘋掉的表現。”

我一直覺得程式的本質就是迴圈,把繁瑣的重複勞動編成程式,好把人類解放出來,用寶貴的創造性思維做些有意義的事情,比如打個遊戲,看個電影什麼的。所以,每個程式裡面,至少有一個你躲不開的while迴圈。

coffeescript裡一般迴圈都用while

while work > time then freakOut()

while time > work 
  relax()
  mozyAround()

// Raw JS

while (work > time) {
  freakOut();
}
while (time > work) {
  relax();
  mozyAround();
}

只寫一行就加個then,換行就不用了。

if not有對應的unlesswhile not也有對應的until

workOut() until energy < exhaustion 

// Raw JS

while (!(energy < exhaustion)) {
  workOut();
}

除了while,還有loop,它相當於 while true,要想退出迴圈只能靠break,比如:

current = 20
loop
  if current % 7 is 0
    break
  current++
show current

集合的遍歷

coffeescript裡的 for 有兩種用法,for ... infor ... of。先看第一個:

sites = ['CodeCanyon','ThemeForest','ActiveDen']
for site in sites
 alert site

還可以放到一行裡(這樣寫是最佳實踐):

sites = ['CodeCanyon','ThemeForest','ActiveDen']
alert site for site in sites

編譯結果都是下面這樣基本的for迴圈:

var site, sites, _i, _len;
sites = ['CodeCanyon', 'ThemeForest', 'ActiveDen'];
for (_i = 0, _len = sites.length; _i < _len; _i++) {
  site = sites[_i];
  alert(site);
}

for ... of 用來處理關聯陣列(或者雜湊表,或者叫字典,或者叫鍵值對):

managers = 'CodeCanyon': 'Jeffrey Way', 'ThemeForest': 'Mark Brodhuber', 'ActiveDen': 'Lance Snider'

for site, manager of managers
  alert manager + " manages " + site

'for' 還可以帶 by 從句,以固定步長處理陣列,比如:

show number for number in [0..12] by 2

這次我們不看編譯結果,看輸出結果:

0
2
4
6
8
10
12

函式

用Coffeescript建立和使用函式極其簡單。要定義函式,只要先把引數列出來,然後給個 ->,接著實現函式的功能就行了,請看:

playing = (console, game = "Mass Effect") ->
  alert "Playing #{game} on my #{console}."

playing 'Xbox 360', 'New Vegas'

可以設定預設值,呼叫時也不需要括號。

生成的javascript還會做引數是否為null的判斷:

var playing;
playing = function(console, game) {
  if (game == null) {
    game = "Mass Effect";
  }
  return alert("Playing " + game + " on my " + console + ".");
};
playing('Xbox 360', 'New Vegas');

就是這麼簡單。

之前我們都是先看coffeescript,然後看編譯出來的javascript。現在要翻過來,先看一段javascript,然後想想怎麼把它改成coffeescript。

$(document).ready(function() {
 elemCollection = $('.collection');
  for (i=0; i<=elemCollection.length;i++)
  {
    item = elemCollection[i];
   // check for some random property here. I am skipping the check here  
   colortoAdd = item.hasProperty()? yesColor : noColor;
   // I'm quite aware there are better ways to do this 
   $(item).css ('background-color', colortoAdd);
  }
});

是的,這是一段用了jQuery庫的程式碼。用coffeescript還能用jQuery嗎?問這個問題的人肯定是腦子壞掉了,當然能了,白痴!請不要跟我一樣傻,好嗎?

$(document).ready ->
    elemCollection = $('.collection')
    for item in elemCollection    
      colortoAdd = if item.hasProperty() then yesColor else noColor
      $(item).css 'background-color', colortoAdd

說到jQuery,還有一點需要提一下。在需要將函式繫結到this當前值上時,要用胖箭頭=>。在使用事件處理器或基於callback的類庫時,你會需要胖箭頭。比如:

class Widget
  id: 'I am a widget'
  display: => show @id

class Container
  id: 'I am a container'
  callback: (f) ->
    show @id
    f()

a = new Widget
a.display()
b = new Container
b.callback a.display

這段程式碼的輸出是:

   I am a widget
   I am a container
   I am a widget

你可以試著給箭頭減肥,看看是什麼效果。

Coffeescript程式設計正規化

空著也是空著
求推薦,求銀子,求關注。
宣告 正經事兒越來越多,這個不一定能弄完。

預告:

  • 函數語言程式設計
  • 搜尋
  • 物件導向
  • 正規表示式
  • 模組化

函數語言程式設計

治大國如烹小鮮

原文 functional programming

當程式不斷變大,就愈發變得複雜和讓人難以理解。作為一名碼農,我們肯定是極聰明的,但程式的複雜程度總是向常人無法理解的方向發展。更何況其中還經常出現各種即興發揮的奇思妙想,今天就有位同仁在微博上抱怨已經看不懂自己幾個月前寫的程式碼了,研究半天之後,終於看懂了,順便又佩服了一下自己,當時設計的太巧妙了。

技術日新月異,我們又總是忍不住好奇,想嘗試各種新奇的程式設計技巧,可進度也必須要趕,所以往往沒有足夠的時間去認真鑽研。所以大多數碼農都在用半生不熟的技術,同時靠google和論壇來完成任務。這種工作非常驚險刺激,簡直就像讓你對著一定時炸彈,給把鉗子讓你挑根引線剪斷。好在你搞砸了也不會被炸死,頂多讓客戶損失個幾千萬,然後拍拍屁股走人。

好在大多數人都不會負責那麼重要的程式。但作為一名碼農,還是要隨時做好準備,不能忘了學習,別拿自己的職業生涯開玩笑。多看書,少扯淡,遊戲小說電影肥皂劇什麼的,偶爾看一下就好了。

我們要把有限的生命,投入到無限的程式碼簡化工作當中。這裡要介紹一種重要的方法,就是讓程式碼儘可能地抽象。寫程式的時候,注意力很容易被各種小問題打散。碰到個小問題,解決掉,然後再去解決下一個小問題,如此反覆,程式碼就變得跟老奶奶的絮叨一樣:

哦,親愛的,要做豌豆湯,你得先準備豌豆,要那種乾的。然後最少得先泡一晚上,不然好幾個小時都煮不爛。我記得有一回,我那個傻兒子想做豌豆湯。你猜怎麼著?他事先居然沒泡。唉呀媽呀,差點沒把我們的牙給崩掉嘍,所有人都嚼不動。不管怎麼著,泡豆子的時候,你得按一人一杯的量準備,而且得注意啊,一泡豆子就會漲,所以你要是不找個合適的傢伙,豆子很可能會冒出來,所以你還得加夠水。泡好之後,煮豆子的水也有講究,你得按四杯水一杯幹豆子的量加水。蓋上蓋先燜倆小時,什麼也別管,就咕嘟著,然後加點洋蔥,切好的芹菜梗,可能還得加點胡蘿蔔,或者再來點火腿。都放進去再煮幾分鐘,就可以吃了。

老奶奶的這道菜普還有種說法:

每人:一杯幹豆子,半塊洋蔥,半個胡蘿蔔,一根芹菜梗,火腿可選。豆子泡一晚,先按四比一的水量燜兩個小時,然後加蔬菜和火腿,再煮10來分鐘。

這個短多了,但如果你不知道怎麼泡豆子,很可能會因為加的水太少而泡不好。但怎麼泡豆子可以查到,這是技巧的問題。如果你知道受眾具備一定的基礎知識,那就能用更寬泛的概念跟他們交流,講述方式也可以更加簡短和清晰。

這就是所謂的抽象

這個菜譜跟程式設計有什麼關係呢?哦,很明顯,菜譜就是程式。更進一步說,烹調的基礎知識跟程式設計師所掌握的函式和其它結構是相對應的。如果你還記得本書的介紹部分,應該沒忘掉讓構建迴圈更容易的while,還有資料結構部分寫的那些簡單的函式,以便讓其它函式更短,更直白。這些工具中,有些是語言自身帶的,有些是我們碼農編的,我們靠它們把一些瑣碎而又無聊的細節從程式裡剝離出來,讓程式更好用,也更易懂。

本章的主題是函數語言程式設計,通過組合函式的方式實現抽象。用函式的基本思想武裝起來的碼農,或者說,知道怎麼使用函式的碼農要比那些從頭開始的毛頭小子們更有效率。可標準的Coffeescript環境中沒準備幾個基本函式,所以我們得自己編,或者用別人寫好的程式碼。

本章會寫一些實用的函式,以幫助我們理解它們的工作原理,並通過解決問題來掌握如何使用它們。後續章節中我們會大量用到CoffeeScript的Underscore 類庫中的函式。

並不是只有函數語言程式設計才能實現抽象,還有其它比較流行的方法,物件導向程式設計就是其中之一。

高階函式

遍歷陣列中的元素之於程式設計就像馬步之於武術,是基本功。很多程式語言都是跟C學的:

size_t i;
size_t N = sizeof(thing) / sizeof(thing[0]);
for (i = 0; i < N; ++i) {
  do_something(thing[i]);
}

好醜,只要你是個有品位的人,看了肯定會翻白眼。這在C++和JavaScript中已經有所改善了。而在Coffeescript中,陣列的遍歷已經做得非常漂亮了:

# Say we have:
thing = [5..7]
doSomething = show

# then
for i in [0...thing.length] then doSomething thing[i]
# or better - you can see the generated code with: show -> \
for element in thing then doSomething element

我們是一遍遍的寫迴圈語句,還是能用更抽象的方式表示它?

大多數函式只是輸入一些值,把它們組合起來,再返回點兒東西。這樣的迴圈裡有一塊必須執行的程式碼。我們很容易寫一個遍歷陣列並顯示其中每個元素的函式:

printArray = (array) ->
  for element in array
    show element
  return

printArray [7..9]

如果除了顯示它們,我們還想做別的處理,該怎麼辦?既然‘處理’可以表示為一個函式,而函式也是值,我們可以把處理動作當做函式值傳進去:

forEach = (array, action) ->
  for element in array
    action element
  #return

forEach ['Wampeter', 'Foma', 'Granfalloon'], show
runOnDemand -> show forEach # View generated code

在Coffeescript中,大多數語句都是會返回值的表示式,for語句也不例外。可以在forEach最後放個return語句返回undefined。但我們希望讓forEach返回它的值,見到map函式的時候你就知道原因了。

藉助匿名函式,像最開始那個for迴圈這樣的東西可以這樣寫:

sum = (numbers) ->
  total = 0
  forEach numbers, (number) -> total += number
  total
show sum [1, 10, 100]

根據coffeescript的作用域規則,變數total在匿名函式裡是可見的。還有,這個版本比for迴圈那一版也沒短多少。

在上面的程式碼中,我們把變數number綁到陣列中的當前元素上,不再需要numbers[i],並且如果這個陣列是由表示式建立的,也沒必要把它存在變數裡,因為可以直接傳給forEach 。

看下面這段程式碼:

paragraphs = email.split '\n'
for paragraph in paragraphs
  handleParagraph paragraph

現在可以改寫成:

forEach email.split('\n'), handleParagraph

總體而言,用更抽象(或 更高階)的結構能給出更多的資訊,噪音也會減少:sum中的程式碼可以讀作“對numbers中的每個number,將其加到total上“,而不是 ”這裡有個從0開始的變數,並且它會一直增長到陣列numbers的長度,對於該變數的每一個值,我們要找到陣列中對應的元素並把它加到total上“。

操作其它函式的函式被稱為高階函式。通過操作其它函式,他們能在全新的層面談論動作。函式那一節的makeAddFunction 也是高階函式。它不是以函式值為引數,而是產生一個新的函式。

高階函式能用來概括很多常規函式難以表達的演算法。在你掌握了這些函式後,能以更清晰的思路考慮你的程式碼:你可以把演算法分解成幾個基本演算法的組合,可以通過名稱呼叫,而不是亂糟糟的變數和迴圈,需要一次又一次的輸入。

用程式碼來表示我們要做什麼,而不是我們怎麼做,表明我們在更高的抽象層面上工作。實際上這就是更短,更清晰,也更加賞心悅目的程式碼。

我不知道你用了幾個引數

高階函式還能改變傳給它的函式值:

negate = (func) ->
  (x) -> not func x

isNotNaN = negate isNaN
show isNotNaN NaN

這段程式碼的輸出結果是false。

由negate返回的函式isNotNaN將傳給它的引數傳給原始函式func,然後對func返回的結果取反。但如果func的引數不止一個,該怎麼辦?用arguments陣列可以訪問傳給函式的引數,但在你還不知道有多少個引數的時候,怎麼呼叫這個函式呢?

函式中有個apply方法,可以用來解決這個問題。它有兩個引數,實際上只用第二個引數就能解決這個問題了,第一個引數等到討論物件導向程式設計的時候再說,現在用null就行了。第二個引數是一個陣列,包含函式必須處理的引數。

show Math.min.apply null, [5, 6]

negate = (func) ->
  -> not func.apply null, arguments

morethan = (x,y) -> x > y
lessthan = negate morethan
show lessthan 5, 7

除了apply方法,還可以用splats…:

show Math.min [5, 6]...

negate = (func) ->
  (args...) -> not func args...

morethan = (x,y) -> x > y
lessthan = negate morethan
show lessthan 5, 7

reduce 與 map

是的,就是mapreduce

我們前面一直在以陣列為例介紹函數語言程式設計的各種概念,本節再講兩個基本的演算法。在高階函式那一節中的sum函式,實際上是reduce(也叫foldl)的一種變體。

reduce = (array, combine, base) ->
  forEach array, (element) ->
    base = combine base, element
  base

add = (a, b) -> a + b
sum = (numbers) -> reduce numbers, add, 0
show sum [1, 10, 100]

reduce有三個引數,陣列array,函式combine,基準值base。reduce將base和array中的每個元素 combine,最後返回base。sum就是這種模式,所以用reduce肯定可以縮短程式碼,只是 相加 在coffeescript裡是操作符,不是函式,所以先要定義個add函式。

習題21
寫一個函式 countZeroes, 用reduce找出一個數值陣列裡0的個數。然後再寫一個高階函式count,以陣列和檢測函式為引數,返回陣列中經檢測函式判斷返回true的元素的個數。然後用這個函式重新實現countZeroes。

map是另一個跟陣列相關的基本演算法。它也是遍歷陣列,用一個函式處理其中的每個元素,就跟forEach一樣。但它會把函式返回的結果存下來,用它們構造一個新陣列。

map = (array, func) ->
  result = []
  forEach array, (element) ->
    result.push func element
  result

show map [0.01, 2, 9.89, Math.PI], Math.round

結果是:

[0,2,10,3]

注意,後面那個引數是func,不是function,因為function是關鍵字,因此不能當做變數的名稱。還有:

# Leave out result since forEach already returns it
map = (array, func) ->
  forEach array, (element) ->
    func element

# Leave out func arguments
map = (array, func) ->
  forEach array, func

# Leave out forEach arguments
map = forEach

show map [0.01, 2, 9.89, Math.PI], Math.round

從上面這些簡化的過程可以看出函式和表示式如何縮短了一個函式。

習題的答案請到原文中找

隱者之書

別惹編輯

曾經,在Transylvania(羅馬尼亞,特產吸血鬼)的大山深處,住著一位隱士。他經常徜徉在山林之間,賞山巔明月,聞鳥語花香。當然,隱士的人生也並非總是陽光明媚,偶爾也會有場大雨傾盆而降,讓他只能躲在自己的小棚子裡;也會有呼嘯的山風,讓他感到自己是那麼的渺小,所以,這位隱士急切地想寫點兒什麼,讓世人看到他的思想,好讓他覺得自己更有力量。

在嘗試過詩歌,小說和哲學之後,這位隱士最終決定還是寫本技術書。他年輕的時候當過碼農,並且他覺得自己要能寫本程式設計方面的書,名氣和讚譽應該會接踵而至。

所以他動筆了。一開始他往樹皮上寫,但後來發現不太現實。於是就到山下的村子裡買了檯筆記本。寫了幾章之後,他意識到應該用HTML寫,好放到自己的網站上......

如果你不懂HTML......此處略去2000字......

書歸正傳,上回說到這位隱士要把自己的書寫成HTML格式的。他最初直接在手稿中寫各種標籤,但各種大於號和小於號確實太多了,有點傷,而且在他需要&的時候,總是忘了應該寫成&amp; 。他簡直要煩死了。後來他改成了Word,然後再另存為HTML。但Word儲存的HTML檔案特別複雜,比原來大15倍。而且他也不太喜歡Word。

最後,他找到了解決辦法:還是用普通文字寫,但有幾個簡單的規則,定義如何劃分段落,以及怎麼表示標題。然後再寫個程式把這些文字轉成他想要的HTML。

規則如下:

  • 段落之間用空行分割
  • 以‘%’符號開始的段落是標題。‘%’符號越多,標題級別越小。
  • 在段落內,用星號括起重點內容
  • 腳註寫在括號內

經過大半年的艱苦奮鬥,這位隱士只寫出了幾段。上帝終於失去了耐心,用閃電把他的小棚子給劈了,他也掛了,那顆寫書的心也安息了。從他那臺燒焦的筆記本里,我找到了下面這段手稿,然後把它放進了一個全域性字串變數 recluseFile 裡,所以你可以把它用作一個字串值。

recluseFile = """
% The Book of Programming

%% The Two Aspects 雙生花

Below the surface of the machine, the program moves.
Without effort, it expands and contracts. In great harmony, 
electrons scatter and regroup. The forms on the monitor
are but ripples on the water. The essence stays invisibly
below.

硬體為體,軟體為靈。張弛有度,似行雲流水。電散電聚,若化雨春風。然色相於外,難窺其心。

When the creators built the machine, they put in the
processor and the memory. From these arise the two aspects
of the program.

硬體之形,具 。程式之花,由此雙生。

The aspect of the processor is the active substance. It is
called Control. The aspect of the memory is the passive 
substance. It is called Data.

算之一支,主攻為陽,Control是也。存之一支,以受為陰,Data是也。

Data is made of merely bits, yet it takes complex forms.
Control consists only of simple instructions, yet it
performs difficult tasks. From the small and trivial, the
large and complex arise.

Data者,零一而已,然其形千變萬化。Control者,不過小令,然可解雜難之務。溪匯成海,土聚為山。

The program source is Data. Control arises from it. The
Control proceeds to create new Data. The one is born from
the other, the other is useless without the one. This is
the harmonious cycle of Data and Control.

Data者,軟體之源。Control因之而生,然Control起而新Data生。陽由陰生,陰為陽用。Data Control,相輔相成。

Of themselves, Data and Control are without structure. The
programmers of old moulded their programs out of this raw
substance. Over time, the amorphous Data has crystallised
into data types, and the chaotic Control was restricted
into control structures and functions.

混沌初開之時,Data Control尚未成形。有猿之長者勵精圖治,煉Data之散沙為型別之結晶,熔Control之亂流成構件及函式。

%% Short Sayings 程式設計者說

When a student asked Fu-Tzu about the nature of the cycle
of Data and Control, Fu-Tzu replied 'Think of a compiler,
compiling itself.'

學生問夫子,Data和Control迴圈的本質是什麼?夫子答:就像編譯自己的編譯器。[1]

[1]夫子這是暗指Coffeescript?

A student asked 'The programmers of old used only simple
machines and no programming languages, yet they made
beautiful programs. Why do we use complicated machines
and programming languages?'. Fu-Tzu replied 'The builders
of old used only sticks and clay, yet they made beautiful
huts.'

學生問:“先輩們用簡單的機器,沒有程式語言,也能做出漂亮的程式。我們為什麼還要用複雜的機器和程式語言呢?”夫子答曰:“先輩們用樹枝和粘土也能搭出漂亮的茅屋。”

A hermit spent ten years writing a program. 'My program can
compute the motion of the stars on a 286-computer running
MS DOS', he proudly announced. 'Nobody owns a 286-computer
or uses MS DOS anymore.', Fu-Tzu responded.

一個隱士用十年的時間寫了個程式。他說:“我的程式能在跑DOS系統的286上算出星星執行的軌跡,我驕傲!”夫子說:“現在找不到286了,也沒人用DOS。”

Fu-Tzu had written a small program that was full of global
state and dubious shortcuts. Reading it, a student asked
'You warned us against these techniques, yet I find them in
your program. How can this be?' Fu-Tzu said 'There is no
need to fetch a water hose when the house is not on fire.'
{This is not to be read as an encouragement of sloppy
programming, but rather as a warning against neurotic
adherence to rules of thumb.}

夫子寫的一個小程式裡有很多全域性狀態和不三不四的快捷操作。一個學生看到了,問他,“你跟我們說不能這樣,可你卻這樣,這是怎麼回事兒?”夫子說:“房子沒著火的時候沒必要去拿水管。”{這不是說你可以稀裡糊塗地寫程式,而是告訴你不要墨守成規。}

%% Wisdom 智慧

這個我沒有,你們玩吧。

A student was complaining about digital numbers. 'When I
take the root of two and then square it again, the result
is already inaccurate!'. Overhearing him, Fu-Tzu laughed.
'Here is a sheet of paper. Write down the precise value of
the square root of two for me.'

Fu-Tzu said 'When you cut against the grain of the wood,
much strength is needed. When you program against the grain
of a problem, much code is needed.'

Tzu-li and Tzu-ssu were boasting about the size of their
latest programs. 'Two-hundred thousand lines', said Tzu-li,
'not counting comments!'. 'Psah', said Tzu-ssu, 'mine is
almost a *million* lines already.' Fu-Tzu said 'My best
program has five hundred lines.' Hearing this, Tzu-li and
Tzu-ssu were enlightened.

A student had been sitting motionless behind his computer
for hours, frowning darkly. He was trying to write a
beautiful solution to a difficult problem, but could not
find the right approach. Fu-Tzu hit him on the back of his
head and shouted '*Type something!*' The student started
writing an ugly solution. After he had finished, he
suddenly understood the beautiful solution.

%% Progression

A beginning programmer writes his programs like an ant
builds her hill, one piece at a time, without thought for
the bigger structure. His programs will be like loose sand.
They may stand for a while, but growing too big they fall
apart{Referring to the danger of internal inconsistency
and duplicated structure in unorganised code.}.

Realising this problem, the programmer will start to spend
a lot of time thinking about structure. His programs will
be rigidly structured, like rock sculptures. They are solid,
but when they must change, violence must be done to them
{Referring to the fact that structure tends to put
restrictions on the evolution of a program.}.

The master programmer knows when to apply structure and
when to leave things in their simple form. His programs
are like clay, solid yet malleable.

%% Language

When a programming language is created, it is given
syntax and semantics. The syntax describes the form of
the program, the semantics describe the function. When the
syntax is beautiful and the semantics are clear, the
program will be like a stately tree. When the syntax is
clumsy and the semantics confusing, the program will be
like a bramble bush.

Tzu-ssu was asked to write a program in the language
called  Java, which takes a very primitive approach to
functions. Every morning, as he sat down in front of his
computer, he started complaining. All day he cursed,
blaming the language for all that went wrong. Fu-Tzu
listened for a while, and then reproached him, saying
'Every language has its own way. Follow its form, do not
try to program as if you were using another language.'
"""
show # 'The End'
思路

為了紀念這位善良的隱士,我覺得自己來完成他未竟的事業,寫一個生成HTML的程式。下面是我解決這個問題的大致思路:

  1. 按空行將整個檔案切分成多個段落。
  2. 從標題中去掉字元%,並轉換成相應級別的標題。
  3. 處理段落中文字,分成普通的文字塊,加重部分,以及腳註部分。
  4. 把所有的腳註都放到文件的底部,把數字[S]留在原位
  5. 把每一塊都包進對應的HTML標籤中。
  6. 然後把它們都合併到一個HTML文件中。

不過按這種思路有個問題,不能在加重文字中新增腳註,反之亦然。雖然粗陋,但程式碼寫起來簡單。不過如果你有興趣,可以把這個當作練習來試一下,寫一個能支援標記嵌入的程式。

隱士寫的內容都儲存在 ’06-RecluseFile.text’檔案中。可以用前面那個readTextFile函式讀取它,作為一個字串值。比如recluseFile = readTextFile ’06-RecluseFile.text’

相關文章