關于這個話題,其實在(六)里面已經討論了一半了。學過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!一樣。在處理這種語言級別的事情的時候,千萬不要去管代碼它實際上在干什么,這其實是次要的。最重要的是形式。什么是形式呢?也就是說,同樣一個任務,是如何被不同的方法表達出來的。上面說的“味道”就都在“表達”的這個事情上面了。
這里我就要提一個問題了。
- Haskell有Monad,所以我們可以給自己定義的類型實現一個Monad,從而讓我們的類型可以用do和<-來操作。
- F#有computation expression,所以我們可以給自己定義的類型實現一個computation expression,從而讓我們的類型可以用let!來操作。
- 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是這么對應的:
- fmap對應于Select
- >>=對應于SelectMany
- >>= + 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的語法:
- 首先把yield return修改成yield
- 其次吧yield break修改成return
- 然后再給函數打上一個叫做seq的東西,跟async對稱,就當他是個關鍵字
- 給所有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#里面唯一可以用來寫代碼的東西,就算看起來再奇怪,也不可能是別的。
- seq和async到底是什么?
- seq下面的yield和return的類型分別是什么?
- 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要怎么運作,但是隨著這些線索,我們就可以先把類型搞出來。搞出了類型之后,就可以來填代碼了。
- 對于yield,yield接受了一個T,沒有返回值。一個沒有返回值的函數的continuation是什么呢?當然就是一個沒有參數的函數了。
- return則連輸入都沒有。
- 而且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 on 2013-07-26 19:12
陳梓瀚(vczh) 閱讀(14727)
評論(15) 編輯 收藏 引用 所屬分類:
啟示