C#Actorに適したメッセージ実行方式(2):C#Actorの気まずさ

16758 ワード

前回の記事では,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.

The new message {get_status_info, From, Option} will introduce a conflict if it is placed below the {Mod, Func, Args} message.
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.

最初のセグメントコードで使用されるモードは、3つのバインドを持つメタグループです.Erlangの弱いタイプの特性のために,3つの要素を持つどのメタグループも一致し,これは優れた実践ではない.第2の例では、各モードは、比較的特定のメッセージを取得するために「原子」を使用して制約されます.どうして「相対」と言うのですか.やはりErlangの弱いタイプの特性のため、ErlangはFromとOptionについてより多くの説明を提出することができません.同様にexecuteやget_もわかりませんstatus_infoという2つの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は「何をするか」、すなわちメッセージの「機能」を表す.卓球のテストでは、2つのメッセージがあり、3つの「意味」があります.Erlangはtagとして原子を用い,.NETでは当然列挙を用いることができる.
public enum PingMsg
{ 
    Finished,
    Ping
}

public enum PongMsg
{ 
    Pong
}

ここでは、簡単なActorLiteを使用してプレゼンテーションを行います(ActorLiteの使い方を参照).したがって,PingとPongはいずれもActorクラスに継承され,その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#は強いタイプの言語である以上、なぜErlangのTag Messageを学ぶのかという友人もいるかもしれません.なぜPingをActorと定義し、PongをActorと定義しないのですか?
ええと、ここでTag Messageを使うのは確かに「虎を描くのは反類犬ではない」という味がします.しかし、物事もあなたが想像していたほど簡単ではありません.実際の状況では、1つのActorがさまざまな外部サービスと付き合う可能性があるため、さまざまなメッセージが受信されます.たとえば、データ・サービスの場所を問い合わせるためのリクエストをサービス・Locatorに送信すると、サービス・LocatorResponseメッセージが受信されます.その後、データ・サービスにリクエストが送信され、DataAccessResponseメッセージが受信されます.すなわち,各ActorをActorとして定義し,メッセージのタイプ判断,変換,処理を行わなければならない可能性が高い.
確かに,この方法はTag Messageに対して一定の強いタイプの利点(例えば静的検査)を有する.しかし、これを選択すると、さまざまなメッセージに異なるタイプを定義する必要があります.この点では、追加の開発コストがかかります.メッセージの数はActorタイプの数に等しくなく、Pingのような簡単なActorでも2つの異なるメッセージ(PingとFinished)が送信され、各メッセージにはそれぞれのパラメータがあることを知っておく必要があります.一般的に、あるActorが2~3種類のメッセージを受信するのは正常な状況です.メッセージタイプの汪洋に直面すると、Tag Messageというやり方が懐かしいかもしれません.その時になると文句を言うかもしれません.
「弱いタイプは弱いタイプにしよう.エランもちゃんと使ってるじゃないか…」
F#でのモードマッチング
パターンマッチングといえば、F#に詳しい学生たちは喜ぶかもしれません.モードマッチングはF#の重要な特性であり、F#の静的タイプシステムの柔軟性を十分に体現しています.また、コードを節約することもできます(趙さんの以前の文章でも言及されています).では、卓球テストでのF#のパフォーマンスをもう一度見てみましょう.
まずPingMsgとPongMsgを定義します.
type PingMsg = 
    | Ping of PongMsg Actor
    | Finished
and PongMsg = 
    | Pong of PingMsg Actor

ここではF#タイプシステムにおけるDiscriminated Unionsを体現している.簡単に言えば、Haskellなどのプログラミング言語でよく見られる1つのタイプを複数の表現形式として定義する役割を果たしています.Discriminated Unionsはモードマッチングに適しており、現在のpingオブジェクトとpongオブジェクトは以下のように定義できます(ここではF#標準ライブラリのMailboxProcessorではなくActorLiteを使用しています):
let (<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#ではオペレータの役割を柔軟に定義できます.ここでは「<
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" }

オブジェクトをActorが受信したメッセージ・タイプとして使用する必要があるため、パターンマッチングを行うにはパラメータ判断しかできません.データをさらに「マイニング」するには、PingMsgやPongMsgなどのもう一度パターンマッチングやstring*intメタグループなどの付与が必要になる可能性があります..このような状況になると、私から見ればそれほど理想的ではなく、コードを節約したり、コードを読みやすくしたりすることはありません.C#に比べて、F#の比較的柔軟なタイプのシステムが唯一のメリットかもしれません.
C#は使いにくくて、F#もだめです......それでは私达はまたどうすればいいですか?
関連記事
  • C#Actorに適したメッセージ実行方式(1):Erlangにおけるモードマッチング
  • C#Actorに適したメッセージ実行方式(2):C#Actorの気まずさ
  • C#Actorに適したメッセージ実行方式(3):中には見られないソリューション
  • C#Actorに適したメッセージ実行方式(4):段階的総括
  • C#Actorに適したメッセージ実行方式(5):簡単なネットワーク爬虫類
  • C#Actorに適したメッセージ実行方式(6):コヒーレントとインバータ