Des traits ont été introduits récemment dans Mojo, alors j'ai pensé les essayer. Actuellement, les caractéristiques intégrées incluent CollectionElement , Copyable , Destructable , Intable , Movable , Sized et Stringable (on dirait que le suffixe "-able" fait partie de ces conventions de dénomination !).
Les traits fonctionnent sur les structures. Pour implémenter un trait, vous ajoutez simplement une méthode à une structure qui correspond au trait ; puis passez le nom du trait en paramètre :
@value struct Duck(Quackable): fn quack(self): print("Quack!")
Le décorateur @value
de Mojo insère les méthodes de cycle de vie comme __init__()
, __copyinit__()
et __moveinit__()
dans la structure, simplifiant un peu notre vie car nous n'avons pas besoin de les ajouter nous-mêmes.
Les traits dans Mojo ne prennent pas encore en charge les implémentations par défaut des méthodes, d'où le ...
dans le corps de la méthode Quackable
ci-dessus. Vous pouvez également utiliser pass
, qui aura le même effet qu’en Python.
Malgré le nom différent, l'approche de base des traits dans Mojo me rappelle les interfaces Go. Dans Go, vous définiriez une structure Duck
et implémenterais une interface Quackable
comme celle-ci :
type Quackable interface { quack() }
Et pour créer une structure qui satisfait cette interface :
type Duck struct {} func (d Duck) quack() { fmt.Println("Quack!") }
Comparez-le à une implémentation Mojo :
trait Quackable: fn quack(self): ... @value struct Duck(Quackable): fn quack(self): print("Quack!")
Je pense que la version Mojo est encore plus concise, imbriquant la définition de la méthode quack()
dans le type de structure Duck
. En plus de travailler sur les structures, les traits dans Mojo ne nécessitent pas de mot-clé implements
, tout comme dans Go.
Il est intéressant de noter que la documentation Mojo indique que les implémentations par défaut ne sont pas encore autorisées, ce qui signifie qu'elles pourraient l'être à l'avenir. Cela signifie que Mojo pourrait poursuivre une approche différente de Go, qui se concentre sur les structures satisfaisant une interface et non sur sa mise en œuvre .
Les implémentations de méthodes par défaut ne sont pas nécessaires dans Go, et un effet similaire est obtenu avec l'intégration, car l'implémentation d'interface/trait est simplement un concept qui n'existe pas dans Go. Les choses pourraient être différentes à Mojo.
La valeur des traits et des interfaces est de rendre le code réutilisable. Par exemple, dans Mojo, vous pouvez écrire des fonctions qui acceptent des types de traits…
fn make_it_quack[T: Quackable](could_be_duck: T): could_be_duck.quack()
Et puis transmettez différentes structures qui implémentent le même trait en entrée - tout fonctionnera ! Par exemple, voici une structure Goose
qui implémente Quackable
:
@value struct Goose(Quackable): fn quack(self): print("Honk!")
Et ici, pour appeler make_it_quack
à la fois sur Goose
et Duck
(rappelez-vous, vous avez besoin de la fonction main
de Mojo comme point d'entrée à votre programme) :
def main(): make_it_quack(Duck()) make_it_quack(Goose())
Le résultat de ceci sera
Quack! Honk!
Si j'essaie de transmettre quelque chose qui n'implémente pas le trait Quackable
à la fonction make_it_quack
, disons StealthCow
, le programme ne compilera pas :
@value struct StealthCow(): pass
make_it_quack(StealthCow())
Le message d'erreur ci-dessous aurait pu être plus descriptif ; peut-être que l'équipe Mojo l'améliorera ?
error: invalid call to 'make_it_quack': callee expects 1 input parameter, but 0 were specified
Idem si je supprime la méthode quack
de Goose
; ici, nous obtenons une belle erreur descriptive :
struct 'Goose' does not implement all requirements for 'Quackable' required function 'quack' is not implemented
L'avantage de ce type de vérification de type statique par rapport à l'approche Python pure est que nous pouvons détecter facilement les erreurs et éviter de les transmettre à la production, car le code ne sera tout simplement pas compilé.
Les traits dans Mojo prennent déjà en charge l'héritage, donc notre trait « Quackable » peut étendre un trait « Audible » comme ceci :
trait Audible: fn make_sound(self): ...
trait Quackable(Audible): fn quack(self): ...
Cela signifie qu'une structure Duck
devra implémenter à la fois quack
et make_sound
pour se conformer au trait Quackable
.
Ceci est similaire au concept « d'intégration d'interface » dans Go, où pour créer une nouvelle interface qui hérite d'autres interfaces, vous intégreriez des interfaces parents comme ceci :
type Quackable interface { Audible // includes methods of Audible in Quackable's method set }
Les traits acceptent également les méthodes statiques qui fonctionnent sans créer d'instance de structure :
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()
Vous appelez une méthode statique comme ceci :
def main(): make_it_quack(Duck()) make_it_quack(Goose()) Duck.swim()
Ce qui produira :
Quack! Honk! Swimming
Notez que lors du dernier appel de méthode, aucune instance de Duck n'est créée. C'est ainsi que fonctionnent les méthodes statiques en Python, s'éloignant quelque peu de la programmation orientée objet. Mojo s'appuie sur cette fonctionnalité Python.
Fait intéressant, l'astuce Go avec une interface{}
qui permet de transmettre n'importe quel type et qui était populaire auprès de la communauté Go avant l'introduction de Go Generics, ne fonctionnera pas avec une fonction fn
typée Mojo.
Votre structure doit implémenter au moins une des méthodes de cycle de vie telles que __len__
ou __str__
, ce qui dans ce cas la rendrait conforme à Sized
ou Stringable
, à utiliser avec des fonctions qui acceptent les types de traits.
Ce n'est en fait pas une réelle limitation et cela a beaucoup de sens dans Mojo, puisque, avec Mojo, si vous avez besoin d'un typage dynamique, vous pouvez simplement vous rabattre sur la fonction def
plus flexible, puis appliquer la magie Python habituelle pour travailler avec un inconnu. les types.
Les fonctions Mojo fn
plus strictes fonctionnent également sur des structures génériques utilisant des types comme DynamicVector
; en savoir plus à ce sujet ici .
Une autre limitation que je vois concerne les structures et leurs méthodes plutôt que les traits et constitue un obstacle potentiel à la mise en œuvre d'une architecture propre/à la séparation du code en différentes parties.
Considérons l'un des exemples précédents avec une définition de méthode struct Go vs. Mojo :
type Duck struct {} func (d Duck) quack() { fmt.Println("Quack!") }
@value struct Duck(Quackable): fn quack(self): print("Quack!")
L'exemple Mojo, suivant une syntaxe plus proche de celle de Python, imbrique la définition de la méthode directement dans la structure, tandis que la version Go lui permet de la séparer de la structure elle-même. Dans cette version, si j'ai une structure très longue avec de nombreux types et méthodes, elle sera un peu plus difficile à lire.
Ce n’est cependant pas une différence critique, juste quelque chose dont il faut être conscient.
Les traits Mojo sont déjà très utiles, même si le langage en est à ses débuts. Bien que la philosophie Go soit axée sur la simplicité et tente de réduire les fonctionnalités au minimum, il est probable que nous verrons davantage de fonctionnalités ajoutées aux traits Mojo à l'avenir, les rendant encore plus puissants et permettant des tonnes de cas d'utilisation différents.