Треки
/
Cairo
Cairo
/
Вправи
/
День виборів
День виборів

День виборів

Навчальна вправа

Вступ

Структури

Структури - це власні типи даних, які дозволяють дати імʼя та обʼєднати в одне ціле кілька повʼязаних значень.

Щоб визначити структуру, використаймо ключове слово struct, після якого йде назва та фігурні дужки з полями, їхніми назвами й типами:

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

Щоб створити примірник структури, вкажімо назву struct і використаймо фігурні дужки з парами ключ-значення для полів.

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

Щоб отримати доступ до полів примірника змінної структури або змінити їх, використаймо точкову нотацію.

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. Одночасно може бути лише один власник.
  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, який повідомляє компілятору, що тип можна безпечно знищити, коли він більше не потрібен.

#[derive(Drop)]
struct A {}

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

Наразі реалізацію Drop можна вивести для всіх типів, крім словників (Felt252Dict) і типів, що містять словники.

Значення також можна перемістити: це відбувається, коли значення передають іншій функції, знищуючи початкову змінну, яка вказувала на це значення (тим самим роблячи змінну непридатною), і створюючи нову змінну для зберігання того самого значення.

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.
}

У цьому випадку ми намагаємося передати те саме значення (структуру Point у змінній p) в обидва виклики функції. Щойно ми передали змінну функції foo першого разу, змінну більше не можна використовувати.

Один зі способів розвʼязати це - вивести трейт 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 є дві можливості передати значення, не знищуючи й не переміщуючи його, які називаються 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
}

Щоб перетворити знімок назад у звичайну змінну, можна скористатися оператором *, який знімає знімок і діє як протилежність оператору @. Зняти знімок можна лише з тих типів, які реалізують трейт 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. Створіть лічильник голосів

Новій системі голосування в першу чергу потрібен лічильник голосів. Цей лічильник дає змогу відстежувати голоси, які має певний кандидат.

Створіть функцію new_vote_counter, яка приймає кількість початкових голосів за кандидата і повертає знімок структури VoteCounter, ініціалізований заданою кількістю початкових голосів.

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

2. Отримайте кількість голосів із лічильника

Тепер у нас є спосіб створювати нові лічильники! Але тепер ми розуміємо, що новій системі також знадобиться спосіб отримувати кількість голосів із лічильника.

Створіть функцію vote_count, яка приймає лічильник (@VoteCounter) як аргумент і повертає кількість голосів у лічильнику.

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

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

3. Збільште кількість голосів лічильника

Нарешті настав час підрахувати голоси! Тепер нам потрібен спосіб збільшувати кількість голосів у лічильнику.

Створіть функцію increment_vote_count, яка приймає лічильник (@VoteCounter) як аргумент і кількість голосів, і збільшує лічильник на цю кількість голосів.

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)".

Створіть функцію display_result, яка отримує @ElectionResult як аргумент і повертає ByteArray із повідомленням для показу.

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

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

6. Перерахунок голосів

Щоб переконатися, що остаточні результати точні, голоси перерахували. Під час перерахунку виявилося, що кількість голосів деяких кандидатів відрізнялася на одиницю.

Створіть функцію 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, щоб вивчати й опановувати Cairo, а також 25 концепцій68 вправ та справжнє наставництво від людей, і все це безкоштовно.