Rutas
/
Cairo
Cairo
/
Ejercicios
/
Día de las elecciones
Día de las elecciones

Día de las elecciones

Ejercicio de aprendizaje

Introducción

Estructuras

Las estructuras son tipos de datos personalizados que te permiten nombrar y agrupar varios valores relacionados.

Para definir una estructura, usa la palabra clave struct seguida de un nombre y unas llaves que contienen los campos con sus nombres y tipos:

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

Para crear una instancia de una struct, especifica el nombre de la struct y usa llaves con pares clave-valor para los campos.

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

Usa la notación de punto para acceder a los campos de una instancia mutable de una struct o para modificarlos.

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

Propiedad

Los valores en Cairo son inmutables, y esta inmutabilidad garantiza que, aunque muchas variables hagan referencia al mismo valor, el valor en sí permanezca constante y sin alterar. El propietario de una variable es el código que puede leer esa variable (y escribir en ella si es mutable).

Reglas de la propiedad en Cairo:

  1. Cada variable en Cairo tiene un propietario.
  2. Solo puede haber un propietario a la vez.
  3. Cuando el propietario sale del scope, la variable se destruye.

Un scope es el rango dentro de un programa en el que una variable es 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

Cuando una variable sale del scope, se destruye, lo que garantiza que el recurso asociado se libere correctamente. Esto se consigue implementando o derivando el trait Drop, que indica al compilador que el tipo se puede destruir de forma segura una vez que ya no es útil.

#[derive(Drop)]
struct A {}

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

Por el momento, la implementación de Drop se puede derivar para todos los tipos, excepto para los diccionarios (Felt252Dict) y los tipos que contienen diccionarios.

Un valor también se puede mover, lo que ocurre cuando se pasa un valor a otra función: se destruye la variable original que hacía referencia al valor (dejando así la variable inservible) y se crea una nueva variable que contiene el mismo 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 equivale a moverlos.

El ejemplo siguiente muestra una función que devuelve un 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 una variable se destruye después de moverla, el compilador nos prohíbe volver a usarla después.

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

En este caso, intentamos pasar el mismo valor (la struct Point de la variable p) a ambas llamadas a función. Una vez que hemos pasado la variable a la función foo por primera vez, la variable ya no se puede usar.

Una forma de solucionarlo es derivar el trait Copy en nuestra estructura, lo que cambiaría lo que ocurre cuando pasamos una variable Copy a una función: ya no se movería el valor, sino que se crearía una nueva variable que hace referencia al mismo valor. Esta operación no tiene coste porque las variables son una abstracción de Cairo y los valores son siempre inmutables.

Para implementar el trait Copy en tu tipo, añade la anotación #[derive(Copy)] a la definición 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
}

Referencias y snapshots

Cairo tiene dos funcionalidades para pasar un valor sin destruirlo ni moverlo, llamadas references y snapshots.

Los snapshots proporcionan una «vista» inmutable de un valor en un momento determinado, lo que te permite conservar la propiedad de una variable mientras accedes a su estado. Cualquier modificación de la variable crea una nueva celda de memoria, lo que deja la vista del snapshot sin cambios.

Para crear un snapshot, basta con anteponer @ a la instancia de la variable.

// 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 convertir un snapshot de nuevo en una variable normal, puedes usar el operador desnap *, que es lo opuesto al operador @. Solo se les puede aplicar desnap a aquellos tipos que implementan el trait 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);
}

Las referencias te permiten pasar valores mutables a funciones y que estos se devuelvan de forma implícita, manteniendo la propiedad en el contexto de llamada. Esto se puede conseguir con el modificador ref, siempre que la variable se declare como mutable con la palabra clave mut.

Podemos actualizar el ejemplo anterior de Point para que use referencias mutables en lugar de depender del trait Copy, y seguiría funcionando.

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

Instrucciones

Una escuela local cercana tiene una asociación de estudiantes muy activa. La asociación de estudiantes está dirigida por un presidente y cada 2 años se celebran elecciones para elegir a uno nuevo. En las elecciones de este año se decidió que hacía falta un nuevo sistema digital para contar los votos.

La escuela necesita tu ayuda para crear este nuevo sistema.

1. Crea un contador de votos

Una de las primeras cosas que necesita el nuevo sistema de votación es un contador de votos. Este contador sirve para llevar la cuenta de los votos que tiene un candidato concreto.

Crea una función new_vote_counter que acepte el número de votos iniciales de un candidato y devuelva una instantánea de una struct VoteCounter, inicializada con el número de votos iniciales indicado.

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

2. Obtén el número de votos de un contador

¡Ya tienes una forma de crear contadores nuevos! Pero ahora te das cuenta de que el nuevo sistema también necesitará una forma de obtener el número de votos de un contador.

Crea una función vote_count que reciba un contador (@VoteCounter) como argumento y devuelva el número de votos del contador.

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

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

3. Incrementa los votos de un contador

¡Por fin es el momento de contar los votos! Ahora necesitas una forma de incrementar los votos de un contador.

Crea una función increment_vote_count que reciba un contador (@VoteCounter) como argumento y un número de votos, e incremente el contador en ese 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. Crea los resultados de la elección

Con todos los votos ya contados, es el momento de preparar el anuncio de los resultados para toda la escuela. Para ello, te das cuenta de que tener solo contadores de votos no es suficiente. Hace falta una forma de asociar el número de votos con un candidato concreto.

Crea una función new_election_result que reciba el nombre de un candidato y su número de votos y devuelva un nuevo resultado de la elección.

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

La struct de resultado de la elección ya está creada y se define así:

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

5. Anuncia los resultados

¡Es el momento de anunciar al nuevo presidente a la escuela! El nuevo presidente se anunciará en los pequeños paneles digitales de mensajes que tiene la escuela.

El mensaje debe mostrar el nombre del nuevo presidente y los votos que obtuvo, con el siguiente formato: <candidate_name> (<votes>). Este es un ejemplo de ese mensaje: "Peter (51)".

Crea una función display_result que reciba un @ElectionResult como argumento y devuelva un ByteArray con el mensaje que hay que mostrar.

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

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

6. Recuento de votos

Para asegurarse de que los resultados finales eran exactos, se volvieron a contar los votos. En el recuento se descubrió que el número de votos de algunos candidatos difería en uno.

Crea una función decrement_votes_of_candidate que reciba los resultados finales y el nombre de un candidato cuyo número de votos debes decrementar. Los resultados finales se dan en forma de Felt252Dict<u32>, donde las claves son los nombres de los candidatos y los valores son sus votos totales.

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 en GitHub El enlace se abre en una ventana o pestaña nueva
Cairo Exercism

¿Listo para empezar Día de las elecciones?

Regístrate en Exercism para aprender y dominar Cairo con 25 conceptos68 ejercicios y mentoría humana real, todo gratis.