0179 El Lenguaje de Programacion Rust PDF
0179 El Lenguaje de Programacion Rust PDF
Tabla de contenido
Introducción 0
Primeros Pasos 1
Instalando Rust 1.1
¡Hola, mundo! 1.2
¡Hola, Cargo! 1.3
Aprende Rust 2
El Juego de las Adivinanzas 2.1
La Cena de los Filósofos 2.2
Rust dentro de otros Lenguajes 2.3
Rust Efectivo 3
La Pila y el Montículo 3.1
Pruebas 3.2
Compilación Condicional 3.3
Documentación 3.4
Iteradores 3.5
Concurrencia 3.6
Manejo de Errores 3.7
FFI 3.8
Borrow y AsRef 3.9
Canales de Distribución 3.10
Sintaxis y Semantica 4
Enlaces a Variables 4.1
Funciones 4.2
Tipos Primitivos 4.3
Comentarios 4.4
if 4.5
Ciclos 4.6
Pertenencia 4.7
Referencias y Préstamo 4.8
Tiempos de Vida 4.9
2
El Lenguaje de Programacion Rust
Mutabilidad 4.10
Estructuras 4.11
Enumeraciones 4.12
Match 4.13
Patrones 4.14
Sintaxis de Métodos 4.15
Vectores 4.16
Cadenas de Caracteres 4.17
Genéricos 4.18
Traits 4.19
Drop 4.20
if let 4.21
Objetos Trait 4.22
Closures 4.23
Sintaxis Universal de Llamada a Funciones 4.24
Crates y Módulos 4.25
`const` y `static` 4.26
Atributos 4.27
Alias `type` 4.28
Conversión entre Tipos 4.29
Tipos Asociados 4.30
Tipos sin Tamaño 4.31
Operadores y Sobrecarga 4.32
Coerciones Deref 4.33
Macros 4.34
Apuntadores Planos 4.35
`unsafe` 4.36
Rust Nocturno 5
Plugins del Compilador 5.1
Ensamblador en Linea 5.2
No stdlib 5.3
Intrínsecos 5.4
Items de Lenguaje 5.5
Enlace Avanzado 5.6
3
El Lenguaje de Programacion Rust
4
El Lenguaje de Programacion Rust
¡Bienvenido! Este libro te enseñará acerca del Lenguaje de Programación Rust. Rust es un
lenguaje de programación de sistemas enfocado en tres objetivos: seguridad, velocidad y
concurrencia. Rust logra estos objetivos sin tener un recolector de basura, haciendolo util
para un numero de casos de uso para los cuales otros lenguajes no son tan buenos:
embeber en otros lenguajes, programas con requerimientos específicos de tiempo y
espacio, escritura de código de bajo nivel, como controladores de dispositivo y sistemas
operativos. Rust mejora por sobre los lenguajes actuales en este nicho a través de un
numero de chequeos en tiempo de compilación que no incurren ninguna penalidad en
tiempo de ejecución, eliminando al mismo tiempo las condiciones de carrera. Rust también
implementa 'abstracciones con cero costo', abstracciones que se sienten como las de un
lenguaje de alto nivel. Aun así, Rust permite control preciso tal y como un lenguaje de bajo
nivel lo haria.
“El Lenguaje de Programación Rust” esta dividido en siete secciones. Esta introducción es
la primera. Después de esta:
Contribuyendo
Los archivos fuente de los cuales este libro es generado pueden ser encontrados en Github:
github.com/goyox86/elpr-sources
Introducción 5
El Lenguaje de Programacion Rust
fn main() {
let mut x = vec!["Hola", "mundo"];
}
Este programa crea un una variable llamada x .El valor de esta variable es Vec<T> , un
‘vector’, que creamos a través de una macro definida en la biblioteca estandar. Esta macro
se llama vec , las macros son invocadas con un ! . Todo esto siguiendo un principio
general en Rust: hacer las cosas explícitas. Las macros pueden hacer cosas
significativamente más complejas que llamadas a funciones, es por ello que son
visualmente distintas. El ! ayuda también al análisis sintáctico, haciendo la escritura de
herramientas más fácil, lo cual es también importante.
Hemos usado mut para hacer x mutable: En Rust las variables son inmutables por
defecto. Más tarde en este ejemplo estaremos mutando este vector.
Es importate mencionar que no necesitamos una anotación de tipos aquí: si bien Rust es
estaticamente tipado, no necesitamos anotar el tipo de forma explicita. Rust posee
inferencia de tipos para balancear el poder de el tipado estático con la verbosidad de las
anotaciones de tipos.
Rust prefiere asignación de memoria desde la pila que desde el montículo: x es puesto
directamente en la pila. Sin embargo, el tipo Vec<T> asigna espacio para los elementos del
vector en el montículo. Si no estas familiarizado con esta distinción puedes ignorarla por
ahora o echar un vistazo ‘La Pila y el Monticulo’. Rust como un lenguaje de programación
de sistemas, te da la habilidad de controlar como la memoria es asignada, pero como
estamos comenzando no es tan relevante.
Introducción 6
El Lenguaje de Programacion Rust
fn main() {
let mut x = vec!["Hola", "mundo"];
let y = &x[0];
}
Agreguemos una tercera línea. Dicha línea luce inocente pero causa un error de
compilación:
fn main() {
let mut x = vec!["Hola", "mundo"];
let y = &x[0];
x.push("foo");
}
push es un metodo en los vectores que agrega un elemento al final del vector. Cuando
}
^
¡Uff! El compilador de Rust algunas veces puede proporcionar errores bien detallados y esta
vez una de ellas. Como el error lo explica, mientras hacemos la variable mutable no
podemos llamar a push . Esto es porque ya tenemos una referencia a un elemento del
vector, y . Mutar algo mientras existe una referencia a ello es peligroso, porque podemos
Introducción 7
El Lenguaje de Programacion Rust
invalidar la referencia. En este caso en especifico, cuando creamos el vector, solo hemos
asignado espacio para dos elementos. Agregar un tercero significaría asignar un nuevo
segmento de memoria para todos los elementos, copiar todos los valores anteriores y
actualizar el apuntador interno a esa memoria. Todo eso esta bien. El problema es que y
no seria actualizado, generando un ‘puntero colgante’. Lo cual esta mal. Cualquier uso de
y seria un error en este caso, y el compilador nos ha prevenido de ello.
Entonces, ¿cómo resolvemos este problema? Hay dos enfoques que podríamos tomar. El
primero es hacer una copia en lugar de una referencia:
fn main() {
let mut x = vec!["Hola", "mundo"];
let y = x[0].clone();
x.push("foo");
}
Rust tiene por defecto semántica de movimiento, entonces si queremos hacer una copia de
alguna data, llamamos el método clone() . En este ejemplo y ya no es una referencia a
el vector almacenado en x , sino una copia de su primer elemento, "Hola" . Debido a que
no tenemos una referencia nuestro push() funciona perfectamente.
fn main() {
let mut x = vec!["Hola", "mundo"];
{
let y = &x[0];
}
x.push("foo");
}
Con el par adicional de llaves hemos creado un ámbito interno. y saldrá de ámbito antes
que llamemos a push() , entonces no hay problema.
Este concepto de pertenencia no es solo bueno para prevenir punteros colgantes, sino un
conjunto entero de problemas, como invalidación de iteradores, concurrencia y más.
Introducción 8
El Lenguaje de Programacion Rust
% Primeros Pasos
Esta primera sección del libro te pondrá andando en Rust y sus herramientas. Primero,
instalaremos Rust. Después, el clásico programa ‘Hola Mundo’. Finalmente, hablaremos de
Cargo, el sistema de construcción y manejador de paquetes de Rust.
Primeros Pasos 9
El Lenguaje de Programacion Rust
% Instalando Rust
¡El primer paso para usar Rust es instalarlo! Existen un número de maneras para instalar
Rust, pero la más fácil es usar el script rustup . Si estás en Linux o una Mac, todo lo que
necesitas hacer es (sin escribir los $ s, solo indican el inicio de cada comando):
$ curl -f -L https://static.rust-lang.org/rustup.sh -O
$ sh rustup.sh
Desintalando
Si decides que ya no quieres más Rust, estaremos un poco tristes pero está bien, ningún
lenguaje de programación es para todo el mundo. Simplemente ejecuta el script de
desinstalación:
$ sudo /usr/local/lib/rustlib/uninstall.sh
Algunas personas, con mucho derecho, se ven perturbadas cuando les dices que deben
curl | sh . Básicamente, al hacerlo, estás confiando en que la buena gente que mantiene
Rust no va a hackear tu computador y hacer cosas malas. ¡Eso es buen instinto! Si eres
una de esas personas por favor echa un vistazo a la documentación acerca de compilar
Rust desde el codigo fuente, o la página oficial de descargas de binarios.
Instalando Rust 10
El Lenguaje de Programacion Rust
$ rustc --version
Deberías ver el número de versión, hash del commit, fecha del commit y la fecha de
compilación:
Este instalador también instala una copia de la documentación localmente para que puedas
leerla aún estando desconectado. En sistemas UNIX, /usr/local/share/doc/rust es la
locación. En Windows, es en el directorio share/doc , dentro de donde sea que hayas
instalado Rust.
Si no, hay un número de lugares en los que puedes obtener ayuda. El más fácil es el canal
de IRC #rust en irc.mozilla.org, al cual puedes acceder con Mibbit. Sigue ese enlace y
estarás hablando con Rusteros (a tonto apodo que usamos entre nosotros), y te
ayudaremos. Otros excelentes recursos son el foro de usuarios, y Stack Overflow.
Instalando Rust 11
El Lenguaje de Programacion Rust
% ¡Hola, mundo!
Ahora que has instalado Rust, escribamos tu primer programa. Es tradición que tu primer
programa en cualquier lenguaje sea uno que imprima el texto “¡Hola, mundo!” a la pantalla.
Lo bueno de comenzar con un programa tan simple es que verificas no solo que el
compilador esta instalado, sino que esta funcionando. Imprimir información a la pantalla es
una cosa muy común.
La primera cosa que debemos hacer es crear un archivo en donde poner nuestro código. A
mi me gusta crear un directorio proyectos en mi directorio de usuario y mantener todos mis
proyectos allí. A Rust no le importa dónde reside el código.
Esto lleva en cuestión a otro asunto que debemos aclarar: esta guía asumirá que posees
una familiaridad básica con la linea de comandos. Rust no demanda nada con respecto a
tus herramientas de edición o dónde vive tu código. Si prefieres un IDE a la interfaz de linea
de comandos, probablemente deberías echar un vistazo a SolidOak, o donde sea que los
plugins estén para tu IDE favorito. Existen un número de extensiones de calidad variada en
desarrollo por la comunidad. El equipo detrás de Rust también publica plugins para varios
editores. Configurar tu editor o IDE escapa de los objetivos de éste tutorial, chequea la
documentación para tu configuración específica.
$ mkdir ~/proyectos
$ cd ~/proyectos
$ mkdir hola_mundo
$ cd hola_mundo
Creemos un nuevo archivo de código fuente. Llamaremos main.rs a nuestro archivo. Los
archivos Rust terminan con la extensión .rs . Si estás usando más de una palabra en tu
nombre de archivo, usa un subguion: hola_mundo.rs en vez de holamundo.rs .
fn main() {
println!("¡Hola, mundo!");
}
¡Hola, mundo! 12
El Lenguaje de Programacion Rust
$ rustc main.rs
$ ./main # o main.exe en Windows
¡Hola, mundo!
fn main() {
Estas lineas definen una función en Rust. La función main es especial: es el principio de
todo programa Rust. La primera línea dice: "Estoy declarando una función llamada main la
cual no recibe argumentos y no retorna nada." Si existieran argumentos, estos irían dentro
de paréntesis ( ( and ) ), y debido a que no estamos retornando nada de esta función,
podemos omitir el tipo de retorno completamente. Llegaremos a esto más adelante.
También notaras que la función está envuelta en llaves ( { and } ). Rust requiere dichas
llaves delimitando todos los cuerpos de función. Es también considerado buen estilo colocar
la llave de apertura en la misma linea de la declaración de la función, con un espacio
intermedio.
println!("¡Hola, mundo!");
Esta línea hace todo el trabajo en nuestro pequeño programa. Hay un número de detalles
que son importantes aquí. El primero es que está sangrado con cuatro espacios, no
tabulaciones. Por favor, configura tu editor a insertar cuatro espacios con la tecla tab.
Proveemos algunas configuraciones de ejemplo para varios editores.
El segundo punto es la parte println!() . Esto es llamar a una macro Rust macro, que es
como se hace metaprogramación en Rust. Si esto fuese en cambio una función, luciria asi:
println() . Para nuestros efectos, no necesitamos preocuparnos acerca de esta diferencia.
Solo saber que algunas veces verás ! , y que esto significa que estas llamando a una
macro en vez de una función normal. Rust implementa println! como una macro en vez
de como una función por buenas razones, pero eso es un tópico avanzado. Una última cosa
por mencionar: Las macros en Rust son diferentes de las macros en C, si las has usado. No
estés asustado de usar macros. Llegaremos a los detalles eventualmente, por ahora
simplemente debes confiar en nosotros.
¡Hola, mundo! 13
El Lenguaje de Programacion Rust
$ rustc main.rs
$ ls
main main.rs
O en Windows:
$ dir
main.exe main.rs
Hay dos archivos: nuestro código fuente, con la extensión .rs , y el ejecutable ( main.exe
en Windows, main en los demás)
¡Hola, mundo! 14
El Lenguaje de Programacion Rust
¡Hola, mundo! 15
El Lenguaje de Programacion Rust
% Hola, Cargo!
Cargo es una herramienta que los Rusteros usan como ayuda para administrar sus
proyectos Rust. Cargo esta actualmente en estado pre-1.0, y debido a ello es todavía un
trabajo en proceso. Sin embargo ya es lo suficientemente bueno para muchos proyectos
Rust, y se asume que los proyectos Rust usaran Cargo desde el principio.
Si has instalado Rust a través de los instaladores oficiales entonces deberías tener Cargo.
Si has instalado Rust de alguna otra manera, podrías echar un vistazo a el README de
Cargo para instrucciones especificas acerca de como instalarlo.
Migrando a Cargo
Convirtamos Hola Mundo a Cargo.
$ mkdir src
$ mv main.rs src/main.rs
Nota que debido a que estamos creando un ejecutable, usamos main.rs . Si quisiéramos
crear una biblioteca, deberíamos usar lib.rs . Locaciones personalizadas para el punto de
entrada pueden ser especificadas con una clave [ [[lib]] or [[bin]] ]crates-custom en el
archivo TOML descrito a continuación.
Cargo espera que tus archivos de código fuente residan en el directorio src . Esto deja el
nivel raíz para otras cosas, como READMEs, información de licencias, y todo aquello no
relacionado con tu código. Cargo nos ayuda a mantener nuestros proyectos agradables y
ordenados. Un lugar para todo, y todo en su lugar.
$ editor Cargo.toml
¡Hola, Cargo! 16
El Lenguaje de Programacion Rust
[package]
name = "hola_mundo"
version = "0.0.1"
authors = [ "Tu nombre <[email protected]>" ]
Este archivo esta en formato TOML. Dejemos que sea el mismo quien se explique:
Una vez que tengas este archivo es su lugar, deberíamos estar listos para compilar! Prueba
esto:
$ cargo build
Compiling hola_mundo v0.0.1 (file:///home/tunombre/proyectos/hola_mundo)
$ ./target/debug/hola_mundo
Hola, mundo!
$ cargo run
Running `target/debug/hola_mundo`
Hola, mundo!
Nótese que no reconstruimos el proyecto esta vez. Cargo determino que no habíamos
cambiado el archivo de código fuente, así que simplemente ejecuto el binario. Si
hubiéremos realizado una modificación, deberíamos haberlo visto haciendo los dos pasos:
$ cargo run
Compiling hola_mundo v0.0.1 (file:///home/tunombre/proyectos/hola_mundo)
Running `target/debug/hola_mundo`
Hola, mundo!
¡Hola, Cargo! 17
El Lenguaje de Programacion Rust
Esto no nos ha aportado mucho por encima de nuestro simple uso de rustc , pero piensa
en el futuro: cuando nuestros proyectos se hagan mas complicados, necesitaremos hacer
mas cosas para lograr que todas las partes compilen correctamente. Con Cargo, a medida
que nuestro proyecto crece, simplemente ejecutamos cargo build , y todo funcionara de
forma correcta.
Cuando nuestro proyecto esta finalmente listo para la liberacion, puedes usar cargo build -
-release para compilarlo con optimizaciones.
[root]
name = "hola_mundo"
version = "0.0.1"
Este archivo es usado por Cargo para llevar el control de las dependencias usadas en tu
aplicación. Por ahora, no tenemos ninguna, y esta un poco disperso. Nunca deberías
necesitar tocar este archivo por tu cuenta, solo deja a Cargo manejarlo.
Eso es todo! Hemos construido satisfactoriamente hola_mundo con Cargo. Aunque nuestro
programa sea simple, esta usando gran parte de las herramientas reales que usaras por el
resto de tu carrera con Rust. Puedes asumir que para comenzar virtualmente con todo
proyecto Rust harás lo siguiente:
Un Proyecto Nuevo
No tienes que pasar por todo ese proceso completo cada vez que quieras comenzar un
proyecto nuevo! Cargo posee la habilidad de crear un directorio plantilla en el cual puedes
comenzar a desarrollar inmediatamente.
¡Hola, Cargo! 18
El Lenguaje de Programacion Rust
$ cd hola_mundo
$ tree .
.
├── Cargo.toml
└── src
└── main.rs
1 directory, 2 files
Esto es todo lo que necesitas para comenzar. Primero veamos nuestro Cargo.toml :
[package]
name = "hola_mundo"
version = "0.0.1"
authors = ["Tu Nombre <[email protected]>"]
Cargo a rellenado este archivo con valores por defecto basado en los argumentos que le
proporcionaste y tu configuración global git . Podrias notar tambien que Cargo ha
inicializado el directorio hola_mundo como un repositorio git .
fn main() {
println!("Hola, mundo!");
}
Cargo ha generado un "Hola, mundo!" para nosotros, ya estas listo para empezar a codear.
Cargo posee su propia guia la cual cubre todas sus características con mucha mas
profundidad.
Ahora que hemos aprendido las herramientas, comencemos en realidad a aprender mas de
Rust como lenguaje. Esto es la báse que te servirá bien por el resto de tu tiempo con Rust.
Tienes dos opciones: Sumergirte en un proyecto con ‘Aprende Rust’, o comenzar desde el
fondo y trabajar hacia arriba con ‘Sintaxis y Semántica’. Programadores de sistemas mas
experimentados probablemente preferiran ‘Aprende Rust’, mientras que aquellos
provenientes de lenguajes dinamicos podrian tambien disfrutarlo. Gente diferente aprende
diferente! Escoje lo que funcione mejor para ti.
¡Hola, Cargo! 19
El Lenguaje de Programacion Rust
% Aprende Rust
¡Bienvenido! Esta sección tiene unos pocos tutoriales que te enseñarán Rust a través de la
construcción de proyectos. Obtendrás una visión general, pero también le echaremos un
vistazo a los detalles.
Si prefieres una experiencia más al estilo ‘de abajo hacia arriba’, echa un vistazo a Sintaxis
y Semántica.
Aprende Rust 20
El Lenguaje de Programacion Rust
% Juego de Adivinanzas
Configuración Inicial
Creemos un nuevo proyecto. Anda a tu directorio de proyectos. Recuerdas como creamos
nuestra estructura de directorios y un Cargo.toml para hola_mundo ? Cargo posse un
comando que hace eso por nosotros. Probemoslo:
$ cd ~/proyectos
$ cargo new adivinanzas --bin
$ cd adivinanzas
Pasamos el nombre de nuestro proyecto a cargo new , junto con el flag --bin , debido a
que estamos creando un binario, en vez de una biblioteca.
[package]
name = "adivinanzas"
version = "0.1.0"
authors = ["Tu Nombre <[email protected]>"]
fn main() {
println!("Hello, world!");
}
$ cargo build
Compiling adivinanzas v0.1.0 (file:///home/tu/proyectos/adivinanzas)
Excelente! Abre tu src/main.rs otra vez. Estaremos escribiendo todo nuestro codigo en
este archivo.
Antes de continuar, dejame mostrarte un comando mas de Cargo: run . cargo run es una
especie de cargo build , pero con la diferencia de que tambien ejecuta el binario
producido. Probemoslo:
$ cargo run
Compiling adivinanzas v0.1.0 (file:///home/tu/proyectos/adivinanzas)
Running `target/debug/adivinanzas`
Hola, mundo!
Grandioso! El comando run es muy útil cuando necesitas iterar rapido en un proyecto.
Nuestro juego es uno de esos proyectos, necesitaremos probar rapidamente cada iteración
antes de movernos a la siguiente.
use std::io;
fn main() {
println!("Adivina el numero!");
io::stdin().read_line(&mut corazonada)
.ok()
.expect("Fallo al leer linea");
use std::io;
Necesitaremos recibir entrada del usuario, para luego imprimir el resultado como salida.
Debido a esto necesitamos la biblioteca io de la biblioteca estandar. Rust solo importa
unas pocas cosas para todos los programas, este conjunto de cosas se denomina ‘preludio’.
Si no esta en el preludio tendrás que llamarlo directamente a traves de use .
fn main() {
Como has visto con anterioridad, la función main() es el punto de entrada a tu programa.
La sintaxis fn declara una nueva función, los () s indican que no hay argumentos y {
comienza el cuerpo de la función. Debido a que no incluimos un tipo de retorno, se asume
ser () una tupla vacía.
println!("Adivina el numero!");
Anteriormente aprendimos que println!() es una macro que imprime una cadena de
caracteres a la pantalla.
Ahora nos estamos poniendo interesantes! Hay un montón de cosas pasando en esta
pequeña linea. La primera cosas a notar es que es una sentencia let, usada para crear
variables. Tiene la forma:
Esto creara una nueva variable llamada foo , y la enlazara al valor bar . En muchos
lenguajes, esto es llamado una ‘variable’ pero las variables de Rust tienen un par de trucos
bajo la manga.
Por ejemplo, son immutables por defecto. Es por ello que nuestro ejemplo usa mut : esto
hace un binding mutable, en vez de inmutable. let no solo toma un nombre del lado
izquierdo, let acepta un ‘patrón’. Usaremos los patrones un poco mas tarde. Es suficiente
por ahora usar:
Ah, // inicia un comentario, hasta el final de la linea. Rust ignora todo en comentarios
Entonces sabemos que let mut corazonada introducirá un binding mutable llamado
corazonada , pero tenemos que ver al otro lado del = para saber a que esta siendo
asociado: String::new() .
Esta funcion es llamada new() , porque crea un nuevo String vacio. Encontraras una
función new() en muchos tipos, debido a que es un nombre común para la creación de un
nuevo valor de algun tipo.
Continuemos:
io::stdin().read_line(&mut corazonada)
.ok()
.expect("Fallo lectura de linea");
Otro monton! Vallamos pieza por pieza. La primera linea tiene dos partes. He aqui la
primera:
io::stdin()
La siguiente parte usara dicho handle para obtener entrada del usuario:
.read_line(&mut corazonada)
Aqui, llamamos el metodo read_line() en nuestro handle. Los metodos son similares a las
funciones asociadas, pero solo estan disponibles en una instancia en particular de un tipo,
en vez de en el tipo en si. También estamos pasando un argumento a read_line() : &mut
corazonada .
Recuerdas cuando creamos corazonada ? Dijimos que era mutable. Sin embargo
read_line no acepta un String como argumento: acepta un &mut String . Rust posee
Porque read_line() acepta una referencia mutable a una cadena de caracteres. Su trabajo
es tomar lo que el usuario ingresa en la entrada estandar, y colocarlo en una cadena de
caracteres. Debido a ello toma dicha cadena como argumento, y debido a que debe de
agregar la entrada del usuario, este necesita ser mutable.
Todavia no hemos terminado con esta linea. Si bien es una sola linea de texto, es solo la
primera parte de una linea logica de código completa:
.ok()
.expect("Fallo lectura de linea");
Cuando llamas a un metodo con la sintaxis .foo() puedes introducir un salto de linea y
otro espacio. Esto te ayuda a dividir lineas largas. Pudimos haber escrito:
Pero eso es mas difícil de leer. Así que lo hemos dividido en tres lineas para tres llamadas a
metodo. Ya hemos hablado de read_line() , pero que acerca de ok() y expect() ?
Bueno, ya mencionamos que read_line() coloca la entrada del usuario en el &mut String
que le proprocionamos. Pero tambien retorna un valor: en este caso un io::Result . Rust
posee un numero de tipos llamados Result en su biblioteca estandar: un Result
generico, y versiones especificas para sub-bibliotecas, como io::Result .
El proposito de esos Result es codificar información de manejo de errores. Valores del tipo
Result tienen metodos definidos en ellos. En este caso io::Result posee un metodo
ok() , que se traduce en ‘queremos asumir que este valor es un valor exitoso. Sino,
descarta la información acerca del error’. Porque descartar la información acerca del error?,
para un programa básico, queremos simplemente imprimir un error generico, cualquier
problema que signifique que no podamos continuar. El metodo ok() retorna un valor que
tiene otro metodo definito en el: expect() . El metodo expect() toma el valor en el cual es
llamado y si no es un valor exitoso, hace panico panic! con un mensaje que le hemos
proporcionado. Un panic! como este causara que el programa tenga una salida abrupta
(crash), mostrando dicho mensaje.
Si quitamos las llamadas a esos dos metodos, nuestro programa compilara, pero
obtendremos una advertencia:
$ cargo build
Compiling adivinanzas v0.1.0 (file:///home/tu/proyectos/adivinanzas)
src/main.rs:10:5: 10:44 warning: unused result which must be used, #[warn(unused_must_use)] on by def
src/main.rs:10 io::stdin().read_line(&mut corazonada);
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Rust nos advierte que no hemos usado el valor Result . Esta advertencia viene de una
anotación especial que tiene io::Result . Rust esta tratando de decirte que no has
manejado un posible error. La manera correcta de suprimir el error es, en efecto escribir el
código para el manejo de erroes. Por suerte, si solo queremos terminar la ejecución del
programa de haber un problema, podemos usar estos dos pequeños metodos. Si
pudieramos recuperarnos del error de alguna manera, hariamos algo diferente, pero
dejemos eso para un proyecto futuro.
Esta linea imprime la cadena de caracteres en la que guardamos nuestra entrada. Los {} s
son marcadores de posición, es por ello que pasamos adivinanza como argumento. De
haber habido multiples {} s, debiamos haber pasado multiples argumentos:
let x = 5;
let y = 10;
Fácil.
$ cargo run
Compiling guessing_game v0.1.0 (file:///home/tu/proyectos/adivinanzas)
Running `target/debug/adivinanzas`
Adivina el numero!
Por favor introduce tu corazonada.
6
Tu corazonada fue: 6
En hora buena! Nuestra primera parte ha terminado: podemos obtener entrada del teclado e
imprimirla de vuelta.
Usar crates externos es donde Cargo realmente brilla. Antes que podamos escribir código
que haga uso de rand , debemos modificar nuestro archivo Cargo.toml . Abrelo, y agrega
estas lineas al final:
[dependencies]
rand="0.3.0"
$ cargo build
Updating registry `https://github.com/rust-lang/crates.io-index`
Downloading rand v0.3.8
Downloading libc v0.1.6
Compiling libc v0.1.6
Compiling rand v0.3.8
Compiling adivinanzas v0.1.0 (file:///home/tu/proyectos/adivinanzas)
Un montón de salida mas! Ahora que tenemos una dependencia externa, Cargo descarga
del registro las ultimas versiones de todo, lo cual puede copiar datos desde Crates.io.
Crates.io es donde las personas del ecosistema Rust publican sus proyectos open source
para que otros los usen.
queriamos depender en rand , y tambien obtuvimos una copia de libc . Esto es debido a
que rand depende a su vez de libc para funcionar. Despues de descargar las
dependencias, Cargo las compila, para despues compilar nuestro código.
$ cargo build
Asi es, no hay salida! Cargo sabe que nuestro proyecto ha sido construido, asi como todas
sus dependencias, asi que no nay razón para hacer todo el proceso otra vez. Sin nada que
hacer, simplemente termina la ejecucion. Si abrimos src/main.rs otra vez, hacemos un
cambio trivial, salvamos los cambios, solamente veriamos una linea:
$ cargo build
Compiling adivinanzas v0.1.0 (file:///home/tu/proyectos/adivinanzas)
Entonces, le hemos dicho a Cargo que queriamos cualquier versión 0.3.x de rand , y
este descargo la ultima versión para el momento de la escritura de este tutorial, v0.3.8 .
Pero que pasa cuando la siguiente versión v0.3.9 sea publicada con un importante
bugfix? Si bien recibir bugfixes es importante, que pasa si 0.3.9 contiene una regresion
que rompe nuestro codigo?
existe, y usara las versiones especificadas en el mismo, en vez de hacer todo el trabajo de
determinar las versiones otra vez. Esto te permite tener una construcción reproducible de
manera automatica. En otras palabras, nos quedaremos en 0.3.8 hasta que subamos de
version de manera explicita, de igual manera lo hará la gente con la que hemos compartido
nuestro código, gracias al archivo Cargo.lock .
Pero que pasa cuando queremos usar v0.3.9 ? Cargo posee otro comando, update , que
se traduce en ‘ignora el bloqueo y determina todas las ultimas versiones que coincidan con
lo que hemos especficado. De funcionar esto, escribe esas versiones al archivo de bloqueo
Cargo.lock ’. Pero, por defecto, Cargo solo buscara versiones mayores a 0.3.0 y menores
Hay mucho mas que decir acerca de Cargo y su ecosistema, pero por ahora, eso es todo lo
que necesitamos saber. Cargo hace realmente fácil reusar bibliotecas, y los Rusteros
tienden a escribir proyectos pequenos los cuales estan construidos por un conjunto de
paquetes mas pequeños.
use std::io;
use rand::Rng;
fn main() {
println!("Adivina el numero!");
io::stdin().read_line(&mut corazonada)
.ok()
.expect("Fallo al leer linea");
La primera cosa que hemos hecho es cambiar la primera linea. Ahora dice extern crate
rand . Debido a que declaramos rand en nuestra sección [dependencies] , podemos usar
extern crate para hacerle saber a Rust que estaremos haciendo uso de rand . Esto es
equivalente a un use rand; , de manera que podamos hacer uso de lo que sea dentro del
crate rand a traves del prefijo rand:: .
Después, hemos agregado otra linea use : use rand::Rng . En unos momentos estaremos
haciendo uso de un metodo, y esto requiere que Rng este disponible para que funcione. La
idea basica es la siguiente: los metodos estan dentro de algo llamado ‘traits’ (Rasgos), y
para que el metodo funcione necesita que el trait este disponible. Para mayores detalles
dirigete a la sección Rasgos (Traits).
Hacemos uso de la función rand::thread_rng() para obtener una copia del generador de
numeros aleatorios, el cual es local al hilo de ejecucion en el cual estamos. Debido a que
hemos hecho disponible a rand::Rng a traves de use rand::Rng , este tiene un metodo
gen_range() disponible. Este metodo acepta dos argumentos, y genera un numero
aleatorio entre estos. Es inclusivo en el limite inferior, pero es exclusivo en el limite superior,
por eso necesitamos 1 y 101 para obtener un numero entre uno y cien.
La segunda linea solo imprime el numero secreto. Esto es útil mietras desarrollamos
nuestro programa, de manera tal que podamos probarlo. Estaremos eliminando esta linea
para la version final. No es un juego si imprime la respuesta justo cuando lo inicias!
$ cargo run
Compiling adivinanzas v0.1.0 (file:///home/tu/proyectos/adivinanzas)
Running `target/debug/adivinanzas`
Adivina el numero!
El numero secreto es: 7
Por favor introduce tu corazonada.
4
Tu corazonada fue: 4
$ cargo run
Running `target/debug/adivinanzas`
Adivina el numero!
El numero secreto es: 83
Por favor introduce tu corazonada.
5
Tu corazonada fue: 5
Comparando adivinanzas
Ahora que tenemos entrada del usuario, comparemos la adivinanza con nuestro numero
secreto. He aqui nuestro siguiente paso, aunque todavia no funciona completamente:
use std::io;
use std::cmp::Ordering;
use rand::Rng;
fn main() {
println!("Adivina el numero!");
io::stdin().read_line(&mut corazonada)
.ok()
.expect("Fallo al leer linea");
match corazonada.cmp(&numero_secreto) {
Ordering::Less => println!("Muy pequeño!"),
Ordering::Greater => println!("Muy grande!"),
Ordering::Equal => println!("Haz ganado!"),
}
}
Algunas piezas acá. La primera es otro use . Hemos hecho disponible un tipo llamado
std::cmp::Ordering . Despues, cinco nuevas lineas al fondo que lo usan:
match corazonada.cmp(&numero_secreto) {
Ordering::Less => println!("Muy pequeño!"),
Ordering::Greater => println!("Muy grande!"),
Ordering::Equal => println!("Haz ganado!"),
}
El metodo cmp() puede ser llamado an cualquier cosa que pueda ser comparada, este
toma una referencia a la cosa con la cual quieras comparar. Retorna el tipo Ordering que
hicimos disponible anteriormente. Hemos usado una sentencia match para determinar
exactamente que tipo de Ordering es. Ordering es un enum , abreviacion para
‘enumeration’, las cuales lucen de la siguiente manera:
enum Foo {
Bar,
Baz,
}
Con esta definición, cualquier cosa de tipo Foo puede ser bien sea un Foo::Bar o un
Foo::Baz . Usamos el :: para indicar el espacio de nombres para una variante enum en
particular.
La enum Ordering tiene tres posibles variantes: Less , Equal , and Greater (menor, igual
y mayor respectivamente). La sentencia match toma un valor de un tipo, y te permite crear
un ‘brazo’ para cada valor posible. Debido a que tenemos tres posibles tipos de Ordering ,
tenemos tres brazos:
match guess.cmp(&secret_number) {
Ordering::Less => println!("Muy pequeño!"),
Ordering::Greater => println!("Muy grande!"),
Ordering::Equal => println!("Haz ganado!"),
}
$ cargo build
Compiling adivinanzas v0.1.0 (file:///home/tu/proyectos/adivinanzas)
src/main.rs:28:21: 28:35 error: mismatched types:
expected `&collections::string::String`,
found `&_`
(expected struct `collections::string::String`,
found integral variable) [E0308]
src/main.rs:28 match corazonada.cmp(&numero_secreto) {
^~~~~~~~~~~~~~
error: aborting due to previous error
Could not compile `adivinanzas`.
debido a que Rust por defecto usa i32 . Sin embargo, en este caso, Rust no sabe como
comparar corazonada con numero_secreto . Ambos necesitan ser del mismo tipo. A la final,
queremos convertir el String que leimos como entrada en un tipo real de numero, para
efectos de la comparación. Podemos hacer eso con tres lineas mas. He aqui nuestro nuevo
programa:
use std::io;
use std::cmp::Ordering;
use rand::Rng;
fn main() {
println!("Adivina el numero!");
io::stdin().read_line(&mut corazonada)
.ok()
.expect("Fallo al leer linea");
match corazonada.cmp(&numero_secreto) {
Ordering::Less => println!("Muy pequeño!"),
Ordering::Greater => println!("Muy grande!"),
Ordering::Equal => println!("Haz ganado!"),
}
}
Espera un momento, pensé que ya teniamos una corazonada ? La tenemos, pero Rust nos
permite sobreescribir (‘shadow’) la corazonada previa con una nueva. Esto es usado con
frecuencia en esta misma situación, en donde corazonada es un String , pero queremos
convertirlo a un u32 . Este shadowing nos permite reusar el nombre corazonada en vez de
forzarnos a idear dos nombres únicos como corazonada_str y corazonada , u otros.
Estamos asociando corazonada a una expresión que luce como algo que escribimos
anteriormente:
guess.trim().parse()
Seguido por una invocación a ok().expect() . Aquí corazonada hace referencia a la vieja
versión, la que era un String que contenía nuestra entrada de usuario en ella. El metodo
trim() en los String s elimina cualquier espacio en blanco al principio y al final de
nuestras cadenas de caracteres. Esto es importante, debido a que tuvimos que presionar la
tecla ‘retorno’ para satisfacer a read_line() . Esto significa que si escribimos 5 y
presionamos ‘retorno’ corazonada luce como así: 5\n . El \n representa ‘nueva linea’
(‘newline’), la tecla enter. trim() se deshace de esto, dejando nuestra cadena de
caracteres solo con el 5 . El metodo parse() en las cadenas caracteres parsea una
cadena de caracteres en algún tipo de numero. Debido a que puede parsear una variedad
de numeros, debemos darle a Rust una pista del tipo exacto de numero que deseamos. De
ahí la parte let corazonada: u32 . Los dos puntos ( : ) despues de corazonada le dicen a
Rust que vamos a anotar el tipo. u32 es un entero sin signo de treinta y dos bits. Rust
posee una variedad de tipos numero integrados, pero nosotros hemos escojido u32 . Es
una buena opción por defecto para un numero positivo pequeño.
Al igual que read_line() , nuestra llamada a parse() podria causar un error. Que tal si
nuestra cadena de caracteres contiene A ���? No habría forma de convertir eso en un
numero. Es por ello que haremos lo mismo que hicimos con read_line() : usar los metodos
ok() y expect() para terminar abruptamente si hay algun error.
$ cargo run
Compiling adivinanzas v0.1.0 (file:///home/tu/proyectos/adivinanzas)
Running `target/adivinanzas`
Adivina el numero!
El numero secreto es: 58
Por favor introduce tu corazonada.
76
Tu corazonada fue: 76
Muy grande!
Excelente! Puedes ver que incluso he agregado espacios antes de mi intento, y aun así el
programa determino que intente 76. Ejecuta el programa unas pocas veces, y verifica que
adivinar el numero funciona, asi como intentar un numero muy pequeno.
Ahora tenemos la mayoria del juego funcionando, pero solo podemos intentar adivinar una
vez. Tratemos de cambiar eso agregando ciclos!
Iteración
La palabra clave loop nos proporciona un ciclo infinito. Agreguemosla:
use std::io;
use std::cmp::Ordering;
use rand::Rng;
fn main() {
println!("Adivina el numero!");
loop {
println!("Por favor introduce tu corazonada.");
io::stdin().read_line(&mut corazonada)
.ok()
.expect("Fallo al leer linea");
match corazonada.cmp(&numero_secreto) {
Ordering::Less => println!("Muy pequeño!"),
Ordering::Greater => println!("Muy grande!"),
Ordering::Equal => println!("Haz ganado!"),
}
}
}
Pruebalo. Pero espera, no acabamos de agregar un ciclo infinito? Sip. Recuerdas nuestra
discusión acerca de parse() ? Si damos una respuesta no numérica, retornaremos
( return ) y finalizaremos la ejecución. Observa:
$ cargo run
Compiling adivinanzas v0.1.0 (file:///home/tu/proyectos/adivinanzas)
Running `target/adivinanzas`
Adivina el numero!
El numero secreto es: 59
Por favor introduce tu corazonada.
45
Tu corazonada fue: 45
Muy pequeño!
Por favor introduce tu corazonada.
60
Tu corazonada fue: 60
Muy grande!
Por favor introduce tu corazonada.
59
Tu corazonada fue: 59
Haz ganado!
Por favor introduce tu corazonada.
quit
thread '<main>' panicked at 'Please type a number!'
Ja! quit en efecto termina la ejecución. Asi como cualquier otra entrada que no sea un
numero. Bueno, esto es suboptimo por decir lo menos. Primero salgamos cuando ganemos:
use std::io;
use std::cmp::Ordering;
use rand::Rng;
fn main() {
println!("Adivina el numero!");
loop {
println!("Por favor introduce tu corazonada.");
io::stdin().read_line(&mut corazonada)
.ok()
.expect("Fallo al leer linea");
match corazonada.cmp(&numero_secreto) {
Ordering::Less => println!("Muy pequeño!"),
Ordering::Greater => println!("Muy grande!"),
Ordering::Equal => {
println!("Haz ganado!");
break;
}
}
}
}
Al agregar la linea break despues del "Haz ganado!", romperemos el ciclo cuando
ganemos. Salir del ciclo también significa salir del programa, debido a que es la ultima cosa
en main() . Solo nos queda una sola mejora por hacer: cuando alguien introduzca un valor
no numérico, no queremos terminar la ejecución, queremos simplemente ignorarlo.
Podemos hacerlo de la siguiente manera:
use std::io;
use std::cmp::Ordering;
use rand::Rng;
fn main() {
println!("Adivina el numero!");
loop {
println!("Por favor introduce tu corazonada.");
io::stdin().read_line(&mut corazonada)
.ok()
.expect("Fallo al leer linea");
match corazonada.cmp(&numero_secreto) {
Ordering::Less => println!("Muy pequeño!"),
Ordering::Greater => println!("Muy grande!"),
Ordering::Equal => {
println!("Haz ganado!");
break;
}
}
}
}
$ cargo run
Compiling adivinanzas v0.1.0 (file:///home/tu/proyectos/adivinanzas)
Running `target/adivinanzas`
Adivina el numero!
El numero secreto es: 61
Por favor introduce tu corazonada.
10
Tu corazonada fue: 10
Muy pequeño!
Por favor introduce tu corazonada.
99
Tu corazonada fue: 99
Muy pequeño!
Por favor introduce tu corazonada.
foo
Por favor introduce tu corazonada.
61
Tu corazonada fue: 61
Haz ganado!
Genial! Con una ultima mejora, finalizamos el juego de las advinanzas. Te imaginas cual
es? Es correcto, no queremos imprimir el numero secreto. Era bueno para las pruebas, pero
arruina nuestro juego. He aqui nuestro código fuente final:
use std::io;
use std::cmp::Ordering;
use rand::Rng;
fn main() {
println!("Adivina el numero!");
loop {
println!("Por favor introduce tu corazonada.");
io::stdin().read_line(&mut corazonada)
.ok()
.expect("Fallo al leer linea");
match corazonada.cmp(&numero_secreto) {
Ordering::Less => println!("Muy pequeño!"),
Ordering::Greater => println!("Muy grande!"),
Ordering::Equal => {
println!("Haz ganado!");
break;
}
}
}
}
Completado!
En este punto, has terminado satisfactoriamente el juego de las adivinanza! Felicitaciones!
Este primer proyecto te enseno un montón: let , match , metodos, funciones asociadas,
usar crates externos, y mas. Nuestro siguiente proyecto demostrara aun mas.
struct Filosofo {
nombre: String,
}
impl Filosofo {
fn new(nombre: &str) -> Filosofo {
Filosofo {
nombre: nombre.to_string(),
}
}
}
fn main() {
let f1 = Filosofo::new("Judith Butler");
let f2 = Filosofo::new("Gilles Deleuze");
let f3 = Filosofo::new("Karl Marx");
let f4 = Filosofo::new("Emma Goldman");
let f5 = Filosofo::new("Michel Foucault");
}
Acá, creamos una estructura (struct) para representar un filósofo. Por ahora el nombre es
todo lo que necesitamos. Elegimos el tipo String para el nombre, en vez de &str .
Generalmente hablando, trabajar con tipo que es dueño (posee pertenencia) de su data es
mas fácil que trabajar con uno que use referencias.
Continuemos:
# struct Filosofo {
# nombre: String,
# }
impl Filosofo {
fn new(nombre: &str) -> Filosofo {
Filosofo {
nombre: nombre.to_string(),
}
}
}
Este bloque impl nos permite definir cosas en estructuras Filosofo . En este caso
estamos definiendo una ‘función asociada’ llamada new . La primera linea luce así:
# struct Filosofo {
# nombre: String,
# }
# impl Filosofo {
fn new(nombre: &str) -> Filosofo {
# Filosofo {
# nombre: nombre.to_string(),
# }
# }
# }
# struct Filosofo {
# nombre: String,
# }
# impl Filosofo {
# fn new(nombre: &str) -> Filosofo {
Filosofo {
nombre: nombre.to_string(),
}
# }
# }
cual crea una copia de la cadena a la que apunta nuestro &str , y nos da un nuevo
String , que es del tipo del campo nombre de Filosofo .
copia. Para este pequeño programa, esto no es particularmente importante, y que sabemos
que estaremos usando cadenas cortas de cualquier modo.
Una ultima cosas que habrás notado: solo definimos un Filosofo , y no parecemos hacer
nada con el. Rust es un lenguaje ‘basado en expresiones’, lo que significa que casi
cualquier cosa en Rust es una expresión que retorna un valor. Esto es cierto para las
funciones también, la ultima expresión es retornada automáticamente. Debido a que
creamos un nuevo Filosofo como la ultima expresión de esta función, terminamos
retornándolo.
El nombre new() , no es nada especial para Rust, pero es una convención para funciones
que crean nuevas instancias de estructuras. Antes que hablemos del porque, echamos un
vistazo a main() otra vez:
# struct Filosofo {
# nombre: String,
# }
#
# impl Filosofo {
# fn new(nombre: &str) -> Filosofo {
# Filosofo {
# nombre: nombre.to_string(),
# }
# }
# }
#
fn main() {
let f1 = Filosofo::new("Judith Butler");
let f2 = Filosofo::new("Gilles Deleuze");
let f3 = Filosofo::new("Karl Marx");
let f4 = Filosofo::new("Emma Goldman");
let f5 = Filosofo::new("Michel Foucault");
}
Acá, creamos cinco variables con cinco nuevos filósofos. Estos son mis cinco favoritos, pero
puedes substituirlos con quienes prefieras. De no haber definido la función new() , main()
luciría así:
# struct Filosofo {
# nombre: String,
# }
fn main() {
let f1 = Filosofo { nombre: "Judith Butler".to_string() };
let f2 = Filosofo { nombre: "Gilles Deleuze".to_string() };
let f3 = Filosofo { nombre: "Karl Marx".to_string() };
let f4 = Filosofo { nombre: "Emma Goldman".to_string() };
let f5 = Filosofo { nombre: "Michel Foucault".to_string() };
}
Un poco mas ruidoso. Usar new tiene también posee otras ventajas, pero incluso en este
simple caso termina por ser de mejor utilidad.
Ahora que tenemos lo básico en su lugar, hay un numero de maneras en las cuales
podemos atacar el problema mas amplio. A mi me gusta comenzar por el final: creemos una
forma para que cada filosofo pueda finalizar de comer. Como un paso pequeño, hagamos
un método, y luego iteremos a través de todos los filósofos llamándolo:
struct Filosofo {
nombre: String,
}
impl Filosofo {
fn new(nombre: &str) -> Filosofo {
Filosofo {
nombre: nombre.to_string(),
}
}
fn comer(&self) {
println!("{} ha finalizado de comer.", self.nombre);
}
}
fn main() {
let filosofos = vec![
Filosofo::new("Judith Butler"),
Filosofo::new("Gilles Deleuze"),
Filosofo::new("Karl Marx"),
Filosofo::new("Emma Goldman"),
Filosofo::new("Michel Foucault"),
];
for f in &filosofos {
f.comer();
}
}
Primero veamos a main() . En lugar de tener cinco variables individuales para nuestros
filósofos, creamos un Vec<T> . Vec<T> es llamado también un ‘vector’, y es un arreglo
capaz de crecer. Después usamos un ciclo [ for ][for] para iterar a través del vector,
obteniendo un referencia a cada filosofo a la vez.
fn comer(&self) {
println!("{} ha finalizado de comer.", self.nombre);
}
En Rust, los métodos reciben un parámetro explícito self . Es por ello que comer() es un
método y new es una función asociada: new() no tiene self . Para nuestra primera
version de comer() , solo imprimimos el nombre del filósofo, y mencionamos que ha
finalizado de comer. Ejecutar este programa deber generar la siguiente salida:
Muy fácil, todos han terminado de comer! Pero no hemos implementado el problema real
todavía, así que aun no terminamos!
A continuación, no solo queremos solo finalicen de comer, sino que efectivamente coman.
He aquí la siguiente versión:
use std::thread;
struct Filosofo {
nombre: String,
}
impl Filosofo {
fn new(nombre: &str) -> Filosofo {
Filosofo {
nombre: nombre.to_string(),
}
}
fn comer(&self) {
println!("{} esta comiendo.", self.nombre);
thread::sleep_ms(1000);
fn main() {
let filosofos = vec![
Filosofo::new("Judith Butler"),
Filosofo::new("Gilles Deleuze"),
Filosofo::new("Karl Marx"),
Filosofo::new("Emma Goldman"),
Filosofo::new("Michel Foucault"),
];
for f in &filosofos {
f.comer();
}
}
use std::thread;
modulo thread de la biblioteca estándar, y es por ello que necesitamos hacer use en el.
fn comer(&self) {
println!("{} esta comiendo.", self.nombre);
thread::sleep_ms(1000);
use std::thread;
struct Filosofo {
nombre: String,
}
impl Filosofo {
fn new(nombre: &str) -> Filosofo {
Filosofo {
nombre: nombre.to_string(),
}
}
fn comer(&self) {
println!("{} esta comiendo.", self.nombre);
thread::sleep_ms(1000);
fn main() {
let filosofos = vec![
Filosofo::new("Judith Butler"),
Filosofo::new("Gilles Deleuze"),
Filosofo::new("Karl Marx"),
Filosofo::new("Emma Goldman"),
Filosofo::new("Michel Foucault"),
];
for h in handles {
h.join().unwrap();
}
}
Todo lo que hemos hecho es cambiar el ciclo en main() , y agregado un segundo! Este es
el primer cambio:
Aun así son solo cinco lineas, son cinco densas lineas. Analicemos por partes.
Introducimos una nueva variable, llamada handles . Le hemos dado este nombre porque
crearemos algunos nuevos hilos, que resultaran en algunos handles (agarradores, manillas)
a esos dichos hilos los cuales nos permitirán controlar su operación. Necesitamos anotar el
tipo explícitamente, debido a algo que haremos referencia mas adelante. El _ es un
marcador de posición para un tipo. Estamos diciendo “ handles es un vector de algo, pero
tu, Rust, puedes determinar que es ese algo.”
filosofos.into_iter().map(|f| {
Tomamos nuestra lista de filósofos y llamamos into_iter() en ella. Esto crea un iterador
que se adueña (toma pertenencia) de cada filosofo. Necesitamos hacer esto para poder
pasar los filósofos a nuestros hilos. Luego tomamos ese iterador y llamamos map en el,
método que toma un closure como argumento y llama dicho closure en cada uno de los
elementos a la vez.
thread::spawn(move || {
f.comer();
})
}).collect();
tipo de retorno: queremos un Vec<T> . Los elementos son los valores retornados de las
llamadas a thread::spawn , que son handles a esos hilos. Whew!
for h in handles {
h.join().unwrap();
}
Al final de main() , iteramos a través de los handles llamando join() en ellos, lo cual
bloquea la ejecución hasta que el hilo haya completado su ejecución. Esto asegura que el
hilo complete su ejecución antes que el programa termine.
Si ejecutas este programa, veras que los filósofos comen sin orden! Tenemos multi-hilos!
Pero que acerca de los tenedores no los hemos modelado del todo todavía.
use std::sync::Mutex;
struct Mesa {
tenedores: Vec<Mutex<()>>,
}
Esta Mesa contiene un vector de Mutex es. Un mutex es una forma de controlar
concurrencia, solo un hilo puede acceder el contenido a la vez. Esta es la exactamente la
propiedad que necesitamos para nuestros tenedores. Usamos una dupla vacía, () , dentro
del mutex, debido a que no vamos a usar el valor, solo nos aferraremos a el.
use std::thread;
use std::sync::{Mutex, Arc};
struct Filosofo {
nombre: String,
izquierda: usize,
derecha: usize,
}
impl Filosofo {
fn new(nombre: &str, izquierda: usize, derecha: usize) -> Filosofo {
Filosofo {
nombre: nombre.to_string(),
izquierda: izquierda,
derecha: derecha,
}
}
thread::sleep_ms(1000);
struct Mesa {
tenedores: Vec<Mutex<()>>,
}
fn main() {
let mesa = Arc::new(Mesa { tenedores: vec![
Mutex::new(()),
Mutex::new(()),
Mutex::new(()),
Mutex::new(()),
Mutex::new(()),
]});
thread::spawn(move || {
f.comer(&mesa);
})
}).collect();
for h in handles {
h.join().unwrap();
}
}
Muchos cambios! Sin embargo, con esta iteración, hemos obtenido un programa funcional.
Veamos los detalles:
struct Filosofo {
nombre: String,
izquierda: usize,
derecha: usize,
}
Vamos a necesitar agregar dos campos mas a nuestra estructura Filosofo . Cada filosofo
tendrá dos tenedores: el de la izquierda, y el de la derecha. Usaremos el tipo usize para
indicarlos, debido a que este es el tipo con el cual se indexan los vectores. Estos dos
valores serán indices en los tenedores que nuestra Mesa posee.
Ahora necesitamos construir esos valores izquierda y derecha , de manera que podamos
agregarlos a new() .
thread::sleep_ms(1000);
Tenemos dos nuevas lineas, también hemos agregado un argumento, mesa . Accedemos a
la lista de tenedores de la Mesa , y después usamos self.izquierda y self.derecha para
acceder al tenedor en un indice en particular. Eso nos da acceso al Mutex en ese indice,
en donde llamamos lock() . Si el mutex esta siendo accedido actualmente por alguien
mas, nos bloquearemos hasta que este disponible.
Otra cosa extraña acerca de esta lineas: hemos nombrado los resultados _izquierda and
_derecha . Que hay con ese sub-guion? Bueno, en realidad no planeamos usar el valor
dentro del bloqueo. Solo queremos adquirirlo. A consecuencia, Rust nos advertirá que
nunca usamos el valor. A través del uso del sub-guion le decimos a Rust que es lo que
quisimos, de esa manera no generara la advertencia.
Que acerca de soltar el bloqueo?, Bueno, esto ocurrirá cuando _izquierda y _derecha
salgan de ámbito, automáticamente.
thread::spawn(move || {
f.comer(&mesa);
})
}).collect();
ámbito, la decrementa. Notaras que podemos introducir una nueva variable mesa , y esta
sobre escribirá (shadow) la anterior. Esto es frecuentemente usado de manera tal de no
tener que inventar dos nombres únicos.
Con todo esto, nuestro programa funciona! Solo dos filosofo pueden comer en un momento
dado y en consecuencia tendrás salida se vera así:
Para nuestro tercer proyecto, elegiremos algo que demuestra una de las mayores fortalezas
de Rust: la ausencia de un entorno de ejecución.
A medida que las organizaciones crecen, estas de manera progresiva, hacen uso de una
multitud de lenguajes de programación. Diferentes lenguajes de programación poseen
diferentes fortalezas y debilidades, y una arquitectura poliglota permite usar un lenguaje en
particular donde sus fortalezas hagan sentido y otro lenguaje sea débil.
Rust posee soporte para FFI en ambas direcciones: puede llamar código en C de manera
fácil, pero crucialmente puede ser llamado tan fácilmente como C. Combinado con la
ausencia de un recolector de basura y bajos requerimientos en tiempo de ejecución, Rust
es un candidato para ser embebido dentro de otros lenguajes cuando necesites esos
ooKmh extra.
Existe un capitulo completo dedicado a FFI y sus detalles en otra parte del libro, pero en
este capitulo, examinaremos el uso particular de FFI con ejemplos en Ruby, Python, y
JavaScript.
El problema
Existen muchos problemas que podríamos haber escogido, pero elegiremos un ejemplo en
el cual Rust tiene una ventaja clara por encima de otros lenguajes: computación numérica e
hilos.
Segundo, muchos lenguajes poseen un ‘bloqueo global del interprete’ (‘global interpreter
lock’) (GIL), que limita la concurrencia en muchas situaciones. Esto es hecho en el nombre
de la seguridad lo cual es un efecto positivo, pero limita la cantidad de trabajo que puede
ser llevado a cabo de manera concurrente, lo cual es un gran negativo.
Para enfatizar estos 2 aspectos, crearemos un pequeño proyecto que usa estos dos
aspectos en gran medida. Debido a que el foco del ejemplo es embeber Rust en otros
lenguajes, en vez de el problema en si mismo, usaremos un ejemplo de juguete:
Inicia diez hilos. Dentro de cada hilo, cuenta desde uno hasta cinco millones. Después
que todos los hilos hayan finalizado, imprime "completado!".
threads = []
10.times do
threads << Thread.new do
count = 0
5_000_000.times do
count += 1
end
end
end
Intenta ejecutar este ejemplo, y escoge un numero que corra por unos segundos.
Dependiendo en el hardware de tu computador, tendrás que incrementar o decrementar el
numero.
En mi sistema, ejecutar este programa toma 2.156 . Si uso alguna tipo de herramienta de
monitoreo de procesos, como top , puedo ver que solo usa un núcleo en mi maquina. El
GIL presente haciendo su trabajo.
Si bien es cierto que este en un programa sintético, uno podría imaginar muchos problemas
similares a este en el mundo real. Para nuestros propósitos, levantar unos pocos hilos y
ocuparlos representa una especie de computación paralela y costosa.
Escribamos este problema en Rust. Primero, creemos un proyecto nuevo con Cargo:
use std::thread;
fn procesar() {
let handles: Vec<_> = (0..10).map(|_| {
thread::spawn(|| {
let mut _x = 0;
for _ in (0..5_000_000) {
_x += 1
}
})
}).collect();
for h in handles {
h.join().ok().expect("No se pudo unir un hilo!");
}
}
Algo de esto debe lucir familiar a ejemplos anteriores. Iniciamos diez hilos, colectandolos en
un vector handles . Dentro de cada hilo, iteramos cinco millones de veces, agregando uno
a _x en cada iteración. Porque el sub-guion? Bueno, si lo removemos y luego
compilamos:
$ cargo build
Compiling embeber v0.1.0 (file:///Users/goyox86/Code/rust/embeber)
src/lib.rs:3:1: 16:2 warning: function is never used: `procesar`, #[warn(dead_code)] on by default
src/lib.rs:3 fn procesar() {
src/lib.rs:4 let handles: Vec<_> = (0..10).map(|_| {
src/lib.rs:5 thread::spawn(|| {
src/lib.rs:6 let mut x = 0;
src/lib.rs:7 for _ in (0..5_000_000) {
src/lib.rs:8 x += 1
...
src/lib.rs:6:17: 6:22 warning: variable `x` is assigned to, but never used, #[warn(unused_variables)]
src/lib.rs:6 let mut x = 0;
^~~~~
Hasta el momento, sin embargo, es una biblioteca Rust, y no expone nada que pueda ser
llamado desde C. Si quisiéramos conectarla con otro lenguaje, en su estado actual, no
funcionaria. Solo necesitamos hacer unos pequeños cambios para arreglarlo. Lo primero es
modificar el principio de nuestro código:
#[no_mangle]
pub extern fn procesar() {
Debemos agregar un nuevo atributo, no_mangle . Cuando creamos una biblioteca Rust, este
cambia el nombre de la función en la salida compilada. Las razones de esto escapan del
alcance de este tutorial, pero para que otros lenguajes puedan saber como llamar a la
función, debemos evitar que el compilador cambie el nombre en la salida compilada. Este
atributo desactiva ese comportamiento.
El otro cambio es el pub extern . El pub significa que esta función puede ser llamada
desde afuera de este modulo, y el extern dice que esta puede ser llamada desde C. Eso
es todo! No muchos cambios.
[lib]
name = "embeber"
crate-type = ["dylib"]
Estas lineas le informan a Rust que queremos compilar nuestra biblioteca en una biblioteca
dinámica estándar. Rust compila un ‘rlib’, un formato especifico de Rust.
Hemos elegido cargo build --release , lo cual construye el proyecto con optimizaciones.
Queremos que sea lo mas rápido posible! Puedes encontrar la salida de la biblioteca en
target/release :
$ ls target/release/
build deps examples libembeber.dylib native
Ruby
Crea un archivo embeber.rb dentro de nuestro proyecto, y coloca esto dentro:
require 'ffi'
module Hola
extend FFI::Library
ffi_lib 'target/release/libembeber.dylib'
attach_function :procesar, [], :void
end
Hola.procesar
puts 'completado!'
$ ruby embeber.rb
completado!
$
Whoa, eso fue rápido! En mi sistema, tomo 0.086 segundos, a diferencia de los dos
segundos que la version en Ruby puro. Analicemos este código Ruby:
require 'ffi'
Primero necesitamos requerir la gema ffi . Nos permite interactuar con una biblioteca
Rust como una biblioteca en C.
module Hola
extend FFI::Library
ffi_lib 'target/release/libembeber.dylib'
El modulo Hola es usado para adjuntar las funciones nativas de la biblioteca compartida.
Dentro, extend emos el modulo FFI::Library y luego llamamos el método ffi_lib para
cargar nuestra biblioteca de objetos compartidos. Simplemente pasamos la ruta en la cual
nuestra biblioteca esta almacenada, la cual, como vimos anteriormente, es
target/release/libembeber.dylib .
Hola.procesar
código Rust!
puts 'completado!'
Eso es todo! Como hemos visto, hacer un puente entre los dos lenguajes es realmente fácil,
y nos compra mucho performance.
Python
Crea un archivo embeber.py en este directorio, y coloca esto en el:
lib = cdll.LoadLibrary("target/release/libembeber.dylib")
lib.procesar()
print("completado!")
Aun mas fácil! Usamos cdll del modulo ctypes . Una llamada rápida a LoadLibrary
después, y luego podemos llamar procesar() .
Node.js
Node no es un lenguaje, pero es actualmente la implementación de Javascript dominante
del lado del servidor.
lib.procesar();
console.log("completado!");
Luce mas parecido al ejemplo Ruby que al de Python. Usamos el modulo ffi para
obtener acceso a ffi.Library() , la cual nos permite cargar nuestra biblioteca de objetos
compartidos. Necesitamos anotar el tipo de retorno y los tipos de los argumentos de la
función, que son void para el retorno y un arreglo vacío para representar ningún
argumento. De allí simplemente llamamos a la función procesar() e imprimimos el
resultado.
Conclusion
Como puedes ver, las bases de hacer FFI son muy fáciles. Por supuesto hay mucho mas
que podríamos hacer aquí. Echa un vistazo al capitulo FFI para mas detalles.
% Rust Efectivo
Entonces, haz aprendido como escribir algo de código Rust. Pero hay una diferencia entre
escribir cualquier código Rust y escribir buen código Rust.
Rust Efectivo 65
El Lenguaje de Programacion Rust
% La Pila y el Montículo
Manejo de memoria
Estos dos términos hacen referencia a el manejo de la memoria. La pila y el montículo son
abstracciones que ayudan a determinar cuando asignar y liberar memoria.
La pila es muy rápida, y es de donde la memoria es asignada por defecto en Rust. Pero la
asignación es local a una llamada a función, y es limitada en tamaño. El montículo por otro
lado, es mas lento, y es asignado por tu programa. Pero es efectivamente de un tamaño
ilimitado, y es globalmente accesible.
La Pila
Hablemos acerca de este programa Rust:
fn main() {
let x = 42;
}
Este programa posee una variable (variable binding), x . La memoria tiene que ser
asignada desde algún sitio. Rust asigna desde la pila por defecto, lo que se traduce en que
los valores básicos ‘van a la pila’. Pero, que significa esto?
Veamos, cuando una función es llamada, algo de memoria es asignada para sus variables
locales y otra información extra. Dicha memoria es llamada ‘registro de activación’ (‘stack
frame’), para el propósito de este tutorial, ignoraremos la información extra y solo
consideraremos las variables locales a las que estamos asignando memoria. Así que en
este caso, cuando main() es ejecutada, asignamos un entero de 32 bits para nuestro
registro de activación. Todo esto es manejado automáticamente, como has podido ver, no
tuvimos que escribir ningún código Rust especial o alguna otra cosa.
La Pila y el Montículo 66
El Lenguaje de Programacion Rust
Eso es todo para este simple programa. Lo clave a entender aquí es que la asignación de
memoria desde la pila es muy, muy rápida. Debido a que conocemos por adelantado todas
las variables locales, podemos obtener toda la memoria de una sola vez. Y debido a que la
desecharemos toda completa, podemos deshacernos de ella muy rápido, también.
La desventaja es que no podemos mantener valores rondando por allí si los necesitamos
por un periodo mas largo que el tiempo de vida de una función. Tampoco hemos hablado
acerca de que significa ese nombre, ‘pila’. Para hacerlo necesitamos un ejemplo
ligeramente mas complejo:
fn foo() {
let y = 5;
let z = 100;
}
fn main() {
let x = 42;
foo();
}
Este programa tiene tres variables en total: dos en foo() , una en main() . Al igual que
antes, cuando main() es llamada, un solo entero es asignado para su registro de
activación. Pero antes que demostremos que es lo que pasa cuando foo() es llamada,
necesitamos visualizar que es lo que esta pasando en memoria. Tu sistema operativo
presenta a tu programa una visión muy simple: una lista inmensa de direcciones, desde 0
hasta un numero muy grande, que representa cuanta memoria RAM posee la maquina. Por
ejemplo si tienes un gigabyte de RAM, tus direcciones irán desde 0 hasta 1,073,741,824 ,
numero que proviene de 230, el numero de bytes en un gigabyte.
Esta memoria es una especie de arreglo gigante: las direcciones comienzan en cero y se
incrementan hasta el numero final. Entonces, he aquí un diagrama de nuestro primer
registro de activación:
0 x 42
La Pila y el Montículo 67
El Lenguaje de Programacion Rust
2 z 100
1 y 5
0 x 42
Debido a que 0 fue reservado para la primera frame, 1 y 2 son usados para el registro
de activación de foo() . La pila crece hacia arriba, a medida que llamamos a mas
funciones.
Hay algunas cosas importantes que debemos notar aquí. Los números 0, 1 y 2 existen solo
para propósitos ilustrativos, y no poseen ninguna relación con los números que una
computadora realmente usaría. En particular, la serie de direcciones están separadas por
un numero de bytes, y esa separación puede incluso exceder el tamaño del valor que esta
siendo almacenado.
Y luego después de que main() finaliza, este ultimo valor se va. Fácil!
Es llamada una pila (‘stack’) debido a que funciona como una pila de platos: el primer plato
que colocas es el ultimo plato que sacarás. Las pilas son algunas veces llamadas ‘colas
ultimo que entra, primero que sale’ (‘last in, first out queues’), por estas razones el ultimo
valor que pusiste en la pila será el primero que obtendrás de ella.
La Pila y el Montículo 68
El Lenguaje de Programacion Rust
fn bar() {
let i = 6;
}
fn foo() {
let a = 5;
let b = 100;
let c = 1;
bar();
}
fn main() {
let x = 42;
foo();
}
1 a 5
0 x 42
4 i 6
3 c 1
2 b 100
1 a 5
0 x 42
La Pila y el Montículo 69
El Lenguaje de Programacion Rust
Después que bar() termina, su registro de activación es liberado, dejando solo a foo() y
main() :
2 b 100
1 a 5
0 x 42
Hemos terminado entonces. Se entiende? Es como apilar platos: agregas al tope y sacas
de el.
El Montículo
Ahora, todo esto trabaja bien, pero no todo funciona de esa manera. Algunas veces,
necesitas pasar memoria entre diferentes funciones, o mantener memoria viva por un
tiempo mayor que la ejecución de una función. Para esto usamos el montículo.
En Rust, puedes asignar memoria desde el montículo con el tipo Box<T> (caja).
He aqui un ejemplo:
fn main() {
let x = Box::new(5);
let y = 42;
}
0 x ??????
Asignamos espacio para dos variables en la pila. y es 42 , como conocemos hasta ahora,
pero que acerca de x ? Bueno, x es un Box<i32> , y las cajas (boxes) asignan memoria
desde el montículo. El valor de la caja en cuestión es una estructura que posee un
La Pila y el Montículo 70
El Lenguaje de Programacion Rust
230 5
0 x 230
Tenemos 230 en nuestra computadora hipotética con 1GB de RAM. Y debido a que nuestra
pila crece desde cero, la forma mas fácil para asignar memoria es desde el otro extremo.
Entonces nuestro primer valor esta en el lugar mas alto en la memoria. Y el valor de la
estructura en x tiene un apuntador plano (raw pointer) a el lugar que hemos asignado en
el montículo, entonces el valor de x es 230, la dirección de la memoria que hemos
solicitado.
No hemos hablado mucho acerca de que significa en realidad asignar y liberar memoria en
estos contextos. Entrar en el profundo detalle de ello esta fuera del alcance de este tutorial,
lo importante a resaltar es que el montículo no es una simple pila que crece desde el lado
opuesto. Tendremos un ejemplo de esto mas adelante en el libro, pero debido a que el
montículo puede ser asignado y liberado en cualquier orden, puede terminar con ‘vacios’.
He aquí un diagrama de la distribución de la memoria de un programa que ha estado
corriendo por algún tiempo:
230 5
(230) - 1
(230) - 2
(230) - 3 42
3 y (230) - 3
2 y 42
1 y 42
0 x 230
La Pila y el Montículo 71
El Lenguaje de Programacion Rust
En este caso, hemos asignado cuatro cosas en el montículo, pero hemos liberado dos de
ellas. Hay un vacío entre 230 y (230) - 3 que no esta siendo usado actualmente. El detalle
especifico acerca de como y porque esto sucede depende de la estrategia usada para
manejar el montículo. Diferentes programas pueden usar diferentes ‘asignadores de
memoria’ (‘memory allocators’), que son bibliotecas encargadas de manejar la asignación
de memoria por ti. Los programas en Rust usan jemalloc para dicho propósito.
De cualquier modo, y de vuelta a nuestro ejemplo. Debido a que esta memoria esta en el
montículo, puede permanecer viva mas tiempo que la función que crea la caja (box). Sin
embargo, en este caso, esto no sucede. moving cuando la función termina, necesitamos
liberar el registro de activación de main() . Box<T> , sin embargo, tienen un truco bajo la
manga: Drop. La implementación de Drop para Box libera la memoria que ha sido
asignada cuando la caja es creada. Grandioso! Así que cuando x se va (sale de contexto),
primero libera la memoria asignada desde el montículo:
moving. Podemos hacer que la memoria permanezca viva mas tiempo transfiriendo la
fn foo(i: &i32) {
let z = 42;
}
fn main() {
let x = 5;
let y = &x;
foo(y);
}
La Pila y el Montículo 72
El Lenguaje de Programacion Rust
1 y 0
0 x 5
0 x 5
Los registros de activación no son solo para variables locales, son también para
argumentos. En este caso, necesitamos tener ambos i , nuestro argumento, y z nuestra
variable local. i es una copia del argumento, y . Debido a que el valor de y es 0
entonces ese es el valor de i .
Esta es una razón por la cual tomar prestada una variable no libera ninguna memoria: el
valor de la referencia es solo un apuntador a una dirección de memoria. Si nos
deshiciéramos de la memoria subyacente, las cosas no irían del todo bien.
Un ejemplo complejo
Bien, vayamos a través de este programa complejo paso-a-paso:
La Pila y el Montículo 73
El Lenguaje de Programacion Rust
fn foo(x: &i32) {
let y = 10;
let z = &y;
baz(z);
bar(x, z);
}
baz(e);
}
fn baz(f: &i32) {
let g = 100;
}
fn main() {
let h = 3;
let i = Box::new(20);
let j = &h;
foo(j);
}
230 20
2 j 0
1 i 230
0 h 3
La Pila y el Montículo 74
El Lenguaje de Programacion Rust
230 20
3 x 0
2 j 0
1 i 230
0 h 3
230 20
3 x 0
2 j 0
1 i 230
0 h 3
Hemos asignado memoria para f y g . baz() es muy corta, así que cuando termina, nos
deshacemos de su registro de activación:
La Pila y el Montículo 75
El Lenguaje de Programacion Rust
230 20
3 x 0
2 j 0
1 i 230
0 h 3
230 20
(230) - 1 5
9 d (230) - 1
8 c 5
7 b 4
6 a 0
5 z 4
4 y 10
3 x 0
2 j 0
1 i 230
0 h 3
Terminamos asignando otro valor en el montículo, así que tenemos que restar uno a 230. Es
mas fácil escribir eso que 1,073,741,823 . En cualquier caso, seteamos las variables como
ya es usual.
La Pila y el Montículo 76
El Lenguaje de Programacion Rust
230 20
(230) - 1 5
11 f 9
10 e 9
9 d (230) - 1
8 c 5
7 b 4
6 a 0
5 z 4
4 y 10
3 x 0
2 j 0
1 i 230
0 h 3
Con esto, estamos en nuestro punto mas profundo! Wow! Felicitaciones por haber seguido
todo esto y haber llegado tan lejos.
La Pila y el Montículo 77
El Lenguaje de Programacion Rust
230 20
(230) - 1 5
9 d (230) - 1
8 c 5
7 b 4
6 a 0
5 z 4
4 y 10
3 x 0
2 j 0
1 i 230
0 h 3
230 20
4 y 10
3 x 0
2 j 0
1 i 230
0 h 3
La Pila y el Montículo 78
El Lenguaje de Programacion Rust
230 20
1 i 230
0 h 3
Cual usar?
Si la pila es mas rápida y mas fácil de usar, porque necesitamos el montículo? Una gran
razón es que la asignación desde la pila significa que solo tienes semántica LIFO para
reclamar almacenamiento. La asignación desde el montículo es estrictamente mas general,
permitiendo que el almacenamiento pueda ser tomado y retornado a el pool en orden
arbitrario, pero con un costo en complejidad.
Generalmente, deberías preferir asignación desde la pila, es por ello que Rust asigna desde
la pila por defecto. El modelo LIFO de la pila es mas simple, a nivel fundamental. Esto tiene
dos grandes impactos: eficiencia en tiempo de ejecución e impacto semántico.
La Pila y el Montículo 79
El Lenguaje de Programacion Rust
arbitrarios, y cada bloque de memoria asignada desde el montículo pude ser de un tamaño
arbitrario, el administrador de memoria generalmente debe trabajar mucho mas duro para
identificar memoria que pueda ser reusada.
Si quisieras sumergirte mas en este tópico con mayor detalle, este paper es una muy buena
introducción.
Impacto semantico
Stack-allocation impacts the Rust language itself, and thus the developer’s mental model.
The LIFO semantics is what drives how the Rust language handles automatic memory
management. Even the deallocation of a uniquely-owned heap-allocated box can be driven
by the stack-based LIFO semantics, as discussed throughout this chapter. The flexibility (i.e.
expressiveness) of non LIFO-semantics means that in general the compiler cannot
automatically infer at compile-time where memory should be freed; it has to rely on dynamic
protocols, potentially from outside the language itself, to drive deallocation (reference
counting, as used by Rc and Arc, is one example of this).
La asignación desde la pila impacta a Rust como lenguaje, y con ello el modelo mental del
desarrollador. La semántica LIFO es lo que conduce como el lenguaje Rust maneja el
manejo automático de memoria. Incluso la liberación de una caja asignada desde el
montículo con un único dueño puede ser manejada por la semántica LIFO, tal y como se ha
discutido en este capitulo. La flexibilidad (e.j. expresividad) de la semántica no-LIFO
significa que en general el compilador no puede inferir de manera automática y en tiempo
de compilación donde la memoria debería ser liberada; tiene que apoyarse en protocolos
dinámicos, potencialmente externos a el lenguaje, para efectuar liberación de memoria
(conteo de referencias, como el usado en Rc<T> y Arc<T> , es un ejemplo).
La Pila y el Montículo 80
El Lenguaje de Programacion Rust
% Pruebas
Probar programas puede ser una forma efectiva de mostrar la presencia de bugs, pero
es desesperanzadamente inadecuada para mostrar su ausencia. Edsger W. Dijkstra,
"The Humble Programmer" (1972)
El atributo test
En esencia, una prueba en Rust es una función que esta anotada con el atributo test .
Vamos a crear un nuevo proyecto llamado sumador con Cargo:
Cargo generara automáticamente una prueba simple cuando creas un proyecto nuevo. He
aqui el contenido de src/lib.rs :
#[test]
fn it_works() {
}
Nota el #[test] . Este atributo indica que esta es una función de prueba. Actualmente no
tiene cuerpo. Pero eso es suficiente para pasar! Podemos ejecutar los tests con cargo
test :
Pruebas 81
El Lenguaje de Programacion Rust
$ cargo test
Compiling sumador v0.1.0 (file:///Users/goyox86/Code/rust/sumador)
Running target/debug/sumador-ba17f4f6708ca3b9
running 1 test
test it_works ... ok
Doc-tests sumador
running 0 tests
Cargo compilo y ejecuto nuestros tests. Hay dos conjuntos de salida aquí: uno para las
pruebas que nosotros escribimos, y otro para los tests de documentación. Hablaremos
acerca de estos mas tarde. Por ahora veamos esta linea:
fn it_works() {
# }
Entonces, porque nuestras pruebas vacías pasan? Cualquier prueba que no haga panic!
pasa, y cualquier prueba que hace panic! falla. Hagamos fallar a nuestra prueba:
#[test]
fn it_works() {
assert!(false);
}
assert! es una macro proporcionada por Rust la cual toma un argumento: si el argumento
Pruebas 82
El Lenguaje de Programacion Rust
$ cargo test
Compiling sumador v0.1.0 (file:///Users/goyox86/Code/rust/sumador)
Running target/debug/sumador-ba17f4f6708ca3b9
running 1 test
test it_works ... FAILED
failures:
failures:
it_works
$ echo $?
101
Esto es muy útil para integrar cargo test con otras herramientas.
#[test]
#[should_panic]
fn it_works() {
assert!(false);
}
Pruebas 83
El Lenguaje de Programacion Rust
$ cargo test
Compiling sumador v0.1.0 (file:///Users/goyox86/Code/rust/sumador)
Running target/debug/sumador-ba17f4f6708ca3b9
running 1 test
test it_works ... ok
Doc-tests sumador
running 0 tests
Rust proporciona otra macro, assert_eq! , que compara dos argumentos para verificar
igualdad:
#[test]
#[should_panic]
fn it_works() {
assert_eq!("Hola", "mundo");
}
Esta prueba pasa o falla? Debido a la presencia del atributo should_panic , pasa:
$ cargo test
Compiling sumador v0.1.0 (file:///Users/goyox86/Code/rust/sumador)
Running target/debug/sumador-ba17f4f6708ca3b9
running 1 test
test it_works ... ok
Doc-tests sumador
running 0 tests
Las pruebas should_panic pueden ser frágiles, es difícil garantizar que la prueba no falló
por una razón inesperada. Para ayudar en esto, un parámetro opcional expected puede ser
agregado a el atributo should_panic . La prueba se asegurara que el mensaje de error
contenga el mensaje proporcionado. Una versión mas segura de la prueba seria:
Pruebas 84
El Lenguaje de Programacion Rust
#[test]
#[should_panic(expected = "assertion failed")]
fn it_works() {
assert_eq!("Hola", "mundo");
}
#[test]
fn it_works() {
assert_eq!(4, suma_dos(2));
}
Este es un uso muy común de assert_eq! : llamar alguna función con algunos argumentos
conocidos y comparar la salida de dicha llamada con la salida esperada.
El modulo tests
Hay una forma en la cual nuestro ejemplo no es idiomático: le falta el modulo tests . La
manera idiomática de escribir nuestro ejemplo luce así:
#[cfg(test)]
mod tests {
use super::suma_dos;
#[test]
fn it_works() {
assert_eq!(4, suma_dos(2));
}
}
Hay unos cuantos cambios acá. El primero es la inclusion de un mod tests con un atributo
cfg . El modulo nos permite agrupar todas nuestras pruebas, y también nos permite definir
funciones de soporte de ser necesario, todo eso no forma parte de nuestro crate. El atributo
Pruebas 85
El Lenguaje de Programacion Rust
cfg solo compila nuestro código de pruebas si estuviéramos intentando correr las
pruebas. Esto puede ahorrar tiempo de compilación, también se asegura que nuestras
pruebas queden completamente excluidas de una compilación normal.
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
assert_eq!(4, suma_dos(2));
}
}
$ cargo test
Compiling sumador v0.1.0 (file:///Users/goyox86/Code/rust/sumador)
Running target/debug/sumador-ba17f4f6708ca3b9
running 1 test
test tests::it_works ... ok
Doc-tests sumador
running 0 tests
Funciona!
La convención actual es usar el modulo tests para contener tus pruebas de "estilo-
unitario". Cualquier cosa que solo pruebe un pequeño pedazo de funcionalidad va aquí.
Pero que acerca de las pruebas "estilo-integracion"? Para ellas, tenemos el directorio
tests .
Pruebas 86
El Lenguaje de Programacion Rust
El directorio tests
Para escribir una prueba de integración, creemos un directorio tests y coloquemos un
archivo tests/lib.rs dentro, con el siguiente de contenido:
#[test]
fn it_works() {
assert_eq!(4, sumador::suma_dos(2));
}
Luce similar a nuestras pruebas anteriores, pero ligeramente diferente. Ahora tenemos un
extern crate sumador al principio. Esto es debido a que las pruebas en el directorio son un
crate separado, entonces debemos importar nuestra biblioteca. Esto es también el porque
tests es un lugar idóneo pera escribir tests de integración: estas pruebas usan la
Ejecutemoslas:
$ cargo test
Compiling sumador v0.1.0 (file:///Users/goyox86/Code/rust/sumador)
Running target/debug/lib-f71036151ee98b04
running 1 test
test it_works ... ok
Running target/debug/sumador-ba17f4f6708ca3b9
running 1 test
test tests::it_works ... ok
Doc-tests sumador
running 0 tests
Ahora tenemos tres secciones: nuestras pruebas anteriores también fueron ejecutadas,
junto con la nueva prueba de integración.
Pruebas 87
El Lenguaje de Programacion Rust
Eso fue todo para el directorio tests . El modulo tests no es necesario aquí, debido a
que el modulo completo esta dedicado a pruebas.
Pruebas de documentación
Nada es mejor que documentación con ejemplos. Nada es peor que ejemplos que no
funcionan, debido a que el código a cambiado desde que la documentación fue escrita.
Respecto a esto, Rust soporta la ejecución automática de los ejemplos presentes en tu
documentación. He aquí un src/lib.rs pulido con ejemplos:
//! The `adder` crate provides functions that add numbers to other numbers.
//!
//! # Examples
//!
//!
/// This function adds two to its argument. /// /// # Examples /// /// /// use adder::add_two;
/// /// assert_eq!(4, add_two(2)); /// pub fn add_two(a: i32) -> i32 { a + 2 }
[cfg(test)]
mod tests { use super::*;
#[test]
fn it_works() {
assert_eq!(4, add_two(2));
}
Pruebas 88
El Lenguaje de Programacion Rust
Nota la documentación a nivel de modulo con `//!` y la documentación a nivel de función con `///`. La
```bash
$ cargo test
Compiling sumador v0.1.0 (file:///Users/goyox86/Code/rust/sumador)
Running target/debug/lib-f71036151ee98b04
running 1 test
test it_works ... ok
Running target/debug/sumador-ba17f4f6708ca3b9
running 1 test
test tests::it_works ... ok
Doc-tests sumador
running 2 tests
test _0 ... ok
test suma_dos_0 ... ok
Ahora tenemos los tres tipos de pruebas corriendo! Nota los nombres de las pruebas de
documentación: el _0 es generado para la prueba del modulo, y suma_dos_0 para la
prueba de función. Estos números se auto incrementaran con nombres como suma_dos_1 a
medida que mas ejemplos son agregados.
Pruebas 89
El Lenguaje de Programacion Rust
% Compilación Condicional
Rust posee un atributo especial, #[cfg] , que te permite compilar código basado en una
opción proporcionada al compilador. Tiene dos formas:
#[cfg(foo)]
# fn foo() {}
#[cfg(bar = "baz")]
# fn bar() {}
#[cfg(any(unix, windows))]
# fn foo() {}
#[cfg(not(foo))]
# fn not_foo() {}
Para activar o desactivar estos switches, si estas usando Cargo, se configuran en la sección
[features] de tu Cargo.toml
[features]
# No features por defecto
default = []
--cfg feature="${feature_name}"
La suma de esas opciones cfg determinara cuales son activadas, y en consecuencia, cual
código sera compilado. Tomemos este código:
Compilación Condicional 90
El Lenguaje de Programacion Rust
#[cfg(feature = "foo")]
mod foo {
}
Si lo compilamos con cargo build --features "foo" , Cargo enviara la opción --cfg
feature="foo" a rustc , y la salida tendrára el mod foo . Si compilamos con un cargo
build normal, ninguna opción extra sera proporcionada, y debido a esto, ningún modulo
foo existirá.
cfg_attr
También puedes configurar otro atributo basado en una variable cfg con cfg_attr :
#[cfg_attr(a, b)]
# fn foo() {}
Sera lo mismo que #[b] si a es configurado por un atributo cfg , y nada de cualquier
otra manera.
cfg!
La extension de sintaxis te permite también usar este tipo de opciones en cualquier otro
punto de tu código:
Compilación Condicional 91
El Lenguaje de Programacion Rust
% Documentación
Acerca de rustdoc
La distribución de Rust incluye una herramienta, rustdoc , encargada de generar la
documentación. rustdoc es también usada por Cargo a través de cargo doc .
La documentación puede ser generada de dos formas: desde el código fuente, o desde
archivos Markdown.
/// use std::rc::Rc; /// /// let cinco = Rc::new(5); /// ``` pub fn new(value: T) -> Rc { // la
implementación va aqui }
El código anterior genera documentación que luce como [esta][rc-new](ingles). He dejado la implementa
Rust mantiene un registro de dichos comentarios, registro que usa al momento de generar la documentac
```rust
/// El tipo `Option`. Vea [la documentación a nivel de modulo](../) para mas información.
enum Option<T> {
/// Ningún valor
None
/// Algún valor `T`
Some(T),
}
Documentación 92
El Lenguaje de Programacion Rust
/// El tipo `Option`. Vea [la documentación a nivel de modulo](../) para mas información.
enum Option {
None, /// Ningún valor
Some(T), /// Algún valor `T`
}
Obtendrás un error:
///
/// Otros detalles acerca de la construcción de `Rc<T>`s, quizás describiendo semántica
/// complicada, tal vez opciones adicionales, cualquier cosa extra.
///
# fn foo() {}
Nuestro ejemplo original solo tenia una linea de resumen, pero si hubiésemos tenido mas
cosas que decir, pudimos haber agregado mas explicación en un párrafo nuevo.
Secciones especiales
/// # Examples
# fn foo() {}
Documentación 93
El Lenguaje de Programacion Rust
A continuación están las secciones especiales. Estas son indicadas con una cabecera, # .
Hay tres tipos de cabecera que se usan comúnmente. Estos no son sintaxis especial, solo
convención, por ahora.
/// # Panics
# fn foo() {}
Malos e irrecuperables usos de una función (e.j. Errores de programación) en Rust son
usualmente indicados por pánicos (panics), los cuales matan el hilo actual como mínimo. Si
tu función posee un contrato no trivial como este, que es detectado/impuesto por pánicos,
documentarlo es muy importante.
/// # Failures
# fn foo() {}
Si tu función o método retorna un Result<T, E> , entonces describir las condiciones bajo las
cuales retorna Err(E) es algo bueno por hacer. Esto es ligeramente menos importante que
Panics , a consecuencia de que es codificado en el sistema de tipos, pero es aun, algo que
se recomienda hacer.
/// # Safety
# fn foo() {}
Si tu función es unsafe (insegura), deberías explicar cuales son las invariantes que deben
ser mantenidas por el llamador.
/// # Examples
///
///
/// use std::rc::Rc; /// /// let cinco = Rc::new(5); /// ```
fn foo() {}
Documentación 94
El Lenguaje de Programacion Rust
Tercero, `Examples`, incluye uno o mas ejemplos del uso de tu función o método, y tus usuarios te que
```rust
/// # Examples
///
/// Patrones `&str` simples:
///
///
/// let v: Vec<&str> = "Mary tenia un corderito".split(' ').collect(); /// assert_eq!(v, vec!["Mary",
"tenia", "un", "corderito"]); /// /// /// Patrones mas complejos con lambdas: /// /// /// let v:
Vec<&str> = "abc1def2ghi".split(|c: char| c.is_numeric()).collect(); /// assert_eq!(v, vec!["abc",
"def", "ghi"]); /// ```
fn foo() {}
Para escribir alguna código Rust en un comentario, usa los graves triples:
```rust
///
fn foo() {}
```rust
/// ```c
/// printf("Hola, mundo\n");
///
Documentación 95
El Lenguaje de Programacion Rust
fn foo() {}
La sintaxis de esta sección sera resaltada de acuerdo al lenguaje que estés mostrando. Si solo estas
Acá, es importante elegir la anotación correcta, debido a que `rustdoc` la usa de una manera interesa
```rust
///
fn foo() {}
Notaras que no necesitas una `fn main()` o algo mas. `rustdoc` agregara un main() automáticamente alr
```rust
///
/// use std::rc::Rc; /// /// let cinco = Rc::new(5); /// ```
fn foo() {}
Se convertirá en la prueba:
```rust
fn main() {
use std::rc::Rc;
let cinco = Rc::new(5);
}
He aquí el algoritmo completo que rustdoc usa para post-procesar los ejemplos:
1. Cualquier atributo #![foo] sobrante es dejado intacto como atributo del crate.
Documentación 96
El Lenguaje de Programacion Rust
Algunas veces, todo esto no es suficiente. Por ejemplo, todos estos ejemplos de código con
/// de los que hemos estado hablando? El texto plano:
Si, es correcto: puedes agregar lineas que comiencen con # , y estas serán eliminadas de
la salida, pero serán usadas en la compilación de tu código. Puedes usar esto como
ventaja. En este caso, los comentarios de documentación necesitan aplicar a algún tipo de
función, entonces si quiero mostrar solo un comentario de documentación, necesito agregar
una pequeña definición de función debajo. Al mismo tiempo, esta allí solo para satisfacer al
compilador, de manera tal que esconderla hace el ejemplo mas limpio. Puedes usar esta
técnica para explicar ejemplos mas largos en detalle, preservando aun la capacidad de tu
documentación para ser probada. Por ejemplo, este código:
let x = 5;
let y = 6;
println!("{}", x + y);
let x = 5;
# let y = 6;
# println!("{}", x + y);
Documentación 97
El Lenguaje de Programacion Rust
# let x = 5;
let y = 6;
# println!("{}", x + y);
# let x = 5;
# let y = 6;
println!("{}", x + y);
let x = 5;
# let y = 6;
# println!("{}", x + y);
# let x = 5;
let y = 6;
# println!("{}", x + y);
# let x = 5;
# let y = 6;
println!("{}", x + y);
Al repetir todas las partes del ejemplo, puedes asegurarte que tu ejemplo aun compila,
mostrando solo las partes relevantes a tu explicación.
Documentando macros
He aquí un ejemplo de la documentación a una macro:
/// Panic con un mensaje proporcionado a menos que la expression sea evaluada a true.
///
/// # Examples
///
///
Documentación 98
El Lenguaje de Programacion Rust
/// # #[macro_use] extern crate foo; /// # fn main() { /// panic_unless!(1 + 1 == 2, “Las
mathematicas estan rotas.”); /// # } /// /// /// should_panic /// # #[macro_use] extern crate
foo; /// # fn main() { /// panic_unless!(true == false, “Yo estoy roto.”); /// # } /// ```
[macro_export]
macro_rules! panic_unless { ($condition:expr, $($rest:expr),+) => ({ if ! $condition { panic!
($($rest),+); } }); }
fn main() {}
Notaras tres cosas: necesitamos agregar nuestro propia linea `extern crate`, de tal manera que podamo
```bash
$ rustdoc --test ruta/a/mi/crate/root.rs
# ó
$ cargo test
Correcto, cargo test prueba la documentación embebida también. Sin embargo, cargo
test , no probara crates binarios, solo bibliotecas. Esto debido a la forma en la que
rustdoc funciona: enlaza con la biblioteca a ser probada, pero en el caso de un binario, no
Hay unas pocas anotaciones mas que son útiles para ayudar a rustdoc a hacer la cosa
correcta cuando pruebas tu código:
/// ```ignore
/// fn foo() {
///
fn foo() {}
Documentación 99
El Lenguaje de Programacion Rust
La directiva `ignore` le dice a Rust que ignore el codigo. Esta es la forma que casi nunca querrás, p
```rust
/// ```should_panic
/// assert!(false);
///
fn foo() {}
`should_panic` le dice a `rustdoc` que el código debe compilar correctamente, pero sin la necesidad d
```rust
/// ```no_run
/// loop {
/// println!("Hola, mundo");
/// }
///
fn foo() {}
El atributo `no_run` compilara tu código, pero no lo ejecutara. Esto es importante para ejemplos como
Rust posee otro tipo de comentario de documentación, `//!`. Este comentario no documenta el siguiente
```rust
mod foo {
//! Esta es documentation para el modulo `foo`.
//!
//! # Examples
// ...
}
Es aquí en donde veras //! usado mas a menudo: para documentación de módulos. Si
tienes un modulo en foo.rs , frecuentemente al a abrir su código veras esto:
Documentación 100
El Lenguaje de Programacion Rust
Otra documentación
Todo este comportamiento funciona en archivos no Rust también. Debido a que los
comentarios son escritos en Markdown, frecuentemente son archivos .md .
/// # Examples
///
///
/// use std::rc::Rc; /// /// let cinco = Rc::new(5); /// ```
fn foo() {}
es solo
~~~markdown
# Examples
use std::rc::Rc;
Documentación 101
El Lenguaje de Programacion Rust
~~~
cuando esta en un archivo Markdown. Solo hay un detalle, los archivos markdown necesitan tener un tit
```markdown
% Titulo
atributos doc
A un nivel mas profundo, los comentarios de documentación son otra forma de escribir
atributos de documentación:
/// this
# fn foo() {}
#[doc="this"]
# fn bar() {}
//! this
#![doc="/// this"]
No veras frecuentemente este atributo siendo usado para escribir documentación, pero
puede ser util cuando se esten cambiando ciertas opciones, o escribiendo una macro.
Re-exports
rustdoc mostrara la documentación para un re-export publico en ambos lugares:
Lo anterior creara documentación para bar dentro de la documentación para el crate foo ,
así como la documentación para tu crate. Sera la misma documentación en ambos lugares.
Documentación 102
El Lenguaje de Programacion Rust
#[doc(no_inline)]
pub use foo::bar;
Controlando HTML
Puedes controlar algunos aspectos de el HTML que rustdoc genera a través de la versión
#![doc] del atributo:
#![doc(html_logo_url = "http://www.rust-lang.org/logos/rust-logo-128x128-blk-v2.png",
html_favicon_url = "http://www.rust-lang.org/favicon.ico",
html_root_url = "http://doc.rust-lang.org/")]
Esto configura unas pocas opciones, con un logo, favicon, y URL raíz.
Opciones de generación
rustdoc también contiene unas pocas opciones en la linea de comandos, para mas
personalización:
</head> .
renderizado.
Nota de seguridad
El Markdown en los comentarios de documentación es puesto sin procesar en la pagina
final. Se cuidadoso con HTML literal:
/// <script>alert(document.cookie)</script>
# fn foo() {}
Documentación 103
El Lenguaje de Programacion Rust
% Iteradores
Hablemos de ciclos.
for x in 0..10 {
println!("{}", x);
}
Ahora que sabes mas Rust, podemos hablar en detalle acerca de como este código
funciona. Los rangos (el 0..10 ) son iteradores. Un iterador es algo en lo que podemos
llamar el método .next() repetitivamente, y el iterador nos proporciona una secuencia de
elementos.
Por ejemplo:
loop {
match rango.next() {
Some(x) => {
println!("{}", x);
},
None => { break }
}
}
Creamos un enlace (una variable) a el rango, nuestro iterador. Luego iteramos mediante el
ciclo loop , con un match interno. Dicho match usa el resultado de rango.next() , que
nos proporciona una referencia a el siguiente valor en el iterador. next retorna un
Option<i32> , en este caso, que será Some(i32) cuando tenemos un valor y None cuando
Este ejemplo de código es básicamente el mismo que nuestra version de un ciclo for for .
El ciclo for es solo una forma practica de escribir una construcción loop / match / break .
Sin embargo, los ciclos for no son la única cosa que usa iteradores. Escribir tu propio
iterador implica implementar el trait Iterator . Si bien hacerlo esta fuera del ámbito de esta
guía, Rust provee a un numero de iteradores útiles para llevar a cabo diversas tareas. Antes
de hablar de eso, debemos hablar acerca de un anti-patron. Dicho anti-patron es usar
rangos de la manera expuesta anteriormente.
Iteradores 104
El Lenguaje de Programacion Rust
Si, acabamos de hablar acerca de cuan cool son los rangos. Pero son también muy
primitivos. Por ejemplo, si necesitamos iterar a través del contenido de un vector, podríamos
estar tentados a escribir algo como esto:
for i in 0..nums.len() {
println!("{}", nums[i]);
}
Hay dos razones para hacerlo de esta manera. Primero, expresa lo que queremos de
manera mas directa. Iteramos a través del vector completo, en vez de iterar a través de
indices para luego indexar el vector. Segundo, esta version es mas eficiente: en la primera
version tendremos chequeos de limites extra debido a que usa indexado, nums[i] . En el
segundo ejemplo y debido a que con el iterador cedemos una referencia a cada elemento a
la vez, ho hay chequeo de limites. Esto es muy común en iteradores: podemos ignorar
chequeos de limites innecesarios, sabiendo al mismo tiempo que estamos seguros.
Hay otro detalle aquí que no esta el 100% claro debido a el funcionamiento println! . num
es de tipo &i32 . Una referencia a un i32 , no un i32 . println! maneja el
dereferenciamiento por nosotros, es por ello que no lo vemos. Este código también es
correcto:
Iteradores 105
El Lenguaje de Programacion Rust
creación de una copia de la data para después darnos esa copia. Con referencias, solo
estamos haciendo un préstamo ('borrowing') de una referencia a la data, y por ello solo
pasamos una referencia, sin necesidad de transferir la pertenencia.
Entonces, ahora que hemos establecido que los rangos a veces no son lo que queremos,
hablemos de lo queremos.
Hay tres amplias clases de cosas que son relevantes: iteradores, adaptadores de iteradores
y consumidores. He aquí algunas definiciones:
Consumidores
Un consumidor opera en un iterador, retornando algún tipo de valor o valores. El
consumidor mas común es collect() . Este código no compila, pero muestra la intención:
Como puedes ver, llamamos collect() en el iterador. collect() toma tantos valores
como el iterador le proporcione, retornando una colección de resultados. Entonces, porque
este código no compilara? Rust no puede determinar que tipo de cosas quieres recolectar, y
es por ello necesitas hacerle saber. Esta es la version que compila:
Si recuerdas, la sintaxis ::<> te permite dar una indicio acerca del tipo, en nuestro caso
estamos diciendo que queremos un vector de enteros. No siempre es necesario usar el tipo
completo. _ te permitirá dar un indicio parcial acerca del tipo:
Esto dice "Recolecta en un Vec<T> , por favor, pero infiere que es T por mi.". _ es por
esta razón llamado algunas veces "marcador de posición de tipo".
Iteradores 106
El Lenguaje de Programacion Rust
collect() es el consumidor mas común, pero hay otros. find() es uno de ellos:
match mayores_a_cuarenta_y_dos {
Some(_) => println!("Tenemos algunos números!"),
None => println!("No se encontraron números :("),
}
find recibe un closure, y trabaja en una referencia a cada elemento de un iterador. Dicho
llamado base. El segundo es un closure que a su vez toma dos argumentos: el primero es
llamado el acumulador, y el segundo es un elemento. En cada iteración, el closure es
llamado, y el resultado es usado como el valor del acumulador en la siguiente iteración. En
la primera iteración, la base es el valor del acumulador.
Bien, eso es un poco confuso. Examinemos los valores de todas las cosas en este iterador:
0 3 3 6
# (1..4)
.fold(0, |suma, x| suma + x);
Iteradores 107
El Lenguaje de Programacion Rust
obtenemos.
Whew. fold puede ser un poco extraño a primera vista, pero una vez hace click, puedes
usarlo en todos lados. Cada vez que tengas una lista de cosas, y necesites un único
resultado, fold es apropiado.
Los consumidores son importantes debido a una propiedad adicional de los iteradores de la
que no hemos hablado todavia: pereza (laziness). Hablemos mas acerca de los iteradores y
veras porque los consumidores son importantes.
Iteradores
Como hemos dicho antes, un iterador es algo en lo que podemos llamar el método .next()
repetidamente, y este nos devuelve una secuencia de elementos. Debido a que
necesitamos llamar a el método, los iteradores pueden ser perezosos y no generar todos
los valores por adelantado. Este código, por ejemplo, no genera los números 1-99 . En su
lugar crea un valor que representa la secuencia:
Debido a que no hicimos nada con el rango, este no genero la secuencia. Agreguemos un
consumidor:
Los rangos son una de las dos formas básicas de iteradores que veras. La otra es iter() .
iter() puede transformar un vector en un iterador simple que proporciona un elemento a
la vez:
Iteradores 108
El Lenguaje de Programacion Rust
Estos dos iteradores básicos deberian ser de utilidad. Existen iteradores mas avanzados,
incluyendo aquellos que son infinitos.
Adaptadores de iterador
Los adaptadores de iteradores toman un iterador y lo modifican de alguna manera,
produciendo uno nuevo. El mas simple es llamado map :
(1..100).map(|x| x + 1);
map es llamado en otro iterador, map produce un iterador nuevo en el que cada referencia
warning: unused result which must be used: iterator adaptors are lazy and
do nothing unless consumed, #[warn(unused_must_use)] on by default
(1..100).map(|x| x + 1);
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
La pereza ataca de nuevo! Ese closure nunca se ejecutara. Este ejemplo no imprime ningún
numero:
Si estas intentanto ejecutar un closure en un iterador para obtener sus efectos colaterales
(side-effects) usa un for .
for i in (1..).take(5) {
println!("{}", i);
}
Esto imprimira:
Iteradores 109
El Lenguaje de Programacion Rust
1
2
3
4
5
filter() es un adaptador que toma un closure como argumento. Dicho closure retorna
true o false . El nuevo iterador que filter() produce solo elementos para los que el
for i in (1..100).filter(|&x| x % 2 == 0) {
println!("{}", i);
}
Esto imprimirá todos los números pares entre uno y cien. (Nota que debido a que filter
no consume los elementos que están siendo iterados, a este se le pasa una referencia a
cada elemento, debido a ello, el predicado usa el patron &x para extraer el entero.)
(1..)
.filter(|&x| x % 2 == 0)
.filter(|&x| x % 3 == 0)
.take(5)
.collect::<Vec<i32>>();
Esta es una pequeña muestra de las cosas en las cuales los iteradores, adaptadores de
iteradores, y consumidores pueden ayudarte. Existe una variedad de iteradores realmente
útiles, junto al hecho que puedes escribir tus propios. Los iteradores proporcionan una
manera segura y eficiente de manipular todo tipo de listas. Son un poco inusuales a primera
vista, pero si juegas un poco con ellos, quedaras enganchado. Para una lista de los
diferentes iteradores y consumidores echa un vistazo a la documentacion del modulo
iterator (ingles).
Iteradores 110
El Lenguaje de Programacion Rust
% Concurrencia
Antes de que hablemos de las características de concurrencia que vienen con Rust, es
importante entender algo: Rust es de bajo nivel, suficientemente bajo al punto que todas
estas facilidades están implementadas en la biblioteca estándar. Esto significa que si no te
gusta algún aspecto de la manera en la que Rust maneja la concurrencia, puedes
implementar una forma alternativa de hacerlo. mio es un vivo ejemplo de este principio en
acción.
Send
El primer trait del cual hablaremos es Send (ingles). Cuando un tipo T implementa Send ,
le indica al compilador que algo de este tipo puede transferir la pertenencia entre hilos de
forma segura.
Esto es importante para hacer cumplir ciertas restricciones. Por ejemplo si tenemos un
canal, conectando dos hilos, deberíamos querer transferir algunos datos a el otro hilo través
del canal. Por lo tanto, nos aseguramos de que Send haya sido implementado para ese
tipo.
De manera opuesta, si estamos envolviendo una biblioteca con FFI que no es threadsafe,
no deberíamos querer implementar Send , de manera tal que el compilador nos ayude a
asegurarnos que esta no pueda abandonar el hilo actual.
Concurrencia 111
El Lenguaje de Programacion Rust
Sync
El segundo de estos traits es llamado Sync (ingles). Cuando un tipo T implementa Sync ,
le indica al el compilador que algo de este tipo no tiene posibilidad de introducir inseguridad
en memoria cuando es usado de manera concurrente por multiples hilos de ejecución.
Por ejemplo, compartir data inmutable con una cuenta de referencias atómica es
threadsafe. Rust provee dicho tipo, Arc<T> , el cual implementa Sync , y es por ello que es
seguro de compartir entre hilos.
Estos dos traits te permiten usar el sistema de tipos para hacer garantías fuertes acerca de
las propiedades de tu código bajo concurrencia. En primer lugar, antes de demostrar
porque, necesitamos aprender como crear un programa concurrente en Rust!
Hilos
La biblioteca estándar de Rust provee una biblioteca para el manejo de hilos, que te permite
ejecutar código rust de forma paralela. He aqui un ejemplo basico del uso de std::thread :
use std::thread;
fn main() {
thread::spawn(|| {
println!("Hola desde un hilo!");
});
}
use std::thread;
fn main() {
let handle = thread::spawn(|| {
"Hola desde un hilo!"
});
println!("{}", handle.join().unwrap());
}
Concurrencia 112
El Lenguaje de Programacion Rust
Como ejemplo, un programa Rust que tendría una condición de carrera en muchos
lenguajes. En Rust no compilaría:
use std::thread;
fn main() {
let mut data = vec![1u32, 2, 3];
for i in 0..3 {
thread::spawn(move || {
data[i] += 1;
});
}
thread::sleep_ms(50);
}
Concurrencia 113
El Lenguaje de Programacion Rust
En este caso, sabemos que nuestro código deberia ser seguro, pero Rust no esta seguro de
ello. En realidad no es seguro, si tuviéramos una referencia a data en cada hilo, y el hilo
toma pertenencia de la referencia, tendríamos tres dueños! Eso esta mal. Podemos arreglar
esto a través del uso del tipo Arc<T> , un apuntador con conteo atómico de referencias. La
parte 'atómico' significa que es seguro compartirlo entre hilos.
Arc<T> asume una propiedad mas acerca de su contenido para asegurarse que es seguro
compartirlo entre hilos: asume que su contenido es Sync . Pero en nuestro caso, queremos
mutar el valor. Necesitamos un tipo que pueda asegurarse que solo una persona a la vez
pueda mutar lo que este dentro. Para eso, podemos usar el tipo Mutex<T> . He aquí la
segunda version de nuestro código. Aun no funciona, pero por una razón diferente:
use std::thread;
use std::sync::Mutex;
fn main() {
let mut data = Mutex::new(vec![1u32, 2, 3]);
for i in 0..3 {
let data = data.lock().unwrap();
thread::spawn(move || {
data[i] += 1;
});
}
thread::sleep_ms(50);
}
He aqui el error:
:9:9: 9:22 error: the trait `core::marker::Send` is not implemented for the type `std::sync::mutex::M
:11 thread::spawn(move || {
^~~~~~~~~~~~~
:9:9: 9:22 note: `std::sync::mutex::MutexGuard<'_, collections::vec::Vec>` cannot be sent between thr
:11 thread::spawn(move || {
^~~~~~~~~~~~~
Concurrencia 114
El Lenguaje de Programacion Rust
fn main() {
let data = Arc::new(Mutex::new(vec![1u32, 2, 3]));
for i in 0..3 {
let data = data.clone();
thread::spawn(move || {
let mut data = data.lock().unwrap();
data[i] += 1;
});
}
thread::sleep_ms(50);
}
Ahora llamamos clone() en nuestro Arc , lo cual incrementa el contador interno. Este
handle es entonces movido dentro del nuevo hilo. Examinemos el cuerpo del hilo mas de
cerca:
Primero, llamamos lock() , lo cual obtiene el bloqueo de exclusion mutua. Debido a que
esta operación puede fallar, este método retorna un Result<T, E> , y debido a que esto es
solo un ejemplo, hacemos unwrap() en el para obtener una referencia a la data. Código
real tendría un manejo de errores mas robusto en este lugar. Somos libres de mutarlo,
puesto que tenemos el bloqueo.
Por ultimo, mientras que los hilos se ejecutan, esperamos por la culminación de un
temporizador corto. Esto no es ideal: pudimos haber escogido un tiempo razonable para
esperar pero lo mas probable es que esperemos mas de lo necesario, o no lo suficiente,
dependiendo de cuanto tiempo los hilos toman para terminar la computación cuando el
programa corre.
Concurrencia 115
El Lenguaje de Programacion Rust
Una alternativa mas precisa a el temporizador seria el uso de uno de los mecanismos
proporcionados por la biblioteca estándar de Rust para la sincronización entre hilos.
Hablemos de ellos: los canales.
Canales
He aquí una version nuestro código que usa canales para la sincronización, en lugar de
esperar por un tiempo especifico:
fn main() {
let data = Arc::new(Mutex::new(0u32));
for _ in 0..10 {
let (data, tx) = (data.clone(), tx.clone());
thread::spawn(move || {
let mut data = data.lock().unwrap();
*data += 1;
tx.send(());
});
}
for _ in 0..10 {
rx.recv();
}
}
Hacemos uso del método mpsc::channel() para construir un canal nuevo. Enviamos (a
través de send ) un simple () a través del canal, y luego esperamos por el regreso diez
de ellos.
Mientras que este canal esta solo enviando una senal genérica, podemos enviar cualquier
data que sea Send a través del canal!
Concurrencia 116
El Lenguaje de Programacion Rust
use std::thread;
use std::sync::mpsc;
fn main() {
let (tx, rx) = mpsc::channel();
for _ in 0..10 {
let tx = tx.clone();
thread::spawn(move || {
let respuesta = 42u32;
tx.send(respuesta);
});
}
Un u32 es Send porque podemos hacer una copia de el. Entonces creamos un hilo, y le
solicitamos que calcule la respuesta, este luego nos envía la respuesta de regreso (usando
send() ) a través del canal.
Panicos
Un panic! causara la finalización abrupta (crash) del hilo de ejecución actual. Puedes usar
los hilos de Rust como un mecanismo de aislamiento sencillo:
use std::thread;
assert!(resultado.is_err());
Nuestro Thread nos devuelve un Result , el cual nos permite chequear si el hilo ha hecho
pánico o no.
Concurrencia 117
El Lenguaje de Programacion Rust
% Manejo de Errores
Los planes mejor establecidos por ratones y hombres a menudo se tuercen. "Tae a
Moose", Robert Burns
Algunas veces, las cosas simplemente salen mal. Es importante tener un plan para cuando
lo inevitable suceda. Rust posee un soporte rico para el manejo de errores que podrían
(seamos honestos: ocurrirán) ocurrir en tus programas.
Existen dos tipos de errores que pueden ocurrir en tus programas: fallas y pánicos.
Hablaremos de las diferencias entre los dos, y luego discutiremos como manejar cada uno.
Después discutiremos como promover fallas a pánicos.
Que queremos decir con "recuperar"? Bueno, en la mayoría de los casos, la posibilidad de
un error es esperada. Por ejemplo, considera la función parse :
"5".parse();
Este método convierte una cadena de caracteres a otro tipo. Pero debido a que es una
cadena de caracteres, no se puede estar seguro de que la conversion efectivamente tenga
éxito. Por ejemplo, a que debería ser convertido esto?:
"hola5mundo".parse();
Lo anterior no funciona. Sabemos que el método parse() solo tendrá éxito para algunas
entradas. Es un comportamiento esperado. Es por ello que llamamos a este error una falla.
Por otro lado, algunas veces, hay errores que son inesperados, o de los cuales no nos
podemos recuperar. Un ejemplo clásico es un assert! :
# let x = 5;
assert!(x == 5);
Usamos assert! para declarar que algo es cierto (true). Si la declaración no es verdad,
entonces algo esta muy mal. Suficientemente mal como para no poder continuar la
ejecución en el estado actual. Otro ejemplo es el uso de la macro unreachable!() :
use Evento::NuevoLanzamiento;
enum Evento {
NuevoLanzamiento,
}
fn main() {
println!("{}", probabilidad_descriptiva(NuevoLanzamiento));
}
Si bien sabemos que hemos cubierto todos los casos posibles, Rust no puede saberlo. No
sabe cual es la probabilidad entre 0.0 y 1.0. Es por ello que agregamos otro caso:
use Evento::NuevoLanzamiento;
enum Evento {
NuevoLanzamiento,
}
fn main() {
println!("{}", probabilidad_descriptiva(NuevoLanzamiento));
}
Nunca deberíamos alcanzar el caso _ , debido a esto hacemos uso de la macro para
indicarlo. unreachable!() produce un tipo diferente de error que Result . Rust llama a ese
tipo de errores pánicos.
let s = "foo";
assert_eq!(s.find('f'), Some(0));
assert_eq!(s.find('z'), None);
Esto es apropiado para casos simples, pero no nos da mucha información en el caso de
una falla. Que tal si quisiéramos saber el porque la función falló? Para ello, podemos usar el
tipo Result<T, E> . Que luce así:
Esta enum es proporcionada por Rust, es por ello que no necesitas definirla si deseas hacer
uso de ella. La variante Ok(T) representa éxito, y la variante Err(E) representa una falla.
Retornar un Result en lugar de un Option es recomendable para la mayoría de los casos
no triviales:
#[derive(Debug)]
enum Version { Version1, Version2 }
#[derive(Debug)]
enum ErrorParseo { LongitudCabeceraInvalida, VersionInvalida }
fn main() {
let version = parsear_version(&[1, 2, 3, 4]);
match version {
Ok(v) => {
println!("trabajando con la version: {:?}", v);
}
Err(e) => {
println!("error parseando cebecera: {:?}", e);
}
}
}
Esta función hace uso de un enum, ErrorParseo , para enumerar los errores que pueden
ocurrir.
El trait Debug es el que nos permite imprimir el valor del enum usando la operación de
formato {:?} .
panic!("boom");
resulta en
thread '
' panicked at 'boom', hello.rs:2
cuando lo ejecutas.
Debido a que estas situaciones son relativamente raras, usa los pánicos con moderación.
io::stdin().read_line(&mut buffer).unwrap();
unwrap() hará un pánico ( panic! ) si el Result es Err . Esto básicamente dice "Dame el
valor, y si algo sale mal, simplemente aborta la ejecución". Esto es menos confiable que
hacer match en el error y tratar de recuperarnos, pero al mismo tiempo es significativamente
mas corto. Algunas veces, la terminación abrupta es apropiada.
Hay una manera que de hacer lo anterior que es un poco mejor que unwrap() :
ok() convierte el Result en un Option , y expect() hace lo mismo que unwrap() , pero
Usando try!
Cuando escribimos código que llama a muchas funciones que retornan el tipo Result , el
manejo de errores se puede tornar tedioso. La macro try! esconde algo de el código
repetitivo correspondiente a la propagación de errores en la pila de llamadas.
try! reemplaza:
use std::fs::File;
use std::io;
use std::io::prelude::*;
struct Info {
nombre: String,
edad: i32,
grado: i32,
}
return Ok(());
}
Con:
use std::fs::File;
use std::io;
use std::io::prelude::*;
struct Info {
nombre: String,
edad: i32,
grado: i32,
}
return Ok(());
}
Envolver una expresión con try! resultara en el valor ( Ok ) exitoso desenvuelto, a menos
que el resultado sea Err , caso en el cual Err es retornado de manera temprana por la
función que envuelve al try.
Es importante hacer mención a el hecho de que solo puedes usar try! desde una función
que retorna un Result , lo que se traduce en que no puedes usar try! dentro de main() ,
debido a que main() no retorna nada.
try! hace uso de From<Error> (ingles) para determinar que retornar en el caso de error.
Introducción
Esta guía usara la biblioteca de compresión/descompresión snappy como introducción a la
escritura de bindings a código externo. Rust actualmente no puede llamar código en una
biblioteca C++ de manera directa, pero snappy incluye una interfaz en C (documentada en
snappy-c.h ).
El siguiente es un ejemplo minino de como llamar una función foránea que compilara
asumiendo que snappy esta instalada:
# #![feature(libc)]
extern crate libc;
use libc::size_t;
#[link(name = "snappy")]
extern {
fn snappy_max_compressed_length(source_length: size_t) -> size_t;
}
fn main() {
let x = unsafe { snappy_max_compressed_length(100) };
println!("longitud maxima de un buffer the 100 bytes comprimido: {}", x);
}
El bloque extern es una lista de firmas de función en una biblioteca foránea, en este caso
con la interfaz binaria de aplicación C (ABI en ingles) de la plataforma. El atributo #
[link(...)] es usado para instruir al enlazador a enlazar con la biblioteca snappy de modo
Se asume que las interfaces a funciones foráneas son inseguras, es por ello que las
llamadas a ellas deben estar dentro de un bloque unsafe {} como una promesa al
compilador de que todo lo contenido en el es realmente seguro. Bibliotecas en C a algunas
veces exponen interfaces que no son thread-safe, y casi cualquier función que toma un
apuntador como argumento no es valida para todas las entradas posibles debido a que el
apuntador podría ser un apuntador colgante, y los apuntadores planos quedan por fuera del
modelo de memoria segura de Rust.
Cuando se declaran los tipos de los argumentos de una función foránea, el compilador de
Rust no puede chequear si la declaración es correcta, a consecuencia de esto, especificarla
de manera correcta forma parte de mantener el binding correcto en tiempo de ejecución.
El bloque extern puede ser extendido para cubrir la API completa de snappy:
FFI 125
El Lenguaje de Programacion Rust
# #![feature(libc)]
extern crate libc;
use libc::{c_int, size_t};
#[link(name = "snappy")]
extern {
fn snappy_compress(input: *const u8,
input_length: size_t,
compressed: *mut u8,
compressed_length: *mut size_t) -> c_int;
fn snappy_uncompress(compressed: *const u8,
compressed_length: size_t,
uncompressed: *mut u8,
uncompressed_length: *mut size_t) -> c_int;
fn snappy_max_compressed_length(source_length: size_t) -> size_t;
fn snappy_uncompressed_length(compressed: *const u8,
compressed_length: size_t,
result: *mut size_t) -> c_int;
fn snappy_validate_compressed_buffer(compressed: *const u8,
compressed_length: size_t) -> c_int;
}
# fn main() {}
Envolver las funciones que esperan bufers involucra el uso de el modulo slice::raw para
la manipulación de vectores Rust como apuntadores a memoria. Los vectores de Rust están
garantizados a ser un bloque de memoria contiguo. La longitud es el numero de elementos
actualmente contenidos, y la capacidad es el tamaño total de la memoria asignada. La
longitud es menor o igual a la capacidad.
# #![feature(libc)]
# extern crate libc;
# use libc::{c_int, size_t};
# unsafe fn snappy_validate_compressed_buffer(_: *const u8, _: size_t) -> c_int { 0 }
# fn main() {}
pub fn validar_buffer_comprimido(src: &[u8]) -> bool {
unsafe {
snappy_validate_compressed_buffer(src.as_ptr(), src.len() as size_t) == 0
}
}
FFI 126
El Lenguaje de Programacion Rust
# #![feature(libc)]
# extern crate libc;
# use libc::{size_t, c_int};
# unsafe fn snappy_compress(a: *const u8, b: size_t, c: *mut u8,
# d: *mut size_t) -> c_int { 0 }
# unsafe fn snappy_max_compressed_length(a: size_t) -> size_t { a }
# fn main() {}
pub fn comprimir(orig: &[u8]) -> Vec<u8> {
unsafe {
let long_orig = orig.len() as size_t;
let porig = orig.as_ptr();
FFI 127
El Lenguaje de Programacion Rust
# #![feature(libc)]
# extern crate libc;
# use libc::{size_t, c_int};
# unsafe fn snappy_uncompress(compressed: *const u8,
# compressed_length: size_t,
# uncompressed: *mut u8,
# uncompressed_length: *mut size_t) -> c_int { 0 }
# unsafe fn snappy_uncompressed_length(compressed: *const u8,
# compressed_length: size_t,
# result: *mut size_t) -> c_int { 0 }
# fn main() {}
pub fn descomprimir(orig: &[u8]) -> Option<Vec<u8>> {
unsafe {
let long_orig = orig.len() as size_t;
let porig = orig.as_ptr();
Como referencia, los ejemplos usados aquí están disponibles también como una biblioteca
en Github.
Destructores
La bibliotecas externas usualmente transfieren la pertenencia de los recursos a el código
llamador. Cuando esto ocurre, debemos usar los destructores de Rust para proveer
seguridad y la garantía de la liberación de dichos recursos (especialmente en el caso de un
pánico).
Para mas información acerca de los destructores, echa un vistazo a la sección del trait
Drop.
Rust
Algunas bibliotecas externas requieren el uso de callbacks para reportar de vuelta su estado
o data parcial a el llamador. Es posible pasar funciones definidas en Rust a una biblioteca
externa. El requerimiento para esto es que la función callback este marcada como extern
y con la convención de llamadas correcta para hacer posible su llamado desde código C.
La función callback puede entonces ser enviada a través de una llamada de registro a la
biblioteca en C y su posterior invocación desde allá.
Código Rust:
#[link(name = "extlib")]
extern {
fn registrar_callback(cb: extern fn(i32)) -> i32;
fn disparar_callback();
}
fn main() {
unsafe {
registrar_callback(callback);
disparar_callback(); // Dispara el callback
}
}
Código C:
void disparar_callback() {
cb(7); // Llamara a callback(7) en Rust
}
FFI 129
El Lenguaje de Programacion Rust
Todo esto puede ser logrado a través del paso de un apuntador plano a la biblioteca en C.
La biblioteca en C entonces puede incluir el apuntador a el objeto Rust en la notificación.
Esto permitirá a el callback acceder de manera insegura el objeto Rust referenciado.
Código Rust:
#[repr(C)]
struct ObjetoRust {
a: i32,
// otros miembros
}
#[link(name = "extlib")]
extern {
fn registrar_callback(objetivo: *mut ObjetoRust,
cb: extern fn(*mut ObjetoRust, i32)) -> i32;
fn disparar_callback();
}
fn main() {
// Creando el objeto que sera referenciado en el callback
let mut objeto_rust = Box::new(ObjetoRust { a: 5 });
unsafe {
registrar_callback(&mut *objeto_rust, callback);
disparar_callback();
}
}
Código C:
FFI 130
El Lenguaje de Programacion Rust
void disparar_callback() {
cb(objetivo_cb, 7); // Llamare a callback(&ObjetoRust, 7) in Rust
}
Callbacks Asíncronos
En los ejemplos anteriores los callbacks son invocados como una reacción directa a una
llamada a función a la biblioteca externa en C. El control sobre el hilo actual es cambiado de
Rust a C para la ejecución del callback, pero al final el callback es ejecutado en el mismo
hilo que llamo a la función que disparo el callback.
Las cosas se vuelven mas complicadas cuando la biblioteca externa crea sus propios hilos
e invoca los callbacks desde ellos. En estos casos el acceso a las estructuras de datos Rust
dentro de los callbacks es especialmente inseguro y mecanismos de sincronización
apropiados deben ser usados. Ademas de los mecanismos clásicos de sincronización como
mutexes, una posibilidad en Rust es el uso de canales (en std::sync::mpsc ) para transferir
data desde el hilo C que invoco el callback a un hilo Rust.
Enlace
El atributo link en bloques extern proporciona el bloque de construcción basico para
instruir a rustc como enlazar con bibliotecas nativas. Hoy en dia hay dos formas del atributo
link:
#[link(name = "foo")]
FFI 131
El Lenguaje de Programacion Rust
En ambos casos, foo es el nombre de la biblioteca nativa con la cual estamos enlazando,
y en el segundo caso bar es el tipo de biblioteca nativa a la cual esta enlazando el
compilador. Actualmente hay tres tipos de bibliotecas nativas conocidos:
Los diferentes valores kind tienen como objeto diferenciar como la biblioteca nativa
participa en el enlace. Desde la perspectiva del enlace, el compilador de Rust crea dos tipos
de artefactos: parciales (rlib/staticlib) y finales (dylib/binary). Las dependencias en
bibliotecas nativas y frameworks son propagadas a la frontera de artefacto final, mientras
que las dependencias estáticas no son propagadas del todo, debido a que las bibliotecas
estáticas están integradas directamente dentro del artefacto subsecuente.
Unos pocos ejemplos de como este modelo puede ser usado son:
Independientemente del tipo de salida para el crate, la biblioteca nativa estática sera
incluida en la salida, significando que la distribución de la biblioteca estática nativa no es
necesaria.
Una dependencia dinámica normal: Bibliotecas comunes del sistema (como readline )
están disponibles en un gran numero de sistemas, y a menudo una copia estática de
esas biblioteca puede no existir. Cuando esta dependencia es incluida en un crate
Rust, objetivos parciales (como rlibs) no enlazaran a la biblioteca, pero cuando el rlib es
incluido en un objetivo final (como un binario), la biblioteca sera enlazada.
En OSX, los frameworks se comportan con la misma semantica que una biblioteca
dinámica.
Bloques Unsafe
FFI 132
El Lenguaje de Programacion Rust
Algunas operaciones, como dereferenciar punteros planos o llamadas a funciones que han
sido marcadas como inseguras solo son permitidas dentro de bloques unsafe. Los bloques
unsafe aíslan la inseguridad y son una promesa al compilador que la inseguridad no se
filtrara hacia el exterior del bloque.
Las funciones unsafe, por otro lado, hacen publicidad de la inseguridad a el mundo exterior.
Una función insegura se escribe de la siguiente manera:
Esta función puede ser invocada solo desde un bloque unsafe u otra función unsafe .
# #![feature(libc)]
extern crate libc;
#[link(name = "readline")]
extern {
static rl_readline_version: libc::c_int;
}
fn main() {
println!("Tienes la version {} de readline.",
rl_readline_version as i32);
}
Alternativamente, podrías necesitar alterar estado global proporcionado por una interfaz
foránea. Para hacer esto, los estáticos pueden ser declarados con mut con la finalidad de
poder mutarlos.
FFI 133
El Lenguaje de Programacion Rust
# #![feature(libc)]
extern crate libc;
use std::ffi::CString;
use std::ptr;
#[link(name = "readline")]
extern {
static mut rl_prompt: *const libc::c_char;
}
fn main() {
let prompt = CString::new("[my-awesome-shell] $").unwrap();
unsafe {
rl_prompt = prompt.as_ptr();
println!("{:?}", rl_prompt);
rl_prompt = ptr::null();
}
}
Nota que toda la interacción con un static mut es insegura, ambos lectura y escritura.
Lidiar con estado global mutable requiere de un gran cuidado.
# #![feature(libc)]
extern crate libc;
Esto aplica a el bloque extern completo. La lista de restricciones ABI soportadas son:
stdcall
FFI 134
El Lenguaje de Programacion Rust
aapcs
cdecl
fastcall
Rust
rust-intrinsic
system
win64
La mayoría de las abis son auto-explicativas, pero el abi system puede parecer un poco
raro. Esta restricción selecciona cualquiera que sea el ABI apropiado para interoperar con
las bibliotecas objetivo. Por ejemplo, en win32 con una arquitectura x86, significa que el abi
usado sera stdcall . En x86_64, sin embargo, Windows usa la convención de llamadas
C , entonces se usa C . Esto se traduce a que en nuestro ejemplo anterior, pudimos haber
hecho uso de extern "system" { ... } para definir un bloque para todos los sistemas
Windows, no solo los x86.
Los boxes Rust ( Box<T> ) usan apuntadores no-nulables como handles que apuntan a el
objeto contenido. Sin embargo, no deben ser creados manualmente debido que son
manejados por asignadores internos. La referencias pueden ser asumidas de manera
segura como apuntadores no-nulables directos al tipo. Sin embargo, romper el chequeo de
préstamo (borrowing) o las reglas de mutabilidad no se garantiza ser seguro, es por ello que
se prefiere el uso de apuntadores planos ( * ) de ser necesario debido a que el compilador
no puede asumir muchas cosas acerca de ellos.
FFI 135
El Lenguaje de Programacion Rust
#[no_mangle]
pub extern fn hola_rust() -> *const u8 {
"Hola, mundo!\0".as_ptr()
}
# fn main() {}
El extern hace que esta función se adhiera a la convención de llamadas de C, tal y como
se discute en "Convenciones de llamadas foráneas". El atributo no_mangle desactiva el
estropeo (mangling) de nombres de Rust, de manera tal que sea mas facil de enlazar.
FFI y pánicos
Es importante estar consciente de los panic! os cuando se trabaja con FFI. Un panic! en
al limite de FFI es comportamiento indefinido. Si estas escribiendo código que pueda hacer
pánico, debes ejecutarlo en otro hilo, de esta forma el pánico no se propagara hasta C:
FFI 136
El Lenguaje de Programacion Rust
use std::thread;
#[no_mangle]
pub extern fn oh_no() -> i32 {
let h = thread::spawn(|| {
panic!("Oops!");
});
match h.join() {
Ok(_) => 1,
Err(_) => 0,
}
}
# fn main() {}
FFI 137
El Lenguaje de Programacion Rust
% Borrow y AsRef
Lost traits Borrow and AsRef son muy similares, pero diferentes. He aquí un breve repaso
acerca de lo que significan dichos traits:
Borrow
El trait Borrow se usa cuando estamos escribiendo un estructura de datos, y deseamos
usar un tipo owned o un tipo borrowed como sinónimo por alguna razón.
struct HashMap {
manera, podemos crear un HashMap que use claves String pero al momento de buscar
use &str s:
use std::collections::HashMap;
assert_eq!(map.get("Foo"), Some(&42));
Esto es debido a que la biblioteca estándar posee una impl Borrow<str> for String .
Para la mayoría de los tipos, cuando desees tomar un tipo ya sea owned o borrowed, un &T
es suficiente. Pero un area en la cual Borrow es efectivo es cuando hay mas de un tipo de
valor borrowed. Esto es especialmente cierto en referencias y slices: puedes tener ambos
un &T un &mut T . Si queremos aceptar ambos tipos Borrow es lo que necesitamos:
use std::borrow::Borrow;
use std::fmt::Display;
let mut i = 5;
foo(&i);
foo(&mut i);
AsRef
El trait AsRef es un trait de conversion. Es usado para convertir algún valor a una
referencia en código generico. Como esto:
let s = "Hola".to_string();
fn foo<T: AsRef<str>>(s: T) {
let slice = s.as_ref();
}
Escoge Borrow cuando quieras abstraerte por encima de diferentes tipos de borrowing, o
cuando estas construyendo una estructura de datos que trata los valores owned y borrowed
de formas equivalentes, como el hashing y la comparación.
Usa AsRef cuando desees convertir algo directamente en una referencia, y estés
escribiendo código genérico.
% Canales de Distribución
El proyecto Rust usa un concepto denominado ‘canales de distribución’ para administrar los
releases.
Es importante entender este proceso para así poder decidir cual version de Rust debe usar
tu proyecto.
Visión general
Hay tres canales para los releases de Rust:
Nocturno (nightly)
Beta
Estable (stable)
Los releases nocturnos son creados una vez al día. Cada seis semanas, el ultimo release
nocturno es promovido a 'Beta'. En este punto, solo recibirá parches que arreglen errores
serios. Seis semanas después el beta es promovido a 'Estable', y se convierte en el nuevo
release de 1.x .
Este proceso ocurre en paralelo. Cada seis semanas, en el mismo día el nocturno sube a
beta, el beta es promovido a estable. Cuando 1.x es liberado, al mismo tiempo, 1.(x +
1)-beta es liberado, y el nocturno se vuelve la primera version de 1.(x + 2)-nightly .
Que hay acerca de beta? Nosotros recomendamos a todos los usuarios Rust que usan el
canal de distribución estable a que también prueben en sus sistemas de integración
continua con el canal beta. Esto ayudara a advertir al equipo en caso de que exista una
regresión accidental.
% Sintaxis y Semántica
Esta sección divide a Rust en pequeños pedazos, uno para cada concepto.
Si deseas aprender Rust de abajo hacia arriba, leer esta sección en orden es una muy
buena forma de hacerlo.
Estas secciones forman una referencia para cada concepto, si estas leyendo otro tutorial y
encuentras algo confuso, puedes encontrarlo explicado en algún lugar de esta sección.
% Enlaces a Variable
Virtualmente cualquier programa no-'Hola Mundo’ usa enlaces a variables. Dichos enlaces a
variables lucen así:
fn main() {
let x = 5;
}
En muchos lenguajes, esto es llamado una variable, pero los enlaces a variable de Rust
tienen un par de trucos bajo la manga. Por ejemplo el lado izquierdo de una expresión let
es un ‘patron’, no un simple nombre de variable. Esto se traduce en que podemos hacer
cosas como:
Después de que esta expresión es evaluada, x sera uno, y y sera dos. Los patrones son
realmente poderosos, y tienen su propia sección en el libro. No necesitamos esas
facilidades por ahora, solo mantengamos esto en nuestras mentes mientras avanzamos.
Podemos agregar el tipo si lo deseamos. Los tipos vienen después de dos puntos ( : ):
let x: i32 = 5;
Si te pidiera leer esto en voz alta al resto de la clase, dirías “ x es un enlace con el tipo
i32 y el valor cinco .”
En este caso decidimos representar x como un entero con signo de 32 bits. Rust posee
muchos tipos de enteros primitivos diferentes. Estos comienzan con i para los enteros
con signo y con u para los enteros sin signo. Los tamaños posibles para enteros son 8,
16, 32, y 64 bits.
fn main() {
let x = 5; // x: i32
}
Nota las similitudes entre la anotación y la sintaxis que usas con let . Incluir esta clase de
comentarios no es idiomático en Rust, pero los incluiremos ocasionalmente para ayudarte a
entender cuales son los tipos que Rust infiere.
let x = 5;
x = 10;
Si deseas que un enlace a variable sea mutable, puedes hacer uso de mut :
No hay una razón única por la cual los enlaces a variable son inmutables por defecto, pero
podemos pensar acerca de ello a través de uno de los focos principales de Rust: seguridad.
Si olvidas decir mut . el compilador lo notará, y te hará saber que has mutado algo que no
tenias intención de mutar. Si los enlaces a variables fuesen mutables por defecto, el
compilador no seria capaz de decirte esto. Si tenias la intención de mutar algo, entonces la
solución es muy fácil: agregar mut .
Hay otras buenas razones para evitar estado mutable siempre que sea posible, pero estan
fuera del alcance de esta guisa. En general, puedes frecuentemente evitar mutación
explicita, y esto es preferible en Rust. Dicho esto, algunas veces, la mutación es justo lo que
necesitas, es por ello que no esta prohibida.
Volvamos a los enlaces a variables. Los enlaces a variable en Rust poseen un aspecto mas
que difiere de otros lenguajes: se requiere esten inicializados a un valor antes que puedas
usarlos.
Pongamos esto a prueba. Cambia tu archivo src/main.rs para que luzca así:
fn main() {
let x: i32;
println!("Hola, mundo!");
}
Puedes usar cargo build en la linea de comando para compilarlo. Obtendrás una
advertencia pero aun así imprimirá "Hola, mundo!":
Rust nos advierte que nunca hacemos uso del enlace a variable, pero debido a que nunca
la usamos, no hay peligro, no hay falta. Sin embargo, las cosas cambian si efectivamente
intentamos usar x . Hagamos eso. Cambia tu programa para que luzca de la siguiente
manera:
fn main() {
let x: i32;
$ cargo build
Compiling hola_mundo v0.0.1 (file:///home/tu/proyectos/hola_mundo)
src/main.rs:4:39: 4:40 error: use of possibly uninitialized variable: `x`
src/main.rs:4 println!("El valor de x es: {}", x);
^
note: in expansion of format_args!
<std macros>:2:23: 2:77 note: expansion site
<std macros>:1:1: 3:2 note: in expansion of println!
src/main.rs:4:5: 4:42 note: expansion site
error: aborting due to previous error
Could not compile `hello_world`.
Rust no te permitirá usar un valor que no haya sido inicializado previamente. A continuación
hablemos de lo que le hemos agregado a println! .
Cuando usas las llaves, Rust intentará mostrar el valor de una forma que tenga sentido
después de chequear su tipo. Si deseamos especificar un formato mas detallado, existen un
amplio numero de opciones disponible. Por ahora, nos apegaremos al comportamiento por
defecto: los números enteros no son muy complicados de imprimir.
% Funciones
fn main() {
}
fn foo() {
}
Entonces, que hay acerca de recibir argumentos? A continuación una función que imprime
un numero:
fn imprimir_numero(x: i32) {
println!("x es: {}", x);
}
fn main() {
imprimir_numero(5);
}
fn imprimir_numero(x: i32) {
println!("x es: {}", x);
}
Como podrás ver, los argumentos de función trabajan de manera muy similar a las
declaraciones let : agregas un tipo a el nombre del argumento, después de dos puntos.
He aquí un programa completo que suma dos números y luego los imprime:
fn main() {
imprimir_suma(5, 6);
}
Funciones 147
El Lenguaje de Programacion Rust
Los argumentos son separados por una coma, en ambos casos, cuando llamas a la función
así como cuando la declaras.
fn imprimir_suma(x, y) {
println!("suma es: {}", x + y);
}
Esto es una deliberada decisión de diseño. Aunque la inferencia del programa completo es
possible, los lenguajes que la poseen, como Haskell, a menudo sugieren que documentar
tus tipos de manera explicita es una buena practica. Nosotros coincidimos en que obligar a
las funciones a declarar los tipos al mismo tiempo permitiendo inferencia dentro de la
función es un maravilloso punto medio entre inferencia completa y la ausencia de inferencia.
Que hay acerca de retornar un valor? A continuación una función que suma uno a un
entero:
Obtendríamos un error:
Funciones 148
El Lenguaje de Programacion Rust
Entonces, cual es la diferencia? Las expresiones retornan un valor, y las sentencias no. Es
por ello que terminamos con el error ‘not all control paths return a value’ aquí: la sentencia
x + 1; no retorna ningún valor.Hay dos tipos de sentencias en Rust: ‘sentencias de
En algunos lenguajes, los enlaces a variable pueden ser escritos como expresiones, no solo
sentencias. Como en Ruby:
x = y = 5
En Rust, sin embargo, el uso de let para introducir un enlace a variable no es una
expresión. Lo siguiente producirá un error en tiempo de compilación:
El compilador nos dice que estaba esperando el comienzo de una expresión, y un let
puede ser solo una sentencia, no una expresión.
Nota que la asignación a una variable que ha sido previamente asignada (e.j. y = 5 ) es
una expresión, aun así su valor no es particularmente util. A diferencia de otros lenguajes en
los que una asignación es evaluada a el valor asignado (e.j. 5 en ejemplo anterior), en
Rust el valor de una asignación es una tupla vacía () debido a que el valor asignado
puede tener un solo dueño y cualquier otro valor de retorno seria sorpresivo:
Funciones 149
El Lenguaje de Programacion Rust
let mut y = 5;
Entonces, Cual es la excepción que nos hace decir "casi"? Ya lo has visto en el código
anterior:
Nuestra función clama retornar un i32 , pero con un punto y coma, retornaría () . Rust
determina que esto no es probablemente lo que queremos, y sugiere la remoción del punto
y coma en el error que vimos anteriormente.
Retornos tempranos
Pero que hay acerca de los retornos tempranos? Rust proporciona una palabra reservada
para ello, return :
Usar un return como la ultima linea de una función es valido, pero es considerado pobre
estilo:
Funciones 150
El Lenguaje de Programacion Rust
La definición previa sin el return puede lucir un poco extraña si nunca has trabajado con
un lenguaje basado en expresiones, pero esta se vuelve intuitiva con el tiempo.
Funciones divergentes
Rust tiene una sintaxis especial para las ‘funciones divergentes’, que son funciones que no
retornan:
fn diverge() -> ! {
panic!("Esta función nunca retorna!");
}
println!() , panic!() causa que el hilo de ejecución actual termine de manera abrupta
Debido a que esta función causara una salida abrupta, esta nunca retornara, es por ello que
posee el tipo ‘ ! ’, que se lee ‘diverge’. Una función divergente puede ser usada como
cualquier tipo:
# fn diverge() -> ! {
# panic!("Esta función nunca retorna!");
# }
let x: i32 = diverge();
let x: String = diverge();
Apuntadores a función
También podemos crear enlaces a variables que apunten a funciones:
f es un enlace a variable que apunta a una función que toma un i32 como argumento y
Funciones 151
El Lenguaje de Programacion Rust
// inferencia de tipos
let f = mas_uno;
Funciones 152
El Lenguaje de Programacion Rust
% Tipos Primitivos
Rust posee un conjunto de tipos que son considerados ‘primitivos’. Esto significa que están
integrados en el lenguaje. Rust esta estructurado de tal manera que la biblioteca estándar
también provee una numero de tipos útiles basados en los primitivos, pero estos son los
mas primitivos.
Booleanos
Rust posee un tipo booleano integrado, denominado bool . Tiene dos posibles valores
true y false :
let x = true;
char
El tipo char representa un unico valor escalar Unicode. Puedes crear char s con comillas
simples: ( ' )
let x = 'x';
let dos_corazones = '';
A diferencia de otros lenguajes, esto significa que char en Rust no es un solo byte, sino
cuatro.
Tipos numéricos
Rust posee una variedad de tipos numéricos en unas pocas categorías: con signo y sin
signo, fijos y variables, de punto flotante y enteros.
Dichos tipos consisten de dos partes: la categoría, y el tamaño. Por ejemplo, u16 es un
tipo sin signo con un tamaño de dieciséis bits. Mas bits te permiten almacenar números mas
grandes.
Si un literal de numero no posee nada que cause la inferencia de su tipo, se usan tipos por
defecto:
He aquí una lista de los diferentes tipos numéricos, con enlaces a su documentación en la
biblioteca estándar (ingles):
i8
i16
i32
i64
u8
u16
u32
u64
isize
usize
f32
f64
Los tipos sin signo usan una u para su categoria, y los tipos con signo usan i . La i es
de integer (entero). Entonces, u8 es un numero de ocho bits sin signo, y un i8 es un
numero de ocho bits con signo.
Arreglos
Como muchos lenguajes de programación, Rust posee tipos lista para representar una
secuencia de cosas. El mas básico es el arreglo, una lista de elementos del mismo tipo y de
tamaño fijo. Por defecto los arreglos son inmutables.
Hay un atajo para la inicialización de cada uno de los elementos del arreglo a el mismo
valor. En este ejemplo, cada elemento de a sera inicializado a 0 :
Slices
Un slice es una referencia a (o una “vista” dentro de) otra estructura de datos. Los slices
son útiles para permitir acceso seguro y eficiente a una porción de un arreglo sin involucrar
el copiado. Por ejemplo, podrías querer hacer referencia a una sola linea de una archivo
que ha sido previamente leído en memoria. Por naturaleza, un slice no es creado
directamente, estos son creados para una variable que ya existe. Los slices poseen una
longitud, pueden ser mutables o inmutables, y en muchas formas se comportan como los
arreglos:
Los slices poseen el tipo &[T] . Hablaremos acerca de T cuando cubramos los genericos.
str
El str de Rust es el tipo de cadena de caracteres mas primitivo. Como un tipo sin tamaño,
y no es muy util en si mismo, pero se vuelve muy util cuando es puesto detrás de una
referencia, como &str . Es por ello que lo dejaremos hasta aquí.
Tuplas
Una tupla es una lista ordenada de tamaño fijo. Como esto:
Los paréntesis y comas forman esta tupla de longitud dos. He aquí el mismo código pero
con anotaciones de tipo:
Como puedes ver, el tipo de una tupla es justo como la tupla, pero con cada posición
teniendo el tipo en lugar del valor. Los lectores cuidadosos también notaran que las tuplas
son heterogéneas: tenemos un i32 y a &str en esta tupla. En los lenguajes de
programación de sistemas, las cadenas de caracteres son un poco mas complejas que en
otros lenguajes. Por ahora solo lee &str como un slice de cadena de caracteres.
Aprenderemos mas dentro de poco.
Puedes asignar una tupla a otra, si estas tienen los mismo tipos contenidos y la misma
aridad. La tuples poseen la misma aridad cuando estas tienen el mismo tamaño.
x = y;
Puedes acceder a los campos de una tupla a través de un let con destructuracion. He aquí
un ejemplo:
Recuerdas anteriormente cuando dije que el lado izquierdo de una sentencia let era mas
poderoso que la simple asignación de un binding? Henos aquí. Podemos colocar un patron
en el lado izquierdo de un let , y si este concuerda con el lado derecho, podemos asignar
multiples bindings a variable de una sola vez. En este caso, el let “destructura” o “parte”
la tupla, y asigna las partes a los tres bindings a variable.
Puedes eliminar la ambigüedad entre una tupla de un solo elemento y un valor encerrado
en paréntesis usando una coma:
Indexado en tuplas
Puedes también acceder los campos de una tupla con la sintaxis de indexado:
let x = tupla.0;
let y = tupla.1;
let z = tupla.2;
Funciones
Las funciones también tienen un tipo! Estas lucen así:
En este caso, x es un ‘apuntador’ a una función que recibe un i32 y retorna un i32 .
% Comentarios
Ahora que hemos vistos algunas funciones, es buena idea aprender sobre los comentarios.
Los comentarios son notas que dejas a otros programadores con la finalidad de explicar
algún aspecto de tu código. El compilador mayormente, los ignora.
Rust posee dos tipos de comentarios que te deben interesar: comentarios de linea y
comentarios de de documentación
// Los comentarios de linea son cualquier cosa después de ‘//’ y se extienden hasta el final de la li
/// let cinco = 5; /// /// assert_eq!(6, suma_uno(5)); /// # fn suma_uno(x: i32) -> i32 { /// # x + 1
/// # } /// ``` fn suma_uno(x: i32) -> i32 { x + 1 }
Existe otro estilo de comentarios, `//!`, con la finalidad de comentar los items contenedores (e.j cr
//! # La Biblioteca Estándar de Rust //! //! La Biblioteca Estándar de Rust proporciona la
funcionalidad //! en tiempo de ejecución esencial para la construcción de software //! Rust
portable. ```
Cuando se escriben comentarios doc, proporcionar algunos ejemplos de uso es muy, muy
util. Notaras que hemos hecho uso de una macro: assert_eq! . Esta compara dos valores, y
hace panic! o si estos no son iguales. Es muy util en la documentación. También existe
otra macro, assert! , la cual hace panic! o si el valor proporcionado es false .
Comentarios 159
El Lenguaje de Programacion Rust
% if
let x = 5;
if x == 5 {
println!("x es cinco!");
}
De haber cambiado el valor de x a algo diferente, esta linea no hubiese sido impresa.
Para ser mas especifico, si la expresión después del if es evaluada a true , entonces el
bloque de código es ejecutado. Si es false , dicho bloque no se invoca.
Si deseas que algo ocurra en el caso de false , debes hacer uso de un else :
let x = 5;
if x == 5 {
println!("x es cinco!");
} else {
println!("x no es cinco :(");
}
let x = 5;
if x == 5 {
println!("x es cinco!");
} else if x == 6 {
println!("x es seis!");
} else {
println!("x no es ni cinco ni seis :(");
}
Todo esto es bien estándar. Sin embargo, también puedes hacer esto:
if 160
El Lenguaje de Programacion Rust
let x = 5;
let y = if x == 5 {
10
} else {
15
}; // y: i32
let x = 5;
if 161
El Lenguaje de Programacion Rust
% Ciclos
Rust actualmente provee tres enfoques para realizar actividad iterativa. loop , while y
for . Cada uno de dichos enfoques tiene sus propios usos.
loop
El ciclo infinito loop es el ciclo mas simple disponible en Rust. A traves del uso de la
palabra reservada loop , Rust proporciona una forma de iterar indefinidamente hasta que
alguna sentencia de terminación sea alcanzada. El loop infinito de Rust luce así:
loop {
println!("Itera por siempre!");
}
while
Rust también tiene un ciclo while . Luce de esta manera:
while !completado {
x += x - 3;
println!("{}", x);
if x % 5 == 0 {
completado = true;
}
}
Los ciclos while son la elección correcta cuando no estas seguro acerca de cuantas veces
necesitas iterar.
De necesitar un ciclo infinito, podrías sentirte tentado a escribir algo como esto:
while true {
loop {
Ciclos 162
El Lenguaje de Programacion Rust
El análisis de flujo de control de Rust trata esta construcción de manera diferente que a un
while true , debido a que sabemos que iteraremos por siempre. En general, mientras mas
for
El ciclo for es usado para iterar un numero particular de veces. Los ciclos for de Rust,
sin embargo, trabajan de manera diferente a los de otros lenguajes de programación de
sistemas. El for de Rust no luce como este ciclo for al “estilo C”
for x in 0..10 {
println!("{}", x); // x: i32
}
En nuestro ejemplo, 0..10 es una expresión que toma una posición de inicio y una de fin,
y devuelve un iterador por sobre esos valores. El limite superior es exclusivo, entonces,
nuestro loop imprimirá de 0 hasta 9 , no 10 .
Ciclos 163
El Lenguaje de Programacion Rust
Enumerate
Cuando necesitas llevar registro de cuantas veces has iterado, puedes usar la función
.enumerate() .
En rangos:
Salida:
i = 0 y j = 5
i = 1 y j = 6
i = 2 y j = 7
i = 3 y j = 8
i = 4 y j = 9
En iteradores:
Outputs:
Ciclos 164
El Lenguaje de Programacion Rust
let mut x = 5;
let mut completado = false;
while !completado {
x += x - 3;
println!("{}", x);
if x % 5 == 0 {
completado = true;
}
}
En este caso, podemos escribir el ciclo de una mejor forma con break :
let mut x = 5;
loop {
x += x - 3;
println!("{}", x);
if x % 5 == 0 { break; }
}
Ahora iteramos de manera indefinida con loop y usamos break para romper el ciclo de
manera temprana. Usar un return explícito también sirve para la terminación temprana del
ciclo.
continue es similar, pero en lugar de terminar el ciclo, nos hace ir a la siguiente iteración.
for x in 0..10 {
if x % 2 == 0 { continue; }
println!("{}", x);
}
Etiquetas loop
Ciclos 165
El Lenguaje de Programacion Rust
También podrías encontrar situaciones en las cuales tengas ciclos anidados y necesites
especificar a cual de los ciclos pertenecen tus sentencias break o continue . Como en la
mayoría de los lenguajes, por defecto un break o continue aplica a el ciclo mas interno.
En el caso de que desearas aplicar un break o continue para alguno de los ciclos
externos, puedes usar etiquetas para especificar a cual ciclo aplica la sentencia break o
continue . Lo siguiente solo imprimirá cuando ambos x y y sean impares:
Ciclos 166
El Lenguaje de Programacion Rust
% Pertenencia
Esta guía es una de las tres presentando el sistema de pertenencia de Rust. Este es una de
las características mas únicas e irresistibles de Rust, con la que desarrolladores Rust deben
estar familiarizados. A través de la pertenencia es como Rust logra su objetivo mas
importante, la seguridad. Existen unos pocos conceptos distintos, cada uno con su propio
capitulo:
Estos tres capítulos están relacionados, en orden. Necesitaras los tres para entender
completamente el sistema de pertenencia de Rust.
Meta
Antes de entrar en detalle, dos notas importantes acerca del sistema de pertenencia.
Rust tiene foco en seguridad y velocidad. Rust logra esos objetivos a travez de muchas
‘abstracciones de cero costo’, lo que significa que en Rust, las abstracciones cuestan tan
poco como sea posible para hacerlas funcionar. El sistema de pertenencia es un ejemplo
primordial de una abstracción de cero costo. Todo el análisis del que estaremos hablando
en la presente guía es llevado a cabo en tiempo de compilación. No pagas ningún costo en
tiempo de ejecución por ninguna de estas facilidades.
Sin embargo, este sistema tiene cierto costo: la curva de aprendizaje. Muchos usuarios
nuevos Rust experimentan algo que nosotros denominamos ‘pelear con el comprobador de
préstamo’ (‘fighting with the borrow checker’), situación en la cual el compilador de Rust se
rehusa a compilar un programa el cual el autor piensa valido. Esto ocurre con frecuencia
debido a que el modelo mental del programador acerca de como funciona la pertenencia no
concuerda con las reglas actuales implementadas en Rust. Probablemente tu experimentes
cosas similares al comienzo. Sin embargo, hay buenas noticias: otros desarrolladores Rust
experimentados reportan que una vez que trabajan con las reglas del sistema de
pertenencia por un periodo de tiempo, pelean cada vez menos con el comprobador de
préstamo.
Pertenencia
Pertenencia 167
El Lenguaje de Programacion Rust
Los Bindings a variable poseen una propiedad en Rust: Estos ‘poseen pertenencia’ sobre lo
que están asociados. Lo que se traduce a que cuando un binding a variable sale de ámbito,
Rust libera los recursos asociados a este. Por ejemplo:
fn foo() {
let v = vec![1, 2, 3];
}
Cuando v entra en ámbito, un nuevo Vec<T> es creado. En este caso el vector también
asigna algo de memoria desde el montículo, para los tres elementos. Cuando v sale de
ámbito al final de foo() , Rust limpiara todo lo relacionado al vector, incluyendo la memoria
asignada desde el montículo. Esto ocurre de manera determinista, al final de el ámbito.
Semantica de movimiento
Hay algo mas sutil acá, Rust se asegura que solo exista exactamente un binding a cualquier
recurso en particular. Por ejemplo, si tenemos un vector, podemos asignarlo a otro binding a
variable:
let v2 = v;
let v2 = v;
Algo similar ocurre si definimos una función que tome pertenencia, y tratamos de usar algo
después de habérselo pasado como argumento:
Pertenencia 168
El Lenguaje de Programacion Rust
fn tomar(v: Vec) {
// lo que sucede acá no es relevante
}
tomar(v);
El mismo error: ‘use of moved value’ (uso de valor movido). Cuando transferimos la
pertenencia a algo, decimos que hemos ‘movido’ la cosa a la cual nos estamos refiriendo.
No necesitas ningún tipo de anotación especial para ello, es lo que Rust hace por defecto.
Los detalles
La razón por la cual no podemos usar un binding a variable después de haberlo movido es
sutil, pero importante. Cuando escribimos código como este:
let v2 = v;
La primera linea asigna memoria para el objeto vector, v , y para la data que contiene. El
objeto vector es entonces almacenado en la pila pila y contiene un apuntador a el contenido
( [1, 2, 3] ) almacenado en el monticulo. Cuando movemos v a v2 , se crea una copia
de dicho apuntador para v2 . Todo esto significa que existirían dos apuntadores para el
contenido del vector en el montículo. Lo cual viola las garantías de seguridad de Rust,
introduciendo una condición de carrera. Es por ello que Rust prohibe el uso de v después
que el movimiento ha sido llevado a cabo.
Es importante destacar que algunas optimizaciones podrían remover la copia de los bytes
en la pila, dependiendo de ciertas circunstancias. Así que puede no ser tan ineficiente a
como luce inicialmente.
Tipos Copy
Hemos establecido que cuando transferimos la pertenencia a otro binding a variable, no
podemos usar el binding original. Sin embargo, existe un trait trait que cambia este
comportamiento, se llama Copy . No hemos discutido los traits (rasgos) todavía, pero por
Pertenencia 169
El Lenguaje de Programacion Rust
ahora puedes verlos como una anotación hecha a un tipo en particular la cual agrega
comportamiento extra. Por ejemplo:
let v = 1;
let v2 = v;
En este caso, v es un i32 , que implementa el trait Copy . Esto significa que, justo como
en un movimiento, cuando asignamos v a v2 , una copia de la data es hecha. Pero, a
diferencia de lo que ocurre en un movimiento, podemos hacer uso de v después. Esto es
debido a que un i32 no posee apuntadores a data en ningún otro lugar, copiarlos significa
copiado completo.
Todos los tipos primitivos implementan el trait Copy y su pertenencia no es movida como
uno podría asumir, siguiendo las reglas de pertenencia . Como ejemplo, los siguientes dos
pedazos de código solo compilan porque los tipos i32 y bool implementan el trait Copy .
fn main() {
let a = 5;
let _y = doblar(a);
println!("{}", a);
}
fn main() {
let a = true;
let _y = cambiar_verdad(a);
println!("{}", a);
}
De haber tenido tipos que no implementasen el trait Copy , hubiésemos obtenido un error
de compilación por tratar de usar un valor movido.
Pertenencia 170
El Lenguaje de Programacion Rust
Discutiremos como hacer que tus propios tipos sean Copy en la sección de traits
// devolviendo pertenencia
v
}
Las cosas se volverían bastante tediosas. Se pone aun peor mientras tengamos mas cosas
sobre las cuales queramos tener pertenencia:
Ugh! El tipo de retorno, la linea de retorno, y el llamado a la función se vuelven mucho mas
complicados.
Por suerte, Rust ofrece una facilidad, el préstamo, facilidad que nos sirve para solucionar
este problema.
Pertenencia 171
El Lenguaje de Programacion Rust
% Referencias y Préstamo
Esta guía es una de las tres presentando el sistema de pertenencia de Rust. Esta es una de
las características mas únicas y atractivas de Rust, con la que los desarrolladores Rust
deben estar bien familiarizados. La pertenencia es como Rust logra su objetivo mayor,
seguridad en el manejo de memoria. Existen unos pocos conceptos distintos, cada uno con
su propio capitulo:
Estos tres capítulos están relacionados, y en orden. Necesitaras leer los tres para entender
completamente el sistema de pertenencia.
Meta
Antes de entrar en detalle, dos notas importantes acerca del sistema de pertenencia.
Rust tiene foco en seguridad y velocidad. Rust logra esos objetivos a travez de muchas
‘abstracciones de cero costo’, lo que significa que en Rust, las abstracciones cuestan tan
poco como sea posible para hacerlas funcionar. El sistema de pertenencia es un ejemplo
primordial de una abstracción de cero costo. Todo el análisis del que estaremos hablando
en la presente guía es llevado a cabo en tiempo de compilación. No pagas ningún costo en
tiempo de ejecución por ninguna de estas facilidades.
Sin embargo, este sistema tiene cierto costo: la curva de aprendizaje. Muchos usuarios
nuevos Rust experimentan algo que nosotros denominamos ‘pelear con el comprobador de
préstamo’ (‘fighting with the borrow checker’), situación en la cual el compilador de Rust se
rehusa a compilar un programa el cual el autor piensa valido. Esto ocurre con frecuencia
debido a que el modelo mental del programador acerca de como funciona la pertenencia no
concuerda con las reglas actuales implementadas en Rust. Probablemente tu experimentes
cosas similares al comienzo. Sin embargo, hay buenas noticias: otros desarrolladores Rust
experimentados reportan que una vez que trabajan con las reglas del sistema de
pertenencia por un periodo de tiempo, pelean cada vez menos con el comprobador de
préstamo.
Préstamo
Al final de la sección de pertenencia, teníamos una función fea que lucia así:
// retornando la respuesta
42
}
Las referencias son inmutables, justo como los enlaces a variable. Esto se traduce a que
dentro de foo() , los vectores no pueden ser cambiados:
fn foo(v: &Vec) {
v.push(5);
}
let v = vec![];
foo(&v);
falla con:
referencias &mut
Existe un segundo tipo de referencia: &mut T . Una ‘referencia mutable’ que permite mutar
el recurso que estas tomando prestado. Por ejemplo:
let mut x = 5;
{
let y = &mut x;
*y += 1;
}
println!("{}", x);
De otro modo, las referencias &mut son como las referencias. Existe una gran diferencia
entre las dos, y como estas interactuan. Habrás notado que existe algo que no huele muy
bien en el ejemplo anterior, puesto que necesitamos ese ámbito extra, con los { y } . Si
los removemos, obtenemos un error:
}
^
Las Reglas
He aqui las reglas acerca del préstamo en Rust:
Posiblemente notes que esto es muy similar, pero no exactamente igual, a la definición de
una condición de carrera:
Hay una ‘condición de carrera’ cuando dos o mas apuntadores acceden a la misma
locación en memoria al mismo tiempo, en donde al menos uno esta escribiendo, y las
operaciones no están sincronizadas.
Con las referencias, puedes tener cuantas desees, debido a que ninguna de ellas esta
escribiendo. Si estas escribiendo, y necesitas dos o mas apuntadores a la misma memoria,
puedes tener solo un &mut a la vez. Es así como Rust previene las condiciones de carrera
en tiempo de compilación: obtendremos errores si rompemos las reglas.
Pensando en ámbitos
He aqui el código:
let mut x = 5;
let y = &mut x;
*y += 1;
println!("{}", x);
Esto es debido a que hemos violado las reglas: tenemos un &mut T apuntando a x , y en
consecuencia no tenemos permitido crear ningún &T s. Una cosa u otra. La nota apunta a
como pensar acerca de este problema:
}
^
let mut x = 5;
Los ámbitos entran en conflicto: no podemos crear un &x mientras y esta en ámbito.
let mut x = 5;
{
let y = &mut x; // -+ préstamo &mut de x comienza aqui
*y += 1; // |
} // -+ ... y termina aqui
No hay problema. Nuestro préstamo mutable sale de ámbito antes de que creemos un
préstamo inmutable. El ámbito es clave para ver cuanto dura el préstamo.
Invalidación de Iteradores
Un ejemplo es la ‘invalidación de iteradores’, que ocurre cuando tratas de mutar una
colección mientras estas iterando sobre ella. El comprobador de prestamos de Rust evita
que esto ocurra:
for i in &v {
println!("{}", i);
}
Lo anterior imprime desde uno hasta tres. A medida que iteramos los vectores, solo se nos
proporcionan referencias a sus elementos. v en si mismo es tomado prestado de manera
inmutable, lo que se traduce en que no podamos cambiarlo mientras lo iteramos:
for i in &v {
println!("{}", i);
v.push(34);
}
He aqui el error:
let y: &i32;
{
let x = 5;
y = &x;
}
println!("{}", y);
note: ...but borrowed value is only valid for the block suffix following
statement 0 at 4:18
let x = 5;
y = &x;
}
En otras palabras, y es valido solo para el ámbito en donde x existe. Tan pronto como
x se va, se hace invalido hacerle referencia. Es por ello que el error dice que el préstamo,
‘no vive lo suficiente’ (‘doesn’t live long enough’) ya que no es valido por la cantidad de
tiempo correcta.
let y: &i32;
let x = 5;
y = &x;
println!("{}", y);
println!("{}", y);
}
note: ...but borrowed value is only valid for the block suffix following
statement 1 at 3:14
let x = 5;
y = &x;
println!("{}", y);
}
En el ejemplo anterior, y es declarada antes que x , significando que y vive mas que
x , lo cual no esta permitido.
% Tiempos de Vida
Esta guía es una de las tres presentando el sistema de pertenencia de Rust. Esta es una de
las características mas únicas y atractivas de Rust, con la que los desarrolladores Rust
deben estar bien familiarizados. La pertenencia es como Rust logra su objetivo mayor,
seguridad en el manejo de memoria. Existen unos pocos conceptos distintos, cada uno con
su propio capitulo:
Estos tres capítulos están relacionados, y en orden. Necesitaras leer los tres para entender
completamente el sistema de pertenencia.
Meta
Antes de entrar en detalle, dos notas importantes acerca del sistema de pertenencia.
Rust tiene foco en seguridad y velocidad. Rust logra esos objetivos a travez de muchas
‘abstracciones de cero costo’, lo que significa que en Rust, las abstracciones cuestan tan
poco como sea posible para hacerlas funcionar. El sistema de pertenencia es un ejemplo
primordial de una abstracción de cero costo. Todo el análisis del que estaremos hablando
en la presente guía es llevado a cabo en tiempo de compilación. No pagas ningún costo en
tiempo de ejecución por ninguna de estas facilidades.
Sin embargo, este sistema tiene cierto costo: la curva de aprendizaje. Muchos usuarios
nuevos Rust experimentan algo que nosotros denominamos ‘pelear con el comprobador de
préstamo’ (‘fighting with the borrow checker’), situación en la cual el compilador de Rust se
rehusa a compilar un programa el cual el autor piensa valido. Esto ocurre con frecuencia
debido a que el modelo mental del programador acerca de como funciona la pertenencia no
concuerda con las reglas actuales implementadas en Rust. Probablemente tu experimentes
cosas similares al comienzo. Sin embargo, hay buenas noticias: otros desarrolladores Rust
experimentados reportan que una vez que trabajan con las reglas del sistema de
pertenencia por un periodo de tiempo, pelean cada vez menos con el comprobador de
préstamo.
Tiempos de vida
Prestar una referencia a otro recurso del que alguien mas es dueño puede ser complicado.
Por ejemplo, imagina este conjunto de operaciones:
Para arreglar esto, tenemos que asegurarnos que el paso cuatro nunca ocurra después del
paso tres. El sistema de pertenencia de Rust lleva esto a cabo a través de un concepto
denominado tiempos de vida, los cuales describen el ámbito en el cual una referencia es
valida.
Cuando tenemos una función que toma una referencia como argumento, podemos ser
implícitos o explícitos acerca del tiempo de vida de la referencia:
// implicito
fn foo(x: &i32) {
}
// explicito
fn bar<'a>(x: &'a i32) {
}
El 'a se lee ‘el tiempo de vida a’. Técnicamente, toda referencia posee un tiempo de vida
asociado a ella, pero el compilador te permite omitirlas en casos comunes. Antes que
lleguemos a eso, analicemos el pedazo de código explícito:
fn bar<'a>(...)
entre los <> s, y los tiempos de vida son un tipo de parámetro genérico. Discutiremos otros
tipos de genericos mas tarde en el libro, pero por ahora, enfoquémonos solo en el aspecto
de los tiempos de vida.
Usamos <> para declarar nuestros tiempos de vida. Esto dice que bar posee un tiempo
de vida, 'a . De haber tenido referencias como parámetros, hubiese lucido de esta
manera:
fn bar<'a, 'b="">(...)
Entonces en nuestra lista de parámetros, usamos los tiempos de vida que hemos
nombrado:
Si comparas &mut i32 con &'a mut i32 , son lo mismo, es solo que el tiempo de vida 'a
se ha metido entre el & y el mut i32 . Leemos &mut i32 como ‘una referencia mutable a
un i32 ’ y &'a mut i32 como ‘una referencia mutable a un i32 con el tiempo de vida
'a ’.
En struct s
También necesitaras tiempos de vida explícitos cuando trabajes con struct s:
struct Foo<'a> {
x: &'a i32,
}
fn main() {
let y = &5; // esto es lo mismo que `let _y = 5; let y = &_y;`
let f = Foo { x: y };
println!("{}", f.x);
}
Como puedes ver, los struct s pueden también tener tiempos de vida. En una forma
similar a las funciones,
struct Foo<'a> {
# x: &'a i32,
# }
# struct Foo<'a> {
x: &'a i32,
# }
hace uso de el. Entonces, porque necesitamos un tiempo de vida aquí? Necesitamos
asegurarnos que cualquier referencia a un Foo no pueda vivir mas que la referencia a un
i32 que este contiene.
bloques impl
Implementemos un metodo en Foo :
struct Foo<'a> {
x: &'a i32,
}
impl<'a> Foo<'a> {
fn x(&self) -> &'a i32 { self.x }
}
fn main() {
let y = &5; // esto es lo mismo que `let _y = 5; let y = &_y;`
let f = Foo { x: y };
Como puedes ver, necesitamos declarar un tiempo de vida para Foo en la linea impl .
Repetimos 'a dos veces, justo como en funciones: impl<'a> define un tiempo de vida
'a , y Foo<'a> hace uso de el.
Lo anterior dice que ambos x y y viven por el mismo ámbito, y que el valor de retorno
también esta vivo para dicho ámbito. Si hubieses querido que x y y tuviesen diferentes
tiempos de vida, pudiste haber hecho uso de multiples parámetros de tiempos de vida:
En este ejemplo, x y y tienen diferentes ámbitos validos, pero el valor de retorno tiene el
mismo tiempo de vida que x .
Pensando en ámbitos
Una forma de pensar acerca de los tiempos de vida es visualizar el ámbito en el cual es
valida una referencia. Por ejemplo:
fn main() {
let y = &5; // -+ y entra en ambito
// |
// stuff // |
// |
} // -+ y sale de ambito
struct Foo<'a> {
x: &'a i32,
}
fn main() {
let y = &5; // -+ y entra en ambito
let f = Foo { x: y }; // -+ f entra en ambito
// stuff // |
// |
} // -+ f y y salen de ambito
Nuestro f vive dentro de el ámbito de y , es por ello que todo funciona. Que pasaría de lo
contrario? El siguiente código no funcionaria:
struct Foo<'a> {
x: &'a i32,
}
fn main() {
let x; // -+ x entra en ambito
// |
{ // |
let y = &5; // ---+ y entra en ambito
let f = Foo { x: y }; // ---+ f entra en ambito
x = &f.x; // | | error aqui
} // ---+ f y y salen de ambito
// |
println!("{}", x); // |
} // -+ x sale de ambito
Uff! Como puedes ver aqui, los ámbitos de f y y son menores que el ámbito de x . Pero
cuando hacemos x = &f.x , hacemos a x una referencia a algo que estar por salir de
ámbito.
Los tiempos de vida con nombre son una forma de darles a dichos ámbitos un nombre.
Darle un nombre a algo es el primer paso hacia poder hablar acerca de el.
'static
El tiempo de vida denominado ‘static’ es un tiempo de vida especial. Este señala que algo
posee el tiempo de vida de el programa completo. La mayoría de los desarrolladores Rust
conocen a 'static cuando lidian con cadenas de caracteres:
Los literales de cadenas de caracteres poseen el tipo &'static str puesto que la
referencia esta siempre viva: estos son colocados en el segmento de datos del binario final.
Otro ejemplo son las globales:
Rust soporta una inferencia de tipos poderosa en los cuerpos de función, pero esta
prohibido en las firmas de elementos permitir razonamiento basado únicamente en la firma.
Sin embargo, por razones ergonomicas, una inferencia secundaria muy restricta llamada
“elision de tiempos de vida” se aplica en las firmas de función. La “elision de tiempos de
vida” infiere basandose solo en los componentes de la firma sin basarse en el cuerpo de la
función, unicamnete infiere parámetros de tiempos de vida, y hace esto con solo tres reglas
fácilmente memorizables e inambiguas. Todo esto hace a la elision de tiempos de vida un
atajo para escribir una firma, sin necesidad de ocultar los tipos involucrados puesto a que
inferencia local completa sera aplicada a ellos.
Si existe exactamente un solo tiempo de vida de entrada, elidido o no, ese tiempo de
vida es asignado a todos los tiempos de vida elididos en los valores de retorno de esa
función.
Si existen multiples tiempos de vida de entrada, pero una de ellos es &self o &mut
self , el tiempo de vida de self es asignado a todos los tiempos de vida de salida
elididos.
Ejemplos
He aqui algunos ejemplos de funciones con tiempos de vida elididos. Hemos pareado cada
ejemplo de un tiempo de vida elidido con su forma expandida.
// En el ejemplo anterior, `lvl` no necesita un tiempo de vida debido a que no es una referencia (`&`
% Mutabilidad
Mutabilidad, es la habilidad que una cosa posee para ser cambiada, funciona un poco
diferente en Rust que en otros lenguajes. El primer aspecto de la mutabilidad es que no
esta habilitada por defecto:
let x = 5;
x = 6; // error!
let mut x = 5;
x = 6; // no hay problema!
Esto es un enlace a variable mutable. Cuando un enlace a variable es mutable, significa que
tienes permitido cambiar a lo que el enlace apunta. Entonces, en el ejemplo anterior, no
esta cambiando el valor en x , en cambio, el enlace cambio de un i32 a otro.
Si deseas cambiar a que apunta el enlace a variable, necesitaras una referencia mutable:
let mut x = 5;
let y = &mut x;
puedes asociar y a otra cosa ( y = &mut z ), pero puedes mutar lo que sea a lo que y
esta asociado ( *y = 5 ). Una diferencia muy sutil.
let mut x = 5;
let mut y = &mut x;
Ahora y puede ser asociado a otro valor, y el valor que esta referenciando puede ser
cambiado.
Es importante notar que mut es parte de un patron, de manera tal que puedas hacer cosas
como:
fn foo(mut x: i32) {
# }
Mutabilidad 189
El Lenguaje de Programacion Rust
use std::sync::Arc;
let x = Arc::new(5);
let y = x.clone();
Para entender esto, debemos volver al núcleo de la filosofía que guía a Rust, seguridad en
el manejo de memoria, y el mecanismo a través del cual Rust la garantiza, el sistema de
pertenencia, y mas específicamente, el préstamo:
Puedes tener uno u otro de estos dos tipos de prestamo, pero no los dos al mismo
tiempo:
Otros tipos como los del modulo std::cell , poseen lo opuesto: mutabilidad interior. Por
ejemplo:
use std::cell::RefCell;
let x = RefCell::new(42);
let y = x.borrow_mut();
Mutabilidad 190
El Lenguaje de Programacion Rust
use std::cell::RefCell;
let x = RefCell::new(42);
let y = x.borrow_mut();
let z = x.borrow_mut();
# (y, z);
Esto, en efecto, hará pánico en tiempo de ejecución. Esto es lo que RefCell hace: aplica
las reglas de préstamo de Rust en tiempo de ejecución, y hace panic! os si dichas reglas
son violadas. Lo anterior nos permite acercarnos a otro aspecto de las reglas de mutabilidad
de Rust. Hablemos acerca de ello primero.
struct Punto {
x: i32,
mut y: i32, // nope
}
struct Punto {
x: i32,
y: i32,
}
a.x = 10;
Mutabilidad 191
El Lenguaje de Programacion Rust
use std::cell::Cell;
struct Punto {
x: i32,
y: Cell<i32>,
}
punto.y.set(7);
Mutabilidad 192
El Lenguaje de Programacion Rust
% Estructuras (Structs)
Las estructuras ( struct s) son una forma de crear tipos de datos mas complejos. Por
ejemplo, si estuviéramos haciendo cálculos que involucraran coordenadas en un espacio
2D, necesitaríamos ambos, un valor x y un valor y :
let origen_x = 0;
let origen_y = 0;
una struct nos permite combinar ambos en un único tipo de datos unificado:
struct Punto {
x: i32,
y: i32,
}
fn main() {
let origen = Punto { x: 0, y: 0 }; // origin: Punto
Hay muchas cosas pasando acá, así que analicémoslo por partes. Declaramos una
estructura con la palabra reservada struct , seguida de un nombre. Por convención, los
struct s comienzan con una letra mayúscula y son camel cased: PuntoEnElEspacio , no
Punto_En_El_Espacio .
Podemos crear una instancia de nuestra struct via let , como es usual, pero usamos
una sintaxis estilo clave: valor para asignar cada campo. El order no necesita ser el
mismo que en la declaración original.
Finalmente, debido a que tenemos nombres de campos, podemos acceder a ellos a través
de la notación de puntos: origen.x .
Los valores en struct s son inmutables por defecto, justo como los demás enlaces a
variables en Rust. Usa mut para hacerlos mutables:
Estructuras 193
El Lenguaje de Programacion Rust
struct Punto {
x: i32,
y: i32,
}
fn main() {
let mut punto = Punto { x: 0, y: 0 };
punto.x = 5;
Rust no soporta mutabilidad de campos a nivel de lenguaje, es por ello que no puedes
escribir algo como esto:
struct Punto {
mut x: i32,
y: i32,
}
struct Punto {
x: i32,
y: i32,
}
fn main() {
let mut punto = Punto { x: 0, y: 0 };
punto.x = 5;
let punto = punto; // ahora, este nuevo enlace a variable no puede ser cambiado
Sintaxis de actualización
Estructuras 194
El Lenguaje de Programacion Rust
Una struct puede incluir .. para indicar que deseas usar una copia de algún otro
struct para algunos de los valores. Por ejemplo:
struct Punto3d {
x: i32,
y: i32,
z: i32,
}
# struct Punto3d {
# x: i32,
# y: i32,
# z: i32,
# }
let origen = Punto3d { x: 0, y: 0, z: 0 };
let punto = Punto3d { z: 1, x: 2, .. origen };
Las dos anteriores no serán iguales, incluso si poseen los mismos valores:
Casi siempre es mejor usar una struct que una tuple struct . En su lugar, podríamos
escribir Color and Punto así:
Estructuras 195
El Lenguaje de Programacion Rust
struct Color {
rojo: i32,
azul: i32,
verde: i32,
}
struct Punto {
x: i32,
y: i32,
z: i32,
}
Ahora, tenemos nombres, en lugar de posiciones. Los buenos nombres son importantes, y
con una struct , tenemos nombres.
Hay un caso en el cual una tupla estructura es muy útil, y es una tupla estructura con un
solo elemento. Se denomina patrón nuevotipo ( newtype ), puesto a que crear un nuevo
tipo, distinto a el valor que contiene, expresando su propia semántica:
struct Pulgadas(i32);
Como notaras, puedes extraer el entero contenido con un let de destructuración, justo
como en las tuplas regulares. En este caso, el let Pulgadas(longitud_enteros) asigna 10
a longitud_enteros .
struct Electron;
Lo anterior es raramente util en si mismo (aunque algunas veces puede servir como un tipo
marcador), pero en combinación con otras características, puede tornarse útil. Por ejemplo,
una librería puede requerir crear una estructura que implementa cierto trait para manejar
Estructuras 196
El Lenguaje de Programacion Rust
Estructuras 197
El Lenguaje de Programacion Rust
% Enumeraciones
Una enumeración ( enum ) en Rust es un tipo que representa data que puede ser una de un
conjunto de variantes posible:
enum Mensaje {
Salir,
CambiarColor(i32, i32, i32),
Mover { x: i32, y: i32 },
Escribir(String),
}
Cada variante puede opcionalmente tener data asociada. La sintaxis para la definición de
variantes es semejante a la sintaxis usada para definir estructuras ( struct s): puedes tener
variantes sin datos (como las estructuras tipo-unitario), variantes con data nombrada, y
variantes con data sin nombres (como tupla estructuras). Sin embargo, a diferencia de las
definiciones de estructuras, un enum es un único tipo. Un valor de un enum puede coincidir
con cualquiera de las variantes. Por esta razón, un enum es denominado algunas veces un
‘tipo suma’ (‘sum type’): el conjunto de valores posibles del enum es la suma de los
conjuntos de valores posibles para cada variante.
Utilizamos la sintaxis :: para hacer uso de cada variante: las variantes están dentro del
ámbito del enum . Lo que hace que lo siguiente sea valido:
# enum Mensaje {
# Mover { x: i32, y: i32 },
# }
let x: Mensaje = Mensaje::Mover { x: 3, y: 4 };
enum TurnoJuegoMesa {
Mover { celdas: i32 },
Pasar,
}
Ambas variantes poseen el nombre Mover , pero debido a que su ámbito es dentro del
nombre del enum, ambas pueden ser usadas sin conflicto.
Un valor de un tipo enum contiene información acerca de cual variante es, en adición a
cualquier data asociada con dicha variante. Esto es algunas veces denominado ‘union
etiquetada’ (‘tagged union’), puesto a que la data incluye una ‘etiqueta’ (‘tag’) que indica que
tipo es. El compilador usa esta información para asegurarse que estemos accediendo a la
data en el enum de manera segura. Por ejemplo, no puedes simplemente tratar de
destructurar un valor como si este fuere una de las posibles variantes:
Enumeraciones 198
El Lenguaje de Programacion Rust
fn procesar_cambio_color(msj: Mensaje) {
let Mensaje::CambiarColor(r, v, a) = msj; // compile-time error
}
No soportar este tipo de operaciones puede lucir un poco limitante, pero es una limitación
que podemos superar. Existen dos maneras, implementando la igualdad por nuestra cuenta,
o a través de el pattern matching con expresiones match , que aprenderás en la siguiente
sección. Todavía no sabemos lo suficiente de Rust para implementar la igualdad por nuestra
cuenta, pero lo veremos en la sección traits .
# enum Mensaje {
# Escribir(String),
# }
let m = Mensaje::Escribir("Hola, mundo".to_string());
Es lo mismo que
# enum Mensaje {
# Escribir(String),
# }
fn foo(x: String) -> Mensaje {
Mensaje::Escribir(x)
}
Esto no es inmediatamente util para nosotros, pero cuando lleguemos a los closures ,
hablaremos acerca de pasar funciones como argumentos a otras funciones. Por ejemplo,
con los iteradores, podemos convertir un vector de String s en un vector de
Message::Escribir s:
# enum Mensaje {
# Escribir(String),
# }
Enumeraciones 199
El Lenguaje de Programacion Rust
Enumeraciones 200
El Lenguaje de Programacion Rust
% Match
A menudo, un simple if / else no es suficiente, debido a que tienes mas de dos opciones
posibles. También, las condiciones pueden ser complejas. Rust posee una palabra
reservada, match , que te permite reemplazar construcciones if / else complicadas con
algo mas poderoso. Echa un vistazo:
let x = 5;
match x {
1 => println!("uno"),
2 => println!("dos"),
3 => println!("trees"),
4 => println!("cuatro"),
5 => println!("cinco"),
_ => println!("otra cosa"),
}
match toma una expresión y luego bifurca basado en su valor. Cada brazo de la rama
tiene la forma valor => expresión . Cuando el valor coincide, la expresión del brazo es
evaluada. Es llamado match por el termino ‘pattern matching’, del cual match es una
implementación. Hay una sección entera acerca de los patrones que cubre todos los
patrones posibles.
Entonces, cual es la gran ventaja? Bueno, hay unas pocas. Primero que todo, match
impone chequeo de agotamiento (‘exhaustiveness checking’). Ves el ultimo brazo, el que
posee el guión bajo ( _ )? Si removemos ese brazo, Rust nos proporcionara un error:
En otras palabras, Rust esta tratando de decirnos que olvidamos un valor. Debido a que x
es un entero, Rust sabe que x puede tener un numero de valores diferentes - por ejemplo,
6 . Sin el _ , sin embargo, no hay brazo que pueda coincidir, y en consecuencia Rust se
rehusa a compilar el código. _ actúa como un ‘brazo que atrapa todo’. Si ninguno de los
otros brazos coincide, el brazo con el _ lo hará, y puesto que tenemos dicho ‘brazo que
atrapa todo’, tenemos ahora un brazo para cada valor posible de x , y como resultado,
nuestro programa compilara satisfactoriamente.
match es también una expresión, lo que significa que lo podemos usar en el lado derecho
Match 201
El Lenguaje de Programacion Rust
let x = 5;
enum Mensaje {
Salir,
CambiarColor(i32, i32, i32),
Mover { x: i32, y: i32 },
Escribir(String),
}
fn salir() { /* ... */ }
fn cambiar_color(r: i32, g: i32, b: i32) { /* ... */ }
fn mover_cursor(x: i32, y: i32) { /* ... */ }
fn procesar_mensaje(msj: Mensaje) {
match msj {
Mensaje::Salir => salir(),
Mensaje::CambiarColor(r, g, b) => cambiar_color(r, g, b),
Mensaje::Mover { x: x, y: y } => mover_cursor(x, y),
Mensaje::Escribir(s) => println!("{}", s),
};
}
Otra vez, el compilador de Rust chequea agotamiento, demandando que tengas un brazo
para cada variante de la enumeración. Si dejas uno por fuera, Rust generara un error en
tiempo de compilación, a menos que uses _ .
A diferencia de los usos previos de match , no puedes usar de la sentencia if para hacer
esto. Puedes hacer uso de una sentencia if let que puede ser vista como una forma
abreviada de match .
Match 202
El Lenguaje de Programacion Rust
% Patrones
Los patrones son bastante comunes en Rust. Los usamos en enlaces a variable, sentencias
match, y otros casos. Embarquemonos en un tour torbellino por todas las cosas que los
patrones son capaces de hacer!
Un repaso rápido: puedes probar patrones contra literales directamente, y _ actúa como
un caso cualquiera :
let x = 1;
match x {
1 => println!("uno"),
2 => println!("dos"),
3 => println!("tres"),
_ => println!("cualquiera"),
}
Imprime uno .
Multiples patrones
Puedes probar multiples patrones con | :
let x = 1;
match x {
1 | 2 => println!("uno o dos"),
3 => println!("tres"),
_ => println!("cualquiera"),
}
Destructuracion
Si posees un tipo de datos compuesto, como un struct , puedes destructurarlo dentro de
un patron:
Patrones 203
El Lenguaje de Programacion Rust
struct Punto {
x: i32,
y: i32,
}
match origen {
Punto { x, y } => println!("({},{})", x, y),
}
struct Punto {
x: i32,
y: i32,
}
match origen {
Punto { x: x1, y: y1 } => println!("({},{})", x1, y1),
}
Si solo nos importan algunos valores, no tenemos que darle nombres a todos:
struct Punto {
x: i32,
y: i32,
}
match origen {
Punto { x, .. } => println!("x es {}", x),
}
Esto imprime x es 0 .
Patrones 204
El Lenguaje de Programacion Rust
struct Punto {
x: i32,
y: i32,
}
match origen {
Punto { y, .. } => println!("y es {}", y),
}
Lo anterior imprime y es 0 .
_ es valido en cualquier patron que cree un enlace a variable. También puede ser util para
Patrones 205
El Lenguaje de Programacion Rust
enum TuplaOpcional {
Valor(i32, i32, i32),
Faltante,
}
match x {
TuplaOpcional::Valor(..) => println!("Tupla obtenida!"),
TuplaOpcional::Faltante => println!("Sin suerte."),
}
let x = 5;
match x {
ref r => println!("Referencia a {} obtenida", r),
}
Acá, la r dentro del match posee el tipo &i32 . En otras palabras la palabra reservada
ref crea una referencia, para ser usada dentro del patrón. Si necesitas una referencia
let mut x = 5;
match x {
ref mut rm => println!("Referencia mutable a {} obtenida", rm),
}
Rangos
Puedes probar un rango de valors con ... :
Patrones 206
El Lenguaje de Programacion Rust
let x = 1;
match x {
1 ... 5 => println!("uno al cinco"),
_ => println!("cualquier cosa"),
}
let x = '';
match x {
'a' ... 'j' => println!("letra temprana"),
'k' ... 'z' => println!("letra tardia"),
_ => println!("algo mas"),
}
Enlaces a variable
Puedes asociar valores a nombres con @ :
let x = 1;
match x {
e @ 1 ... 5 => println!("valor de rango {} obtenido", e),
_ => println!("lo que sea"),
}
This prints valor de rango 1 obtenido . Lo anterior es util cuando desees hacer un match
complicado a una parte de una estructura de datos:
Patrones 207
El Lenguaje de Programacion Rust
#[derive(Debug)]
struct Persona {
nombre: Option<String>,
}
Si usas @ con | , necesitas asegurarte de que el nombre sea asociado en cada parte del
patron:
let x = 5;
match x {
e @ 1 ... 5 | e @ 8 ... 10 => println!("valor de rango {} obtenido", e),
_ => println!("lo que sea"),
}
Guardias
Puedes introducir guardias match (‘match guards’) con if :
enum EnteroOpcional {
Valor(i32),
Faltante,
}
let x = EnteroOpcional::Value(5);
match x {
EnteroOpcional::Valor(i) if i > 5 => println!("Entero mayor a cinco obtenido!"),
EnteroOpcional::Valor(..) => println!("Entero obtenido!"),
EnteroOpcional::Faltante => println!("Sin suerte."),
}
Patrones 208
El Lenguaje de Programacion Rust
let x = 4;
let y = false;
match x {
4 | 5 if y => println!("si"),
_ => println!("no"),
}
(4 | 5) if y => ...
y no así:
4 | (5 if y) => ...
Mezcla y Match
Uff! Eso fue un montón de formas diferentes para probar cosas, y todas pueden ser
mezcladas y probadas, dependiendo de los que estés haciendo:
match x {
Foo { x: Some(ref nombre), y: None } => ...
}
Patrones 209
El Lenguaje de Programacion Rust
% Sintaxis de Métodos
Las funciones son geniales, pero si deseas llamar bastantes en alguna data, puede tornarse
incomodo. Considera este código:
baz(bar(foo));
Pudiéramos leer esto de izquierda a derecha, veríamos entonces ‘baz bar foo’. Pero ese no
es el orden en el cual las funciones son llamadas, el orden de hecho, es de adentro hacia
afuera: ‘foo bar baz’. No seria excelente si pudiéramos hacerlo:
foo.bar().baz();
Por suerte, como habrás podido deducir de la pregunta anterior, por supuesto que
podemos! Rust provee la habilidad de usar la denominada ‘sintaxis de llamada a métodos’
(‘method call syntax’) a través de la palabra reservada impl .
Llamadas a métodos
Así funcionan:
struct Circulo {
x: f64,
y: f64,
radio: f64,
}
impl Circulo {
fn area(&self) -> f64 {
std::f64::consts::PI * (self.radio * self.radio)
}
}
fn main() {
let c = Circulo { x: 0.0, y: 0.0, radio: 2.0 };
println!("{}", c.area());
}
Los métodos reciben un primer parámetro especial, del cual existen tres variantes: self ,
&self , y &mut self . Puedes pensar acerca de este primer parámetro como el foo en
foo.bar() . Las tres variantes corresponden a los tres tipos de cosas que foo podría ser:
self si es solo un valor en la pila, &self si es una referencia y &mut self si es una
Deberíamos usar por defecto &self , así como preferir también el préstamo por encima de
la toma de pertenencia y recibir referencias inmutables en vez de mutables, en lo posible.
He aquí un ejemplo de las tres variantes:
struct Circulo {
x: f64,
y: f64,
radio: f64,
}
impl Circle {
fn referencia(&self) {
println!("recibiendo a self como una referencia!");
}
fn referencia_mutable(&mut self) {
println!("trecibiendo a self como una referencia mutable!");
}
fn toma_pertenecia(self) {
println!("tomando pertenencia de self!");
}
}
struct Circulo {
x: f64,
y: f64,
radio: f64,
}
impl Circulo {
fn area(&self) -> f64 {
std::f64::consts::PI * (self.radio * self.radio)
}
fn main() {
let c = Circulo { x: 0.0, y: 0.0, radio: 2.0 };
println!("{}", c.area());
let d = c.agrandar(2.0).area();
println!("{}", d);
}
# struct Circulo;
# impl Circulo {
fn agrandar(&self, incremento: f64) -> Circulo {
# Circulo } }
Solo decimos que retornamos un Circulo . Con este método, podemos agrandar un nuevo
Circulo a un tamaño arbitrario.
Funciones asociadas
Puedes también definir funciones asociadas que no tomen el parámetro self . He aquí un
patron muy común en código Rust:
struct Circulo {
x: f64,
y: f64,
radio: f64,
}
impl Circulo {
fn new(x: f64, y: f64, radio: f64) -> Circulo {
Circulo {
x: x,
y: y,
radio: radio,
}
}
}
fn main() {
let c = Circulo::new(0.0, 0.0, 2.0);
}
Esta ‘función asociada’ construye un nuevo Circulo . Nota que las funciones asociadas son
llamadas con la sintaxis Struct::funcion() , en lugar de ref.metodo() . Otros lenguajes
llaman a las funciones asociadas ‘métodos estáticos’
struct Circulo {
x: f64,
y: f64,
radio: f64,
}
impl Circulo {
fn area(&self) -> f64 {
std::f64::consts::PI * (self.radio * self.radio)
}
}
struct ConstructorCirculo {
x: f64,
y: f64,
radio: f64,
}
impl ConstructorCirculo {
fn new() -> ConstructorCirculo {
ConstructorCirculo { x: 0.0, y: 0.0, radio: 1.0, }
}
fn main() {
let c = ConstructorCirculo::new()
.x(1.0)
.y(2.0)
.radio(2.0)
.finalizar();
Lo que hemos hecho es crear otra struct , ConstructorCirculo . Hemos definido nuestros
métodos constructores en ella. También hemos definido nuestro método area() en
Circulo . Hemos agregado otro método en ConstructorCirculo : finalizar() . Este
método crea nuestro Circulo desde el constructor. Ahora, hemos usado el sistema de
tipos para hacer valer nuestros intereses: podemos usar los métodos en
ConstructorCirculo para crear Circulo s en la forma que decidamos.
% Vectores
capitulo de [genéricos][generics] para mas información). Los vectores siempre alojan sus
datos en el montículo. Puedes crear vectores con la macro vec! :
(Nota que a diferencia de la macro println! que hemos usado en el pasado, usamos
corchetes [] con la macro vec! . Rust te permite usar cualquiera en cualquier situación,
en esta oportunidad es por pura convención)
Accediendo a elementos
Para obtener el valor en un indice en particular del vector, usamos [] s:
Iterando
Una vez poseas un vector, puedes iterar a través de sus elementos con for . Hay tres
versiones:
Vectores 215
El Lenguaje de Programacion Rust
for i in &v {
println!("Una referencia a {}", i);
}
for i in &mut v {
println!("Una referencia mutable a {}", i);
}
for i in v {
println!("Tomando pertenencia del vector y su elemento {}", i);
}
Los vectores poseen muchos otros métodos útiles, acerca de los cuales puedes leer en su
documentation
Vectores 216
El Lenguaje de Programacion Rust
% Cadenas de Caracteres
Entremos en los detalles. Una ‘cadena de caracteres’ (‘string’) es una secuencia de valores
escalares Unicode codificada como un flujo de bytes UTF-8. Todas las cadenas de
caracteres están garantizadas a ser una codificación valida de secuencias UTF-8.
Adicionalmente, y a diferencia de otros lenguajes de sistemas, las cadenas de caracteres
no son terminadas en null y pueden contener bytes null.
Rust posee dos tipos principales de cadenas de caracteres: &str y String . Hablemos
primero acerca de &str . Estos son denominados ‘pedazos de cadenas de caracteres’
(‘string slices’). Los literales String son del tipo &'static str`:
Por otro lado, un String , es una cadena de caracteres asignada desde el montículo. Dicha
cadena puede crecer, y también esta garantizada ser UTF-8. Los String son creados
comúnmente a través de la conversión de un pedazo de cadena de carácter usando el
método to_string .
s.push_str(", mundo.");
println!("{}", s);
fn recibe_pedazo(pedazo: &str) {
println!("Recibí: {}", pedazo);
}
fn main() {
let s = "Hola".to_string();
recibe_pedazo(&s);
}
Esta coerción no ocurre para las funciones que aceptan uno de los traits &str ’s en lugar
de &str . Por ejemplo, TcpStream::connect posee un parámetro de tipo ToSocketAddrs . Un
&str esta bien pero un String debe ser explícitamente convertido usando &* .
use std::net::TcpStream;
Ver un String como un &str es barato, pero convertir el &str a un String involucra
asignación de memoria. No hay razón para hacer eso a menos que sea necesario!
Indexado
Debido a que las cadenas de caracteres son UTF-8 validos, no soportan indexado:
let s = "hola";
Usualmente, el acceso a un vector con [] es muy rápido. Pero, puesto a que cada
carácter codificado en una cadena UTF-8 puede tener multiples bytes, debes recorrer toda
la cadena para encontrar la nᵗʰ letra de una cadena. Esta es una operación
significativamente mas cara, y no queremos generar confusiones. Incluso, ‘letra’ no es
exactamente algo definido en Unicode. Podemos escoger ver a una cadena de caracteres
como bytes individuales, o como codepoints:
for b in hachiko.as_bytes() {
print!("{}, ", b);
}
println!("");
for c in hachiko.chars() {
print!("{}, ", c);
}
println!("");
Lo anterior imprime:
229, 191, 160, 231, 138, 172, 227, 131, 143, 227, 131, 129, 229, 133, 172,
忠, 犬, ハ, チ, 公,
Como puedes ver, hay mas bytes que caracteres ( char s).
Esto enfatiza que tenemos que caminar desde el principio de la lista de chars .
Cortado (Slicing)
Puedes obtener un pedazo de una cadena de caracteres con la sintaxis de cortado:
thread '
' panicked at 'index 0 and/or 2 in `忠犬ハチ公` do not lie on
character boundary'
Concatenación
Si posees un String , puedes concatenarle un &str al final:
Esto es porque &String puede hacer coercion automática a un &str . Esta característica
es denominada ‘coerciones Deref ’.
% Genéricos
Algunas veces, cuando se escribe una función o una estructura de datos, podríamos desear
que esta pudiese funcionar con multiples tipos de argumentos. En Rust podemos lograr esto
a través de los genéricos. Los genéricos son llamados ‘polimorfismo parametrico’ en la
teoría de tipos, lo que significa que son tipos o funciones que poseen multiples formas
(‘poly’ de multiple, ‘morph’ de forma) sobre un determinado parámetro (‘parametrico’).
enum Option<T> {
Some(T),
None,
}
La parte <T> , la cual has visto unas cuantas veces anteriormente, indica que este es un
tipo de datos genérico. Dentro de la declaración de nuestro enum, donde sea que veamos
una T sustituimos ese tipo por el mismo tipo usado en el genérico. He aquí un ejemplo del
uso de Option<T> , con algunos anotaciones de tipo extra:
En la declaración del tipo, decimos Option<i32> . Nota cuan similar esto luce esto a
Option<T> . Entonces, en este Option , T posee el valor de i32 . En el lado derecho del
Eso no significa que no podamos crear Option<T> s que contengan un f64 . Simplemente
deben coincidir:
Los genéricos no deben necesariamente ser genéricos sobre un solo tipo. Considera otro
tipo similar en la biblioteca estándar de Rust, Result<T, E> :
Genéricos 221
El Lenguaje de Programacion Rust
Este tipo es genérico sobre dos tipos: T y E . Por cierto, las letras mayusculas pueden ser
cualquiera. Pudimos haber definido Result<T, E> como:
de haber querido. La convención dice que el primer parametro generico debe ser T , de
‘tipo’, y que usemos E para ‘error’. A Rust, sin embargo no le importa.
El tipo Result<T, E> se usa para retornar el resultado de una computación, con la
posibilidad de retornar un error en caso de que dicha computación no haya sido exitosa.
Funciones genéricas
Podemos escribir funciones que tomen tipos genéricos con una sintaxis similar:
fn recibe_cualquier_cosa<T>(x: T) {
// hacer algo con x
}
La sintaxis posee dos partes: el <T> dice “esta función es generica sobre un tipo, T ”, y la
parte x: T dice “x posee el tipo T .”
fn recibe_dos_cosas_del_mismo_tipo<T>(x: T, y: T) {
// ...
}
fn recibe_dos_cosas_de_distintos_tipos<T, U>(x: T, y: U) {
// ...
}
Genéricos 222
El Lenguaje de Programacion Rust
Structs genericos
También puedes almacenar un tipo genérico en una estructura:
struct Punto<T> {
x: T,
y: T,
}
# struct Punto<T> {
# x: T,
# y: T,
# }
#
impl<T> Punto<T> {
fn intercambiar(&mut self) {
std::mem::swap(&mut self.x, &mut self.y);
}
}
Hasta ahora solo has visto genéricos que aceptan absolutamente cualquier tipo. Estas son
útiles en muchos casos, ya has visto Option<T> , y mas tarde conoceras contenedores
universales como Vec<T> . Por otro lado, a veces querrás intercambiar esa flexibilidad por
mayor poder expresivo. Lee acerca de limites trait para ver porque y como.
Genéricos 223
El Lenguaje de Programacion Rust
% Traits
Un trait es una facilidad del lenguaje que le indica compilador de Rust acerca de la
funcionalidad que un tipo debe proveer.
Recuerdas la palabra reservada impl ?, usada para llamar a una función con la sintaxis de
métodos?
struct Circulo {
x: f64,
y: f64,
radio: f64,
}
impl Circulo {
fn area(&self) -> f64 {
std::f64::consts::PI * (self.radio * self.radio)
}
}
Los traits son similares, excepto que definimos un trait con solo la firma de método y luego
implementamos el trait para la estructura. Así:
struct Circulo {
x: f64,
y: f64,
radio: f64,
}
trait TieneArea {
fn area(&self) -> f64;
}
Como puedes ver, el bloque trait luce muy similar a el bloque impl , pero no definimos
un bloque, solo la firma de tipos. Cuando implementamos un trait, usamos impl Trait for
Item , en vez de solo impl Item .
Traits 224
El Lenguaje de Programacion Rust
Los traits son útiles porque permiten a un tipo hacer ciertas promesas acerca de su
comportamiento. La funciones genéricas pueden explotar esto para restringir los tipos que
aceptan. Considera esta función, la cual no compila:
fn imrimir_area(figura: T) {
println!("Esta figura tiene un area de {}", figura.area());
}
Rust se queja:
error: no method named `area` found for type `T` in the current scope
Debido a que T puede ser de cualquier tipo, no podemos estar seguros que implementa el
método area . Pero podemos agregar una ‘restricción de trait’ a nuestro T genérico,
asegurándonos de que lo implemente:
# trait TieneArea {
# fn area(&self) -> f64;
# }
fn imrimir_area<T: TieneArea>(shape: T) {
println!("Esta figura tiene un area de {}", figura.area());
}
estar seguros que cualquier tipo que implemente TieneArea tendrá un método .area() .
Traits 225
El Lenguaje de Programacion Rust
trait TieneArea {
fn area(&self) -> f64;
}
struct Circulo {
x: f64,
y: f64,
radio: f64,
}
struct Cuadrado {
x: f64,
y: f64,
lado: f64,
}
fn imrimir_area<T: TieneArea>(figura: T) {
println!("Esta figura tiene un area de {}", figura.area());
}
fn main() {
let c = Circulo {
x: 0.0f64,
y: 0.0f64,
radio: 1.0f64,
};
let s = Cuadrado {
x: 0.0f64,
y: 0.0f64,
lado: 1.0f64,
};
imrimir_area(c);
imrimir_area(s);
}
Traits 226
El Lenguaje de Programacion Rust
Como puedes ver, imrimir_area ahora es genérica, pero también asegura que hallamos
proporcionado los tipos correctos. Si pasamos un tipo incorrecto:
imrimir_area(5);
error: the trait `TieneArea` is not implemented for the type `_` [E0277]
struct Rectangulo<T> {
x: T,
y: T,
ancho: T,
altura: T,
}
fn main() {
let mut r = Rectangulo {
x: 0,
y: 0,
ancho: 47,
altura: 47,
};
assert!(r.es_cuadrado());
r.altura = 42;
assert!(!r.es_cuadrado());
}
Traits 227
El Lenguaje de Programacion Rust
es_cuadrado() necesita chequear que los lados son iguales, y para ello los tipos deben ser
Ahora, un rectángulo puede ser definido en función de cualquier tipo que pueda ser
comparado por igualdad.
Hemos definido una nueva estructura Rectangulo que acepta números de cualquier
precision, objetos de cualquier tipo siempre y cuando puedan ser comparados por igualdad.
Podríamos hacer lo mismo para nuestras estructuras TieneArea , Cuadrado y Circulo ? Si,
pero estas necesitan multiplicación, y para trabajar con eso necesitamos saber mas de los
traits de operadores.
trait TieneArea {
fn area(&self) -> f64;
}
*self as f64
}
}
5.area();
Se considera pobre estilo implementar métodos en esos tipos primitivos, aun cuando es
posible.
Esto puede lucir como el viejo oeste, pero hay dos restricciones acerca de la
implementación de traits que previenen que las cosas se salgan de control. La primera es
que si el trait no esta definido en tu ámbito, no aplica. He aquí un ejemplo: la biblioteca
estándar provee un trait Write que agrega funcionalidad extra a los File s, posibilitando
la E/S de archivos. Por defecto, un File no tendría sus métodos:
Traits 228
El Lenguaje de Programacion Rust
He aqui el error:
error: type `std::fs::File` does not implement any method in scope named `write`
let resultado = f.write(buf);
^~~~~~~~~~
use std::io::Write;
Esto significa que incluso si alguien hace algo malo como agregar métodos a i32 , no te
afectara, a menos que hagas use de ese trait.
Hay una restricción mas acerca de la implementación de traits: uno de los dos bien sea el
trait o el tipo para el cual estas escribiendo la impl , debe ser definido por ti. Entonces,
podríamos implementar el trait TieneArea para el tipo i32 , puesto que TieneArea esta en
nuestro código. Pero si intentáramos implementar ToString , un trait proporcionado por
Rust para i32 , no podríamos, debido a que ni el trait o el tipo están en nuestro código.
Una ultima cosa acerca de los traits: las funciones genéricas con un limite de trait usan
‘monomorfizacion’ (‘monomorphization’) (mono: uno, morfos: forma), y por ello son
despachadas estáticamente. Que significa esto? Echa un vistazo a el capitulo acerca de
objetos trait para mas detalles.
Traits 229
El Lenguaje de Programacion Rust
fn foo<T: Clone>(x: T) {
x.clone();
}
use std::fmt::Debug;
La clausula where
Escribir funciones con solo unos pocos tipos genéricos y un pequeño numero de limites de
trait no es tan feo, pero a medida que el numero se incrementa, la sintaxis se vuelve un
poco extraña:
use std::fmt::Debug;
Traits 230
El Lenguaje de Programacion Rust
use std::fmt::Debug;
fn main() {
foo("Hola", "mundo");
bar("Hola", "mundo");
}
foo() usa la sintaxis demostrada previamente, y bar() usa una clausula where . Todo lo
que necesitas es dejar los limites por fuera cuando definas tus parámetros de tipo y luego
agregar un where después de la lista de parámetros. Para listas mas largas, espacios en
blanco pueden ser agregados:
use std::fmt::Debug;
fn bar<T, K>(x: T, y: K)
where T: Clone,
K: Clone + Debug {
x.clone();
y.clone();
println!("{:?}", y);
}
La clausula where es también mas poderosa que la sintaxis mas simple. Por ejemplo:
Traits 231
El Lenguaje de Programacion Rust
trait ConvertirA<Salida> {
fn convertir(&self) -> Salida;
}
Lo anterior demuestra una característica adicional de where : permite limites en los que el
lado izquierdo es un tipo arbitrario ( i32 en este caso), no solo un simple parámetro de tipo
(como T ).
trait Foo {
fn es_valido(&self) -> bool;
Los implementadores del trait Foo necesitan implementar es_valido() , pero no necesitan
implementar es_invalido() . Lo obtendrán por defecto. También pueden sobreescribir la
implementación por defecto si lo desean:
Traits 232
El Lenguaje de Programacion Rust
# trait Foo {
# fn es_valido(&self) -> bool;
#
# fn es_invalido(&self) -> bool { !self.es_valido() }
# }
struct UsaDefault;
struct SobreescribeDefault;
Herencia
Algunas veces, implementar un trait requiere implementar otro:
trait Foo {
fn foo(&self);
}
Traits 233
El Lenguaje de Programacion Rust
# trait Foo {
# fn foo(&self);
# }
# trait FooBar : Foo {
# fn foobar(&self);
# }
struct Baz;
error: the trait `main::Foo` is not implemented for the type `main::Baz` [E0277]
Traits 234
El Lenguaje de Programacion Rust
% Drop
Ahora que hemos discutido los traits, hablemos de un trait particular proporcionado por la
biblioteca estándar de Rust, Drop . El trait Drop provee una forma de ejecutar código
cuando un valor sale de ámbito. Por ejemplo:
struct TieneDrop;
fn main() {
let x = TieneDrop;
// hacemos algo
Eso es todo! La mecánica de Drop es muy simple, sin embargo, hay algunos detalles. Por
ejemplo, los valores son liberados (dropped) en orden opuesto a como fueron declarados.
He aquí otro ejemplo:
struct Explosivo {
potencia: i32,
}
fn main() {
let petardo = Explosivo { potencia: 1 };
let tnt = Explosivo { potencia: 100 };
}
Lo anterior imprimira:
Drop 235
El Lenguaje de Programacion Rust
El TNT se va primero que el petardo, debido que fue creado después. Ultimo en entrar,
primero en salir.
Entonces para que es bueno Drop ? Generalmente, es usado para limpiar cualquier
recurso asociado a un struct . Por ejemplo, el tipo Arc<T> es un tipo con conteo de
referencias. Cuando Drop es llamado, este decrementará el contador de referencias, y si el
numero total de referencias es cero, limpiará el valor subyacente.
Drop 236
El Lenguaje de Programacion Rust
% if let
coincidencia de patrones.
Por ejemplo, digamos que tenemos algún tipo de Option<T> . Queremos llamar a una
función sobre ello en caso de ser un Some<T> , pero no hacer nada si es None . Seria algo
así:
Ninguna de esas dos opciones es particularmente atractiva. Podemos usar if let para
hacer lo mismo, pero de mejor forma:
Si un patrón concuerda satisfactoriamente, asocia cualquier parte apropiada del valor a los
identificadores en el patrón, para luego evaluar la expresión. Si el patrón no concuerda, no
pasa nada.
Si quisieras hacer algo en el caso de que el patrón no concuerde, puedes usar else :
if let 237
El Lenguaje de Programacion Rust
while let
De manera similar, while let puede ser usado cuando desees iterar condicionalmente
mientras el valor concuerde con cierto patrón. Convierte código como este:
if let 238
El Lenguaje de Programacion Rust
% Objetos Trait
Bases
Por el resto de este capitulo, necesitaremos un trait y algunas implementaciones. Creemos
uno simple, Foo . Foo posee un solo método que retorna un String .
trait Foo {
fn metodo(&self) -> String;
}
Despacho estático
Podemos usar el trait para efectuar despacho estático mediante el uso de limites de trait:
fn main() {
let x = 5u8;
let y = "Hola".to_string();
hacer_algo(x);
hacer_algo(y);
}
Rust utiliza ‘monomorfizacion’ para el despacho estático en este código. Lo que significa
que creara una versión especial de hacer_algo() para ambos u8 y String ,
reemplazando luego los lugares de llamada con invocaciones a estas funciones
especializadas. En otras palabras, Rust genera algo así:
fn hacer_algo_string(x: String) {
x.metodo();
}
fn main() {
let x = 5u8;
let y = "Hola".to_string();
hacer_algo_u8(x);
hacer_algo_string(y);
}
Lo anterior posee una gran ventaja: el despacho estático permite que las llamadas a función
puedan ser insertadas en linea debido a que el receptor es conocido en tiempo de
compilación, y la inserción en linea es clave para una buena optimizacion. El despacho
estático es rápido, pero viene con una desventaja: ‘código inflado’ (‘code bloat’), a
consecuencia de las repetidas copias de la misma función que son insertadas en el binario,
una para cada tipo.
Ademas, los compiladores no son perfectos y pueden “optimizar” código haciendolo mas
lento. Por ejemplo, funciones insertadas en linea de manera ansiosa inflaran la cache de
instrucciones (y el cache gobierna todo a nuestro alrededor). Es por ello que #[inline] y
#[inline(always)] deben ser usadas con cautela, y una razón por la cual usar despacho
Sin embargo, el caso común es que el despacho estático sea mas eficiente. Uno puede
tener una delgada función envoltorio despachada de manera estática efectuando despacho
dinámico, pero no vice versa, es decir; las llamadas estáticas son mas flexibles. Es por esto
que la biblioteca estándar intenta ser despachada dinámicamente siempre y cuando sea
posible.
Despacho dinámico
Rust proporciona despacho dinámico a través de una facilidad denominada ‘objetos trait’.
Los objetos trait, como &Foo o Box<Foo> , son valores normales que almacenan un valor
de cualquier tipo que implementa el trait determinado, en donde el tipo preciso solo puede
ser determinado en tiempo de ejecución.
Un objeto trait puede ser obtenido de un apuntador a un tipo concreto que implemente el
trait convirtiéndolo (e.j. &x as &Foo ) o aplicándole coercion (e.j. usando &x como un
argumento a una función que recibe &Foo ).
Esas coerciones y conversiones también funcionan para apuntadores como &mut T a &mut
Foo y Box<T> a Box<Foo> , pero eso es todo hasta el momento. Las coerciones y
Esta operación puede ser vista como el ‘borrado’ del conocimiento del compilador acerca
del tipo especifico del apuntador, y es por ello que los objetos traits son a veces referidos
como borrado de tipos .
Volviendo a el ejemplo anterior, podemos usar el mismo trait para llevar a cabo despacho
dinámico con conversión de objetos trait:
fn hacer_algo(x: &Foo) {
x.metodo();
}
fn main() {
let x = 5u8;
hacer_algo(&x as &Foo);
}
o a traves de la coercion:
fn hacer_algo(x: &Foo) {
x.method();
}
fn main() {
let x = "Hola".to_string();
hacer_algo(&x);
}
Una función que recibe un objeto trait no es especializada para cada uno de los tipos que
implementan Foo : solo una copia es generada, resultando algunas veces (pero no
siempre) en menos inflación de código. Sin embargo, el despacho dinámico viene con el
costo de requerir las llamadas mas lentas a funciones virtuales, efectivamente inhibiendo
cualquier posibilidad de inserción en linea y las optimizaciones relacionadas.
Porque apuntadores?
Rust, a diferencia de muchos lenguajes administrados, no coloca cosas detrás de
apuntadores por defecto, lo que se traduce en que los tipos tengan diferentes tamaños.
Conocer el tamaño de un valor en tiempo de compilación es importante para cosas como:
pasarlo como argumento a una función, moverlo en la pila y asignarle (y deasignarle)
espacio en el montículo para su almacenamiento.
Para Foo , necesitaríamos tener un valor que podría ser mas pequeño que un String (24
bytes) o un u8 (1 byte), así como cualquier otro tipo que pueda implementar Foo en
crates dependientes (cualquier numero de bytes). No hay forma de garantizar que este
ultimo caso pueda funcionar si los valores no son almacenados en un apuntador, puesto
que esos otros tipos pueden ser de tamaño arbitrario.
Colocar el valor detrás de un apuntador significa que el tamaño del valor no es relevante
cuando estemos lanzando un objeto trait por los alrededores, solo el tamaño del apuntador
en si mismo.
Representación
Los métodos del trait pueden ser llamados en un objeto trait a través de un registro de
apuntadores a función tradicionalmente llamado ‘vtable’ (creado y administrado por el
compilador).
Los objetos trait son simples y complejos al mismo tiempo: su representation y distribución
es bastante directa, pero existen algunos mensajes de error un poco raros y algunos
comportamientos sorpresivos por descubrir.
# mod foo {
pub struct TraitObject {
pub data: *mut (),
pub vtable: *mut (),
}
# }
Eso es, un trait object como &Foo consiste de un apuntador ‘data’ y un apuntador ‘vtable’.
El apuntador data apunta hacia los datos (de un tipo desconocido T ) que guarda el objeto
trait, y el vtable pointer apunta a la vtable (table de metodos virtuales’) (‘virtual method
table’) correspondiente a la implementación de Foo para T .
struct FooVtable {
destructor: fn(*mut ()),
tamano: usize,
alineacion: usize,
metodo: fn(*const ()) -> String,
}
// u8:
byte.metodo()
}
// String:
string.metodo()
}
El campo destructor en cada vtable apunta a una función que limpiara todos los recursos
del tipo de la vtable: para u8 es trivial, pero para String liberara memoria. Esto es
necesario para adueñarse de objetos trait como Box<Foo> , que necesitan limpiar ambos la
asignación Box así como el tipo interno cuando salgan de ámbito. Los campos tamano y
estos son esencialmente no usados por el momento puesto que la información es embebida
en el destructor, pero sera usada en el futuro, a medida que los objetos trait sean
progresivamente hechos mas flexibles.
Supongamos que tenemos algunos valores que implementen Foo . La forma explicita de
construcción y uso de objetos trait Foo puede lucir un poco como (ignorando las
incongruencias entre tipos: son apuntadores de cualquier manera):
// let y: &Foo = x;
let y = TraitObject {
// almacena los datos
data: &x,
// almacena los metodos
vtable: &Foo_vtable_para_u8
};
// b.metodo();
(b.vtable.metodo)(b.data);
// y.metodo();
(y.vtable.metodo)(y.data);
Seguridad de Objetos
No todo trait puede ser usado para crear un objeto trait. Por ejemplo, los vectores
implementan Clone , pero si intentamos crear un objeto trait:
Obtenemos un error:
error: cannot convert to a trait object because trait `core::clone::Clone` is not object-safe [E0038]
let o = &v as &Clone;
^~
note: the trait cannot require that `Self : Sized`
let o = &v as &Clone;
^~
El error dice que Clone no es ‘seguro para objetos’ (‘object-safe’). Solo los traits que son
seguros para objetos pueden ser usados en la creación de objetos trait. Un trait es seguro
para objetos si ambas condiciones son verdaderas:
Entonces, que hace a un metodo seguro para objetos? Cada metodo debe requerir que
Self: Sized o todas de las siguientes:
Uff! Como podemos ver, casi todas estas reglas hablan acerca de Self . Una buena
intuición seria “exceptuando circunstancias especiales, si tu metodo de trait usa Self , no
es seguro para objetos.”
% Closures
Algunas veces es util envolver una función y sus variables libres para mejor claridad y
reusabilidad. Las variables libres que pueden ser usadas provienen del ámbito exterior y
son ‘cerradas’ (‘closed over’) cuando son usadas en la función. De allí el nombre ‘closure’.
Rust provee una muy buena implementación, como veremos a continuación.
Sintaxis
Los closures lucen así:
assert_eq!(2, suma_uno(1));
resultado += 1;
resultado += 1;
resultado
};
assert_eq!(4, suma_dos(2));
Notaras un par de cosas acerca de los closures que son un poco diferentes de las funciones
regulares definidas con fn . Lo primero es que no necesitamos anotar los tipos de los
argumentos los valores de retorno. Podemos:
assert_eq!(2, suma_uno(1));
Closures 247
El Lenguaje de Programacion Rust
closures es raramente documentada puesto a que casi siempre son anónimos, y no causan
los problemas de tipo de error-a-distancia que la inferencia en funciones con nombre
pueden causar.
La segunda sintaxis es similar, pero un tanto diferente. He agregado espacios acá para
facilitar la comparación:
Closures y su entorno
El entorno para un closure puede incluir enlaces a variable del ámbito que los envuelve en
adición a los parámetros y variables locales. De esta manera:
let num = 5;
let suma_num = |x: i32| x + num;
assert_eq!(10, suma_num(5));
Closures 248
El Lenguaje de Programacion Rust
Un error un tanto verbose pero igual de util! Como lo dice, no podemos tomar un préstamo
mutable en num debido a que el closure ya esta tomándolo prestado. Si dejamos el closure
fuera de ámbito, entonces es posible:
Sin embargo, si tu closure así lo requiere, Rust tomara pertenecia y moverá el entorno. Lo
siguiente no funciona:
println!("{:?}", nums);
note: `nums` moved into closure environment here because it has type
`[closure(()) -> collections::vec::Vec]`, which is non-copyable
let toma_nums = || nums;
^~~~~~~
Closures 249
El Lenguaje de Programacion Rust
Vec<T> posee pertenecia de su contenido, y es por ello, que al hacer referencia a el dentro
Closures move
Podemos forzar nuestro closure a tomar pertenecia de su entorno con la palabra reservada
move :
let num = 5;
Ahora, aun cuando la palabra reservada move esta presente, las variables siguen la
semántica normal. En este caso, 5 implementa Copy , y en consecuencia,
toma_pertenecia_num toma pertenecia de una copia de num . Entonces, cual es la
diferencia?
{
let mut suma_num = |x: i32| num += x;
suma_num(5);
}
assert_eq!(10, num);
En este caso, nuestro closure tomo una referencia mutable a num , y cuando llamamos a
suma_num , este muto el valor subyacente, tal y como lo esperábamos. También
necesitamos declarar suma_num como mut , puesto a que estamos mutando su entorno.
{
let mut suma_num = move |x: i32| num += x;
suma_num(5);
}
assert_eq!(5, num);
Closures 250
El Lenguaje de Programacion Rust
Otra forma de pensar acerca de los closures move es: estos proporcionan a el closure su
propio registro de activación. Sin move , el closure puede ser asociado a el registro de
activación que lo creo, mientras que un closure move es autocontenido. Lo que significa,
por ejemplo, que generalmente no puedes retornar un closure no- move desde una función.
Pero antes de hablar de recibir closures como parámetros y usarlos como valores de
retorno, debemos hablar un poco mas acerca de su implementación. Como un lenguaje de
programación de sistemas Rust te proporciona una tonelada de control acerca de lo que tu
código hace, y los closures no son diferentes.
La clave para entender como funcionan los closures es algo un poco extraño: Usar ()
para llamar una función, como foo() , es un operador sobrecargable. Partiendo desde esta
premisa, todo lo demás encaja. En Rust hacemos uso de el sistema de traits para
sobrecargar operadores. Llamar funciones no es diferente. Existen tres traits que podemos
sobrecargar:
# mod foo {
pub trait Fn<Args> : FnMut<Args> {
extern "rust-call" fn call(&self, args: Args) -> Self::Output;
}
Closures 251
El Lenguaje de Programacion Rust
Notaras unas pocas diferencias entre dichos traits, pero una grande es self : Fn recibe
&self , FnMut toma &mut self y FnOnce recibe self . Lo anterior cubre los tres tipos de
self a través de la sintaxis usual de llamadas a métodos. Pero han sido separados en tres
traits, en lugar de uno solo. Esto nos proporciona gran control acerca del tipo de closures
que podemos recibir.
La sintaxis || {} es una sintaxis alterna para esos tres traits. Rust generara un struct
para el entorno, impl el trait apropiado, y luego hará uso de este.
Lo anterior también significa que podemos elegir entre despacho estático o dinámico.
Primero, creemos una función que reciba algo llamable, ejecute una llamada sobre el y
luego retorne el resultado:
algun_closure(1)
}
assert_eq!(3, respuesta);
Closures 252
El Lenguaje de Programacion Rust
Debido a que Fn es un trait, podemos limitar nuestro genérico con el. En este caso,
nuestro closure recibe un i32 y retorna un i32 , es por ello que el limite de genéricos que
usamos es Fn(i32) -> i32 .
Hay otro punto clave acá: debido a que estamos limitando un genérico con un trait, la
llamada sera monomorfizada, y en consecuencia, estaremos haciendo despacho estático
en el closure. Eso es super cool. En muchos lenguajes, los closures son inherentemente
asignados desde el montículo, y casi siempre involucraran despacho dinámico. En Rust
podemos asignar el entorno de nuestros closures desde la pila, así como despachar la
llamada de manera estática. Esto ocurre con bastante frecuencia con los iteradores y sus
adaptadores, quienes reciben closures como argumentos.
Por supuesto, si deseamos despacho dinámico, podemos tenerlo también. Un objeto trait,
como es usual, maneja este caso:
assert_eq!(3, answer);
Ahora recibimos un objeto trait, un &Fn . Y tenemos que hacer una referencia a nuestro
closure cuando lo pasemos a llamar_con_uno , es por ello que usamos &|| .
Closures 253
El Lenguaje de Programacion Rust
let f = suma_uno;
assert_eq!(2, respuesta);
Retornando closures
Es muy común para código con estilo funcional el retornar closures en diversas situaciones.
Si intentas retornar un closure, podrías incurrir en un error. Al principio puede parecer
extraño, pero mas adelante lo entenderemos. Probablemente intentarías retornar un closure
desde una función de esta manera:
|x| x + num
}
let f = factory();
Closures 254
El Lenguaje de Programacion Rust
Para retornar algo desde una función, Rust necesita saber el tamaño del tipo de retorno.
Pero debido q que Fn es un trait, puede ser varias cosas de diversos tamaños: muchos
tipos pueden implementar Fn . Una manera fácil de darle tamaño a algo es tomando una
referencia a este, debido a que las referencias tienen un tamaño conocido. En lugar de lo
anterior podemos escribir:
|x| x + num
}
let f = factory();
Bien. Debido a que tenemos una referencia, necesitamos proporcionar un tiempo de vida.
pero nuestra función factory() no recibe ningun argumento y por ello la elision de tiempos
de vida no es posible en este caso. Entonces que opciones tenemos? Intentemos con
'static :
Closures 255
El Lenguaje de Programacion Rust
|x| x + num
}
let f = factory();
El error nos dice que no tenemos un &'static Fn(i32) -> i32 , sino un
[closure@<anon>:7:9: 7:20] . Un momento, que?
Debido a que nuestro closure genera su propio struct para el entorno así como una
implementación para Fn , FnMut y FnOnce , dichos tipos son anónimos. Solo existen para
este closure. Es por ello que Rust los muestra como closure@<anon> en vez de algún
nombre autogenerado.
El error también habla de que se espera que el tipo de retorno sea una referencia, pero lo
que estamos tratando de retornar no lo es. Mas aun, no podemos asignar directamente un
tiempo de vida 'static' a un objeto. Entonces tomaremos un enfoque diferente y
retornaremos un ‘trait object’ envolviendo el Fn en un Box . Lo siguiente casi funciona:
Box::new(|x| x + num)
}
# fn main() {
let f = factory();
Closures 256
El Lenguaje de Programacion Rust
error: closure may outlive the current function, but it borrows `num`,
which is owned by the current function [E0373]
Box::new(|x| x + num)
^~~~~~~~~~~
Bueno, como discutimos anteriormente, los closures toman su entorno prestado. Y en este
caso, nuestro entorno esta basado en un 5 asignado desde la pila, la variable num .
Debido a esto el prestamo posee el tiempo de vida del registro de activación. De retornar
este closure, la llamada a función podría terminar, el registro de activación desaparecería y
nuestro closure estaría capturando un entorno de memoria basura! Con un ultimo arreglo,
podemos hacer que funcione:
Al hacer el closure interno un move Fn , hemos creado un nuevo registro de activación para
nuestro closure. Envolviéndolo con un Box , le hemos proporcionado un tamaño conocido,
permitiéndole escapar nuestro registro de activación.
Closures 257
El Lenguaje de Programacion Rust
Algunas veces, varias funciones pueden tener el mismo nombre. Considera el siguiente
código:
trait Foo {
fn f(&self);
}
trait Bar {
fn f(&self);
}
struct Baz;
let b = Baz;
# trait Foo {
# fn f(&self);
# }
# trait Bar {
# fn f(&self);
# }
# struct Baz;
# impl Foo for Baz {
# fn f(&self) { println!("impl de Foo en Baz"); }
# }
# impl Bar for Baz {
# fn f(&self) { println!("impl de Bar en Baz"); }
# }
# let b = Baz;
Foo::f(&b);
Bar::f(&b);
Foo::
Bar::
Dichas mitades de invocacion son los tipos de los traits: Foo y Bar . Lo anterior es
responsable de la eliminación de la ambigüedad entre las dos llamadas: Rust llama la
función del trait que nombraste.
f(&b)
Usando <>
La forma de SULF de la que acabamos de hablar:
Trait::metodo(args);
Es una version corta. Existe una version expandida que puede ser necesaria en algunas
situaciones:
::metodo(args);
La sintaxis <>:: es una forma de proporcionar una pista de tipos. El tipo va dentro de los
<> s. En este caso es Type as Trait indicando que deseamos llamar a la version Trait
de metodo . La sección as Trait es opcional de no ser ambigua. Lo mismo con los <> s,
de allí proviene version mas corta.
trait Foo {
fn clone(&self);
}
#[derive(Clone)]
struct Bar;
<Bar as Clone>::clone(self);
}
}
Lo anterior llamara el metodo clone en el trait Clone , en vez de la version clone del trait
Foo .
% Crates y Modulos
Cada crate posee un modulo raiz implícito que contiene el código para dicho crate. Puedes
definir un árbol de sub-modulos bajo el módulo raíz. Los módulos te permiten particionar tu
codigo dentro del crate.
Como ejemplo, creemos un crate frases, que nos proveerá varias frases en diferentes
idiomas. Para mantener las cosas simples, nos apegaremos solo a ‘saludos’ y ‘despedidas’
como los dos tipos de frases, así como Ingles y Japonés (日本語) para los dos idiomas en
los que las frases estarán representadas. Tendremos la siguiente distribución de módulos:
+-----------+
+---| saludos |
| +-----------+
+----------+ |
+---| ingles |---+
| +----------+ | +-----------+
| +---| despedidas |
+----------+ | +-----------+
| frases |---+
+----------+ | +-----------+
| +---| saludos |
| +-----------+ | +-----------+
+---| japonés |--+
+-----------+ |
| +------------+
+---| despedidas |
+------------+
En este ejemplo, frases es el nombre de nuestro crate. El resto son módulos. Puedes
observar que los módulos forman un árbol, partiendo desde la raíz, que a su vez es la raíz
del árbol frases en si.
Ahora que tenemos un plan, definamos estos módulos en código. Para comenzar,
generemos un proyecto con Cargo:
$ tree .
.
├── Cargo.toml
└── src
└── lib.rs
1 directory, 2 files
anterior. above.
Definiendo Módulos
Para definir cada uno de nuestros módulos, usamos la palabra reservada mod . Hagamos
que nuestro src/lib.rs se vea así:
mod ingles {
mod saludos {
}
mod despedidas {
}
}
mod japones {
mod saludos {
}
mod despedidas {
}
}
Después de la palabra reservada mod debes proporcionar el nombre del modulo. Los
nombres de de modulo siguen las mismas convenciones que los demás identificadores en
Rust: snake_case_en_minusculas . El contenido de cada modulo esta delimitado por llaves
( {} ).
Dentro de un determinado mod , puedes declarar sub- mod s. Podemos referirnos a los sub-
modulos con una notación dos puntos dobles ( :: ): nuestros cuatro módulos anidados son
ingles::saludos , ingles::despedidas , japones::saludos , y japones::despedidas . Debido
a que estos sub-modulos están dentro del espacio de nombres del modulo padre, los
nombres no entran en conflicto: ingles::saludos y japones::saludos son distintos incluso
cuando sus nombres son ambos saludos .
Debido a que este crate no posee una función main() , y se llama lib.rs , Cargo
construirá este crate como una biblioteca:
$ cargo build
Compiling frases v0.0.1 (file:///home/tu/proyectos/frases)
$ ls target/debug
build deps examples libfrases-a7448e02a0468eaa.rlib native
libfrases-hash.rlib es el crate compilado. Antes de ver como usar este crate desde otro,
mod ingles {
// el contenido del modulo va aquí
}
mod ingles;
Nota que todos en estos archivos, no necesitas re-declarar el modulo: con la declaración
inicial mod es suficiente.
Usando estas dos técnicas, podemos partir nuestro crate en dos directorios y siete archivos:
$ tree .
.
├── Cargo.lock
├── Cargo.toml
├── src
│ ├── ingles
│ │ ├── despedidas.rs
│ │ ├── saludos.rs
│ │ └── mod.rs
│ ├── japones
│ │ ├── despedidas.rs
│ │ ├── saludos.rs
│ │ └── mod.rs
│ └── lib.rs
└── target
└── debug
├── build
├── deps
├── examples
├── libfrases-a7448e02a0468eaa.rlib
└── native
mod ingles;
mod japones;
Estas dos declaraciones le dicen a Rust que busque bien sea src/ingles.rs y
src/japones.rs , o src/ingles/mod.rs y src/japones/mod.rs , dependiendo de nuestra
preferencia. En este caso, debido a que nuestros módulos poseen sub-modulos hemos
seleccionado la segunda. Ambos src/ingles/mod.rs y src/japones/mod.rs se ven lucen de
esta manera:
mod saludos;
mod despedidas;
De nuevo, estas declaraciones hacen que Rust busque bien sea por src/ingles/saludos.rs
y src/japones/saludos.rs o src/ingles/despedidas/mod.rs and
src/japones/despedidas/mod.rs . Debido a que los sub-modulos no poseen sus propios sub-
Por supuesto, puedes copiar y pegar el Japones desde esta pagina web, o simplemente
escribe alguna otra cosa. No es importante que en efecto coloques ‘konnichiwa’ para
aprender acerca del sistema de módulos de Rust.
Ahora que tenemos algo de funcionalidad en nuestro crate, intentemos usarlos desde otro.
fn main() {
println!("Hola en Ingles: {}", frases::ingles::saludos::hola());
println!("Adios en Ingles: {}", frases::ingles::despedidas::adios());
La declaración extern crate le informa a Rust que necesitamos compilar y enlazar al crate
frases . Podemos entonces usar el modulo frases aqui. Como mencionamos
anteriormente, puedes hacer uso de dos puntos dobles para hacer referencia a los sub-
modulos y las funciones dentro de ellos.
Nota: cuando se importa un crate que tiene guiones en su nombre "como-este", lo cual no
es un identificador valido en Rust, sera convertido cambiándole los guiones a guiones bajos,
así que escribirías algo como extern crate como_este;
Lo anterior todavía no funciona. Obtenemos cuatro errores que lucen similares a estos:
$ cargo build
Compiling frases v0.0.1 (file:///home/tu/proyectos/frases)
src/main.rs:4:38: 4:72 error: function `hola` is private
src/main.rs:4 println!("Hola en Ingles: {}", frases::ingles::saludos::hola());
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
note: in expansion of format_args!
<std macros>:2:25: 2:58 note: expansion site
<std macros>:1:1: 2:62 note: in expansion of print!
<std macros>:3:1: 3:54 note: expansion site
<std macros>:1:1: 3:58 note: in expansion of println!
frases/src/main.rs:4:5: 4:76 note: expansion site
Por defecto todo es privado en Rust. Hablemos de esto con mayor detalle.
Rust te permite controlar de manera precisa cuales aspectos de tu interfaz son públicos, y
private es el defacto. Para hacer a algo publico, debes hacer uso de la palabra reservada
pub . Enfoquemonos primero en el modulo ingles , para ello, reduzcamos nuestro
src/main.rs a:
fn main() {
println!("Hola en Ingles: {}", frases::ingles::saludos::hola());
println!("Adios en Ingles: {}", frases::ingles::despedidas::adios());
}
Y también en src/ingles/despedidas.rs :
Ahora, nuestro crate compila, con unas pocas advertencias acerca de no haber usado las
funciones en japones :
$ cargo run
Compiling frases v0.0.1 (file:///home/tu/proyectos/frases)
src/japones/saludos.rs:1:1: 3:2 warning: function is never used: `hola`, #[warn(dead_code)] on by def
src/japones/saludos.rs:1 fn hola() -> String {
src/japones/saludos.rs:2 "こんにちは".to_string()
src/japones/saludos.rs:3 }
src/japones/despedidas.rs:1:1: 3:2 warning: function is never used: `adios`, #[warn(dead_code)] on by
src/japones/despedidas.rs:1 fn adios() -> String {
src/japones/despedidas.rs:2 "さようなら".to_string()
src/japones/despedidas.rs:3 }
Running `target/debug/frases`
Hola en Ingles: Hello!
Adios en Ingles: Goodbye.
pub también aplica a las struct s y sus campos miembro. Y Rust teniendo siempre su
tendencia hacia la seguridad, el hacer una struct public no hará a sus miembros public
automáticamente: debes marcarlos individualmente como pub .
Ahora que nuestras funciones son public, podemos hacer uso de ellas. Grandioso! Sin
embargo, escribir frases::ingles::saludos::hola() es largo y repetitivo. Rust posee otra
palabra reservada para importar nombres en el ámbito actual, de manera que puedas hacer
referencia a ellos con nombres mas cortos, Hablemos acerca de use .
use frases::ingles::saludos;
use frases::ingles::despedidas;
fn main() {
println!("Hola en Ingles: {}", saludos::hola());
println!("Adios en Ingles: {}", despedidas::adios());
}
Las dos lineas use importan cada modulo en el ámbito local, de manera tal que podamos
referirnos a las funciones con nombres mucho mas cortos. Por convención, cuando se
importan funciones, se considera una buena practica importar el modulo, en lugar de la
función directamente. En otras palabras, puedes hacer esto:
use frases::ingles::saludos::hola;
use frases::ingles::despedidas::adios;
fn main() {
println!("Hola en Ingles: {}", hola());
println!("Adios en Ingles: {}", adios());
}
use frases::ingles::saludos::hola;
use frases::japones::saludos::hola;
fn main() {
println!("Hola en Ingles: {}", hola());
println!("Hola en Japones: {}", hola());
}
use phrases::ingles::saludos;
use phrases::ingles::despedidas;
fn main() {
println!("Hola en Ingles: {}", saludos::hola());
println!("Adios en Ingles: {}", despedidas::adios());
Y luego en src/japones/despedidas.rs :
mod saludos;
mod despedidas;
La declaración pub use trae la función a el ámbito en esta parte de nuestra jerarquía de
modulos. Debido que hemos hecho pub use dentro de nuestro modulo japones , ahora
tenemos una función frases::japones::hola() y una función frases::japones::adios() ,
aun cuando el código para ellas vive en frases::japones::saludos::hola() y
frases::japones::despedidas::adios()`. Nuestra organización interna no define nuestra
interfaz externa.
Acá tenemos un pub use para cada función que deseamos traer en el ámbito de japones .
Alternativamente pudimos haber usado la sintaxis alternativa de comodín para incluir todo
desde saludos en el ámbito actual: pub use self::saludos::*
Que hay acerca de el self ? Bueno, por defecto, las declaraciones use son rutas
absolutas, partiendo desde la raíz de tu crate. self , a diferencia, hace esa ruta relativa a
tu lugar actual dentro de la jerarquía. Hay una ultima forma especial de usar use : puedes
usar use super:: para alcanzar un nivel superior en la jerarquía desde tu posición actual.
Algunas personas gustan ver a self como . y super como .. similarmente los
usados por los shells para mostrar los directorios actual y padre respectivamente.
Fuera de use , las rutas son relativas: foo::bar() se refiere a una función dentro de foo
en relación a en donde estamos. Si posee un prefijo :: , como en ::foo::bar() , entonces
se refiere a un foo diferente, una ruta absoluta desde la raíz de tu crate.
$ cargo run
Compiling frases v0.0.1 (file:///home/tu/proyectos/frases)
Running `target/debug/frases`
Hola in Ingles: Hello!
Adios in Ingles: Goodbye.
Hola in Japones: こんにちは
Adios in Japones: さようなら
Importes complejos
Rust ofrece un numero de opciones avanzadas que pueden hacer tus sentencias extern
crate mas compactas y convenientes. He aquí un ejemplo:
fn main() {
println!("Hola en Ingles; {}", saludos_en::hola());
println!("Y en Japones: {}", saludos_ja::hola());
println!("Goodbye in Ingles: {}", ingles::despedidas::adios());
println!("Otra vez: {}", despedidas_en::adios());
println!("Y en Japones: {}", adios());
}
Primero, ambos extern crate y use permiten renombrar lo que esta siendo importado.
Entonces el crate todavía se llama "frases", pero aquí nos referiremos a el como "dichos".
Similarmente, el primer use trae el modulo japones::saludos desde el crate, pero lo hace
disponible a través del nombre saludos_ja en lugar de simplemente saludos . Lo anterior
puede ayudar a evitar ambigüedad cuando se importan nombres similares desde distintos
lugares.
El segundo use posee un asterisco para traer todos los símbolos desde el modulo
dichos::japones::despedidas . Como podrás ver mas tarde podemos referirnos al adios
Japones sin calificadores de modulo. Este tipo de glob debe ser usando con cautela.
El tercer use requiere un poco mas de explicación. Esta usando "expansion de llaves"
para comprimir tres sentencias use en una (este tipo de sintaxis puede serte familiar si has
escrito scripts del shell de Linux). La forma descomprimida de esta sentencia seria:
use dichos::ingles;
use dichos::ingles::saludos as saludos_en;
use dichos::ingles::despedidas as despedidas_en;
Como puedes ver, las llaves comprimen las sentencias use para varios items bajo la
misma ruta, y en este contexto self hace referencia a dicha ruta. Nota: Las llaves no
pueden ser anidadas o mezcladas con globbing de asteriscos.
% const y static
Rust posee una manera de definir constantes con la palabra reservada const :
const N: i32 = 5;
Las constantes viven por el tiempo de vida completo del programa. Para ser mas
específicos, las constantes en Rust no tienen direcciones de memoria fijas. Esto es debido
a que estas son insertadas en linea en cada lugar en el que son usadas. Por esta razón,
referencias a la misma constante no están garantizadas a apuntar a la misma dirección de
memoria.
static
Rust proporciona una facilidad de tipo ‘variable global’ en items estáticos. Son similares a
las constantes, pero los items estáticos no son insertados en linea tras su uso. Esto significa
que solo existe una instancia para cada valor, y esta ubicada en una dirección de memoria
fija.
He aqui un ejemplo:
static N: i32 = 5;
Los items static viven por el tiempo de vida del programa completo, y en consecuencia
cualquier referencia es almacenada en una constante posee un tiempo de vida 'static :
Mutabilidad
Puedes introducir mutabilidad con la palabra reservada mut :
Debido que es mutable, un hilo podría estar actualizando N mientras que otro este
leyéndolo, causando inseguridad en el manejo de memoria. Es por ello que ambos el
acceso y la mutación de un static mut son inseguros , y en consecuencia deben ser
efectuados dentro de un bloque unsafe :
unsafe {
N += 1;
Aunado a esto, cualquier tipo almacenado en un static debe ser Sync , y podría no tener
una implementación de Drop .
Inicialización
Ambos const y static tienen requerimientos al proporcionarles un valor. Solo se les
puede proporcionar un valor que sea una expresión constante. En otras palabras, no
puedes usar el resultado de una llamada a función, algo similarmente complejo o algo
determinado en tiempo de ejecución.
% Atributos
Las declaraciones pueden ser anotadas con ‘atributos’ en Rust. Los atributos lucen así:
#[test]
# fn foo() {}
o de esta manera:
# mod foo {
#![test]
# }
La diferencia entre los dos es el ! , que cambia a que cosa aplica el atributo:
#[foo]
struct Foo;
mod bar {
#![bar]
}
El atributo #[foo] aplica a el siguiente item, que es la declaración del struct . El atributo
#![bar] aplica a el item que lo encierra, la declaración mod . De resto, son lo mismo.
#[test]
fn comprobar() {
assert_eq!(2, 1 + 1);
}
Esta marcado con #[test] . Esto significa que es especial: cuando ejecutes las pruebas,
esta función sera ejecutada. Cuando compilas normalmente, no sera incluida ni siquiera. La
función es ahora una función de prueba.
#[inline(always)]
fn fn_super_rapida() {
# }
Atributos 275
El Lenguaje de Programacion Rust
#[cfg(target_os = "macos")]
mod solo_macos {
# }
Los atributos son usados para un numero de cosas diferentes en Rust. Hay una lista de
atributos completa en la referencia. Actualmente, no tienes permitido crear tus propios
atributos, el compilador de Rust los define.
Atributos 276
El Lenguaje de Programacion Rust
% Alias type
Sin embargo, nota, que es un alias, no un nuevo tipo. En otras palabras, debido a que Rust
es fuertemente tipificado, podrías esperar que una comparación entre dos tipos diferentes
falle:
let x: i32 = 5;
let y: i64 = 5;
if x == y {
// ...
}
lo anterior, produce:
let x: i32 = 5;
let y: Num = 5;
if x == y {
// ...
}
Compilaría sin errores. Valores de un tipo Num son lo mismo que un valor de tipo i32 , en
todos los aspectos.
use std::result;
enum ErrorConcreto {
Foo,
Bar,
}
El código anterior, crea una version especializada de el tipo Result , la cual posee siempre
un ErrorConcreto para la parte E de Result<T, E> . Esta practica es usada comúnmente
en la biblioteca estándar para la creación de errores personalizados para cada sub-seccion.
Por ejemplo, io::Result.
Rust, con su foco en seguridad, proporciona dos formas diferentes de conversión entre
tipos. La primera, as , es para conversión segura. En contraste, transmute permite
conversion arbitraria, y es una de las características mas peligrosas de Rust!
as
La palabra reservada as lleva a cabo conversion básica:
let x: i32 = 5;
let y = x as i64;
Es una ‘conversion no escalar’ (‘non-scalar cast’) porque tenemos multiples valores: los
cuatro elementos en el arreglo. Este tipo de conversiones son muy peligrosas, debido a que
asumen cosas acerca de como las multiples estructuras subyacentes están implementadas.
Para esto, necesitamos algo mas peligroso.
transmute
La función transmute es proporcionada por una intrínseca del compilador, y lo que hace es
muy simple, pero al mismo tiempo muy peligroso. Le dice a Rust que trate un valor de un
tipo como si fuera un valor de otro tipo. Lleva acabo esto sin respetar el sistema de tipos, y
confía completamente en ti.
use std::mem;
unsafe {
let a = [0u8, 0u8, 0u8, 0u8];
Debemos envolver la operación en un bloque unsafe para que el código anterior compile
satisfactoriamente. Técnicamente, solo la llamada a mem::transmute necesita estar dentro
del bloque, pero en este caso esta bien tener todo lo relacionado a la conversion de manera
que sepas en donde buscar. En este caso, los detalles acerca de a son también
importantes, es por ello que están dentro del bloque. Veras código en cualquiera de los dos
estilos, algunas veces el contexto esta tan lejos que envolver todo el código en un bloque
unsafe no es una gran idea.
Mientras que transmute hace muy pocos chequeos, se asegura, al menos, que los tipos
sean del mismo tamaño. Lo siguiente, falla:
use std::mem;
unsafe {
let a = [0u8, 0u8, 0u8, 0u8];
con:
error: transmute called on types with different sizes: [u8; 4] (32 bits) to u64
(64 bits)
% Tipos Asociados
Los tipos asociados son una parte poderosa del sistema de tipos de Rust. Se relacionan
con la idea de una ‘familia de tipos’, en otras palabras, la agrupación de multiples tipos. Esa
descripción es una poco abstracta, es mejor que nos adentremos de una vez en un ejemplo.
Si queremos escribir un trait Grafo , tenemos dos tipos por encima de los cuales debemos
ser genéricos: el tipo de los nodos y el tipo de los vertices. Podríamos escribir un trait,
Grafo<N, V> como este:
Si bien esto de alguna manera funciona, termina siendo un poco raro. Por ejemplo,
cualquier función que quiera recibir un Grafo como parámetro ahora también necesita ser
genérica por sobre los tipos N odo y V ertice:
Nuestro calculo de la distancia funciona sin tomar en cuenta nuestro tipo Vértice , en
consecuencia la V en esta firma es simplemente una distracción.
Lo que realmente queremos expresar es que ciertos tipos V ertice y N odo vienen juntos
para formar cada clase de Grafo . Podemos hacer esto con tipos asociados:
trait Grafo {
type N;
type V;
trait Graph {
type N;
type V;
Simple. Los tipos asociados usan la palabra reservada type , y van dentro del cuerpo del
trait en conjunto con las funciones.
Dichas declaraciones type pueden tener lo mismo que las funciones. Por ejemplo si
deseáramos que nuestro tipo N implementase Display , de manera que pudiésemos
imprimir los nodos, podríamos hacer lo siguiente:
use std::fmt;
trait Grafo {
type N: fmt::Display;
type V;
# trait Grafo {
# type N;
# type V;
# fn tiene_vertice(&self, &Self::N, &Self::N) -> bool;
# fn vertices(&self, &Self::N) -> Vec<Self::V>;
# }
struct Nodo;
struct Vertice;
struct MiGrafo;
Esta tonta implementación siempre retorna true y un Vec<Vertice> vacío, pero te da una
idea de como se implementa este tipo de traits con tipos asociados. Primero necesitamos
tres struct s, una para el grafo, una para el nodo, y una para el vértice. De haber tenido
mas sentido usar un tipo diferente, también hubiese funcionado, ahora usaremos struct s
para los tres.
De aquí en adelante, usamos = para definir nuestros tipos asociados. El nombre que el
trait usa va del lado izquierdo del = , y el tipo en concreto para el que estamos
implementando este trait va del lado derecho. Finalmente, podemos usar tipos concretos en
nuestras declaraciones de función.
# trait Grafo {
# type N;
# type V;
# fn tiene_vertice(&self, &Self::N, &Self::N) -> bool;
# fn vertices(&self, &Self::N) -> Vec;
# }
# struct Nodo;
# struct Vertice;
# struct MiGrafo;
# impl Grafo for MiGrafo {
# type N = Node;
# type V = Vertice;
# fn tiene_vertice(&self, n1: &Nodo, n2: &Nodo) -> bool {
# true
# }
# fn vertices(&self, n: &Nodo) -> Vec {
# Vec::new()
# }
# }
let grafo = MiGrafo;
let obj = Box::new(grafo) as Box;
error: the value of the associated type `V` (from the trait `main::Grafo`) must
be specified [E0191]
let obj = Box::new(grafo) as Box;
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~
24:44 error: the value of the associated type `N` (from the trait
`main::Grafo`) must be specified [E0191]
let obj = Box::new(grafo) as Box;
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~
No podemos crear objetos trait de esta manera, puesto a que no conocemos los tipos
asociados. En su lugar podríamos escribir:
# trait Grafo {
# type N;
# type V;
# fn has_edge(&self, &Self::N, &Self::N) -> bool;
# fn edges(&self, &Self::N) -> Vec<Self::V>;
# }
# struct Nodo;
# struct Vertice;
# struct MiGrafo;
# impl Grafo for MiGrafo {
# type N = Nodo;
# type V = Vertice;
# fn tiene_vertice(&self, n1: &Nodo, n2: &Nodo) -> bool {
# true
# }
# fn vertices(&self, n: &Nodo) -> Vec<Vertice> {
# Vec::new()
# }
# }
let grafo = MiGrafo;
let obj = Box::new(grafo) as Box<Grafo<N=Nodo, V=Vertice>>;
La sintaxis N=Nodo nos permite crear un tipo concreto, Nodo , para el parámetro de tipo N .
Lo mismo con V=Vertice . De no haber proporcionado esta restricción, no hubiéramos
podido determinar contra cual impl debe ser usado el objeto trait.
La mayoría de los tipos posee un tamaño particular, en bytes, que es conocido en tiempo de
compilación. Por ejemplo un i32 tiene un tamaño de treinta y dos bits, o cuatro bytes. Sin
embargo, hay algunos tipos que son útiles de expresar, pero no tienen un tamaño definido.
Dichos tipos son denominados tipos ‘sin tamaño’ (‘unsized’) o ‘de tamaño dinámico’
(‘dynamically sized’). Un ejemplo es [T] . Dicho tipo representa cierto numero de T en
secuencia. Pero no sabemos cuantos de ellos hay, y en consecuencia el tamaño es
desconocido.
Rust entiende algunos de estos tipos, pero tienen algunas restricciones. Son tres:
1. Solo podemos manipular una instancia de un tipo sin tamaño a través de un apuntador.
Un &[T] funciona perfecto, pero un [T] no.
2. Las variables y argumentos no pueden tener tipos de tamaño dinámico.
3. Solo el ultimo campo en un struct puede tener un tipo de datos de tamaño dinámico;
los otros campos no. Las variantes de enums no deben tener tipos de tamaño dinámico
como data.
Entonces, porque molestarse? Bueno, debido a que [T] puede solo ser usado detrás de
un apuntador, de no tener soporte a nivel de lenguaje para tipos sin tamaño, seria imposible
escribir lo siguiente:
Significando que esta implementación solo funcionaria para referencias, y no otro tipos de
apuntadores. Con el impl for str , todos los apuntadores, incluyendo (en algún punto, hay
algunos bugs que arreglar primero) apuntadores inteligentes definidos por el usuario,
pueden usar este impl .
?Sized
Si deseas escribir una función que acepte un tipo de datos de tamaño dinámico, puedes
usar limite de trait especial, ?Sized :
Este ? , leído como “T puede ser Sized ”, significa que este limite de trait es especial: nos
permite hacer match con mas tipos, no menos. Es justo casi como que cualquier T
implícitamente es T: Sized , y el ? deshace este comportamiento por defecto.
% Operadores y Sobrecarga
Rust permite una forma limitada de sobrecarga de operadores. Existen ciertos operadores
que pueden ser sobrecargados. Para soportar un operador particular entre tipos, hay un trait
en especifico que puedes implementar, el cual sobrecarga el operador.
use std::ops::Add;
#[derive(Debug)]
struct Punto {
x: i32,
y: i32,
}
fn main() {
let p1 = Punto { x: 1, y: 0 };
let p2 = Punto { x: 2, y: 3 };
let p3 = p1 + p2;
println!("{:?}", p3);
}
En main , podemos hacer uso de + en nuestros dos Puntos s, puesto que hemos
implementado Add<Output=Punto> para Punto .
Existe un numero de operadores que pueden ser sobrecargados de esta manera, y todos
sus traits asociados viven en el modulo std::ops . Echa un vistazo a la documentación
para una lista completa.
Implementar dichos traits sigue un patron. Analicemos a Add con mas detalle:
# mod foo {
pub trait Add<RHS = Self> {
type Output;
Hay en total tres tipos involucrados aqui: El tipo para el cual estas implementado Add ,
RHS (de right hand side), que por defecto es Self , y Output . Para una expresion let z
# struct Punto;
# use std::ops::Add;
impl Add<i32> for Punto {
type Output = f64;
use std::ops::Mul;
trait TieneArea<T> {
fn area(&self) -> T;
}
struct Cuadrado<T> {
x: T,
y: T,
lado: T,
}
fn main() {
let s = Cuadrado {
x: 0.0f64,
y: 0.0f64,
lado: 12.0f64,
};
El método area requiere que podamos multiplicar los lados, es por ello que declaramos
que T debe implementar std::ops::Mul . Como Add , mencionado anteriormente, Mul
toma un parámetro Output : debido a que sabemos que los números no cambian de tipo
cuando son multiplicados, también lo definimos como T . T también debe soportar
copiado, de manera que Rust no trate de mover self.lado a el valor de retorno.
% Coerciones Deref
use std::ops::Deref;
struct EjemploDeref<T> {
valor: T,
}
fn main() {
let x = EjemploDeref { valor: 'a' };
assert_eq!('a', *x);
}
Lo anterior es útil para escribir tipos personalizados de apuntadores. Sin embargo, hay una
facilidad del lenguaje relacionada a Deref : las ‘coerciones deref’. He aquí la regla: Si
tienes un tipo U , y este implementa Deref<Target=T> , los valores de &U harán coercion
Automatica a un &T . A continuación un ejemplo:
fn foo(s: &str) {
// tomar la cadena prestada por un segundo
}
Usando un ampersand en frente del valor tomamos una referencia a el. Entonces owned es
un String , &owned es un &String , y debido a impl Deref<Target=str>
para String , &String hará deref a &str que es tomado por foo() .
Eso es todo. Dicha regla es uno de los únicos lugares en los que Rust hace conversiones
automáticas por nosotros, pero al mismo tiempo agrega mucha flexibilidad. Por ejemplo el
tipo Rc<T> implementa Deref<Target=T> , de manera que esto funciona:
use std::rc::Rc;
fn foo(s: &str) {
// tomar la cadena prestada por un segundo
}
Todo lo que hemos hecho es envolver nuestro String en un Rc<T> . Pero ahora podemos
pasar el Rc<String> a cualquier lugar en el cual tengamos un a String . La firma de foo
no cambio, pero funciona de igual forma con cualquiera de los dos tipos. Este ejemplo tiene
dos conversiones: de Rc<String> a String y luego de String a &str . Rust hará esto
tantas veces como sea posible hasta que los tipos coincidan.
fn foo(s: &[i32]) {
// tomar el pedazo prestado por un segundo
}
foo(&owned);
ejemplo:
struct Foo;
impl Foo {
fn foo(&self) { println!("Foo"); }
}
let f = &&Foo;
f.foo();
Aunque f es un &&Foo y Foo recibe a &self , lo anterior funciona. Todo esto como
consecuencia de que todas estas cosas son lo mismo:
f.foo();
(&f).foo();
(&&f).foo();
(&&&&&&&&f).foo();
% Macros
Por ahora, has aprendido mucho sobre las herramientas que Rust ofrece para abstraer y
reutilizar código. Estas unidades de reutilización poseen una rica estructura semántica. Por
ejemplo, las funciones tienen una firma de tipos, los parámetros de tipo tienen límites de
traits y las funciones sobrecargadas deben pertenecer a un trait particular.
Esta estructura significa que las principales abstracciones en Rust poseen un poderoso
mecanismo de chequeo en tiempo de compilación. Pero, el precio es una flexibilidad
reducida. Si se identifica visualmente un patrón de código repetido, podría ser difícil o
tedioso expresar ese patrón como una función genérica, un trait, o cualquier otro elemento
de la semántica de Rust.
Las macros nos permiten abstraer a un nivel sintáctico. Una invocación de macro es la
abreviatura de una forma sintáctica "expandida". Dicha expansión ocurre durante la
compilación, antes de comprobación estática. Como resultado, las macros pueden capturar
muchos patrones de reutilización de código que las abstracciones fundamentales de Rust
no pueden.
El inconveniente es que el código basado en macros puede ser más difícil de entender,
porque menos de las normas internas de Rust aplican. Al igual que una función ordinaria,
una macro bien hecha se puede utilizar sin entender detalles de implementación. Sin
embargo, puede ser difícil diseñar una macro con un buen comportamiento! Además, los
errores de compilación en código de macros son más difíciles de entender, porque
describen problemas en el código expandido, no a nivel del código fuente que usan los
desarrolladores.
Estos inconvenientes hacen de las macros una "herramienta de último recurso". Lo anterior
no quiere decir que las macros son malas; forman parte de Rust porque a veces son
necesarias para código conciso y abstracto. Simplemente mantén en cuenta este equilibrio.
Esto no puede ser una función ordinaria, porque acepta cualquier número de argumentos.
Pero podemos imaginarlo como una abreviación sintáctica para
Macros 294
El Lenguaje de Programacion Rust
let x: Vec<u32> = {
let mut temp_vec = Vec::new();
temp_vec.push(1);
temp_vec.push(2);
temp_vec.push(3);
temp_vec
};
# assert_eq!(&[1,2,3], &x);
macro_rules! vec {
( $( $x:expr ),* ) => {
{
let mut temp_vec = Vec::new();
$(
temp_vec.push($x);
)*
temp_vec
}
};
}
# fn main() {
# assert_eq!(vec![1,2,3], [1, 2, 3]);
# }
Lo anterior dice que estamos definiendo una macro llamada vec , al igual que fn vec
definiría una función llamada vec . En prosa, informalmente escribimos el nombre de una
macro con un signo de exclamación, por ejemplo, vec! . Este signo de exclamación es
parte de la sintaxis de invocación y sirve para distinguir una macro de una función ordinaria.
Coincidencia de patrones
Macros 295
El Lenguaje de Programacion Rust
Una macro se define a través de una serie de reglas, las cuales son casos de coincidencia
de patrones. Anteriormente vimos
Esto es similar a un brazo de una expresión match , pero las pruebas para coincidencia
ocurren sobre los árboles de sintaxis Rust en tiempo de compilación. El punto y coma es
opcional en el caso final (aquí, el único caso). El "patrón" en el lado izquierdo del => es
conocido como un 'matcher'. Los matchers tienen su propia pequeño gramática dentro del
language.
El matcher $x:expr coincidirá con cualquier expresión Rust, asociando ese árbol sintáctico
a la 'metavariable' $x . El identificador expr es un 'especificador fragmento'; todas las
posibilidades se enumeran en el capitulo avanzado de macros. Al rodear el matcher con
$(...),* , coincidirá con cero o más expresiones separadas por comas.
Ademas de la sintaxis especial de matchers, los tokens Rust que aparecen en un matcher
deben coincidir de manera exacta. Por ejemplo:
macro_rules! foo {
(x => $e:expr) => (println!("modo X: {}", $e));
(y => $e:expr) => (println!("modo Y: {}", $e));
}
fn main() {
foo!(y => 3);
}
imprimirá
modo Y: 3
Con
Expansión
Macros 296
El Lenguaje de Programacion Rust
El lado derecho de una regla macro es sintaxis Rust ordinaria, en su mayor parte. Pero
podemos insertar partes de sintaxis capturadas por el matcher. Del ejemplo original:
$(
temp_vec.push($x);
)*
Debido a que $x ya fue marcado como una coincidencia con una expresión, no repetimos
:expr en el lado derecho. Además, no incluimos una coma separando como parte del
operador de repetición. En cambio, tenemos un punto y coma que termina dentro del bloque
repetido.
Otro detalle: la macro vec tiene dos pares de llaves en el lado derecho. A menudo se
combinan de este modo:
macro_rules! foo {
() => {{
...
}}
}
Las llaves exteriores son parte de la sintaxis de macro_rules! . De hecho, se puede utilizar
() o [] en su lugar. Simplemente delimitan el lado derecho como un todo.
Las llaves interiores son parte de la sintaxis expandida. Recuerda que la macro vec! se
utiliza en un contexto de expresión. Para escribir una expresión con varias sentencias, entre
ellas enlaces a variable, utilizamos un bloque. Si la macro se expande a una sola expresión,
no necesitas dicha capa extra de llaves.
Hay que tener también en cuenta que nunca declaramos que la macro produce una
expresión. De hecho, esto no se determina hasta que usamos la macro como una
expresión. Con cuidado, se puede escribir una macro cuya expansión funcione en varios
contextos. Por ejemplo, la abreviatura de un tipo de datos podría ser válida como una
expresión o un patrón.
Repetición
El operador de repetición sigue dos reglas principales:
Macros 297
El Lenguaje de Programacion Rust
1. $(...)* camina a través de una "capa" de repeticiones, para todos los $nombre s que
Esta macro barroca ilustra la duplicación de las variables de los niveles de repetición
exteriores.
macro_rules! o_O {
(
$(
$x:expr; [ $( $y:expr ),* ]
);*
) => {
&[ $($( $x + $y ),*),* ]
}
}
fn main() {
let a: &[i32]
= o_O!(10; [1, 2, 3];
20; [4, 5, 6]);
Esa es la mayor parte de la sintaxis de los matcher. Todos los ejemplos utilizan $(...)* ,
que coincide con "cero o más" elementos sintácticos. Alternativamente, puedes escribir
$(...)+ que coincide con "uno o más". Ambas formas incluyen, opcionalmente, un
Higiene
Algunos lenguajes implementan macros con sustitución de texto simple, lo que trae como
consecuencia diversos problemas. Por ejemplo, este programa C imprime 13 en lugar de
la esperada 25 .
#define CINCO_VECES(x) 5 * x
int main() {
printf("%d\n", CINCO_VECES(2 + 3));
return 0;
}
Macros 298
El Lenguaje de Programacion Rust
macro_rules! cinco_veces {
($x:expr) => (5 * $x);
}
fn main() {
assert_eq!(25, cinco_veces!(2 + 3));
}
Otro problema común en los sistemas de macro es la "captura de variable". Aquí hay una
macro C, utilizando una extensión de GNU C para emular bloques de expresión de Rust.
#define LOG(msj) ({ \
int estado = obtener_estado_log(); \
if (estado > 0) { \
printf("log(%d): %s\n", estado, msj); \
} \
})
Lo anterior se expande a
Macros 299
El Lenguaje de Programacion Rust
fn main() {
let estado: &str = "estrías reticuladas";
log!(estado);
}
Esta versión funciona porque Rust tiene un sistema de macros higiénico. Cada expansión
de macro ocurre en un "contexto de sintaxis" distinto, y cada variable está asociada con el
contexto de sintaxis donde fue introducida. Es como si la variable estado dentro main
está pintado de un "color" diferente de la variable estado dentro de la macro, y por lo tanto
no entran en conflicto.
El sistema de macros de Rust también restringe la capacidad de las macros para introducir
nuevos enlaces en el sitio de invocación. Código como el siguiente no funcionará:
macro_rules! foo {
() => (let x = 3);
}
fn main() {
foo!();
println!("{}", x);
}
macro_rules! foo {
($v:ident) => (let $v = 3);
}
fn main() {
foo!(x);
println!("{}", x);
}
Macros 300
El Lenguaje de Programacion Rust
Lo anterior es válido para enlaces let y etiquetas de bucle, pero no para items. Así que el
siguiente código compila:
macro_rules! foo {
() => (fn x() { });
}
fn main() {
foo!();
x();
}
Macros recursivas
La expansión de una macro puede incluir más invocaciones a macro, incluyendo
invocaciones de la misma macro que esta siendo expandida. Estas macros recursivas son
útiles para el procesamiento de entrada con estructura de árbol, como se ilustra en esta
(simplista) taquigrafía HTML:
Macros 301
El Lenguaje de Programacion Rust
# #![allow(unused_must_use)]
macro_rules! write_html {
($w:expr, ) => (());
fn main() {
# // FIXME(#21826)
use std::fmt::Write;
let mut out = String::new();
write_html!(&mut out,
html[
head[title["Macros guide"]]
body[h1["Macros are the best!"]]
]);
assert_eq!(out,
"<html><head><title>Macros guide</title></head>\
<body><h1>Macros are the best!</h1></body></html>");
}
rustc ofrece dos extensiones de sintaxis que ayudan con la depuración de macros. Por
Macros 302
El Lenguaje de Programacion Rust
Mas información
El capitulo avanzado de macros entra en mas detalles acerca de la sintaxis de macros.
También describe como compartir macros entre diferentes crates y módulos.
Macros 303
El Lenguaje de Programacion Rust
% Apuntadores Planos
% Unsafe
unsafe fn peligro_will_robinson() {
// cosas peligrosas
}
Todas las funciones llamadas desde FFI deben ser marcadas como unsafe , por ejemplo.
El segundo uso de unsafe es un bloque unsafe:
unsafe {
// osas peligrosas
}
`unsafe` 305
El Lenguaje de Programacion Rust
Seguro, en el contexto de Rust, significa ‘no hacer nada inseguro’. Es importante saber que
hay ciertos comportamientos que son probablemente indeseables en tu código, pero son
expresamente no inseguros:
Deadlocks.
Perdida de memoria u otros recursos.
Salida sin llamada a los destructores.
Desbordamiento de enteros.
Rust no puede prevenir todos los tipos de problemas de software. Las cosas de la lista
anterior no son buenas, pero tampoco califican como unsafe específicamente.
En adición, los siguientes son todos comportamiento indefinido en Rust, y deben ser
evitadas, incluso cuando se escribe código unsafe :
Condiciones de carrera.
Deereferenciar un apuntador nulo/colgante.
Lectura de memoria undef (memoria no inicializada)
Violación de las reglas de aliasing de apuntadores a través de apuntadores planos.
&mut T y &T siguen el modelo noalias de LLVM, excepto cuando el &T contiene un
Superpoderes Unsafe
En ambos funciones y bloques unsafe, Rust te permitirá hacer tres cosas que normalmente
no podrías hacer. Solo tres. Y son:
`unsafe` 306
El Lenguaje de Programacion Rust
Eso es todo. Es importante destacar que unsafe , por ejemplo, no ‘apaga el comprobador
de prestamos’. Agregar de manera aleatoria unsafe a algún código no cambia su
semántica, no comenzara a aceptar algo. Pero te permitirá escribir cosas que si rompen
algunas de las reglas.
Esta habilidad es poderosa y variada. Rust expone algunos intrínsecos del compilador como
funciones unsafe, y algunas funciones unsafe hacen bypass de algunos chequeos de
seguridad, intercambiando seguridad por velocidad.
Lo repetiré de nuevo: aun y cuando puedes hacer cosas arbitrarias en bloques unsafe y
funciones no significa que debas hacerlo. El compilador actuara como si tu eres el
responsable estuvieses de mantener arriba todas las invariantes, así que debes ser
cuidadoso!
`unsafe` 307
El Lenguaje de Programacion Rust
% Rust Nocturno
Rust proporciona tres canales de distribución: nocturno (nightly), beta y estable (stable). Las
facilidades inestables están solo disponibles en Rust nocturno. Para mayor detalle acerca
de este proceso, véase ‘Estabilidad como un entregable’.
Si tienes alguna duda acerca de la [inseguridad potencial] del uso de curl | sh , por favor,
continua leyendo y encontraras nuestro disclaimer. Siéntete también en libertad de usar una
version de dos pasos de la instalación examinando nuestro script:
$ curl -f -L https://static.rust-lang.org/rustup.sh -O
$ sh rustup.sh --channel=nightly
Si estas en Windows, por favor descarga bien sea el instalador de 32 bits o el instalador de
64 bits y ejecutalo.
Desinstalando
Si has decidido que ya no quieres mas a Rust, estaremos un poco triste, pero esta bien. No
todos los lenguajes de programación son para todo el mundo. Simplemente ejecuta el script
de desinstalación:
$ sudo /usr/local/lib/rustlib/uninstall.sh
Algunas personas, y de alguna forma con derecho, se sienten perturbados cuando les
decimos que hagan curl | sh . Básicamente, cuando haces esto, estas confiando que la
buena gente que mantiene Rust no van a hackear tu computadora y hacer cosas malas.
Eso es =buen instinto! Si eres una de esas personas, por favor echa un vistazo a la
documentación acerca de la instalación de Rust desde fuentes. O las descargas de los
ejecutables oficiales.
$ rustc --version
Deberías ver el numero de version, el hash de commit, fecha del commit y fecha de
compilación:
Este instalador también instala una copia local de la documentación, de manera que puedas
leerla offline. En sistemas UNIX, la locación es /usr/local/share/doc/rust . En windows, se
ubica en el directorio share/doc , en donde sea que hayas instalado Rust.
Si no, hay una variedad de lugares en los que puedes solicitar ayuda. El mas fácil es el
canal #rust en irc.mozilla.org, el cual puedes acceder via Mibbit. Cliquea ese enlace, y
estarás chateando con Rutaceos (un tonto apodo con el que nos referimos a nosotros), y
podremos ayudarte. Otros muy buenos recursos incluyen el foro de usuarios, y Stack
Overflow.
Introducción
rustc puede cargar plugins de compilador, que son bibliotecas de usuario que extienden el
Un plugin puede ser una biblioteca dinámica con una función registradora designada se
encarga registrar la extension con rustc . Otros crates pueden cargar estas extensiones a
través del uso del atributo #![plugin(...)] . Dirígete a la documentación de rustc_plugin
para mayor detalle acerca de la mecánica de la definición y carga de un plugin.
De estar presentes, los argumentos pasados como #![plugin(foo(... args ...))] no son
interpretados por rustc. Son proporcionados al plugin a través del método args de
Registry .
En la gran mayoría de los casos, un plugin solo debería ser usado a través de #![plugin]
y no por medio de un item extern crate . Enlazar un plugin traería a libsyntax y librustc
como dependencias de tu crate. Esto es generalmente indeseable a menos que estés
construyendo un plugin. El lint plugin_as_library chequea estos lineamientos.
La practica usual es colocar a los plugins del compilador en su propio crate, separados de
cualquier macro macro_rules! o código Rust ordinario que vayan a ser usados por los
consumidores de la biblioteca.
Extensiones de sintaxis
Los plugins pueden extender la sintaxis de Rust de varias maneras. Una forma de extension
de sintaxis son las macros procedurales. Estas son invocadas de la misma forma que las
macros ordinarias, pero la expansión es llevada a cabo por código Rust arbitrario que
manipula arboles de sintaxis en tiempo de compilación.
#![crate_type="dylib"]
#![feature(plugin_registrar, rustc_private)]
use syntax::codemap::Span;
use syntax::parse::token;
use syntax::ast::TokenTree;
use syntax::ext::base::{ExtCtxt, MacResult, DummyResult, MacEager};
use syntax::ext::build::AstBuilder; // trait para expr_usize
use rustc_plugin::Registry;
if args.len() != 1 {
cx.span_err(
sp,
&format!("argument should be a single identifier, but got {} arguments", args.len()));
return DummyResult::any(sp);
}
MacEager::expr(cx.expr_usize(sp, total))
}
#[plugin_registrar]
pub fn plugin_registrar(reg: &mut Registry) {
reg.register_macro("rn", expand_rn);
}
#![feature(plugin)]
#![plugin(roman_numerals)]
fn main() {
assert_eq!(rn!(MMXV), 2015);
}
Las ventajas de esto por encima de una simple fn(&str) -> u32 son:
En adicion a las macros procedurales, puedes definir attributos derive y otros tipos de
expansiones. Dirigete a Registry::register_syntax_extension y a el enum
SyntaxExtension . Para un ejemplo mas involucrado con macros, ve a regex_macros .
Tips y trucos
Algunos de los tips de depuración de macros son aplicables.
Mirar a el código del parser libsyntax te dara una idea de como funciona la infraestructura
de parseo.
Mantén los Span s de todo lo que parsees, para un mejor reporte de errores. Puedes
envolver Spanned alrededor de tus estructuras de datos,
Para imprimir fragmentos de sintaxis para depuración, pudes usar span_note en conjunción
con syntax::print::pprust::*_to_string .
Plugins lint
Los plugins pueden extender la infraestructura de lints de Rust con chequeos adicionales
para estilo de código, seguridad, etc. Escribamos ahora un plugin lint_plugin_test.rs que
nos advierte acerca de cualquier item llamado lintme .
#![feature(plugin_registrar)]
#![feature(box_syntax, rustc_private)]
struct Pass;
#[plugin_registrar]
pub fn plugin_registrar(reg: &mut Registry) {
reg.register_early_lint_pass(box Pass as EarlyLintPassObject);
}
#![plugin(lint_plugin_test)]
fn lintme() { }
Los passes lint son recorridos de sintaxis, pero corren en una etapa de la compilación en la
que la información de tipos esta disponible. Los lints integrados de Rust usan mayormente
la infraestructura de los plugins lint, y proveen ejemplos de como acceder a la información
de tipos.
Los lints definidos por plugins son controlados por los flags y atributos usuales del
compilador, e.j #[allow(test_lint)] o -A test-lint . estos identificadores son derivados
del primer argumentos a declare_lint! , con las conversiones de capitalización y
puntuación apropiadas.
Puedes ejecutar rustc -W help foo.rs para ver una lista de los lints que rustc conoce,
incluyendo aquellos proporcionados por plugins cargados por foo.rs .
% Ensamblador en linea
Para manipulaciones de muy bajo nivel y por razones de desempeño, uno podría desear
controlar la CPU de manera directa. Para esto Rust soporta el uso de ensamblador en linea
a través de la macro asm! . La sintaxis es parecida a la de GCC & Clang:
asm!(plantilla ensamblador
: operandos de salida
: operandos de entrada
: clobbers
: opciones
);
Cualquier uso de asm esta protegido por puertas de feature (requiere #![feature(asm)] en
el crate) y por supuesto requiere de un bloque unsafe .
Plantilla ensamblador
La plantilla ensamblador es el único parámetro mandatorio y debe ser un literal de cadena
de caracteres (e.j. "" )
#![feature(asm)]
// otras plataformas
#[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))]
fn foo() { /* ... */ }
fn main() {
// ...
foo();
// ...
}
Los operandos de salida, operandos de entrada, clobbers y opciones son todos opcionales
pero debes agregar el numero correcto de : en caso de que desees saltarlos:
# #![feature(asm)]
# #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
# fn main() { unsafe {
asm!("xor %eax, %eax"
:
:
: "{eax}"
);
# } }
# #![feature(asm)]
# #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
# fn main() { unsafe {
asm!("xor %eax, %eax" ::: "{eax}");
# } }
Operandos
Los operandos de entrada y salida siguen el mismo formato: restriccion1"(expr1),
"restriccion2"(expr2), ..." . Las expresiones de operandos de salida deben ser valores
# #![feature(asm)]
# #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
fn add(a: i32, b: i32) -> i32 {
let c: i32;
unsafe {
asm!("add $2, $0"
: "=r"(c)
: "0"(a), "r"(b)
);
}
c
}
# #[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))]
# fn add(a: i32, b: i32) -> i32 { a + b }
fn main() {
assert_eq!(add(3, 14159), 14162)
}
Sin embargo, Si desearas usar operandos reales en esta posición, seria obligatorio colocar
llaves {} alrededor del registro que deseas, y también estarías obligado a colocar el
tamaño especifico del operando. Lo anterior es muy util para programación de muy bajo
nivel, para la que es importante cual registro es el que sea usa.
# #![feature(asm)]
# #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
# unsafe fn read_byte_in(puerto: u16) -> u8 {
let resultado: u8;
asm!("in %dx, %al" : "={al}"(resultado) : "{dx}"(puerto));
resultado
# }
Clobbers
Algunas instrucciones modifican registros los cuales de otra forma hubieran mantenido
valores diferentes es por ello que usamos las lista de clobbers para indicar al compilador a
que no asuma que ningún valor cargado en dichos registros se mantendrá valido.
# #![feature(asm)]
# #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
# fn main() { unsafe {
// Colocando el valor 0x200 en eax
asm!("mov $$0x200, %eax" : /* sin salidas */ : /* sin entradas */ : "{eax}");
# } }
Los registros de entrada y salida no necesitan ser listados puesto a que esa información ya
esta comunicada por las determinadas restricciones. De lo contrario, cualquier otro registro
usado ya sea de manera explicita o implícita debe ser listado.
Opciones
La ultima sección, opciones es específica de Rust. El formato es una lista de cadenas de
caracteres separadas por comma (e.j: :"foo", "bar", "baz" ). Es usado para especificar
información extra acerca del ensamblador:
# #![feature(asm)]
# #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
# fn main() {
let resultado: i32;
unsafe {
asm!("mov eax, 2" : "={eax}"(resultado) : : : "intel")
}
println!("eax es actualmente {}", resultado);
# }
Mas Información
La implementación actual de la macro asm! es un binding directo a las expresiones
ensamblador en linea de LLVM, así que asegurate de echarle un vistazo a su
documentación para mayor información acerca de clobbers, restricciones, etc.
% No stdlib
No stdlib 320
El Lenguaje de Programacion Rust
% Intrínsecos
Nota: los intrínsecos por siempre tendrán una interfaz inestable, se recomienda usar
las interfaces estables de libcore en lugar de intrínsecos directamente.
Estas son importadas como si fuesen funciones FFI, con el ABI especial rust-intrinsic .
Por ejemplo, si uno estuviese en un contexto libre, pero desease poder hacer transmute
entre tipos, y llevar a cabo aritmética de apuntadores eficiente, uno importaría dichas
funciones via una declaración como:
#![feature(intrinsics)]
# fn main() {}
extern "rust-intrinsic" {
fn transmute<T, U>(x: T) -> U;
Al igual que cualquier otra función FFI, estas son siempre unsafe de llamar.
Intrínsecos 321
El Lenguaje de Programacion Rust
% Items de Lenguaje
% Enlace Avanzado
Los casos comunes de enlace con Rust han sido cubiertos anteriormente en este libro, pero
soportar el rango de posibilidades disponibles en lenguajes es importante para Rust para
lograr una buena interacción con bibliotecas nativas.
Argumentos de enlace
Hay otra forma de decirle a rustc como personalizar el enlazado, y es via el atributo
link_args . Este atributo es aplicado a los bloques extern y especifica flags planos que
#![feature(link_args)]
Nota que actualmente esta facilidad esta escondida detrás la puerta feature(link_args)
debido a que no es una forma sancionada de efectuar enlazado. Actualmente rutsc
envuelve a el enlazador del sistema ( gcc en la mayoría de los sistemas, link.exe en
MSVC), es por ello que tiene sentido proveer argumentos de linea de comando extra, pero
no siempre sera este el caso. En el futuro rustc pudiera usar LLVM directamente para
enlazar con bibliotecas nativas, escenario en donde link_args no tendría sentido. Puedes
lograr el mismo efecto del atributo link_args con el argumento -C link-args de rustc .
Enlazado estatico
El enlace estático se refiere a el proceso de crear una salida que contenga todas las
bibliotecas requeridas y por lo tanto no requiera que las bibliotecas estén instaladas en cada
sistema en el que desees usar tu proyecto compilado. La dependencias puras Rust son
enlazadas estáticamente por defecto de manera que puedas usar las bibliotecas y
ejecutables creados sin instalar Rust en todos lados. En contraste, las bibliotecas nativas
(e.j libc y libm ) son usualmente enlazadas de manera dinámica, pero esto se puede
cambiar y enlazarlas también de manera estática.
Linux
Por defecto, todos los programas en Linux se enlazaran con la biblioteca libc del sistema
en conjunto con otro grupo de bibliotecas. Veamos un ejemplo en una maquina linux de 64
bits con GCC y glibc (por lejos la libc mas común en Linux):
$ cat example.rs
fn main() {}
$ rustc example.rs
$ ldd example
linux-vdso.so.1 => (0x00007ffd565fd000)
libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007fa81889c000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007fa81867e000)
librt.so.1 => /lib/x86_64-linux-gnu/librt.so.1 (0x00007fa818475000)
libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007fa81825f000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fa817e9a000)
/lib64/ld-linux-x86-64.so.2 (0x00007fa818cf9000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007fa817b93000)
El enlace dinámico en linux puede ser indeseable si deseas usar las facilidades de la nueva
biblioteca en sistemas viejos o sistemas destino que no poseen las dependencias
requeridas para ejecutar el programa.
El enlace estático es soportado a través de una libc alternativa, musl . Puedes compilar
tu version de Rust con musl habilitado e instalarlo en un directorio personalizado con las
siguientes instrucciones:
$ mkdir musldist
$ PREFIX=$(pwd)/musldist
$
$ # Construir musl
$ curl -O http://www.musl-libc.org/releases/musl-1.1.10.tar.gz
$ tar xf musl-1.1.10.tar.gz
$ cd musl-1.1.10/
musl-1.1.10 $ ./configure --disable-shared --prefix=$PREFIX
musl-1.1.10 $ make
musl-1.1.10 $ make install
musl-1.1.10 $ cd ..
$ du -h musldist/lib/libc.a
2.2M musldist/lib/libc.a
$
$ # Construir libunwind.a
$ curl -O http://llvm.org/releases/3.7.0/llvm-3.7.0.src.tar.xz
$ tar xf llvm-3.7.0.src.tar.xz
$ cd llvm-3.7.0.src/projects/
llvm-3.7.0.src/projects $ curl http://llvm.org/releases/3.7.0/libunwind-3.7.0.src.tar.xz | tar xJf -
llvm-3.7.0.src/projects $ mv libunwind-3.7.0.src libunwind
llvm-3.7.0.src/projects $ mkdir libunwind/build
llvm-3.7.0.src/projects $ cd libunwind/build
llvm-3.7.0.src/projects/libunwind/build $ cmake -DLLVM_PATH=../../.. -DLIBUNWIND_ENABLE_SHARED=0 ..
llvm-3.7.0.src/projects/libunwind/build $ make
llvm-3.7.0.src/projects/libunwind/build $ cp lib/libunwind.a $PREFIX/lib/
llvm-3.7.0.src/projects/libunwind/build $ cd ../../../../
$ du -h musldist/lib/libunwind.a
164K musldist/lib/libunwind.a
$
$ # Construir Rust con musl habilitado
$ git clone https://github.com/rust-lang/rust.git muslrust
$ cd muslrust
muslrust $ ./configure --target=x86_64-unknown-linux-musl --musl-root=$PREFIX --prefix=$PREFIX
muslrust $ make
muslrust $ make install
muslrust $ cd ..
$ du -h musldist/bin/rustc
12K musldist/bin/rustc
Ahora posees un Rust con musl habilitado! Y debido a que lo hemos instalado en un
prefijo personalizado necesitamos asegurarnos de que nuestro sistema pueda encontrar los
ejecutables y bibliotecas apropiadas cuando tratemos de ejecutarlo:
$ export PATH=$PREFIX/bin:$PATH
$ export LD_LIBRARY_PATH=$PREFIX/lib:$LD_LIBRARY_PATH
Probémoslo!
Exito! Este binario puede ser copiado a casi cualquier maquina Linux con la misma
arquitectura y ser ejecutado sin ningún problema.
cargo build también permite la opción --target por medio de la cual puedes construir tus
crates normalmente. Sin embargo, podrías necesitar recompilar tus bibliotecas nativas con
musl antes de poder enlazar con ellas.
% Patrones de Slice
% Constantes Asociadas
#![feature(associated_consts)]
trait Foo {
const ID: i32;
}
fn main() {
assert_eq!(1, i32::ID);
}
#![feature(associated_consts)]
trait Foo {
const ID: i32;
}
resulta en
#![feature(associated_consts)]
trait Foo {
const ID: i32 = 1;
}
fn main() {
assert_eq!(1, i32::ID);
assert_eq!(5, i64::ID);
}
Como puedes ver, al implementar Foo , puedes dejarlo sin implementación, como en el
caso de i32 . Entonces este usara el valor por defecto. Pero, también y al igual que como
en i64 , podemos agregar nuestra propia definición.
Las constantes asociadas no necesitan solo funcionan con traits. Un bloque impl para una
struct o un enum es también valido:
#![feature(associated_consts)]
struct Foo;
impl Foo {
const FOO: u32 = 3;
}
% Investigacion Academica