C# Actor的尷尬與F#美麗外表下的遺憾
在上一篇文章中,我們簡單解讀了Erlang在執行消息時候的方式。而現在,我們就一起來看看,C# Actor究竟出現了什么樣的尷尬。此外,我還打算用F#進行補充說明,最終我們會發現,雖然F#看上去很美,但是在實際使用過程中依舊有些遺憾。
Erlang中的Tag Message
老趙在上一篇文章里提到,Erlang中有一個“約定俗成”,使用“原子(atom)”來表示這條消息“做什么”,并使用“綁定(binding)”來獲取做事情所需要的“參數”。Erlang大拿,《Programming Erlang》一書的主要譯者jackyz同學看了老趙的文章后指出,這一點在Erlang編程規范中有著明確的說法,是為“Tag Message”:
5.7 Tag messages
All messages should be tagged. This makes the order in the receive statement less important and the implementation of new messages easier.
Don’t program like this:
- loop(State) ->
- receive
- ...
- {Mod, Funcs, Args} -> % Don't do this
- apply(Mod, Funcs, Args},
- loop(State);
- ...
- end.
If messages are synchronous, the return message should be tagged with a new atom, describing the returned message. Example: if the incoming message is tagged get_status_info, the returned message could be tagged status_info. One reason for choosing different tags is to make debugging easier.
This is a good solution:
- loop(State) ->
- receive
- ...
- {execute, Mod, Funcs, Args} -> % Use a tagged message.
- apply(Mod, Funcs, Args},
- loop(State);
- {get_status_info, From, Option} ->
- From ! {status_info, get_status_info(Option, State)},
- loop(State);
- ...
- end.
第一段代碼使用的模式為擁有三個“綁定”的“元組”。由于Erlang的弱類型特性,任何擁有三個元素的元組都會被匹配到,這不是一個優秀的實踐。在第二個示例中,每個模式使用一個“原子”來進行約束,這樣可以獲取到相對具體的消息。為什么說“相對”?還是因為Erlang的弱類型特性,Erlang無法對From和Option提出更多的描述。同樣它也無法得知execute或get_status_info這兩個tag的來源——當然,在許多時候,它也不需要關心是誰發送給它的。
在C#中使用Tag Message
在C#中模擬Erlang里的Tag Message很簡單,其實就是把每條消息封裝為Tag和參數列表的形式。同樣的,我們使用的都是弱類型的數據——也就是object類型。如下:
- public class Message
- {
- public object Tag { get; private set; }
- public ReadOnlyCollection﹤object> Arguments { get; private set; }
- public Message(object tag, params object[] arguments)
- {
- this.Tag = tag;
- this.Arguments = new ReadOnlyCollection﹤object>(arguments);
- }
- }
我們可以使用這種方式來實現一個乒乓測試。既然是Tag Message,那么定義一些Tag便是首要任務。Tag表示“做什么”,即消息的“功能”。在乒乓測試中,有兩種消息,共三個“含義”。Erlang使用原子作為tag,在.NET中我們自然可以使用枚舉:
- public enum PingMsg
- {
- Finished,
- Ping
- }
- public enum PongMsg
- {
- Pong
- }
在這里,我們使用簡單的ActorLite進行演示(請參考ActorLite的使用方式)。因此,Ping和Pong均繼承于Actor﹤Message>類,并實現其Receive方法。
對于Ping對象來說,它會維護一個計數器。每當收到PongMsg.Pong消息后,會將計數器減1。如果計數器為0,則回復一條PingMsg.Finished消息,否則就回復一個PingMsg.Ping:
- public class Ping : Actor﹤Message>
- {
- private int m_count;
- public Ping(int count)
- {
- this.m_count = count;
- }
- public void Start(Actor﹤Message> pong)
- {
- pong.Post(new Message(PingMsg.Ping, this));
- }
- protected override void Receive(Message message)
- {
- if (message.Tag.Equals(PongMsg.Pong))
- {
- Console.WriteLine("Ping received pong");
- var pong = message.Arguments[0] as Actor﹤Message>;
- if (--this.m_count > 0)
- {
- pong.Post(new Message(PingMsg.Ping, this));
- }
- else
- {
- pong.Post(new Message(PingMsg.Finished));
- this.Exit();
- }
- }
- }
- }
對于Pong對象來說,如果接受到PingMsg.Ping消息,則回復一個PongMsg.Pong。如果接受的消息為PingMsg.Finished,便立即退出:
- public class Pong : Actor﹤Message>
- {
- protected override void Receive(Message message)
- {
- if (message.Tag.Equals(PingMsg.Ping))
- {
- Console.WriteLine("Pong received ping");
- var ping = message.Arguments[0] as Actor﹤Message>;
- ping.Post(new Message(PongMsg.Pong, this));
- }
- else if (message.Tag.Equals(PingMsg.Finished))
- {
- Console.WriteLine("Finished");
- this.Exit();
- }
- }
- }
啟動乒乓測試:
new Ping(5).Start(new Pong());結果如下:
Pong received ping
Ping received pong
Pong received ping
Ping received pong
Pong received ping
Ping received pong
Pong received ping
Ping received pong
Pong received ping
Ping received pong
Finished
從上述代碼中可以看出,由于沒有Erlang的模式匹配,我們必須使用if…else…的方式來判斷消息的Tag,接下來還必須使用麻煩而危險的cast操作來獲取參數。更令人尷尬的是,與Erlang相比,在C#中使用Tag Message沒有獲得任何好處。同樣是弱類型,同樣得不到靜態檢查。那么好處在哪里?至少我的確看不出來。
C# Actor,強類型與弱類型的考慮
有朋友可能會說,C#既然是一門強類型的語言,為什么要學Erlang的Tag Message?為什么不把Ping定義為Actor﹤PingMessage>,同時把Pong定義為Actor﹤PingMessage>呢?
呃……我承認,在這里使用Tag Message的確有種“畫虎不成反類犬”的味道。不過,事情也不是您想象的那么簡單。因為在實際情況中,一個Actor可能與各種外部服務打交道,它會接受到各式各樣的消息。例如,它先向Service Locator發送一個請求,用于查詢數據服務的位置,這樣它會接受到一個ServiceLocatorResponse消息。然后,它會向數據服務發送一個請求,再接受到一個DataAccessResponse消息。也就是說,很可能我們必須把每個Actor都定義為Actor﹤object>,然后對消息進行類型判斷,轉換,再加以處理。
誠然,這種方法相對于Tag Message擁有了一定的強類型優勢(如靜態檢查)。但是如果您選擇這么做,就必須為各種消息定義不同的類型,在這方面會帶來額外的開發成本。要知道,消息的數量并不等于Actor類型的數量,即使是如Ping這樣簡單的Actor,都會發送兩種不同的消息(Ping和Finished),而且每種消息擁有各自的參數。一般來說,某個Actor會接受2-3種消息都是比較正常的狀況。在面對消息類型的汪洋時,您可能就會懷念Tag Message這種做法了。到時候您可能就會發牢騷說:
“弱類型就弱類型吧,Erlang不也用的好好的么……”
F#中的模式匹配
提到模式匹配,熟悉F#的同學們可能會歡喜不已。模式匹配是F#中的重要特性,它將F#中靜態類型系統的靈活性體現地淋漓盡致。而且——它還很能節省代碼(這點在老趙以前的文章中也有所提及)。那么我們再來看一次F#在乒乓測試中的表現。
首先還是定義PingMsg和PongMsg:
- type PingMsg =
- | Ping of PongMsg Actor
- | Finished
- and PongMsg =
- | Pong of PingMsg Actor
這里體現了F#類型系統中的Discriminated Unions。簡單地說,它的作用是把一種類型定義為多種表現形式,這個特性在Haskell等編程語言中非常常見。Discriminated Unions非常適合模式匹配,現在的ping對象和pong對象便可定義如下(在這里還是使用了ActorLite,而不是F#標準庫中的MailboxProcessor來實現Actor模型):
- let (﹤﹤) (a:_ Actor) msg = a.Post msg
- let ping =
- let count = ref 5
- { new PongMsg Actor() with
- override self.Receive(message) =
- match message with
- | Pong(pong) ->
- printfn "Ping received pong"
- count := !count - 1
- if (!count > 0) then
- pong ﹤﹤ Ping(self)
- else
- pong ﹤﹤ Finished
- self.Exit() }
- let pong =
- { new PingMsg Actor() with
- override self.Receive(message) =
- match message with
- | Ping(ping) ->
- printfn "Pong received ping"
- ping ﹤﹤ Pong(self)
- | Finished ->
- printf "Fininshed"
- self.Exit() }
例如在pong對象的實現中,我們使用模式匹配,減少了不必要的類型轉換和賦值,讓代碼變得簡潔易讀。還有一點值得順帶一提,我們在F#中可以靈活的定義一個操作符的作用,在這里我們便把“﹤﹤”定義為“發送”操作,避免Post方法的顯式調用。這種做法往往可以簡化代碼,從語義上增強了代碼的可讀性。例如,我們可以這樣啟動乒乓測試:
ping ﹤﹤ Pong(pong)至于結果則與C#的例子一模一樣,就不再重復了。
F#中的弱類型消息
可是,F#的世界就真的如此美好嗎?試想,我們該如何實現一個需要接受多種不同消息的Actor對象呢?我們只能這樣做:
- let another =
- { new obj Actor() with
- override self.Receive(message) =
- match message with
- | :? PingMsg as pingMsg ->
- // sub matching
- match pingMsg with
- | Ping(pong) -> null |> ignore
- | Finished -> null |> ignore
- | :? PongMsg as pongMsg ->
- // sub matching
- match pongMsg with
- | Pong(ping) -> null |> ignore
- | :? (string * int) as m ->
- // sub binding
- let (s, i) = m
- null |> ignore
- | _ -> failwith "Unrecognized message" }
由于我們必須使用object作為Actor接受到的消息類型,因此我們在對它作模式匹配時,只能進行參數判斷。如果您要更進一步地“挖掘”其中的數據,則很可能需要進行再一次的模式匹配(如PingMsg或PongMsg)或賦值(如string * int元組)。一旦出現這種情況,在我看來也變得不是那么理想了,我們既沒有節省代碼,也沒有讓代碼變得更為易讀。與C#相比,唯一的優勢可能就是F#中相對靈活的類型系統吧。
C# Actor不好用,F#也不行……那么我們又該怎么辦?
【編輯推薦】