這個(gè)系列的起因是這樣的,王垠寫了一篇噴go的博客http://www.yinwang.org/blog-cn/2013/04/24/go-language/,里面說go已經(jīng)爛到無可救藥了,已經(jīng)懶得說了,所以讓大家去看http://www.mindomo.com/view.htm?m=8cc4f95228f942f8886106d876d1b041,里面有詳細(xì)的解釋。然后這篇東西被發(fā)上了微博,很多博友立刻展示了人性丑陋的一面:
1、那些go的擁護(hù)者們,因?yàn)間o被噴了,就覺得自己的人格受到了侮辱一樣,根本來不及看到最后一段的鏈接,就開始張牙舞爪。
2、王垠這個(gè)人的確是跟人合不來,所以很多人就這樣斷定他的東西“毫無參考價(jià)值”。
不過說實(shí)話,文章里面是噴得有點(diǎn)不禮貌,這也在一定程度上阻止了那些不學(xué)無術(shù)的人們繼續(xù)閱讀后面的精華部分。如果所有的文章都這樣那該多好啊,那么爛人永遠(yuǎn)都是爛人,不糾正自己的心態(tài)永遠(yuǎn)獲得不了任何有用的知識(shí),永遠(yuǎn)過那種月入一蛆的日子,用垃圾的語言痛苦的寫一輩子沒價(jià)值的程序。
廢話就說到這里了,下面我來說說我自己對于語言的觀點(diǎn)。為什么要設(shè)計(jì)一門新語言?原因無非就兩個(gè),要么舊的語言實(shí)在是讓人受不了,要么是針對領(lǐng)域設(shè)計(jì)的專用語言。后一種我就不講了,因?yàn)槿绻麤]有具體的領(lǐng)域知識(shí)的話,這種東西永遠(yuǎn)都做不好(譬如SQL永遠(yuǎn)不可能出自一個(gè)數(shù)據(jù)庫很爛的人手里),基本上這不是什么語言設(shè)計(jì)的問題。所以這個(gè)系列只會(huì)針對前一種情況——也就是設(shè)計(jì)一門通用的語言。通用的語言其實(shí)也有自己的“領(lǐng)域”,只是太多了,所以被淡化了。縱觀歷史,你讓一個(gè)只做過少量的領(lǐng)域的人去設(shè)計(jì)一門語言,如果他沒有受過程序設(shè)計(jì)語言理論的系統(tǒng)教育,那只能做出屎。譬如說go就是其中一個(gè)——雖然他爹很牛逼,但反正不包含“設(shè)計(jì)語言”這個(gè)事情。
因此,在21世紀(jì)你還要做一門語言,無非就是對所有的通用語言都不滿意,所以你想自己做一個(gè)。不滿意體現(xiàn)在什么方面?譬如說C#的原因可能就是他爹不夠帥啦,譬如說C++的原因可能就是自己智商太低hold不住啦,譬如說Haskell的原因可能就是用的人太少招不到人啦,譬如說C的原因可能就是實(shí)在是無法完成人和抽象所以沒有l(wèi)inus的水平的人都會(huì)把C語言寫成屎但是你又招不到linus啦,總之有各種各樣的原因。不過排除使用者的智商因素來講,其實(shí)有幾個(gè)語言我還是很欣賞的——C++、C#、Haskell、Rust和Ruby。如果要我給全世界的語言排名,前五名反正是這五個(gè),雖然他們之間可能很難決出勝負(fù)。不過就算如此,其實(shí)這些語言也有一些讓我不爽的地方,讓我一直很想做一個(gè)新的語言(來給自己用(?)),證據(jù)就是——“看我的博客”。
那么。一個(gè)好的語言的好,體現(xiàn)在什么方面呢?一直以來,人們都覺得,只有庫好用,語言才會(huì)好用。其實(shí)這完全是顛倒了因果關(guān)系,如果沒有好用的語法,怎么能寫出好用的庫呢?要找例子也很簡單,只要比較一下Java和C#就夠了。C#的庫之所以好用,跟他語言的表達(dá)能力強(qiáng)是分不開的,譬如說linq(,to xml,to sql,to parser,etc),譬如說WCF(僅考慮易用性部分),譬如說WPF。Java能寫得出來這些庫嗎?硬要寫還是可以寫的,但是你會(huì)發(fā)現(xiàn)你無論如何都沒辦法把他們做到用起來很順手的樣子,其實(shí)這都是因?yàn)镴ava的語法垃圾造成的。這個(gè)時(shí)候可以抬頭看一看我上面列出來的五種語言,他們的特點(diǎn)都是——因?yàn)檎Z法的原因,庫用起來特別爽。
當(dāng)然,這并不要求所有的人都應(yīng)該把語言學(xué)習(xí)到可以去寫庫。程序員的分布也是跟金字塔的結(jié)構(gòu)一樣的,庫讓少數(shù)人去寫就好了,大多數(shù)人盡管用,也不用學(xué)那么多,除非你們想成為寫庫的那些。不過最近有一個(gè)很不好的風(fēng)氣,就是有些人覺得一個(gè)語言難到自己無法【輕松】成為寫庫的人,就開始說他這里不好那里不好了,具體都是誰我就不點(diǎn)名了,大家都知道,呵呵呵。
好的語言,除了庫寫起來又容易又好用以外,還有兩個(gè)重要的特點(diǎn):容易學(xué),容易分析。關(guān)于容易學(xué)這一點(diǎn),其實(shí)不是說,你隨便看一看就能學(xué)會(huì),而是說,只要你掌握了門道,很多未知的特性你都可以猜中。這就有一個(gè)語法的一致性問題在里面了。語法的一致性問題,是一個(gè)很容易讓人忽略的問題,因?yàn)樗幸驗(yàn)檎Z法的一致性不好而引發(fā)的錯(cuò)誤,原因都特別的隱晦,很難一眼看出來。這里我為了讓大家可以建立起這個(gè)概念,我來舉幾個(gè)例子。
第一個(gè)例子是我們喜聞樂見的C語言的指針變量定義啦:
相信很多人都被這種東西坑過,所以很多教科書都告訴我們,當(dāng)定義一個(gè)變量的時(shí)候,類型最后的那些星號(hào)都要寫在變量前面,避免讓人誤解。所以很多人都會(huì)想,為什么要設(shè)計(jì)成這樣呢,這明顯就是挖個(gè)坑讓人往下跳嘛。但是在實(shí)際上,這是一個(gè)語法的一致性好的例子,至于為什么他是個(gè)坑,問題在別的地方。
我們都知道,當(dāng)一個(gè)變量b是一個(gè)指向int的指針的時(shí)候,*b的結(jié)果就是一個(gè)int。定義一個(gè)變量int a;也等于在說“定義a是一個(gè)int”。那我們來看上面那個(gè)變量聲明:int *b;。這究竟是在說什么呢?其實(shí)真正的意思是“定義*b是一個(gè)int”。這種“定義和使用相一致”的方法其實(shí)正是我們要推崇的。C語言的函數(shù)定義參數(shù)用逗號(hào)分隔,調(diào)用的時(shí)候也用逗號(hào)分隔,這是好的。Pascal語言的函數(shù)定義參數(shù)用分號(hào)分隔,調(diào)用的時(shí)候用逗號(hào)分隔,這個(gè)一致性就少了一點(diǎn)。
看到這里你可能會(huì)說,你怎么知道C語言他爹就是這么想的呢?我自己覺得如果他不是這么想的估計(jì)也不會(huì)差到哪里去,因?yàn)檫€有下面一個(gè)例子:
int F(int a, int b);
int (*f)(int a, int b);
這也是一個(gè)“定義和使用相一致”的例子。就第一行代碼來說,我們要如何看待“int F(int a, int b);”這個(gè)寫法呢?其實(shí)跟上面一樣,他說的是“定義F(a, b)的結(jié)果為int”。至于a和b是什么,他也告訴你:定義a為int,b也為int。所以等價(jià)的,下面這一行也是“定義(*f)(a, b)的結(jié)果為int”。函數(shù)類型其實(shí)也是可以不寫參數(shù)名的,不過我們還是鼓勵(lì)把參數(shù)名寫進(jìn)去,這樣Visual Studio的intellisense會(huì)讓你在敲“(”的時(shí)候把參數(shù)名給你列出來,你看到了提示,有時(shí)候就不需要回去翻源代碼了。
關(guān)于C語言的“定義和使用相一致”還有最后一個(gè)例子,這個(gè)例子也是很美妙的:
int a;
typedef int a;
int (*f)(int a, int b);
typedef int (*f)(int a, int b);
typedef是這樣的一個(gè)關(guān)鍵字:他把一個(gè)符號(hào)從變量給修改成了類型。所以每當(dāng)你需要給一個(gè)類型名一個(gè)名字的時(shí)候,就先想一想,怎么定義一個(gè)這個(gè)類型的變量,寫出來之后往前面加個(gè)typedef,事情就完成了。
不過說實(shí)話,就一致性來講,C語言也就到此為止了。至于說為什么,因?yàn)樯厦孢@幾條看起來很美好的“定義和使用相一致”的規(guī)則是不能組合的,譬如說看下面這一行代碼:
typedef int(__stdcall*f[10])(int(*a)(int, int));
這究竟是個(gè)什么東西呢,誰看得清楚呀!而且這也沒辦法用上面的方法來解釋了。究其原因,就是C語言采用的這種“定義和使用相一致”的手法剛好是一種解方程的手法。譬如說int *b;定義了“*b是int”,那b是什么呢,我們看到了之后,都得想一想。人類的直覺是有話直說開門見山,所以如果我們知道int*是int的指針,那么int* b也就很清楚了——“b是int的指針”。
因?yàn)镃語言的這種做法違反了人類的直覺,所以這條本來很好的原則,采用了錯(cuò)誤的方法來實(shí)現(xiàn),結(jié)果就導(dǎo)致了“坑”的出現(xiàn)。因?yàn)榇蠹叶剂?xí)慣“int* a;”,然后C語言告訴大家其實(shí)正確的做法是“int *a;”,那么當(dāng)你接連的出現(xiàn)兩三個(gè)變量的時(shí)候,問題就來了,你就掉坑里去了。
這個(gè)時(shí)候我們再回頭看一看上面那一段長長的函數(shù)指針數(shù)組變量的聲明,會(huì)發(fā)現(xiàn)其實(shí)在這種時(shí)候,C語言還是希望你把它看成“int* b;”的這種形式的:f是一個(gè)數(shù)組,數(shù)組返回了一個(gè)函數(shù)指針,函數(shù)返回int,函數(shù)的參數(shù)是int(*a)(int, int)所以他還是一個(gè)函數(shù)指針。
我們?yōu)槭裁磿?huì)覺得C語言在這一個(gè)知識(shí)點(diǎn)上特別的難學(xué),就是因?yàn)樗瑫r(shí)混用了兩種原則來設(shè)計(jì)語法。那你說好的設(shè)計(jì)是什么呢?讓我們來看看一些其它的語言的作法:
C++:
function<int __stdcall(function<int(int, int)>)> f[10];
C#:
Func<Func<int, int, int>, int>[] f;
Haskell:
f :: [(int->int->int)->int]
Pascal:
var f : array[0..9] of function(a : function(x : integer; y : integer):integer):integer;
這些語言的做法,雖然并沒有遵守“定義和使用相一致”的原則,但是他們比C語言好的地方在于,他們只采用一種原則——這就比好的和壞的混在一起要強(qiáng)多了(這一點(diǎn)go也是,做得比C語言更糟糕)。
當(dāng)然,上面這個(gè)說法對Haskell來說其實(shí)并不公平。Haskell是一種帶有完全類型推導(dǎo)的語言,他不認(rèn)為類型聲明是聲明的一部分,他把類型聲明當(dāng)成是“提示”的一部分。所以實(shí)際上當(dāng)你真的需要一個(gè)這種復(fù)雜結(jié)構(gòu)的函數(shù)的時(shí)候,實(shí)際上你并不會(huì)真的去把它的類型寫出來,而是通過寫一個(gè)正確的函數(shù)體,然后讓Haskell編譯器幫你推導(dǎo)出正確的類型。我來舉個(gè)例子:
superApply fs x = (foldr id (.) fs) x
關(guān)于foldr有一個(gè)很好的理解方法,譬如說foldr 0 (+) [1,2,3,4]說的就是1 + (2 + (3 + (4 + 0)))。而(.)其實(shí)是一個(gè)把兩個(gè)函數(shù)合并成一個(gè)的函數(shù):f (.) g = \x->f(g( x ))。所以上述代碼的意思就是,如果我有下面的三個(gè)函數(shù):
add1 x = x + 1
mul2 x = x * 2
sqr x = x * x
那么當(dāng)我寫下下面的代碼的時(shí)候:
superApply [sqr, mul2, add1] 1
的時(shí)候,他做的其實(shí)是sqr(mul2(add1(1)) = ((1+1)*2) * ((1+1)*2) = 16。當(dāng)然,Haskell還可以寫得更直白:
superApply [(\x->x*x), (*2), (+1)] 1
Haskell代碼的簡潔程度真是喪心病狂啊,因?yàn)槿绻覀円肅++來寫出對應(yīng)的東西的話(C語言的參數(shù)無法是一個(gè)帶長度的數(shù)組類型所以其實(shí)是寫不出等價(jià)的東西的),會(huì)變成下面這個(gè)樣子:
template<typename T>
T SuperApply(const vector<function<T(T)>>& fs, const T& x)
{
T result = x;
for(int i=fs.size()-1; i>=0; i--)
{
result = fs[i](result);
}
return result;
}
C++不僅要把每一個(gè)步驟寫得很清楚,而且還要把類型描述出來,整個(gè)代碼就變得特別的混亂。除此之外,C++還沒辦法跟Haskell一樣吧三個(gè)函數(shù)直接搞成一個(gè)vector然后送進(jìn)這個(gè)SuperApply里面直接調(diào)用。當(dāng)然有人會(huì)說,這還不是因?yàn)镠askell里面有foldr嘛。那讓我們來看看同樣有foldr(reverse + aggregate = foldr)的C#會(huì)怎么寫:
T SuperApply<T>(Func<T, T>[] fs, T x)
{
return (fs
.Reverse()
.Aggregate(x=>x, (a, b)=>y=>b(a(y)))
)(x);
}
C#基本上已經(jīng)達(dá)到跟Haskell一樣的描述過程了,而且也可以寫出下面的代碼了,就是無論聲明和使用的語法的噪音稍微有點(diǎn)大……
SuperApply(new Func<T, T>[]{
x=>x*x,
x=>x*2,
x=>x+1
}, 1);
為什么要在討論語法的一致性的時(shí)候說這些問題呢,在這里我想向大家展示Haskell的另一種“定義和使用相一致”的做法。Haskell整個(gè)語言都要用pattern matching去理解,所以上面的這段代碼
superApply fs x = (foldr id (.) fs) x
說的是,凡是你出現(xiàn)類似superApply a b的這種“pattern”,你都可以把它當(dāng)成(foldr id (.) a) b來看。譬如說
superApply [(\x->x*x), (*2), (+1)] 1
其實(shí)就是
(foldr id (.) [(\x->x*x), (*2), (+1)]) 1
只要superApply指的是這個(gè)函數(shù),那無論在什么上下文里面,
你都可以放心的做這種替換而程序的意思絕對不會(huì)有變化——這就是haskell的帶有一致性的原則。那讓我們來看看Haskell是如何執(zhí)行他這個(gè)一致性的。在這里我們需要知道一個(gè)東西,就是如果我們有一個(gè)操作符+,那我們要把+當(dāng)成函數(shù)來看,我們就要寫(+)。如果我們有一個(gè)函數(shù)f,如果我們要把它當(dāng)成操作符來看,那就要寫成`f`(這是按鍵!左邊的那個(gè)符號(hào))。因此Haskell其實(shí)允許我們做下面的聲明:
(Point x y) + (Point z w) = Point (x+z) (y+w)
(+) (Point x y) (Point z w) = Point (x+z) (y+w)
(Point x y) `Add` (Point z w) = Point (x+z) (y+w)
Add (Point x y) (Point z w) = Point (x+z) (y+w)
斐波那契數(shù)列的簡單形式甚至還可以這么寫:
f 1 = 1
f 2 = 1
f (n+2) = f(n+1) + f(n)
甚至連遞歸都可以寫成:
GetListLength [] = 0
GetListLength (x:xs) = 1 + GetListLength xs
Haskell到處都貫徹了“函數(shù)和操作符的替換關(guān)系”和“pattern matching”兩個(gè)原則來做“定義和實(shí)現(xiàn)相一致”的基礎(chǔ),從而實(shí)現(xiàn)了一個(gè)比C語言那個(gè)做了一半的混亂的原則要好得多的原則。
有些人可能會(huì)說,Haskell寫遞歸這么容易,那會(huì)不會(huì)因?yàn)楣膭?lì)人們寫遞歸,而整個(gè)程序充滿了遞歸,很容易stack overflow或者降低運(yùn)行效率呢?在這里你可以往上翻,在這篇文章的前面有一句話“好的語言,除了庫寫起來又容易又好用以外,還有兩個(gè)重要的特點(diǎn):容易學(xué),容易分析。”,這在Haskell里面體現(xiàn)得淋漓盡致。
我們知道循環(huán)就是尾遞歸,所以如果我們把代碼寫成尾遞歸,那Haskell的編譯器就會(huì)識(shí)別出來,從而在生成x86代碼的時(shí)候把它處理成循環(huán)。一個(gè)尾遞歸遞歸函數(shù)的退出點(diǎn),要么是一個(gè)不包含自身函數(shù)調(diào)用的表達(dá)式,要么就是用自身函數(shù)來和其它參數(shù)來調(diào)用。聽起來比較拗口,不過說白了其實(shí)就是:
GetListLength_ [] c = c
GetListLength_ (x:xs) c = GetListLength_ xs (c+1)
GetListLength xs = GetListLength_ xs 0
當(dāng)你寫出這樣的代碼的時(shí)候,Haskell把你的代碼編譯了之后,就會(huì)真的輸出一個(gè)循環(huán),從而上面的擔(dān)心都一掃而空。
實(shí)際上,有很多性能測試都表明,在大多數(shù)平臺(tái)上,Haskell的速度也不會(huì)被C/C++慢超過一倍的同時(shí),要遠(yuǎn)比go的性能高出許多。在Windows上,函數(shù)式語言最快的是F#。Linux上則是Scala。Haskell一直都是第二名,但是只比第一名慢一點(diǎn)點(diǎn)。
為了不讓文章太長,好分成若干次發(fā)布,每次間隔都較短,所以今天的坑我只想多講一個(gè)——C++的指針的坑。剩下的坑留到下一篇文章里面。下面要講的這個(gè)坑,如果不是在粉絲群里面被問了,我還不知道有人會(huì)這么做:
class Base
{
...
};
class Derived : public Base
{
...
};
Base* bs = new Derived[10];
delete[] bs;
我想說,這完全是C++兼容C語言,然后讓C語言給坑了。其實(shí)這個(gè)問題在C語言里面是不會(huì)出現(xiàn)的,因?yàn)镃語言的指針其實(shí)說白了只有一種:char*。很多C語言的函數(shù)都接受char*,void*還是后來才有的。C語言操作指針用的malloc和free,其實(shí)也是把他當(dāng)char*在看。所以當(dāng)你malloc了一個(gè)東西,然后cast成你需要的類型,最后free掉,這一步cast存在不存在對于free能否正確執(zhí)行來說是沒有區(qū)別的。
但是事情到了C++就不一樣了。C++有繼承,有了繼承就有指針的隱式類型轉(zhuǎn)換。于是看上面的代碼,我們new[]了一個(gè)指針是Derived*類型的,然后隱式轉(zhuǎn)換到了Base*。最后我們拿他delete[],因?yàn)閐elete[]需要調(diào)用析構(gòu)函數(shù),但是Base*類型的指針式不能正確計(jì)算出Derived數(shù)組的10個(gè)析構(gòu)函數(shù)需要的this指針的位置的,所以在這個(gè)時(shí)候,代碼就完蛋了(如果沒完蛋,那只是巧合)。
為了兼容C語言,“new[]的指針需要delete[]”和“子類指針可以轉(zhuǎn)父類指針”的兩條規(guī)則成功的沖突到了一起。實(shí)際上,如果需要解決這種問題,那類型應(yīng)該怎么改呢?其實(shí)我們可以跟C#一樣引入Derived[]的這種指針類型。這還是new[]出來的東西,C++里面也可以要求delete[],但是區(qū)別是他再也不能轉(zhuǎn)成Base[]了。只可惜,T[]這種類型被C語言占用了,在函數(shù)參數(shù)類型里面當(dāng)T*用。C語言浪費(fèi)語法罪該萬死呀……
待續(xù)
posted on 2013-04-27 01:24
陳梓瀚(vczh) 閱讀(33255)
評(píng)論(37) 編輯 收藏 引用 所屬分類:
啟示