• <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>
            隨筆-341  評論-2670  文章-0  trackbacks-0
             

            第一步:如果從未發布過博客文章的話,需要在菜單里面選這里添加博客賬號

            ?

            第二步:選擇正確的設置

            ?

            第三步:寫完博客之后,按這里就可以發布了!

            ?

            如果以后需要寫新的博客的話,還可以直接點這里:

            ?

            Word 2013就是簡單好用啊,雖然Live Writer沒有了,但是有了Word 2013,其實也是一樣的。

            posted @ 2013-11-03 09:30 陳梓瀚(vczh) 閱讀(5227) | 評論 (3)編輯 收藏

            大家看到這個標題肯定會歡呼雀躍了,以為功能少的語言就容易學。其實完全不是這樣的。功能少的語言如果還適用范圍廣,那所有的概念必定是正交的,最后就會變得跟數學一樣。數學的概念很正交吧,正交的東西都特別抽象,一點都不直觀的。不信?出門轉左看Haskell,還有抽象代數。因此刪減語言的功能是需要高超的技巧的,這跟大家想的,還有跟go那幫人想的,可以斷定完全不一樣。

            首先,我們要知道到底為什么需要刪減功能。在這里我們首先要達成一個共識——人都是很賤的。一方面在發表言論的時候光面堂皇的表示,要以需求變更和可維護性位中心;另一方面自己寫代碼的時候又總是不惜“后來的維護者所支付的代價代價”進行偷懶。有些時候,人就是被語言慣壞的,所以需要對功能進行刪減的同時,又不降低語言的表達能力,從而讓做不好的事情變得更難(完全不讓別人做不好的事情是不可能的),這樣大家才會傾向于寫出結構好的程序。

            于是,語法糖到底是不是需要被刪減的對象呢?顯然不是。一個好的語言,采用的概念是正交的。盡管正交的概念可以在拼接處我們需要的概念的時候保持可維護性和解耦,但是往往這么做起來卻不是那么舒服的,所以需要語法糖。那如果不是語法糖,到底需要刪減什么呢?

            這一集我們就來討論面向對象的語言的事情,看看有什么是可以去掉的。

            在面向對象剛剛流行起來的時候,大家就在討論什么搭積木編程啊、is-a、has-a這些概念啊、面向接口編程啊、為什么硬件的互相插就這么容易軟件就不行呢,然后就開始搞什么COM啊、SOA啊這些的確讓插變得更容易,但是部署起來又很麻煩的東西。到底是什么原因造成OO沒有想象中那么好用呢?

            之所以會想起這個問題,其實是因為最近在我們研究院的工位上出現了一個相機的三腳架,這個太三腳架用來固定一個手機干點邪惡的事情,于是大家就圍繞這個事情展開了討論——譬如說為什么手機和三腳架是正交的,中間只要一個前凸后凹的用來插的小鐵塊就可以搞定,而軟件就不行呢?

            于是我就在想,這不就是跟所謂的面向接口編程一樣,只要你全部東西都用接口,那軟件組合起來就很簡單了嗎。這樣就算剛好對不上,只要寫個adaptor,就可以搞定了。其實這種做法我們現在還是很常見的。舉個例子,有些時候我們需要Visual C++ 2013這款全球最碉堡的C++ IDE來開發世界上最好的復雜的軟件,不過自帶的那個cl.exe實在是算不上最好的。那怎么辦,為了用一款更好的編譯器,放棄這個IDE嗎?顯然不是。正確的解決方法是,買intel的icc,然后換掉cl.exe,然后一切照舊。

            其實那個面向接口編程就有點這個意思。有些時候一個系統大部分是你所需要的,別人又不能滿足,但是剛好這個系統的一個重要部分你手上又有更好的零件可以代替。那你是選擇更好的零件,還是選擇大部分你需要的周邊工具呢?為什么就非得二選一呢?如果大家都是面向接口編程,那你只需要跟cl.exe換成icc一樣,寫個adaptor就可以上了。

            好了,那接口是什么?其實這并沒有什么深奧的理解,接口指的就是java和C#里面的那個interface,是很直白的。不知道為什么后來傳著傳著這條建議就跟一些封裝偶合在一起,然后各種非面向對象語言就把自己的某些部分曲解為interface,成功地把“面向接口編程”變成了一句廢話。

            不過在說interface之前,有一個更簡單但是可以類比的例子,就是函數和lambda expression了。如果一個語言同時存在函數和lambda expression,那么其實有一個是多余的——也就是函數了。一個函數總是可以被定義為初始化的時候給了一個lambda expression的只讀變量。這里并不存在什么性能問題,因為這種典型的寫法,編譯器往往可以識別出來,最終把它優化成一個函數。當我們把一個函數名字當成表達式用,獲得一個函數指針的時候,其實這個類型跟lambda expression并沒有任何區別。一個函數就只有這兩種用法,因此實際上把函數去掉,留下lambda expression,整個語言根本沒有發生變化。于是函數在這種情況下就屬于可以刪減的功能。

            那class和interface呢?跟上面的討論類似,我主張class也是屬于可以刪減的功能之一,而且刪減了的話,程序員會因為人類的本性而寫出更好的代碼。把class刪掉其實并沒有什么區別,我能想到的唯一的區別也就是class本身從此再也不是一個類型,而是一個函數了。這有關系嗎?完全沒有,你用interface就行了。

            class和interface的典型區別就是,interface所有的函數都是virtual的,而且沒有局部變量。class并不是所有的函數都是virtual的——java的函數默認virtual但是可以改,C++和C#則默認不virtual但是可以改。就算你把所有的class的函數都改成virtual,那你也會因此留下一些狀態變量。這有什么問題呢?假設C++編譯器是一個接口,而Visual C++和周邊的工具則是依賴于這個class所創造出來的東西。如果你想把cl.exe替換成icc,實際上只要new一個新的icc就可以了。而如果C++編譯器是一個class的話,你就不能替換了——就算class所有的函數都是virtual的,你也不可能給出一個規格相同而實現不同的icc——因為你已經被class所聲明的構造函數、析構函數以及寫好的一些狀態變量(成員變量)所綁架了!

            那我們可以想到的一個迫使大家都寫出傾向于比以前更可以組合的程序,要怎么改造語言才可以呢?其實很簡單,只需要不把class的名字看成一個類型,而把他看成一個函數就可以了。class本身有多個構造函數,其實也就是這個意思。這樣的話,所有原本要用到class的東西,我們就會去定義一個接口了。而且這個接口往往會是最小化的,因為完全沒有必要去聲明一些用不到的函數。

            于是跟去掉函數而留下匿名函數(也就是lambda expression)類似,我們也可以去掉class而留下匿名class的。Java有匿名class,所以我們完全不會感到這個概念有多么的陌生。于是我們可以來檢查一下,這樣會不會讓我們喪失什么表達方法。

            首先,是關于類的繼承。我們有四種方法來使用類的繼承。

            1、類似于C#的Control繼承出Button。這完全是接口規格的繼承。我們繼承出一個Button,不是為了讓他去實現一個Control,而是因為Button比Control多出了一些新東西,而且直接體現在成員函數上面。因此在這個框架下,我們需要做的是IControl繼承出IButton。

            2、類似于C#的TextReader繼承出StreamReader。StreamReader并不是為了給TextReader添加新功能,而是為了給TextReader指定一個來源——Stream。因此這更類似于接口和實現的區別。因此在這個框架下,我們需要的是用CreateStreamReader函數來創建一個ITextReader。

            3、類似于C#的XmlNode繼承出XmlElement。這純粹是數據的繼承關系。我們之所以這么做,不是因為class的用法是設計來這么用的,而是因為C++、Java或者C#并沒有別的辦法可以讓我們來表達這些東西。在C里面我們可以用一個union加上一個enum來做,而且大家基本上都會這么做,所以我們可以看到這實際上是為了拿到一個tag來讓我們知道如何解釋那篇內存。但是C語言的這種做法只有大腦永遠保持清醒的人可以使用,而且我們可以看到在函數式語言里面,Haskell、F#和Scala都有自己的一種獨有的強類型的union。因此在這個框架下,我們需要做的是讓struct可以繼承,并且提供一個Nullable<T>(C#也可以寫成T?)的類型——等價于指向struct的引用——來讓我們表達“這里是一個關于數據的union:XmlNode,他只可能是XmlElement、XmlText、XmlCData等有限幾種可能”。這完全不關class的事情。

            4、在Base里面留幾個純虛函數,讓Derived繼承自Base并且填補他們充當回調使用——臥槽都知道是回調了為什么還要用class?設計模式幫我們準備好了Template Method Pattern,我們完全可以把這幾個回調寫在一個interface里面,讓Base的構造函數接受這個interface,效果完全沒有區別。

            因此我們可以看到,干掉class留下匿名class,根本不會對語言的表達能力產生影響。而且這讓我們可以把所有需要的依賴都從class轉成interface。interface是很好adapt的。還是用Visual C++來舉例子。我們知道cl.exe和icc都可以裝,那gcc呢?cl.exe和icc是兼容的,而gcc完全是另一套。我們只需要簡單地adapt一下(盡管有可能不那么簡單,但總比完全不能做強多了),就可以讓VC++使用gcc了。class和interface的關系也是類似的。如果class A依賴于class B,那這個依賴是綁死的。盡管class A我們很欣賞,但是由于class B實現得太傻比從而導致我們必須放棄class A這種事情簡直是不能接受的。如果class A依賴于interface IB,就算他的缺省實現CreateB()函數我們不喜歡,我們可以自己實現一個CreateMyB(),從而吧我們自己的IB實現給class A,這樣我們又可以提供更好的B的同時不需要放棄我們很需要的A了。

            不過其實每次CreateA(CreateMyB())這種事情來得到一個IA的實現也是很蠢得,優點化神奇為腐朽的意思。不過這里就是IoC——Inverse of Control出場的地方了。這完全是另一個話題,而且Java和C#的一些類庫(包括我的GacUI)已經深入的研究了IoC、正確使用了它并且發揮得淋漓盡致。這就是另一個話題了。如何用好interface,跟class是否必須是類型,沒什么關系。

            但是這樣做還有一個小問題。假設我們在寫一個UI庫,定義了IControl并且有一個函數返回了一個IControl的實現,那我們在開發IButton和他的實現的時候,要如何利用IControl的實現呢?本質上來說,其實我們只需要創造一個IControl的實現x,然后把IButton里面所有原本屬于IControl的函數都重定向到這個x上面去,就等價于繼承了。不過這個寫起來就很痛苦了,因此我們需要一個語法糖來解決它,傳說中的Mixin就可以上場了。不知道Mixin?這種東西跟prototype很接近但是實際上他不是prototype,所以類似的想法經常在javascript和ruby等動態語言里面出現。相信大家也不會陌生。

            上面基本上論證了把class換成匿名class的可能性(完全可能),和他對語言表達能力的影響(毫無影響),以及他對系統設計的好處(更容易通過人類的人性的弱點來引導我們寫出比現在更加容易解耦的系統)。盡管這不是銀彈,但顯然比現在的做法要強多了。最重要的是,因為class不是一個類型,所以你沒辦法從IX強轉成XImpl了,于是我們只能夠設計出不需要知道到底誰實現了IX的算法,可靠性迅速提高。如果IY繼承自IX的話,那IX可以強轉成IY就類似于COM的QueryInterface一樣,從“查看到底是誰實現的”升華到了“查看這個IX是否具有IY所描述的功能”,不僅B格提高了,而且會讓你整個軟件的質量都得到提高。

            因此把class換成匿名class,讓原本正確使用OO的人更容易避免無意識的偷懶,讓原本不能正確使用OO的人迅速掌握如何正確使用OO,封死了一大堆因為偷懶而破壞質量的后門,具有相當的社會意義(哈哈哈哈哈哈哈哈)。

            我之所以寫這篇文章是為了告訴大家,通過刪減語言的功能來讓語言變得更好完全是可能的。但這并不意味著你能通過你自己的口味、偷懶的習慣、B格、因為智商低而學不會等各種奇怪的理由來衡量一個語言的功能是否應該被刪除。只有冗余的東西在他帶來危害的時候,我們應該果斷刪除它(譬如在有interface前提下的class)。而且通常我們為了避免正交的概念所本質上所不可避免的增加理解難度所帶來的問題,我們還需要相應的往語言里面加入語法糖或者新的結構(匿名class、強類型union等)。讓語言變得更簡單從來不是我們的目標,讓語言變得更好用才是。而且一個語言不容易學會的話,我們有各種方法可以解決——譬如說增加常見情況下可以解決問題的語法糖、免費分享知識、通過努力提高自己的智商(雖然有一部分人會因此感到絕望不過反正社會上有那么多職業何必非得跟死程死磕)等等有效做法。

            于是在我自己設計的腳本里面,我打算全面實踐這個想法。

            posted @ 2013-10-19 05:51 陳梓瀚(vczh) 閱讀(9461) | 評論 (17)編輯 收藏

            幾個月前就一直有博友關心DSL的問題,于是我想一想,我在gac.codeplex.com里面也創建了一些DSL,于是今天就來說一說這個事情。

            創建DSL恐怕是很多人第一次設計一門語言的經歷,很少有人一開始上來就設計通用語言的。我自己第一次做這種事情是在高中寫這個傻逼ARPG的時候了。當時做了一個超簡單的腳本語言,長的就跟匯編差不多,雖然每一個指令都寫成了調用函數的形態。雖然這個游戲需要腳本在劇情里面控制一些人物的走動什么的,但是所幸并不復雜,于是還是完成了任務。一眨眼10年過去了,現在在寫GacUI,為了開發的方便,我自己做了一些DSL,或者實現了別人的DSL,漸漸地也明白了一些設計DSL的手法。不過在講這些東西之前,我們先來看一個令我們又愛(對所有人)又恨(反正我不會)的DSL——正則表達式!

            一、正則表達式

            正則表達式可讀性之差我們人人都知道,而且正則表達式之難寫好都值得O’reilly出一本兩厘米厚的書了。根據我的經驗,只要先學好編譯原理,然后按照.net的規格自己擼一個自己的正則表達式,基本上這本書就不用看了。因為正則表達式之所以要用奇怪的方法去寫,只是因為你手上的引擎是那么實現的,所以你需要順著他去寫而已,沒什么特別的原因。而且我自己的正則表達式擁有DFA和NFA兩套解析器,我的正則表達式引擎會通過檢查你的正則表達式來檢查是否可以用DFA,從而可以優先使用DFA來運行,省去了很多其實不是那么重要的麻煩(譬如說a**會傻逼什么的)。這個東西我自己用的特別開心,代碼也放在gac.codeplex.com上面。

            正則表達式作為一門DSL是當之無愧的——因為它用了一種緊湊的語法來讓我們可以定義一個字符串的集合,并且取出里面的特征。大體上語法我還是很喜歡的,我唯一不喜歡的是正則表達式的括號的功能。括號作為一種指定優先級的方法,幾乎是無法避免使用的。但是很多流行的正則表達式的括號竟然還帶有捕獲的功能,實在是令我大跌眼鏡——因為大部分時候我是不需要捕獲的,這個時候只會浪費時間和空間去做一些多余的事情而已。所以在我自己的正則表達式引擎里面,括號是不捕獲的。如果要捕獲,就得用特殊的語法,譬如說(<name>pattern)把pattern捕獲到一個叫做name的組里面去。

            那我們可以從正則表達式的語法里面學到什么DSL的設計原則呢?我認為,DSL的原則其實很簡單,只有以下三個:

            1. 短的語法要分配給常用的功能
            2. 語法要么可讀性特別好(從而比直接用C#寫直接),要么很緊湊(從而比直接用C#寫短很多)
            3. API要容易定義(從而用C#調用非常方便,還可以確保DSL的目標是明確又簡單的)

            很多DSL其實都滿足這個定義。SQL就屬于API簡單而且可讀性好的那一部分(想想ADO.NET),而正則表達式就屬于API簡單而且語法緊湊的那一部分。為什么正則表達式可以設計的那么緊湊呢?現在讓我們來一一揭開它神秘的面紗。

            正則表達式的基本元素是很少的,只有連接、分支和循環,還有一些簡單的語法糖。連接不需要字符,分支需要一個字符“|”,循環也只需要一個字符“+”或者“*”,還有代表任意字符的“.”,還有代表多次循環的{5,},還有代表字符集合的[a-zA-Z0-9_]。對于單個字符的集合來講,我們甚至不需要[],直接寫就好了。除此之外因為我們用了一些特殊字符所以還得有轉義(escaping)的過程。那讓我們數數我們定義了多少字符:“|+*[]-\{},.()”。用的也不多,對吧。

            盡管看起來很亂,但是正則表達式本身也有一個嚴謹的語法結構。關于我的正則表達式的語法樹定義可以看這里:https://gac.codeplex.com/SourceControl/latest#Common/Source/Regex/RegexExpression.h。在這里我們可以整理出一個語法:

            DIGIT ::= [0-9]
            LITERAL ::= [^|+*\[\]\-\\{}\^,.()]
            ANY_CHAR ::= LITERAL | "^" | "|" | "+" | "*" | "[" | "]" | "-" | "\" | "{" | "}" | "," | "." | "(" | ")"
            
            CHAR
                ::= LITERAL
                ::= "\" ANY_CHAR
            
            CHARSET_COMPONENT
                ::= CHAR
                ::= CHAR "-" CHAR
            
            CHARSET
                ::= CHAR
                ::= "[" ["^"] { CHARSET_COMPONENT } "]"
            
            REGEX_0
                ::= CHARSET
                ::= REGEX_0 "+"
                ::= REGEX_0 "*"
                ::= REGEX_0 "{" { DIGIT } ["," [ { DIGIT } ]] "}"
                ::= "(" REGEX_2 ")"
            
            REGEX_1
                ::= REGEX_0
                ::= REGEX_1 REGEX_0
            
            REGEX_2
                ::= REGEX_1
                ::= REGEX_2 "|" REGEX_1
            
            REGULAR_EXPRESSION
                ::= REGEX_2

            這只是隨手寫出來的語法,盡管可能不是那么嚴謹,但是代表了正則表達式的所有結構。為什么我們要熟練掌握EBNF的閱讀和編寫?因為當我們用EBNF來看待我們的語言的時候,我們就不會被愈發的表面所困擾,我們會投過語法的外衣,看到語言本身的結構。脫別人衣服總是很爽的。

            于是我們也要透過EBNF來看到正則表達式本身的結構。其實這是一件很簡單的事情,只要把EBNF里面那些“fuck”這樣的字符字面量去掉,然后規則就會分為兩種:

            1:規則僅由終結符構成——這是基本概念,譬如說上面的CHAR什么的。
            2:規則的構成包含非終結符——這就是一個結構了。

            我們甚至可以利用這種方法迅速從EBNF確定出我們需要的語法樹長什么樣子。具體的方法我就不說了,大家自己聯系一下就會悟到這個簡單粗暴的方法了。但是,我們在設計DSL的時候,是要反過來做的。首先確定語言的結構,翻譯成語法樹,再翻譯成不帶“fuck”的“骨架EBNF”,再設計具體的細節寫成完整的EBNF。

            看到這里大家會覺得,其實正則表達式的結構跟四則運算式子是沒有區別的。正則表達式的*是后綴操作符,|是中綴操作符,連接也是中最操作符——而且操作符是隱藏的!我猜perl系正則表達式的作者當初在做這個東西的時候,肯定糾結過“隱藏的中綴操作符”應該給誰的問題。不過其實我們可以通過收集一些素材,用不同的方案寫出正則表達式,最后經過統計發現——隱藏的中綴操作符給連接操作是最靠譜的。

            為什么呢?我們來舉個例子,如果我們把連接和分支的語法互換的話,那么原本“fuck|you”就要寫成“(f|u|c|k)(y|o|u)”了。寫多幾個你會發現,的確連接是比分支更常用的,所以短的那個要給連接,所以連接就被分配了一個隱藏的中綴操作符了。

            上面說了這么多廢話,只是為了說明白一個道理——要先從結構入手然后才設計語法,并且要把最短的語法分配給最常用的功能。因為很多人設計DSL都反著來,然后做成了屎。

            二、Fpmacro

            第二個要講的是Fpmacro。簡單來說,Fpmacro和C++的宏是類似的,但是C++的宏是從外向內展開的,這意味著dynamic scoping和call by name。Fpmacro是從內向外展開的,這意味著lexical scoping和call by value。這些概念我在第七篇文章已經講了,大家也知道C++的宏是一件多么不靠譜的事情。但是為什么我要設計Fpmacro呢?因為有一天我終于需要類似于Boost::Preprocessor那樣子的東西了,因為我要生成類似這樣的代碼。但是C++的宏實在是太他媽惡心了,惡心到連我都不能駕馭它。最終我就做出了Fpmacro,于是我可以用這樣的宏來生成上面提到的文件了。

            我來舉個例子,如果我要生成下面的代碼:

            int a1 = 1;
            int a2 = 2;
            int a3 = 3;
            int a4 = 4;
            cout<<a1<<a2<<a3<<a4<<endl;

            就要寫下面的Fpmacro代碼:

            $$define $COUNT 4 /*定義數量:4*/
            $$define $USE_VAR($index) a$index /*定義變量名字,這樣$USE_VAR(10)就會生成“a10”*/
            
            $$define $DEFINE_VAR($index) $$begin /*定義變量聲明,這樣$DEFINE_VAR(10)就會生成“int a10 = 10;”*/
            int $USE_VAR($index) = $index;
            $( ) /*用來換行——會多出一個多余的空格不過沒關系*/ 
            $$end
            
            $loop($COUNT,1,$DEFINE_VAR) /*首先,循環生成變量聲明*/
            cout<<$loopsep($COUNT,1,$USE_VAR,<<)<<endl; /*其次,循環使用這些變量*/

            順便,Fpmacro的語法在這里,FpmacroParser.h/cpp是由這個語法生成的,剩下的幾個文件就是C++的源代碼了。不過因為今天講的是如何設計DSL,那我就來講一下,我當初為什么要把Fpmacro設計成這個樣子。

            在設計之前,首先我們需要知道Fpmacro的目標——設計一個沒有坑的宏,而且這個宏還要支持分支和循環。那如何避免坑呢?最簡單的方法就是把宏看成函數,真正的函數。當我們把一個宏的名字當成參數傳遞給另一個宏的時候,這個名字就成為了函數指針。這一點C++的宏是不可能完全的做到的,這里的坑實在是太多了。而且Boost::Preprocessor用來實現循環的那個技巧實在是我操太他媽難受了。

            于是,我們就可以把需求整理成這樣:

            1. Fpmacro的代碼由函數組成,每一個函數的唯一目的都是生成C++代碼的片段。
            2. 函數和函數之間的空白可以用來寫代碼。把這些代碼收集起來就可以組成“main函數”了,從而構成Fpmacro代碼的主體。
            3. 函數可以有內部函數,在代碼復雜的時候可以充當一些namespace的功能,而且內部函數都是私有的。
            4. Fpmacro代碼可以include另一份Fpmacro代碼,可以實現全局配置的功能。
            5. Fpmacro必須支持分支和循環,而且他們的語法和函數調用應該一致。
            6. 用來代表C++代碼的部分需要的轉義應該降到最低。
            7. 即使是非功能代碼部分,括號也必須配對。這是為了定義出一個清晰的簡單的語法,而且因為C++本身也是括號配對的,所以這個規則并沒有傷害。
            8. C++本身對空格是有很高的容忍度的,因此Fpmacro作為一個以換行作為分隔符的語言,并不需要具備特別精確的控制空格的功能。

            為什么要強調轉義呢?因為如果用Fpmacro隨便寫點什么代碼都要到處轉義的話,那還怎么寫得下去呀!

            這個時候我們開始從結構入手。Fpmacro的結構是簡單的,只有下面幾種:

            1. 普通C++代碼
            2. 宏名字引用
            3. 宏調用
            4. 連接
            5. 括號
            6. 表達數組字面量(最后這被證明是沒有任何意義的功能)

            根據上面提到的DSL三大原則,我們要給最常用的功能配置最短的語法。那最短的功能是什么呢?跟正則表達式一樣,是連接。所以要給他一個隱藏的中綴運算符。其次就要考慮到轉義了。如果Fpmacro大量運用的字符與C++用到的字符一樣,那么我們在C++里面用這個字符的時候,就得轉義了。這個是絕對不能接受的。我們來看看鍵盤,C++沒用到的也就只有@和$了。這里我因為個人喜好,選擇了$,它的功能大概跟C++的宏里面的#差不多。

            那我們如何知道我們的代碼片段是訪問一個C++的名字,還是訪問一個Fpmacro的名字呢?為了避免轉義,而且也順便可以突出Fpmacro的結構本身,我讓所有的Fpmacro名字都要用$開頭,無論是函數名還是參數都一樣。于是定義函數就用$$define開始,而且多行的函數還要用$$begin和$$end來提示(見上面的例子)。函數調用就可以這么做:$名字(一些參數)。因為不管是參數名還是函數名都是$開頭的,所以函數調用肯定也是$開頭的。那寫出來的代碼真的需要轉義怎么辦呢?直接用$(字符)就行了。這個時候我們可以來檢查一下這樣做是不是會定義出歧義的語法,答案當然是不會。

            我們定義了$作為Fpmacro的名字前綴之后,是不是一個普通的C++代碼(因此沒有$),直接貼上去就相當于一個Fpmacro代碼呢?結論當然是成立的。仔細選擇這些語法可以讓我們在只想寫C++的時候可以專心寫C++而不會被各種轉義干擾到(想想在C++里面寫正則表達式的那一堆斜杠臥槽)。

            到了這里,就到了最關鍵的一步了。那我們把一個Fpmacro的名字傳遞給參數的時候,究竟是什么意思呢?一個Fpmacro的名字,要么就是一個字符串,要么就是一個Fpmacro函數,不會有別的東西了(其實還可能是數組,但是最后證明沒用)。這個純潔性要一直保持下去。就跟我們在C語言里面傳遞一個函數指針一樣,不管傳遞到了哪里,我們都可以隨時調用它。

            那Fpmacro的函數到底有沒有包括上下文呢?因為Fpmacro和pascal一樣有“內部函數”,所以當然是要有上下文的。但是Fpmacro的名字都是只讀的,所以只用shared_ptr來記錄就可以了,不需要出動GC這樣的東西。關于為什么帶變量的閉包就必須用GC,這個大家可以去想一想。這是Fpmacro的函數像函數式語言而不是C語言的一個地方,這也是為什么我把名字寫成了Fpmacro的原因了。

            不過Fpmacro是不帶lambda表達式的,因為這樣只會把語法搞得更糟糕。再加上Fpmacro允許定義內部函數和Fpmacro名字是只讀的這兩條規則,所有的lambda表達式都可以簡單的寫成一個內部函數然后賦予它一個名字。因此這一點沒有傷害。那什么時候需要傳遞一個Fpmacro函數呢進另一個函數呢?當然就只有循環了。Fpmacro的內置函數有分支循環還有簡單的數值計算和比較功能。

            我們來做一個小實驗,生成下面的代碼:

            void Print(int a1)
            {
                cout<<"1st"<<a1<<endl;
            }
            
            void Print(int a1, int a2)
            {
                cout<<"1st"<<a1<<", "<<"2nd"<<a2<<endl;
            }
            
            ....
            
            void Print(int a1, int a2, ... int a10)
            {
                cout<<...<<"10th"<<a10<<endl;
            }
            
            ....

            我們需要兩重循環,第一重是生成Print,第二重是里面的cout。cout里面還要根據數字來產生st啊、nd啊、rd啊、這些前綴。于是我們可以開始寫了。Fpmacro的寫法是這樣的,因為沒有lambda表達式,所以循環體都是一些獨立的函數。于是我們來定義一些函數來生成變量名、參數定義和cout的片段:

            $$define $VAR_NAME($index) a$index /*$VAR_NAME(3) -> a3*/
            $$define $VAR_DEF($index) int $VAR_NAME($index) /*$VAR_DEF(3) -> int a3*/
            $$define $ORDER($index) $$begin /*$ORDER(3) -> 3rd*/
                $$define $LAST_DIGIT $mod($index,10)
                $index$if($eq($LAST_DIGIT,1),st,$if($eq($LAST_DIGIT,2),nd,$if($eq($LAST_DIGIT,3),rd,th)))
            $$end
            $$define $OUTPUT($index) $(")$ORDER($index)$(")<<$VAR_NAME($index) /*$OUTPUT(3) -> "3rd"<<a3*/

            接下來就是實現Print函數的宏:

            $$define $PRINT_FUNCTION($count) $$begin
            void Print($loopsep($count,1,$VAR_DEF,$(,)))
            {
                cout<<$loopsep($count,1,$OUTPUT,<<)<<endl;
            }
            $( ) $$end

            最后就是生成整片代碼了:

            $define $COUNT 10 /*就算是20,那上面的代碼的11也會生成11st,特別方便*/
            $loop($COUNT,1,$PRINT_FUNCTION)

            注意:注釋其實是不能加的,因為如果你加了注釋,這些注釋最后也會被生成成C++,所以上面那個$COUNT就會變成10+空格+注釋,他就不能放進$loop函數里面了。Fpmacro并沒有添加“Fpmacro注釋”的代碼,因為我覺得沒必要

            為什么我們不需要C++的宏的#和##操作呢?因為在這里,A(x)##B(x)被我們處理成了$A(x)$B(x),而L#A(x)被我們處理成了L$(“)$A(x)$(“)。雖然就這么看起來好像Fpmacro長了一點點,但是實際上用起來是特別方便的。$這個前綴恰好幫我們解決了A(x)##B(x)的##的問題,寫的時候只需要直接寫下去就可以了,譬如說$ORDER里面的$index$if…。

            那么這樣做到底行不行呢?看在Fpmacro可以用這個宏來生成這么復雜的代碼的份上,我認為“簡單緊湊”和“C++代碼幾乎不需要轉義”和“沒有坑”這三個目標算是達到了。DSL之所以為DSL就是因為我們是用它來完成特殊的目的的,不是general purpose的,因此不需要太復雜。因此設計DSL要有一個習慣,就是時刻審視一下,我們是不是設計了多余的東西?,F在我回過頭來看,Fpmacro支持數組就是多余的,而且實踐證明,根本沒用上。

            大家可能會說,代碼遍地都是$看起來也很亂???沒關系,最近我剛剛搞定了一個基于語法文件驅動的自動著色和智能提示的算法,只需要簡單地寫一個Fpmacro的編輯器就可以了,啊哈哈哈哈。

            三、尾聲

            本來我是想舉很多個例子的,還有語法文件啊,GUI配置啊,甚至是SQL什么的。不過其實設計一個DSL首先要求你對領域本身有著足夠的理解,在長期的開發中已經在這個領域里面感受到了極大的痛苦,這樣你才能真的設計出一個專門根除痛點的DSL來。

            像正則表達式,我們都知道手寫字符串處理程序經常要人肉做錯誤處理和回溯等工作,正則表達式幫我們自動完成了這個功能。

            C++的宏生成復雜代碼的時候,動不動就會因為dynamic scoping和call by name掉坑里而且還沒有靠譜的工具來告訴我們究竟要怎么做,Fpmacro就解決了這個問題。

            開發DSL需要語法分析器,而且帶Visitor模式的語法樹可擴展性好但是定義起來特別的麻煩,所以我定義了一個語法文件的格式,寫了一個ParserGen.exe(代碼在這里)來替我生成代碼。Fpmacro的語法分析器就是這么生成出來的。

            GUI的構造代碼寫起來太他媽煩了,所以還得有一個配置的文件。

            查詢數據特別麻煩,而且就算是只有十幾個T的小型數據庫也很難自己設計一個靠譜的容器,所以我們需要SQLServer。這個DSL做起來不簡單,但是用起來簡單。這也是一個成功的DSL。

            類似的,Visual Studio為了生成代碼還提供了T4這種模板文件。這個東西其實超好用的——除了用來生成C++代碼,所以我還得自己擼一個Fpmacro……

            用MVC的方法來寫HTML,需要從數據結構里面拼HTML。用過php的人都知道這種東西很容易就寫成了屎,所以Visual Studio里面又在ASP.NET MVC里面提供了razor模板。而且他的IDE支持特別號,razor模板里面可以混著HTML+CSS+Javascript+C#的代碼,智能提示從不出錯!

            還有各種數不清的配置文件。我們都知道,一個強大的配置文件最后都會進化成為lisp,哦不,DSL的。

            這些都是DSL,用來解決我們的痛點的東西,而且他本身又不足以復雜到用來完成程序所有的功能(除了連http service都能寫的SQLServer我們就不說了=_=)。設計DSL的時候,首先要找到痛點,其次要理清楚DSL的結構,然后再給他設計一個要么緊湊要么可讀性特別高的語法,然后再給一個簡單的API,用起來別提多爽了。

            posted @ 2013-09-15 17:26 陳梓瀚(vczh) 閱讀(8602) | 評論 (11)編輯 收藏

            類型是了解編程語言的重要一環。就算是你喜歡動態類型語言,為了想實現一個靠譜的東西,那也必須了解類型。舉個簡單的例子,我們都知道+和-是對稱的——當然這只是我們的愿望了,在javascript里面,"1"+2和"1"-2就不是一回事。這就是由于不了解類型的操作而犯下的一些滑稽的錯誤。什么,你覺得因為"1"的類型是string所以"1"+2就應該是"12"?啐!"1"的類型是(string | number),這才是正確的做法。

            了解編程語言的基本原理并不意味著你一定要成為一名編譯器的前端,正如同學習Haskell可以讓你的C++寫得更好一樣,如果你知道怎么設計一門語言,那遇到語言里面的坑,你十有八九可以當場看到,不會跳進去。當然了,了解編程語言的前提是你是一個優秀的程序員,至少要寫程序,對吧。于是我這里推薦幾門語言是在此之前要熟悉的。編程語言有好多種,每一種都有其代表作,為了開開眼界,知道編程語言可以設計成什么樣子,你至少應該學會:

            1. C++
            2. C#
            3. F#
            4. Haskell
            5. Ruby
            6. Prolog

            其實這一點也不多,因為只是學會而已,知道那些概念就好了,并不需要你成為一個精通xx語言的人。那為了了解類型你應該學會什么呢?沒錯——就是C++了!很多人可能不明白,為什么長得這么難看的C++竟然有這么重要的作用呢?其實如果詳細了解了程序設計語言的基本原理之后,你會發現,C++在除了兼容那個可憐的C語言之外的那些東西,是設計的非常科學的。當然現在講這些還太早,今天的重點是類型。

            如果你們去看相關的書籍或者論文的話,你們會發現類型這個領域里面有相當多的莫名其妙的類型系統,或者說名詞。對于第一次了解這個方面的人來說,熟練掌握Haskell和C++是很有用的,因為Haskell可以讓你真正明白類型在程序里面的重要做喲的同時。幾乎所有流行的東西都可以在C++里面找到,譬如說:

            1. 面向對象→class
            2. polymorphic type→template
            3. intersection type→union / 函數重載
            4. dependent type→帶數字的模板類型
            5. System F→在泛型的lambda表達式里面使用decltype(看下面的例子)
            6. sub typing的規則→泛型lambda表達式到函數指針的隱式類型轉換

            等等等等,因有盡有,取之不盡,用之不竭。你先別批判C++,覺得他東西多所以糟糕。事實是,只要編譯器不用你寫,那一門語言是不可能通過拿掉feature來使它對你來說變得更牛逼的。不知道為什么有那么多人不了解這件事情,需要重新去念一念《形式邏輯》,早日爭取做一個靠譜的人。

            泛型lambda表達式是C++14(沒錯,是14,已經基本敲定了)的內容,應該會有很多人不知道,我在這里簡單地講一下。譬如說要寫一個lambda表達式來計算一個容器里所有東西的和,但是你卻不知道容器和容器里面裝的東西是什么。當然這種情況也不多,但是有可能你需要把這個lambda表達使用在很多地方,對吧,特別是你#include <algorithm>用了里面超好用的函數之后,這種情況就變得常見了。于是這個東西可以這么寫:

            auto lambda = [](const auto& xs)
            {
                decltype(*xs.begin()) sum = 0;
                for(auto x : xs)
                {
                    sum += x;
                }
                return sum;
            };

            于是你就可以這么用了:

            vector<int> a = { ... };
            list<float> b = { ... };
            deque<double> c = { ... };
            
            int sumA = lambda(a);
            float sumB = lambda(b);
            double sumC = lambda(c);

            然后還可以應用sub typing的規則把這個lambda表達式轉成一個函數指針。C++里面所有中括號不寫東西的lambda表達式都可以被轉成一個函數指針的,因為他本來就可以當成一個普通函數,只是你為了讓業務邏輯更緊湊,選擇把這個東西寫在了你的代碼里面而已:

            doube(*summer)(const vector<double>&);
            summer = lambda;

            只要搞明白了C++之后,那些花里胡俏的類型系統的論文的概念并不難理解。他們深入研究了各種類型系統的主要原因是要做系統驗證,證明這個證明那個。其實編譯器的類型檢查部分也可以當成是一個系統驗證的程序,他要檢查你的程序是不是有問題,于是首先檢查系統。不過可惜的是,除了Haskell以外的其他程序語言,就算你過了類型系統檢查,也不見得你的程序就是對的。當然了,對于像javascript這種動態類型就罷了還那么多坑(ruby在這里就做得很好)的語言,得通過大量的自動化測試來保證。沒有類型的幫助,要寫出同等質量的程序,需要花的時間要更多。什么?你不關心質量?你不要當程序員了!是因為老板催得太緊?我們Microsoft最近有招聘了,快來吧,可以慢慢寫程序!

            不過正因為編譯器會檢查類型,所以我們其實可以把一個程序用類型武裝起來,使得錯誤的寫法會變成錯誤的語法被檢查出來了。這種事情在C++里面做尤為方便,因為它支持dependent type——好吧,就是可以在模板類型里面放一些不是類型的東西。我來舉一個正常人都熟練掌握的例子——單位。

            一、類型檢查(type rich programming)

            我們都知道物理的三大基本單位是米、秒和千克,其它東西都可以從這些單位拼出來(大概是吧,我忘記了)。譬如說我們通過F=ma可以知道力的單位,通過W=FS可以知道功的單位,等等。然后我們發現,單位之間的關系都是乘法的關系,每個單位還帶有自己的冪。只要弄清楚了這一點,那事情就很好做了?,F在讓我們來用C++定義單位:

            template<int m, int s, int kg>
            struct unit
            {
                double value;
            
                unit():value(0){}
                unit(double _value):value(_value){}
            };

            好了,現在我們要通過類型系統來實現幾個操作的約束。對于乘除法我們要自動計算出單位的同時,加減法必須在相同的單位上才能做。其實這樣做還不夠完備,因為對于任何的單位x來講,他們的差單位Δx還有一些額外的規則,就像C#的DateTime和TimeSpan一樣。不過這里先不管了,我們來做出加減乘除幾個操作:

            template<int m, int s, int kg>
            unit<m, s, kg> operator+(unit<m, s, kg> a, unit<m, s, kg> b)
            {
                return a.value + b.value;
            }
            
            template<int m, int s, int kg>
            unit<m, s, kg> operator-(unit<m, s, kg> a, unit<m, s, kg> b)
            {
                return a.value - b.value;
            }
            
            template<int m, int s, int kg>
            unit<m, s, kg> operator+(unit<m, s, kg> a)
            {
                return a.value;
            }
            
            template<int m, int s, int kg>
            unit<m, s, kg> operator-(unit<m, s, kg> a)
            {
                return -a.value;
            }
            
            template<int m1, int s1, int kg1, int m2, int s2, int kg2>
            unit<m1+m2, s1+s2, kg1+kg2>operator*(unit<m1, s1, kg1> a, unit<m2, s2, kg2> b)
            {
                return a.value * b.value;
            }
            
            template<int m1, int s1, int kg1, int m2, int s2, int kg2>
            unit<m1-m2, s1-s2, kg1-kg2>operator/(unit<m1, s1, kg1> a, unit<m2, s2, kg2> b)
            {
                return a.value / b.value;
            }

            但是這個其實還不夠,我們還需要帶單位的值乘以或除以一個系數的代碼。為什么不能加減呢?因為不同單位的東西本來就不能加減。系數其實是可以描寫成unit<0, 0, 0>的,但是為了讓代碼更緊湊,于是多定義了下面的四個函數:

            template<int m, int s, int kg>
            unit<m, s, kg> operator*(double v, unit<m, s, kg> a)
            {
                return v * a.value;
            }
            
            template<int m, int s, int kg>
            unit<m, s, kg> operator*(unit<m, s, kg> a, double v)
            {
                return a.value * v;
            }
            
            template<int m, int s, int kg>
            unit<m, s, kg> operator/(double v, unit<m, s, kg> a)
            {
                return v / a.value;
            }
            
            template<int m, int s, int kg>
            unit<m, s, kg> operator/(unit<m, s, kg> a, double v)
            {
                return a.value / v;
            }

            我們已經用dependent type之間的變化來描述了帶單位的量的加減乘除的規則。這看起來好像很復雜,但是一旦我們加入了下面的新的函數,一切將變得簡單明了:

            constexpr unit<1, 0, 0> operator""_meter(double value)
            {
                return value;
            }
            
            constexpr unit<0, 1, 0> operator""_second(double value)
            {
                return value;
            }
            
            constexpr unit<0, 0, 1> operator""_kilogram(double value)
            {
                return value;
            }
            
            constexpr unit<1, -2,1> operator""_N(double value) // 牛不知道怎么寫-_-
            {
                return value;
            }
            
            constexpr unit<2, -2,1> operator""_J(double value) // 焦耳也不知道怎么寫-_-
            {
                return value;
            }

            然后我們就可以用來寫一些神奇的代碼了:

            auto m = 16_kilogram; // unit<0, 0, 1>(16)
            auto s = 3_meter; // unit<1, 0, 0>(3)
            auto t = 2_second; // unit<0, 1, 0>(2)
            auto a = s / (t*t); // unit<1, -2, 0>(3/4)
            auto F = m * a; // unit<1, -2, 1>(12)

            下面的代碼雖然也神奇,但因為違反了物理定律,所以C++編譯器決定不讓他編譯通過:

            auto W = F * s; // unit<2, -2, 1>(36)
            auto x = F + W; // bang!

            這樣你還怕你在物理引擎里面東西倒騰來倒騰去然后公式手抖寫錯了嗎?類似的錯誤是不可能發生的!除非系數被你弄錯了……如果沒有unit,要用原始的方法寫出來:

            double m = 16;
            double s = 3;
            double t = 2;
            double a = s / (t*t);
            double F = m * a;
            double W = F * s;
            double x = F + W; //????

            時間過得久了以后,根本不知道是什么意思了。所以為了解決這個問題,我們得用應用匈牙利命名法(這個不是那個臭名昭著的你們熟悉的傻逼(系統)匈牙利命名法)。我舉個例子:

            string dogName = "kula";
            Person person;
            person.name = dogName;

            這個代碼大家一看就知道不對對吧,這就是應用匈牙利命名法了。我們通過給名字一個單位——狗的——來讓person.name = dogName;這句話顯得很滑稽,從而避免低級錯誤的發生。上面的unit就更進一步了,把這個東西帶進了類型系統里面,就算寫出來不滑稽,編譯器都會告訴你,錯誤的東西就是錯誤的。

            然后大家可能會問,用unit這么寫程序的性能會不會大打折扣呀?如今已經是2013年了,靠譜的C++編譯器編譯出來的代碼,跟你直接用幾個double倒騰來倒騰去的代碼其實是一樣的。C++比起其他語言的抽象的好處就是,就算你要用來做高性能的程序,也不怕因為抽象而喪失性能。當然如果你使用了面向對象的技術,那就另當別論了。

            注,上面這段話我寫完之后貼到了粉絲群里面,然后九姑娘跟我講了很多量綱分析的故事,然后升級到航空領域的check list,最后講到了醫院把這一技術引進了之后有效地阻止了手術弄錯人等嚴重事故。那些特別靠譜的程序還得用C++來寫,譬如說洛克希德馬丁的戰斗機,NASA的衛星什么的。

            人的精力是有限的,需要一些錯誤規避來防止引進低級的錯誤或者負擔,保留精力解決最核心的問題。很多軟件都是這樣的。譬如說超容易配置的MSBuild、用起來巨爽無比的Visual Studio,出了問題反正用正版安裝程序點一下repair就可以恢復的windows,給我們帶來的好處就是——保留精力解決最核心的問題。編程語言也是如此,類型系統也是如此,人類發明出的所有東西,都是為了讓你可以把更多的精力放在更加核心的問題上,更少的精力放在周邊的問題上。

            但是類型到處都出現其實也會讓我們程序寫起來很煩的,所以現代的語言都有第二個功能,就是類型推導了。

            二、類型推導

            這里講的類型推導可不是Go語言那個半吊子的:=賦值操作符。真正的類型推導,就要跟C++的泛型lambda表達式、C#的linq語法糖,或者Haskell的函數一樣,要可以自己計算出模板的類型參數的位置或者內容,才能全方位的實現什么類型都不寫,都還能使用強類型和type rich programming帶來的好處。C++的lambda表達式上面已經看到了,所以還是從Haskell一個老掉牙的demo開始講起吧。

            今天,我們用Haskell來寫一個merge sort:

            merge [] [] = []
            merge [] xs = xs
            merge xs [] = xs
            merge (x:xs) (y:ys) = if x<y then x:(merge xs (y:ys)) else y:(merge (x:xs) ys)
            
            mergeSort [] = []
            mergeSort xs = merge (mergeSort a) (mergeSort b)
                where
                    len = length xs
                    a = take $ len `div` 2 $ xs
                    b = drop $ len - len `div` 2 $ xs

            我們可以很清楚的看出來,merge的類型是[a] –> [a] –> [a],mergeSort的類型是[a] –> [a]。到底編譯器是怎么計算出類型的呢?

            1. 首先,[]告訴我們,這是一個空列表,但是類型是什么不知道,所以他是forall a –> [a]。所以merge [] [] = []告訴我們,merge的類型至少是[a] –> [b] –> [c]。
            2. 其次,merge []  xs = xs告訴我們,merge的類型至少是[d] –> e –> e。這個類型跟[a]->[b]->[c]求一個交集就會得到merge的更準確的類型:[a] –> [b] –> [b]。
            3. 然后,merge xs [] = []告訴我們,merge的類型至少是f –> [g] –> f。這個類型跟[a] –> [b] –> [b]求一個交集就會得到merge的更準確的類型:[a] –> [a] –> [a]。
            4. 最后看到那個長長的式子,根據一番推導之后,會發現[a]->[a]->[a]就是我們要的最終類型了。
            5. 只要把相同的技術放在mergeSort上面,就可以得到mergeSort的類型是[a]->[a]了。

            當然對于Haskell這種Hindley-Milner類型系統來說,只要我們在代碼里面計算出所有類型的方程,然后一個一個去解,最后就可以收斂到一個最準確的類型上面了。倘若我們在迭代的時候發現收斂之后無解了,那這個程序就是錯的。這種簡單粗暴的方法很容易構造出一些只要人夠蛋定就很容易使用的語言,譬如說Haskell。

            Haskell看完就可以來看C#了。C#的linq真是個好東西啊,只要不把它看成SQL,那很多事情都可以表達的。譬如說是個人都知道的linq to object啦,后面還有linq to xmllinq to sqlreactive programming,甚至是parser combinator等等。一個典型的linq的程序是長這個樣子的:

            var w = 
                from x in xs
                from y in ys
                from z in zs
                select f(x, y, z);

             

            光看這個程序可能看不出什么來,因為xs、ys、zs和f這幾個單詞都是沒有意義的。但是linq的魅力正在這里。如果from和select就已經強行規定了xs、ys、zs和f的意思的話。那可擴展性就全沒有了。因此當我們看到一個這樣的程序的時候,其實可以是下面這幾種意思:

            W f(X x, Y y, Z z);
            
            var /*IEnumerable<W>*/w = 
                from /*X*/x in /*IEnumerable<X>*/xs
                from /*Y*/y in /*IEnumerable<Y>*/ys
                from /*Z*/z in /*IEnumerable<Z>*/zs
                select f(x, y, z);
            
            var /*IObservable<W>*/w = 
                from /*X*/x in /*IObservable<X>*/xs
                from /*Y*/y in /*IObservable<Y>*/ys
                from /*Z*/z in /*IObservable<Z>*/zs
                select f(x, y, z);
            
            var /*IParser<W>*/w = 
                from /*X*/x in /*IParser<X>*/xs
                from /*Y*/y in /*IParser<Y>*/ys
                from /*Z*/z in /*IParser<Z>*/zs
                select f(x, y, z);
            var /*IQueryable<W>*/w =
            from /*X*/x in /*IQueryable<X>*/xs
            from /*Y*/y in /*IQueryable<Y>*/ys
            from /*Z*/z in /*IQueryable<Z>*/zs
            select f(x, y, z);
            var /*?<W>*/w = 
                from /*X*/x in /*?<X>*/xs
                from /*Y*/y in /*?<Y>*/ys
                from /*Z*/z in /*?<Z>*/zs
                select f(x, y, z);

             

            相信大家已經看到了里面的pattern了。只要你有一個?<T>類型,而它又支持linq provider的話,你就可以把代碼寫成這樣了。

            不過我們知道,把程序寫成這樣并不是我們編程的目的,我們的目的是要讓程序寫得讓具有同樣知識背景的人可以很快就看懂。為什么要看懂?因為總有一天你會不維護這個程序的,這樣就可以讓另一個合格的人來繼續維護了。一個軟件是要做幾十年的,那些只有一兩年甚至只有半年生命周期的,只能叫垃圾了。

            那現在讓我們看一組有意義的linq代碼。首先是linq to object的,求一個數組里面出現最多的數字是哪個:

            var theNumber = (
                from n in numbers
                group by n into g
                select g.ToArray() into gs
                order by gs.Length descending
                select gs[0]
                ).First()

             

            其次是一個parser,這個parser用來得到一個函數調用表達式:

            IParser<FunctionCallExpression> Call()
            {
                return
                    from name in PrimitiveExpression()
                    from _1 in Text("(")
                    from arguments in
                        many(
                            Expression().
                            Text(",")
                        )
                    from _2 in Text(")")
                    select new FunctionCallExpression
                    {
                        Name = name,
                        Arguments = arguments.ToArray(),
                    };
            }

             

            我們可以看到,一旦linq表達式里面的元素都有了自己的名字,就不會跟上面的xyz的例子一樣莫名其妙了。那這兩個例子到底為什么要用linq呢?

            第一個例子很簡單,因為linq to object就是設計來解決這種問題的。

            第二個例子就比較復雜一點了,為什么好好地parser要寫成這樣呢?我們知道,parser時可能會parse失敗的。一個大的parser,里面的一些小部件失敗了,那么大parser就要回滾,token stream的當前光標也要回滾,等等需要類似的一系列的操作。如果我們始終都讓這些邏輯都貫穿在整個parser里面,那代碼根本不能看。于是我們可以寫一個linq provider,讓SelectMany函數來處理所有的回滾操作,然后把parser寫成上面這個樣子。上面這個parser的所有的in左邊是臨時變量,所有的in右邊剛好組成了一個EBNF文法:

            PrimitiveExpression "(" [Expression {"," Expression}] ")"

             

            最后的select語句告訴我們在所有小parser都parse成功之后,如何用parse得到的臨時變量來返回一顆語法樹。整個parsing的代碼就會非常的容易看懂。當然,前提是你必須要懂的EBNF。不過一個不懂EBNF的人,又如何能寫語法分析器呢。

            那這跟類型推導有什么關系呢?我們會發現上面的所有linq的例子里面,除了函數簽名以外,根本沒有出現任何類型的聲明。而且更重要的是,這些類型盡管沒有寫出來,但是每一個中間變量的類型都是自明的。當然這有一部分歸功于好的命名方法——也就是應用匈牙利命名法了。剩下的部分是跟業務邏輯相關的。譬如說,一個FunctionCallExpression所調用的函數當然也是一個Expression了。如果這是唯一的選擇,那為什么要寫出來呢?

            我們可以看到,正是因為有了類型推導,我們可以在寫出清晰的代碼的同時,還不需要花費那么多廢話來指定各種類型。程序員都是怕麻煩的,無論復雜的方法有多好,他總是會選擇簡單的(廢話,用復雜的那個不僅要加班修bug,還沒有漲工資。用簡單的那個,先讓他過了,bug留給下一任程序員去頭疼就好了——某web程序員如是說)。類型推導讓type rich programming的程序寫起來簡單了許多。所以我們一旦有了類型推導,就可以放心大膽的使用type rich programming了。

            三、大道理

            有了type rich programming,就可以讓編譯器幫我們檢查一些模式化的手都會犯的錯誤。讓我們重溫一下這篇文章前面的一段話:

            人的精力是有限的,需要一些錯誤規避來防止引進低級的錯誤或者負擔,保留精力解決最核心的問題。很多軟件都是這樣的。譬如說超容易配置的MSBuild、用起來巨爽無比的Visual Studio,出了問題反正用正版安裝程序點一下repair就可以恢復的windows,給我們帶來的好處就是——保留精力解決最核心的問題。編程語言也是如此,類型系統也是如此,人類發明出的所有東西,都是為了讓你可以把更多的精力放在更加核心的問題上,更少的精力放在周邊的問題上。

            這讓我想起了一個在微博上看到的故事:NASA的員工在推一輛裝了衛星的小車的時候,因為忘記看check list,沒有固定號衛星,結果衛星一推倒在了地上摔壞了,一下子沒了兩個億的美元。

            寫程序也一樣。一個代表力的變量,只能跟另一個代表力的變量相加,這就是check list。但是我們知道,每一個程序都相當復雜,check list需要檢查的地方遍布所有文件。那難道我們在code review的時候可以一行一行仔細看嗎?這是不可能的。正因為如此,我們要把程序寫成“讓編譯器可以檢查很多我們可能會手抖犯的錯誤”的形式,讓我們從這些瑣碎的事情里面解放出來。

            銀彈這種東西是不存在的,所以type rich programming能解決的事情就是防止手抖而犯錯誤。有一些錯誤不是手抖可以抖出來的,譬如說錯誤的設計,這并不是type rich programming能很好地處理的范圍。為了解決這些事情,我們就需要更多可以遵守的best practice了。

            當然,其中的一個將是DSL——domain specific language,領域特定語言了。敬請關注下一篇,《如何設計一門語言(十)——DSL與建模》。

            posted @ 2013-08-17 00:26 陳梓瀚(vczh) 閱讀(12892) | 評論 (16)編輯 收藏

            關于這個話題,其實在(六)里面已經討論了一半了。學過Haskell的都知道,這個世界上很多東西都可以用monad和comonad來把一些復雜的代碼給抽象成簡單的、一看就懂的形式。他們的區別,就像用js做一個復雜的帶著幾層循環的動畫,直接寫出來和用jquery的“回調”寫出來的代碼一樣。前者能看不能用,后者能用不能看。那有沒有什么又能用又能看的呢?我目前只能在Haskell、C#和F#里面看到。至于說為什么,當然是因為他們都支持了monad和comonad。只不過C#作為一門不把“用庫來改造語言”作為重要特征的語言,并沒打算讓你們能跟haskell和F#一樣,把東西抽象成monad,然后輕松的寫出來。C#只內置了yield return和async await這樣的東西。

            把“用庫來改造語言”作為重要特征的語言其實也不多,大家熟悉的也就只有lisp和C++,不熟悉的有F#。F#除了computation expression以外,還有一個type provider的功能。就是你可以在你的當前的程序里面,寫一小段代碼,通知編譯器在編譯你的代碼的時候執行以下(有點類似雞生蛋的問題但其實不是)。這段代碼可以生成新的代碼(而不是跟lisp一樣修改已有的代碼),然后給你剩下的那部分程序使用。例子我就不舉了,有興趣的大家看這里:http://msdn.microsoft.com/en-us/library/vstudio/hh361034.aspx。里面有一個例子講的是如何在F#里面創造一個強類型的正則表達式庫,而且并不像boost的spirit或者xpress那樣,正則表達式仍然使用字符串來寫的。這個正則表達式在編譯的時候就可以知道你有沒有弄錯東西了,不需要等到運行才知道。

            Haskell和F#分別嘗試了monad/comonad和computation expression,為的就是能用一種不會失控(lisp的macro就屬于會失控的那種)方法來讓用戶自己表達屬于自己的可以天然被continuation passing style變換處理的東西。在介紹C#的async await的強大能力之前,先來講一下Haskell和F#的做法。為什么按照這個程序呢,因為Haskell的monad表達能力最低,其次是F#,最后是C#的那個。當然C#并不打算讓你自己寫一個支持CPS變換的類型。作為補充,我將在這篇文章的最后,講一下我最近正在設計的一門語言,是如何把C#的yield return和async await都變成庫,而不是編譯器的功能的。

            下面我將拋棄所有跟學術有關的內容,只會留下跟實際開發有關系的東西。

            一、Haskell和Monad

            Haskell面臨的問題其實比較簡單,第一是因為Haskell的程序都不能有隱式狀態,第二是因為Haskell沒有語句只有表達式。這意味著你所有的控制流都必須用遞歸或者CPS來做。從這個角度上來講,Monad也算是CPS的一種應用了。于是我為了給大家解釋一下Monad是怎么運作的,決定來炒炒冷飯,說error code的故事。這個故事已經在(七)里面講了,但是今天用的是Haskell,別有一番異域風情。

            大家用C/C++的時候都覺得處理起error code是個很煩人的事情吧。我也不知道為什么那些人放著exception不用,對error code那么喜歡,直到有一天,我聽到有一個傻逼在微博上講:“error code的意思就是我可以不理他”。我終于明白了,這個人是一個真正的傻逼。不過Haskell還是很體恤這些人的,就跟耶穌一樣,凡是信他就可以的永生,傻逼也可以。可惜的是,傻逼是學不會Monad的,所以耶穌只是個傳說。

            由于Haskell沒有“引用參數”,所以所有的結果都必須出現在返回值里面。因此,倘若要在Haskell里面做error code,就得返回一個data。data就跟C語言的union一樣,區別是data是強類型的,而C的union一不小心就會傻逼了:

            data Unsure a = Sure a | Error string

            然后給一些必要的實現,首先是Functor:

            instance Functor Unsure where
                fmap f (Sure x) = Sure (f x)
                fmap f (Error e) = Error e

            剩下的就是Monad了:

            instance Monad Unsure where
                return = Sure
                fail = Error
                (Sure s) >>= f = f s
                (Error e) >>= f = Error e

            看起來也不多,加起來才八行,就完成了error code的聲明了。當然就這么看是看不出Monad的強大威力的,所以我們還需要一個代碼。譬如說,給一個數組包含了分數,然后把所有的分數都轉換成“牛逼”、“一般”和“傻逼”,重新構造成一個數組。一個真正的Haskell程序員,會把這個程序分解成兩半,第一半當然是一個把分數轉成數字的東西:

            // Tag :: integer -> Unsure string
            Tag f = 
                if f < 0 then Error "分數必須在0-100之間" else
                if f<60 then Sure "傻逼" else
                if f<90 then Sure "一般" else
                if f<=100 then Sure "牛逼" else
                Error "分數必須在0-100之間"

            后面就是一個循環了:

            // TagAll :: [integer] -> Unsure [string]
            
            TagAll [] = []
            TagAll (x:xs) = do
                first <- Tag x
                remains <- TagAll xs
                return first:remains

            TagAll是一個循環,把輸入的東西每一個都用Tag過一遍。如果有一次Tag返回失敗了,整個TagAll函數都會失敗,然后返回錯誤。如果全部成功了,那么TagAll函數會返回整個處理后的數組。

            當然一個循環寫成了非尾遞歸不是一個真正的Haskell程序員會做的事情,真正的Haskell程序員會把事情做成這樣(把>>=展開之后你們可能會覺得這個函數不是尾遞歸,但是因為Haskell是call by need的,所以實際上會成為一個尾遞歸的函數):

            // TagAll :: [integer] -> Unsure [string]
            TagAll xs = reverse $ TagAll_ xs [] where
                TagAll [] ys = Sure ys
                TagAll (x:xs) ys = do
                    y <- Tag x
                    TagAll xs (y:ys)

            為什么代碼里面一句“檢查Tag函數的返回值”的代碼都沒有呢?這就是Haskell的Monad的表達能力的威力所在了。Monad的使用由do關鍵字開始,然后這個表達式可以被這么定義:

            MonadExp
                ::= "do" FragmentNotNull
            
            FragmentNotNull
                ::= [Pattern "<-"] Expression EOL FragmentNull
            
            FragmentNull
                ::= FragmentNotNull
                ::= ε

            意思就是說,do后面一定要有“東西”,然后這個“東西”是這么組成的:
            1、第一樣要是一個a<-e這樣的東西。如果你不想給返回值命名,就省略“a<-”這部分
            2、然后重復

            這表達的是這樣的一個意思:
            1、先做e,然后把結果保存進a
            2、然后做下面的事情

            看到了沒有,“然后做下面的事情”是一個典型的continuation passing style的表達方法。但是我們可以看到,在例子里面所有的e都是Unsure T類型的,而a相應的必須為T。那到底是誰做了這個轉化呢?

            聰明的,哦不,正常的讀者一眼就能看出來,“<-”就是調用了我們之前在上面實現的一個叫做“>>=”的函數了。我們首先把“e”和“然后要做的事情”這兩個參數傳進了>>=,然后>>=去解讀e,得到a,把a當成“然后要做的事情”的參數調用了一下。如果e解讀失敗的到了錯誤,“然后要做的事情”自然就不做了,于是整個函數就返回錯誤了。

            Haskell一下就來尾遞歸還是略微復雜了點,我們來寫一個簡單點的例子,寫一個函數判斷一個人的三科成績里面,有多少科是牛逼的:

            // Count牛逼 :: integer -> integer -> integer –> Unsure integer
            
            Count牛逼 chinese math english = do
                a <- Tag chinese
                b <- Tag math
                c <- Tag english
                return length [x | x <- [a, b, c], x == "牛逼"]

            根據上文的描述,我們已經知道,這個函數實際上會被處理成:

            // Count牛逼 :: integer -> integer -> integer –> Unsure integer
            
            Count牛逼 chinese math english
                Tag chinese >>= \a->
                Tag math >>= \b->
                Tag english >>= \c->
                return length [x | x <- [a, b, c], x == "牛逼"]

            >>=函數的定義是

            instance Monad Unsure where
                return = Sure
                fail = Error
                (Sure s) >>= f = f s
                (Error e) >>= f = Error e

            這是一個運行時的pattern matching。一個對參數帶pattern matching的函數用Haskell的case of寫出來是很難看的,所以Haskell給了這么個語法糖。但這個時候我們要把>>=函數展開在我們的“Count牛逼”函數里面,就得老老實實地用case of了:

            // Count牛逼 :: integer -> integer -> integer –> Unsure integer
            
            Count牛逼 chinese math english
                case Tag chinese of {
                    Sure a -> case Tag math of {
                        Sure b -> case Tag english of {
                            Sure c -> Sure $ length [x | x <- [a, b, c], x == "牛逼"]
                            Error e -> Error e
                        }
                        Error e -> Error e
                    }
                    Error e -> Error e
                }

            是不是又回到了我們在C語言里面被迫做的,還有C++不喜歡用exception的人(包含一些覺得error code可以忽略的傻逼)做的,到處檢查函數返回值的事情了?我覺得只要是一個正常人,都會選擇這種寫法的:

            // Count牛逼 :: integer -> integer -> integer –> Unsure integer
            
            Count牛逼 chinese math english
                Tag chinese >>= \a->
                Tag math >>= \b->
                Tag english >>= \c->
                return length [x | x <- [a, b, c], x == "牛逼"]

            于是我們用Haskell的Monad,活生生的把“每次都檢查函數返回值”的代碼壓縮到了Monad里面,然后就可以把代碼寫成try-catch那樣的東西了。error code跟exception本來就是一樣的嘛,只是一個寫起來復雜所以培養了很多覺得錯誤可以忽略的傻逼,而一個只需要稍微訓練一下就可以把代碼寫的很簡單罷了。

            不過Haskell沒有變量,那些傻逼們可能會反駁:C/C++比Haskell復雜多了,你怎么知道exception就一定沒問題呢?這個時候,我們就可以看F#的computation expression了。

            二、F#和computation expression

            F#雖然被設計成了一門函數式語言,但是其骨子里還是跟C#一樣帶狀態的,而且編譯成MSIL代碼之后,可以直接讓F#和C#互相調用。一個真正的Windows程序員,從來不會拘泥于讓一個工程只用一個語言來寫,而是不同的大模塊,用其適合的最好的語言。微軟把所有的東西都設計成可以強類型地互操作的,所以在Windows上面從來不存在什么“如果我用A語言寫了,B就用不了”的這些事情。這是跟Linux的一個巨大的區別。Linux是沒有強類型的互操作的(字符串信仰者們再見),而Windows有。什么,Windows不能用來做Server?那Windows Azure怎么做的,bing怎么做的。什么,只有微軟才知道怎么正確使用Windows Server?你們喜歡玩的EVE游戲的服務器是怎么做的呢?

            在這里順便黑一下gcc。錢(區別于財產)對于一個程序員是很重要的。VC++和clang/LLVM都是領著工資寫的,gcc不知道是誰投資的(這也就意味著寫得好也漲不了工資)。而且我們也都知道,gcc在windows上編譯的慢出來的代碼還不如VC++,gcc在linux上編譯的慢還不如clang,在mac/ios上就不說了,下一個版本的xcode根本沒有什么gcc了。理想主義者們醒醒,gcc再見。

            為什么F#有循環?答案當然是因為F#有變量了。一個沒有變量的語言是寫不出循環退出條件的,只能寫出遞歸退出條件。有了循環的話,就會有各種各樣的東西,那Monad這個東西就不能很好地給“東西”建模了。于是F#本著友好的精神,既然大家都那么喜歡Monad,那他做出一個computation expression,學起來肯定就很容易了。

            于是在F#下面,那個TagAll終于可以讀入一個真正的列表,寫出一個真正的循環了:

            let TagAll xs = unsure
            {
                let r = Array.create xs.length ""
                for i in 0 .. xs.length-1 do
                    let! tag = Tag xs.[i]
                    r.[i]<-tag
                return r
            }

            注意那個let!,其實就是Haskell里面的<-。只是因為這些東西放在了循環里,那么那個“Monad”表達出來就沒有Haskell的Monad那么純粹了。為了解決這個問題,F#引入了computation expression。所以為了讓那個unsure和let!起作用,就得有下面的代碼,做一個名字叫做unsure的computation expression:

            type UnsureBuilder() =
                member this.Bind(m, f) = match m with
                    | Sure a -> f a
                    | Error s -> Error s
                member this.For(xs, body) =unsure
                {
                     match xs with
                    | [] -> Sure ()
                    | x::xs -> 
                        let! r = Tag x
                        body r
                        return this.For xs body
                }
                .... // 還有很多別的東西
            let unsure = new UnsureBuilder()

            所以說帶有副作用的語言寫出來的代碼又長,不帶副作用的語言寫出來的代碼又難懂,這之間很難取得一個平衡。

            如果輸入的分數數組里面有一個不在0到100的范圍內,那么for循環里面的“let! tag = Tag xs.[i]”這句話就會引發一個錯誤,導致TagAll函數失敗。這是怎么做到的?

            首先,Tag引發的錯誤是在for循環里面,也就是說,實際運行的時候是調用UnsuerBuilder類型的unsure.For函數來執行這個循環的。For函數內部使用“let! r = Tag x”,這個時候如果失敗,那么let!調用的Bind函數就會返回Error s。于是unsure.Combine函數判斷第一個語句失敗了,那么接下來的語句“body r ; return this.For xs body”也就不執行了,直接返回錯誤。這個時候For函數的遞歸終止條件就產生作用了,由一層層的return(F#自帶尾遞歸優化,所以那個For函數最終會被編譯成一個循環)往外傳遞,導致最外層的For循環以Error返回值結束。TagAll里面的unsure,Combine函數看到for循環完蛋了,于是return r也不執行了,返回錯誤。

            這個過程跟Haskell的那個版本做的事情完全是一樣的,只是由于F#多了很多語句,所以Monad展開成computation expression之后,表面上看起來就會復雜很多。如果明白Haskell的Monad在干什么事情的話,F#的computation expression也是很容易就學會的。

            當然,覺得“error code可以忽略”的傻逼是沒有可能的。

            三、C#的yield return和async await

            如果大家已經明白了Haskell的>>=和F#的Bind(其實也是let!)就是一回事的話,而且也明白了我上面講的如何把do和<-變成>>=的方法的話,大家應該對CPS在實際應用的樣子心里有數了。不過,這種理解的方法實際上是相當有限的。為什么呢?讓我們來看C#的兩個函數:

            IEnumerable<T> Concat(this IEnumerable<T> a, IEnumerable<T> b)
            {
                foreach(var x in a)
                    yield return x;
                foreach(var x in b)
                    yield return x;
            }

            上面那個是關于yield return和IEnumerable<T>的例子,講的是Linq的Concat函數是怎么實現的。下面還有一個async await和Task<T>的例子:

            async Task<T[]> SequencialExecute(this Task<T>[] tasks)
            {
                var ts = new T[tasks.Length];
                for(int i=0;i<tasks.Length;i++)
                    ts[i]=await tasks[i];
                return ts;
            }

            這個函數講的是,如果你有一堆Task<T>,如何構造出一個內容來自于異步地挨個執行tasks里面的每個Task<T>的Task<T[]>的方法。

            大家可能會注意到,C#的yield return和await的“味道”,就跟Haskell的<-和>>=、F#的Bind和let!一樣。在處理這種語言級別的事情的時候,千萬不要去管代碼它實際上在干什么,這其實是次要的。最重要的是形式。什么是形式呢?也就是說,同樣一個任務,是如何被不同的方法表達出來的。上面說的“味道”就都在“表達”的這個事情上面了。

            這里我就要提一個問題了。

            1. Haskell有Monad,所以我們可以給自己定義的類型實現一個Monad,從而讓我們的類型可以用do和<-來操作。
            2. F#有computation expression,所以我們可以給自己定義的類型實現一個computation expression,從而讓我們的類型可以用let!來操作。
            3. C#有【什么】,所以我們可以給自己定義的類型實現一個【什么】,從而讓我們的類型可以用【什么】來操作?

            熟悉C#的人可能很快就說出來了,答案是Linq、Linq Provider和from in了。這篇《Monadic Parser Combinator using C# 3.0》http://blogs.msdn.com/b/lukeh/archive/2007/08/19/monadic-parser-combinators-using-c-3-0.aspx 介紹了一個如何把語法分析器(也就是parser)給寫成monad,并且用Linq的from in來表達的方法。

            大家可能一下子不明白什么意思。Linq Provider和Monad是這么對應的:

            1. fmap對應于Select
            2. >>=對應于SelectMany
            3. >>= + return也對應與Select(回憶一下Monad這個代數結構的幾個定理,就有這么一條)

            然后諸如這樣的Haskell代碼:

            // Count牛逼 :: integer -> integer -> integer –> Unsure integer
            
            Count牛逼 chinese math english = do
                a <- Tag chinese
                b <- Tag math
                c <- Tag english
                return length [x | x <- [a, b, c], x == "牛逼"]

            就可以表達成:

            Unsure<int> Count牛逼(int chinese, int math, int english)
            {
                return
                    from a in Tag(chinese)
                    from b in Tag(math)
                    from c in Tag(english)
                    return new int[]{a, b, c}.Where(x=>x=="牛逼").Count();
            }

            不過Linq的這個表達方法跟yield return和async await一比,就有一種Monad和computation expression的感覺了。Monad只能一味的遞歸一個一個往下寫,而computation expression則還能加上分支循環異常處理什么的。C#的from in也是一樣,沒辦法表達循環異常處理等內容。

            于是上面提到的那個問題

            C#有【什么】,所以我們可以給自己定義的類型實現一個【什么】,從而讓我們的類型可以用【什么】來操作?

            其實并沒有回答完整。我們可以換一個角度來體味。假設IEnumerable<T>和Task<T>都是我們自己寫的,而不是.net framework里面的內容,那么C#究竟要加上一個什么樣的(類似于Linq Provider的)功能,從而讓我們可以寫出接近yield return和async await的效果的代碼呢?如果大家對我的那篇《時隔多年我又再一次體驗了一把跟大神聊天的感覺》還有點印象的話,其實我當時也對我自己提出了這么個問題。

            我那個時候一直覺得,F#的computation expression才是正確的方向,但是我怎么搞都搞不出來,所以我自己就有點動搖了。于是我跑去問了Don Syme,他很斬釘截鐵的告訴我說,computation expression是做不到那個事情的,但是需要怎么做他也沒想過,讓我自己research。后來我就得到了一個結論。

            四、Koncept(我正在設計的語言)的yield return和async await(問題)

            Koncept主要的特征是concept mapping和interface。這兩種東西的關系就像函數和lambda表達式、instance和class一樣,是定義和閉包的關系,所以相處起來特別自然。首先我讓函數只能輸入一個參數,不過這個參數可以是一個tuple,于是f(a, b, c)實際上是f.Invoke(Tuple.Create(a, b, c))的語法糖。然后所有的overloading都用類似C++的偏特化來做,于是C++11的不定模板參數(variadic template argument)在我這里就成為一個“推論”了,根本不是什么需要特殊支持就自然擁有的東西。這也是concept mapping的常用手法。最后一個跟普通語言巨大的變化是我刪掉了class,只留下interface。反正你們寫lambda表達時也不會給每個閉包命名字(沒有C++11的C++除外),那為什么寫interface就得給每一個閉包(class)命名字呢?所以我給刪去了。剩下的就是我用類似mixin的機制可以把函數和interface什么的給mixin到普通的類型里面去,這樣你也可以實現class的東西,就是寫起特別來麻煩,于是我在語法上就鼓勵你不要暴露class,改為全部暴露function、concept和interface。

            不過這些都不是重點,因為除了這些差異以外,其他的還是有濃郁的C#精神在里面的,所以下面在講Koncept的CPS變換的時候,我還是把它寫成C#的樣子,Koncept長什么樣子以后我再告訴你們,因為Koncept的大部分設計都跟CPS變換是沒關系的。

            回歸正題。之前我考慮了許久,覺得F#的computation expression又特別像是一個正確的解答,但是我怎么樣都找不到一個可以把它加入Koncept地方法。這個問題我從NativeX(這里、這里、這里這里)的時候就一直在想了,中間兜了一個大圈,整個就是試圖山寨F#結果失敗的過程。為什么F#的computation expression模型不能用呢,歸根結底是因為,F#的循環沒有break和continue。C#的跳轉是自由的,不僅有break和continue,你還可以從循環里面return,甚至goto。因此一個for循環無論如何都表達不成F#的那個函數:M<U> For(IEnumerable<T> container, Func<T, M<U>> body);。break、continue、return和goto沒辦法表達在類型上。

            偉大的先知Eric Meijer告訴我們:“一個函數的類型表達了關于函數的業務的一切”。為什么我們還要寫函數體,是因為編譯器還沒有聰明到看著那個類型就可以幫我們把代碼填充完整。所以其實當初看著F#的computation expression的For的定義的時候,是因為我腦筋短路,沒有想起Eric Meijer的這句話,導致我浪費了幾個月時間。當然我到了后面也漸漸察覺到了這個事情,產生了動搖,自己卻無法確定,所以去問了Don Syme。于是,我就得到了關于這個問題的結論的一半:在C#(其實Koncept也是)支持用戶可以自由添加的CPS變換(譬如說用戶添加IEnumerable<T>的時候添加yield return和yield break,用戶添加Task<T>的時候添加await和return)的話,使用CPS變換的那段代碼,必須用控制流圖(control flow graph)處理完之后生成一個狀態機來做,而不能跟Haskell和F#一樣拆成一個一個的小lambda表達式。

            其實C#的yield return和async await,從一開始就是編譯成狀態機的。只是C#沒有開放那個功能,所以我一直以為這并不是必須的。想來微軟里面做語言的那幫牛逼的人還是有牛逼的道理的,一下子就可以找到問題的正確方向,跟搞go的二流語言專家(盡管他也牛逼但是跟語言一點關系也沒有)是完全不同的。連Mozilla的Rust的設計都比go強一百倍。

            那另一半的問題是什么呢?為了把問題看得更加清楚,我們來看兩個長得很像的yield return和async await的例子。為了把本質的問題暴露出來,我決定修改yield return的語法:

            1. 首先把yield return修改成yield
            2. 其次吧yield break修改成return
            3. 然后再給函數打上一個叫做seq的東西,跟async對稱,就當他是個關鍵字
            4. 給所有CPS operator加上一個感嘆號,讓他變得更清楚(這里有yield、await和return)。為什么return也要加上感嘆號呢?因為如果我們吧seq和aysnc摘掉的話,我們會發現return的類型是不匹配的。所以這不是一個真的return。

            然后就可以來描述一個類似Linq的TakeWhile的事情了:

            seq IEnumerable<T> TakeWhile(this IEnumerable<T> source, Predicate<T> predicate)
            {
                foreach(var x in source)
                {
                    if(!predicate(x))
                        return!;
                    yield! x
                }
            }
            
            async Task<T[]> TakeWhile(this Task<T>[] source, Predicate<T> predicate)
            {
                List<T> result=new List<T>();
                foreach(var t in source)
                {
                    var x = await! t;
                    if(!predicate(x))
                        return! result.ToArray();
                    result.Add(x);
                }
                return! result.ToArray();
            }
            于是問題就很清楚了。如果我們想讓用戶自己通過類庫的方法來實現這些東西,那么yield和await肯定是兩個函數,因為這是C#里面唯一可以用來寫代碼的東西,就算看起來再奇怪,也不可能是別的。
            1. seq和async到底是什么?
            2. seq下面的yield和return的類型分別是什么?
            3. async下面的await和return的類型分別是什么?

            其實這里還有一個謎團。其實seq返回的東西應該是一個IEnumerator<T>,只是因為C#覺得IEnumerable<T>是更好地,所以你兩個都可以返回。那么,是什么機制使得,函數可以構造出一個IEnumerable<T>,而整個狀態機是在IEnumerator<T>的MoveNext函數里面驅動的呢?而async和Task<T>就沒有這種情況了。

            首先解答第一個問題。因為yield、return和await都是函數,是函數就得有個namespace,那我們可以拿seq和async做namespace。所以seq和async,設計成兩個static class也是沒有問題的。

            其次,seq的yield和return修改了某個IEnumerator<T>的狀態,而async的await和return修改了某個Task<T>的狀態。而seq和async的返回值分別是IEnumerable<T>和Task<T>。因此對于一個CPS變換來說,一共需要兩個類型,第一個是返回值,第二個是實際運行狀態機的類。

            第三,CPS變換還需要有一個啟動函數。IEnumerator<T>的第一次MoveNext調用了那個啟動函數。而Task<T>的Start調用了那個啟動函數。啟動函數自己維護著所有狀態機的內容,而狀態機本身是CPS operator們看不見的。為什么呢?因為一個狀態機也是一個類,這些狀態機類是沒有任何公共的contract的,也就是說無法抽象他們。因此CPS operator必須不能知道狀態機類。

            而且yield、return和await都叫CPS operator,那么他們不管是什么類型,本身肯定看起來像一個CPS的函數。之前已經講過了,CPS函數就是把普通函數的返回值去掉,轉而添加一個lambda表達式,用來代表“拿到返回之后的下一步計算”。

            因此總的來說,我們拿到了這四個方程,就可以得出一個解了。解可以有很多,我們選擇最簡單的部分。

            那現在就開始來解答上面兩個TakeWhile最終會被編譯成什么東西了。

            五、Koncept(我正在設計的語言)的yield return和async await(seq答案)

            首先來看seq和yield的部分。上面講到了,yield和return都是在修改某個IEnumerator<T>的狀態,但是編譯器自己肯定不能知道一個合適的IEnumerator<T>是如何被創建出來的。所以這個類型必須由用戶來創建。而為了第一次調用yield的時候就已經有IEnumerator<T>可以用,所以CPS的啟動函數就必須看得到那個IEnumerator<T>。但是CPS的啟動函數又不可能去創建他,所以,這個IEnumerator<T>對象肯定是一個continuation的參數了。

            看,其實寫程序都是在做推理的。盡管我們現在還不知道整個CPS要怎么運作,但是隨著這些線索,我們就可以先把類型搞出來。搞出了類型之后,就可以來填代碼了。

            1. 對于yield,yield接受了一個T,沒有返回值。一個沒有返回值的函數的continuation是什么呢?當然就是一個沒有參數的函數了。
            2. return則連輸入都沒有。
            3. 而且yield和return都需要看到IEnumerator<T>。所以他們肯定有一個參數包含這個東西。

            那么這三個函數的類型就都確定下來了:

            public static class seq
            {
                public static IEnumerator<T> CreateCps<T>(Action<seq_Enumerator<T>>);
                public static void yield<T>(seq_Enumerator<T> state, T value, Action continuation);
                public static void exit<T>(seq_Enumerator<T> state /*沒有輸入*/ /*exit代表return,函數結束的意思就是不會有一個continuation*/);
            }

            什么是seq_Enumerator<T>呢?當然是我們那個“某個IEnumerator<T>”的真是類型了。

            于是看著類型,唯一可能的有意義又簡單的實現如下:

            public class seq_Enumerable<T> : IEnumerable<T>
            {
                public Action<seq_Enumerator<T>> startContinuation;
            
                public IEnumerator<T> CreateEnumerator()
                {
                    return new seq_Enumerator<T>
                    {
                        startContinuation=this.startContinuation)
                    };
                }
            }
            
            public class seq_Enumerator<T> : IEnumerator<T>
            {
                public T current;
                bool available;
                Action<seq_Enumerator<T>> startContinuation;
                Action continuation;
            
                public T Current
                {
                    get
                    {
                        return this.current;
                    }
                }
            
                public bool MoveNext()
                {
                    this.available=false;
                    if(this.continuation==null)
                    {
                        this.startContinuation(this);
                    }
                    else
                    {
                        this.continuation();
                    }
                    return this.available;
                }
            }
            
            public static class seq
            {
                public static IEnumerable<T> CreateCps<T>(Action<seq_Enumerator<T>> startContinuation)
                {
                    return new seq_Enumerable
                    {
                        startContinuation=startContinuation
                    };
                }
            
                public static void yield<T>(seq_Enumeartor<T> state, T value, Action continuation)
                {
                    state.current=value;
                    state.available=true;
                    state.continuation=continuation;
                }
            
                public static void exit<T>(seq_Enumeartor<T> state)
                {
                }
            }

            那么那個TakeWhile函數最終會變成:

            public class _TakeWhile<T>
            {
                seq_Enumerator<T> _controller;
                Action _output_continuation_0= this.RunStateMachine;
                int _state;
                IEnumerable<T> _source;
            
                IEnumerator<T> _source_enumerator;
                Predicate<T> _predicate;
                T x;
            
                public void RunStateMachine()
                {
                    while(true)
                    {
                        switch(this.state)
                        {
                        case 0:
                            {
                                this._source_enumerator = this._source.CreateEnumerator();
                                this._state=1;
                            }
                            break;
                        case 1:
                            {
                                if(this._state_enumerator.MoveNext())
                                {
                                    this.x=this._state_enumerator.Current;
                                    if(this._predicate(this.x))
                                    {
                                        this._state=2;
                                        var input=this.x;
                                        seq.yield(this._controller. input, this._output_continuation_0);
                                        return;
                                    }
                                    else
                                    {
                                        seq.exit(this._controller);
                                    }
                                }
                                else
                                {
                                    state._state=3;
                                }
                            }
                            break;
                        case 2:
                            {
                                this.state=1;
                            }
                            break;
                        case 3:
                            {
                                seq.exit(this._controller);
                            }
                            break;
                        }
                    }
                }
            }

            但是TakeWhile這個函數是真實存在的,所以他也要被改寫:

            IEnumerable<T> TakeWhile(this IEnumerable<T> source, Predicate<T> predicate)
            {
                return seq.CreateCps(controller=>
                {
                    var sm = new _Where<T>
                    {
                        _controller=controller,
                        _source=source,
                        _predicate=predicate,
                    };
            
                    sm.RunStateMachine();
                });
            }

            最終生成的TakeWhile會調用哪個CreateCps函數,然后把原來的函數體經過CFG的處理之后,得到一個狀態機。在狀態機內所有調用CPS operator的地方(就是yield!和return!),都把“接下來的事情”當成一個參數,連同那個原本寫上去的CPS operator的參數,還有controller(在這里是seq_Enumeartor<T>)一起傳遞過去。而return是帶有特殊的寓意的,所以它調用一次exit之后,就沒有“然后——也就是continuation”了。

            現在回過頭來看seq類型的聲明

            public static class seq
            {
                public static IEnumerator<T> CreateCps<T>(Action<seq_Enumerator<T>>);
                public static void yield<T>(seq_Enumerator<T> state, T value, Action continuation);
                public static void exit<T>(seq_Enumerator<T> state /*沒有輸入*/ /*exit代表return,函數結束的意思就是不會有一個continuation*/);
            }

            其實想一想,CPS的自然屬性決定了,基本上就只能這么定義它們的類型。而他們的類型唯一定義了一個最簡單有效的函數體。再次感嘆一下,寫程序就跟在做推理完全是一摸一樣的

            六、Koncept(我正在設計的語言)的yield return和async await(async答案)

            因為CPS operator都是一樣的,所以在這里我給出async類型的聲明,然后假設Task<T>的樣子長的就跟C#的System.Tasks.Task<T>一摸一樣,看看大家能不能得到async下面的幾個函數的實現,以及上面那個針對Task<T>的TakeWhile函數最終會被編譯成什么:

            public static class async
            {
                public static Task<T> CreateCps<T>(Action<FuturePromiseTask<T>> startContinuation);
                {
                    /*請自行填補*/
                }
            
                public static void await<T>(FuturePromiseTask<T> task, Task<T> source, Action<T> continuation);
                {
                    /*請自行填補*/
                }
            
                public static void exit<T>(FuturePromiseTask<T> task, T source); /*在這里async的return是有參數的,所以跟seq的exit不一樣*/
                {
                    /*請自行填補*/
                }
            }
            
            public class FuturePromiseTask<T> : Task<T>
            {
                /*請自行填補*/
            }
            posted @ 2013-07-26 19:12 陳梓瀚(vczh) 閱讀(14697) | 評論 (15)編輯 收藏

            人們都很喜歡討論閉包這個概念。其實這個概念對于寫代碼來講一點用都沒有,寫代碼只需要掌握好lambda表達式和class+interface的語義就行了?;旧现挥性趯懢幾g器和虛擬機的時候才需要管什么是閉包。不過因為系列文章主題的緣故,在這里我就跟大家講一下閉包是什么東西。在理解閉包之前,我們得先理解一些常見的argument passing和symbol resolving的規則。

            首先第一個就是call by value了。這個規則我們大家都很熟悉,因為流行的語言都是這么做的。大家還記得剛開始學編程的時候,書上總是有一道題目,說的是:

            void Swap(int a, int b)
            {
                int t = a;
                a = b;
                b = t;
            }
            
            int main()
            {
                int a=0;
                int b=1;
                Swap(a, b);
                printf("%d, %d", a, b);
            }

             

            然后問程序會輸出什么。當然我們現在都知道,a和b仍然是0和1,沒有受到變化。這就是call by value。如果我們修改一下規則,讓參數總是通過引用傳遞進來,因此Swap會導致main函數最后會輸出1和0的話,那這個就是call by reference了。

            除此之外,一個不太常見的例子就是call by need了。call by need這個東西在某些著名的實用的函數式語言(譬如Haskell)是一個重要的規則,說的就是如果一個參數沒被用上,那傳進去的時候就不會執行。聽起來好像有點玄,我仍然用C語言來舉個例子。

            int Add(int a, int b)
            {
                return a + b;
            }
            
            int Choose(bool first, int a, int b)
            {
                return first ? a : b;
            }
            
            int main()
            {
                int r = Choose(false, Add(1, 2), Add(3, 4));
                printf("%d", r);
            }

             

            這個程序Add會被調用多少次呢?大家都知道是兩次。但是在Haskell里面這么寫的話,就只會被調用一次。為什么呢?因為Choose的第一個參數是false,所以函數的返回值只依賴與b,而不依賴與a。所以在main函數里面它感覺到了這一點,于是只算Add(3, 4),不算Add(1, 2)。不過大家別以為這是因為編譯器優化的時候內聯了這個函數才這么干的,Haskell的這個機制是在運行時起作用的。所以如果我們寫了個快速排序的算法,然后把一個數組排序后只輸出第一個數字,那么整個程序是O(n)時間復雜度的。因為快速排序的average case在把第一個元素確定下來的時候,只花了O(n)的時間。再加上整個程序只輸出第一個數字,所以后面的他就不算了,于是整個程序也是O(n)。

            于是大家知道call by name、call by reference和call by need了?,F在來給大家講一個call by name的神奇的規則。這個規則神奇到,我覺得根本沒辦法駕馭它來寫出一個正確的程序。我來舉個例子:

            int Set(int a, int b, int c, int d)
            {
                a += b;
                a += c;
                a += d;
            }
            
            int main()
            {
                int i = 0;
                int x[3] = {1, 2, 3};
                Set(x[i++], 10, 100, 1000);
                printf("%d, %d, %d, %d", x[0], x[1], x[2], i);
            }

             

            學過C語言的都知道這個程序其實什么都沒做。如果把C語言的call by value改成了call by reference的話,那么x和i的值分別是{1111, 2, 3}和1。但是我們知道,人類的想象力是很豐富的,于是發明了一種叫做call by name的規則。call by name也是call by reference的,但是區別在于你每一次使用一個參數的時候,程序都會把計算這個參數的表達式執行一遍。因此,如果把C語言的call by value換成call by name,那么上面的程序做的事情實際上就是:

            x[i++] += 10;
            x[i++] += 100;
            x[i++] += 1000;

             

            程序執行完之后x和i的值就是{11, 102, 1003}和3了。

            很神奇對吧,稍微不注意就會中招,是個大坑,基本沒法用對吧。那你們還整天用C語言的宏來代替函數干什么呢。我依稀記得Ada有網友指出這是Algol 60)還是什么語言就是用這個規則的,印象比較模糊。

            講完了argument passing的事情,在理解lambda表達式之前,我們還需要知道兩個流行的symbol resolving的規則。所謂的symbol resolving講的就是解決程序在看到一個名字的時候,如何知道這個名字到底指向的是誰的問題。于是我又可以舉一個簡單粗暴的例子了:

            Action<int> SetX()
            {
                int x = 0;
                return (int n)=>
                {
                    x = n;
                };
            }
            
            void Main()
            {
                int x = 10;
                var setX = SetX();
                setX(20);
                Console.WriteLine(x);
            }

             

            弱智都知道這個程序其實什么都沒做,就輸出10。這是因為C#用的symbol resolving地方法是lexical scoping。對于SetX里面那個lambda表達式來講,那個x是SetX的x而不是Main的x,因為lexical scoping的含義就是,在定義的地方向上查找名字。那為什么不能在運行的時候向上查找名字從而讓SetX里面的lambda表達式實際上訪問的是Main函數里面的x呢?其實是有人這么干的。這種做法叫dynamic scoping。我們知道,著名的javascript語言的eval函數,字符串參數里面的所有名字就是在運行的時候查找的。

            =======================我是背景知識的分割線=======================

            想必大家都覺得,如果一個語言的lambda表達式在定義和執行的時候采用的是lexical scoping和call by value那該有多好呀。流行的語言都是這么做的。就算規定到這么細,那還是有一個分歧。到底一個lambda表達式抓下來的外面的符號是只讀的還是可讀寫的呢?python告訴我們,這是只讀的。C#和javascript告訴我們,這是可讀寫的。C++告訴我們,你們自己來決定每一個符號的規則。作為一個對語言了解得很深刻,知道自己每一行代碼到底在做什么,而且還很有自制力的程序員來說,我還是比較喜歡C#那種做法。因為其實C++就算你把一個值抓了下來,大部分情況下還是不能優化的,那何苦每個變量都要我自己說明我到底是想只讀呢,還是要讀寫都可以呢?函數體我怎么用這個變量不是已經很清楚的表達出來了嘛。

            那說到底閉包是什么呢?閉包其實就是那個被lambda表達式抓下來的“上下文”加上函數本身了。像上面的SetX函數里面的lambda表達式的閉包,就是x變量。一個語言有了帶閉包的lambda表達式,意味著什么呢?我下面給大家展示一小段代碼。現在要從動態類型的的lambda表達式開始講,就湊合著用那個無聊的javascript吧:

            function pair(a, b) {
                return function(c) {
                    return c(a, b);
                };
            }
            
            function first(a, b) {
                return a;
            }
            
            function second(a, b) {
                return b;
            }
            
            var p = pair(1, pair(2, 3));
            var a = p(first);
            var b = p(second)(first);
            var c = p(second)(second);
            print(a, b, c);

             

            這個程序的a、b和c到底是什么值呢?當然就算看不懂這個程序的人也可以很快猜出來他們是1、2和3了,因為變量名實在是定義的太清楚了。那么程序的運行過程到底是怎么樣的呢?大家可以看到這個程序的任何一個值在創建之后都沒有被第二次賦值過,于是這種程序就是沒有副作用的,那就代表其實在這里call by value和call by need是沒有區別的。call by need意味著函數的參數的求值順序也是無所謂的。在這種情況下,程序就變得跟數學公式一樣,可以推導了。那我們現在就來推導一下:

            var p = pair(1, pair(2, 3));
            var a = p(first);
            
            // ↓↓↓↓↓
            
            var p = function(c) {
                return c(1, pair(2, 3));
            };
            var a = p(first);
            
            // ↓↓↓↓↓
            
            var a = first(1, pair(2, 3));
            
            // ↓↓↓↓↓
            
            var a = 1;

             

            這也算是個老掉牙的例子了啊。閉包在這里體現了他強大的作用,把參數保留了起來,我們可以在這之后進行訪問。仿佛我們寫的就是下面這樣的代碼:

            var p = {
                first : 1,
                second : {
                    first : 1,
                    second : 2,
                }
            };
            
            var a = p.first;
            var b = p.second.first;
            var c = p.second.second;

             

            于是我們得到了一個結論,(帶閉包的)lambda表達式可以代替一個成員為只讀的struct了。那么,成員可以讀寫的struct要怎么做呢?做法當然跟上面的不一樣。究其原因,就是因為javascript使用了call by value的規則,使得pair里面的return c(a, b);沒辦法將a和b的引用傳遞給c,這樣就沒有人可以修改a和b的值了。雖然a和b在那些c里面是改不了的,但是pair函數內部是可以修改的。如果我們要堅持只是用lambda表達式的話,就得要求c把修改后的所有“這個struct的成員變量”都拿出來。于是就有了下面的代碼:

            // 在這里我們繼續使用上面的pair、first和second函數
            
            function mutable_pair(a, b) {
                return function(c) {
                    var x = c(a, b);
                    // 這里我們把pair當鏈表用,一個(1, 2, 3)的鏈表會被儲存為pair(1, pair(2, pair(3, null)))
                    a = x(second)(first);
                    b = x(second)(second)(first);
                    return x(first);
                };
            }
            
            function get_first(a, b) {
                return pair(a, pair(a, pair(b, null)));
            }
            
            function get_second(a, b) {
                return pair(b, pair(a, pair(b, null)));
            }
            
            function set_first(value) {
                return function(a, b) {
                    return pair(undefined, pair(value, pair(b, null)));
                };
            }
            
            function set_second(value) {
                return function(a, b) {
                    return pair(undefined, pair(a, pair(value, null)));
                };
            }
            
            var p = mutable_pair(1, 2);
            var a = p(get_first);
            var b = p(get_second);
            print(a, b);
            p(set_first(3));
            p(set_second(4));
            var c = p(get_first);
            var d = p(get_second);
            print(c, d);

             

            我們可以看到,因為get_first和get_second做了一個只讀的事情,所以返回的鏈表的第二個值(代表新的a)和第三個值(代表新的b)都是舊的a和b。但是set_first和set_second就不一樣了。因此在執行到第二個print的時候,我們可以看到p的兩個值已經被更改成了3和4。

            雖然這里已經涉及到了“綁定過的變量重新賦值”的事情,不過我們還是可以嘗試推導一下,究竟p(set_first(3));的時候究竟干了什么事情:

            var p = mutable_pair(1, 2);
            p(set_first(3));
            
            // ↓↓↓↓↓
            
            p = return function(c) {
                var x = c(1, 2);
                a = x(second)(first);
                b = x(second)(second)(first);
                return x(first);
            };
            p(set_first(3));
            
            // ↓↓↓↓↓
            
            var x = set_first(3)(1, 2);
            p.a = x(second)(first); // 這里的a和b是p的閉包內包含的上下文的變量了,所以這么寫會清楚一點
            p.b = x(second)(second)(first);
            // return x(first);出來的值沒人要,所以省略掉。
            
            // ↓↓↓↓↓
            
            var x = (function(a, b) {
                return pair(undefined, pair(3, pair(b, null)));
            })(1, 2);
            p.a = x(second)(first);
            p.b = x(second)(second)(first);
            
            // ↓↓↓↓↓
            
            x = pair(undefined, pair(3, pair(2, null)));
            p.a = x(second)(first);
            p.b = x(second)(second)(first);
            
            // ↓↓↓↓↓
            
            p.a = 3;
            p.b = 2;

             

            由于涉及到了上下文的修改,這個推導嚴格上來說已經不能叫推導了,只能叫解說了。不過我們可以發現,僅僅使用可以捕捉可讀寫的上下文的lambda表達式,已經可以實現可讀寫的struct的效果了。而且這個struct的讀寫是通過getter和setter來實現的,于是只要我們寫的復雜一點,我們就得到了一個interface。于是那個mutable_pair,就可以看成是一個構造函數了。

            大括號不能換行的代碼真他媽的難讀啊,遠遠望去就像一坨屎!go語言還把javascript自動補全分號的算法給抄去了,真是沒品位。

            所以,interface其實跟lambda表達是一樣,也可以看成是一個閉包。只是interface的入口比較多,lambda表達式的入口只有一個(類似于C++的operator())。大家可能會問,class是什么呢?class當然是interface內部不可告人的實現細節的。我們知道,依賴實現細節來編程是不對的,所以我們要依賴接口編程。

            當然,即使是倉促設計出javascript的那個人,大概也是知道構造函數也是一個函數的,而且類的成員跟函數的上下文鏈表的節點對象其實沒什么區別。于是我們會看到,javascript里面是這么做面向對象的事情的:

            function rectangle(a, b) {
                this.width = a;
                this.height = height;
            }
            
            rectangle.prototype.get_area = function() {
                return this.width * this.height;
            };
            
            var r = new rectangle(3, 4);
            print(r.get_area());

             

            然后我們就拿到了一個3×4的長方形的面積12了。不過javascript給我們帶來的一點點小困惑是,函數的this參數其實是dynamic scoping的,也就是說,這個this到底是什么,要看你在哪如何調用這個函數。于是其實

            obj.method(args)

             

            整個東西是一個語法,它代表method的this參數是obj,剩下的參數是args??上У氖牵@個語法并不是由“obj.member”和“func(args)”組成的。那么在上面的例子中,如果我們把代碼改為:

            var x = r.get_area;
            print(x());

             

            結果是什么呢?反正不是12。如果你在C#里面做這個事情,效果就跟javascript不一樣了。如果我們有下面的代碼:

            class Rectangle
            {
                public int width;
                public int height;
            
                public int GetArea()
                {
                    return width * height;
                }
            };

             

            那么下面兩段代碼的意思是一樣的:

            var r = new Rectangle
            {
                width = 3;
                height = 4;
            };
            
            // 第一段代碼
            Console.WriteLine(r.GetArea());
            
            // 第二段代碼
            Func<int> x = r.GetArea;
            Console.WriteLine(x());

             

            究其原因,是因為javascript把obj.method(a, b)解釋成了GetMember(obj, “method”).Invoke(a, b, this = r);了。所以你做r.get_area的時候,你拿到的其實是定義在rectangle.prototype里面的那個東西。但是C#做的事情不一樣,C#的第二段代碼其實相當于:

            Func<int> x = ()=>
            {
                return r.GetArea();
            };
            Console.WriteLine(x());

             

            所以說C#這個做法比較符合直覺啊,為什么dynamic scoping(譬如javascript的this參數)和call by name(譬如C語言的宏)看起來都那么屌絲,總是讓人掉坑里,就是因為違反了直覺。不過javascript那么做還是情有可原的。估計第一次設計這個東西的時候,收到了靜態類型語言太多的影響,于是把obj.method(args)整個當成了一個整體來看。因為在C++里面,this的確就是一個參數,只是她不能讓你obj.method,得寫&TObj::method,然后還有一個專門填this參數的語法——沒錯,就是.*和->*操作符了。

            假如說,javascript的this參數要做成lexical scoping,而不是dynamic scoping,那么能不能用lambda表達式來模擬interface呢?這當然是可以,只是如果不用prototype的話,那我們就會喪失javascript愛好者們千方百計絞盡腦汁用盡奇技淫巧鎖模擬出來的“繼承”效果了:

            function mutable_pair(a, b) {
                _this = {
                    get_first = function() { return a; },
                    get_second = function() { return b; },
                    set_first = function(value) { a = value; },
                    set_second = function(value) { b = value; }
                };
            return _this; } var p = new mutable_pair(1, 2); var a = p.get_first(); var b = p.get_second(); print(a, b); var c = p.set_first(3); var d = p.set_second(4); print(c, d);

             

            這個時候,即使你寫

            var x = p.set_first;
            var y = p.set_second;
            x(3);
            y(4);

             

            代碼也會跟我們所期望的一樣正常工作了。而且創造出來的r,所有的成員變量都屏蔽掉了,只留下了幾個函數給你。與此同時,函數里面訪問_this也會得到創建出來的那個interface了。

            大家到這里大概已經明白閉包、lambda表達式和interface之間的關系了吧。我看了一下之前寫過的六篇文章,加上今天這篇,內容已經覆蓋了有:

            1. 閱讀C語言的復雜的聲明語法
            2. 什么是語法噪音
            3. 什么是語法的一致性
            4. C++的const的意思
            5. C#的struct和property的問題
            6. C++的多重繼承
            7. 封裝到底意味著什么
            8. 為什么exception要比error code寫起來干凈、容易維護而且不需要太多的溝通
            9. 為什么C#的有些interface應該表達為concept
            10. 模板和模板元編程
            11. 協變和逆變
            12. type rich programming
            13. OO的消息發送的含義
            14. 虛函數表是如何實現的
            15. 什么是OO里面的類型擴展開放/封閉與邏輯擴展開放/封閉
            16. visitor模式如何逆轉類型和邏輯的擴展和封閉
            17. CPS(continuation passing style)變換與異步調用的異常處理的關系
            18. CPS如何讓exception變成error code
            19. argument passing和symbol resolving
            20. 如何用lambda實現mutable struct和immutable struct
            21. 如何用lambda實現interface

            想了想,大概通俗易懂的可以自學成才的那些東西大概都講完了。當然,系列是不會在這里就結束的,只是后面的東西,大概就需要大家多一點思考了。

            寫程序講究行云流水。只有自己勤于思考,勤于做實驗,勤于造輪子,才能讓編程的學習事半功倍。

            posted @ 2013-07-05 06:31 陳梓瀚(vczh) 閱讀(9414) | 評論 (12)編輯 收藏

            代碼可以在這里直接下載到:http://www.shnenglu.com/Files/vczh/Cppblog備份工具.rar

            這是一個C#寫的命令行程序,在資源管理器雙擊運行之后輸入你的用戶名和密碼,然后就可以把目錄、博客內容、圖片和文件下載到當前目錄下的一個叫做CppblogPosts的文件夾下面了。在此需要注意,我只會下載在博客里面引用了的、上傳到了cppblog的圖片和文件。下載的文件格式如下:

            Posts.xml:記錄了所有博客文章的一些元數據,還有每一個博客的id。
            Post[博客id].txt:每一篇博客的內容。
            Images.xml:保存了所有圖片的“url”到“文件名”的映射。
            Image[GUID]文件名.xxx:文件名。一個文件名究竟對應什么url可以再Images.xml里面查到。
            Files.xml:保存了所有文件的“url”到“文件名”的映射。
            File[GUID]文件名.xxx:文件名。一個文件名究竟對應什么url可以再Files.xml里面查到。

            之所以安排成這樣的格式是因為,下載完之后你們就可以自己寫程序隨便你們怎么處理了。

            ================無恥的分割線================

            在做這個程序之前,我發現cppblog支持metaweblog的api,但是發現這個api沒辦法遍歷帖子的id。我為此還發信給了博客園的管理員,最終讓他們加上了這個功能,于是就有了現在這個程序了。在這個程序的代碼里面,你們還能看到我用C#寫的一個簡單的XmlRpc的輪子。之所以不找別人的是因為,自己寫比上網找然后學習怎么用快多了,啊哈哈哈哈。

            這個輪子可是很漂亮的哦!

            posted @ 2013-06-29 05:57 陳梓瀚(vczh) 閱讀(12983) | 評論 (5)編輯 收藏

            跟大神聊天是很開心的。這不是因為我激動,而是因為大神說出來的每一個字都是有價值的,一針見血,毫無廢話。至于為什么說又,當然是這種事情以前發生過。

            第一次是在高中認識了龔敏敏。那個時候我剛做完那個傻逼的2D ARPG不久,龔敏敏已經是M$RA的實習生了,圖形學上的造詣肯定要比我高許多,其中的差距構成了大神跟菜鳥的關系。當然現在我盡管中心已經放在了程序設計語言(programming language,以下簡稱PL)上,但是還知道一些圖形學的內容,跟龔敏敏的差距自然也已經縮小到了不構成大神和菜鳥的關系的程度了。盡管他還是比我多知道很多東西。

            第二次是在大學的時候認識了g9yuayon。g9菊苣是做形式化和證明的,自然也知道很多PL的事情。那應該是我大二的時候,在CSDN上偶然發現了g9菊苣的博客,覺得文章寫的很好,就順便把博客上面的email“密碼”給破了之后發email給他。后來g9菊苣告訴了我很多諸如在哪里可以獲得知識的事情,于是我也就做了PL。盡管現在已經很少跟g9菊苣聯系了,不過我感覺目前我跟g9的差距應該還屬于大神跟菜鳥的關系,因為他很久以前寫的博客我都還不能完全搞明白。

            第三次就是今天的事情了。大家都知道最近我在寫一個《如何設計一門語言》的系列文章。這個系列文章肯定是會繼續寫下去的,因為我的語言都還沒做出來。所以可以很明顯地看出來,我現在也在做一個語言。這跟王垠的那個one當然是不一樣的,因為我從一開始就沒打算代替所有東西,而且目標也很明確,就是把它做成跟C++/C#一樣,菜鳥可以很容易上手寫出清晰易懂的代碼,大神也可以在里面挖掘出很多奇技淫巧。于是我不可避免的就遇到了CPS的問題。

            大家都知道C#有yield和await兩個關鍵字,F#也有computation expression。于是我就在想,如果yield和await不是關鍵字,而是一個函數,會發生什么事情。展開來講,就是如果要讓程序員自己實現一個為特定目的服務的CPS變換,那我的語法要怎么做。對于沒有怎么設計過程序語言的人來說,“設計一個語法”這種事情其實是很容易被誤解的。語法并不是說要在這里放一個括號,在那里放一個關鍵字,在別的地方還能省略一個什么東西(瞧瞧go抄了javascript那個屎一樣的分號省略策略)。這些都屬于品味的問題。品味是不需要設計的,那是靠感覺的,是一種藝術。只要你拿出來覺得漂亮,那就是好的。真正需要思考的東西是什么,那自然是圍繞早上面的類型系統了。

            我用通俗易懂的方法來解釋一下,什么是類型系統,或者說在我們這些做PL的人看來,眼中的程序大概是什么樣子的。我們拿一個C#的異步程序來說,其實也就是上一篇文章講的那個例子了。

            async void button4_Click(object sender, EventArgs e)
            {
                try
                {
                    string a=await Http.DownloadAsync(url1);
                    string b=await Http.DownloadAsync(url2);
                    textBox1.Text=a+b;
                }
                catch(Exception ex)
                {
                    textBox1.Text=ex.Message;
                }
            }

            大家都很熟悉吧。如果這個這么簡單的程序還看不懂的話,那肯定是沒有認真閱讀我的《如何》系列。好了,現在開始來講,做PL的人到底是如何看待這個程序的呢:

            async void button4_Click(Object, EventArgs)
            {
                try
                {
                    String=await (String -> Task<String>) (String);
                    String=await (String -> Task<String>) (String);
                    (TextBox -> String -> Void#TextBox.Text) (TextBox, String + String);
                }
                catch(Exception)
                {
                    (TextBox -> String -> Void#TextBox.Text) (TextBox, (Exception -> String#Exception.Message) (Exception));
                }
            }

            嗯,差不多就是這個樣子。這個函數究竟是下載一個盜版小說,還是下載一個帶節操的日本電影,究竟是同步下載,還是異步下載,是下載到一個文件夾,還是下載到skydrive——關我屁事!我只看這里關于類型的部分。

            所以,如果await是一個函數的話,那他應該是什么類型?如果yield也是一個函數,那他應該是什么類型?如果這門語言讓程序員來創建屬于自己的await和yield甚至是他自己的想要的計算,那我應該如何做一個框架讓他往里面套,或者他寫出來的這個函數究竟要在什么上下文里面滿足什么樣的一個類型的關系呢?我最近就一直在想這個問題。

            一開始我就把目光投向了F#的computation expression,因為F#的這個東西就具有我想要的一切功能。后來我想把這個功能搬進來的時候,發現怎樣都套不上。當然我很快就發現了,這其實是因為F#歸根結底還是一個函數是語言,他是不能在一個for循環里面寫break、continue或者return的。F#的一個for循環,永遠是一個完美的for循環。但是我的語言是可以的,于是這樣在類型上就不完美了——不過這是小事,犧牲一點點完美換來易用性是值得的。當然,犧牲很多完美來滿足易用性,我覺得是不值得的。

            既然for循環里面可以帶break/continue/return,那么“我的computation expression”的For函數,就不能是類似于IEnumerable<T>->(T->M<U>)->M<U>這種純粹的東西了。那我應該怎么做呢?

            寫到這里,我覺得在微軟工作就是好啊。關于編程語言領域的很多改進其實都是從微軟這里做出來的。通俗的部分,看看完美的C#,看看ASP.NET MVC的razor模板在Visual Studio里面的智能提示的功能——這可是一個可以混合HTML+CSS+Javascript+C#的代碼,寫的時候絲般順滑,行云流水,儼然這四門語言就是一門語言一樣。在學術上,微軟的各個研究院也貢獻了相當多的東西——不過我覺得你們對這些應該是不感興趣的,盡管你們在linux上面也用了很多微軟的成果。

            那這能說明什么問題呢?這就意味著,我可以隨時access到微軟做編程語言的大神們,抓他們來問問題。不過他們是很忙的,經常不在線(我們也有一個類似QQ這樣子的東西)。不過今天我隨手打開了一下,展開了我積累的幾個大神的組,發現F#他爹竟然是綠的,于是我隨手就發了一句hi,看看人家在不在。人家回了我,于是我就開始問這個問題了。

            什么,你不知道F#他爹是誰?他當然是Don Syme了。寫函數式語言不認識Don Syme,就猶如讀物理不認識牛頓,讀數學不認識柯西,寫C++不知道Bjarne Stroustrup,用操作系統不知道Dave Cutler一樣,要跪著爬回自己學校里重新讀書。

            Don Syme是微軟的Principle Researcher,翻譯過來大概就是“頂級科學家”的意思吧,很少有更牛逼的東西了。

            于是故事到這里就結束了,因為Don Syme大神他很快就回復我說,如果for循環支持break/continue/return,那我就不應該從F#的computation expression里面獲取靈感。至于我的問題要怎么辦,這還是個open question。于是我們愉快的聊天就用下面的一句話結束了:

            Don Syme: Research 微笑

            posted @ 2013-06-25 09:17 陳梓瀚(vczh) 閱讀(12471) | 評論 (15)編輯 收藏

            我一直以來對于exception的態度都是很明確的。首先exception是好的,否則就不會有絕大多數的語言都支持他了。其次,error code也沒什么問題,只是需要一個前提——你的語言得跟Haskell一樣有monad和comonad。你看Haskell就沒有exception,大家也寫的很開心。為什么呢?因為只要把返回帶error code結果的函數給做成一個monad/comonad,那么就可以用CPS變換把它變成exception了。所以說CPS作為跟goto同樣基本的控制流語句真是當之無愧呀,只是CPS是type rich的,goto是type poor的。

            其實很多人對于exception的恐懼心理在于你不知道一個函數會拋什么exception出來,然后程序一crash你就傻逼了。對于server來講情況還好,出了問題只要殺掉快速重啟就行了,如今沒個replication和fault tolerance還有臉說你在寫后端(所以不知道那些做web的人究竟在反對什么)?這主要的問題還是在于client。只要client上面的東西還沒保存,那你一crash數據就完蛋了是不是——當然這只是你的想象啦,其實根本不是這樣子的。

            我們的程序拋了一個access violation出來,和拋了其它exception出來,究竟有什么區別呢?access violation是一個很奇妙的東西,一旦拋了出來就告訴你你的程序沒救了,繼續執行下去說不定還會有破壞作用。特別是對于C/C++/Delphi這類語言來說,你不小心把錯誤的東西寫進了什么亂七八糟的指針里面去,那會兒什么事情都沒發生,結果程序跑著跑著就錯了。因為你那個算錯了得到的野指針,說不定是隔壁的不知道什么object的成員變量,說不定是heap里面的數據結構,或者說別的什么東西,就這么給你寫了。如果你寫了別的object的成員變量那封裝肯定就不管用了,這個類的不變量就給你破壞了。既然你的成員函數都是基于不變量來寫的,那這個時候出錯時必須的。如果你寫到了heap的數據結構那就更加呵呵呵了,說不定下次一new就崩了,而且你還不知道為什么。

            出了access violation以外的exception基本是沒什么危害的,最嚴重的大概也就是網線被拔了,另一塊不是裝OS的硬盤突然壞了什么的這種反正你也沒辦法但是好歹還可以處理的事情。如果這些exception是你自己拋出來的那就更可靠了——那都是計劃內的。只要程序未來不會進入access violation的狀態,那證明你現在所能拿到的所有變量,還有指針指向的memory,基本上都還是靠譜的。出了你救不了的錯誤,至少你還可以吧數據安全的保存下來,然后讓自己重啟——就跟word一樣。但是你有可能會說,拿出了access violation怎么就不能保存數據了呢?因為這個時候內存都毀了,指不定你保存數據的代碼new點東西然后掛了,這基本上是沒準的。

            所以無論你喜歡exception還是喜歡error code,你所希望達到的效果本質上就是避免程序未來會進入access violation的狀態。想做到這一點,方法也是很簡單粗暴的——只要你在函數里面把運行前該對函數做的檢查都查一遍就好了。這個無論你用exception還是用error code,寫起來都是一樣的。區別在于調用你的函數的那個人會怎么樣。那么我來舉個例子,譬如說你覺得STL的map實在是太傻比了,于是你自己寫了一個,然后有了一個這樣子的函數:

            // exception版本
            Symbol* SymbolMap::Lookup(const wstring& name);
            
            // error code版本
            int SymbolMap::Lookup(const wstring& name, Symbol*& result);
            
            // 其實COM就是你們最喜歡的error code風格了,寫起來應該很開心才對呀,你們的雙重標準真嚴重
            HRESULT ISymbolMap::Lookup(BSTR name, ISymbol** result);

            于是拿到了Lookup函數之后,我們就要開始來完成一個任務了,譬如說拿兩個key得到兩個symbol然后組合出一個新的symbol。函數的錯誤處理邏輯是這樣的,如果key失敗了,因為業務的原因,我們要告訴函數外面說key不存在的。調用了一個ComposeSymbol的函數丟出什么IndexOutOfRangeException顯然是不合理的。但是合并的那一步,因為業務都在同一個領域內,所以suppose里面的異常外面是可以接受的。如果出現了計劃外的異常,那我們是處理不了的,只能丟給上面了,外面的代碼對于不認識的異常只需要報告任務失敗了就可以了。于是我們的函數就會這么寫:

            Symbol* ComposeSymbol(const wstring& a, const wstring& b, SymbolMap* map)
            {
                Symbol* sa=0;
                Symbol* sb=0;
                try
                {
                    sa=map->Lookup(a);
                    sa=map->Lookup(b);
                }
                catch(const IndexOutOfRangeException& ex)
                {
                    throw SymbolKeyException(ex.GetIndex());
                }
                return CreatePairSymbol(sa, sb);
            }

            看起來還挺不錯。現在我們可以開始考慮error code的版本了。于是我們需要思考幾個問題。首先第一個就是Lookup失敗的時候要怎么報告?直接報告key的內容是不可能的,因為error code是個int。

            題外話,error code當然可以是別的什么東西,如果需要返回豐富內容的錯誤的話,那怎樣都得是一個指針了,這個時候你們就會面臨下面的問題——這已經他媽不滿足誰構造誰釋放的原則了呀,而且我這個指針究竟直接返回出去外面理不理呢,如果只要有一個環節不理了,那內存豈不是泄露了?如果我要求把錯誤返回在參數里面的話,我每次調用函數都要創建出那么個結構來保存異常,不僅有if的復雜度,還有創建空間的復雜度,整個代碼都變成了屎。所以還是老老實實用int吧……

            那我們要如何把key的信息給編碼在一個int里面呢?因為key要么是來自于a,要么是來自于b,所以其實我們就需要兩個code了。那Lookup的其他錯誤怎么辦呢?CreatePairSymbol的錯誤怎么辦呢?萬一Lookup除了ERROR_KEY_NOT_FOUND以外,或者是CreatePairSymbol的錯誤剛好跟a或者b的code重合了怎么辦?對于這個問題,我只能說:

            要不你們team的人先開會討論一下最后記錄在文檔里面備查以免后面的人看了傻眼了……

            好了,現在假設說會議取得了圓滿成功,會議雙方加深了互相的理解,促進了溝通,最后還寫了一個白皮書出來,有效的落實了對a和b的code的指導,于是我們終于可以寫出下面的代碼了:

            #define SUCCESS 0 // global error code for success
            #define ERROR_COMPOSE_SYMBOL_WRONG_A 1
            #define ERROR_COMPOSE_SYMBOL_WRONG_B 2
            
            int ComposeSymbol(const wstring& a, const wstring& b, SymbolMap* map, Symbol*& result)
            {
                int code=SUCCESS;
                Symbol* sa=0;
                Symbol* sb=0;
                switch(code=map->Lookup(a, sa))
                {
                case SUCCESS:
                    break;
                case ERROR_SYMBOL_MAP_KEY_NOT_FOUND:
                    return ERROR_COMPOSE_SYMBOL_WRONG_A;
                default:
                    return code;
                }
                switch(code=map->Lookup(b, sb))
                {
                case SUCCESS:
                    break;
                case ERROR_SYMBOL_MAP_KEY_NOT_FOUND:
                    return ERROR_COMPOSE_SYMBOL_WRONG_B;
                default:
                    return code;
                }
                return CreatePairSymbol(sa, sb, result);
            }

            啊,好像太長,干脆我還是不負責任一點吧,反正代碼寫的好也漲不了工資,干脆不認識的錯誤都返回ERROR_COMPOSE_SYMBOL_UNKNOWN_ERROR好了,于是就可以把代碼變成下面這樣……都到這份上了不要叫自己程序員了,叫程序狗吧……

            #define SUCCESS 0 // global error code for success
            #define ERROR_COMPOSE_SYMBOL_WRONG_A 1
            #define ERROR_COMPOSE_SYMBOL_WRONG_B 2
            #define ERROR_COMPOSE_SYMBOL_UNKNOWN_ERROR 3
            
            int ComposeSymbol(const wstring& a, const wstring& b, SymbolMap* map, Symbol*& result)
            {
                Symbol* sa=0;
                Symbol* sb=0;
                if(map->Lookup(a, sa)!=SUCCESS)
                    return ERROR_COMPOSE_SYMBOL_UNKNOWN_ERROR;
                if(map->Lookup(b, sb)!=SUCCESS)
                    return ERROR_COMPOSE_SYMBOL_UNKNOWN_ERROR;
                if(CreatePairSymbol(sa, sb, result)!=SUCCESS)
                    return ERROR_COMPOSE_SYMBOL_UNKNOWN_ERROR;
                return SUCCESS;
            }

            當然,如果大家都一樣不負責任的話,還是exception完爆error code:

            Symbol* ComposeSymbol(const wstring& a, const wstring& b, SymbolMap* map)
            {
                return CreatePairSymbol(map->Lookup(a), map->Lookup(b));
            }

            大部分人人只會用在當前條件下最容易寫的方法來設計軟件,而不是先設計出軟件然后再看看怎樣寫比較容易,這就是為什么我說,只要你一個月給程序員還給不到一狗半,還是老老實實在政策上落實exception吧。至少exception寫起來還不會讓人那么心煩,可以把程序寫得堅固一點。

            好了,單線程下面至少你還可以爭吵說究竟exception好還是error code好,但是到了異步程序里面就完全不一樣了。現在的異步程序都很多,譬如說有良心的手機app啦,譬如說javascript啦,metro程序等等。一個try根本沒辦法跨線程使用所以一個這樣子的函數(下面開始用C#,C++11的future/promise我用的還不熟):

            class Normal
            {
                public string Do(string args);
            }

            最后就會變成這樣:

            class Async
            {
                // before .NET 4.0
                IAsyncResult BeginDo(string args, Action<IAsyncResult> continuation);
                string EndDo(IAsyncResult ar);
            
                // after .NET 4.0
                Task<string> DoAsync(string args);
            }

            當你使用BeginDo的時候,你可以在continuation里面調用EndDo,然后得到一個string,或者得到一個exception。但是因為EndDo的exception不是在BeginDo里面throw出來的,所以無論你EndDo返回string也好,返回Tuple<string, Exception>也好,對于BeginDo和EndDo的實現來說其實都一樣,沒有上文所說的exception和error code的區別。

            不過.NET從BeginDo/EndDo到DoAsync經歷了一個巨大的進步。雖然形式上都一樣,但是由于C#并不像Haskell那樣可以完美的操作函數,C#還是面向對象做得更好,于是如果我們吧Task<T>看成下面的樣子,那其實兩種寫法是沒有區別的:

            class Task<T>
            {
                public IAsyncResult BeginRun(Action<IAsyncResult> continuation);
                public T EndRun(IAsyncResult ar);
            }

            不過如果還是用BeginRun/EndRun這種方法來調用的話,使用起來還是很不方便,而且也很難把更多的Task組合在一起。所以最后.NET給出的Task是下面這個樣子的(Comonad!):

            class Task<T>
            {
                public Task<U> ContinueWith<U>(Func<Task<T>, U> continuation);
            }

            盡管真實的Task<T>要比上面那個復雜得多,但是總的來說其實就是圍繞著基本簡單的函數建立起來的一大堆helper function。到這里C#終于把CPS變換在異步處理上的應用的這一部分給抽象出來了。在看CPS的效果之前,我們先來看一個同步函數:

            void button1_Clicked(object sender, EventArgs e)
            {
                    // 假設我們有string Http.Download(string url);
                    try
                    {
                            string a = Http.Download(url1);
                            string b = Http.Download(url2);
                            textBox1.Text=a+b;
                    }
                    catch(Exception ex)
                    {
                            textBox1.Text=ex.Message;
                    }
            }

            這段代碼顯然是一個GUI里面的代碼。我們如果在一個GUI程序里面這么寫,就會把程序寫得跟QQ一樣卡了。所以實際上這么做是不對的。不過為了表達程序需要做的所有事情,就有了這么一個同步的版本。那么我們嘗試吧這個東西修改成異步的把!

            void button2_Clicked(object sender, EventArgs e)
            {
                // 假設我們有Task<string> Http.DownloadAsync(string url);
                // 需要MethodInvoker是因為,對textBox1.Text的修改只能在GUI線程里面做
                Http.DownloadAsync(url1).ContinueWith(ta=>new MethodInvoker(()=>
                {
                    try
                    {
                        // 這個時候ta已經運行完了,所以對ta.Result的取值不會造成GUI線程等待IO。
                        // 而且如果DownloadAsync內部出了錯,異常會在這里拋出來。
                        string a=ta.Result;
                        Http.DownloadAsync(url2).ContinueWith(tb=>new MethodInvoker(()=>
                        {
                            try
                            {
                                string b=tb.Result;
                                textBox1.Text=a+b;
                            }
                            catch(Exception ex)
                            {
                                textBox1.Text=ex.Message;
                            }
                        })));
                    }
                    catch(Exception ex)
                    {
                        textBox1.Text=ex.Message;
                    }
                })));
            }

            我們發現,異步操作發生的異常,把優越的exception拉低到了丑陋的error code的同一個情況上面——我們需要不斷地對每一個操作重復同樣的錯誤處理過程!而且在這種地方我們連“不負責任”的選項都沒有了,如果你不try-catch(或者不檢查error code),那到時候程序就會發生一些莫名其妙的問題,在GUI那一層你什么事情都不知道,整個程序就變成了傻逼。

            現在可以開始解釋一下什么是CPS變換了。CPS變換就是把所有g(f(x))都給改寫成f(x, r=>g(r))的過程。通俗一點講,CPS變換就是幫你把那個同步的button1_Click給改寫成異步的button2_Click的這個過程。盡管這么說可能不太嚴謹,因為button1_Click跟button2_Click所做的事情是不一樣的,一個會讓GUI卡成qq,另一個不會。但是我們討論CPS變換的時候,我們討論的是對代碼結構的變換,而不是別的什么東西。

            現在就是激動人心的一步了。既然CPS可以把返回值變換成lambda表達式,那反過來我們也可以把所有的以這種形式存在的lambda表達式都改寫成返回值嘛?,F在我們滾回去看一看button2_Click,會發現這個程序其實充滿了下面的pattern:

            // lambda的參數名字故意起了跟前面的變量一樣的名字(previousTask)因為其實他們就是同一個東西
            previousTask.ContinueWith(previousTask=>new MethodInvoker(()=>
            {
                try
                {
                    continuation(previousTask.Result);
                }
                catch(Exception ex)
                {
                    textBox1.Text=ex.Message;
                }
            })));

            我們可以“發明”一個語法來代表這個過程。C#用的是await關鍵字,那我們也來用await關鍵字。假設說上面的代碼永遠等價于下面的這個代碼:

            try
            {
                var result=await previousTask;
                continuation(result);
            }
            catch(Exception ex)
            {
                textBox1.Text=ex.Message;
            }

            兩段代碼的關系就跟i++;和i=i+1;一樣是可以互相替換的,只是不同的寫法而已。那我們就可以用相同的方法來把button2_Click給替換成下面的button3_Click了:

            void button3_Click(object sender, EventArgs e)
            {
                try
                {
                    var a=await Http.DownloadAsync(url1);
                    try
                    {
                        var b=await Http.DownloadAsync(url2);
                        textBox1.Text=a+b;
                    }
                    catch(Exception ex)
                    {
                        textBox1.Text=ex.Message;
                    }
                }
                catch(Exception ex)
                {
                    textBox1.Text=ex.Message;
                }
            }

            聰明的讀者立刻就想到了,兩個try其實是重復的,那為什么不把他們合并成一個呢!當然我想告訴大家的是,異常是在不同的線程里面拋出來的,只是我們用CPS變換把代碼“改寫”成這種形式而已。理論上兩個try是不能合并的。但是!我們的C#編譯器君是很聰明的。正所謂語言的抽象高級了一點,那么編譯器對你的代碼也就理解得更多了一點。如果編譯器發現你在try里面寫了兩個await,馬上就明白了過來他需要幫你復制catch的部分——或者說他可以幫你自動的復制catch的部分,那情況就完全不同了,最后就可以寫成:

            // C#要求函數前面要加一個async來允許你在函數內使用await
            // 當然同時你的函數也就返回Task而不是void了
            // 不過沒關系,C#的event也可以接受一個標記了async的函數,盡管返回值不一樣
            // 設計語言這種事情就是牽一發而動全身呀,加個await連event都要改
            async void button4_Click(object sender, EventArgs e)
            {
                try
                {
                    string a=await Http.DownloadAsync(url1);
                    string b=await Http.DownloadAsync(url2);
                    textBox1.Text=a+b;
                }
                catch(Exception ex)
                {
                    textBox1.Text=ex.Message;
                }
            }

            把兩個await換成回調已經讓我們寫的夠辛苦了,那么如果我們把await寫在了循環里面,事情就不那么簡單了。CPS需要把循環翻譯成遞歸,那你就得把lambda表達時拿出來寫成一個普通的函數——這樣他就可以有名字了——然后才能遞歸(寫出一個用于CPS的Y-combinator是一件很困難的事情,盡管并沒有比Y-combinator本身困難多少)。這個例子就復雜到爆炸了,我在這里就不演示了。

            總而言之,C#因為有了CPS變換(await),就可以把button4_Click幫你寫成button3_Click然后再幫你寫成button2_Click,最后把整個函數變成異步和回調的形式(真正的做法要更聰明一點,大家可以反編譯去看)在異步回調的寫法里面,exception和error code其實是一樣的。但是CPS+exception和CPS+error code就跟單線程下面的exception和error code一樣,有著重大的區別。這就是為什么文章一開始會說,我只會在帶CPS變換的語言(Haskell/F#/etc)里面使用error code。

            在這類語言里面利用相同的技巧,就可以不是異步的東西也用CPS包裝起來,譬如說monadic parser combinator。至于你要選擇monad還是comonad,基本上就是取決于你要自動提供錯誤處理還是要手動提供錯誤處理。像上面的Task.ContinueWith,是要求你手動提供錯誤處理的(因為你catch了之后可以干別的事情,Task無法自動替你選擇最好的措施),所以他就把Task.ContinueWith寫成了comonad的那個樣子。

            寫到這里,不禁要同情寫前端的那幫javascript和自以為可以寫后端的node.js愛好者們,你們因為小小的eval的問題,不用老趙的windjs(windjs給javascript加上了await但是它不是一個altjs所以得顯式調用eval),是一個多大的損失……

            posted @ 2013-06-09 23:01 陳梓瀚(vczh) 閱讀(13974) | 評論 (8)編輯 收藏

            面向對象這個抽象的特例總是有說不完的話題,更糟糕的是很多語言都錯誤地實現了面向對象——class居然可以當一個變量類型什么的這只是讓人們寫代碼寫的更糟糕而已。當然這個話題第三篇文章已經說過了,現在來談談人們喜歡拿來裝逼的另一個話題——消息發送。

            按照慣例先來點題外話。說到消息發送,有些人喜歡跳出來說,objective-c的消息做得多優雅啊,代碼都可以寫成一句話[golang screw:you you:suck]之類的。其實這個還做得不夠徹底。在幾年前易語言曾經火了一陣,但是為什么大家這么討厭他呢?其實顯然不是因為每個token都是漢字,而是因為他做的一點都不像中文,誰會說話的時候帶那么多符號呀。其實objective-c也一樣,沒人會因為想在一句英語里面用冒號來分割短語的。

            當我還在讀大三的時候,我由于受到了Apple Script(也是蘋果做的)的啟發,試圖發明一門語言,讓他可以盡量寫起來像自然語言——當然他仍然是嚴格的編程語言。但是這門語言因為其奇特的語法結構,我只好自己想出了一個兩遍parse地方法。第一遍parse出所有函數頭,讓后用這些函數頭臨時組成一個parser,用它來parse語句的部分。后面整個也實現出來了,然后我就去做了一下調查,發現大家不喜歡,原因是要輸入的東西太多了。不過我這里還是貼一下當初是怎么設計的:

            phrase print(content) is
                external function "writeln"
            end phrase
            
            phrase first (count) items of fibonacci sequence is
                if count equals to 1 then
                    result is [1]
                else if count equals to 2 then
                    result is [1,1]
                else
                    let list be [1,1]
                    repeat with i from 3 to count
                        let list be list joins with item length of list - 1 of list + item length of list - 2 of list
                    end
                    result is list
                end
            end phrase
            
            phrase (number) is odd is
                result is number mod 2 is 0
            end phrase alias odd number
            
            phrase append (item) after (list) is
                let 0 element of list from length of list be [item]
            end phrase
            
            phrase ((item) is validated) in (list) is
                let filtered list be []
                repeat with item in list
                    append item after filtered list if item is validated
                end
                result is filtered list
            end phrase
            
            phrase main is
                print odd number in first 10 items of fibonacci sequence
            end phrase

            倒數第二個函數聲明甚至連函數指針的聲明也如此的優雅(我自己認為的),整個程序組織起來,我們要輸出斐波那契數列里面前10個數字中間的奇數,于是就寫成了

            print odd number in first 10 items of fibonacci sequence

            看起來比objective-c要漂亮把。其實如果想把所有的東西換成中文,算法也不需要變化?,F在用空格來分割一個一個的詞,中文直接用字符就好了,剩下的都一樣。要parse這個程序根本沒辦法用流行的那些方法來parse。當然我知道大家也不會關心這些特別復雜的問題,于是題外話就到這里結束了,這個語言的實現的代碼你們大概也永遠都不會看到的,啊哈哈哈哈。

            為什么要提這件事情呢?我主要是想告訴大家,就算你在用面向對象語言,想在程序里面給一個對象發送一條消息,這個對象并不是非得寫在最前面的。為什么呢?有的時候對象不止一個——這個東西叫multiple dispatching,著名的問題就是如何給一堆面向對象的幾何體類做他們的求交函數——用面向對象的慣用做法做起來會特別的難受。不過現在我們先來看一下普通的消息發送是什么樣子的。

            對于一個我們知道他是什么類型的對象來說,發送一個消息就跟直接調用一個函數一樣,因為你不需要去resolve一下這個函數到底是誰。譬如說下面的代碼:

            class Language
            {
            public:
                void YouSuck(){ ... }
            };
            
            Language golang;
            golang.YouSuck();

            最終翻譯出來的結果會近似

            struct Language
            {
            };
            
            void Language_YouSuck(Language* const this)
            {
                ...
            }
            
            Language golang;
            Language_YouSuck(&golang);

            很多人其實并不能在學習面向對象語言的時候就直接意識到這一點。其實我也是在高中的時候玩delphi突然就在網上看見了這么一篇文章,然后我才明白的??雌饋磉@個過渡并不是特別的自然是不是。

            當你要寫一個獨立的class,不繼承自任何東西的時候,這個class的作用只有兩個。第一個是封裝,這個第三篇文章已經提到過了。第二個作用就是給里面的那些函數做一個匿名的namespace。這是什么意思呢?就像上面的代碼一樣,你寫golang.YouSuck(),編譯器會知道golang是一個Language,然后去調用Language::YouSuck()。如果你調用lisp.YouSuck()的時候,說不定lisp是另一個叫做BetterThanGolangLanguage的類型,然后他就去里面找了YouSuck。這里并不會因為兩個YouSuck的名字一樣,編譯器就把它搞混了。這個東西這跟重載也差不多,我就曾經在Microsoft Research里面看見過一個人做了一個語言(主要是用來驗證語言本身的正確性的),其中a.b(c, d)是b(a, c, d)的語法糖,這個“.”毫無特別之處。

            有一天,情況變了。專門開發蹩腳編譯器的AMD公司看見golang很符合他們的口味,于是也寫了一個golang的實現。那這個事情應該怎么建模呢?因為golang本身是一套標準,你可也可以稱呼他為協議,然后下面有若干個實現。所以Language本身作為一個category也只好跟著golang變成interface了。為了程序簡單我們只看其中的一個小片段:

            class IGolang
            {
            public:
                virtual void YouSuck()=0;
            };
            
            class GoogleGolang : public IGolang
            {
            public:
                void YouSuck()override{ /*1*/ }
            };
            
            class AmdGolang : public IGolang
            {
            public:
                void YouSuck()override{ /*2*/ }
            };
            
            IGolang* golang = new GoogleGolang;
            golang->YouSuck();

            我很喜歡VC++的專有關鍵字override,他可以在我想override但是不小心寫錯了一點的時候提示我,避免了我大量的錯誤的發生。當然這個東西別的編譯器不支持,所以我在我的代碼的靠前的地方寫了一個宏,發現不是VC++再編譯,我就把override給#define成空的。反正我的程序里面不會用關鍵字來當變量名的。

            看著這個程序,已經不能單純的用GoogleGolang_YouSuck(golang)來代替這個消息發送了,因為類型是IGolang的話說不定下面是一個AmdGolang。所以在這里我們就要引入虛函數表了。一旦引入了虛函數表,代碼就會瞬間變得復雜起來。我見過很多人問,虛函數表那么大,要是每一個類的實例都帶一個表的話豈不是很浪費內存?這種人就應該先去看《Inside the C++ Object Model》,然后再反省一下自己的問題有多么的——呃——先看帶有虛函數表的程序長什么樣子好了:

            struct vtable_IGolang
            {
                void (*YouSuck)(IGolang* const this);
            };
            
            struct IGolang
            {
                vtable_IGolang* vtable;
            };
            
            //---------------------------------------------------
            
            vtable_IGolang vtable_GoogleGolang;
            vtable_GoogleGolang.YouSuck = &vtable_GoogleGolang_YouSuck;
            
            struct GoogleGolang
            {
                IGolang parent;
            };
            
            void vtable_GoogleGolang_YouSuck(IGolang* const this)
            {
                int offset=(int)(&((GoogleGolang*)0)->parent);
                GoogleGolang_YouSuck((GoogleGolang*)((char*)this-offset));
            }
            
            void GoogleGolang_YouSuck(GoogleGolang* const this)
            {
                /*1*/
            }
            
            void GoogleGolang_ctor(GoogleGolang* const this)
            {
                this->parent->vtable = &vtable_GoogleGolang;
            }
            
            //---------------------------------------------------
            // AmdGolang略,長得都一樣
            //---------------------------------------------------
            
            GoogleGolang* tmp = (GoogleGolang*)malloc(sizeof(GoogleGolang));
            GoogleGolang_ctor(tmp);
            IGolang* golang = &tmp->parent;
            golang->vtable->YouSuck(golang);

            基本上已經面目全非了。當然實際上C++生成的代碼比這個要復雜得多。我這里只是不想把那些細節牽引進來,針對我們的那個例子寫了個可能的實現。面向對象的語法糖多么的重要啊,盡管你也可以在需要的時候用C語言把這些東西寫出來(就跟那個愚蠢的某著名linux GUI框架一樣),但是可讀性已經完全喪失了吧。明明那么幾行就可以表達出來的東西,我們為了達到同樣的性能,用C寫要把代碼寫成屎。東西一多,名字用完了,都只好對著代碼發呆了,決定把C扔了,完全用C++來寫。萬一哪天用到了virtual繼承——在某些情況下其實是相當好用的,譬如說第三篇文章講的,在C++里面用interface,而且也很常見——那用C就只能呵呵呵了,寫出來的代碼再也沒法讀了,沒法再把OOP實踐下去了。

            好了,消息發送的簡單的實現大概也就講到這里了。只要不是C++,其他語言譬如說只有單根繼承的Delphi,實現OOP大概也就是上面這個樣子。于是我們圍繞著消息發送的語法糖玩了很久,終于遇到了兩大終極問題。這兩個問題說白了都是開放和封閉的矛盾。我們用基類和一大堆子類的結構來寫程序的時候,需要把邏輯都封裝在虛函數里面,不然的話你就得cast了,cast是將程序最終導向失控的根源之一。這個時候我們對類型擴展是開放的,而對邏輯擴展是封閉的。這是什么意思呢?讓我們來看下面這個例子:

            class Shape
            {
            public:
                virtual double GetArea()=0;
                virtual bool HitTest(Point p)=0;
            };
            
            class Circle : public Shape ...;
            class Rectangle : public Shape ... ;

            我們每當添加一個新形狀的時候,只要實現GetArea和HitTest,那么事情就做完了。所以你可以無限的添加新形狀——所以類型擴展是開放的。但是你卻永遠只能做GetArea和HitTest——對邏輯擴展是封閉的。你如果想做除了GetArea和HitTest以外的更多的事情的話,這個時候你就被迫做cast了。那么在類型相對穩定的情況下有沒有別的方法呢?設計模式告訴我們,我們可以用Visitor來把情況扭轉過來——做成對類型擴展封閉,而對邏輯擴展開放的:

            class IShapeVisitor
            {
            public:
                virtual void Visit(Circle* shape)=0;
                virtual void Visit(Rectangle* shape)=0;
            };
            
            class Shape
            {
            public:
                virtual void Accept(IShapeVisitor* visitor)=0;
            };
            
            class Circle : public Shape
            {
            public:
                ...
            
                void Accept(IShapeVIsitor* visitor)override
                {
                    visitor->Visit(this);  // 因為重載的關系,會調用到第一個Visit函數
                }
            };
            
            class Rectangle : public Shape
            {
            public:
                ...
            
                void Accept(IShapeVIsitor* visitor)override
                {
                    visitor->Visit(this);  // 因為重載的關系,會調用到第二個Visit函數
                }
            };
            
            //------------------------------------------
            
            class GetAreaVisitor : public IShapeVisitor
            {
            public:
                double result;
            
                void Visit(Circle* shape)
                {
                    result = ...;
                }
            
                void Visit(Rectangle* shape)
                {
                    result = ...;
                }
            };
            
            class HitTestVisitor : public IShapeVisitor ...;

            這個時候GetArea可能調用起來就不是那么方便了,不過我們總是可以把它寫成一個函數:

            double GetArea(Shape* shape)
            {
                GetAreaVisitor visitor;
                shape->Accept(&visitor);
                return visitor.result;
            }

            這個時候你可以隨意的做新的事情了,但是一旦需要添加新類型的時候,你需要改動很多東西,首先是Visitor的接口,其實是讓所有的邏輯都支持新類型,這樣你就不能僅僅通過添加新代碼來擴展新類型了。所以這就是對邏輯擴展開放,而對類型擴展封閉了。

            所以第一個問題就是:能不能做成類型擴展也開放,邏輯擴展也開放呢?在回答這個問題之前,我們先來看下一個問題。我們要對兩個Shape進行求交,看看他們是不是有重疊在一起的部分。但是每一個具體的Shape,譬如Circle啊Rectangle啊,定義都是不一樣的,沒辦法有通用的處理辦法,所以我們只能寫3個函數了(RR, CC, CR)。如果有3各類型,那么我們就需要6個函數。如果有4個類型,那我們就需要有10個函數——才能處理所有情況。公式倒是可以一下子看出來,函數數量就等于1+2+ … +n,n等于類型的數量。

            這看起來好像是一個類型擴展開放的問題是吧,但是實際上他只能用邏輯擴展的方法來做。為什么呢?你看我們的一個visitor其實很像是我們對一個一個的具體類型都試一下看看shape是不是這個類型,從而做出正確的處理。不過這跟我們直接用if地方法相比有兩個優點:1、快;2、編譯器替你查錯有保證。

            那實際上應該怎么做呢?想想,我們這里有兩次“if type”。第一次針對第一個參數,第二次針對第二個參數。所以我們一共需要n+1=3個visitor。寫的方法倒是不復雜,首先我們得準備好RR,CC,CR三個邏輯,然后用visitor去識別類型然后調用它們:

            bool IntersectCC(Circle* s1, Circle* s2){ ... }
            bool IntersectCR(Circle* s1, Rectangle* s2){ ... }
            bool IntersectRR(Rectangle* s1, Rectangle* s2){ ... }
            // RC和CR是一樣的
            
            class IntersectWithCircleVisitor : public IShapeVisitor
            {
            public:
                Circle* s1;
                bool result;
            
                void Visit(Circle* shape)
                {
                    result=IntersectCC(s1, shape);
                }
            
                void Visit(Rectangle* shape)
                {
                    result=IntersectCR(s1, shape);
                }
            };
            
            class IntersectWithRectangleVisitor : public IShapeVisitor
            {
            public:
                Rectangle* s1;
                bool result;
            
                void Visit(Circle* shape)
                {
                    result=IntersectCR(shape, s1);
                }
            
                void Visit(Rectangle* shape)
                {
                    result=IntersectRR(s1, shape);
                }
            };
            
            class IntersectVisitor : public IShapeVisitor
            {
            public:
                bool result;
                IShape* s2;
            
                void Visit(Circle* shape)
                {
                    IntersectWithCircleVisitor visitor;
                    visitor.s1=shape;
                    s2->Accept(&visitor);
                    result=visitor.result;
                }
            
                void Visit(Rectangle* shape)
                {
                    IntersectWithRectangleVisitor visitor;
                    visitor.s1=shape;
                    s2->Accept(&visitor);
                    result=visitor.result;
                }
            };
            
            bool Intersect(Shape* s1, Shape* s2)
            {
                IntersectVisitor visitor;
                visitor.s2=s2;
                s1->Accept(&visitor);
                return visitor.result;
            }

            我覺得你們現在心里的想法肯定是:“我屮艸芔茻?!编?,這種事情在物理引擎里面是經常要碰到的。然后當你需要添加一個新的形狀的時候,呵呵呵呵呵呵呵呵。不過這也是沒辦法的,誰讓現在的要求運行時性能的面向對象語言都這么做呢?

            當然,如果在不要求性能的情況下,我們可以用ruby和它的mixin來做。至于說怎么辦,其實你們應該發現了,添加一個Visitor和添加一個虛函數的感覺是差不多的。所以只要把Visitor當成虛函數的樣子,讓Ruby給mixin一堆新的函數進各種類型就好了。不過只有支持運行時mixin的語言才能做到這一點。強類型語言我覺得是別想了。

            Mixin地方法倒是很直接,我們只要把每一個Visitor里面的Visit函數都給加進去就好了,大概感覺上就類似于:

            class Shape
            {
            public:
                // Mixin的時候等價于給每一個具體的Shape類都添加下面三個虛函數的重寫
                virtual bool Intersect(Shape* s2)=0;
                virtual bool IntersectWithCircle(Circle* s1)=0;
                virtual bool IntersectWithRectangle(Rectangle* s1)=0;
            };
            
            //--------------------------------------------
            
            bool Circle::Intersect(Shape* s2)
            {
                return s2->IntersectWithCircle(this);
            }
            
            bool Rectangle::Intersect(Shape* s2)
            {
                return s2->IntersectWithRectangle(this);
            }
            
            //--------------------------------------------
            
            bool Circle::IntersectWithCircle(Circle* s1)
            {
                return IntersectCC(s1, this);
            }
            
            bool Rectangle::IntersectWithCircle(Circle* s1)
            {
                return IntersectCR(s1, this);
            }
            
            //--------------------------------------------
            
            bool Circle::IntersectWithRectangle(Rectangle* s1)
            {
                return IntersectCR(this, s1);
            }
            
            bool Rectangle::IntersectWithRectangle(Rectangle* s1)
            {
                return IntersectRR(s1, this);
            }

            這下子應該看出來為什么我說這種方法只能用Visitor了吧,否則就要把所有類型都寫進Shape,就會很奇怪了。如果這樣的邏輯一多,類型也有四五個的話,那每加一個邏輯就得添加一批虛函數,Shape類很快就會被玩壞了。而代表邏輯的Visitor是可以放在不同的地方的,互相之間是隔離的,維護起來就會比較容易。

            那現在我們就要有第二個問題了:在擁有兩個“this”的情況下,我們要如何做才能把邏輯做成類型擴展也開放,邏輯擴展也開放呢?然后參考我們的第一個問題:能不能做成類型擴展也開放,邏輯擴展也開放呢?你應該心里有數了吧,答案當然是——不能做。

            這就是語言的極限了。面向對象才用的single dispatch的方法,能做到的東西是很有限的。情況稍微復雜那么一點點——就像上面對兩個形狀求交這種正常的問題——寫起來都這么難受。

            那呼應一下標題,如果我們要設計一門語言,來支持上面這種multiple dispatch,那可以怎么修改語法呢?這里面分為兩種,第一種是像C++這樣運行時load dll不增加符號的,第二種是像C#這樣運行時load dll會增加符號的。對于前一種,其實我們可以簡單的修改一下語法:

            bool Intersect(switch Shape* s1, switch Shape* s2);
            
            bool Intersect(case Circle* s1, case Circle* s2){ ... }
            bool Intersect(case Circle* s1, case Rectangle* s2){ ... }
            bool Intersect(case Rectangle* s1, case Circle* s2){ ... }
            bool Intersect(case Rectangle* s1, case Rectangle* s2){ ... }

            然后修改一下編譯器,把這些東西翻譯成虛函數塞回原來的Shape類里面就行了。對于第二種嘛,其實就相當于Intersect的根節點、Circle和CC寫在dll1,Rectangle和CR、RC、RR寫在dll2,然后dll1運行時把dll2給動態地load了進來,再之后調用Intersect的時候就好像“虛函數已經進去了”一樣。至于要怎么做,這個大家回去慢慢思考一下吧,啊哈哈哈。

            posted @ 2013-05-24 19:08 陳梓瀚(vczh) 閱讀(11461) | 評論 (5)編輯 收藏
            僅列出標題
            共35頁: 1 2 3 4 5 6 7 8 9 Last 
            国产精品久久网| 国产精品成人无码久久久久久 | 久久亚洲中文字幕精品一区四 | 丰满少妇高潮惨叫久久久| 亚洲国产成人久久综合一区77| 久久久久久久97| 久久99国产精品成人欧美| 久久久久亚洲AV无码麻豆| 亚洲精品无码久久毛片| 1000部精品久久久久久久久| 久久精品国产亚洲av麻豆蜜芽 | 午夜精品久久久久9999高清| 久久久久中文字幕| 久久精品国产亚洲AV无码偷窥 | 亚洲国产成人久久综合区| 成人精品一区二区久久| 2021精品国产综合久久| 日韩精品无码久久久久久| 日韩十八禁一区二区久久| 成人亚洲欧美久久久久| 国内精品伊人久久久久影院对白| 亚洲精品乱码久久久久久按摩| 免费一级欧美大片久久网| 国内精品久久久久久久久 | 久久久久国产精品熟女影院| 波多野结衣久久一区二区| 四虎影视久久久免费| 亚洲国产精品综合久久网络| 久久婷婷五月综合色99啪ak| 久久久久噜噜噜亚洲熟女综合| 精品久久国产一区二区三区香蕉 | 久久综合亚洲鲁鲁五月天| 久久青青国产| 一级女性全黄久久生活片免费| 性做久久久久久久久老女人| 久久久久99这里有精品10| 尹人香蕉久久99天天拍| 久久久久久国产精品无码下载 | 国产精品岛国久久久久| 久久精品国产精品亚洲精品| 91精品日韩人妻无码久久不卡|