關(guān)于這個話題,其實在(六)里面已經(jīng)討論了一半了。學(xué)過Haskell的都知道,這個世界上很多東西都可以用monad和comonad來把一些復(fù)雜的代碼給抽象成簡單的、一看就懂的形式。他們的區(qū)別,就像用js做一個復(fù)雜的帶著幾層循環(huán)的動畫,直接寫出來和用jquery的“回調(diào)”寫出來的代碼一樣。前者能看不能用,后者能用不能看。那有沒有什么又能用又能看的呢?我目前只能在Haskell、C#和F#里面看到。至于說為什么,當(dāng)然是因為他們都支持了monad和comonad。只不過C#作為一門不把“用庫來改造語言”作為重要特征的語言,并沒打算讓你們能跟haskell和F#一樣,把東西抽象成monad,然后輕松的寫出來。C#只內(nèi)置了yield return和async await這樣的東西。
把“用庫來改造語言”作為重要特征的語言其實也不多,大家熟悉的也就只有l(wèi)isp和C++,不熟悉的有F#。F#除了computation expression以外,還有一個type provider的功能。就是你可以在你的當(dāng)前的程序里面,寫一小段代碼,通知編譯器在編譯你的代碼的時候執(zhí)行以下(有點類似雞生蛋的問題但其實不是)。這段代碼可以生成新的代碼(而不是跟lisp一樣修改已有的代碼),然后給你剩下的那部分程序使用。例子我就不舉了,有興趣的大家看這里:http://msdn.microsoft.com/en-us/library/vstudio/hh361034.aspx。里面有一個例子講的是如何在F#里面創(chuàng)造一個強類型的正則表達(dá)式庫,而且并不像boost的spirit或者xpress那樣,正則表達(dá)式仍然使用字符串來寫的。這個正則表達(dá)式在編譯的時候就可以知道你有沒有弄錯東西了,不需要等到運行才知道。
Haskell和F#分別嘗試了monad/comonad和computation expression,為的就是能用一種不會失控(lisp的macro就屬于會失控的那種)方法來讓用戶自己表達(dá)屬于自己的可以天然被continuation passing style變換處理的東西。在介紹C#的async await的強大能力之前,先來講一下Haskell和F#的做法。為什么按照這個程序呢,因為Haskell的monad表達(dá)能力最低,其次是F#,最后是C#的那個。當(dāng)然C#并不打算讓你自己寫一個支持CPS變換的類型。作為補充,我將在這篇文章的最后,講一下我最近正在設(shè)計的一門語言,是如何把C#的yield return和async await都變成庫,而不是編譯器的功能的。
下面我將拋棄所有跟學(xué)術(shù)有關(guān)的內(nèi)容,只會留下跟實際開發(fā)有關(guān)系的東西。
一、Haskell和Monad
Haskell面臨的問題其實比較簡單,第一是因為Haskell的程序都不能有隱式狀態(tài),第二是因為Haskell沒有語句只有表達(dá)式。這意味著你所有的控制流都必須用遞歸或者CPS來做。從這個角度上來講,Monad也算是CPS的一種應(yīng)用了。于是我為了給大家解釋一下Monad是怎么運作的,決定來炒炒冷飯,說error code的故事。這個故事已經(jīng)在(七)里面講了,但是今天用的是Haskell,別有一番異域風(fēng)情。
大家用C/C++的時候都覺得處理起error code是個很煩人的事情吧。我也不知道為什么那些人放著exception不用,對error code那么喜歡,直到有一天,我聽到有一個傻逼在微博上講:“error code的意思就是我可以不理他”。我終于明白了,這個人是一個真正的傻逼。不過Haskell還是很體恤這些人的,就跟耶穌一樣,凡是信他就可以的永生,傻逼也可以。可惜的是,傻逼是學(xué)不會Monad的,所以耶穌只是個傳說。
由于Haskell沒有“引用參數(shù)”,所以所有的結(jié)果都必須出現(xiàn)在返回值里面。因此,倘若要在Haskell里面做error code,就得返回一個data。data就跟C語言的union一樣,區(qū)別是data是強類型的,而C的union一不小心就會傻逼了:
data Unsure a = Sure a | Error string
然后給一些必要的實現(xiàn),首先是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的聲明了。當(dāng)然就這么看是看不出Monad的強大威力的,所以我們還需要一個代碼。譬如說,給一個數(shù)組包含了分?jǐn)?shù),然后把所有的分?jǐn)?shù)都轉(zhuǎn)換成“牛逼”、“一般”和“傻逼”,重新構(gòu)造成一個數(shù)組。一個真正的Haskell程序員,會把這個程序分解成兩半,第一半當(dāng)然是一個把分?jǐn)?shù)轉(zhuǎn)成數(shù)字的東西:
// Tag :: integer -> Unsure string
Tag f =
if f < 0 then Error "分?jǐn)?shù)必須在0-100之間" else
if f<60 then Sure "傻逼" else
if f<90 then Sure "一般" else
if f<=100 then Sure "牛逼" else
Error "分?jǐn)?shù)必須在0-100之間"
后面就是一個循環(huán)了:
// TagAll :: [integer] -> Unsure [string]
TagAll [] = []
TagAll (x:xs) = do
first <- Tag x
remains <- TagAll xs
return first:remains
TagAll是一個循環(huán),把輸入的東西每一個都用Tag過一遍。如果有一次Tag返回失敗了,整個TagAll函數(shù)都會失敗,然后返回錯誤。如果全部成功了,那么TagAll函數(shù)會返回整個處理后的數(shù)組。
當(dāng)然一個循環(huán)寫成了非尾遞歸不是一個真正的Haskell程序員會做的事情,真正的Haskell程序員會把事情做成這樣(把>>=展開之后你們可能會覺得這個函數(shù)不是尾遞歸,但是因為Haskell是call by need的,所以實際上會成為一個尾遞歸的函數(shù)):
// 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函數(shù)的返回值”的代碼都沒有呢?這就是Haskell的Monad的表達(dá)能力的威力所在了。Monad的使用由do關(guān)鍵字開始,然后這個表達(dá)式可以被這么定義:
MonadExp
::= "do" FragmentNotNull
FragmentNotNull
::= [Pattern "<-"] Expression EOL FragmentNull
FragmentNull
::= FragmentNotNull
::= ε
意思就是說,do后面一定要有“東西”,然后這個“東西”是這么組成的:
1、第一樣要是一個a<-e這樣的東西。如果你不想給返回值命名,就省略“a<-”這部分
2、然后重復(fù)
這表達(dá)的是這樣的一個意思:
1、先做e,然后把結(jié)果保存進(jìn)a
2、然后做下面的事情
看到了沒有,“然后做下面的事情”是一個典型的continuation passing style的表達(dá)方法。但是我們可以看到,在例子里面所有的e都是Unsure T類型的,而a相應(yīng)的必須為T。那到底是誰做了這個轉(zhuǎn)化呢?
聰明的,哦不,正常的讀者一眼就能看出來,“<-”就是調(diào)用了我們之前在上面實現(xiàn)的一個叫做“>>=”的函數(shù)了。我們首先把“e”和“然后要做的事情”這兩個參數(shù)傳進(jìn)了>>=,然后>>=去解讀e,得到a,把a當(dāng)成“然后要做的事情”的參數(shù)調(diào)用了一下。如果e解讀失敗的到了錯誤,“然后要做的事情”自然就不做了,于是整個函數(shù)就返回錯誤了。
Haskell一下就來尾遞歸還是略微復(fù)雜了點,我們來寫一個簡單點的例子,寫一個函數(shù)判斷一個人的三科成績里面,有多少科是牛逼的:
// 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 == "牛逼"]
根據(jù)上文的描述,我們已經(jīng)知道,這個函數(shù)實際上會被處理成:
// 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 == "牛逼"]
>>=函數(shù)的定義是
instance Monad Unsure where
return = Sure
fail = Error
(Sure s) >>= f = f s
(Error e) >>= f = Error e
這是一個運行時的pattern matching。一個對參數(shù)帶pattern matching的函數(shù)用Haskell的case of寫出來是很難看的,所以Haskell給了這么個語法糖。但這個時候我們要把>>=函數(shù)展開在我們的“Count牛逼”函數(shù)里面,就得老老實實地用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可以忽略的傻逼)做的,到處檢查函數(shù)返回值的事情了?我覺得只要是一個正常人,都會選擇這種寫法的:
// 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,活生生的把“每次都檢查函數(shù)返回值”的代碼壓縮到了Monad里面,然后就可以把代碼寫成try-catch那樣的東西了。error code跟exception本來就是一樣的嘛,只是一個寫起來復(fù)雜所以培養(yǎng)了很多覺得錯誤可以忽略的傻逼,而一個只需要稍微訓(xùn)練一下就可以把代碼寫的很簡單罷了。
不過Haskell沒有變量,那些傻逼們可能會反駁:C/C++比Haskell復(fù)雜多了,你怎么知道exception就一定沒問題呢?這個時候,我們就可以看F#的computation expression了。
二、F#和computation expression
F#雖然被設(shè)計成了一門函數(shù)式語言,但是其骨子里還是跟C#一樣帶狀態(tài)的,而且編譯成MSIL代碼之后,可以直接讓F#和C#互相調(diào)用。一個真正的Windows程序員,從來不會拘泥于讓一個工程只用一個語言來寫,而是不同的大模塊,用其適合的最好的語言。微軟把所有的東西都設(shè)計成可以強類型地互操作的,所以在Windows上面從來不存在什么“如果我用A語言寫了,B就用不了”的這些事情。這是跟Linux的一個巨大的區(qū)別。Linux是沒有強類型的互操作的(字符串信仰者們再見),而Windows有。什么,Windows不能用來做Server?那Windows Azure怎么做的,bing怎么做的。什么,只有微軟才知道怎么正確使用Windows Server?你們喜歡玩的EVE游戲的服務(wù)器是怎么做的呢?
在這里順便黑一下gcc。錢(區(qū)別于財產(chǎn))對于一個程序員是很重要的。VC++和clang/LLVM都是領(lǐng)著工資寫的,gcc不知道是誰投資的(這也就意味著寫得好也漲不了工資)。而且我們也都知道,gcc在windows上編譯的慢出來的代碼還不如VC++,gcc在linux上編譯的慢還不如clang,在mac/ios上就不說了,下一個版本的xcode根本沒有什么gcc了。理想主義者們醒醒,gcc再見。
為什么F#有循環(huán)?答案當(dāng)然是因為F#有變量了。一個沒有變量的語言是寫不出循環(huán)退出條件的,只能寫出遞歸退出條件。有了循環(huán)的話,就會有各種各樣的東西,那Monad這個東西就不能很好地給“東西”建模了。于是F#本著友好的精神,既然大家都那么喜歡Monad,那他做出一個computation expression,學(xué)起來肯定就很容易了。
于是在F#下面,那個TagAll終于可以讀入一個真正的列表,寫出一個真正的循環(huán)了:
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里面的<-。只是因為這些東西放在了循環(huán)里,那么那個“Monad”表達(dá)出來就沒有Haskell的Monad那么純粹了。為了解決這個問題,F(xiàn)#引入了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()
所以說帶有副作用的語言寫出來的代碼又長,不帶副作用的語言寫出來的代碼又難懂,這之間很難取得一個平衡。
如果輸入的分?jǐn)?shù)數(shù)組里面有一個不在0到100的范圍內(nèi),那么for循環(huán)里面的“l(fā)et! tag = Tag xs.[i]”這句話就會引發(fā)一個錯誤,導(dǎo)致TagAll函數(shù)失敗。這是怎么做到的?
首先,Tag引發(fā)的錯誤是在for循環(huán)里面,也就是說,實際運行的時候是調(diào)用UnsuerBuilder類型的unsure.For函數(shù)來執(zhí)行這個循環(huán)的。For函數(shù)內(nèi)部使用“l(fā)et! r = Tag x”,這個時候如果失敗,那么let!調(diào)用的Bind函數(shù)就會返回Error s。于是unsure.Combine函數(shù)判斷第一個語句失敗了,那么接下來的語句“body r ; return this.For xs body”也就不執(zhí)行了,直接返回錯誤。這個時候For函數(shù)的遞歸終止條件就產(chǎn)生作用了,由一層層的return(F#自帶尾遞歸優(yōu)化,所以那個For函數(shù)最終會被編譯成一個循環(huán))往外傳遞,導(dǎo)致最外層的For循環(huán)以Error返回值結(jié)束。TagAll里面的unsure,Combine函數(shù)看到for循環(huán)完蛋了,于是return r也不執(zhí)行了,返回錯誤。
這個過程跟Haskell的那個版本做的事情完全是一樣的,只是由于F#多了很多語句,所以Monad展開成computation expression之后,表面上看起來就會復(fù)雜很多。如果明白Haskell的Monad在干什么事情的話,F(xiàn)#的computation expression也是很容易就學(xué)會的。
當(dāng)然,覺得“error code可以忽略”的傻逼是沒有可能的。
三、C#的yield return和async await
如果大家已經(jīng)明白了Haskell的>>=和F#的Bind(其實也是let!)就是一回事的話,而且也明白了我上面講的如何把do和<-變成>>=的方法的話,大家應(yīng)該對CPS在實際應(yīng)用的樣子心里有數(shù)了。不過,這種理解的方法實際上是相當(dāng)有限的。為什么呢?讓我們來看C#的兩個函數(shù):
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;
}
上面那個是關(guān)于yield return和IEnumerable<T>的例子,講的是Linq的Concat函數(shù)是怎么實現(xiàn)的。下面還有一個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;
}
這個函數(shù)講的是,如果你有一堆Task<T>,如何構(gòu)造出一個內(nèi)容來自于異步地挨個執(zhí)行tasks里面的每個Task<T>的Task<T[]>的方法。
大家可能會注意到,C#的yield return和await的“味道”,就跟Haskell的<-和>>=、F#的Bind和let!一樣。在處理這種語言級別的事情的時候,千萬不要去管代碼它實際上在干什么,這其實是次要的。最重要的是形式。什么是形式呢?也就是說,同樣一個任務(wù),是如何被不同的方法表達(dá)出來的。上面說的“味道”就都在“表達(dá)”的這個事情上面了。
這里我就要提一個問題了。
- Haskell有Monad,所以我們可以給自己定義的類型實現(xiàn)一個Monad,從而讓我們的類型可以用do和<-來操作。
- F#有computation expression,所以我們可以給自己定義的類型實現(xiàn)一個computation expression,從而讓我們的類型可以用let!來操作。
- C#有【什么】,所以我們可以給自己定義的類型實現(xiàn)一個【什么】,從而讓我們的類型可以用【什么】來操作?
熟悉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來表達(dá)的方法。
大家可能一下子不明白什么意思。Linq Provider和Monad是這么對應(yīng)的:
- fmap對應(yīng)于Select
- >>=對應(yīng)于SelectMany
- >>= + return也對應(yīng)與Select(回憶一下Monad這個代數(shù)結(jié)構(gòu)的幾個定理,就有這么一條)
然后諸如這樣的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 == "牛逼"]
就可以表達(dá)成:
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的這個表達(dá)方法跟yield return和async await一比,就有一種Monad和computation expression的感覺了。Monad只能一味的遞歸一個一個往下寫,而computation expression則還能加上分支循環(huán)異常處理什么的。C#的from in也是一樣,沒辦法表達(dá)循環(huán)異常處理等內(nèi)容。
于是上面提到的那個問題
C#有【什么】,所以我們可以給自己定義的類型實現(xiàn)一個【什么】,從而讓我們的類型可以用【什么】來操作?
其實并沒有回答完整。我們可以換一個角度來體味。假設(shè)IEnumerable<T>和Task<T>都是我們自己寫的,而不是.net framework里面的內(nèi)容,那么C#究竟要加上一個什么樣的(類似于Linq Provider的)功能,從而讓我們可以寫出接近yield return和async await的效果的代碼呢?如果大家對我的那篇《時隔多年我又再一次體驗了一把跟大神聊天的感覺》還有點印象的話,其實我當(dāng)時也對我自己提出了這么個問題。
我那個時候一直覺得,F(xiàn)#的computation expression才是正確的方向,但是我怎么搞都搞不出來,所以我自己就有點動搖了。于是我跑去問了Don Syme,他很斬釘截鐵的告訴我說,computation expression是做不到那個事情的,但是需要怎么做他也沒想過,讓我自己research。后來我就得到了一個結(jié)論。
四、Koncept(我正在設(shè)計的語言)的yield return和async await(問題)
Koncept主要的特征是concept mapping和interface。這兩種東西的關(guān)系就像函數(shù)和lambda表達(dá)式、instance和class一樣,是定義和閉包的關(guān)系,所以相處起來特別自然。首先我讓函數(shù)只能輸入一個參數(shù),不過這個參數(shù)可以是一個tuple,于是f(a, b, c)實際上是f.Invoke(Tuple.Create(a, b, c))的語法糖。然后所有的overloading都用類似C++的偏特化來做,于是C++11的不定模板參數(shù)(variadic template argument)在我這里就成為一個“推論”了,根本不是什么需要特殊支持就自然擁有的東西。這也是concept mapping的常用手法。最后一個跟普通語言巨大的變化是我刪掉了class,只留下interface。反正你們寫lambda表達(dá)時也不會給每個閉包命名字(沒有C++11的C++除外),那為什么寫interface就得給每一個閉包(class)命名字呢?所以我給刪去了。剩下的就是我用類似mixin的機制可以把函數(shù)和interface什么的給mixin到普通的類型里面去,這樣你也可以實現(xiàn)class的東西,就是寫起特別來麻煩,于是我在語法上就鼓勵你不要暴露class,改為全部暴露function、concept和interface。
不過這些都不是重點,因為除了這些差異以外,其他的還是有濃郁的C#精神在里面的,所以下面在講Koncept的CPS變換的時候,我還是把它寫成C#的樣子,Koncept長什么樣子以后我再告訴你們,因為Koncept的大部分設(shè)計都跟CPS變換是沒關(guān)系的。
回歸正題。之前我考慮了許久,覺得F#的computation expression又特別像是一個正確的解答,但是我怎么樣都找不到一個可以把它加入Koncept地方法。這個問題我從NativeX(這里、這里、這里和這里)的時候就一直在想了,中間兜了一個大圈,整個就是試圖山寨F#結(jié)果失敗的過程。為什么F#的computation expression模型不能用呢,歸根結(jié)底是因為,F#的循環(huán)沒有break和continue。C#的跳轉(zhuǎn)是自由的,不僅有break和continue,你還可以從循環(huán)里面return,甚至goto。因此一個for循環(huán)無論如何都表達(dá)不成F#的那個函數(shù):M<U> For(IEnumerable<T> container, Func<T, M<U>> body);。break、continue、return和goto沒辦法表達(dá)在類型上。
偉大的先知Eric Meijer告訴我們:“一個函數(shù)的類型表達(dá)了關(guān)于函數(shù)的業(yè)務(wù)的一切”。為什么我們還要寫函數(shù)體,是因為編譯器還沒有聰明到看著那個類型就可以幫我們把代碼填充完整。所以其實當(dāng)初看著F#的computation expression的For的定義的時候,是因為我腦筋短路,沒有想起Eric Meijer的這句話,導(dǎo)致我浪費了幾個月時間。當(dāng)然我到了后面也漸漸察覺到了這個事情,產(chǎn)生了動搖,自己卻無法確定,所以去問了Don Syme。于是,我就得到了關(guān)于這個問題的結(jié)論的一半:在C#(其實Koncept也是)支持用戶可以自由添加的CPS變換(譬如說用戶添加IEnumerable<T>的時候添加yield return和yield break,用戶添加Task<T>的時候添加await和return)的話,使用CPS變換的那段代碼,必須用控制流圖(control flow graph)處理完之后生成一個狀態(tài)機來做,而不能跟Haskell和F#一樣拆成一個一個的小lambda表達(dá)式。
其實C#的yield return和async await,從一開始就是編譯成狀態(tài)機的。只是C#沒有開放那個功能,所以我一直以為這并不是必須的。想來微軟里面做語言的那幫牛逼的人還是有牛逼的道理的,一下子就可以找到問題的正確方向,跟搞go的二流語言專家(盡管他也牛逼但是跟語言一點關(guān)系也沒有)是完全不同的。連Mozilla的Rust的設(shè)計都比go強一百倍。
那另一半的問題是什么呢?為了把問題看得更加清楚,我們來看兩個長得很像的yield return和async await的例子。為了把本質(zhì)的問題暴露出來,我決定修改yield return的語法:
- 首先把yield return修改成yield
- 其次吧yield break修改成return
- 然后再給函數(shù)打上一個叫做seq的東西,跟async對稱,就當(dāng)他是個關(guān)鍵字
- 給所有CPS operator加上一個感嘆號,讓他變得更清楚(這里有yield、await和return)。為什么return也要加上感嘆號呢?因為如果我們吧seq和aysnc摘掉的話,我們會發(fā)現(xiàn)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();
}
于是問題就很清楚了。如果我們想讓用戶自己通過類庫的方法來實現(xiàn)這些東西,那么yield和await肯定是兩個函數(shù),因為這是C#里面唯一可以用來寫代碼的東西,就算看起來再奇怪,也不可能是別的。
- seq和async到底是什么?
- seq下面的yield和return的類型分別是什么?
- async下面的await和return的類型分別是什么?
其實這里還有一個謎團(tuán)。其實seq返回的東西應(yīng)該是一個IEnumerator<T>,只是因為C#覺得IEnumerable<T>是更好地,所以你兩個都可以返回。那么,是什么機制使得,函數(shù)可以構(gòu)造出一個IEnumerable<T>,而整個狀態(tài)機是在IEnumerator<T>的MoveNext函數(shù)里面驅(qū)動的呢?而async和Task<T>就沒有這種情況了。
首先解答第一個問題。因為yield、return和await都是函數(shù),是函數(shù)就得有個namespace,那我們可以拿seq和async做namespace。所以seq和async,設(shè)計成兩個static class也是沒有問題的。
其次,seq的yield和return修改了某個IEnumerator<T>的狀態(tài),而async的await和return修改了某個Task<T>的狀態(tài)。而seq和async的返回值分別是IEnumerable<T>和Task<T>。因此對于一個CPS變換來說,一共需要兩個類型,第一個是返回值,第二個是實際運行狀態(tài)機的類。
第三,CPS變換還需要有一個啟動函數(shù)。IEnumerator<T>的第一次MoveNext調(diào)用了那個啟動函數(shù)。而Task<T>的Start調(diào)用了那個啟動函數(shù)。啟動函數(shù)自己維護(hù)著所有狀態(tài)機的內(nèi)容,而狀態(tài)機本身是CPS operator們看不見的。為什么呢?因為一個狀態(tài)機也是一個類,這些狀態(tài)機類是沒有任何公共的contract的,也就是說無法抽象他們。因此CPS operator必須不能知道狀態(tài)機類。
而且yield、return和await都叫CPS operator,那么他們不管是什么類型,本身肯定看起來像一個CPS的函數(shù)。之前已經(jīng)講過了,CPS函數(shù)就是把普通函數(shù)的返回值去掉,轉(zhuǎn)而添加一個lambda表達(dá)式,用來代表“拿到返回之后的下一步計算”。
因此總的來說,我們拿到了這四個方程,就可以得出一個解了。解可以有很多,我們選擇最簡單的部分。
那現(xiàn)在就開始來解答上面兩個TakeWhile最終會被編譯成什么東西了。
五、Koncept(我正在設(shè)計的語言)的yield return和async await(seq答案)
首先來看seq和yield的部分。上面講到了,yield和return都是在修改某個IEnumerator<T>的狀態(tài),但是編譯器自己肯定不能知道一個合適的IEnumerator<T>是如何被創(chuàng)建出來的。所以這個類型必須由用戶來創(chuàng)建。而為了第一次調(diào)用yield的時候就已經(jīng)有IEnumerator<T>可以用,所以CPS的啟動函數(shù)就必須看得到那個IEnumerator<T>。但是CPS的啟動函數(shù)又不可能去創(chuàng)建他,所以,這個IEnumerator<T>對象肯定是一個continuation的參數(shù)了。
看,其實寫程序都是在做推理的。盡管我們現(xiàn)在還不知道整個CPS要怎么運作,但是隨著這些線索,我們就可以先把類型搞出來。搞出了類型之后,就可以來填代碼了。
- 對于yield,yield接受了一個T,沒有返回值。一個沒有返回值的函數(shù)的continuation是什么呢?當(dāng)然就是一個沒有參數(shù)的函數(shù)了。
- return則連輸入都沒有。
- 而且yield和return都需要看到IEnumerator<T>。所以他們肯定有一個參數(shù)包含這個東西。
那么這三個函數(shù)的類型就都確定下來了:
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,函數(shù)結(jié)束的意思就是不會有一個continuation*/);
}
什么是seq_Enumerator<T>呢?當(dāng)然是我們那個“某個IEnumerator<T>”的真是類型了。
于是看著類型,唯一可能的有意義又簡單的實現(xiàn)如下:
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函數(shù)最終會變成:
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這個函數(shù)是真實存在的,所以他也要被改寫:
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會調(diào)用哪個CreateCps函數(shù),然后把原來的函數(shù)體經(jīng)過CFG的處理之后,得到一個狀態(tài)機。在狀態(tài)機內(nèi)所有調(diào)用CPS operator的地方(就是yield!和return!),都把“接下來的事情”當(dāng)成一個參數(shù),連同那個原本寫上去的CPS operator的參數(shù),還有controller(在這里是seq_Enumeartor<T>)一起傳遞過去。而return是帶有特殊的寓意的,所以它調(diào)用一次exit之后,就沒有“然后——也就是continuation”了。
現(xiàn)在回過頭來看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,函數(shù)結(jié)束的意思就是不會有一個continuation*/);
}
其實想一想,CPS的自然屬性決定了,基本上就只能這么定義它們的類型。而他們的類型唯一定義了一個最簡單有效的函數(shù)體。再次感嘆一下,寫程序就跟在做推理完全是一摸一樣的。
六、Koncept(我正在設(shè)計的語言)的yield return和async await(async答案)
因為CPS operator都是一樣的,所以在這里我給出async類型的聲明,然后假設(shè)Task<T>的樣子長的就跟C#的System.Tasks.Task<T>一摸一樣,看看大家能不能得到async下面的幾個函數(shù)的實現(xiàn),以及上面那個針對Task<T>的TakeWhile函數(shù)最終會被編譯成什么:
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是有參數(shù)的,所以跟seq的exit不一樣*/
{
/*請自行填補*/
}
}
public class FuturePromiseTask<T> : Task<T>
{
/*請自行填補*/
}
posted on 2013-07-26 19:12
陳梓瀚(vczh) 閱讀(14697)
評論(15) 編輯 收藏 引用 所屬分類:
啟示