using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }

sample_map_device := class(creative_device):

    var CompletedAgents:[agent]logic = map{}

    Complete(InAgent:agent):void=
        if(set CompletedAgents[InAgent] = true){}

    Reset(InAgent:agent):void=
        if(set CompletedAgents[InAgent] = false){}

    IsCompleted(InAgent:agent)<decides><transacts>:void=
        CompletedAgents[InAgent]?

    DoForCompletedAgents(InFunction:agent->void):void=
        for:
            AgentElement -> State : CompletedAgents
            State?
        do:
            InFunction(AgentElement)

sample_array_device := class(creative_device):

    var CompletedAgents:[]agent = array{}

    Complete(InAgent:agent):void=
        if(not CompletedAgents.Find[InAgent]):
            set CompletedAgents += array{InAgent}

    Reset(InAgent:agent):void=
        set CompletedAgents = CompletedAgents.RemoveAllElements(InAgent)

    IsCompleted(InAgent:agent)<decides><transacts>:void=
        CompletedAgents.Find[InAgent]

    DoForCompletedAgents(InFunction:agent->void):void=
        for:
            AgentElement : CompletedAgents
        do:
            InFunction(AgentElement)

まったく同じ結果が返ってくるデバイスを二つ用意しました。これらは「達成したか」のフラグを保存しています。

さて、Verseの知識がある方ならどこが違うかは一目でわかりますね。そう、CompletedAgentsの型が「agentをキーにしたlogicのmap型」と「agentを配列に取る型」と違っています。

多くの方は、mapを用いてlogicのフラグの管理をすると思います。しかし、私はこの管理の仕方が好きではないため、配列を用いた管理を普段使用しています。

理由1

というのも、第一にmapでの管理は全て失敗コンテキスト内で行われます。「set XXX[key] = value」をいちいちif内で書くというのは、自分としては可読性の意味であまり好きではありません。しかも、ifでは書くものの理論上はこのifが失敗することはありません。つまり、形式として必要なifというわけなんです。そのために、ifの中身を書かずに{}ですぐ閉じたりするのですが…あまりスマートではありませんよね。

配列で管理する場合、単純に配列にagentを足してあげるだけで大丈夫です。重複も考えずに足すだけでよければ、Findでのチェックも必要ないかもしれません(ただ、無限にデータが溜まっていくのは良くないので確認しています)

消す際はRemoveAllElementsという失敗関数ではない専用の関数も用意されています。そのため、いちいちifを経由しなくてもagentのフラグをオフにできるのです。

理由2

そもそも、logicというのは「true」と「false」という二つの状態を持つ型です。この二つの状態というのは、見方を変えれば「存在する」か「存在しないか」とも取れるはずです。

つまり、配列内に「存在する場合(Findで見つかる)」場合はlogicのtrueとして扱い、「存在しない場合(Findで見つからない)」はfalseとして扱うことで、ほぼ同じ感覚で管理できるはずです。

理由3

logicの状態を取りだす際も、配列の方がケアレスミスを減らせると思います。

# 配列の場合
CompletedAgents.Find[Agent]

# mapの場合
CompletedAgents[Agent] #このままだとダメ
CompletedAgents[Agent]? # ?を足してあげる

マップの場合、存在そのものが失敗する可能性があるため、中身の状態を確認せずとも「マップに登録されているか」で失敗かどうかが確認されます。そのため、その中身に対して「?」で状態を確認していなくても、エラーも出ず通常通りコードは動くはずです。

この場合、Reset関数でfalseに変更したとしても、map自体には登録されているため、ifは常に成功します。また、Complete関数をまだ呼び出していない場合も、mapには登録されていないためifは失敗します。これを、「mapに登録されているかのif」を「logicの状態に基づくif」として勘違いしてしまう可能性もあるわけです(とはいえ、注意すればよい話ではありますが)

これを配列を使うことで、「.Find」と条件を明示する必要があるため上記のようなミスを減らせると思います。

理由3

DoForCompletedAgents関数を見ていただくとわかる通り、forで回す際も配列の方が良かったりします。

というのも、map型は登録された値をすべて取り出すという作業をしているので、取り出し後に「値がtrueか」という工程も挟む必要があるのです。

それに対し、配列であればfalseのタイミングで配列から消しているため、forで実行されることもありません。

こちらもたった一行の差ですが、一目で処理がわかりやすいと思うので、配列の方が好きだったりします。

最後に

ということで、雑なメモとして自論を語りました。

まあ、当然コードの方法なんて好きに書けばいいと思っているので、この方法が絶対正義とは思いません。ただ、、、一応便利だよっていうのと、こういう観点でコードを書くのも楽しいよって紹介でした

また機会があればこういう記事も書きたいな…ということで。以上、ごろ助でした!