Percursos
/
Cairo
Cairo
/
Exercícios
/
Dia de eleições
Dia de eleições

Dia de eleições

Exercício de aprendizagem

Introdução

Structs

As structs são tipos de dados personalizados que te permitem dar um nome e agrupar vários valores relacionados.

Para definir uma struct, usa a palavra-chave struct seguida de um nome e de chavetas que contêm os campos com os respetivos nomes e tipos:

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

Para criar uma instância de uma struct, indica o nome da struct e usa chavetas com pares chave-valor para os campos.

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

Usa a notação de ponto para aceder aos campos de uma instância de struct mutável ou para os modificar.

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

Propriedade

Os valores em Cairo são imutáveis, e essa imutabilidade garante que, mesmo que muitas variáveis se refiram ao mesmo valor, o próprio valor permanece constante e inalterado. O proprietário de uma variável é o código que pode ler (e escrever, se for mutável) essa variável.

Regras de propriedade em Cairo:

  1. Cada variável em Cairo tem um proprietário.
  2. Só pode haver um proprietário de cada vez.
  3. Quando o proprietário sai do âmbito, a variável é destruída.

Um âmbito é a região de um programa na qual uma variável é válida.

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 uma variável sai do âmbito, é destruída, o que garante que o recurso associado é libertado corretamente. Isto consegue-se implementando ou derivando o trait Drop, que indica ao compilador que o tipo pode ser destruído em segurança quando já não for útil.

#[derive(Drop)]
struct A {}

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

De momento, a implementação de Drop pode ser derivada para todos os tipos, exceto para dicionários (Felt252Dict) e para tipos que contenham dicionários.

Um valor também pode ser movido. Isto acontece quando um valor é passado a outra função: a variável original que se referia ao valor é destruída (ficando assim inútil) e é criada uma nova variável para conter o mesmo valor.

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
}

Devolver valores é equivalente a movê-los.

O exemplo abaixo mostra uma função que devolve um valor:

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

Como uma variável é destruída depois de ser movida, o compilador impede-nos de a reutilizar a seguir.

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

Neste caso, tentamos passar o mesmo valor (a struct Point na variável p) às duas chamadas de função. Depois de passarmos a variável à função foo pela primeira vez, a variável já não pode ser usada.

Uma forma de resolver isto é derivar o trait Copy na nossa struct, o que alteraria o que acontece quando passamos uma variável Copy a uma função: deixaria de mover o valor e criaria uma nova variável que se refere ao mesmo valor. Esta é uma operação gratuita, porque as variáveis são uma abstração de Cairo e os valores são sempre imutáveis.

Para implementar o trait Copy no teu tipo, adiciona a anotação #[derive(Copy)] à definição do 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
}

Referências e snapshots

Cairo tem duas funcionalidades para passar um valor sem o destruir ou mover, chamadas references e snapshots.

Os snapshots fornecem uma "vista" imutável de um valor num determinado momento, permitindo-te manter a propriedade de uma variável enquanto acedes ao seu estado. Qualquer modificação à variável cria uma nova célula de memória, deixando a vista do snapshot inalterada.

Para criar um snapshot, basta colocar @ antes da instância da variável.

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

Para converter um snapshot de volta numa variável normal, podes usar o operador desnap *, que funciona como o oposto do operador @. Só os tipos que implementam o trait Copy podem sofrer 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);
}

As referências permitem-te passar valores mutáveis a funções e recebê-los de volta implicitamente, mantendo a propriedade no contexto de quem chama. Isto consegue-se com o modificador ref, desde que a variável seja declarada como mutável com a palavra-chave mut.

Podemos atualizar o exemplo anterior da struct Point para usar referências mutáveis em vez de depender do trait Copy, e continuaria a funcionar.

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

Instruções

Uma escola local perto de ti tem uma associação de estudantes muito ativa. A associação de estudantes é gerida por um presidente e, a cada 2 anos, realizam-se eleições para eleger um novo presidente. Nas eleições deste ano, decidiu-se que era necessário um novo sistema digital para contar os votos.

A escola precisa da tua ajuda para construir este novo sistema.

1. Cria um contador de votos

Uma das primeiras coisas de que o novo sistema de votação precisa é de um contador de votos. Este contador é uma forma de acompanhar os votos que um determinado candidato tem.

Cria uma função new_vote_counter que aceita o número de votos iniciais de um candidato e devolve um snapshot de uma struct VoteCounter, inicializada com o número de votos iniciais indicado.

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

2. Obter o número de votos de um contador

Agora já tens uma forma de criar novos contadores! Mas agora reparas que o novo sistema também vai precisar de uma forma de obter o número de votos de um contador.

Cria uma função vote_count que recebe um contador (@VoteCounter) como argumento e devolve o número de votos do contador.

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

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

3. Incrementar os votos de um contador

Chegou finalmente a hora de contar os votos! Agora precisas de uma forma de incrementar os votos de um contador.

Cria uma função increment_vote_count que recebe um contador (@VoteCounter) como argumento e um número de votos, e incrementa o contador com esse número de votos.

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. Criar os resultados das eleições

Com todos os votos contados, é hora de preparar o anúncio dos resultados para toda a escola. Para isso, reparas que ter apenas contadores para os votos é insuficiente. É preciso haver uma forma de associar o número de votos a um determinado candidato.

Cria uma função new_election_result que recebe o nome de um candidato e o seu número de votos e devolve um novo resultado eleitoral.

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

A struct do resultado eleitoral já está criada para ti e está definida como:

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

5. Anunciar os resultados

É hora de anunciar o novo presidente à escola! O presidente será anunciado nos pequenos painéis de mensagens digitais que a escola tem.

A mensagem deve mostrar o nome do novo presidente e os votos que teve, no seguinte formato: <candidate_name> (<votes>). Este é um exemplo dessa mensagem: "Peter (51)".

Cria uma função display_result que recebe um @ElectionResult como argumento e devolve um ByteArray com a mensagem a apresentar.

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

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

6. Recontagem dos votos

Para garantir que os resultados finais estavam corretos, os votos foram recontados. Na recontagem, verificou-se que o número de votos de alguns candidatos estava errado por uma unidade.

Cria uma função decrement_votes_of_candidate que recebe os resultados finais e o nome de um candidato cuja contagem de votos deves decrementar. Os resultados finais são dados sob a forma de um Felt252Dict<u32>, em que as chaves são os nomes dos candidatos e os valores são o total de votos de cada um.

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
Editar via GitHub A ligação abre numa nova janela ou separador
Cairo Exercism

Estás pronto para começar Dia de eleições?

Inscreve-te no Exercism para aprenderes e dominares Cairo com 25 conceitos68 exercícios, e mentoria humana real, tudo grátis.