考不上三本也能給自己心愛的語言加上Coroutine(四)

你現在所閱讀的並不是第一篇文章,你可能想看目錄和前言。

這是《三本》的Coroutine系列的最後一篇了。雖然《三本》以後還會有,但是Coroutine從此就沒有了。今天要說的是,如何把yield return和await這樣的東西編譯成Coroutine。如果你想把這兩個語法寫死在語言裡面的話,過程是相當粗暴的。但是我現在要說的是 GacUI的Workflow腳本語言 是如何做的。

在做這種事情的時候,你先要在紙上不斷地嘗試各種不同的語法,最後找到一個最有感覺的定下來。這次我給腳本加上Coroutine的功能的時候,選擇了這樣的語法:

func GetNumbers() : int{}n${ntfor(i in range[0, 4])nt{ntt$Yield i;nt}n}n

熟悉C#的人應該相當容易理解。"${"這兩個符號之間其實省略了一個名字,這個名字指向了一個Coroutine的Provider,這個Provider就是具體實現$Yield等功能的一個類。我這裡還提供了$Join,讓你直接yield一個集合出去,不用跟C#一樣要寫個for循環裡面慢慢yield return。

大部分語言都是使用偏特化(或者pattern matching,或者type class,隨你喜歡怎麼叫)來讓語言找到這個Provider的。但是Workflow作為一門專門用來跟C++交互的語言,對象模型必然就跟COM差不多,那麼也就沒有什麼模板。C++/CX表面看起來好像支持泛型,實際上你去找生成的IDL文件用MIDL的CX版本去編譯,會有驚喜(逃

沒有模板的語言,要支持Provider的查找,自然有自己的辦法。我這裡選擇了使用類型的名字去匹配。既然在這裡我們把"$Enumerable{"省去變成了"${",那麼只能從函數的返回值名字入手了。int{}這個類型其實是不存在的,它的全稱是system::Enumerable<int>^。而Workflow裡面又沒有泛型,所以system::Enumerable<int>^實際上就是system::Enumerable^(^就是vl::Ptr,跟shared_ptr是同一個意思)。那麼在這裡我就會去枚舉這個類型自己和所有基類的名字,按順序列出來:

  • system::Enumerable

  • system::Interface

然後加上一個後綴

  • system::EnumerableCoroutine

  • system::InterfaceCoroutine

然後看看第幾個名字最先存在。如果你寫的是"$Enumerable{"的話,我會在當前的上下文里查找Enumearble類型,如果存在的話就試試看加上Coroutine後綴,如果失敗了就直接找EnumerableCoroutine。

那麼顯而易見的,這次的Coroutine Provider就是一個叫做system::EnumerableCoroutine的類,這個類實際上是在C++裡面的。作為一個專門跟C++對象交互的腳本,當然可以隨便調用C++的東西啊。在這個文件的62行,我們可以發現這個類真實的身份是vl::reflection::description::EnumerableCoroutine,它的聲明和實現分別在GuiTypeDescriptorPredefined.h:234和GuiTypeDescriptorPredefined.cpp:137。

背景就先講到這裡。那麼到底要如何去定義EnumerableCoroutine類呢?我們先來思考一下,如果要直接使用Coroutine來實現一個返回0到4的IEnumerable<T>,那麼到底要怎麼做?在這裡先簡單介紹一下Workflow裡面Coroutine介面的樣子:

enum class CoroutineStatusn{ntWaiting,ntExecuting,ntStopped,n};nnclass CoroutineResult : public virtual IDescriptable, public Description<CoroutineResult>n{nprotected:ntValue result;ntPtr<IValueException> failure;nnpublic:ntValue GetResult();ntvoid SetResult(const Value& value);ntPtr<IValueException> GetFailure();ntvoid SetFailure(Ptr<IValueException> value);n};nnclass ICoroutine : public virtual IDescriptable, public Description<ICoroutine>n{npublic:ntvirtual void Resume(bool raiseException, Ptr<CoroutineResult> output) = 0;ntvirtual Ptr<IValueException> GetFailure() = 0;ntvirtual CoroutineStatus GetStatus() = 0;n};n

這跟之前幾篇文章的IShitable類其實是差不多的,只是這裡多了一些細節。Resume的參數先別管,實際上ICoroutine類只有Resume函數是重要的,你一直調用它,直到返回true為止。

我們先嘗試一下用Workflow的Coroutine語法來表達循環輸出0到4:(在這裡$coroutine就等於SHIT_CALLABLE!,而$pause就等於SHIT!,區別只是$coroutine是一個lambda表達式)

$coroutinen{n for(i in range[0, 4])n {n $pausen {n DO_SOMETHING_WITH(i);n }n }n}n

這個表達式就返回一個system::Coroutine^介面的實例,就是上面的ICoroutine。然後我們就可以來著手寫一個Enumerable介面的實現了。我這裡的Enumerable介面跟C#完全一致,所以大家不需要學習也應該知道如何做(這裡的object相當於COM的Variant類型):

func GetNumbers() : int{}n{nnreturn cast int{} new Enumerable^n{n override func CreateEnumerator() : Enumerator^n {n return new Enumerator^n {n var coroutine : Coroutine^ = null;n var current : object = null;n var index = -1;nn override func GetCurrent() : object { return current; }n override func GetIndex() : int { return index; }nn override func Next() : booln {n if (coroutine is null)n {n coroutine = $coroutinen {n for(i in range[0, 4])n {n $pausen {n DO_SOMETHING_WITH(i);n }n }n };n }n if (coroutine.Status == Stopped) return false;n if (!coroutine.Resume(true, null)) return false;n return true;n }n }n }n};nn}n

顯而易見地,這個DO_SOMETHING_WITH(i)應該去修改current的值,讓它等於i,然後index自增。$pause的這個大括弧其實意思是說,在設置了Status==Waiting並且寫好下一個跳轉的目的,到暫停之間,你要干一些什麼事情。對於Enumerable來說,DO_SOMETHING_WITH在$pause之前做還是在$pause裡面做都是一樣的,因為畢竟是一個單線程的東西。所以大家也先不要糾結這個。

於是我們就得到了這樣的代碼:

$pausen{n current = i;n index = index + 1;n}n

好了,我們這個Enumerable已經完成了。現在要開始來想,如果我們要把$coroutine語句拿到這個類的外面去,那要怎麼辦?顯然$pause裡面的current和index,就要把this指針替換成一個名字,代碼我們先拆開一半:

interface MyClosureEnumerator : Enumeratorn{n func OnYield(value : object) : void;n}nnfunc CreateCoroutine(impl : MyClosureEnumerator*) : Coroutine^n{n return $coroutinen {n for(i in range[0, 4])n {n $pausen {n impl.OnYield(i);n }n }n };n}nnfunc GetNumbers() : int{}n{nnreturn cast int{} new Enumerable^n{n override func CreateEnumerator() : Enumerator^n {n return new MyClosureEnumerator^n {n ....nn override func Next() : booln {n if (coroutine is null)n {n coroutine = CreateCoroutine(this);n }n ....n }n }n }n};nn}n

這兩份代碼是完全等價的。現在看起來已經有點$Yield的樣子了。但是還有一個問題沒有解決。就是顯然對於所有想要產生Enumerable^的Coroutine來說,那一大段new Enumerable^的代碼肯定是相同的,而不同的內容有不同的CreateCoroutine函數,那到底要怎樣才能把依賴翻轉過來呢?實際上針對這個具體的例子,事情是相當的簡單,只要把CreateCoroutine變成一個參數就可以了:

interface MyClosureEnumerator : Enumeratorn{n func OnYield(value : object) : void;n}nnfunc CreateCoroutine(impl : MyClosureEnumerator*) : Coroutine^n{n return $coroutinen {n for(i in range[0, 4])n {n $pausen {n impl.OnYield(impl, i);n }n }n };n}nnfunc CreateEnumerable(creator : func(MyClosureEnumerator*):Coroutine^) : Enumerable^n{n // 裡面對CreateCoroutine的調用就變成了對creator的調用n return new Enumerable^ { ... };n}nnfunc GetNumbers() : int{}n{n return cast int{} CreateEnumerable(CreateCoroutine);n}n

但是這仍然不足以讓我們實現完全的自動化。一個最終的目標,就是用戶提供一個EnumerableCoroutine類,然後我們把Coroutine編譯成對EnumerableCoroutine的調用。那麼這個EnumearbleCoroutine類裡面到底要有什麼東西呢?其實到這裡應該很清楚了:

  • CreateEnumerable
  • impl.OnYield(i);
  • interface MyClosureEnumerator*

這樣我們就可以嘗試整理一下:

class EnumerableCoroutinen{n interface IImpl : Enumeratorn {n func OnYield(value : object) : void;n }nn static func Yield(impl : IImpl*, value : object) : voidn {n impl.OnYield(value);n }nn static func Create(creator : func(MyClosureEnumerator*):Coroutine^) : Enumerable^n {n return new Enumerable^ { ... };n }n}nnfunc GetNumbers() : int{}n{n return cast int{} EnumerableCoroutine.Create(n func(impl : IImpl*) : Coroutine^n {n return $coroutinen {n for(i in range[0, 4])n {n $pausen {n EnumerableCoroutine.Yield(impl, i);n }n }n };n }n );n}n

現在應該很清楚了。這已經是一個帶EnumerableCoroutine的完整的程序了,對比一下一開始的代碼:

func GetNumbers() : int{}n${ntfor(i in range[0, 4])nt{ntt$Yield i;nt}n}n

實際上編譯器在看到這個代碼之後,要做的就是:

  • 通過規則找到EnumerableCoroutine,在這裡我們叫P
  • $Yield a,b,c;翻譯成$pause{ P.Yield(impl, a, b, c); }
  • 整個函數翻譯成{ return P.Create(func(impl : <從P獲得的impl類型>) : ICoroutine^ { return $coroutine { ... }; }); }

當然光做到這裡是不行的,我們還有一些細節要做,譬如說return怎麼辦, 譬如說$Await要獲取返回值怎麼辦,雖然這些是問題,但已經不是什麼大問題了,使用類似的手段就可以解決。

不知道大家還記不記得Resume的第二個參數CoroutineStatus^,其實這就是用來獲取上一次$pause之後的結果的,譬如說$Await的返回值。$Await函數本身當然是沒有返回值的,因為事情沒做完要先$pause,返回值肯定以後才能給。因此你就想辦法在下次調用Resume的時候傳進來就可以了。這裡加一個參數就很合適。

在實現EnumerableCoroutine的同一對文件裡面,就有IAsync和AsyncCoroutine的實現。這裡的AsyncCoroutine實際上只是個殼,而真正的事情是IAsyncScheduler做的。每一個線程有自己的一個scheduler,一個Async(C#的Task)在哪個線程啟動,就回去拿到哪個線程的scheduler。到時候具體的實現當然是GacUI提供的,而單元測試裡面的scheduler顯然不可能真的去跑多線程,不然就沒有一個固定的調用順序可以測量了。

$Await就一定要放在$pause裡面,因為當$Await沒執行完的時候,可能被$Await的Async對象已經瞬間跑完要執行continuation了,這個時候如果我們沒有先設置好暫停狀態,那麼他就會讀到Executing,然後GG。至於為什麼AsyncCoroutine要那樣寫,就做為我布置給你們的一個作業,你們自己去看。

Coroutine到這裡就圓滿結束了,如果你們有什麼還看不懂,害怕自己不是一個合格的三本學生的話,可以來留言,如果東西多我就再寫一篇。接下來我將會繼續GacUI的開發,然後抽空寫《三本》系列的下一部:《考不上三本也能實現數據綁定》。不要覺得現在XAML也好,各種離譜的前端UI框架也好,數據綁定是一個神奇的東西,這當然不是!實現數據綁定所需要的所有知識裡面,並沒有什麼是考不上三本就一定學不會的!

當然了,這一篇肯定不會跟Coroutine一樣過幾天就發布了。我先繼續折騰GacUI,應該也不需要等太久,總之你們先慢慢等(逃


推薦閱讀:

成人出國一個月遊學,強化英語,或學習新語言,怎麼找遊學學校?
成人教育和網路教育到底哪個更好?
除了全日制大學和成人教育還有其他讀大學的渠道嗎?
上戲成人教育,你怎麼看?
作為基層教師,面對生源素質差的教學環境,如何調整自己的心態,做好自己的工作?

TAG:编程 | 成人教育 | 协程 |