Track
/
Cairo
Cairo
/
Esercizi
/
Giorno delle elezioni
Giorno delle elezioni

Giorno delle elezioni

Esercizio di apprendimento

Introduzione

Struct

Le struct sono tipi di dati personalizzati che permettono di dare un nome e raggruppare insieme più valori correlati.

Per definire una struct, usa la parola chiave struct seguita da un nome e da parentesi graffe che contengono i campi con i loro nomi e tipi:

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

Per creare un'istanza di una struct, specifica il nome della struct e usa le parentesi graffe con coppie chiave-valore per i campi.

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

Usa la notazione con il punto per accedere ai campi di un'istanza di struct mutabile o per modificarli.

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

Ownership

I valori in Cairo sono immutabili, e questa immutabilità garantisce che, anche se molte variabili si riferiscono allo stesso valore, il valore stesso rimane costante e inalterato. Il proprietario di una variabile è il codice che può leggere (e scrivere, se mutabile) quella variabile.

Regole di ownership in Cairo:

  1. Ogni variabile in Cairo ha un proprietario.
  2. Ci può essere solo un proprietario alla volta.
  3. Quando il proprietario esce dallo scope, la variabile viene distrutta.

Lo scope (ambito di visibilità) è l'intervallo all'interno di un programma in cui una variabile è valida.

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

Quando una variabile esce dallo scope viene distrutta, il che garantisce che la risorsa associata venga rilasciata correttamente. Questo si ottiene implementando o derivando il trait Drop, che segnala al compilatore che il tipo può essere distrutto in sicurezza una volta che non è più utile.

#[derive(Drop)]
struct A {}

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

Al momento, l'implementazione di Drop può essere derivata per tutti i tipi, tranne che per i dizionari (Felt252Dict) e i tipi che contengono dizionari.

Un valore può anche essere spostato: questo accade quando un valore viene passato a un'altra funzione, distruggendo la variabile originale che faceva riferimento al valore (rendendola così inutile) e creando una nuova variabile che contiene lo stesso valore.

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
}

Restituire valori equivale a spostarli.

L'esempio qui sotto mostra una funzione che restituisce un valore:

#[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
}

Poiché una variabile viene distrutta dopo essere stata spostata, il compilatore ci impedisce di riutilizzarla in seguito.

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

In questo caso, proviamo a passare lo stesso valore (la struct Point nella variabile p) a entrambe le chiamate di funzione. Una volta passata la variabile alla funzione foo la prima volta, la variabile non è più utilizzabile.

Un modo per risolvere è derivare il trait Copy sulla nostra struct: in questo modo cambierebbe ciò che accade quando passiamo una variabile Copy a una funzione. Il valore non verrebbe più spostato, ma verrebbe creata una nuova variabile che fa riferimento allo stesso valore. È un'operazione gratuita, perché le variabili sono un'astrazione di Cairo e i valori sono sempre immutabili.

Per implementare il trait Copy sul tuo tipo, aggiungi l'annotazione #[derive(Copy)] alla definizione del tipo.

#[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
}

Reference e snapshot

Cairo ha due funzionalità per passare un valore senza distruggerlo o spostarlo, chiamate references e snapshots.

Gli snapshot forniscono una «vista» immutabile di un valore in un certo momento, permettendoti di mantenere la proprietà di una variabile mentre ne accedi allo stato. Qualsiasi modifica alla variabile crea una nuova cella di memoria, lasciando invariata la vista dello snapshot.

Per creare uno snapshot, è sufficiente anteporre @ all'istanza della variabile.

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

Per riconvertire uno snapshot in una variabile normale, puoi usare l'operatore di desnap *, che è l'opposto dell'operatore @. Solo i tipi che implementano il trait Copy possono subire il desnap.

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);
}

I reference ti permettono di passare valori mutabili alle funzioni facendoli restituire implicitamente, mantenendo la proprietà nel contesto chiamante. Questo si può ottenere usando il modificatore ref, a condizione che la variabile sia dichiarata mutabile con la parola chiave mut.

Possiamo aggiornare il precedente esempio Point per usare reference mutabili invece di affidarci al trait Copy e funzionerebbe comunque.

#[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 
}

Istruzioni

Una scuola locale vicino a te ha un'associazione studentesca molto attiva. L'associazione studentesca è gestita da un presidente e ogni 2 anni si tengono le elezioni per eleggerne uno nuovo. Per le elezioni di quest'anno si è deciso che serviva un nuovo sistema digitale per contare i voti.

La scuola ha bisogno del tuo aiuto per costruire questo nuovo sistema.

1. Crea un contatore di voti

Una delle prime cose di cui il nuovo sistema di voto ha bisogno è un contatore di voti. Questo contatore serve a tenere traccia dei voti di un particolare candidato.

Crea una funzione new_vote_counter che accetta il numero di voti iniziali di un candidato e restituisce un'istantanea di una struct VoteCounter, inizializzata con il numero di voti iniziali indicato.

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

2. Ottieni il numero di voti da un contatore

Ora hai un modo per creare nuovi contatori! Ma adesso ti accorgi che il nuovo sistema avrà bisogno anche di un modo per ottenere il numero di voti da un contatore.

Crea una funzione vote_count che prende un contatore (@VoteCounter) come argomento e restituisce il numero di voti presenti nel contatore.

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

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

3. Incrementa i voti di un contatore

È finalmente arrivato il momento di contare i voti! Ora ti serve un modo per incrementare i voti in un contatore.

Crea una funzione increment_vote_count che prende come argomenti un contatore (@VoteCounter) e un numero di voti, e incrementa il contatore di quel numero di voti.

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. Crea i risultati delle elezioni

Ora che tutti i voti sono stati contati, è il momento di preparare l'annuncio dei risultati per tutta la scuola. Per farlo, ti accorgi che avere solo dei contatori per i voti non basta. Serve un modo per associare il numero di voti a un particolare candidato.

Crea una funzione new_election_result che riceve il nome di un candidato e il suo numero di voti e restituisce un nuovo risultato elettorale.

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

La struct del risultato elettorale è già stata creata per te ed è definita così:

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

5. Annuncia i risultati

È il momento di annunciare il nuovo presidente alla scuola! Il nuovo presidente sarà annunciato nei piccoli tabelloni digitali di cui la scuola dispone.

Il messaggio deve mostrare il nome del nuovo presidente e i voti che ha ottenuto, nel formato seguente: <candidate_name> (<votes>). Questo è un esempio di tale messaggio: "Peter (51)".

Crea una funzione display_result che riceve un @ElectionResult come argomento e restituisce un ByteArray con il messaggio da mostrare.

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

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

6. Riconteggio dei voti

Per assicurarsi che i risultati finali fossero accurati, i voti sono stati ricontati. Durante il riconteggio si è scoperto che il numero di voti di alcuni candidati era sbagliato di uno.

Crea una funzione decrement_votes_of_candidate che riceve i risultati finali e il nome di un candidato di cui devi decrementare il numero di voti. I risultati finali sono forniti sotto forma di un Felt252Dict<u32>, dove le chiavi sono i nomi dei candidati e i valori sono il totale dei loro voti.

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
Modifica tramite GitHub Il link si apre in una nuova finestra o scheda
Cairo Exercism

Vuoi iniziare Giorno delle elezioni?

Iscriviti a Exercism per imparare e padroneggiare Cairo con 25 concetti68 esercizi e il mentoring di persone reali, tutto gratis.