Candid とは?

Candid は インターフェース記述言語 の一種です。 Candid の主な目的は、Service(Internet Computer 上で動作する Canister スマートコントラクト としてデプロイされたプログラム)の、公開インターフェースを記述することです。 Candid の大きな利点の一つは言語にとらわれないことであり、Motoko、Rust、JavaScript などの異なるプログラミング言語で書かれた Service やフロントエンドの相互運用を可能にしています。

Candid の典型的なインターフェースの記述は次のようなものです:

service counter : {
  add : (nat) -> ();
  subtract : (nat) -> ();
  get : () -> (int) query;
  subscribe : (func (int) -> ()) -> ();
}

この例では、counter という Service が、以下のパブリックメソッドから構成されていることを表しています。

  • カウンターの値を変更する addsubtract メソッド。

  • カウンターの現在の値を返す get メソッド。

  • 別の関数を呼び出す subscribe メソッド。例えば、カウンターの値が変化するたびに通知用のコールバックメソッドを呼び出すことができます。

上で示した例のように、すべてのメソッドには、引数と返り値の型があります。 また、この例で示した query という表記のように、メソッドには Internet Computer 独自のアノテーションを含めることができます。

このようなシンプルなインターフェースを採用しているため、コマンドラインや Web ベースのフロントエンドを通して直接、 あるいは Rust プログラムや他のプログラミング言語やスクリプト言語からプログラム的に、counter サービスを操作することが可能です。

相互運用性に加え、既存のクライアントを壊さずに変更できる部分を正確に指定することで、Candid はサービスのインターフェースのアップグレードをサポートします。 例えば、既存のクライアントとの互換性を失うことなく、新しいオプションのパラメータを Service に安全に追加することができます。

なぜ新しい IDL を作ったか?

一見、JSON や XML、Protobuf などの他の技術で十分だと思われるかもしれません。 しかしながら、Candid はこれらの他の技術にはない、ユニークな機能を提供しています。 Candid が Internet Computer での Dapps の開発に特に適している特徴は以下の通りです:

  • JSON、XML、Protobuf などの多くの言語は、個々の値をバイトや文字にマッピングする方法のみを記述しています。 これらのデータ記述言語は、サービス全体を記述しているわけではありません。 これらの言語は、そのデータ型を利用するメソッドではなく、転送したいデータの型に焦点を当てているのです。

  • Candid では、Candid の値をホスト言語の型や値に直接マッピングするように実装されています。 Candid では、開発者は、抽象的な Candid の値を構築したり分解したりしません。

  • Candid は、Service とそのインターフェースを健全かつ合成的にアップグレードするためのルールを定義しています。

  • Candid は本質的に高次の言語です。 Candid では、Service やメソッドへの参照など、単なるデータ以上のものを渡すことができます。 安全なアップグレードのための Candid のサポートは、そのような高次の使用が念頭にあります。

  • Candid は、query アノテーションのような、Internet Computer 独自の機能をビルトインでサポートしています。

Candid の型と値

Candid は強力な型付けシステムで、以下で示すように、ほとんどの用途をカバーする型のセットがあります:

  • Unbounded(制限なし)整数型 (nat, int).

  • Bounded(制限付き)整数型 (nat8,nat16, nat32, nat64, int8,int16, int32, int64).

  • 浮動小数点型 (float32, float64).

  • ブール型 (bool).

  • テキストデータ(text)とバイナリデータ(blob)の型

  • バリアントを含むコンテナ型 (opt, vec, record, variant).

  • 参照型 (service, func, principal).

  • nullreservedempty の特殊な型。

全ての型について リファレンスの章で詳細に記述されています。

これらの型の背景にある哲学は、データの 構造 を記述するのに十分であり、情報をエンコードしたり、渡したり、デコードすることができる一方で、表現を記述するのに必要な以上の セマンティクス に関する制約を意図的に記述しないというものです。 例えば、数が偶数であること、ベクトルが固定長であること、ベクトルの要素がソートされていることなどを表現する方法はありません。

Candid がサポートする型によって、Motoko、Rust、JavaScript、その他の言語でコードを書いていても、それぞれのホスト言語に適した合理的な選択に基づき、データ型の自然なマッピングが可能になります。

Candid Service の説明

Candid の型に慣れてきたら、Candid を使って Service を記述することができます。 Candid の Service 記述ファイル(.DID ファイル)は、自分で作成するか、Service の実装から自動生成するかのどちらでも構いません。

特定のホスト言語用の Service 記述ファイルを生成する方法を説明する前に、Service 記述ファイルの構造とその構成要素について例を挙げて詳しく見てみましょう。

公開メソッドを持たない Service に対する最もシンプルな Service 記述ファイルは以下のようになります:

service : {}

この Service はあまり便利ではないので、ping という簡単なメソッドを追加しましょう:

service : {
  ping : () -> ();
}

この例では,ping という単一の公開メソッドをサポートする Sercvice を記述しています。 メソッド名は任意の文字列で構いませんが、平文でない場合は引用符で囲んでください("method with spaces")。

メソッドは、引数と返り値の型の シーケンス を宣言します。 この ping メソッドの場合、引数はなく、返り値もないので、引数と返り値の両方に空のシーケンス () が使用されます。

上の例はあまりにも単純なケースでしたので、もう少し複雑な Service の記述を考えてみましょう。 この Service は、reversedivMod という2つのメソッドで構成されており、それぞれ以下のような引数と返り値の型のシーケンスを持っています:

service : {
  reverse : (text) -> (text);
  divMod : (dividend : nat, divisor : nat) -> (div : nat, mod : nat);
}

reverse メソッドは、text 型の値を引数として1つ受け取り、text 型の値を1つ返します。 divMod メソッドは、nat 型の2つの値を引数とし、返り値も同じく2つの nat 型の値となります。

引数と返り値の命名

前述の例では、divMod メソッドのシグネチャに引数と返り値の名前が含まれています。 メソッドの引数や返り値に名前を付ける目的はドキュメント化です。 使用する名前は、メソッドの型や渡される値を変更するものではありません。 その代わり、引数や返り値は名前とは関係なく、その 場所 によって識別されます。

特に、Candid は以下のように型を変更したり:

  divMod : (dividend : nat, divisor : nat) -> (mod : nat, div : nat);

最初に mod を返すメソッドを期待する Service に上記の divMod を渡すことを妨げません。

これは、意味論的に関連性を持つ レコード 型フィールドとは大きく異なります。

複雑な型の再利用

しばしば、Service 内の複数のメソッドが同じ複合型を参照することがあります。 その場合、その型に名前を付けて複数回再利用することができます。 例えば、以下のようになります:

type address = record {
  street : text;
  city : text;
  zip_code : nat;
  country : text;
};
service address_book : {
  set_address: (name : text, addr : address) -> ();
  get_address: (name : text) -> (opt address) query;
}

これらの型定義は、既存の 型を単に省略しているだけで、新しい型を定義しているわけではありません。 関数のシグネチャで address を使おうが、レコードを書き出そうが関係ありません。 また、名前が異なっていても定義が同じである2つの略語は、同じ型を表しており交換可能です。言い換えれば、Candid は 構造的 型付けを使用しています。

クエリメソッドの指定

全節の最後の例で、get_address メソッドに query アノテーションを使用していることにお気づきでしょうか。 例えば、以下のような形です:

service address_book : {
  set_address: (name : text, addr : address) -> ();
  get_address: (name : text) -> (opt address) query;
}

このアノテーションは、get_address メソッドが Internet Computer の クエリコール として呼び出されることを示しています。

クエリメソッドとアップデートメソッドで説明したように、クエリはコンセンサスを介さずに Canister スマートコントラクトから情報を取得するための効率的な方法ですので、メソッドをクエリとして識別できることは、Internet Computer とやり取りする際に Candid を用いる重要なメリットの1つです。

エンコードとデコード

Candid のポイントは、バイナリ形式にエンコードされた引数を渡し、特定の伝達手段(Internet Computer へのメッセージや Internet Computer 内のメッセージなど)で転送し、相手側でデコードすることで、Service のメソッドをシームレスに呼び出せるようにすることです。

Candid を使えば、このバイナリ形式の詳細を気にする必要はありません。

もしあなたが自分で Candid を 実装 しようと思っているなら(例えば、新しいホスト言語をサポートするためなど)、Candid の仕様で詳細を参照してください。 ただし、自分で実装しない場合であっても、Candid のフォーマットのいくつかの側面は知っておく価値があります。

  • Candid のバイナリ形式は、DIDL... (16進数では、4449444c...) から始まります。 もし、低レベルのログ出力でこれを見ることがあれば、それは Candid でエンコードされた値を見ている可能性が高いです。

  • Candid のバイナリ形式は、メソッドの引数と返り値が型のシーケンスとなっているため、常に値の シーケンス をエンコードします。

  • バイナリ形式は非常にコンパクトです。125,000個の要素を持つ (vec nat64) は 1,000,007 バイトです。

  • バイナリは自己記述式であり、含まれている値の(凝縮された)型に関する記述を含んでいます。 これにより、メッセージが互換性のない型として送られてきたかどうかを、受信側が検出することができます。

  • 受信側が期待する型の通りに送信側が引数をシリアル化している限りにおいて、デシリアライズは成功します。

Service のアップグレード

Service は時間とともに進化し、新しいメソッドが追加されたり、既存のメソッドがより多くのデータを返したり、追加の引数を要求したりします。 通常、開発者は既存のクライアントを壊すことなくアップグレードを成功させたいのです。

Candid は、新しい Service の型が、これまでのインターフェース記述を使用している他のすべての Canister といつまで通信できるのかを示す明確なルールを定義することで、このようなアップグレードをサポートしています。 このための基本となる形式は、サブタイプ です。

Service は次の場合に安全にアップグレードすることができます:

  • 新しいメソッドを追加する場合には、安全にアップグレードされます。

  • 既存のメソッドは、追加の値を返すことができます。つまり、一連の返り値の型を拡張することができます。古いクライアントは、追加された値を単に無視します。

  • 既存のメソッドは、その引数のリストを短くすることができます。古いクライアントは元のメソッドにあった引数を送ることができますが、それらは無視されます。

  • 既存のメソッドは、オプショナルの引数(opt ... 型)を用いることで引数のリストを拡張することができます。その引数を渡さない古いクライアントからのメッセージを読み込む際には、null 値として推定されます。

  • 既存の引数の型は、以前の型の スーパータイプ に限り、変更 することができます。

  • 既存の返り値の型は、以前の型の サブタイプ に限り、変更 することができます。

ある型のスーパータイプとサブタイプに関する情報は、その型に対応する リファレンスセクションを参照してください。

Service をどのようにアップグレードしていくのか、具体的な例を見てみましょう。 以下のような API を持つ Service を考えてみましょう:

service counter : {
  add : (nat) -> ();
  subtract : (nat) -> ();
  get : () -> (int) query;
  subscribe : (func (int) -> ()) -> ();
}

この Service は、以下のインターフェースにアップグレードすることができます:

type timestamp = nat;
service counter : {
  set : (nat) -> ();
  add : (int) -> (new_val : nat);
  subtract : (nat, trap_on_underflow : opt bool) -> (new_val : nat);
  get : () -> (nat, last_change : timestamp) query;
  subscribe : (func (nat) -> (unregister : opt bool)) -> ();
}

Candid のテキスト値

Candid の主な目的は、Motoko、Rust、JavaScript などのホスト言語で書かれたプログラムを Internet Computer に接続することです。 そのため、ほとんどの場合、プログラムのデータを Candid の値として扱う必要はありません。 その代わりに、慣れ親しんだ JavaScript のようなホスト言語を使って作業し、Rust や Motoko で書かれた Canister のスマートコントラクトに値を透過的に転送することを Candid に任せることができます。 値を受け取った Canister は、その値を Rust や Motoko のネイティブな値として扱います。

しかしながら、ログを取ったり、デバッグしたり、コマンドラインで Service を操作するときなど、Candid の値を人間が読める形で直接見ることができると便利な場合があります。 このような場合には、Candid の値に テキスト表現 を使用することができます。

シンタックスは Candid の型と似ています。 例えば、Candid の値を表す典型的なテキスト表現は次のようなものです:

(record {
  first_name = "John";
  last_name = "Doe";
  age = 14;
  membership_status = variant { active };
  email_addresses =
    vec { "john@doe.com"; "john.doe@example.com" };
})

Candid の binary 形式には実際のフィールド名は含まれておらず、単に数値の hash が含まれてるだけです。 そのため、求められている型を知らずに値をきれいに出力しても、レコードやバリアントのフィールド名は含まれません。上記の値は次のように表示されます:

(record {
   4846783 = 14;
   456245371 = variant {373703110};
   1443915007 = vec {"john@doe.com"; "john.doe@example.com"};
   2797692922 = "John"; 3046132756 = "Doe"
})

Service 記述ファイルの生成

前のセクションで、Candid の Service 記述ファイルを自分で書く方法を学びました。 しかし多くの場合、それは必要ですらありません! Service を実装する際に使用する言語によっては、コードから Candid Serivice の記述ファイルを生成することができます。

例えば Motokoでは、Canister スマートコントラクトを以下のように書くことができます:

actor {
  var v : Int = 0;
  public func add(d : Nat) : async () { v += d; };
  public func subtract(d : Nat) : async () { v -= d; };
  public query func get() : async Int { v };
  public func subscribe(handler : func (Int) -> async ()) { … }
}

このプログラムをコンパイルすると,Motoko コンパイラが上記のインターフェイスの Candid Service 記述ファイルを自動的に生成します。

Rust や Cなどの他の言語でも、その言語のネイティブな型を使って Service を開発することができます。例えば、Rust のネイティブな型を使うことができます。 しかし、Rust のような言語で Service を開発した後、Candid で Service 記述ファイルを自動的に生成する方法は今のところありません。

そのため、Rust や C で Service 用のプログラムを書く場合は、Candid の仕様に記載されている規約に従って、Candid のインターフェース記述ファイルを手動で作成する必要があります。

Rust プログラムの Candid Service 記述ファイルの書き方の例については、Rust CDK の例Rust のチュートリアルを参照してください。

使用するホスト言語に関わらず、ホスト言語の型と Candid の型の対応関係を知っておくことは重要です。 リファレンスのサポートされている型の章では、Motoko、Rust、JavaScript の Candid 型との対応関係が説明されています。