paint-brush
Rasgos de Mojo: ¿Cómo se comparan con las interfaces Go?por@a2svior
1,846 lecturas
1,846 lecturas

Rasgos de Mojo: ¿Cómo se comparan con las interfaces Go?

por Valentin Erokhin6m2023/12/26
Read on Terminal Reader

Demasiado Largo; Para Leer

Un tutorial sobre cómo trabajar con Mojo Traits con ejemplos y una comparación de Mojo Traits con las interfaces Go. Limitaciones y advertencias de los rasgos de Mojo.
featured image - Rasgos de Mojo: ¿Cómo se comparan con las interfaces Go?
Valentin Erokhin HackerNoon profile picture
0-item

Los rasgos se introdujeron en Mojo recientemente, así que pensé en probarlos. Actualmente, los rasgos integrados incluyen CollectionElement , Copyable , Destructable , Intable , Movable , Sized y Stringable (¡parece que el sufijo "-able" existe en estas convenciones de nomenclatura!).

Los rasgos funcionan en estructuras. Para implementar un rasgo, simplemente agrega un método a una estructura que coincida con el rasgo; luego pase el nombre del rasgo como parámetro:

 @value struct Duck(Quackable): fn quack(self): print("Quack!")


El decorador @value en Mojo inserta métodos de ciclo de vida como __init__() , __copyinit__() y __moveinit__() en la estructura, simplificando un poco nuestra vida ya que no tenemos que agregarlos nosotros mismos.


Los rasgos en Mojo aún no admiten implementaciones predeterminadas para métodos, por lo tanto, ... en el cuerpo del método Quackable anterior. También puedes usar pass , que tendrá el mismo efecto que en Python.

Rasgos de Mojo versus interfaces Go

A pesar del nombre diferente, el enfoque básico de los rasgos en Mojo me recuerda a las interfaces de Go. En Go, definirías una estructura Duck e implementarías una interfaz Quackable como esta:


 type Quackable interface { quack() }


Y para hacer una estructura que satisfaga esta interfaz:


 type Duck struct {} func (d Duck) quack() { fmt.Println("Quack!") }


Compárelo con una implementación de Mojo:


 trait Quackable: fn quack(self): ... @value struct Duck(Quackable): fn quack(self): print("Quack!")


Creo que la versión Mojo es aún más concisa, ya que anida la definición del método quack() dentro del tipo de estructura Duck . Además de trabajar en estructuras, los rasgos en Mojo no requieren una palabra clave implements , como en Go.


Curiosamente, en los documentos de Mojo se señala que las implementaciones predeterminadas aún no están permitidas, lo que significa que podrían permitirse en el futuro. Esto significa que Mojo podría seguir un enfoque diferente al de Go, que se centra en estructuras que satisfacen una interfaz, no en implementarla .


Las implementaciones de métodos predeterminados no son necesarias en Go, y se logra un efecto similar con la incrustación, ya que la implementación de interfaz/rasgo es simplemente un concepto que no existe en Go. Las cosas podrían ser diferentes en Mojo.

Cómo utilizar los rasgos de Mojo

El valor de los rasgos y las interfaces es hacer que el código sea reutilizable. Por ejemplo, en Mojo, puedes escribir funciones que acepten tipos de rasgos...


 fn make_it_quack[T: Quackable](could_be_duck: T): could_be_duck.quack()


Y luego pase diferentes estructuras que implementen el mismo rasgo como entrada: ¡todo funcionará! Por ejemplo, aquí hay una estructura Goose que implementa Quackable :


 @value struct Goose(Quackable): fn quack(self): print("Honk!")


Y aquí, para llamar make_it_quack tanto en Goose como en Duck (recuerda, necesitas la función main en Mojo como punto de entrada a tu programa):


 def main(): make_it_quack(Duck()) make_it_quack(Goose())


El resultado de esto será


 Quack! Honk!


Errores de rasgos

Si intenté pasar algo que no implementa el rasgo Quackable a la función make_it_quack , digamos StealthCow , el programa no se compilará:


 @value struct StealthCow(): pass


 make_it_quack(StealthCow())


El siguiente mensaje de error podría haber sido más descriptivo; ¿Quizás el equipo de Mojo lo mejore?


 error: invalid call to 'make_it_quack': callee expects 1 input parameter, but 0 were specified


Lo mismo si elimino el método quack de Goose ; Aquí obtenemos un bonito error descriptivo:


 struct 'Goose' does not implement all requirements for 'Quackable' required function 'quack' is not implemented


La ventaja de este tipo de verificación de tipos estáticos sobre el enfoque puro de Python es que podemos detectar errores fácilmente y evitar enviarlos a producción, ya que el código simplemente no se compilará.

Herencia

Los rasgos en Mojo ya admiten herencia, por lo que nuestro rasgo "Quackable" puede extender un rasgo "Audible" de esta manera:


 trait Audible: fn make_sound(self): ...


 trait Quackable(Audible): fn quack(self): ...


Esto significa que una estructura Duck tendrá que implementar tanto quack como make_sound para ajustarse al rasgo Quackable .


Esto es similar al concepto de "incrustación de interfaz" en Go, donde para crear una nueva interfaz que herede de otras interfaces, debería incrustar interfaces principales como esta:


 type Quackable interface { Audible // includes methods of Audible in Quackable's method set }

Métodos estáticos

Los rasgos también aceptan métodos estáticos que funcionan sin crear una instancia de una estructura:


 trait Swims: @staticmethod fn swim(): ... @value struct Duck(Quackable, Swims): fn quack(self): print("Quack!") @staticmethod fn swim(): print("Swimming")


 fn make_it_swim[T: Swims](): T.swim()


Llamas a un método estático como este:


 def main(): make_it_quack(Duck()) make_it_quack(Goose()) Duck.swim()


Que generará:


 Quack! Honk! Swimming


Observe que en la última llamada al método, no se crea una instancia de Duck. Así funcionan los métodos estáticos en Python, alejándose un poco de la programación orientada a objetos. Mojo se basa en esta funcionalidad de Python.

Limitaciones de los rasgos de Mojo en comparación con las interfaces Go

Curiosamente, el truco de Go con una interface{} que permite pasar cualquier tipo y que era popular entre la comunidad de Go antes de que se introdujeran Go Generics, no funcionará con una función fn escrita en Mojo.


Su estructura debe implementar al menos uno de los métodos del ciclo de vida como __len__ o __str__ , que en este caso haría que se ajuste a Sized o Stringable , para usarse con funciones que acepten tipos de rasgos.


En realidad, esto no es una limitación real y tiene mucho sentido en Mojo, ya que, con Mojo, si necesita escritura dinámica, puede recurrir a la función def más flexible y luego aplicar la magia habitual de Python para trabajar con desconocidos. tipos.


Las funciones más estrictas de Mojo fn también funcionan en estructuras genéricas usando tipos como DynamicVector ; Lea mas sobre eso, aqui .


Otra limitación que veo tiene que ver con las estructuras y sus métodos en lugar de con los rasgos y es un obstáculo potencial para implementar una arquitectura limpia/separar el código en diferentes partes.


Considere uno de los ejemplos anteriores con una definición del método de estructura Go vs. Mojo:


 type Duck struct {} func (d Duck) quack() { fmt.Println("Quack!") }


 @value struct Duck(Quackable): fn quack(self): print("Quack!")


El ejemplo de Mojo, siguiendo una sintaxis más parecida a Python, anida la definición del método directamente dentro de la estructura, mientras que la versión Go le permite separarla de la propia estructura. En esta versión, si tengo una estructura muy larga con muchos tipos y métodos, será algo más difícil de leer.


Sin embargo, no es una diferencia crítica, sólo algo a tener en cuenta.


Los rasgos de Mojo ya son bastante útiles, a pesar de que el lenguaje se encuentra en sus inicios. Si bien la filosofía de Go tiene que ver con la simplicidad y trata de mantener las funciones al mínimo, es probable que veamos más funciones agregadas a los rasgos de Mojo en el futuro, haciéndolos aún más poderosos y permitiendo toneladas de casos de uso diferentes.