見出し画像

119. C# コードジェネレータバグフィックス ~ その3

前回の記事

はじめに

さてと…、今回は、dTempSet 問題の解決です。

症状

テスト用に作ったミニマル概念モデルで、

問題発生している状態のエントリアクション

G という概念クラスの ”1. TestState” という状態のエントリアクションの8行目、

SELECT ANY d FROM INSTANCES OF D WHERE SELECTED.Index == PARAM.D_Index ;


をもとに変換された C# のコードが、

// Line : 8
dTempSet = instanceRepository.GetDomainInstances("D").Where(selected => ((((DomainClassD)selected).Attr_Index == D_Index)));
if (instanceRepository.ExternalStorageAdaptor != null) dTempSet = instanceRepository.ExternalStorageAdaptor.CheckInstanceStatus(DomainName, "D", dTempSet, () => { return $"(Index = D_Index)"; }, () => { return DomainClassDBase.CreateInstance(instanceRepository, logger); }, "any").Result;
d = (DomainClassD)(dTempSet.FirstOrDefault());

と、dTempSet の前に”var ”が抜けていることによるビルドエラーです。
ここで生成されたコードの説明をしておきますね。概念情報モデルで定義された概念クラスは、それぞれ C# の interface と class に変換されることを前の記事で紹介しました。概念インスタンスはそれらの instance として扱われることになります。instanceRepository は、概念インスタンスに相当する instance 群をメモリ上に保持するための仕組みです。ここから該当するインスタンス(この場合は、イベント引数でやってきた D_Index と同じ Index の値を持つ D のインスタンスを探している)を取り出します。
この C# コードへの変換ルールは、インスタンス群(圏Iのモデル)を Azure Digital Twins 上で保持することを前提に設計された設計ルールをもとに作られています。Azure Digital Twins の Twin Graph として twin(C# 上の instance に相当)が保持されていますが、twin や twin の property は、子の生成されたコードセット以外からも更新される可能性があるので、メモリ上で保持しているデータ値とAzure Digtal Twins 上の値が異なっている可能性があります。なので、念のため、Azure Digital Twins からも対応する twin の情報を取り出しています。これらは Set 形式の dTempSet というテンポラリの変数にいったん保持され、最終的に、dTempSet の先頭を d という変数に代入している、というコードです。”1. TestState のアクションから生成されたコードの全体を以下に示しておきます。

protected void ActionTestState(int S_Index, int D_Index)
{
    // Action Description on Model as a reference.

    //   1 : SELECT ANY s FROM INSTANCES OF S WHERE SELECTED.Index == PARAM.S_Index ;
    //   2 : assigned = FALSE ;
    //   3 : IF NOT_EMPTY s
    //   4 : 	SELECT ONE c1 RELATED BY s->C1[R1] ;
    //   5 : 	IF NOT_EMPTY c1
    //   6 : 		SELECT ONE d RELATED BY c1->D[R2.'is owner of'] ;
    //   7 : 		IF EMPTY d
    //   8 : 			SELECT ANY d FROM INSTANCES OF D WHERE SELECTED.Index == PARAM.D_Index ;
    //   9 : 			RELATE c1 to d ACROSS R2 USING SELF ;
    //  10 : 			assigned = TRUE ;
    //  11 : 		END IF ;
    //  12 : 	END IF ;
    //  13 : END IF ;
    //  14 : IF assigned == FALSE
    //  15 : 	DELETE OBJECT INSTANCE SELF ;
    //  16 : END IF ;

    // Line : 1
    var sTempSet = instanceRepository.GetDomainInstances("S").Where(selected => ((((DomainClassS)selected).Attr_Index == S_Index)));
    if (instanceRepository.ExternalStorageAdaptor != null) sTempSet = instanceRepository.ExternalStorageAdaptor.CheckInstanceStatus(DomainName, "S", sTempSet, () => { return $"(Index = S_Index)"; }, () => { return DomainClassSBase.CreateInstance(instanceRepository, logger); }, "any").Result;
    var s = (DomainClassS)(sTempSet.FirstOrDefault());

    // Line : 2
    var assigned = false;
    // Line : 3
    if (s != null)
    {
        // Line : 4
        var c1 = s.LinkedR1C1();

        // Line : 5
        if (c1 != null)
        {
            // Line : 6
            DomainClassD d = null;
            var c1In0RL1 = c1.LinkedR2OtherIsOwnerOf();
            if (c1In0RL1 != null)
            {
                d = c1In0RL1.LinkedR2OtherIsOwnerOf();
            }

            // Line : 7
            if (d == null)
            {
                // Line : 8
                dTempSet = instanceRepository.GetDomainInstances("D").Where(selected => ((((DomainClassD)selected).Attr_Index == D_Index)));
                if (instanceRepository.ExternalStorageAdaptor != null) dTempSet = instanceRepository.ExternalStorageAdaptor.CheckInstanceStatus(DomainName, "D", dTempSet, () => { return $"(Index = D_Index)"; }, () => { return DomainClassDBase.CreateInstance(instanceRepository, logger); }, "any").Result;
                d = (DomainClassD)(dTempSet.FirstOrDefault());

                // Line : 9
                // Relate c1 - R2 -> d USING SELF
                target.LinkR2(c1,d);

                // Line : 10
                assigned = true;
            }
        }
    }

    // Line : 14
    if ((assigned == false))
    {
        // Line : 15
        target.DeleteInstance(changedStates);
    }
}

8行目から生成されたコードで、最後の、d という変数への代入文には先頭に var がついていませんが、6行目のアクションから生成された”DomainClassD d = null ;”というコードがあるので、問題なしです。

ついでですが、具体的にこのアクションはどんなことをやっているかを説明しておくことにします。
まず、概念クラス S の概念インスタンスのうち、Index という特徴値の値が、イベント引数の S_Index に合致するものを一つ取り出して、概念クラスで記述されたリレーションシップを、→ R1 → C1 → R2 → D と辿ります。

リレーションシップ

R1 は、”is-a” なので、概念クラス S の概念インスタンスには必ず、C1、C2 いずれかの概念インスタンスが一つだけリンクが存在することになり、それをチェックして C1 の概念インスタンスであることを確認します。C1 の概念インスタンスは、R2 の定義により、D のインスタンスが一つだけ紐づいているか、無いかのどちらかになります。もし、D のインスタンスがあれば、既に R2 で規定された関連クラスの概念インスタンスが存在することになるので、そのまま消えて、無ければ、この状態モデルの持ち主の G の概念インスタンスを R2 の関連クラスのインスタンスとして紐づけますよ、というアクションです。
概念モデリングを学び始めたとき、OAL という馴染みのないプログラミング言語ともちょっと考え方が異なる言語を覚えなければならない、というのは、習得の上での壁ではあるのですが、こういう情報のたどり方に慣れることが習得の第一歩です。SQL を書いたことがある人なら、この辿り方の書き方の便利さはわかってもらえると思いますが。

対処方法

プログラミング言語で変数を使う場合、その変数が有効とされるスコープが存在します。読者の皆さんはご存じのことと思いますが、C# に限らずたいていのプログラミング言語では、if や for、while などの従属ブロックで定義された変数は、そのブロックの中でのみ有効ですが、そのブロックの親ブロックで定義された変数もまた有効という扱いです。同じスコープ内で、既に定義されている有効な変数と同じ名前の変数を新たに定義しようとするとコンパイルエラーになります。BridgePoint のアクション記述言語(OAL:Object Action Language)もまた、変数に関するスコープルールを持っています。ただし、C# の場合は変数を新たに定義する場合は、型をしていするか、”var ”をつけなければなりませんが、OAL の場合は、すべての変数の型はその記述から完全にデータ型を推測できるので、そのような規定はなく、同じスコープに同名の変数がすでに存在しているなら、その変数、存在しないなら新たな変数として扱われます。
…、変換ルールの開発は、そういう言語特性を考慮しながら設計・実装していくことになります。

dTempSet 周りのコード変換は、ActDescripGenerator.cs に記述されています。ちなみにこのファイル、2700行以上あって、あ~、だらだらと書いていったな…という感じ。そのうちリファクタリングをしようと思ってます。

8行目のコードの生成ルールの実装です。

        protected string GenerateACT_SelectFromWhere(CIMClassACT_FIW actFiwDef)
        {
            // SELECT ANY FROM INSTANCES OF WHERE
            // SELECT MANY FROM INSTANCES OF WHERE
            var sb = new StringBuilder();
            var writer = new StringWriter(sb);

            var cardinality = actFiwDef.Attr_cardinality;
            var valDef = actFiwDef.LinkedToR610();
            var varDef = actFiwDef.LinkedToR665();
            var objDef = actFiwDef.LinkedToR676();
            string domainClassName = GeneratorNames.GetDomainClassName(objDef);
            string domainClassImplClassName = GeneratorNames.GetDomainClassImplName(objDef);
            string candVarName = $"candidatesOf{varDef.Attr_Name}";

            string valVarName;
            addDomainClassCast = true;
            string valCode = GenerateV_VAL(valDef, out valVarName);
            addDomainClassCast = false;
            string valVarNameForSQL;
            string valCodeForSql = GenerateV_VAL(valDef, out valVarNameForSQL, true);

            var dstVarDef = HasDeclaredVariable(varDef.Attr_Name);
            if (dstVarDef == null)
            {
                dstVarDef = new VariableDef() { Name = varDef.Attr_Name, Declared = false, VarDef = varDef };
                if (actFiwDef.Attr_cardinality == "any")
                {
                    dstVarDef.Set = false;
                }
                else
                {
                    dstVarDef.Set = true;
                }
            }
            string declCode = "";
            if (dstVarDef.Declared == false)
            {
                declCode = "var ";
                //  dstVarDef.Declared = true;
                DeclaredVariable(dstVarDef);
            }

            if (dstVarDef.Set)
            {
                writer.WriteLine($"{indent}{declCode}{candVarName} = instanceRepository.GetDomainInstances(\"{objDef.Attr_Key_Lett}\").Where(selected => ({valCode}));");
                writer.WriteLine($"{indent}if (instanceRepository.ExternalStorageAdaptor != null) {candVarName} = instanceRepository.ExternalStorageAdaptor.CheckInstanceStatus(DomainName, \"{objDef.Attr_Key_Lett}\", {candVarName}, () => {{ return $\"{valCodeForSql}\"; }}, () => {{ return {domainClassImplClassName}.CreateInstance(instanceRepository, logger); }}, \"{actFiwDef.Attr_cardinality}\").Result;");
                writer.WriteLine($"{indent}{declCode}{varDef.Attr_Name} = new List<{domainClassName}>();");
                writer.WriteLine($"{indent}foreach (var instance in {candVarName})");
                writer.WriteLine($"{indent}" + "{");
                writer.WriteLine($"{indent}{baseIndent}{varDef.Attr_Name}.Add(({domainClassName})instance);");
                writer.WriteLine($"{indent}" + "}");
            }
            else
            {
                writer.WriteLine($"{indent}{declCode}{varDef.Attr_Name}TempSet = instanceRepository.GetDomainInstances(\"{objDef.Attr_Key_Lett}\").Where(selected => ({valCode}));");
                writer.WriteLine($"{indent}if (instanceRepository.ExternalStorageAdaptor != null) {varDef.Attr_Name}TempSet = instanceRepository.ExternalStorageAdaptor.CheckInstanceStatus(DomainName, \"{objDef.Attr_Key_Lett}\", {varDef.Attr_Name}TempSet, () => {{ return $\"{valCodeForSql}\"; }}, () => {{ return {domainClassImplClassName}.CreateInstance(instanceRepository, logger); }}, \"{actFiwDef.Attr_cardinality}\").Result;");
                writer.WriteLine($"{indent}{declCode}{varDef.Attr_Name} = ({domainClassName})({varDef.Attr_Name}TempSet.FirstOrDefault());");
            }


            return sb.ToString();
        }

最後の else ブロックが、該当する変換ルールです。dTempSet という文字列は、”{varDef.Attr_Name}TempSet”で生成されています。その前に、”{declCode}”という変換ルールがあって、この declCode が空文字になっているのが問題。
declCode は、下から二番目の if ブロックで設定されています。dstVarDef.Declared が true だから declCode が空文字だと。
なぜ、dstVarDef.Declared が true かというと、6行目のアクションの生成で、8行目の d の宣言がなされているからです。しかし、この場合、dTempSet は6行目の d の宣言とは無関係なので、結果として”var ” が欠落してしまっている、ということになります。
つまりは、8行目のアクション記述で明示的に書き記されている d の変換時の扱いと、変換の都合でテンポラリに生成される dTempSet の宣言の有無の扱いは、別々にする必要があるということ。
さらには、その dTempSet が同一スコープで宣言有りなのかなしなのかを判断する仕組みが必要だということにもなります。

以上を総合し、以下の解決策を施しました。

まず、ActDescripGenerator class に、

        private class TempSetVarFolder
        {
            private List<List<string>> declaredTempVariable = new List<List<string>>();
            public void AddBlock()
            {
                declaredTempVariable.Add(new List<string>());
            }
            public void DeleteBlock()
            {
                declaredTempVariable.RemoveAt(declaredTempVariable.Count - 1);
            }
            public void Declared(string varName)
            {
                declaredTempVariable[declaredTempVariable.Count - 1].Add(varName);
            }
            public bool HasDeclared(string varName)
            {
                for (int i = declaredTempVariable.Count - 1; i >= 0; i--)
                {
                    if (declaredTempVariable[i].Contains(varName))
                        return true;
                }
                declaredTempVariable[declaredTempVariable.Count - 1].Add(varName);
                return false;
            }
        }
        private TempSetVarFolder currentTempSetVarFolder;

という子 class と、メンバー変数を加えます。プログラミング言語は木構造であり、あるブロックから見れば、スコープはブロックが線形に連なっているものと考えられるので、テンポラリの変数の宣言済み・未宣言の管理は、これで十分です。

  • currentTempVarFolder の初期化は、ある状態エントリアクションの開始時に行う

  • アクション記述のブロックの変換を始めるときに、currentTempVarFolder の AddBlock method をコールする

  • アクション記述のブロックの変換が終わったときに、currentTempVarFolder の DeleteBlock method をコールする

という変換ルールを実装して、問題の変換ルールを

                string tempSetDeclCode = "";
                string varTempSetName = $"{varDef.Attr_Name}TempSet";
                if (!currentTempSetVarFolder.HasDeclared(varTempSetName))
                {
                    tempSetDeclCode = "var ";
                }
                writer.WriteLine($"{indent}{tempSetDeclCode}{varTempSetName} = instanceRepository.GetDomainInstances(\"{objDef.Attr_Key_Lett}\").Where(selected => ({valCode}));");
                writer.WriteLine($"{indent}if (instanceRepository.ExternalStorageAdaptor != null) {varTempSetName} = instanceRepository.ExternalStorageAdaptor.CheckInstanceStatus(DomainName, \"{objDef.Attr_Key_Lett}\", {varDef.Attr_Name}TempSet, () => {{ return $\"{valCodeForSql}\"; }}, () => {{ return {domainClassImplClassName}.CreateInstance(instanceRepository, logger); }}, \"{actFiwDef.Attr_cardinality}\").Result;");
                writer.WriteLine($"{indent}{declCode}{varDef.Attr_Name} = ({domainClassName})({varTempSetName}.FirstOrDefault());");

と、修正すれば問題解決です。同様な問題は、別の箇所にも存在するので、そちらも同じ修正を加えて、作業完了です。

修正確認

テスト用のモデルでの障害問題解決が済んだので、健診受診のモデルの再生成を行います。結果は、ビルドエラーなし!
これで修正完了です。

Object Action Language 考察

変換ツール(ジェネレータ)に潜んでいた障害を一通り修正したので、ここで、OAL について解説を加えておきます。
OAL は、BridgePoint で概念モデル(Shlaer-Mellor 法・xtUML)を記述していくときに、状態のエントリアクション(加えて、Domain Function、Operation、Mathematical Property でも利用可)をテキスト形式で記述するために用意されたアクション記述言語です。通常のプログラミングに慣れた人からすると、「あ~ Yet Another なプログラミング言語ね」と思いがちだと思います。しかし、そんな感覚でいると、概念モデリングの振舞い記述をうまくやることは不可能です。

データフローだよ

概念モデリングでは、4. 概念振舞モデル|Knowledge & Experience で解説しているように、アクションは本来、データフローをセマンティクスとして記述することになっていて、OAL も記述された内容がデータフロー的に動くものとされています。概念モデリングで振舞いを記述するときには、通常のプログラミングコードと、OAL の違いについて腹落ちしていることが重要です。
通常のプログラミングコードは、各行が上から順番に実行されていきます。
一方、OAL 記述は、各行での入力変数が使えるようになった時点で同時並行的に実行されると解釈されます。ある行の実行の結果生まれた出力データが別の行の入力変数となる場合、結果的に出力データを生み出す行が先に実行されるという実行順序が生まれます。
現実世界を考えてみてください。例えば物理現象は、力学や相対性理論、量子力学など、その物理現象を記述する方程式がある場合、それにかかわる事項の値が揃えば、その方程式に従って現実世界の事項が変化します。ビジネスや日常生活でも、必要なモノや情報が揃ったら、様々なプロセスが動いていきます。概念モデリングにおける振舞いの記述は、このような現実世界の動き様を忠実に写し取るためにデータフローの実行形態を想定しているわけです。通常のプログラミングコードでは当たり前の、記述された内容が上から順番に実行されていくというような実行形態は、現実世界ではむしろ異質だと言えるでしょう。現実の世界はデータが使えるようになった時点で様々なプロセスが実行されていくことを考えれば、そのようなデータの生成・利用に基いた同時並行的な振舞いの様を、記述された行の順番で実行されていくような形式で書くことが、理路整然としたプログラムコードを書くコーディングという作業を難しくしている一つの原因なのかもしれません。Essence of Software Design|Knowledge & Experience|note の一連の記事の中でも書いていますが、複雑なロジックをデータフローモデルで書いて明確化した後に、データフローの後段のほうからプログラムコードに落としていくと、比較的すっきりとしたプログラムコードを書くことができます。複雑な条件判断がある場合は、Decision Table による条件の整理も併せて行うとさらに、バグが潜みにくいプログラムコードを書くことができます。
プログラミングコードにおいても、.NET Framework 上で動く C# コードは、マルチコアの計算リソースを有効に使うことを目的として一部データフロー的な実行形態を採用していたりします。スケーラビリティが重要なエンタープライズレベルのソリューションでも、データフロー的観点から設計されたプログラムコードは、本来的な実行順序を決めるデータの依存関係が明確されて、処理の単位が分割されているので、マイクロサービスを実行するためのコンピューティングリソースの利用は必要かつ十分なだけなので、コスト・パフォーマンスに優れたソリューションの開発が可能です。

For Each? While?

OAL はデータフロー的な実行セマンティクスを持つアクション記述言語ではあるのですが、そんな観点からすると、データフローモデルは表現しにくい、For Each や While という文法があるのが不思議に思える人もいるでしょう。

For Each ~ End For は、

For Each a in aSet
    ...
End For ; 

が基本ですが、aSet で使えるのは、概念クラスのインスタンスの集合を保持する変数だけです。For Each ~ End For の内部ブロックは、aSet に入っている概念インスタンスそれぞれに対してのアクション記述です。それぞれの概念インスタンスを使ったアクション実行は同時並行的に、順不同でなされると考えます。概念モデリングでは、通常のプログラミング言語では当たり前の配列に相当するものが存在しません。というわけで、データフローの決まりを破るものではありません。

While ~ End While は、

While condition
    ...
End While ;

が基本です。内部ブロックは、condition が True の間中、実行されることになります。概念モデリング初学者が一番間違った使い方をしやすい文法がこれかもしれません。
通常のプログラミングに慣れた人のありがちな書き方は、

flag = True ;
While flag
    ...
    flag = EE::ExternalBridge() ;
End While :

こんな風に、External Entity の Bridge オペレーションからの復帰を待って帰った値で While ループを抜け出すようなロジックであり、かつ、External Entity はモデル化対象の概念ドメインの主題とは無関係の世界への窓口なので、ハードウェア等の割り込みを待つような想定をしている記述です。
概念モデリングにおいては、アクション記述を構成する全ての記述単位はデータフローのプロセス(バブル)です。従って、External Entity の Bridge オペレーションも単なる値の入出力であす。加えて、Bridge オペレーション内部の機構に対するいかなる仮定もしてはいけません。Bridge オペレーションで何らかの待ちが発生するということは、概念ドメイン外に起因する事象の発生と同じ意味なので、Bridge オペレーションから取得しようとしている値を引数とするイベントとして状態モデルを記述するのが正解です。

While ~ End While が最適なのは、概念モデリング ~ 虎の巻|Knowledge & Experience で解説している、概念インスタンスが順番に並んでいるような概念情報モデルで、順番の先頭や最後を見つけるような時です。

概念クラスA の概念インスタンスは順番に並んでいる

ここから先は

727字

2022年3月にマイクロソフトの中の人から外の人になった Embedded D. George が、現時点で持っている知識に加えて、頻繁に…

この記事が気に入ったらチップで応援してみませんか?