Esta es una continuación de mi artículo anterior, en el que exploré algunos aspectos de Kotlin de los que Rust podría aprender. Esta vez, voy a ver algunas características que realmente disfruto en Rust y que desearía que Kotlin adoptara.
Antes de comenzar, me gustaría reiterar que mi punto no es iniciar una guerra lingüística entre los dos idiomas, ni estoy tratando de convertir un idioma en el otro. Dediqué mucho tiempo a analizar qué características quiero discutir y automáticamente excluí aquellas que tienen perfecto sentido para un idioma y serían absurdas en el otro. Por ejemplo, sería una tontería solicitar recolección de basura en Rust (ya que su propuesta principal es un control muy estricto sobre la asignación de memoria) y, recíprocamente, no tendría sentido que Kotlin adoptara un verificador de préstamos, ya que el hecho de que Kotlin sea La basura recogida es uno de sus principales atractivos.
Las características que cubrí en mi primer artículo y en este son funcionalidades que creo que podrían ser adoptadas por cualquiera de los lenguajes sin poner en peligro sus principales filosofías de diseño, aunque como no conozco las partes internas de ninguno de los dos lenguajes, podría estar equivocado en algunas de ellas. estos, y agradezco comentarios y correcciones.
Vamos a profundizar en.
Macros
Siempre he tenido una relación de amor y odio con las macros en los idiomas, especialmente los no higiénicos. Como mínimo, las macros deberían estar completamente integradas en el lenguaje, lo que requiere dos condiciones:
- El compilador debe conocer las macros (a diferencia, por ejemplo, del preprocesador en C y C++).
- Las macros deben tener acceso completo a un AST tipado estáticamente y poder modificar este AST de forma segura.
Las macros de Rust cumplen con estos dos requisitos y, como resultado, desbloquean un conjunto de capacidades muy interesantes, que estoy bastante seguro de que apenas hemos comenzado a explorar.
Por ejemplo, el dbg!()
macro:
let a = 2;
let b = 3;
dbg!(a + b);
imprimirá
[src\main.rs:158] a + b = 5
Nota: no solo el archivo fuente y el número de línea, sino también la expresión completa que se muestra (“a + b
”).
Otro gran ejemplo del poder de las macros se puede ver en el debug_plotter
caja, que le permite trazar variables:
fn main() {
for a in 0..10 {
let b = (a as f32 / 2.0).sin() * 10.0;
let c = 5 - (a as i32);
debug_plotter::plot!(a, b, c; caption = "My Plot");
}
}
¿Qué tan hermoso y geek es eso?
Kotlin no está completamente desarmado en este departamento ya que la combinación de anotaciones y procesadores de anotaciones proporciona un conjunto de funcionalidades que no están muy lejos de lo que puedes hacer en Rust con macros y atributos. La principal diferencia es que, mientras que el enfoque de Kotlin solo permite que el código Kotlin legal esté presente en un archivo fuente de Kotlin, Rust permite que cualquier sintaxis arbitraria aparezca como un argumento de macro, y depende de la macro generar Rust correcto que el compilador Va a aceptar.
Debo admitir que no estoy del todo decidido sobre este aspecto en particular.
Por un lado, es bueno poder escribir cualquier tipo de código en un archivo fuente de Rust (esto es lo que hace React con JSX), por otro lado, el potencial de abuso es alto y uno puede temer con razón el día en que un El archivo fuente de Rust no se parecería en nada al código de Rust. Sin embargo, hasta ahora, mi miedo nunca se ha materializado y la mayoría de las macros que he encontrado utilizan sintaxis personalizadas de forma muy parsimoniosa.
Otro aspecto muy importante de las macros es que los IDE de Rust las entienden (bueno, al menos CLion las entiende, y potencialmente todos los IDE pueden hacerlo, y lo harán) y le mostrarán errores inmediatamente cuando algo vaya mal.
Las macros se utilizan en una gran variedad de escenarios y proporcionan a Rust algunas capacidades DSL realmente interesantes (por ejemplo, para bibliotecas que admiten SQL, Web, gráficos, etc.).
Además, las macros se integran muy bien con…
Atributos para el preprocesamiento
Los atributos son la versión de anotaciones de Rust y comienzan con #
o #!
:
#![crate_type = "lib"]
#[test]
fn test_foo() {}
No hay nada innovador aquí, pero lo que quiero discutir es el aspecto de la compilación condicional.
La compilación condicional se logra en Rust combinando atributos y macros con cfg
que está disponible como atributo y macro.
La versión macro le permite compilar condicionalmente una declaración o una expresión:
#[cfg(target_os = "macos")]
fn macos_only() {}
En el código anterior, la función macos_only()
Sólo se compilará si el sistema operativo es macOS.
La versión macro de cfg()
le permite agregar más lógica a la condición:
let machine_kind = if cfg!(unix) {
"unix"
} else { … }
A riesgo de repetirme: el código anterior es una macro, lo que significa que se evalúa en tiempo de compilación. El compilador ignorará por completo cualquier parte de la condición que no esté prevista.
Quizás te preguntes, con razón, si dicha característica es necesaria en Kotlin, y yo me hice la misma pregunta.
Rust compila en ejecutables nativos, en múltiples sistemas operativos, lo que hace que este tipo de compilación condicional sea prácticamente un requisito si desea publicar artefactos en múltiples objetivos. Kotlin no tiene este problema ya que produce ejecutables neutrales del sistema operativo que se ejecutan en la JVM.
Aunque los desarrolladores de Java y Kotlin han aprendido a prescindir de un preprocesador desde que el preprocesador C dejó tan mala impresión en casi todos los que lo han usado, ha habido situaciones en mi carrera en las que poder tener una compilación condicional que incluya o excluya el código fuente. archivos, o incluso simplemente declaraciones, expresiones o funciones, habrían sido útiles.
Independientemente de su posición en este debate, debo decir que realmente disfruto cómo dos características muy diferentes en el ecosistema de Rust, las macros y los atributos, pueden trabajar juntas para producir una característica tan útil y versátil.
Rasgos de extensión
Los rasgos de extensión le permiten hacer que una estructura se ajuste a un rasgo «después del hecho», incluso si no posee ninguno de estos. Vale la pena repetir este último punto: no importa si la estructura o el rasgo pertenecen a bibliotecas que usted no escribió. Aún podrás hacer que esa estructura se ajuste a ese rasgo.
Por ejemplo, si queremos implementar un last_digit()
función en el tipo u8
:
trait LastDigit {
fn last_digit(&self) -> u8;
}
impl LastDigit for u8 {
fn last_digit(&self) -> u8 {
self % 10
}
}
fn main() {
println!("Last digit for 123: {}", 123.last_digit());
// prints “3”
}
Podría ser parcial acerca de esta característica porque, a menos que me equivoque, fui la primera persona en sugerir una funcionalidad similar para Kotlin en 2016 (enlace a la discusión).
En primer lugar, encuentro la sintaxis de Rust elegante y minimalista (incluso mejor que la de Haskell y posiblemente mejor que la que propuse para Kotlin). En segundo lugar, poder ampliar los rasgos de esta manera desbloquea mucha extensibilidad y potencia en la forma de modelar problemas, pero no voy a profundizar demasiado en este tema ya que llevaría demasiado tiempo (busque «clases de tipo» para tener una idea de lo que puedes lograr).
Este enfoque también permite a Rust imitar las funciones de extensión de Kotlin y al mismo tiempo proporciona un mecanismo más general para extender no solo funciones sino también tipos, a expensas de una sintaxis un poco más detallada.
En pocas palabras, tienes la siguiente matriz:
Kotlin | Óxido | |
Función de extensión | fun Type.function() {...} |
Rasgo de extensión |
Rasgo de extensión | N / A | Rasgo de extensión |
carga
Esto probablemente resulte una sorpresa, ya que con Gradle, Kotlin tiene un administrador de paquetes y compilaciones muy sólido. Las dos herramientas ciertamente tienen la misma superficie funcional, lo que permite construir proyectos complejos y al mismo tiempo administrar la descarga de bibliotecas y la resolución de dependencias.
La razón por la que pienso cargo
es una alternativa superior a Gradle debido a su clara separación entre la sintaxis declarativa y su lado imperativo. En pocas palabras, las directivas de compilación estándar y comunes se especifican en la declaración cargo.toml
mientras que el archivo es ad hoc, se escriben más pasos de compilación programáticos directamente en Rust en un archivo llamado build.rs
utilizando código Rust llamando a una API de compilación bastante liviana.
Por el contrario, Gradle es un desastre. Primero, porque comenzó a especificarse en Groovy y ahora admite Kotlin como lenguaje de compilación (y esta transición aún continúa, años después de que comenzó), pero también porque la documentación de ambos sigue siendo increíblemente mala.
Por «mala», no me refiero a «falta»: hay mucha documentación, simplemente es… mala, abrumadora, la mayor parte desactualizada o obsoleta, etc…, lo que requiere cientos de líneas de copiar y pegar desde StackOverflow como tan pronto como necesite algo fuera de lo común. El sistema de complementos está definido de manera muy vaga y básicamente permite que todos los complementos accedan a lo que quieran dentro de las estructuras internas de Gradle.
Obviamente, soy bastante obstinado sobre este tema ya que creé una herramienta de compilación inspirada en Gradle pero usando enfoques más modernos de sintaxis y resolución de complementos (se llama Kobalt), pero independientemente de esto, creo cargo
logra lograr un equilibrio muy fino entre una herramienta flexible de compilación + administrador de dependencias que cubre adecuadamente toda la configuración predeterminada sin ser abrumadoramente compleja tan pronto como su proyecto crece.
u8, u16,…
En Rust, los tipos de números son bastante sencillos: u8
es un entero sin signo de 8 bits, i16
es un entero de 16 bits con signo, f32
es un flotante de 32 bits, etc.
Esto es un soplo de aire fresco para mí. Hasta que comencé a usar estos tipos, nunca había identificado completamente lo incómodo que siempre me había sentido con la forma en que C, C++, Java, etc. definen estos tipos. Siempre que necesitaba un número, lo usaba int
o Long
por defecto. En C, a veces llegaba tan lejos como long long
sin entender realmente las implicaciones.
Rust me obliga a prestar mucha atención a todos estos tipos y luego, el compilador me mantendrá implacablemente honesto cada vez que intente realizar conversiones que puedan generar errores. Realmente creo que todos los lenguajes modernos deberían seguir esta convención.
Mensajes de error del compilador
No quiere decir que los mensajes de error de Kotlin sean malos, pero Rust ciertamente estableció un nuevo estándar aquí, en múltiples dimensiones.
En pocas palabras, esto es lo que puede esperar del compilador Rust:
- Gráficos ASCII con flechas, colores y delimitaciones claras de secciones problemáticas.
- Inglés sencillo y mensajes de error detallados.
- Sugerencias sobre cómo podría solucionar el problema.
- Enlaces a documentación relevante donde puede encontrar más información sobre el problema.
Ciertamente espero que los idiomas futuros se inspiren.
Portabilidad
Hace unos veinticinco años, cuando apareció Java, la JVM hizo una promesa: «Escribe una vez, ejecuta en cualquier lugar» («WORA»).
Si bien esta promesa se mantuvo inestable en los primeros años, no se puede negar que WORA es una realidad hoy, y lo ha sido durante un par de décadas. El código JVM no sólo se puede escribir una vez y ejecutar en todas partes, sino que dicho código también se puede escribir en cualquier lugar, lo que representa un importante aumento de productividad para los desarrolladores. Puede escribir su código en cualquiera de Windows, macOS, Linux e implementarlo en cualquiera de Windows, macOS y Linux.
Sorprendentemente, Rust también es capaz de tal versatilidad, aunque produce ejecutables nativos. Independientemente del sistema operativo en el que esté escribiendo su código, producir ejecutables para una multitud es trivial, con el beneficio adicional de que estos ejecutables son nativos y, gracias al increíble logro técnico que es LLVM, también tienen un gran rendimiento.
Antes de Rust, me había resignado al hecho de que si quería ejecutar varios sistemas operativos, tenía que pagar el precio de ejecutarlo en una máquina virtual, pero Rust ahora está demostrando que puedes quedarte con el pastel y comértelo también.
Kotlin (y la JVM en general) también está empezando a aprender esta lección, con iniciativas como GraalVM, pero la producción de ejecutables para código JVM todavía está plagada de restricciones y limitaciones.
Terminando
Tengo mucho más que decir sobre todo esto.
Y por «todo esto» me refiero a «Rust y Kotlin».
Ambos son idiomas muy interesantes. Me gustan ambos, pero por diferentes razones. Espero haber podido transmitir algo de mi cariño en estos dos posts. Aunque estos artículos puedan parecer críticos, en realidad son cartas de amor. Soy un desarrollador muy exigente, alguien que lleva cuarenta años escribiendo código y que piensa seguir haciéndolo mientras sus capacidades mentales se lo permitan. Siento una pasión irracional por los lenguajes de programación y espero que mi pasión brille a través de estos dos artículos.
TestNG es un proyecto que comencé alrededor de 2004 con la única intención de mezclar las cosas. Quería mostrarle al mundo Java que podíamos hacerlo mejor que JUnit. No tenía intenciones de que a nadie le gustara y adoptara TestNG: era un laboratorio de proyectos. Un experimento. Todo lo que quería hacer era demostrar que podíamos hacerlo mejor. Realmente esperaba que el equipo JUnit (o lo que quedara de él) echara un vistazo a TestNG y pensara: “¡Guau, nunca pensé en eso! ¡Podemos incorporar estas ideas en JUnit y hacerlo aún mejor!”.
Este es mi objetivo con este par de publicaciones. Estaría encantado si estos dos mundos muy, muy diferentes (las comunidades Rust y Kotlin) se detuvieran por un segundo en su vertiginoso ritmo de desarrollo, se miraran rápidamente, aunque realmente no tuvieran ningún interés en hacerlo, y darse cuenta de “bueno… eso es interesante… ¿Me pregunto si podríamos hacer esto?”.
Discusiones en reddit:
Esta entrada se publicó el 9 de noviembre de 2021 a las 3:58 am y está guardada en Sin categoría. Puede seguir cualquier respuesta a esta entrada a través de RSS 2.0. Ambos comentarios y pings están actualmente cerrados.
Source link