トラック
/
Cairo
Cairo
/
演習
/
選挙の日
選挙の日

選挙の日

学習演習

はじめに

構造体

構造体とは、複数の関連する値をひとまとめにして名前を付けられる、独自のデータ型です。

構造体を定義するには、structキーワードに続けて名前を書き、波括弧の中に各フィールドの名前と型を書きます。

#[derive(Drop)]
struct User {
    active: bool,
    username: ByteArray,
    email: ByteArray,
    sign_in_count: u64,
}

structのインスタンスを作るには、structの名前を指定し、波括弧の中にフィールドのキーと値のペアを書きます。

let user = User {
   active: true, username: "someusername123", email: "someone@example.com", sign_in_count: 1
};

可変のstructインスタンスのフィールドにアクセスしたり変更したりするには、ドット記法を使います。

let mut user = User {
    active: true, username: "someusername123", email: "someone@example.com", sign_in_count: 1
};
user.email = "anotheremail@example.com";

所有権

Cairoの値は不変であり、この不変性によって、たとえ多くの変数が同じ値を参照していても、値そのものは変わらず、書き換えられないことが保証されます。 変数の_所有者_とは、その変数を読み取れる(可変であれば書き込める)コードのことです。

Cairoにおける所有権のルールは次のとおりです。

  1. Cairoのすべての変数には所有者がいます。
  2. 所有者は同時に1つだけです。
  3. 所有者がスコープから外れると、その変数は破棄されます。

スコープとは、プログラムの中で変数が有効な範囲のことです。

fn main() {
                      // `x` is not valid here, it has not been declared yet
    let x: u32 = 3;   // `x` is declared, it is valid from this point forward
                      // do stuff with `x`

} // this scope ended here, `x` goes out of scope and is no longer valid

変数がスコープから外れると破棄され、それによって関連するリソースが正しく解放されることが保証されます。 これはDropトレイトを実装または導出することで実現します。Dropトレイトは、その型が不要になったときに安全に破棄できることをコンパイラーに伝えます。

#[derive(Drop)]
struct A {}

fn main() {
    A {}; // this value can be destroyed after going out of scope
}

現時点では、辞書(Felt252Dict)と辞書を含む型を除き、すべての型でDropの実装を導出できます。

値は_ムーブ_されることもあります。これは、値が別の関数に渡されたときに起こり、その値を参照していた元の変数は破棄され(その結果、その変数は使えなくなり)、同じ値を保持する新しい変数が作られます。

fn foo(num: u32) { 
    // do something with p
}

fn main() {
    let num: u32 = 10;
    foo(num);
    // ownership of `num` was moved into `foo` and is thus no longer valid in this scope
}

値を返すことは、その値をムーブすることと同じです。

次の例は、ある値を返す関数の例です。

#[derive(Drop)]
struct A {}

fn main() {
    // main now owns a1
    let a1 = gives_ownership();

    // a2 comes into scope
    let a2 = A {};

    // a2 is moved into takes_and_gives_back, which also moves its return value into a3
    let a3 = takes_and_gives_back(a2);
} // Here, a3 and a1 go out of scope and are dropped. a2 was moved, so nothing happens.

// Moves its return value into the function that calls it
fn gives_ownership() -> A {
    let some_a = A {};
    some_a
}

// This function takes an instance some_a of A and returns it
fn takes_and_gives_back(some_a: A) -> A {
    some_a
}

変数はムーブされたあとに破棄されるため、コンパイラーはその後の再利用を許しません。

#[derive(Drop)]
struct Point {
    x: u128,
    y: u128,
}

fn foo(p: Point) { 
    // do something with p
}

fn main() {
    let p = Point { x: 5, y: 10 };
    foo(p);
    foo(p); // compiler error: Variable was previously moved.
}

この場合、同じ値(p変数に入っているPoint構造体)を両方の関数呼び出しに渡そうとしています。 一度foo関数に変数を渡してしまうと、その変数はもう使えません。

これを解決する方法の1つは、構造体でCopyトレイトを導出することです。Copyを実装した変数を関数に渡すと動作が変わり、値はムーブされず、同じ値を参照する新しい変数が作られます。 変数はCairoの抽象化であり、値は常に不変なので、この操作にコストはかかりません。

型にCopyトレイトを実装するには、型定義に#[derive(Copy)]アノテーションを追加します。

#[derive(Copy, Drop)]
struct Point {
    x: u128,
    y: u128,
}

fn foo(p: Point) { 
    // do something with p
}

fn main() {
    let p = Point { x: 5, y: 10 };
    foo(p);
    foo(p); // this now works
}

参照とスナップショット

Cairoには、値を破棄したりムーブしたりせずに渡すための2つの機能があり、referencesとsnapshotsと呼ばれます。

スナップショットは、ある時点での値の不変の「ビュー」を提供し、変数の状態にアクセスしながらその所有権を保持できます。 変数を変更すると新しいメモリセルが作られ、スナップショットのビューは変わりません。

スナップショットを作るには、変数インスタンスの前に@を付けるだけです。

// this function takes in a snapshot using `@` with the data-type
fn get_len(arr: @Array<u128>) -> u32 { // 
    arr.len()
}

fn foo(arr: Array<u128>) {
    arr.pop_front();
}

fn main() {
    let mut new_arr: Array<u128> = array![1, 2, 3, 4, 5, 6, 7, 8];
    let snapshot = @new_arr; // using `@` to create a snapshot of a variable
    let len = get_len(snapshot); // pass the snapshot as argument, retaining ownership
    foo(new_arr); // `new_arr` is still valid up until here and no further
}

スナップショットを通常の変数に戻すには、desnap演算子*を使います。これは@演算子の逆の働きをします。 desnapできるのは、Copyトレイトを実装している型だけです。

fn square(n: @u32) -> u32 {
    let n = *n; // we desnap the value
    n * n
}

fn main() {
    let num: u32 = 10;
    let squared = square(@num); // we use `@` to create a snapshot
    println!("{} squared = {}", num, squared);
}

参照を使うと、可変の値を関数に渡して暗黙的に返してもらいながら、呼び出し側のコンテキストで所有権を保持できます。 これはref修飾子を使って実現できます。ただし、その変数がmutキーワードで可変として宣言されている必要があります。

先ほどのPointの例を、Copyトレイトに頼る代わりに可変の参照を使うように書き換えても、同じように動きます。

#[derive(Drop)]
struct Point {
    x: u128,
    y: u128,
}

fn foo(ref p: Point) { 
    // we can now update the passed values and the changes
    // will be visible in the calling context
    p.x += 1;
}

fn main() {
    let mut p = Point { x: 5, y: 10 };
    foo(ref p);
    foo(ref p); // this now works even without the Copy trait
    println!("p.x = {}", p.x); // p.x = 7 
}

説明

近所にある地元の学校には、とても活発な生徒会があります。 生徒会は会長によって運営されており、2年ごとに新しい会長を選ぶ選挙が行われます。 今年の選挙では、票を数えるための新しいデジタルシステムが必要だということになりました。

学校では、この新しいシステムを作るために皆さんの助けが必要です。

1. 票を数えるカウンターを作成する

新しい投票システムにまず必要なものの1つが、票を数えるカウンターです。 このカウンターは、特定の候補者が獲得した票を把握しておくための仕組みです。

候補者の初期票数を受け取り、与えられた初期票数で初期化されたVoteCounter構造体のスナップショットを返すnew_vote_counterという関数を作成してください。

let initial_votes: u32 = 2;
let counter: @VoteCounter = new_vote_counter(initial_votes);
// @VoteCounter {
//     value: 2,
// }

2. カウンターから票数を取得する

これで新しいカウンターを作れるようになりました。 ところが、新しいシステムにはカウンターから票数を取得する方法も必要になることに気づきます。

カウンター(@VoteCounter)を引数として受け取り、そのカウンターの票数を返すvote_countという関数を作成してください。

let initial_votes: u32 = 3;
let vote_counter: @VoteCounter = new_vote_counter(initial_votes);

let count = vote_count(vote_counter);
// => 3

3. カウンターの票を増やす

いよいよ票を数えるときです! ここで、カウンターの票を増やす方法が必要になります。

カウンター(@VoteCounter)と票数を引数として受け取り、その票数だけカウンターを増やすincrement_vote_countという関数を作成してください。

let votes: u32 = 3;
let vote_counter: @VoteCounter = new_vote_counter(initial_votes);
let vote_counter: VoteCounter = increment_vote_count(vote_counter, 2);

let count = vote_count(@vote_counter);
// => 5

4. 選挙結果を作成する

すべての票を数え終えたので、学校全体に向けて結果を発表する準備をしましょう。 そのためには、票のカウンターだけでは足りないことに気づきます。 票数を特定の候補者と結び付ける方法が必要です。

候補者の名前とその票数を受け取り、新しい選挙結果を返すnew_election_resultという関数を作成してください。

let result: ElectionResult = new_election_result("Peter", 3);
// ElectionResult {
//     name: "Peter",
//     votes: @VoteCounter {
//         value: 3,
//     },
// }

選挙結果の構造体はすでに用意されていて、次のように定義されています。

struct ElectionResult {
    name: ByteArray,
    counter: @VoteCounter,
}

5. 結果を発表する

いよいよ新しい会長を学校に発表するときです! 会長は、学校にある小さなデジタル掲示板で発表されます。

メッセージには、新しい会長の名前とその獲得票数を、<candidate_name> (<votes>)の形式で表示します。 たとえば、こんなメッセージです:"Peter (51)"。

引数として@ElectionResultを受け取り、表示するメッセージをByteArrayで返すdisplay_resultという関数を作成してください。

let result = ElectionResult {
    name: "John",
    votes: @VoteCounter {
        value: 32,
    },
};

let msg = display_result(@result);
// => "John (32)"

6. 票の再集計

最終結果が正確かどうかを確かめるために、票が数え直されました。 数え直したところ、一部の候補者の票数が1票ずれていることが分かりました。

最終結果と、票数を減らす対象の候補者の名前を受け取るdecrement_votes_of_candidateという関数を作成してください。 最終結果はFelt252Dict<u32>の形で渡され、キーは候補者の名前、値はその候補者の合計票数です。

let mut final_results: Felt252Dict<u32> = Default::default();
final_results.insert('Mary', 10);
final_results.insert('John', 51);

decrement_votes_of_candidate(ref final_results, 'Mary');

let decremented_votes = final_results.get('Mary');
// => 9
GitHubで編集する リンクは新しいウィンドウまたはタブで開きます
Cairo Exercism

選挙の日を始める準備はできましたか?

Exercismに登録すれば、25個のコンセプト68個の演習、そして本物の人間によるメンタリングとともに、Cairoを学んでマスターできます。すべて無料です。