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.
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.
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!
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á.
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 }
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.
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.