qi Frameworkの型付け
このページではqi Frameworkの内部で定義されている変数の型をPythonの型などとの互換性という観点で紹介し、いくつかの特殊な型についても紹介しています。
1. 型の対応表
公式ドキュメントにはqi Frameworkの内部定義型(qiTypeと呼ばれます)とC++, Python, Javaなどの型に関する対応が記述されています。以下にはqiType, Python, C#間における型の対応に加え、厳密に型対応を取るために用意されたBaku.LibqiDotNetの型を記載しています。
一つだけ公式ドキュメントと異なる注意として、下記の表では最下部にRaw型というバイナリデータ型を追記していることに注意してください。Raw型は公式の対応表には載っていませんがシグネチャAPIなどには記載があります。
| qi Type | Python | Baku.LibqiDotNet | C# |
|---|---|---|---|
| Void | NoneType | QiValue.Void | void |
| Bool | bool | QiBool | bool |
| Int8 | int | QiInt8 | sbyte |
| Int16 | int | QiInt16 | short |
| Int32 | int | QiInt32 | int |
| Int64 | int | QiInt64 | long |
| UInt8 | int | QiUInt8 | byte |
| UInt16 | int | QiUInt16 | ushort |
| UInt32 | int | QiUInt32 | uint |
| UInt64 | int | QiUInt64 | ulong |
| Float | float | QiFloat | float |
| Double | float | QiDouble | double |
| String | str | QiString | string |
| List |
list | QiList<T> | - |
| Mat |
dict | QiMap<TKey,TValue> | - |
| Object | class instance | QiObject | - |
| Value | underlying type | QiDynamic | - |
| Raw | str | QiByteData | byte[] |
2. QiValueとQiAnyValue
Baku.LibqiDotNetではqi Frameworkの機能をラップするためにQiValueとQiAnyValueという二種類のクラスを用いています。
前者のQiValueクラスはqi Frameworkのデータに相当する生のポインターを保持するクラスです。サービスをロードして関数を呼び出すと、その結果は全てQiValue型の変数にいったん集約されます。つまり、QiValueは外部から受信したデータを表すクラスです。
QiValue型変数が実際に保持する値の型は実行時に確認するか、事前に把握しておく必要があります。特に組み込み型を通常はドキュメンテーションから呼び出す関数の戻り値が分かっているため、その型を指定することで出力値を組み込み型(intなど)に変換することが可能です。具体例については関数の呼び出しを参照してください。
後者のQiAnyValueクラスはQiValueと対照的なクラスです。QiValueが関数の戻り値を表すのに対し、QiAnyValueはqi Frameworkの関数へ入力可能な値の基底となるクラスです。上記の表でBaku.LibqiDotNetの型として記載されているものはQiValue.Void静的プロパティとQiObject型を除けば全てがQiAnyValue型の派生クラスです。
QiAnyValueの派生型の多くは通常のコンストラクタを用いて作成できます。例えば、次のようなコードが実行可能です。
QiAnyValue s = new QiString("Create QiString value from string");
//もちろんこう書くことも出来る
QiString s2 = new QiString("Create QiString value from string");
var s3 = new QiString("Create QiString value from string");
3. 暗黙の型変換
3-1. QiAnyValueに関する暗黙型変換
前節で示した通りQiAnyValueの派生型は通常のコンストラクタを用いて作成できます。しかし常にこのような書き方をするとコードが煩雑になってしまいます。
例えば、ロボットのカメラから画像を取得する設定を行うALVideoDeviceのsubscribeCamera関数は次のようなシグネチャを持っています。
string subscribeCamera(string id, int cameraType, int resolution, int colorspace, int fps);
ただstringやintと書いているのは略記で、正式にはQiStringやQiInt32などのBaku.LibqiDotNetで定義された型です。したがって、このを正式な方法で呼び出すと次のように書けます。
var video = session.GetService("ALVideoDevice");
string idName = (string)video["subscribeCamera"].Call(
new QiString("Hoge"),
new QiInt32(0),
new QiInt32(1),
new QiInt32(11),
new QiInt32(10)
);
上記のコードはより簡潔に、次のように書くこともできます。
var video = session.GetService("ALVideoDevice");
string idName = video["subscribeCamera"].Call("Hoge", 0, 1, 11, 10);
このように記述できるのは、Call関数がQiAnyValueの派生型変数を引数としており、さらにQiAnyValueクラスが暗黙のキャストによってstringやintを適切に変換するためです。たとえばstringに対する暗黙のキャストは次のように実装されています。
class QiAnyValue
{
//...
public static implicit operator QiAnyValue(string x) => new QiString(x);
}
他の組み込み型等についても同様で、型の対応表に記載されたbool, string, byte[]と整数型(int, ushortなど)、小数型(float, double)について暗黙のキャストが可能です。使用頻度が高いと想定されるint[], double[], string[]についても暗黙のキャストをサポートしています。したがって、見かけ上はこれら組み込み型(と一部の配列)はそのまま引数に代入することが出来ます。逆に、二重リスト(リストのリスト)のような複雑なデータ構造を指定する場合はキャストは利用できないため、QiAnyValueの派生型のコンストラクタやファクトリメソッドを用いて正しくインスタンスを生成してください。二重リストを用いる具体例は関数の呼び出しで紹介しています。
3-2. QiValueに関する暗黙の型変換
先ほどの例では組み込み型を含むいくつかの型がQiAnyValue型へと暗黙に変換できる事により、関数の呼び出しコードが簡潔に書けることを紹介しました。
もう一つの機能提供として、QiAnyValueから派生しているそれぞれの組み込み型に関しても、派生型に対応する型からの暗黙型変換を認めています。たとえば次のようなコードが可能です。
QiString s = "Hello, World!";
QiInt32 x = 42;
上のコードはstringからQiStringへの暗黙の型変換ができることを示しています。intについても同様です。このようなキャストを利用すると、コンテナ型(とくに辞書QiMap<TKey, TValue>)の初期化が簡潔に記述できます。
QiMap<QiString, QiInt32> d = new Dictionary<QiString, QiInt32>()
{
["foo"] = 10,
["bar"] = 42
}.ToQiMap();
上記の"foo"や10を与えている箇所には本来ディクショナリのキーと値であるQiString型やQiInt32型を指定しなければなりません。カジュアルな記法を嫌う場合こう書く必要があります。
QiMap<QiString, QiInt32> d = new Dictionary<QiString, QiInt32>()
{
[new QiString("foo")] = new QiInt32(10),
[new QiString("bar")] = new QiInt32(42)
}.ToQiMap();
暗黙の型変換によって表現が簡素になっていることが分かります。
4. 動的型(QiDynamic)
先に忠告しておきますが、これはC#のdynamicとはまったく違います!qi FrameworkのQiDynamic型が提供する機能はC#のdynamicに比べると非常に単純ですが、その分言語に依存しない便利な機構を提供しています。QiDynamicは1要素のリストあるいはタプルとみなすことが出来る、ある種のコンテナクラスです。ただし配列(QiList<T>)やタプル(QiTuple)と異なり、QiDynamicは任意の型のデータを保持できるという特徴があります。以下は実際には動作しない疑似コードですが、QiDynamicのコンセプトを示しています。
var qid = new QiFramework.QiDynamic(new QiInt32(10));
var x = qid.Value; //xはQiInt32型の変数
b.Value = new QiString("Hello!");
var y = qid.Value; //yはQiString型の変数
b.Value = new QiBool(false);
var z = qid.Value; //zはQiBool型の変数
上の例ではValueプロパティに次々と異なる型の変数を代入し、取り出しています。qid変数自体の型はQiDynamicのまま不変ですが、Valueへのアクセスによって見かけ上型が動的に変化しているように扱えていることが分かります。
QiDynamic型はqi Frameworkにおいて、事前に渡される関数のシグネチャが固定しづらい時に利用されています。例えば運動を制御するALMotionでは指定した関節の目標となる角度値を指定できるsetAngles関数がありますが、この関数は内部的に2種類のオーバーロードを持っており、シグネチャがおよそ次のようになっています。
void setAngles(QiString name, QiDynamic angles, QiDynamic fraction);
void setAngles(QiList<QiString> names, QiDynamic angles, QiDynamic fraction);
ドキュメンテーションを読むと分かるのですが、実際には上記のQiDynamic部分には単一のdouble値かdouble[]が渡されます。QiDynamicを使わないとsetAngles関数のオーバーロードは2種類では済まず、具体的に言うと(第一引数、第二引数、第三引数がいずれも2種類の型を取るので)8個ほど必要だったことがうかがい知れます。QiDynamicを用いた関数定義はオーバーロードの個数爆発を防ぐうえで重要であると言えます。
実際にQiDynamic型の変数を作成する例をいくつか試しましょう。
第一に、もっとも標準的な方法として、既存のQiAnyValue派生型変数に対してToDynamic拡張メソッドを呼び出すとQiDynamicでラップされた変数を取得できます。簡単なコードは次のようなものです。
QiString s = new QiString("Hello!");
QiDynamic d = s.ToDynamic();
Console.WriteLine(s.Dump());
Console.WriteLien(d.Dump());
出力例です。
QiString:Hello!
Dynamic
QiString:Hello!
もとの文字列がDynamicによって包み込まれたような形になることが分かります。拡張メソッドによる表現以外では下記のコンストラクタ
public class QiDynamic : QiAnyValue
{
public QiDynamic(QiAnyValue v) { /* .. */ }
}
これを利用する方法も簡潔です。上述した暗黙の型変換を用いることで、次のようなコードが記述できます。
var d = new QiDynamic(42);
Console.WriteLine(d.Dump());
出力です。
Dynamic
QiInt:42
42はint型なので暗黙にQiInt32型へと変換され、これがQiDynamicによって包み込まれた値を生成します。
5. QiValue.Void
QiValue.VoidはQiValueクラスの静的プロパティです。nullに近いような意味があり、qi Frameworkにおける関数の戻り値として「何も返さない」というのを表す値です。
通常のプログラムではこの値を意識する必要はありません。例えばロボットに発話させるためのALTextToSpeech::say関数は戻り値が無い関数ですが、これを呼び出すコードは次の通りです。
var tts = session.GetService("ALTextToSpeech");
tts["say"].Call("Hello.");
通常は上のように、戻り値がvoidな関数ではそもそもCall関数の結果を取得しません。もし戻り値がVoidな値であることを念入りに確認したい場合、例えば次のようにします。
var tts = session.GetService("ALTextToSpeech");
var res = tts["say"].Call("Hello.");
bool isVoid = (res.ContentValueKind == QiValueKind.QiVoid);
以上が通常のケースにおける処理です。ただしサービスを自作する際には戻り値が無い関数を定義したい場合があり、QiValue.Voidはこのような状況で用います。
var objBuilder = QiObjectBuilder.Create();
objBuilder.AdvertiseMethod("test::v()", (sig, arg) =>
{
Console.WriteLine("test was called");
return QiValue.Void;
});
なお、このようなパターンは比較的よく採用されるパターンで、.NETアプリケーションの例で言うとデータバインディング時に「何もしない」というアクションを示すためのBinding.Nothingフィールドなどが近い性質を持っています。