Leer de teclado en Rust

En muchas aplicaciones es necesario leer datos de teclado. Si bien esto podría considerarse sencillo en lenguaje como Python o Ruby, lo cierto es que sí se quiere hacer bien es complicado. Funciones como scanf de C son consideradas inseguras y en Java, hasta la llegada de la clase java.util.Scanner era un dolor de cabeza. En Rust no es distinto, es por ello que muchos tutoriales de Rust obvian esta parte. No obstante, leer de teclado no es tan difícil, como veremos a continuación.

read_line

El método principal para leer de teclado es read_line, que nos lee una línea como String. Para acceder a read_line primero necesitamos tener on objeto stdin. La manera más fácil de hacerlo es usar el módulo std::io.

El procedimiento es el siguiente, en primer lugar creamos una variable de tipo String vacía y mutable donde se va a alojar el resultado, posteriormente leemos y tratamos el resultado.

use std::io;

fn main() {
    println!("Dime tu nombre: ");
    let mut input = String::new();
    io::stdin().read_line(&mut input);
    println!("Tu nombre es {}",input.trim());
}

Como vemos, al leer la línea también se nos guarda el salto de línea. Si queremos quitarlo podemos usar trim.

Este código sin embargo generará una advertencia por el compilador y es que read_line genera devuelve un valor, concretamente un Result, que como vimos, sirven para el manejo de errores en Rust. Si no queremos tratar este Result con especial interés, podemos usar ok y opcionalmente especificar un mensaje de error con expect.

use std::io;

fn main() {
    println!("Dime tu nombre: ");
    let mut input = String::new();
    io::stdin().read_line(&mut input).ok().expect("Error al leer de teclado");
    println!("Tu nombre es {}",input.trim());
}

Si quieres tratar el error mejor puedes, pero read_line no suele fallar.

Leyendo enteros

Hasta aquí todo sencillo, porque leíamos String, en lo que entra todo lo que el usuario puede meter. Pero, ¿y si queremos leer un número de teclado? La cosa se complica. Normalmente se lee de teclado como String y luego se intenta pasar a número. Veamos como.

use std::io;
use std::str::FromStr;

fn main() {
    println!("Dime tu edad: ");
    let mut input = String::new();
    io::stdin().read_line(&mut input).ok().expect("Error al leer de teclado");
    let edad: u32 = u32::from_str(&input.trim()).unwrap();
    let frase = if edad >= 18 {
        "Mayor de edad"
    }else{
        "Menor de edad"
    };
    println!("{}",frase);
}

Como vemos, hay que importar std::str::FromStr para tener disponible las operaciones from_str en los tipos elementales. También se observa que hemos hecho un unwrap, porque from_str devuelve un Result. Este error sin embargo conviene que lo tratemos con más cuidado, pues es bastante probable que salte.

Un ejemplo ideal

En este código vamos a ver como pedir un entero, asegurándonos de que el usuario realmente introduce un entero e insistiendo hasta que finalmente introduce un entero válido.

use std::io;
use std::io::Write;
use std::str::FromStr;
use std::num::ParseIntError;

fn read_input() -> Result<u32,ParseIntError> {
    print!("Dime tu edad: ");
    io::stdout().flush().ok();
    let mut input = String::new();
    io::stdin().read_line(&mut input).ok().expect("Error al leer de teclado");
    let input = input.trim();
    let edad: u32 = u32::from_str(&input)?;
    Ok(edad)
}

fn main() {
    let edad;
    loop {
        if let Ok(e) = read_input(){
            edad = e;
            break;
        }else{
            println!("Introduce un número, por favor");
        }
    }
    let frase = if edad >= 18 {
        "Mayor de edad"
    }else{
        "Menor de edad"
    };
    println!("{}",frase);
}

He decidido separar la parte de pedir el número a otra función que devuelve Result para así poder usar el operador ?. También he usado print! y io::stdout().flush() en vez de println! para que tanto el mensaje como la entrada se realice en la misma línea y quede más bonito.

 

loading...

Autómatas celulares unidimensionales en Python

Estaba yo leyendo este verano un libro titulado Think Complexity cuando en un capítulo empezó a hablar de los autómatas celulares unidimensionales. El tema me interesó y por eso esta entrada. Veamos primero a qué nos referimos cuando hablamos de esto.

Cuando hablamos a autómatas celulares, nos referimos a pequeñas entidades independientes pero que interaccionan entre sí. Celulares porque son la unidad elemental del universo donde van a existir y autómatas porque deciden por ellas mismas, basadas en un conjunto de reglas predefinido, cuando el tiempo avanza de forma discreta (es decir, a pasos).

Este concepto abstracto puede visualizarse con facilidad si nos imaginamos una rejilla. Cada celda es una célula capaz de cambiar su estado según su entorno.

Los autómatas celulares fueron objeto de estudio de Stephen Wolfram, matemático conocido por haber diseñado el programa Mathemathica y Wolfram Alpha.

Los autómatas celulares unidimensionales son aquellos que forman parte de un universo unidimensional. Es decir, cada célula tiene una vecina a su izquierda y a su derecha. En los bordes se pueden definir varios comportamientos pero el concepto no varía. Pensemos en ello como una tira de celdas.

El estudio de estos autómatas es interesante, ya que pueden generarse patrones/situaciones muy complejas en base a unas reglas sencillas.

¿Cómo se definen las reglas?

Wolfram usó un sistema para definir las reglas de estos autómatas que hoy conocemos como Wolfram Code. Se basa en definir una tabla con los estados presentes de la célula y sus vecinas así como el valor que deberá adoptar en esa situación la célula. Como Wolfram usó células con solo dos estados, todo está en binario, y la parte baja de la tabla es un número de 8 bits. Este número se suele pasar a decimal y así se identifica.

Estados presentes 111 110 101 100 011 010 001 000
Estado futuro 0 0 1 1 0 0 1 0

Esta tabla representa la Regla 50, porque 00110010 en binario es 50.

¿Cómo se representan?

Una manera muy interesante de representar estos autómatas es poner cada paso en una fila distinta dentro de una imagen.

Una vez que sabemos esto vamos a hacer un programa en Python que nos permita observar la evolución de estos autómatas.

Usaremos el procedimiento original, que es empezar con todos los estados de los autómatas en 0 salvo el del autómata central, que será 1.

La clase Automata

La clase autómata va a contener las reglas, así como un ID y el estado que posee. Además, por cuestiones técnicas conviene guardar el estado anterior que tuvo.

class Automata(object):

    rules = list()

    def __init__(self,idx=0):
        self.idx = idx
        self.state = False
        self.statePrev = False

Como podéis ver, rules no lleva self, es decir, va a ser una variable compartida entre todas las instancias de Automata. Esto es porque las reglas son idénticas a todos los autómatas.

La clase World

Ahora vamos a definir el universo donde residen estos autómatas. Este universo almacena una lista con los autómatas, se encarga de actualizarlos según las normas y de dibujarlos usando PIL. También he insertado el código que codifica las normas según el número en decimal.

class World(object):
    def __init__(self,rule=50):
        self.rule = rule
        self.im = Image.new("L",(WIDTH,HEIGHT))
        self.data = np.zeros(WIDTH*HEIGHT,dtype=np.uint8)
        b = bin(rule)[2:].zfill(8)
        Automata.rules = [True if c == "1" else False for c in b]
        self.list = list()
        self.step = 0
    
    def add(self):
        automata = Automata(len(self.list))
        self.list.append(automata)

    def update(self):
        for automata in self.list:

            automata.statePrev = automata.state
            p = self.list[automata.idx - 1].statePrev if automata.idx > 0 else False
            n = self.list[automata.idx + 1].state if automata.idx < len(self.list)-1 else False
            s = automata.state

            if p and s and n:
                automata.state = automata.rules[0]
            elif p and s and not n:
                automata.state = automata.rules[1]
            elif p and not s and n:
                automata.state = automata.rules[2]
            elif p and not s and not n:
                automata.state = automata.rules[3]
            elif not p and s and n:
                automata.state = automata.rules[4]
            elif not p and s and not n:
                automata.state = automata.rules[5]
            elif not p and not s and n:
                automata.state = automata.rules[6]
            elif not p and not s and not n:
                automata.state = automata.rules[7]
            

    def draw_row(self):
        if self.step == 0:
            middle = len(self.list) // 2
            self.list[middle].state = True
        for i,automata in enumerate(self.list):
            if automata.state:
                self.data[self.step*HEIGHT+i] = 255
        self.step += 1
    def save(self):
        self.im.putdata(self.data)
        self.im.save("RULE-%d.png" % self.rule)

Con esto ya lo tenemos casi todo. Ahora faltaría poner en marcha todo. La idea es simplemente crear una instancia de World, hacer unas cuantas llamadas a add, y después ir haciendo el ciclo update/draw_row. Una vez hayamos acabado, hacemos save y obtendremos un PNG con la imagen.

Código completo

import numpy as np
from PIL import Image

WIDTH = 5001
HEIGHT = 5001

class Automata(object):

    rules = list()

    def __init__(self,idx=0):
        self.idx = idx
        self.state = False
        self.statePrev = False

class World(object):
    def __init__(self,rule=50):
        self.rule = rule
        self.im = Image.new("L",(WIDTH,HEIGHT))
        self.data = np.zeros(WIDTH*HEIGHT,dtype=np.uint8)
        b = bin(rule)[2:].zfill(8)
        Automata.rules = [True if c == "1" else False for c in b]
        print(Automata.rules)
        self.list = list()
        self.step = 0
    
    def add(self):
        automata = Automata(len(self.list))
        self.list.append(automata)

    def update(self):
        for automata in self.list:

            automata.statePrev = automata.state
            p = self.list[automata.idx - 1].statePrev if automata.idx > 0 else False
            n = self.list[automata.idx + 1].state if automata.idx < len(self.list)-1 else False
            s = automata.state

            if p and s and n:
                automata.state = automata.rules[0]
            elif p and s and not n:
                automata.state = automata.rules[1]
            elif p and not s and n:
                automata.state = automata.rules[2]
            elif p and not s and not n:
                automata.state = automata.rules[3]
            elif not p and s and n:
                automata.state = automata.rules[4]
            elif not p and s and not n:
                automata.state = automata.rules[5]
            elif not p and not s and n:
                automata.state = automata.rules[6]
            elif not p and not s and not n:
                automata.state = automata.rules[7]
            

    def draw_row(self):
        if self.step == 0:
            middle = (len(self.list) // 2)
            self.list[middle].state = True
        for i,automata in enumerate(self.list):
            if automata.state:
                self.data[self.step*HEIGHT+i] = 255
        self.step += 1
    def save(self):
        self.im.putdata(self.data)
        self.im.save("RULE-%d.png" % self.rule)
    def __str__(self):
        s = str()
        for l in self.list:
            s += "T" if l.state else "F"
        return s

def world_run(rule):
    world = World(rule)
    for _ in range(WIDTH):
        world.add()

    for _ in range(HEIGHT):
        world.draw_row()
        world.update()
    world.save()

def main():
    rule = input("Rule: ")
    try:
        rule = int(rule)
        if 255 >= rule >= 0:
            world_run(rule)
            print("Check for the generated RULE-%d.png file" % rule)
        else:
            raise ValueError
    except ValueError:
        print("Please, insert a number between 0 and 255")
        main()

if __name__ == "__main__":
    main()

Algunas reglas importantes

Regla 30

Una de las más importantes a nivel matemático. Ha sido objeto de mucho estudio, sin embargo no vamos a entrar en detalles más allá de su aspecto visual.

Vista ampliada

Regla 110

Esta regla es también muy interesante. ¡Se demostró que era Turing completa!

Vista en detalle

Regla 126

Esta regla no es tan importante, pero personalmente me parece muy bonita.

Vista ampliada

Reglas 57 y 99

Son dos reglas isomorfas. Es decir, son en realidad la misma regla pero aplicada a lados distintos. Elijo estas dos porque se aprecia muy bien el isomorfismo.

Regla 57
Regla 99

Regla 169

Vista en detalle

Regla 129

Regla 90

Es el famoso triángulo de Sierpinski.

Regla 150

Regla 105

Esta regla no tiene isomorfo.

En este artículo no he querido entrar en las complejidades matemáticas de todo esto. Es algo que todavía no entiendo así que no sería sincero por mi parte exponerlo.

Bonus: Richard Feynman y Steve Jobs

Quien me conoce sabe de sobra que uno de los personajes de la historia que más ha influido en mi vida es Richard Feynman. Debo reconocer que entré en un estado de éxtasis al descubrir que Feynman y Wolfram no solo trabajaron juntos, sino que lo hicieron alrededor de la regla 30 antes mostrada. También me sorprendió que Steve Jobs y Wolfram resultasen ser amigos de toda la vida. No dejo de sorprenderme de los contactos de ciertos personajes históricos entre sí.

Feynman a la izquierda, Wolfram a la derecha

Formulario de registro en Elm

Recientemente he estado experimentando con Elm, un lenguaje funcional puro y a la vez framework de backend. De momento he programado ya he visto unas cuántas cosas y puedo decir que me gusta bastante, que es un soplo de aire fresco en el mundo web y que sinceramente no creo que vaya a triunfar algún día. Sin embargo para proyectos personales, lo veo como alternativa superior a Angular, React y Vue.

Uno de los ejercicios que puede venir bien para entender Elm es implementar un formulario de registro con normas para la contraseña. Según vayamos escribiendo se nos informa sobre qué errores tiene la contraseña para ser segura y cuando lo sea mostramos el botón de registro.

Es un código un poco tonto pero me apetecía compartirlo.

import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (onInput)
import Char
import String
import List

main : Program Never Model Msg
main = Html.program {init = init, view = view, update = update, subscriptions = subscriptions}

type alias Model =
{
    name : String,
    password : String,
    passwordAgain : String
}

init : (Model, Cmd Msg)
init = (Model "" "" "", Cmd.none)

subscriptions : Model -> Sub Msg
subscriptions model = Sub.none

type Msg = Name String | Password String | PasswordAgain String

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
    case msg of
        Name name -> ({ model | name = name},Cmd.none)
        Password pass -> ({model | password = pass},Cmd.none)
        PasswordAgain pass -> ({model | passwordAgain = pass},Cmd.none)

view : Model -> Html Msg
view model =
    div []
        [
            input[type_ "text", placeholder "Name", onInput Name][],
            input[type_ "password", placeholder "Password", onInput Password][],
            input[type_ "password", placeholder "Confirm", onInput PasswordAgain][],
            viewValidate model
        ]

viewValidate : Model -> Html Msg
viewValidate model =
    let list = checkMinLength model :: checkPassword model :: checkUpper model :: checkLower model :: checkDigit model :: []
    wrongList = List.filter (\(x,y) -> not x) list
    in
    if List.length wrongList == 0 then
        div [] [button[]]
    else
        div [] (List.map showError wrongList )

showError : (Bool,String) -> Html Msg
showError (_,error) =
    div [style[("color","red")]] 

checkPassword : Model -> (Bool, String)
checkPassword model =
    if model.password == model.passwordAgain then
        (True,"")
    else
        (False,"Passwords do not match")

checkMinLength : Model -> (Bool,String)
checkMinLength model =
    if String.length model.password > 7 then
        (True,"OK")
    else
        (False,"Password must have a minimum length of 8 characters")

checkUpper : Model -> (Bool,String)
checkUpper model =
    let isUp = String.any Char.isUpper model.password
    in
    if isUp then
        (True,"")
    else
        (False,"The password must contain an upper letter")

checkLower : Model -> (Bool,String)
checkLower model =
    let isDown = String.any Char.isLower model.password
    in
    if isDown then
        (True,"")
    else
        (False,"The password must contain a lower letter")

checkDigit : Model -> (Bool,String)
checkDigit model =
    let isDig = String.any Char.isDigit model.password
    in
    if isDig then
        (True,"")
    else
        (False,"The password must contain a digit")

 

Documentación con rustdoc

La documentación es una parte importante y muchas veces olvidada de un proyecto de software. Rust cuenta desde el principio con una herramienta destinada a que escribir documentación sea poco doloroso, se trata de rustdoc.

Comentarios en Markdown

Los comentarios de rustdoc empiezan con 3 barras y pueden llevar Markdown. Se escriben encima de la función, estructura, etc que describamos.

/// Perfil almacena los datos del perfil de un usuario en nuestra webapp
struct Perfil{
    username: String,
    password: String,
    url: Option<String>
}

impl Perfil{
    /// Genera un nuevo Perfil
    /// # Ejemplo
    /// ```
    /// let user = Perfil::new("The42","1234");
    /// ```
    pub fn new(u: &str, p: &str) -> Perfil{
        Perfil {username: String::from(u), password: String::from(p), url: None}
    }
}

Mencionar que el código del comentario es sometido a tests también por cargo test. Para generar la documentación basta con escribir cargo doc y Cargo generará la documentación en formato HTML.

Consultando documentación

El mejor sitio para leer la documentación de Rust es Docs.rs. Docs.rs ejecuta cargo doc a todas las crates de Crates.io y las expone al público sin costo.

 

Tests en Rust

Mientras los programas no puedan verificarse de forma matemática de forma sencilla, la única manera de asegurarse que un programa más o menos funciona es con tests. Rust soporta tests de forma nativa. Gracias a la directiva #[test].

Definiendo una función de test

Una función de test es cualquiera que lleve la directiva #[test]. Normalmente, estos tests se dejan dentro de un módulo con la directiva #[cfg(test)].

fn suma(a: i32,b: i32) -> i32{
    a+b
}

#[cfg(test)]
mod test{

    #[test]
    fn suma(){
        assert_eq!(super::suma(4,5),9);
    }
}

La macro assert_eq! provocará un panic! si sus argumentos no coinciden. También es posible hacer fallar los tests llamando a panic! manualmente. ¿Cómo se ejecutan estos test te preguntarás? Sencillo, con cargo test. Automáticamente Cargo selecciona las funciones de test y las ejecuta todas, generando un informe de tests existosos y tests que han fallado.

Obviamente, existen más opciones dentro de los tests. assert! comprobará que una expresión sea true. #[should_panic] se deberá indicar en aquellas funciones de test en lo que lo correcto sea que ocurra un panic!. Por otro lado, la trait Debug es interesante.

Es posible ejecutar tests de forma manual, con cargo test NOMBRE_TEST.