コンテナ型(List, Map, Tuple)


qi Frameworkでサポートしている3種類のコンテナ型について、

  • 関数の引数として渡す際に用いるQiAnyValue型変数の作成法
  • 関数の戻り値として受け取ったQiValueからの適切なデータ取得法

をそれぞれ紹介します。


3種類のコンテナ型のうち、配列(List)はユーザプログラマがもっとも頻繁に用いるコンテナ型です。辞書(Map)が使われるケースも稀ですが存在します。タプルを表現するQiTupleサービスを自作する際などに利用します。


1. 配列(List)


配列型のデータ構造は最も頻繁に利用されるコンテナ型です。角度値やテキストの受け渡しに用いられるほか、(おそらくAldebaranのライブラリ整備手順など歴史的経緯と思われる理由から)辞書型データを利用すべき箇所でも一部のデータは配列によって表現されています。


1-1.QiAnyValue型の派生型としての配列


QiAnyValueから派生した配列型のクラスはジェネリッククラスQiList<T>です。TQiAnyValueの派生型である必要があります。Baku.LibqiDotNetには非ジェネリックなQiListというクラスも存在しますが、これは静的クラスで、単にQiList<T>を作成するファクトリメソッドを定義しただけのクラスです。


QiList<T>を作成するにはQiList.Create関数を用いるのが最も簡単です。入れ子リストを作るような複雑な例については関数の呼び出しに記載していますが、シンプルな1次元配列であればもっと簡単に、QiList.Create関数を用いて作成できます。

var arr = QiList.Create(new[] { "Hello", "This is", "String Array" });
//arr: QiList<QiString>


QiList.Create関数では組み込み型のIEnumerableから適切なQiListを作成するものを含めてオーバーロードを多数定義しており、通常はこれだけで1次元のQiList<T>が作成可能です。


異なるアプローチとして、QiAnyValueの派生型のIEnumerable<T>(TQiAnyValueの派生型)に対し拡張メソッドToQiList関数を用いることでもQiList<T>が得られます。

var numbers = new QiInt32[] 
{
    new QiInt32(3), new QiInt32(4), new QiInt32(5)
}.ToQiList();

QiInt32intからの暗黙キャストをサポートしているため、上記のコードを次のようにも書けます。

var numbers = new QiInt32[] 
{
    3, 4, 5
}.ToQiList();


1-2. QiValueが保持するデータとしての配列


関数の戻り値としてQiValue型変数を受け取り、その内容が配列であることをあらかじめ知っているとします。このときQiValueから実際にデータを引き出す方法はいくつかありますが、QiValueはプリミティブ配列への明示キャストをサポートしているため、int, double, stringのいずれかの1次元配列については.NETの配列として容易に変換できます。

var motion = session.GetService("ALMotion");
double[] angles = (double[])almotion["getAngles"].Call("Body");


QiValueがキャストをサポートしていない複雑なデータを戻り値として受け取った場合は、よりプリミティブな処理が要求されます。例えば小数の2次元配列について内容を走査するコードは次のようになります。

QiValue response = someService["someMethod"].Call();

for (int i = 0; i < response.Count; i++)
{
    QiValue row = response[i];
    for (int j = 0; j < row.Count; j++) 
    {
        QiValue elem = row[j];
        double value = (double)elem;
        //valueを使った処理
    }
}


QiValue.CountプロパティはQiValueが実際に保持するデータがコンテナ型である場合にデータの個数を取得するために利用できます。配列やタプルではCountの値を考慮したうえで整数値のインデックス指定により、個々の要素へのアクセスが可能です。この操作で得られるデータもまたQiValue型です。上記の例ではresponse変数の各要素rowが1次元配列であると仮定し、さらに内部のデータを取得しています。

なお、上の例に関してはより簡潔に書きたければ次のような記述も可能です。

QiValue response = someService["someMethod"].Call();

double[][] manyNumbers = Enumerable
    .Range(0, response.Count)
    .Select(i => (double[])response[i])
    .ToArray();

//manyNumbersを用いた処理


NOTE: NuGetパッケージ2.0.0相当のライブラリにはQiList.Createによく似た名前のQiList.CreateFromという関数もありますが、これは整備されていない関数なので使わないでください!次、あるいはその次のバージョンでこの関数は廃止される予定です。



2. 辞書(Map)


辞書(Map)はキーと値のペアを保持するデータ構造です。qi Frameworkで辞書を利用する場面は限られていますが、一部の重要な処理で利用されているため、動作を理解しておくのは非常に有意義です。


2-1. QiAnyValue型の派生型としての辞書


辞書のクラスはジェネリッククラスQiMap<TKey, TValue>として定義されています。TKeyTValueQiAnyValueの派生型である必要があります。配列のときと同様、ファクトリメソッドを定義した静的クラスとしてジェネリックでないQiMapも用意されています。

QiMap<TKey, TValue>は(QiList<T>より型の組み合わせが段違いに多いため)組み込み型からの生成をサポートしていませんが、拡張メソッドや暗黙キャストを組み合わせると最小限の煩雑さでインスタンスが生成できます。

QiMap<QiString, QiInt32> d = new Dictionary<QiString, QiInt32>()
{
    ["foo"] = 10,
    ["bar"] = 42,
}.ToQiMap();


ToQiMapIEnumerable<KeyValuePair<TKey, TValue>>に対して動作する拡張メソッドとして定義されており、上記のようにDictionaryからの変換がサポートされています。

辞書に含まれるキー/値ペアの要素が少ない場合、可変長引数を取るQiMap.Create関数を使った方法も比較的簡潔です。以下のように、キー値ペアを指定した分だけ受け入れて辞書を生成することが出来ます。

QiMap<QiString, QiInt32> d = QiMap.Create(
    new KeyValuePair<QiString, QiInt32>("foo", 10),
    new KeyValuePair<QiString, QiInt32>("bar", 42)
    );


2-2. QiValueが保持するデータとしての辞書型


QiValue型変数として受け取った辞書データから値を取り出すにはいくつかの決まった段階を経る必要があります。第一にQiValue.GetKeys()関数でキー一覧の配列を保持したQiValueを取得します。第二に、得られた各キーで実際に辞書式のアクセスを行うことで対応する値が取得できます。キーや値はいずれもQiValueであるため、これらを.NETの値として扱うにはキャストを行います。

以下の例ではQiMap<QiString, QiInt32>を生成してからそれをQiValueに変換し、ふたたびQiValueから内容のデータを取り出すサンプルを示しています。

var d = new Dictionary<QiString, QiInt32>()
{
    ["foo"] = 10,
    ["bar"] = 42
}.ToQiMap();

var dQiValue = d.QiValue;

var keysQiValue = dQiValue.GetKeys();

var keys = Enumerable.Range(0, keysQiValue.Count)
    .Select(i => (string)keysQiValue[i])
    .ToArray();

var values = Enumerable.Range(0, keysQiValue.Count)
    .Select(i => (int)dQiValue[keysQiValue[i]])
    .ToArray();

foreach(var k in keys)
{
    Console.WriteLine(k);
}

foreach(var i in values)
{
    Console.WriteLine(i);
}

出力例です。

bar
foo
42
10



NOTE: 上記のようなキー/値の一覧取得をおこなうコードは一般的であり、本来ならばBaku.LibqiDotNet側でサポートすべき機能です。次のバージョンではQiValueMapKeys, MapValues, MapItemsというプロパティを追加して上記の操作を簡略化できるようにします(GitHubのdeveloper branchでは既にこの機能を公開しています)。

var d = new Dictionary<QiString, QiInt32>()
{
    ["foo"] = 10,
    ["bar"] = 42,
}.ToQiMap();

var dQiValue = d.QiValue;

//MapItemsプロパティはIEnumerable<KeyValuePair<QiValue, QiValue>>型なので
//foreach文を回すとDictionaryへのアクセスと同じようにで扱える
foreach (var pair in dQiValue.MapItems)
{
    string key = (string)pair.Key;
    int value = (int)pair.Value;
    Console.WriteLine($"{key} : {value}");
}

//キー一覧を出力
dQiValue.MapKeys
    .Select(k => (string)k)
    .ToList()
    .ForEach(Console.WriteLine);

//値の一覧を出力
dQiValue.MapValues
    .Select(v => (int)v)
    .ToList()
    .ForEach(Console.WriteLine);


//これはダメ: OfTypeを通過しない
dQiValue.MapKeys
    .OfType<string>()
    .ToList()
    .ForEach(Console.WriteLine);



3. タプル(Tuple)


タプルは複数のデータをひとまとめにする汎用的な方法を提供するデータ構造です。配列と類似していますが、要素ごとに型が異なっても構わないという重要な特徴を備えています。


3-1. QiAnyValue型の派生型としてのタプル


タプル型のクラスはQiTupleです。上述の配列型や辞書型と異なりタプル型は非ジェネリッククラスですが、性質上はジェネリッククラスに近い特徴を持っています。

タプルのインスタンスはQiTuple.Create関数によって生成できます。規約としてタプルを不変値(immutable)にすることは必須ではありませんが、誤操作を防ぐためには一度生成したタプルの要素を書き換えようとしない方が賢明でしょう。

var tuple = QiTuple.Create(
    "Hello",
    10,
    true
    );
Console.WriteLine(tuple.Dump());

出力例です。

Tuple[3]:
  Dynamic
    Dynamic
      QiString:Hello
  Dynamic
    Dynamic
      QiInt:10
  Dynamic
    Dynamic
      QiInt:1

Dynamic型でラップされていますが、データの内容が順に文字列"Hello"と整数10、そして整数化しているもののtrueに対応する整数値である1が順に格納されていることが分かります。


NOTE: 実は上記のようにタプルのデータがDynamicでラップされる仕様は実装ミスで、本来は次のような出力を得られます(GitHubのdeveloper branchコードでは修正が行われており、実際に下記の出力を返します)。

Tuple[3]:
  QiString:Hello
  QiInt:10
  QiInt:1

ただしDynamicによるラップは有害になりづらいため、通常はあまり気にする必要はありません。


3-2. QiValueが保持するデータとしてのタプル型


QiValueとして受け取ったタプルの性質はQiList<T>とほとんど変わりません。値の一覧を取得する場合Countプロパティと整数値インデックス指定による値の取得を組み合わせた処理を記述することになります。

QiValueとしてタプルを受け取るもっとも典型的なケースはサービス自作の際に必要となる、QiObjectBuilder.AdvertiseMethod関数の引数でのメソッド定義です。

var objBuilder = QiObjectBuilder.Create();
objBuilder.AdvertiseMethod("twice::i(i)", (sig, arg) =>
{
    Console.WriteLine("twice was called");
    int i = (int)arg[0];
    return new QiInt32(i * 2).QiValue;
});


上記の例でargとして渡されるのがタプルを保持しているQiValueです。上の例では関数の引数として「単一の整数」を指定している(関数シグネチャの読み方についてはサービス自作を参照)ため、argは常に1要素のタプルであり、その要素は整数であることが保障されます。この例のようにタプルの内容は事前知識に基づいて決定できるため、タプルの内容を適切に処理することはそう難しくありません。