Counter 白紙練習(第一弾) で、白紙から Counter を書いてコンパイルできました。ここからは 改造10本ノック。同じ Counter を、少しずつ作り変えて、ledger と circuit の感覚を手で固めます。
このページが嬉しいのは、Docker もウォレットも要らないこと。使う道具は第一弾と同じ compact compile だけ。だから、何度でも気軽に回せます。デプロイ(第二弾)の前でも、合間でも、いつでもどうぞ。
今日の合言葉:
今日は 「契約」を作り変えて、「コンパイル」で確かめるノックです。
このノックの回し方
おすすめは、ノックごとに新しい練習場を作ること。第一弾と同じ手です。
各ノックは、こう進めます。
土台になる第一弾のゴール(これを毎回いじります):
合言葉の分解も、もう一度。
ノック1:2ずつ増やす
お題:押すたびに、round が 2 ずつ増えるようにしてみよう。
increment を「2ずつ増える」に変えてみよう。
ここで分かること:round.increment(...) の中の数字を変えれば、増える量が変わる。第一弾の Uint<64> + count = count + 1 では型エラーになったのに、Counter なら increment(2) がすんなり通る——これが「増やすための便利部品」の力です。
ノック2:好きな数だけ増やす(disclose の壁)
お題:呼ぶときに数を渡して、incrementBy(n) で n だけ増やしたい。まずは素直に書いてみよう。
これをコンパイルすると…わざと失敗します。
こわい見た目ですが、意味はシンプルです。
「見せてOK」と宣言するのが disclose(...) です。
incrementBy(n) を、コンパイルが通る形に直してみよう(ヒント:disclose)。
ここで分かること:disclose(...) は暗号化ではなく、「この値は公開してOK」とコンパイラに伝える印。Counter は秘密を使わない契約ですが、「外から来た値を公開 ledger に書く」だけでこの確認が出ます。詳しくは Compact の書きかた の disclose の節へ。
ノック3:減らす(decrement)
お題:増やすだけでなく、1 減らす decrement も足してみよう。
increment はそのままに、decrement を1つ足してみよう。
ここで分かること:Counter には increment の相棒 decrement がいる。circuit を増やせば「操作ボタン」が増える。Compiling 2 circuits: と出て、ボタンが2つになったのが分かります。
ノック4:今の値を読む(read)
お題:round の今の数を返すだけの current() を作ってみよう。
round の現在値を返す current() を作ってみよう(ヒント:read、戻り値の型)。
ここで分かること:round.read() で Counter の中身(数)を取り出せる。これまで circuit の戻り値はずっと [](空)でしたが、今回は Uint<64> を返しています。(): [] と (): Uint<64> の違い=「何も返さない/値を返す」が見えてきます。
⚠️ TypeScript を知ってる人ほど引っかかる罠:
[]は「配列」じゃないTypeScript だと
[]は「空の配列」ですよね。でも Compact の(): []の[]は、空のタプル(empty tuple)という“型”です。配列を返せという命令ではありません。Compact ではタプルを
[A, B](2つ組)のように書きます。[]は中身が0個のタプル=「意味のある値を返していない」=他の言語のvoidにあたります。だから「何も返さない」と読むわけです。TypeScript でいうと、こう対応します。
見た目が TypeScript そっくりなので
[]を「配列」と読みたくなりますが、戻り値型の位置に出てくる[]はタプル型で、しかも空だから“何も返さない”。ちなみに本当に並びを返したいときは、Compact ではVector<n, T>(長さ固定の並び)など別の型を使います。
ノック5:上限の門番(assert + read)
お題:第一弾で assert を試したのを覚えていますか。今回は Counter の今の値を読んで、10 未満のときだけ増やす「門番つき increment」を作ってみよう。
round.read() を門番に使って、10 未満のときだけ増やす increment にしてみよう。
ここで分かること:第一弾では、自力版(Uint<64>)に assert を足しても型エラーが消えませんでした。でも Counter + round.read() なら、門番つき increment がちゃんと通ります。assert(条件, "メッセージ") の第2引数(メッセージ)も忘れずに。assert は「この条件のときだけ先へ進める門番」です。
ノック6:2つの数字(round と total)
お題:ledger を2つにしてみよう。round(今回ぶん)と total(ずっとの合計)を両方 1 増やす increment を作ってみよう。
ledger を round と total の2つにして、両方 1 増やす increment にしてみよう。
ここで分かること:export ledger は何個でも置ける。状態の箱を増やせば、契約が覚えられることが増える。circuit は1つでも、その中で複数の ledger を触れます。第一弾の contract-info.json を思い出すと、今回は ledger 欄に round と total の2つが並びます。
🤔 よくある疑問:
roundやtotalの1個1個って、UTXO なの?いいえ。
ledgerは UTXO ではありません。 ここは Midnight を理解するうえで大事なので、はっきりさせておきます。Midnight には別々の2つのしくみがあります。
roundとtotalは ① の住人です。別々の UTXO に散らばっているのではなく、同じコントラクトの状態オブジェクトの中に、隣り合って入っている変数(欄)です。イメージはこう:TypeScript でたとえるなら、
roundとtotalは1つの state オブジェクトの中のプロパティ。別々の箱ではありません。
contract-info.jsonのledger欄に2つ並ぶのも、「この状態オブジェクトには round と total という欄がある」という設計図(スキーマ)を表しているだけ。UTXO の一覧ではありません。では UTXO はどこ? → tDUST などのトークンを送受信するお金の世界(Zswap)で使われます。Cardano の eUTXO を受け継いだ部分です。
increment()でカウンターを増やすのは ①の状態更新で、②の UTXO は動きません。もっと知りたくなったら → 2.2 Account と UTXO(2つのモデルの違い)/2.4 デュアル台帳システム(トークンの二重構造)。
ノック7:reset を作ろうとして…(わざと失敗)
お題:「0 に戻す reset() がほしい」。Counter にもありそうですよね。書いてみましょう。
コンパイルすると…失敗します。
ここで分かること:これは第一弾の count += 1(operation += undefined ...)とそっくりの教訓です。型(ここでは Counter)が持っている操作しか使えない。何が使えるかは、暗記ではなく Compact の書きかた や公式リファレンスで都度確認するのが正解。
どうしても「0 に戻したい」なら、
Counterではなく自分で値を管理する別の設計(Cellなどの ledger 型)が必要になります。今は「Counter には reset が無い」と分かれば十分です。
ノック8:好きな数だけ減らす(decrementBy)
お題:ノック2の応用。引数で渡した n だけ減らす decrementBy(n) を作ってみよう。ノック2で学んだ「壁」を思い出して。
incrementBy(n) と decrementBy(n) を、両方コンパイルが通る形で作ってみよう。
ここで分かること:decrement でも、引数を公開 ledger に渡すなら disclose(...) が要る。ノック2の「壁」は increment 専用ではなく、「外から来た値を公開 ledger に書く」全部に共通だと分かります。
ノック9:全部のせ(combined)
お題:ここまでのノックを1つの契約にまとめてみよう。round と total を持ち、門番つき increment・incrementBy(n)・decrement・current() を全部入れる。
round + total を持ち、increment(門番つき)/ incrementBy / decrement / current を全部入れた契約を書いてみよう。
ここで分かること:Compiling 4 circuits: = ボタンが4つ。contract-info.json の circuits 欄には increment incrementBy decrement current、ledger 欄には round total が並びます。自分で書いた契約の形が、そのまま生成物の形になるのが、はっきり見えます。
ノック10:自分のお題(決まった答えなし)
お題:最後は、自分でお題を決めて作ってみよう。たとえば——
- 押すと
roundは +1、でもtotalは +2 になる current()に加えて「合計を返す」currentTotal()も作る- 上限を 100 から 1000 に変えて、メッセージも変える
incrementBy(n)のnの型をUint<16>からUint<8>に変えてみる(何が起きる?)
自分で決めたお題と、それを作って compile したときに分かったこと(成功でも失敗でも)を書いてみよう。
(ここは自由記述です。たとえば「Uint<8> にしたら、大きい数を渡したときにどうなるか試した」「total だけ incrementBy で増やしてみた」など、自分の実験と気づきを言葉にしてみましょう。失敗ログが出たら、それも立派な発見です。)
ここで分かること:お題を自分で作れるようになったら、もう「ゼロから何を作ればいいか分からない」状態は卒業です。小さな改造を白紙から何度も——それが、Battleship のような複雑なアプリを読める力に、まっすぐつながります。
✍️ 仕上げの白紙練習
Counter が持っている主な操作を3つ挙げてみよう。reset はある?
主に increment(n) / decrement(n) / read()。reset() は無い(operation reset undefined for ledger field type Counter になる)。型が持っている操作しか使えない。
circuit の引数を公開 ledger に書こうとすると disclose が要るのはなぜ?
引数は「外から来た値」で、公開 ledger に書くとその値が世界に見えてしまう可能性がある。Compact は「本当に見せていい?」と確認するので、disclose(...) で「見せてOK」と宣言する。increment でも decrement でも同じ。
ledger(round や total)って、1個1個が UTXO なの?
いいえ。 ledger はアカウント型のコントラクト状態で、round と total は「1つの状態オブジェクトの中の欄(プロパティ)」。別々の UTXO ではない。UTXO は tDUST などのトークンを扱う別レイヤー(Zswap)の話で、increment() の状態更新とは別物。くわしくは 2.2 Account と UTXO / 2.4 デュアル台帳。
(): [] と (): Uint<64> のちがいは? [] は配列?
(): [] は 何も返さない circuit。ここの [] は TypeScript の「空の配列」ではなく、空のタプル型(中身0個=void にあたる)。(): Uint<64> は 値(64ビットの正の整数)を1個返す circuit で、current(): Uint<64> { return round.read(); } のように使う。並びを返したいときは Vector<n, T> など別の型。
今日の呪文
今日の勝利条件
今日のまとめ
Counterの操作は increment / decrement / read。resetは無い——型が持つ操作しか使えない。circuitの引数を公開 ledger に書くならdisclose(...)。ledgerは何個でも、circuitも何個でも増やせる。書いた形が、そのまま生成物の形になる。- 道具は
compact compileだけ。だから何度でも反復できる。エラーが出る改造も、立派な練習。
つぎに読むページ
- 書いた契約を、いよいよチェーンに置く → Counter 白紙練習 第二弾:デプロイ
discloseや型・メソッドをおさらい → Compact の書きかた(文法ツアー)- 第一弾に戻る → Counter 白紙練習