[Update: Part Two is up]
Cuando comencé a estudiar Rust, no esperaba que me gustara.
Desde que escuché sobre esto, hace muchos años, siempre he tenido un enorme respeto por la filosofía de diseño y el objetivo que Rust intentaba lograr, mientras pensaba que este no es el lenguaje para mí. Cuando pasé de diez años de espantoso C++ a Java en 1996, me di cuenta de que ya no estaba interesado en los lenguajes de bajo nivel, especialmente los lenguajes que me obligan a preocuparme por la memoria. Mi cerebro está conectado de cierta manera, y de esa manera toma en cuenta consideraciones de bajo nivel, especialmente la gestión de la memoria, el tipo de problema en el que obtengo poco placer trabajando.
Irónicamente, a pesar de mi gran enfoque en los lenguajes de alto nivel, todavía mantengo una saludable fascinación por los problemas de muy bajo nivel, como los emuladores (hasta ahora escribí tres: CHIP-8, 8080 Space Invaders y Apple).[, all of which required me to become completely fluent in various assembly languages), but for some reason, languages that force me to care about memory management have always left me in a state of absolute dismissal. I just don’t want to deal with memory, ok?
But… I felt bad about it. Not just because the little Rust I knew piqued my curiosity, but also because I thought it would be a learning exercise to embrace its design goal and face memory management heads on. So I eventually decided to learn Rust, more out of curiosity and to expand my horizons than to actually use it. And what I found surprised me.
I ended up liking writing code in it quite a bit. For a so-called “system language”, its design was a breath of fresh air and proof that its creators had not only a vision but also a healthy knowledge of programming language theory, which was refreshing after seeing some… other languages that have appeared in the past fifteen years. I will resist giving names, but I’m sure you know what I’m talking about.
This article is not intended to start a language war. I love both Kotlin and Rust. They are both great languages, both with some flaws, and I am extremely happy to be able to claim a decent understanding of both of them, and to feel equally comfortable to start new projects in either, whichever is the best language for the job.
But these languages have followed different paths and ended up making different compromises, which makes their simultaneous study and comparison extremely interesting to me.
It took me a while to select which features I wanted to include in this list but eventually, I narrowed my selection criterion to a very simple one: it has to be a feature that will not get in the way of Rust’s main value propositions (close to optimal memory management, zero cost abstractions). That’s it.
I think all the features that I describe in this article are of the cosmetic, but crucial, variety. They will enhance the readability and writability of the language without compromising Rust’s relentless pursuit of zero cost memory management. However, since I’m obviously not familiar with the internals of the Rust compiler, some of these might indeed compromise Rust’s laser focus on optimal memory management, in which case I’d love to be corrected.
Enough preamble, let’s dig in. To give you an idea of what lies ahead, here are the Kotlin features that I’ll be discussing below:
- Short constructor syntax
- Overloading
- Default parameters
- Named parameters
- Enums
- Properties
Constructors and default parameters
Let’s say we want to create a Window with coordinates and a boolean
visibility attribute, which defaults to false
. Here is what it looks like in Rust:
struct Window { x: u16,
y: u16,
visible: bool,
}
impl Window {
fn new_with_visibility(x: u16, y: u16, visible: bool) -> Self {
Window {
x, y, visible
}
}
fn new(x: u16, y: u16) -> Self {
Window::new_with_visibility(x, y, false)
}
}
And now in Kotlin:
class Window(x: Int, y: Int, visible: Boolean = false)
That’s a huge difference. Not just in line count, but in cognitive overload. There is a lot to parse in Rust before you conceptually understand what this class is and does, whereas reading one line in Kotlin immediately gives you this information.
Admittedly, this is a pathological case for Rust since this simple example contains all the convenient syntactic sugaring that it’s lacking, namely:
- A compact constructor syntax
- Overloading
- Default parameters
Even Java scores better than Rust here, since it supports at least overloading (but fails on the other two features).
Even to this day, I whine whenever I have to write all this boilerplate in Rust, because you write this kind of code all the time. After a while, it becomes a second nature to parse it, a bit like when you see getters and setters in Java, but it’s still unnecessary cognitive overload, which Kotlin has solved elegantly.
The lack of overloading is the most baffling to me. First, this forces me to come up with unique names, but mostly because it’s a compiler feature that’s pretty trivial to implement in general, which is why most (all?) mainstream languages created these past twenty years support it. Why force the developer to come up with new names while the compiler can do it automatically, and by doing so, reduce the cognitive load on developers and make the code easier to read?
The common counter argument to overloading is about interoperability: once the compiler generates mangled function names, it can become tricky to call these functions from other processes or from other languages. But this objection is trivially resolved by allowing the developer to disable name mangling for specific cases (which is exactly what Kotlin does, and its interoperability with Java is outstanding). Since Rust already relies heavily on attributes, a #[no_mangle] El atributo encajaría perfectamente (y adivina qué, ya se ha discutido).
Parámetros con nombre
Esta es una característica que considero más «agradable de tener» que realmente esencial, pero los parámetros con nombres opcionales también pueden contribuir a reducir una gran cantidad de texto repetitivo. Son especialmente eficaces para reducir la necesidad de patrones de construcción, ya que ahora puede limitar el uso de este patrón de diseño a la validación de parámetros, en lugar de necesitarlo tan pronto como necesite construir estructuras complejas.
Aquí nuevamente, Kotlin alcanza un punto óptimo al permitir nombrar parámetros pero no exigir que uses estos nombres todo el tiempo (un error que cometieron tanto Smalltalk como Objective C). Por lo tanto, obtienes lo mejor de ambos mundos: la mayoría de las veces, invocar una función es bastante intuitivo sin nombrar los parámetros, pero de vez en cuando resultan muy útiles para eliminar la ambigüedad de firmas complejas.
Por ejemplo, imagine que agregamos un valor booleano a la estructura de Ventana anterior para indicar si nuestra ventana es en blanco y negro:
class Window(x: Int, y: Int, visible: Boolean = false, blackAndWhite: Boolean = false)
Sin parámetros con nombre, las llamadas al constructor pueden resultar ambiguas para el lector:
val w = Window(0, 0, false, true) // mmmh, which boolean means what?
Kotlin le permite mezclar parámetros con nombre y sin nombre para eliminar la ambigüedad de la llamada:
val w = Window(0, 0, visible = false, blackAndWhite = true)
Tenga en cuenta que en este código, x
y y
no se nombran explícitamente (porque se supone que su significado es obvio), pero los parámetros booleanos sí lo son.
Como beneficio adicional, los parámetros con nombre se pueden usar en cualquier orden, lo que reduce la carga cognitiva del desarrollador, ya que ya no es necesario recordar en qué orden se definen estos parámetros. Tenga en cuenta también que esta característica se combina armoniosamente con los parámetros predeterminados:
// skip 'visible', use its default value
val w = Window(0, 0, blackAndWhite = true)
En ausencia de esta característica, deberá definir un constructor adicional en su estructura Rust, uno para cada combinación de parámetros que desee admitir. Si lleva la cuenta, ahora necesita cuatro constructores:
- x,y
- x, y, visible
- x, y, blanco_y_negro
- x, y, visible, blanco_y_negro
Puede ver cómo esto conduce rápidamente a una explosión combinatoria de funciones para algo que, de manera realista, solo debería tomar una línea de código, como lo demuestra Kotlin.
Enumeraciones
A pesar de todas las críticas (en su mayoría justificadas) que recibe Java debido a su diseño, hay algunas características que admite que posiblemente sean las mejores en su clase y, en mi opinión, las enumeraciones de Java (y por extensión, también las de Kotlin) son las mejores. enumeraciones diseñadas que he usado alguna vez.
Y la razón es simple: las enumeraciones de Java/Kotlin están muy cerca de ser clases regulares, con todas las ventajas que estas clases traen, siendo las enumeraciones de Kotlin un superconjunto de las de Java, por lo que son aún más poderosas y flexibles.
Las enumeraciones de Rust son casi tan buenas, pero omiten un componente crítico que las hace no tan prácticas como las de Java: no admiten valores en su constructor.
Daré un ejemplo rápido. Uno de mis proyectos recientes fue escribir un emulador de Apple][queincluyeunemuladordeprocesador6502Laemulacióndeprocesadoresunproblemabastantefácilderesolver:definecódigosdeoperaciónconsuvalorhexadecimalrepresentacióndecadenaytamañoeimplementauninterruptorgiganteparahacercoincidirlosbytesqueleedelarchivoconestoscódigosdeoperación[emulatorwhichincludesa6502processoremulatorProcessoremulationisaprettyeasyproblemtosolve:youdefineopcodeswiththeirhexadecimalvaluestringrepresentationandsizeandyouimplementagiantswitchtomatchthebytesthatyoureadfromthefileagainsttheseopcodes
En Kotlin, puedes definir estos códigos de operación como enumeraciones de la siguiente manera:
enum class Opcode(val opcode: Int, val opName: String, val size: Int) {
BRK(0x00, "BRK", 1),
JSR(0x20, "JSR", 3)
//...
}
Si bien las enumeraciones de Rust son bastante poderosas en general (especialmente cuando se combinan con la sintaxis de coincidencia destructiva de Rust), solo le permiten definir firmas para cada una de sus instancias de enumeración (que Kotlin también admite), pero no puede definir parámetros en su nivel de enumeración, por lo que El código anterior es simplemente imposible de replicar en una enumeración de Rust.
Mi solución a este problema específico fue definir primero todos los códigos de operación como constantes y colocarlos en un vector como tuplas:
pub const BRK: u8 = 0x00;
pub const JSR: u8 = 0x20;
// ...
// opcode hex, opcode name, instruction size
let ops: Vec<(u8, &str, usize)> = vec![
(BRK, "BRK", 1),
(JSR, "JSR", 3),
// ...
];
And then enumerate this vector to create instances of an Opcode structure, and put these in a HashMap, indexed by their opcode value:
struct Opcode {
opcode: u8,
name: &'static str,
size: usize,
}
let mut result: HashMap<u8, Opcode> = HashMap::new();
for op in ops {
result.insert(op.0, Opcode::new(op.0, op.1, op.2));
}
Estoy seguro de que hay varias formas de llegar al mismo resultado, pero todas terminan con una cantidad significativa de texto repetitivo y, dado que en realidad no es posible usar las enumeraciones de Rust aquí, perdemos los beneficios que tienen para ofrecer. Permitir que se creen instancias de enumeraciones de Rust con valores constantes disminuiría significativamente la cantidad de texto estándar y, como resultado, lo haría mucho más legible.
Propiedades
Este fue otro paso atrás desagradable, y todavía me resulta rutinariamente doloroso en Rust tener que escribir captadores y definidores para todos los campos que quiero exponer desde una estructura. Aprendimos esta lección de la manera más difícil con Java, e incluso hoy en 2021, los captadores y definidores siguen vivos y coleando en este lenguaje. Afortunadamente, Kotlin lo hace bien (no es el único, C#, Scala, Groovy... también lo hacen bien). Sabemos que tener propiedades y acceso universal es de gran valor, es decepcionante que no tengamos esta característica en Rust.
Como consecuencia, siempre que publica código que va a ser utilizado por terceros, debe tener mucho cuidado si hace público un campo, porque una vez que los clientes comienzan a hacer referencia a ese campo directamente (lectura o escritura), ya no tiene la lujo de ponerlo detrás de un captador o un definidor, o arruinará a sus interlocutores. Y como resultado, probablemente pecará por exceso de precaución y escribirá manualmente captadores y definidores.
Hoy lo sabemos mejor y espero que Rust adopte propiedades en algún momento en el futuro.
Conclusión
Entonces esta es mi lista. Ninguna de estas características faltantes ha sido un obstáculo para el ascenso meteórico de Rust, por lo que obviamente no son críticas, pero creo que contribuirían a hacer que Rust sea mucho más cómodo y agradable de usar de lo que es hoy. No debería sorprendernos. que Kotlin también podría aprender algunas cosas de Rust, por lo que planeo continuar con una publicación inversa que analizará algunas características de Rust que desearía que Kotlin tuviera.
Un agradecimiento especial a Adam Gordon Bell por revisar esta publicación.
Actualizar:
Discusiones en reddit:
Los comentarios están cerrados.
Source link